Pipeline overview¶
klio has two entry paths through the front end: execution
(klio run) and diagnostics (klio check). They share the
lexer and parser; they diverge after the AST.
Execution path (klio run)¶
.kt bytes
│
▼ lexer UTF-8 source → tokens
▼ parser tokens → ast.KotlinFile
▼ (pack loading) merge installed pack ASTs into the module
▼ ast passes typealias expansion, @Serializable and @Composable plugins
▼ ir AST → register IR (lowering)
▼ interp_ir build the IR module, then Vm.run
▼
program output
The Vm executes the lowered IR directly. There is no AST evaluator
and no bytecode VM — ir lowers every supported construct
(classes, lambdas, suspend state machines, reflection, delegates) to
structured IR instructions, and the Vm dispatches on them. Under the
default fast profile, hot loops and functions additionally compile
to native code through the tiered JIT; see
Performance.
Before lowering, whole-program AST passes rewrite the parsed files in
place. The first is typealias expansion (ast.alias_expand): every
reference to a typealias — a type position, a constructor call
Alias(args) / recv.Alias(args), a supertype of a class or object
literal, a value read of an aliased object or companion, a callable
reference ::Alias — becomes the aliased type with the alias's type
parameters substituted, resolved with Kotlin's scoping (enclosing class
body, explicit imports, own package, star imports; the target itself
resolves in the alias's declaring file). Lowering and the runtime then
only ever see the target. An alias name the program also declares as a
classifier, function or value is left for the lowering's scope model.
KLIO_ALIAS_EXPAND=0 skips the pass. The @Serializable and
@Composable plugin passes follow.
With KLIO_EAGER=1 the run path also executes the resolver and type
checker ahead of lowering, and lowering consumes their answers
(call targets, receiver types) instead of deferring those decisions
to runtime; files the checker cannot finish fall back to the lazy
path.
| Module | Responsibility |
|---|---|
span |
Source map, file ids, byte and (line, column) positions. |
lexer |
UTF-8 source → tokens. Raw strings, templates, escapes; L00xx diagnostics. |
parser |
Tokens → ast.KotlinFile. Error recovery; P00xx diagnostics. |
ir |
Lowers the AST to the register IR (Module, Func, Inst). |
interp_ir |
Builds the IR module from one or more files and runs it on the Vm. |
runtime |
Runtime Value, instance data, and the Output sink. |
Diagnostics path (klio check)¶
klio check does not run the program. It resolves names and
type-checks for diagnostics only, then renders them and exits
non-zero on any error.
| Module | Responsibility |
|---|---|
resolver |
Name binding, import expansion, package recognition; R00xx diagnostics. |
typeck |
Type system, smart casts, intersection types, inference; T00xx / W00xx. |
cfa |
Control- and data-flow analyses (definite assignment, reachability) used by type checking. |
types |
Kotlin Type model, variance, inference constraint kinds. |
Type-checking does not gate execution: a program that type-checks
clean and a program that merely parses both run through the same Vm
(under KLIO_EAGER=1 the checker runs on the run path too, but as
an accuracy upgrade for lowering, never as a gate).
Stdlib and packs¶
The standard library ships as stdlib.klio-pack, embedded into the
binary by stdlib_pack as a byte slice. At startup the loader:
- Decodes the embedded stdlib pack and registers its native
bindings against
stdlib'sHostBindings. - Enumerates
~/.klio/packs/and$KLIO_PACKS, topologically sorts packs by their declared dependencies, and merges each pack's parsed AST into the IR module so its top-level declarations become part of the program. - Hands the resolved binding table to the Vm via
set_installed_bindings.
See Pack Format for the on-disk layout.
To avoid re-lowering the stdlib (and selected packs) on every run, the
CLI bakes the lowered dependency base — the IR module, its registry
side tables, the runtime ClassDef graph, and the post-lift AST the
extend path consumes — to a content-addressed image under
~/.klio/cache and extends it with just the user program's
declarations on later runs (src/interp_ir/image.zig,
src/cli/stdlib_image.zig). The image's wire format is the pack
codec's postcard style plus a shared-graph protocol (slice and AST-node
define/backref registries) so cross-references like
ClassDef.methods[].decl and inline-function ASTs decode pointing into
the same decoded tree they did in memory.
Diagnostics model¶
Every front-end pass emits through diagnostics.DiagnosticSink,
which renders to plain text, JSON, or SARIF. Codes are prefixed by
the originating pass — L0001, P0044, R0003, T0050. See
Diagnostics.
Testing¶
- Unit tests live alongside each module as
test {}blocks. - The
paritymodule runs every.ktundertests/fixtures/parity_corpus/andexamples/through bothkotlincand klio and diffs stdout. A green parity sweep is a primary correctness gate. - The upstream stdlib's own
commonTestsuite runs directly under the interpreter (src/itests/stdlib_commontest.zig, driven ad hoc byscripts/commontest-sweep.py). - Negative tests in
src/itests/typeck_negative.ziglock diagnostic wording and codes.
See Testing and verification for the full workflow.