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.
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.
// 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) }
...
}The whole reserved set. Everything else is imported, even print.
x86_64, aarch64, riscv64 and wasm32, from one mk with its own toolchain inside.
No async, no await. The process suspends; the function never knows.
gc, arena, none and c, chosen per process or per object.
From type-level down to your signature. Nothing falls off.
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.
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.
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.
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.
Global scope holds only the inevitable. Nothing is too fundamental to import: print lives in io, panic in runtime.
match commutes structure, values and channels. One loop for for and while. One decl for struct, enum and interface.
assert checks the safety, assume trusts it. Two intents get two words, never one word that sometimes does both.
Four ideas that usually live in four different languages, in one language that never charges you for the ones you skip.
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.
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.
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: interfaceThere is no Any: "any type" is a capability bound you can read.
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.
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.
@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
}@mm(gc)@mm(arena)@mm(none)Ownership moves across a process boundary by @transfer. Nothing is shared: there is no memory two processes can both touch.
Any process can be given a memory ceiling of its own.
Not an error that every single call has to handle.
Strategies a program never uses never ship in its binary.
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.
An obligation that cannot be met at one rung falls to the next one down. It never falls off the ladder.
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
}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]
}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
}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 answerThe 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
}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"
}@must_consumeIndex[8] with 3@where(i64 >= 0)@requires(i < len)analyzer hintassume "caller bounds i"| Family | You write | Guarantees | Home | Usually lands on |
|---|---|---|---|---|
| Ownership | @must_consume / @consume_once | Used exactly once, or at most once | core | rung 0 |
| Refinement | @where(pred) on a type | A value satisfies a predicate | core | rung 1, 2 or 3 |
| Fences | @pure / @total | No effects; it terminates | core | rung 0 |
| Sessions | @protocol, @sends / @receives, dual | A protocol is followed, no deadlock | core | rung 0 |
| Dependent, practical | value-params + @where | Lengths, bounds, units | core, today | rung 0 or 1 |
| Dependent, proof | @proof + D1 to D5 | Full functional correctness | core, opt-in | rung 0 |
| Model checking | @spec + spec.* | Global safety and liveness | extension | rung 2 |
| Tactics | @by { ... } | Proof ergonomics | core, deferred | later |
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.
Interpret
A Go interpreter that proved the design holds up.
Compile
mk lowers Makoto to LLVM IR and ships native binaries. Closed beta.
Self-host
The frontend rewritten in Makoto, compiling itself.
Own the backend
A native code generator in place of LLVM. No date yet.
$ go build -o mk ./cmd/mk
$ mk run hi.mko
hello from makoto
$ mk build hi.mko && ./hi
hello from makoto
The only hard dependency to build mk.
The embedded clang is a static x86_64 musl build.
clang, musl, wasi, binaryen and wasmtime ship inside mk, about 100 MB of toolchain.
About 324 issues are catalogued from an ongoing bug hunt. Expect sharp edges off the main path.
The design document went through 93 iterations. Around it: the formal grammar, the reasoning behind every call, the libraries, and a tutorial. Reading times are computed from the files.