Skip to content

The book · 15

Tooling in practice

book.md · 84 lines · 2 min read

Goal: get to know the mk CLI day to day, the runtime tools (observer/profiler/ debugger), and how the package manager resolves versions.

The foundational decision: the compiler is not a batch that parses, checks, and dies. It is an incremental library of queries (the rust-analyzer model). Almost every tool is a consumer of the understanding the compiler has of the code (the LSP asks “what is the type of X?”, the linter requests semantic info, the doc gen the signatures). That is why there is a single binary, no separate gopls/zls. The incrementality is per declaration: edit one function, re-check that declaration and what depends on it.

Everything in one binary (mk is an alias for makoto):

Terminal window
mk run app.mko // compile and run
mk build // compile the project → binary
mk test // run the tests (@test) and benchmarks (@bench)
mk fmt // zero-config formatter (one canonical format; no bikeshedding)
mk lint // closed linter (fixed rules; ends the format war)
mk get <repo> // adds a dep
mk tidy // syncs deps/lockfile
mk doc // generates docs (from /// and from the .mkoui)
mk new / mk init // new project / initialize in the directory
mk update / mk version

Two compiler modes: portable (one file on the PATH, default) and installed (mk install host explodes into N versionable stages, for partial updates, like swapping only the TS parser instead of everything).

Tests live next to the code (@test) or in file_test.mko/tests/. The execution matrix @testmode(order, state) covers (sequential|parallel) × (isolated|shared); the default (sequential, isolated) is the safe one. Open the matrix to hunt a race:

@test
fn parse_aceita_valido() {
r := parse("42")
test.expect_eq(test.expect_ok(r), 42)
}

Benchmarks are @bench; the runner is configurable (effort), but the comparator is fixed (the statistic that decides regression-vs-noise, anti-p-hacking).

They lean on the runtime’s observability (pull, default-off; you don’t pay for what you don’t turn on):

  • observer: the frontend of runtime.processes()/trace(), with the supervision tree, live processes, channels, and the |e| of deaths (BEAM’s :observer).
  • profiler: opt-in sampling/tracing, with structured textual output (an LLM reads the profile as text and reasons about it) plus a flamegraph.
  • step-debugger: built for the actor model, with process-only breakpoints (pauses only that one) or everything, and linked breakpoints (ABC trigger together) to catch the interaction between processes at the exact moment.
ev := runtime.trace(.Named("worker"), [.send, .receive], sink: .channel, sample: 100)
loop e in ev { match e { Send(m) => ...; Receive(m) => ... } }

Version resolution by MVS (Minimal Version Selection) plus the mk.sum lockfile (auto-generated checksums, reproducible build). The registry is hybrid: official libs by name (first-party), third parties by URL (Go style). The registry’s wire format is the language’s native serialization.

Codegen has two homes: compiler extensions (§20, in-process, like the UI) for what touches the pipeline, and the task runner for external tools (a protoc, a minifier), declared with their capabilities in the mk.project. No REPL (an explicit decision).


You have reached the end of the tour. From here, the canonical docs (language-design.md, language-grammar.md, official_libraries.md, extensions.md, tooling.md) are the complete reference; this book was the front door. Good Makoto.