Skip to content

Start here

Getting started

Build the mk compiler and run Makoto code, in the state the project is in today.

GETTING-STARTED.md · 162 lines · 7 min read

This is the practical guide to building the mk compiler and running Makoto code in the state the project is in today. It is honest about what works, what is rough, and what is design-only. For a tour of the language itself, read MAKOTO-A-GUIDED-TOUR.md; for the full design, docs/english/language-design.md.

Makoto has a native compiler (mk) that lowers Makoto to LLVM IR text and hands it to an embedded clang. The core language compiles and runs: a program using structs, enums, match, loops, Result/errors, processes and channels, @mm, generics, comptime, and arbitrary-precision numbers builds to a native binary today.

Two honesty notes up front:

  • This is pre-1.0, closed-beta work. The design docs describe the target; the implementation is behind them in places. The verification layer (@must_consume, @where, session types, @proof) from the tour is designed, not implemented yet.
  • There is a known-bug ledger (BUG-HUNT-INDEX-BY-VEIN.md, ~324 catalogued issues) from an ongoing bug hunt. Many advanced idioms have open bugs. Expect sharp edges off the main path.

To build the compiler:

  • Go 1.26.x (the module declares go 1.26.3). That is the only hard dependency.
  • Linux x86_64. The embedded clang is a static x86_64/musl build, so a build host is x86_64 Linux.

To build and run native x86_64 Makoto programs: nothing else. The compiler vendors its own C toolchain (see “Dependencies” below), so no system LLVM, clang, or libc install is required.

Terminal window
go build -o mk ./cmd/mk

That produces the mk binary. The compiler embeds ~200 MB of compressed toolchain blobs (LLVM, the musl and WASI sysroots, binaryen, wasmtime, qemu) via go:embed, so the resulting binary is large (~225 MB) and the first build pulls those in. On first use mk extracts each piece it needs into a content-addressed cache once (under ~/.cache/makoto/).

Put mk on your PATH, or call it by path.

use io
fn main() {
io.println("hello from makoto")
}

Save it as hi.mko and run it:

Terminal window
mk run hi.mko

mk run compiles and runs in one step (default -O0). To produce a shippable binary instead:

Terminal window
mk build hi.mko # writes ./hi (default -O2 for build)
./hi

A single .mko file builds that module in isolation. A project is a directory with an mk.project manifest and a main.mko (or main.mkoui) entry:

mk.project
package demo
makoto 0.1.0

Then mk build . (or mk run .) builds the enclosing project from its main entry.

Commands:

Command What it does
mk run [file|.|dir] compile and run (default -O0)
mk build [file|.|dir] compile to a native binary (default -O2)
mk check [file|.|dir] typecheck only, no binary
mk test compile and run the project’s @test functions, each in its own process
mk analyze run the memory analyzer and report (no binary)
mk cc [args...] drop-in C compiler using the embedded hermetic toolchain (static musl)
mk c++ [args...] the C++ driver (links the embedded libc++)
mk get <path>[@version] fetch and pin an external dependency via the vendored Git client
mk export <cmake|ninja> emit build-system glue so a C/C++ project can build and link a .mko library
mk <task> run a task declared in mk.project
mk (no args) start the REPL

Note: bare mk --help currently drops into the REPL; get flag help from mk build --help or mk run --help.

Useful build flags (from mk build --help):

Flag Meaning
-o <path> output path for the binary
-O0 / -O2 optimization level (run defaults to -O0, build to -O2)
--release optimized build (-O2), the artifact you ship
--emit-llvm keep the generated .ll next to the binary
--emit=lib build a C-callable static library plus a generated C header
--header=<path> where to write that header (with --emit=lib)
--scheduler=throughput|deterministic work-stealing (default) or BEAM-style rigid time-slicing
--libc=musl|glibc|glibc.2.<minor>|system hermetic static musl (default); dynamic against the vendored glibc, runnable on any glibc at least the targeted release (the newest, or 2.17–2.44 on request; riscv64 from 2.27); or the host’s libc and headers
--sanitize=address|thread|undefined build with sanitizers (a comma-separated list; address and thread exclude each other): the program’s functions, the runtime (which announces its process-stack switches) and the C it links are instrumented. Needs glibc, so without --libc the build links the vendored glibc; not for wasm/JS. address keeps locals on the process stack; thread follows at most 65,536 nested calls per process; undefined is fatal (exit 1)
--target=x86_64|aarch64|riscv64|wasm32 cross-compile target (default x86_64)
--gc=linear|wasmgc GC backing (wasmgc is --target=wasm32 only)
--gc-roots=shadow|stackmaps root-finding: portable shadow stack (default) or precise stackmaps
--gc-collector=marksweep|moving mark-sweep (default) or compacting collector
--autofree=default|conservative|optimized / --no-autofree free-at-last-use optimization (on by default)
-Wmm / --strict-mm / -Whints engage the memory analyzer (advisory / hard errors / hint tier)
--ext=<name> turn on a compiler extension, e.g. --ext=ui
-- everything after is passed to the program (run only)

Dependencies: what is embedded vs what the system provides

Section titled “Dependencies: what is embedded vs what the system provides”

The core design decision: the compiler emits LLVM IR as text and shells out to tools, rather than binding the LLVM C API. All of those tools are embedded, so every build is hermetic.

Embedded in mk (nothing needed from the system) — all static x86_64/musl executables, built from source by internal/compiler/toolchain/rebuild/build-toolchain.sh (versions and licenses: internal/compiler/toolchain/PROVENANCE.txt):

  • LLVM 23 — one multicall binary (clang, clang++, ld.lld, wasm-ld, llvm-ar, llvm-nm, …) plus opt and llc. This is the entire native pipeline: mk hands clang the .ll, clang compiles and lld links; opt runs the --gc-roots=stackmaps statepoint rewrite, llc validates IR in the test suite.
  • Linux sysroots for x86_64 (the default hermetic static link), aarch64 and riscv64 — each with musl 1.2.6, compiler-rt and libc++, so C and C++ build hermetically for every target.
  • glibc for x86_64, aarch64 and riscv64 — glibc 2.44’s headers, start files and libc_nonshared.a, the ABI lists of every release from 2.17 (riscv64: 2.27) to 2.44, libc++, and compiler-rt with the sanitizers — for --libc=glibc[.2.<minor>]. mk writes the link stubs of the targeted release from those lists; glibc itself never ships.
  • WASI sysroot (wasi-sdk 34: wasip1, wasip1-threads, wasip3 + cooperative threads, libc++ with and without exceptions) + wasm32 compiler-rt + wasm-component-ld — for --target=wasm32.
  • binaryen (wasm-opt, wasm-merge, wasm-as) and wasmtime (49, and 46 for wasi-threads) — for building and running wasm.
  • qemu-user (qemu-aarch64, qemu-riscv64) — mk run --target=aarch64|riscv64 runs the cross-compiled binary under it.
  • the C runtime (internal/compiler/runtime/src/*.c/.h/.S) — go:embedded into mk (rebuild mk after editing runtime source).

NOT embedded — needed only for specific modes:

Tool When it’s needed If missing
node only --target=js (the UI JS transpile), to run the output JS target only
cmake / ninja only mk export cmake|ninja interop export only

The ThreadSanitizer probes of the runtime (internal/compiler/runtime), like every other test, use only the embedded toolchain: they link against the vendored glibc (TSan supports only a dynamically linked glibc process) and run on the machine’s glibc, so they skip on a machine without one.

Overrides: MAKOTO_CLANG=<abs-path> forces a specific clang; MAKOTO_CLANG=system forces the host clang on PATH (used by the differential tests, and as an escape hatch if the embedded blob is stripped).

Solid enough to build real programs (the bug hunt saturated these veins):

  • Values, let/var/const, Optional (no null), the numeric tower up to BigInt/Decimal/BigFloat.
  • Functions, match (all three forms), unified loop, ternary, defer.
  • decl structs/enums/interfaces, pattern matching, @embeds.
  • Result + error{...}, catch / match |e| trailers.
  • Processes, typed channels, the subjectless match (select), @supervisor and the restart strategies.
  • Memory: colorless GC default, @mm(arena|none|c), @transfer/@promote, the memory analyzer (mk analyze).
  • Generics ([T], value-params), comptime, reflect(T), auto-derived serialize/hash.
  • C FFI (@cimport), inline @asm, cross-compile targets, the wasm and (UI) JS backends.

Rough or open (see BUG-HUNT-INDEX-BY-VEIN.md for the catalogued list):

  • Still actively yielding bugs: @cimport/FFI-ABI edges, interfaces+generics dispatch corners, @repr/layout with non-scalar fields, defer+generator interactions, the REPL, some @asm and unsafe/union cases, and a few compiler hangs on pathological input.
  • Collections construction has known parse bugs (e.g. List[T].new() turbofish); prefer slice literals ([]T{...}) on the happy path.
  • Not implemented yet: the whole verification layer (@must_consume, @consume_once, @where, @protocol/session types, @proof/dependent proofs, @spec model checking). It is designed (docs/english/verification.md), not built.
  • The showcase file cmd/mk/hello.mko currently trips a parse error with the checked-in mk; the minimal hello above is the reliable starting point.

Ground truth for behavior is the design docs plus hand-computed values and cross-target consistency, not the Phase-1 interpreter/oracle (which over-simulates and is no longer authoritative).

  • MAKOTO-A-GUIDED-TOUR.md — the language, feature by feature, in one running example.
  • docs/english/language-design.md — the normative design (the north star).
  • docs/english/verification.md — the verification spectrum (designed, not yet built).
  • BUG-HUNT-INDEX-BY-VEIN.md — the current known-issue map, grouped by subsystem.
  • mk test — run the @test suite to see the compiler exercised on a large corpus.