Pular para o conteúdo

Start here

The story so far

Where Makoto came from, what the napkin sketch turned into, and the "no" list.

README.md

Este conteúdo não está disponível em sua língua ainda.

誠: sincerity, truth. A language that doesn’t hide what anything costs.

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:

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

This language was born first as a philosophy and a concept. Only after everything was defined did the syntax naturally emerge.

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.

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.

use io.{println}
fn main() {
println("Hello, Makoto!")
}

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 mandatory

Memory 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 → struct
decl Shape { Circle(int); Square } // variants → enum
decl Drawable { fn draw() } // signatures → interface

The personality of this language shows up as much in what it refuses as in what it adds:

  • No null. Absence is Optional[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/await infecting 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.

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.

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:

  1. Phase 1, here we will built the working Golang interpreter
  2. Phase 2, we will evolve the interpreter into the Golang LLVM backed compiler (the Golang branch will be born after this is done)
  3. Phase 3, we will swap our frontend from Golang to Makoto, creating the self-hosted compiler
  4. 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.

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.

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

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.