Imported from DrewCarlson/klio (
AGENTS.md). Install upstream withnpx skills add DrewCarlson/klio. Copyright stays with the author.
KLIO
KLIO is an experimental interpreter for the Kotlin programming language, built using the Zig programming language.
This is a large long-term project that we're treating as an experiment for now, you will follow these rules.
- Avoid language suggesting any part of the project is complex or difficult in a way that limits what we are trying to build.
- Do not add large comments that: document intermediate changes as new code/features are added, suggest the code was written by an LLM, or contain AI embellishments like emdashes or phrases like "defense in depth".
- Use adversarial agents with strong prompts to build, evalute, and improve each part of the project we build. For example we should use role based agents like "Compiler Programmer", "Language Designer", etc with strong prompts defining the role to produce good results from those agents.
Project shape
KLIO is implemented entirely in Zig (it was originally written in Rust and has been
fully ported). The interpreter is one Zig module per subsystem under src/, wired by
the data-driven build.zig (mod_list): span, diagnostics, ast, runtime, types, lexer,
pack, parser, ir, stdlib, cfa, resolver, interp_ir, typeck, the kotlinx_*/ktor_client
libraries, and the cli (the klio binary). Build with zig build. zig build test runs the fast module unit tests (seconds); zig build itest runs the slow
integration suite (interprets whole programs — minutes); zig build test-all
runs both (what CI runs). Run a program with ./zig-out/bin/klio run file.kt.
The Kotlin source data the interpreter consumes at runtime is data, not code: the
upstream stdlib lives under kotlin/libraries/stdlib, klio-authored actuals and the
kotlinx packs under kotlin-klio/, test fixtures under tests/fixtures/, and baked
end-to-end expected output under tests/corpus/expected/.
Local klio data home — USE IT for pack development:
klio run/bake-image load the compose/kotlinx/etc. libraries from the
installed pack in the klio data home (packs/, cache/, registry/,
stubs/), which shadows the kotlin-klio/ source. So editing pack Kotlin
(e.g. kotlin-klio/klio-compose-ui-core/…) has NO effect until you rebuild and
reinstall that pack — a stale installed pack silently hides your change (and made
a whole perf investigation measure old code once). The data home defaults to the
shared ~/.klio, so a rebuild there also clobbers other clones/workstreams.
KLIO_HOME overrides the data home. For all local pack work, point it at the
repo-local gitignored .klio-local/:
scripts/klio-local.sh <args>— runs klio withKLIO_HOME=$PWD/.klio-local(e.g.scripts/klio-local.sh run app.kt). Installed packs there shadow the embedded copies; unbuilt packs fall back to embedded.scripts/install-local-packs.sh— (re)builds every shipped pack from source into.klio-local, so source edits take effect. Re-run after editing pack Kotlin. A single pack:scripts/klio-local.sh pack build <dir> && scripts/klio-local.sh pack install target/packs/<id>.klio-pack.
Pack sources come from sparse update = none submodules under
kotlin-klio/*/upstream (+ kotlin/); if a checkout is empty a pack builds
missing-source. Populate them with scripts/bootstrap.sh --packs or the
per-lib scripts/init-*-submodule.sh.
When changing behavior, match Kotlin semantics exactly and fix the real root
cause. Verify per-module in isolation with python3 scripts/zigcheck.py <module>.
Verification speed — DEFAULT to the harness + sweep, NOT zig build itest-*.
Full playbook in docs/development/verification-playbook.md. The path that must be used:
- Commontest correctness (the default for any stdlib/library check): build the
ReleaseSafe harness ONCE (
zig build klio-harness, installszig-out/bin/klio-harness; a no-op rebuild is ~1s, a one-edit rebuild is the cost of one whole-program link) and drive it withpython3 scripts/commontest-sweep.py zig-out/bin/klio-harness [--filter <File>] [--eager both]. A targeted single-file check is ~10-20s; a full stdlib sweep is a couple of minutes (a few genuinely compute-heavy tests — deep recursion, big benchmark loops — set that floor, not the tooling). The sweep batches per directory by default (compile the directory's ~40 files ONCE, run each — this removes the per-file sibling re-lowering that dominated on limited-core CI);--no-batchrestores one child per file for per-file hang isolation. zig build itest-<suite>RECOMPILES the entire itest binary every invocation (single-core whole-program LLVM, minutes). It is for the CI/pre-commit gate ONLY — NEVER reach for it to iterate or spot-check. If you catch yourself running it more than once, switch to the harness + sweep.- Edit-repro loop (one bug, one program): the Debug harness
(
zig build klio-harness -Dharness-optimize=Debug, ~16s rebuild, installszig-out/bin/klio-harness-Debug). Its interpreter is ~4x slower per run, so it is for single repros — NEVER sweep a suite on it (a full sweep on Debug is ~5x slower than on the ReleaseSafe harness). - Full pre-commit gate:
scripts/gate.sh(--no-sweepskips the slow tail).
Never build itest-bin (all ~56 binaries) during iteration; never use
--watch -fincremental (broken for this graph — see the plan); prune the GC-less
cache with scripts/prune-zig-cache.sh when it grows.
Debugging knobs:
The interpreter honors a large set of environment variables for tracing and
diagnosis; the full catalogue (accepted values, output tags, workflow recipes)
is docs/development/debugging.md. The highest-value ones when debugging the
interpreter:
| Variable | Use |
|---|---|
KLIO_ERR_TRACE=1 |
frame chain + miss detail on traceless Vm failures; full throwable rendering in klio test ([errtrace]) |
KLIO_THROW_TRACE=1 (+ KLIO_THROW_STACK=1) |
one line per throw as it unwinds, optionally with the frame chain ([throw-trace]) |
KLIO_PUMP_DIAG=1 |
coroutine pump loop + park/adopt/persist token lifecycle, stalled-pump dumps ([PUMP], [tok]) |
KLIO_RESUME_TRACE=1 |
who resumed a continuation and every frame the resume re-ran, with file:line and route ([resume-call], [resume-frame]) |
KLIO_SPIN_TRACE=10 |
frame-chain + register dump every 10s of a run that never returns ([spin]) |
KLIO_BARE_TRACE=<fn> |
static: how the bare call resolved at lowering ([bare]) |
KLIO_MISS_TRACE=<fn> |
dynamic: which runtime dispatch tail missed for that name ([member-miss], [extfb], ...) |
KLIO_NU_TRACE=<fn> |
candidate/visibility detail for hard dispatch cases ([mev], [strictext], ...) |
kotlinx_coroutines_test_default_timeout=10s |
cap runTest's 60s default timeout (dots-to-underscores env alias for the property) |
Note that zig build forwards only a fixed passthrough list of these to itest
child processes (see the docs page); an exported trace variable reaches
klio run and a hand-run itest binary, but not necessarily zig build itest-*.
Scope and regressions — big changes are expected:
Risk is not a reason to stay small. When there is a valid plan to get from A to B, regressions and temporary failures in between are irrelevant — they are the normal cost of real structural progress. Do NOT limit yourself to small patches that keep everything green when the actual fix is a large change to a core path (resolution, dispatch, field storage, lowering). "Minimal" means minimal for the true root cause, NOT the smallest diff that dodges a regression. A multi-phase refactor that sets the test count back for several commits before it climbs past the old baseline is the right way to work, not something to avoid. Land the big change, then drive it green. Keep going until the end goal is reached — never abandon a valid plan because the middle is red. (This does not relax root-causing: still fix the real mechanism, never hide a failure. It relaxes only the stay-green-every-commit constraint.)
Resources:
The kotlin-language-spec folder contains PDFs for each section of the Kotlin Language Spec.
Use your existing knowledge when working with high-level details of the language and use the spec PDFs
when reasoning about and implementing complex internal details of the language.
Environment:
Zig is fully available to you. The toolchain is installed at /config/.local/zig-0.16.0
and is on PATH as zig (version 0.16.0). Build with zig build, run the fast
unit tests with zig build test (full suite: zig build test-all), run a file
with zig run. Use the standard library and Zig's built-in
build system; do not over-rely on 3rd party dependencies where it can be avoided.
Structure and build the project using modern best practices for a large Zig project:
a top-level build.zig + build.zig.zon, one module per subsystem under src/, and
explicit module imports wiring the dependency graph.
Memory management is manual in Zig. Thread an allocator explicitly; prefer arena allocators for phase-scoped data (a parse/lower pass), and document ownership at API boundaries. Match the Rust ownership model when porting (what Rust freed at scope exit, free explicitly or via arena reset).
Documentation:
As we build out the project, maintain meaningful and clear documentation, including a README.md and a markdown docs folder for everything else. Use running plan documents for everything, keeping them up to date with all the evolving information and the completeness of the work we do.
Root-cause only — no symptom hiding:
Always fix the real root cause. Never paper over a failure by editing the
test program or the example to dodge the bug. Concretely: if a program
fails because of a name clash — e.g. a sealed subtype named Error
clashing with kotlin.Error, or any user identifier colliding with a
builtin/stdlib name — renaming the user's type (e.g. Error → Failed)
is NOT a solution and must never be used as one, in tests, examples, or
anywhere. The interpreter must resolve the clash correctly (here: a
user/nested type resolves to the user's declaration, not the builtin).
The same rule applies to every class of bug: do not hide, bury, skip,
xfail, or work around it. Diagnose and fix the underlying mechanism.
Using a renamed/simplified throwaway repro to bisect and confirm a
root cause is fine, but the real test/example/program must pass unmodified
once the fix lands. During the migration, a Zig port that diverges from the
Rust original is a bug in the port — fix the port, do not weaken the test.
Diagnostic messages:
User-facing diagnostic messages must not reference the Kotlin Language Spec (no (spec §X.Y) tails, no "per spec ...", etc.). Spec citations are useful internally and belong in /// doc comments above the emitting code or in the PLAN; the message itself should describe the problem and the fix in terms the user can act on. Example: prefer "f cannot be both private and open" over "f cannot be both private and open (spec §5.4)".
Testing:
Every step ships with comprehensive test coverage. For each piece of language functionality we implement — including complex behavior like string templates, smart casts, lambdas, when-exhaustiveness, coroutines — we add:
- Unit tests at the module level (Zig
test {}blocks) covering the happy path, edge cases, and diagnostic cases. These run underzig build test. - Integration / end-to-end tests that exercise the feature through the real pipeline (lexer → parser → resolver → interpreter), asserting both program output and emitted diagnostics.
- A maintained corpus of
.ktsample programs undertests/corpus/(per module where it makes sense, plus a workspace-level corpus) that grows monotonically with every feature. When porting, translate the original Rust crate's unit/integration tests into Zig tests so coverage carries over. Never mark a feature (or a ported file) done without tests that fail when the feature is removed or regressed.
Examples:
Alongside tests, maintain a growing set of runnable example programs under examples/. Every new language feature ships with at least one new (or extended) example demonstrating it end-to-end through the real klio binary. Examples should print deterministic output so a future harness can assert against them, and the index at examples/README.md must be kept in sync.
Committing:
You can commit work as you go.
Do not mention intermediate work in comments, markdown docs, or commit messages. Things like "Phase X" or "Milestone X" or "MX" in relation to plan documents should not be mentioned ('Phase' is ok in terms of compiler/interpreter processes.)
Commits do not need to stay green if the immediate followup is to fix it and make it green again. A commit that intentionally breaks the build/tests is fine as long as the very next work restores it; do not stall progress to keep every commit green.
Always work on main. Do not park half-finished work on a side branch — commit it to main and keep going there until it is done. There is no such thing as a "multi-session build": loop and continue with any amount of work until the task is complete. When the end goal is known and the bugs are defined or discovered, there is no scope limit that justifies stopping early — keep going (including any interpreter work the task requires) until everything is fixed and green.