The story so far
Where Makoto came from, what the napkin sketch turned into, and the "no" list.
誠: sincerity, truth. A language that doesn’t hide what anything costs.
Origin
Section titled “Origin”This is a programming language designed after I had an epiphany in my kitchen. For the last 5 years I wanted to create a programming language, but I always struggled with the reason and the “identity.” I’ve already built parsers, lexers, and tokenizers for toy languages, but I always wanted something original and genuine. Then, like a lightning strike, I was blessed with a vision that defies the common concept of systems. Without sounding schizophrenic, let me get this right:
The vision
Section titled “The vision”What if we had:
- A concurrent, let-it-crash system, like Erlang and Gleam (the BEAM architecture)
- A powerful type system, like Rust, but without lifetimes
- Channels, like Golang
- Colorless interfaces and comptime, like Zig
- Function overloading
- The simplicity of Odin
- A few sprinkles of Haskell
Philosophy first, syntax second
Section titled “Philosophy first, syntax second”This language was born first as a philosophy and a concept. Only after everything was defined did the syntax naturally emerge.
Defining “system”
Section titled “Defining “system””We usually say “systems programming language” without ever defining what a system is. A system can be the OS, an application, AI, network, or biology. Really, a system can be ANYTHING, as long as it has parts that work together. Bro, a car is a system, for Christ’s sake. So let’s define what this systems programming language is:
Makoto is a systems programming language in the sense that it understands its underlying systems as separate concepts. It builds on top of Zig’s async I/O concept to create a truly colorless programming language with multiple memory management strategies, a well-defined core, agnostic programming, and much more.
So what did the napkin sketch turn into?
Section titled “So what did the napkin sketch turn into?”Turns out most of the wishlist survived contact with reality, just not always in the shape you’d expect. A few things got sharper along the way.
“Function overloading” became dispatch-by-type: match T on a closed whitelist, or a named set of functions. Same itch, scratched without dragging in the C++ overload-resolution rabbit hole.
“Sprinkles of Haskell” turned into HKT, but only arity 1 and 2 (C[_], C[_,_]), declared explicitly and never inferred. That’s enough to write map, filter, and collect once for any container. It’s not enough to summon Traverse and ruin everyone’s afternoon.
“Colorless, like Zig” ended up being the spine of the whole thing, not just the I/O layer. Memory managers, async, even FFI restrictions are colorless: the object carries its own allocator, and the function using it doesn’t care.
The BEAM half isn’t just channels and crashing. Supervision trees fall out of how your spawns are nested in the code, instead of living in a separate behaviour file you have to keep in sync.
A taste of it
Section titled “A taste of it”The simplest.
Section titled “The simplest.”use io.{println}
fn main() { println("Hello, Makoto!")}The complex.
Section titled “The complex.”this is for exemple, how you could implement serialization and deserialization, it show some of the more complex syntax
use compiler.{reflect, fail}use mem.{to_bytes, from_bytes}
fn serialize[T](v: T, out: mut Buffer) -> error{} { comptime match reflect(T).kind { Struct(s) => comptime loop f in s.fields { serialize(f.get(v), out) } Tuple(parts) => comptime loop p in parts { serialize(p.get(v), out) } Enum(e) => { put_varint(out, e.tag(v)) comptime loop variant in e.variants { comptime if variant.payload.is_present() { p := variant.get(v) if p.is_present() { serialize(p.or_panic(), out) } } } } Slice(_) => { put_varint(out, v.len()); loop x in v { serialize(x, out) } } Array(_, _) => loop x in v { serialize(x, out) } Optional(_) => { out.write_byte(v.is_present() as byte); if v.is_present() { serialize(v.or_panic(), out) } } Int(_) | Float(_) | Bool => out.write(v.to_bytes()) Ptr(_) | ManyPtr(_) | Channel(_) | Function(_) | RawPtr => fail("não serializa — endereço/identidade não cruzam a fronteira") _ => fail("kind não serializável") }}
fn decode[T](src: *Reader) -> Result[T, error{Malformed, Truncated}] { comptime match reflect(T).kind { Struct(_) => Ok(reflect(T).construct(fn(f) => decode[f.type](src))) Tuple(_) => Ok(reflect(T).construct(fn(p) => decode[p.type](src))) Array(elem, _) => Ok(reflect(T).construct(fn(p) => decode[p.type](src))) Enum(e) => { tag := read_varint(src) catch |e| { return Err(e) } comptime loop variant in e.variants { if variant.tag == tag { comptime if variant.payload.is_present() { p := decode[variant.payload.or_panic()](src) catch |er| { return Err(er) } return Ok(variant.construct(p)) } else { return Ok(variant.construct()) } } } return Err(error.Malformed) } Slice(elem) => { n := read_varint(src) catch |e| { return Err(e) } var xs := List[elem].new() loop _ in 0..n { xs.push(decode[elem](src) catch |e| { return Err(e) }) } return Ok(xs) } Optional(inner) => { marker := src.read_byte() catch |e| { return Err(e) } if marker == 0 { return Ok(none) } x := decode[inner](src) catch |e| { return Err(e) } return Ok(x) } Int(_) | Float(_) | Bool => T.from_bytes(src) _ => fail("kind não desserializável") }}
fn read_varint(src: *Reader) -> Result[u64, error{Truncated}] { var result: u64 = 0 var shift := 0 loop { b := src.read_byte() catch |e| { return Err(e) } result = result | (((b & 0x7f) as u64) << shift) if (b & 0x80) == 0 { break } shift += 7 } return Ok(result)}Errors are values, no exceptions, no ? operator hiding what happens:
fn parse_port(s: string) -> Result[u16, error{Empty, NotNumber, OutOfRange}] { if s == "" { return Err(error.Empty) } // ...}
port := parse_port(input) catch |e| { return default_port() }Concurrency is processes + channels, supervision is just structure:
spawn worker(data) catch |e| { log("worker died: {{e}}")}
reply := server <-> request timeout(5s) // sync request-reply, deadline mandatoryMemory strategy travels with the object, not with the function touching it:
@mm(gc)fn append_log(buf: mut Buffer, line: string) { buf.push(line) // if this reallocates, it uses buf's OWN mm. Could be an arena. We don't care.}One keyword, three shapes: the body tells you which.
decl Point { x: int; y: int } // fields → structdecl Shape { Circle(int); Square } // variants → enumdecl Drawable { fn draw() } // signatures → interfaceThe “no” list
Section titled “The “no” list”The personality of this language shows up as much in what it refuses as in what it adds:
- No
null. Absence isOptional[T], written into the type, always. - No
unit/void. A function that returns nothing just doesn’t write->. There’s no fake value standing in for “nothing.” - No function coloring. No
async/awaitinfecting every signature up the call stack. The process suspends. The function doesn’t know or care. - No borrow checker, no lifetimes. Safety inside a process comes from there being only one execution line per process, not from convincing a checker your pointers behave.
- No inheritance. Composition only, and even that (
@embeds) gets announced instead of inferred from an anonymous field the way Go does it. - No silent zero-values. Every binding is born with a real, explicit right-hand side.
Where it’s at right now
Section titled “Where it’s at right now”We are basically on the bootstrap phase, writing the bare frontend and runtime to see if this works.
We are using Golang for this because it gives us a lot of things for free to validate the Proof of Concept of this language specially on the concurrent side of things.
The design is done: the language spec, the formal grammar, the stdlib shape, the rationale behind every major call, and a full tutorial. None of it compiles yet. That’s next.
How things will go on
Section titled “How things will go on”What I wanted for this github repo was simple, the main branch will hold the stable upstream, and I wanted that the FIRST EVER COMMIT after the repo creation was a full blown working interpreter and compiler. yes, interpreter so the FFI and MemoryManagement is kinda “simulated” in the interpreter, but the compiler validates them. This way I have a cornerstone to build the self-host against and always a “working point of origin” if things and shit ever hits the fan.
so where do the development will be placed? after the intepreter/compiler commit, I’ll made 3 branches from main:
- dev -> its the actual development, it’s not garanteed that will even compile/work
- canary -> here things do compile it’s like the release candidate/evaluation phase, it works but we are still working/patching in dev.
- golang -> this is a special one, is not like dev and canary that are related to the upstream compiler, this will host the full blown compiler/interpreter written in Golang (a.k.a phase 2), this branch will serve some porpouses like experimenting, teaching/reference and integration (who knows? some day I turn this into a FFI/iterop between Makoto and Golang?)
and yes I said phase 2, here are the 4 main phases of the compiler:
- Phase 1, here we will built the working Golang interpreter
- Phase 2, we will evolve the interpreter into the Golang LLVM backed compiler (the Golang branch will be born after this is done)
- Phase 3, we will swap our frontend from Golang to Makoto, creating the self-hosted compiler
- Phase 4, this one does not has a date yet, but its to swap the LLVM backend with our own selfhosted backend.
Between Phases 2-4 we might start working on the Libraries and Toolchain of the language (and maybe extensions) and maybe we might start implementing esoteric backends, like 16-bit machine code assembly emittion alongside LLVM and
Will this take forever? maybe.
Will I need help? absolutely.
Do I have help? currently, no.
wish me lucky, cuz the notion? I’ve already lost it far in the past.
I’m just a brazillian dude not the xiaoxinpiao that lives in xanghai and can re-write matter in C just to prove a point. But what I am, is an arch-linux user that watches anime; and we are equally terrifying cuz I’ll re-write the internet in assembly just to watch my steins;gate without stuttering.
On the name
Section titled “On the name”Makoto (誠) means sincerity, truth, and the language takes that literally: no unwrap() pretending it can’t panic, no
name that hides a footgun, no decorator that quietly does more than it says. If a function can crash, the name says so.
The design doc even has a whole section just for backing that up, and it also starts with an M, and idk why all my
projects start with M.
The rest of the library
Section titled “The rest of the library”This README is the pitch. Everything it’s a pitch for lives here:
| File | What’s in it |
|---|---|
language-design.md |
The full spec, types, memory, concurrency, errors, FFI, all of it |
language-grammar.md |
The formal grammar, token by token, synced with the spec |
rationale.md |
The why behind every big call, alternatives considered, costs accepted |
book.md |
“Makoto by Example”, the progressive tutorial |
official_libraries.md |
The tier-1 libs: http, regex, containers |
extensions.md |
Deferred stdlib additions: compress, crypto.pq, linalg, intl, etc. |
docs/tooling.md |
The mk CLI, package manager mechanics, build order |
License, contributing and AI Policies
Section titled “License, contributing and AI Policies”We use the Apache 2.0 License (IDK, I was going with the MIT one, because its starts with M, but some ref languages use apache, so it this is what I choose), we don’t have a CONTRIBUTING.md file yet, I’ll need to think carefully about it, but we do accept help and contributions, just flag a issue and I’ll be very happy to talk with you.
Because I don’t have that many friends to talk about, I used LLMs as the “counterparts/opositions” while designing it, the models pointed flaws, problems and even contradictions in the early designs, it tool like 93 iterations of the design doc to get it correctly, so how we stand in this era of AI? you can use LLMs to help you understand things, rules, and general guidance on contributions (more like a reasoning buddy), but the code must be 100% yours, if you ship LLM generated code, it’s YOUR responsability, and every contribution will need the core-team approval (as of now, its just me), and I’m a very persistent in what I stand for, so I’ll read your 8231840983508153 lines pull request, it might take a moment, but I’ll read and take notes on everything and point out in the PR chat, so please, if you don’t want the project frozen for weeks or want your pull request approved more quickly, don’t ask for a bazillion lines PR bro, we all have the same 24 hours man.
also I accept brazillian portuguese contributions, and you’ll find both, portuguese and english versions of documents in this repository.
Cheers, - Mirai.