Memory
The redefinition: memory is colorless, and the manager travels with the object, not with the context. And there is no borrow checker.
The idea
Section titled “The idea”Every systems language has to answer two questions: where the bytes come from and who
frees them. Rust answers with ownership + lifetimes + borrow checker (and &T/&mut T/*const T/*mut T
as distinct types). Go answers with a GC and that is it. Zig answers with allocators passed as a
parameter. Each answer colors the code in a way.
How Makoto redefines it
Section titled “How Makoto redefines it”Every value allocated on the heap carries a pointer to its own MemoryManager (@mm). When
the object grows or reallocates, it uses its @mm, not that of the function manipulating it. The function
mutates a Buffer without knowing or caring whether it is arena-backed, GC or manual:
@mm(gc)fn append_log(buf: mut Buffer, line: string) { buf.push(line) // if push reallocates, it uses buf's @mm (the arena), NOT the function's @mm(gc)}var buf: Buffer @mm(arena) = Buffer.new()append_log(mut buf, "error") // mutates in-place in the arena; the GC never touches itThis is “colorless” in action: a function’s @mm(gc) governs only the new allocations it
originates, never the strategy of the objects that arrive at it. The strategy travels with the data.
Underneath, two orthogonal layers: MemoryManager (the strategy, gc/arena/none/c,
“how I manage”) over MemorySource (where the raw pages come from, mmap/VirtualAlloc/
WASM’s memory.grow/bare-metal fixed buffer, “where the RAM comes from”). Any combination works:
GC-over-WASM, arena-over-native. It is the same decoupling as colorless, descended one level.
The reasoning
Section titled “The reasoning”Why can the borrow checker be abandoned? Because its reason disappears inside a process. The borrow checker exists to prevent concurrent mutable aliasing, two paths mutating the same data at the same time. But in Makoto the process is the unit of concurrency, with a single line of execution; the scheduler only suspends it at explicit points (IO, send/recv), and no other process touches its heap (shared-nothing, ch. 04). Without concurrent aliasing, the main justification for the borrow checker vanishes. Only two dangers remain, and each one has a local defense:
- Aliasing bug (mutating a struct while holding another reference to it): solved by
value semantics by default +
mutfor mutation.mutis a passing mode, not a type (like Swift’sinout): exclusive, write-back, does not escape (it dies at the end of the call). The check is purely local (“is this place alreadymuthere?”), without lifetimes. - Use-after-free: it only exists under
@mm(none)/arena, explicitlyunsafeterritory. Under the GC default, a live*Tpointer is a reference the trace sees, so the target stays alive, safe by construction.
The key that closes the model: a pointer is born from persistent identity, whether a heap allocation (which
already carries its @mm), a struct field or FFI. You cannot take *T from a local stack lvalue;
to mutate an int on the stack, there is only mut. Hence stack dangling is impossible: the & of
a local does not give you a storable pointer.
Why not the alternatives
Section titled “Why not the alternatives”- Lifetimes / borrow checker (Rust).
&mut Tpacks three things at the same time (can alias, is exclusive, has a lifetime), and it is that which forces the borrow checker to exist. Makoto unpacks the bundle:mutis the “exclusive + write-back” without the “can alias”;*Tis the “can alias” without the rest. Separated, neither of the two requires global tracking. - Nullable pointers by default (C/Go). They would make
Optional[*T]redundant and reopen the null ambiguity. In Makoto the pointer is always valid; absence isOptional[*T], explicit. - Allocator as a parameter (Zig). It breaks colorless: the function would come to know which memory it uses,
and passing an arena buffer to a GC function would become a mismatch. With
@mmtraveling in the object, this is safe and invisible. - Smart pointers /
Cell/RefCell(Rust). They would embed in the core a machinery that contradicts “do not embed what is not used”. The@mminterfaces cover the same ground more cleanly. - An explicit
@copymarker. Discarded: a plain copy is the unmarked default, and marking the common case would violate opt-in (you would pay syntax for what happens most). What gets a mark is the opposite: moving (@transfer, invalidates the source) and copying-to-another-MM (@promote(mm), cuts the cord with the source arena).
There was a real, named fear: that mut would die for the pointer shortcut (the way ? became a
universal escape-hatch in Rust/Zig). The design answer was not to make mut more attractive. It was
to make a pointer not a substitute: it solves a different problem (persistent reference), is born
only from heap-identity (you cannot “prefer” it in the common case), and charges an explicit deref (*p) on each
access, so whoever took a pointer “to avoid typing mut” types * all day long.
The concrete pain
Section titled “The concrete pain”The Rust programmer who writes a simple mutation function and has to annotate lifetimes that have
nothing to do with the problem. The Zig one who has to thread allocator: *Allocator through ten layers of
calls. The Go one who has no choice of strategy when the GC does not serve. Each pain is a color of
memory that leaked to where it should not have.
The mental model
Section titled “The mental model”The object carries its memory strategy. You mutate the value; the runtime uses its
@mmfor any growth. No color, no lifetime.
The accepted friction
Section titled “The accepted friction”Under @mm(none)/arena, use-after-free becomes your responsibility again, and the compiler does not cover
it (it is the explicit price of descending to the metal, and it stays in an unsafe island). Non-nullable pointers force
Optional[*T] where C would have a simple nullable. And the mandatory *p deref is noise in
legitimate pointer code, accepted on purpose, because it is exactly what defeats laziness.
Next: 03 · Async and IO