A systems programming languagePre-1.0 · closed beta
EN / PTGitHub
Established MMXXVIConsistent, not simple. You only pay for what you summon.Spec · 72,796 words

The language

A systems language
that tells the truth.

Erlang's let-it-crash processes, Go's channels, Zig's comptime and Rust's types, without the lifetimes. Makoto combines four bets that usually live apart, and never charges you for the ones you skip.

sensors.mko5 subsystems · 0 leaks
// execution: no async, no await; the process suspends
src := fs.open("sensors.csv") catch |e| { return e }

// types: one keyword, the body decides the shape
decl Reading { sensor: string; value: f64 }
decl Scale   { Celsius; Kelvin }

// memory: the strategy travels with the object
@mm(arena)
fn summarize(rs: []Reading) -> Summary { ... }

// concurrency: processes, channels, supervision
spawn ingest(src) catch |e| {
    log("ingest died: {{e}}")
}
reply := store <-> batch timeout(5s)

// errors: values in the signature, never thrown
alias ParseError = error{Empty, Malformed}
fn parse(s: string) -> Result[Reading, ParseError] {
    if s == "" { return Err(error.Empty) }
    ...
}
Fig. 1. One file, five subsystems. Each one keeps its color to itself.
32Keywords

The whole reserved set. Everything else is imported, even print.

4Compile targets

x86_64, aarch64, riscv64 and wasm32, from one mk with its own toolchain inside.

0Function colors

No async, no await. The process suspends; the function never knows.

4Memory strategies

gc, arena, none and c, chosen per process or per object.

6Guarantee rungs

From type-level down to your signature. Nothing falls off.

IPhilosophyRationale, essay 00 · Why Makoto

Rust leaks. Go fuses.
Makoto separates.

Rust

The realms bleed

async does not stay in execution: it colors the type system and escapes into every signature. Lifetimes infect functions that never touch the problem they solve. You spend your time undoing what leaked.

Go

There is one realm

No separate subsystems, only one system. It is simple while it fits, but growing it means touching everything, because there is no isolated realm to grow in. Generics arrived years later, and they hurt.

Makoto

Realms with neutral borders

Isolated subsystems with neutral boundaries. A realm grows without touching the others, the core stays sacred, and a future realm can arrive without rewriting the ones that already exist.

The opt-in test

You only pay for what you summon.

A concept is expensive not because of its internal complexity, but when people who never use it still have to understand it. unsafe is cheap because it lives on small islands. Lifetimes are expensive because they leak into every signature, so even trivial code pays.

Rationale, essay 00 · Why Makoto
  • Non-poisoning

    Global scope holds only the inevitable. Nothing is too fundamental to import: print lives in io, panic in runtime.

  • Unification by concept

    match commutes structure, values and channels. One loop for for and while. One decl for struct, enum and interface.

  • Explicit by marking

    assert checks the safety, assume trusts it. Two intents get two words, never one word that sometimes does both.

IIThe four betsGuided tour §0 · Design doc §3, §4, §9, §10

When Go isn't enough
and Rust is too much.

Four ideas that usually live in four different languages, in one language that never charges you for the ones you skip.

From Erlang

Let it crash.

Every process has a private heap and dies alone. Supervision is not a separate behaviour file: the way your spawns nest is the tree, and catch is the monitor.

@supervisor(strategy: one_for_one)
spawn {
    spawn ingest(src) catch |e| { log("ingest died: {{e}}") }
    spawn flush(out)
}

Two schedulers, one API: deterministic for real time, work-stealing by default.

Design doc §3, §8Read
From Go

Talk over typed channels.

spawn starts a cheap process and typed channels carry the data. A match without a subject is a select, and a request-reply always writes down its deadline.

match {
    data := -> metrics => ingest(data)
    cmd  := -> control => apply(cmd)
} timeout(1s) { flush() }

reply := server <-> request timeout(5s)

No Future, no await, no context.Context. Cancelling is a timeout or a process death.

Design doc §4Read
From Rust

Types, without the lifetimes.

Sum types, generics, exhaustive match and implicit interfaces. Safety inside a process comes from its single line of execution, not from a borrow checker.

decl Point    { x: int; y: int }        // fields: struct
decl Shape    { Circle(int); Square }   // variants: enum
decl Drawable { fn draw() }             // signatures: interface

There is no Any: "any type" is a capability bound you can read.

Design doc §2, §9Read
From Zig

Comptime, with no magic.

Types are compile-time values and reflect(T) is a library call. Serializers, validators and dispatch are written once, in Makoto, and cost nothing at runtime.

comptime match reflect(T).kind {
    Struct(s) => comptime loop f in s.fields {
        serialize(f.get(v), out)
    }
    _ => fail("kind not serializable")
}

A literal regex compiles at comptime: a bad pattern is a build error, not a runtime Err.

Design doc §10Read
IIIMemoryDesign doc §5 · Rationale, essay 02

The object carries
its own strategy.

Colorless memory: the allocator travels with the value, not with the function touching it. A function that appends to a buffer never needs to know whether the buffer lives in an arena or under the GC.

  • @mm(gc)

    Garbage collector. Tracks live references automatically, and is the default when nothing is said.

  • @mm(arena)

    Bulk release. Everything goes at once when the arena is destroyed.

  • @mm(none)

    Manual, like C: explicit alloc and free, and you own the consequences.

  • @mm(c)

    Memory that came from C through FFI, released by C's own deallocator.

pipeline.mko
@mm(arena)
spawn worker(data)              // everything worker allocates: arena

buf: HugeBuffer @mm(arena) = ...  // only this object

fn append_log(buf: mut Buffer, line: string) {
    buf.push(line)              // grows with buf's own manager
}
process A@mm(gc)private heap
process B@mm(arena)private heap
process C@mm(none)private heap

Ownership moves across a process boundary by @transfer. Nothing is shared: there is no memory two processes can both touch.

Fig. 2. Three processes, three strategies, one program. Each heap dies with its process.
Per-process limits

Any process can be given a memory ceiling of its own.

OOM is a process event

Not an error that every single call has to handle.

Dead-MM elimination

Strategies a program never uses never ship in its binary.

IVVerificationVerification guide · Design doc §21

Guarantees fall down a ladder. Never off it.

Every guarantee is an obligation on a predicate, discharged as high on the ladder as it will go. A failed proof becomes a check. An unaffordable check becomes your signature.

The one rule

An obligation that cannot be met at one rung falls to the next one down. It never falls off the ladder.

Designed, not yet implemented · Read the guide

Rung 0 · Type-level

The bad state cannot be written. The wrong program does not compile, so nothing is left to check at runtime.

decl Transaction @must_consume { conn: *Connection }

fn handle(t: Transaction) {
    if ok { commit(t) }
    // ERROR: on the else path, t is never consumed
}

Discharged by the type checker · no runtime cost

Rung 1 · Comptime fold

A literal decides it. The compiler computes the answer while compiling and emits nothing.

alias Index[n: usize] = usize @where(usize < n)

fn third(xs: [8]int) -> int {
    let i: Index[8] = 3     // 3 < 8, decided at comptime
    return xs[i]
}

Discharged at comptime · no runtime cost

Rung 2 · Summoned proof

Dynamic arithmetic. You summon the SMT extension, and it proves the predicate holds on every path.

alias Balance = i64 @where(i64 >= 0)

fn withdraw(b: Balance, amount: Balance) -> Balance {
    if amount > b { return b }
    return b - amount       // proved: b - amount >= 0 here
}

Discharged by an extension · no runtime cost

Rung 3 · Runtime check

Nobody could prove it statically, so it does not vanish: it becomes a check that traps on violation.

@requires(i < m.rows && j < m.cols)
fn (m: Matrix[T]) at(i: usize, j: usize) -> T

// a caller that cannot prove the bounds
// gets a trap instead of a wrong answer

Discharged at runtime · one check, in the open

Rung 4 · Advisory

The memory analyzer warns or hints. It never blocks the build and inserts no cost.

// mk analyze, or -Wmm on any build
fn checksum(data: []byte) -> u32 {
    tmp := mm.alloc(data.len())
    // hint: this pointer never escapes,
    //       it could be @consume
}

Advisory · a warning, no inserted cost

Rung 5 · Human

The last rung is you. You assert it in writing, at the operation, and the reason stays greppable.

@requires(i < buf.len)
fn read(buf: Buffer, i: usize) -> byte {
    return unsafe raw_read(buf.ptr + i)
        assume "Buffer invariant + @requires bounds"
}

Discharged by you · signed, never silent

  1. 0Type-levelThe bad state cannot be written.@must_consume
  2. 1Comptime foldThe compiler computes the answer.Index[8] with 3
  3. 2Summoned proofAn extension proves it with SMT.@where(i64 >= 0)
  4. 3Runtime checkA trap on violation.@requires(i < len)
  5. 4AdvisoryA warning, with no inserted cost.analyzer hint
  6. 5HumanYou assert it, in writing.assume "caller bounds i"

The whole spectrum, on one page

Verification guide §9
FamilyYou writeGuaranteesHomeUsually lands on
Ownership@must_consume / @consume_onceUsed exactly once, or at most oncecorerung 0
Refinement@where(pred) on a typeA value satisfies a predicatecorerung 1, 2 or 3
Fences@pure / @totalNo effects; it terminatescorerung 0
Sessions@protocol, @sends / @receives, dualA protocol is followed, no deadlockcorerung 0
Dependent, practicalvalue-params + @whereLengths, bounds, unitscore, todayrung 0 or 1
Dependent, proof@proof + D1 to D5Full functional correctnesscore, opt-inrung 0
Model checking@spec + spec.*Global safety and livenessextensionrung 2
Tactics@by { ... }Proof ergonomicscore, deferredlater
VStatusGetting started · README

Pre-1.0, and
honest about it.

The native compiler builds real programs today: structs, enums, match, processes and channels, @mm, generics, comptime and arbitrary-precision numbers. The verification layer is designed, not implemented.

  1. InterpretDone

    A Go interpreter that proved the design holds up.

  2. CompileNow

    mk lowers Makoto to LLVM IR and ships native binaries. Closed beta.

  3. Own the backendSomeday

    A native code generator in place of LLVM. No date yet.

Linux x86_64macOS · not yetWindows · not yet
$ go build -o mk ./cmd/mk
$ mk run hi.mko
hello from makoto
$ mk build hi.mko && ./hi
hello from makoto
  • Go 1.26.x

    The only hard dependency to build mk.

  • Linux x86_64

    The embedded clang is a static x86_64 musl build.

  • Nothing else

    clang, musl, wasi, binaryen and wasmtime ship inside mk, about 100 MB of toolchain.

The bug ledger

About 324 issues are catalogued from an ongoing bug hunt. Expect sharp edges off the main path.