Debugging the interpreter¶
Every diagnostic and tuning knob in klio is an environment variable, read at process start or on first use. This page catalogues all of them, grouped by subsystem, with the exact accepted values and the stderr tag each one prints. All of them are safe to combine.
How the switches parse¶
Four idioms cover almost every variable:
- Presence flags: setting the variable to anything (even the
empty string) enables it; only unsetting disables it. Most trace
switches work this way (
KLIO_ERR_TRACE=1). - Truthy flags: non-empty and not
"0"enables;=0or empty disables. Used where a default-on feature needs an off switch (KLIO_STDLIB_IMAGE=0) and by a few gates (KLIO_OR_AUDIT,KLIO_TRACE_PATH,KLIO_TRACE_INVARIANTS). The tables below say "0/empty off" for these. - Name filters: the value is a function/type name the trace is
restricted to, matched exactly (
KLIO_MISS_TRACE=maxOf) or as a substring (KLIO_SUBTYPE_TRACE=Comparable); the tables say<name>or<substr>. - Numbers: a count, size, or interval, noted per variable.
Two cross-cutting caveats:
- The variables are read through libc
getenv, so they work in every libc-linked binary (zig-out/bin/klio, the harness, itest children) but are inert in the no-libc module unit-test binaries. zig buildrun steps forward only a fixed passthrough list to their child processes (interp_env_keysinbuild.zig):KLIO_RACE_JITTER,KLIO_MAX_EVAL_DEPTH,KLIO_THROW_TRACE,KLIO_TRACE_RESOLVE,KLIO_TRACE_CHAIN,KLIO_TRACE_INVARIANTS,KLIO_TRACE_PATH,KLIO_TRACE_HTTP,KLIO_LINK_AUDIT,KLIO_RESOLVE_AUDIT,KLIO_RESOLVE_STRICT,KLIO_STDLIB_PACK,KLIO_PACK_DIAG,KLIO_STDLIB_IMAGE,KLIO_TRACE_STDLIB_IMAGE(plus, for fuzz suites,KLIO_FUZZ_SEED,KLIO_FUZZ_SEEDS,KLIO_SKIP_KOTLINC_PARITY,KLIO_KOTLINC_JVM_HOME,KLIO_KOTLINC_NATIVE,KLIO_NO_AUTO_INSTALL_KOTLINC,KONAN_DATA_DIR). Exporting any other variable reachesklio rundirectly but not azig build itest-*child; run the installed itest binary or the klio binary by hand instead.
Dispatch and resolution traces¶
Runtime dispatch is the Vm side (interp_ir/vm); bare-call
resolution is the lowering side (ir/lower). The static/dynamic
pair to reach for first is KLIO_BARE_TRACE (what lowering picked)
plus KLIO_MISS_TRACE (which runtime tail missed).
| Variable | Values | What it shows/does | Output tag |
|---|---|---|---|
KLIO_BARE_TRACE |
<name> |
How a bare call name(...) statically resolved during lowering: the chosen overload (fqn, params, ext, emit form) or NONE |
[bare] |
KLIO_ALIAS_EXPAND |
0 |
Skip the typealias expansion pass that rewrites every alias reference to its target before lowering; the lowering's per-site alias registry then serves the program (bisect an alias-related difference) | — |
KLIO_EXT_TRACE |
<name> |
How an explicit-receiver extension call resolved during lowering: receiver type, implicit dispatch owners, lexical owner, and exact target; also the static member-resolution verdict and self-recursive-bind arg shapes for name |
[ext-static], [member-static], [self-rec-shape] |
KLIO_MISS_TRACE |
<name> (two field-miss sites fire on any set value) |
Runtime dispatch tails for name that miss or fall back, with frame-chain dumps at several sites; also the member overload scorer's per-candidate verdict and the argument shapes it scored against |
[member-miss], [miss], [extfb], [pno], [cno], [setfield-miss], [lg-tail-a], [lg-tail-b], [ltg-tail], [cmg-tail], [sam-inv], [rim], [rim2], [pmo-shape], [pmo-multi] |
KLIO_CMG_TRACE |
<name> |
Snapshot of CallMemberOrGlobal preconditions for name (receiver tag, constructor-likeness, enclosing fn, this-index, capture count), plus every static Call of name with its first argument values (scalars and instance class@identity) and a [frame-push] line whenever a frame for name is entered — the arg/param values make stale-object reads visible at the call site |
[cmg], [call-inst], [frame-push] |
KLIO_NU_TRACE |
<name>, or 1 for all at some sites |
Candidate/visibility detail for hard dispatch cases: interface factories, member-extension visibility, strict extension member calls, enclosing-scope resolution | [eev], [ifact], [mev], [meoi], [par-miss], [strictext], [sbc] |
KLIO_SAM_TRACE |
set | Implicit-receiver candidate walk and member-arm dispatch shapes | [sam-walk], [sam-direct], [sam-arm], [marm] |
KLIO_RLP_TRACE |
<name> |
Receiver-lambda-param lowering for bare calls of name: which arm engaged (marked/resolved/outer/head) and the receiver-tower this@<label> pick |
[rlp-arm], [rlp-head] |
KLIO_HEAD_TRACE |
set | Runtime head-directed receiver re-selection for CallValueWithThis instructions carrying a declared receiver head |
[cvth] |
KLIO_SDU_TRACE |
set | Every stdlib member-dispatch call that missed both resolve-cache tiers and runs the uncached probe ladder (type, name, cacheability) | [sdu] |
KLIO_SELDBG |
set | Why an intrinsic-host invokeMethod probe declined (error tag + message for each swallowed non-Throw error — the recipe that separates "method missing" from "method ran and failed") |
[seldbg] |
KLIO_MCRT_TRACE |
<name> |
Member-call return-type derivation for a chained arg (recv.map { … }): receiver tag/type and candidate agreement |
[mcrt] |
KLIO_ADM_TRACE |
set | Callable-vs-class adjudication detail inside argDefinitelyNotParamType |
[adm] |
KLIO_MEMO_TRACE |
set | Compose plugin memoization-path decisions per lambda arg (cache / lifted singleton / remember, with capture keys) and the value-invocation call-site $changed bits |
[memo], [bits] |
KLIO_EF_TRACE |
<name> |
Emit-form / member-shadowability decision for a named call (inline target chosen, shadowable routing, receiver-context flags) | [ef], [tbie], [efset] |
KLIO_INLINE_PICK |
<name> |
Inline-overload candidate set (receiver type, owner class, file) plus the receiver chain head | [ipick] |
KLIO_EXTKEY_TRACE |
<fid>[,<fid>] |
The eight-element extension ranking key for the named candidates, plus their parameter type heads. Ranking is lexicographic, so the first differing component is the one that decided | [extkey] |
KLIO_ARGTY_TRACE |
<identifier> |
The static type lowering actually used for that named expression, and whether it came from an inline splice's declared parameter type. Separates "no type" from "wrong type", which look identical from a failing test | [argty] |
KLIO_SPLICE_TRACE |
<function name> |
Whether a named inline fun reaches the splice path (with the declaration and call-site spans), whether the call-site splice landed or bailed to dispatch, and — matched against a lambda PARAMETER name — each caller-lambda splice with its receiver-mark state |
[splice], [splice-ok], [splice-bail], [splice-lam] |
KLIO_THIS_TRACE |
set | Every bare-this lowering: the active splice window, each scope index holding a this binding, and the register resolve picked. The tool for "whose this did this lambda capture" |
[this-trace], [splice-bind] |
KLIO_LEAF_TRACE |
<substring of a function name> |
Why the frameless leaf-expression serve declined for a matching function (unsupported opcode, non-instance field receiver, unclaimed field route, callee that is not a leaf) | [leaf] |
KLIO_SBC_TRACE |
set | Constructor-vs-member routing inputs for each capitalized bare call | [sbc] |
KLIO_SUBTYPE_TRACE |
<substr> |
Instance-supertype search during overload scoring, for target types containing the substring | [sub] |
KLIO_SHADOW_TRACE |
set | Whether an imported pack extension shadows a member call (probe plus each candidate) | [shadow] |
KLIO_BARERET |
<name> or * |
Why a bare call does or does not lend its return type to the local it initializes: the receiver head it resolved against, the target, the final type, and each refusal | [bareret] |
KLIO_LI_NAMES |
set | Names the callee of every local initializer that yields no static type. Pair with KLIO_BARERET on whichever name dominates |
[li-null] |
KLIO_DISPATCH_STATS |
1 |
Route counts per dispatch kind, plus replay-hits: how many named member calls the builtin intrinsic replay served outright. member_ladder names a route, not a walk — compare the two before reading it as name-resolution work |
[dispatch-stats] |
KLIO_NOINST_WHY |
1 |
Why a statically bound slot on a host-backed receiver declined to a member-name walk: no-slot-entry (no (class, slot) mapping) or target-not-executable (the target is bodyless and no native is registered under its FQN) |
[noinst-why] |
KLIO_NORECV_WHY |
a local name, or * |
Why an untyped-receiver site's local has no type: at=file:line, the initializer's AST tag, and the deriver's terminal. NOTE: the census counts CALL sites — probe with x.first(), never x.size, or the file has no counted site at all and the measurement is vacuous |
[norecv-why] |
KLIO_ICRT |
1 |
Each return-type instantiation's pre-solve state and terminal (OK, bindings incomplete, star head), plus which parameter refused the bind and both sides' argument counts — a param=x(Array nargs=1) actual=Array nargs=0 row means the ARGUMENT's recorded type dropped its arguments, not a real mismatch |
[icrt] |
KLIO_MAX_WORKERS |
<n> |
Caps BOTH the dispatcher pool's compute width (default: half the cores) and its elastic IO ceiling (default: max(16, cores)). Raise it when a single instance owns the machine; the commontest sweep sets 2 for its children so a full sweep stays near half the cores |
— |
KLIO_NORECV_NAMES |
a [no-recv-path] bucket name, or * |
Names the receiver identifier behind each untyped bare-path receiver, split by why it is untyped (local_no_decl_type, captured, enclosing_member, unknown) |
[no-recv-name] |
KLIO_ARGSHAPE_UNK |
set | Every argument whose applicability shape carries no type, no literal kind and no callable form — the expression forms that leave a member call unproven. Histogram the tags to pick the next typing channel | [argshape-unk] |
KLIO_NULLEXT_NAMES |
set | Every member call held off the static path because a T? extension of that name exists and the extension itself did not resolve — the residue of the nullable-receiver rule |
[nullext] |
KLIO_SPLICE_REF |
set | The full receiver type each inline splice installs in its window, and the argument it came from. Names the link where a chain loses its type arguments | [splice-ref] |
KLIO_SLOT_BYNAME |
set | Every statically bound virtual slot call that degraded to a by-name member walk, with the slot root. The host boundary made visible | [slot-byname] |
| — | — | NOTE: any captured log carrying $class$ identity-mangle rows embeds NUL bytes and is BINARY to grep — filter with grep -a, or matching rows silently vanish and a dump looks nondeterministic |
— |
KLIO_COMP_TRACE |
set | The type a destructured name takes from its componentN() accessor, or why none was available |
[comp] |
KLIO_INIT_SELF |
0 to disable |
Off, a local's own name shadows its initializer's bare call again (val iterator = iterator()). For A/B measurement of that channel from one binary |
— |
KLIO_TP_HEAD |
0 to disable |
Off, a type-parameter receiver resolves only through a bound that carries no type arguments, so C : MutableCollection<in T> names no owner again |
— |
KLIO_EXT_RECV_PROP |
0 to disable |
Off, a bare name in a top-level extension's body stops resolving to the extension receiver's property, so it gets no declared type | — |
KLIO_MEMBER_INIT |
0 to disable |
Off, a property-read, indexed-read or ALIAS initializer (val node = coord.layoutNode, val held = row[1], val b = a) stops lending its type to the local |
— |
KLIO_RECV_CHAIN |
0 to disable |
Off, a member or indexed receiver is typed only from a declared type or a call's return type, never from the type a local's own initializer lends it | — |
KLIO_BIND_LUB |
0 to disable |
Off, a generic call's type-parameter constraints must be EQUAL across the receiver and every argument — a subsumed constraint (getOrDefault(k, Derived()) on a Map of Base, listOf(Derived(), base)) rejects the instantiation again |
— |
KLIO_TP_DISPROOF |
0 to disable |
Off, a receiver type argument that is a declared TYPE PARAMETER stops disproving concrete-element extension candidates (Array<T> no longer rules out Array<out Double>.minOrNull) |
— |
KLIO_SOLE_EXT |
0 to disable |
Off, the single extension candidate left after the disproof pruned every competitor is withheld again instead of committed | — |
KLIO_DISPROOF_TRACE |
set | Per-candidate receiver-compat decision in extension resolution: subtype result and both disproof-completeness answers | [disproof] |
KLIO_BARE_EXT |
0 to disable |
Off, a bare call in a receiver context resolves only MEMBERS of the implicit receiver — an extension written without this. (toMutableList() in an extension body) stops lending its return type |
— |
KLIO_TP_RECV |
0 to disable |
Off, a call on a receiver typed by a TYPE PARAMETER (M : MutableMap<in K, MutableList<T>>) stops deriving its return type through the parameter's full upper bound, so the local it initializes loses its type again |
— |
KLIO_SOLE_GLOBAL |
0 to disable |
Off, a bare call under a receiver context stops lending its return type even when its name has exactly one declaration program-wide | — |
KLIO_FACTORY_PROP |
0 to disable |
Off, only a CONSTRUCTOR call names an un-annotated property's type — a factory call (val made = newBase()) and a constructor PARAMETER (private val held = start) both stop registering a type head |
— |
KLIO_NULL_CHAIN |
0 to disable |
Off, only a condition that is itself the whole != null check narrows — an && chain and an early-return guard stop smart-casting |
— |
KLIO_AGREED_RET |
0 to disable |
Off, a bare call whose every top-level declaration agrees on a return type stops lending that agreed return to the local it initializes | — |
KLIO_CTOR_RET |
0 to disable |
Off, a direct constructor expression (SlotTable().also { … }) stops naming its own type for the value it produces |
— |
KLIO_EAGER_MEMBER |
0 to disable |
Off, the checker's eager extern-call pick stops serving member-call lowering where the lazy engine has no receiver type or no resolver target | — |
KLIO_ENGINE_LAMBDA |
0 to disable |
Off, a lambda's declared fn type is no longer instantiated through the one-pass call-binding solve (receiver + typed args + explicit type args) | — |
KLIO_ITC_MEMBER |
0 off, 1 on (default), or name1,name2 |
Whether a member the receiver type provably declares commits statically over the OrGlobal fallback; a name list restricts the commit to those calls |
— |
KLIO_LAMBDA_REFUTE |
0 to disable |
Off, a lambda argument stops refuting candidates whose parameter names a resolvable non-fun-interface class | — |
KLIO_LAMBDA_RET |
0 to disable |
Off, lambda argument shapes are no longer enriched from the target's declared Function-typed parameters |
— |
KLIO_MEMBER_EXT_SPLICE |
0 to disable |
Off, a member body's own extension call stops qualifying for the inline splice | — |
KLIO_MEMBER_PROMO |
0 to disable |
Off, a deferred member call is never promoted to a static bind by the member-compatible / every-extension-refuted proof | — |
KLIO_NAMED_COMMIT |
0 to demote |
0, a candidate that named-argument mapping skipped stays a typing-only answer instead of committing for emission |
— |
KLIO_RECV_REFUTE |
set to enable (default off) | On, a candidate whose declared receiver classifier is provably unrelated to the proven static receiver is dropped outright (kotlinc's static receiver semantics; the lazy default keeps runtime-polymorphic leniency) | — |
KLIO_REFSHAPE |
0 to disable |
Off, a callable-reference argument (::f) stops contributing its declaration-read function type to the argument shapes |
— |
KLIO_STAR_RET |
0 to disable |
Off, a type parameter still unbound after the receiver and every argument had their chance refuses the whole return instantiation instead of erasing to * |
— |
KLIO_SUBST_NONGEN |
0 to disable |
Off, a head-only receiver naming a non-generic class stops counting as a complete substitution receiver (SlotWriter.let { … } no longer binds T := SlotWriter) |
— |
KLIO_THIS_NARROW |
0 to disable |
Off, an is-narrowed this stops serving as the innermost implicit receiver head for bare calls |
— |
KLIO_TOWER_EMIT |
0 to disable |
Off, a bare call stops committing statically to an OUTER tower entry's extension through its this@<label> slot |
— |
KLIO_TOWER_EXT |
0 to disable |
Off, typing-side bare-call extension resolution stops trying the outer implicit-receiver tower entries when the innermost head serves none | — |
KLIO_TOWER_SCOPE |
0 to disable |
Off, a this-capturing lambda or parameter thunk never counts its receiver scope complete through the tower | — |
KLIO_VOWN |
0 to disable |
Off, a stub or value-class owner stops emitting its virtual slot and declines to the member-name walk | — |
KLIO_HDR_BOUNDS |
0 to disable |
Off, header-stub type-parameter bounds are not registered at image build, so bare-call resolution loses them | — |
KLIO_HDR_BOUNDS_SKIP |
<substr> |
Skips header-bound registration for functions whose name the value contains — the per-name bisect of KLIO_HDR_BOUNDS |
— |
KLIO_HDR_BOUNDS_LIST |
set | Prints each function's registered header bounds as they are put | [hdrb] |
KLIO_NO_LR_ABSORB |
set disables | Set, an inline body containing return@<fnName> no longer gets the labeled-return absorb region |
— |
KLIO_NO_LR_STATIC |
set disables | Set, a return@<inlineFnName> inside a spliced lambda stops resolving statically to the splice frame's join |
— |
KLIO_SPLICE_HYG |
0 to disable |
Off, a top-level extension's spliced body resolves in the caller's member scope instead of its own declaration scope | — |
KLIO_SPLICE_PIN |
set disables | Set, a bound-receiver member call inside a splice is no longer pinned to its virtual slot — the per-invocation walk arbitrates again | — |
KLIO_SLOT_TRACE |
<name> or * |
Each inherited-slot merge decision for methods of that simple name: the competing FuncIds and which one the class's table keeps | [slot-merge] |
KLIO_SLOT_DUMP |
<name> |
Every (class, slot) -> implementation entry whose target has that simple name, with the target's owner — what runtime virtual dispatch will actually reach |
[slot-dump] |
KLIO_ABSVETO_TRACE |
set | Each inline-overload pick vetoed because a member of the receiver chain takes the call instead | [absveto] |
KLIO_AGREED_TRACE |
<name> or * |
Why the agreed-top-level-return channel accepted or refused the name (top-level usable, enclosing member, class-member namesake, ...) | [agreed] |
KLIO_ALPT |
<fn name> ([alpt-site] rows fire on any set value) |
Argument-lambda parameter-type computation for calls of the named target: entry shape, each filled slot, and which emit site asked | [alpt], [alpt-slot], [alpt-site] |
KLIO_APPLIC_TRACE |
set | Candidates the shared applicability scorer refuses: the runtime re-pick's null scores and the named-arm hard-reject site that declined | [pp-null], [applic-reject] |
KLIO_BAREARM |
set | The bare-member arm of unresolved bare-call lowering: why it broke (no class id, no this register) and each final miss with file:line |
[barearm-break], [barearm-miss] |
KLIO_BARG_TRACE |
<fn name> ([barg-ids] rows fire on any set value) |
Static bare-call argument compatibility: per-argument param-vs-arg verdict with route, plus the receiver class-id scoping rows | [barg], [barg-ids] |
KLIO_BCC_WHY |
set | Why the package/import-scoped bare-call candidate set came back empty (no candidates, no visible tier, other-package tier, no arity match) | [bcc] |
KLIO_BINDS_TRACE |
<name> |
Whether a bare name binds this: receiver-chain head, hierarchy-shadow completeness, own-member state, and the verdict |
[binds] |
KLIO_CHAN |
<name> |
Snapshot of every receiver channel at a bare call in receiver context: this-narrow, splice hint and receiver, owner class, this decl, static head |
[chan] |
KLIO_CITR_TRACE |
<name> |
Constructor-initializer type derivation for a bare ctor call: the local-class head or the bail reason (local/outer binding, function namesake, no class) | [citr] |
KLIO_CIX_TRACE |
<class name> |
Scoped class-by-simple-name resolution: each candidate's fqn, package, and tier | [cix] |
KLIO_CPT_TRACE |
set | Why instantiating a declared receiver typed by a class type parameter declined | [cpt] |
KLIO_CTORLPT_TRACE |
set | Constructor trailing-lambda parameter typing: the class row's parameter shapes at each ctor call carrying lambda args | [ctorlpt] |
KLIO_DECLTY_TRACE |
<name> |
The receiver static type a member call of that name resolved against, and whether it came from a declaration or the full deriver | [declty] |
KLIO_DROP_TRACE |
<name> |
Why each bare-call candidate dropped from the applicable set (form mismatch, low priority, no sig view, inapplicable shape, static-incompatible) | [drop] |
KLIO_EBM_TRACE |
set | Expression-body member registry register/lookup rows (currently keyed to createOnCancellationAction only) |
[ebm] |
KLIO_EMIT_TRACE |
<name> or * |
Every Call/CallVirtual/CallMember/CallMemberOrGlobal instruction pushed for that simple name, with the resolved target and emitting function |
[emit] |
KLIO_EMIT_STACK |
set (needs KLIO_EMIT_TRACE) |
Adds a native stack trace naming the emitting arm at each traced CallMember push |
native stack |
KLIO_EXPECT_HDR_TRACE |
set | Each bodyless expect-class member retained as a header row binding its host symbol |
[expect-hdr] |
KLIO_FORVAR_TRACE |
set | Each for-loop variable whose iterable element type could not be derived, so the variable lowers untyped | [forvar] |
KLIO_GRA_TRACE |
<receiver head> |
The generic-receiver applicability walk for actual receivers with that head: head relation, binding failures, and per-param bound checks | [gra] |
KLIO_HOP_TRACE |
set | The +/- operator's member-call lowering channels, and a type-parameter-headed receiver substituting its full bound before extension ranking |
[binop-in], [binop], [hop] |
KLIO_IMPLPROP_TRACE |
<name> |
Implicit property-read typing for that name: the bare, guard, implicit-receiver, and member arms with their channel state | [implprop], [implprop-bare], [implprop-guard], [implprop-mem] |
KLIO_LAMINH |
set | The lambda-body declared-type inheritance channel: the pending snapshot each lambda body consumes (an empty pending while the enclosing builder holds records means the records were lost) | [laminh] |
KLIO_LAMRET_TRACE |
set | The lambda-return-directed extension-family pick: the derived return head's winner and the instantiated callee return | [lamret-pick], [lamret-inst] |
KLIO_LAMRET_WHY |
<substr of an fqn> |
Per-candidate skip reasons in the lambda-return family walk (no body, receiver mismatch, non-fn last param) | [lamret-why] |
KLIO_LAR_TRACE |
set | The lambda-arg declared-receiver record: each put and get keyed by argument span | [lar-put], [lar-get] |
KLIO_LFN_TRACE |
set | Local-fn overload selection for bare calls: overload count, the selected mangled cell, applicability, and capture reachability | [lfn] |
KLIO_NOCLASS_HEADS |
set (needs KLIO_DISPATCH_STATS) |
Names each receiver head the census counted as having no class id, with its bound record where one exists | [no-class-head] |
KLIO_OPTY_TRACE |
set | Indexed-read return typing (a[i] as get on the container): receiver typing and the answer |
[opty] |
KLIO_OVERRIDES_TRACE |
set | Why overridesSlot rejected each (own method, inherited slot) pair: missing sigs, kind/arity mismatch, no ancestor bindings, param-type mismatch |
[ovr] |
KLIO_PROMO_NAMES |
set | Member-promotion proof verdicts, each PROMOTED/HELD with the refusal reason (the per-candidate [promo-ext] rows also need KLIO_DISPATCH_STATS) |
[promo-ext], [promo-proof] |
KLIO_REF_TRACE |
<name> |
::name callable-reference lowering state: local-ext detection, receiver context, resolve/capture reachability, and the expected fn type |
[ref-trace] |
KLIO_RENAME_TRACE |
set | Each package-scope type-rename resolution (an internal classifier renamed for its whole package) |
[rnm-pkg] |
KLIO_REX_TRACE |
set | Extension-resolution ranking, one window per call: the call row, per-candidate state, each scored key or disqualification, and the exit reason | [rex-call], [rex], [rex-key], [rex-exit] |
KLIO_RH_TRACE |
set | Each receiver-lambda body head the type checker records for the eager channel | [rh-put] |
KLIO_RMC_TRACE |
<name> |
Per-candidate member-args-compatibility verdict during member resolution | [rmc] |
KLIO_SCOPEFN_TRACE |
set | The scope-function (let/run/also/apply) return-derivation arm: enter and each bail reason |
[scopefn] |
KLIO_SCORE_TRACE |
set | The applicability scorer's per-argument refusals: parameter vs argument type at each null score | [score-null] |
KLIO_SCRT_TRACE |
<name> |
The static call-return-type derivation for calls of that name: which channel answered, the explicit-type-arg arm, the agreed-return handoff, and the final answer | [scrt-via], [scrt-out], [scrt-path], [scrt-agreed], [scrt-target], [expl] |
KLIO_SIBEXP_TRACE |
set | The sibling-argument expected-type solve: the pushed instantiation or the bail | [sibexp], [sibexp-inst], [sibexp-bail] |
KLIO_SIBEXP_WHY |
<outer fn name> |
Per-candidate skip reasons in the sibling-expected solve (receiver untyped, no bindings, result not concrete) | [sibexp-why] |
KLIO_SMAC_TRACE |
<fn name> |
Static member-args compatibility: entry state and each argument's instantiated-parameter verdict with route | [smac], [smac-arg] |
KLIO_TLP_TRACE |
<prop name> |
The tiered top-level property type-head lookup: each declaration's package, tier, and head | [tlp] |
KLIO_VABI_NAMES |
set | Each member call declined off the virtual-slot emit, with the owner's receiver ABI and body state | [vabi] |
KLIO_VALTY_TRACE |
<local name> |
Property-decl lowering entry state for that local, and every declared-type write recorded under the name | [valty] |
KLIO_VALTY_STACK |
set (needs KLIO_VALTY_TRACE) |
Adds a native stack trace at each declared-type write | native stack |
KLIO_VARARG_TRACE |
set (most rows keyed to listOf) |
The sole-trailing-vararg full-instantiation derivation (listOf("a") as List<String>): guards, candidate refusals, and element typing |
[vaf-guard], [vaf-nocands], [vaf-enter], [vaf-cand], [vaf-sole], [vaf] |
The 0-to-disable rows above exist so one binary can be compared against
itself: scripts/examples-ab.sh KLIO_SOME_GATE runs the examples corpus both
ways and reports what differs. It skips the twelve examples that never
terminate (each blocks on a window or event loop at ~0% CPU, at every commit) —
left in, they cost twice the timeout apiece for no signal and turn a five-minute
comparison into a three-hour one.
| KLIO_OPERATOR_TY | 0 to disable | Off, an indexed read and the times/div/rem/rangeTo operators stop lending their declared return type to a receiver | — |
| KLIO_GLOBAL_TRACE | <name> | Which arm resolves a global lookup: cached value, function, or intrinsic | [gtrace] |
| KLIO_OUTER_TRACE | <substr> | Inner-class enclosing this@Outer selection for IR names containing the substring | [outer] |
| KLIO_ANON_AUDIT | set | Synthesized class name and captured names at each anonymous-object site | [ANON] |
| KLIO_REBIND_AUDIT | set | Arity-guess this rebinds during closure invocation | [REBIND] |
| KLIO_CVNRC | set | A this-less closure invoked on an instance receiver being rebound to callValueWithThis | [cvnrc] |
| KLIO_TRACE_RESOLVE | name1,name2 or * | Per-dispatch decision log for the named function(s) | [RESOLVE] |
| KLIO_TRACE_CHAIN | set | Adds the enclosing-this chain to each traced dispatch (with KLIO_TRACE_RESOLVE) | [RESOLVE] chain= |
| KLIO_TRACE_PATH | set; 0/empty off | One structured record per terminal dispatch site (proves single-path dispatch; see scripts/assert_single_path.py) | [PATH] |
| KLIO_TRACE_INVARIANTS | set; 0/empty off | Detect-only dispatch invariant checks, one machine-readable line per violation | [INVARIANT] |
| KLIO_TRACE_CAPTURE | set | A lambda capture that fails to resolve and collapses to Unit | [CAPTURE] |
| KLIO_UNRESOLVED_TRACE | set | The unresolved bare name, function, and span just before an Unbound error | [unresolved] |
| KLIO_INIT_DEBUG | set | object/companion initializer first-failure and the cause take/swallow/restash steps | [init-debug] |
| KLIO_ANON_BASE | 0 to disable | Off, anonymous-object synthesis lowers against the empty side module instead of the image-clone, leaving every call in anon bodies name-dynamic | — |
| KLIO_ANON_PROP | 0 to disable | Off, an anon object's property type heads are not carried into its member lowerings, so sibling bodies lose their bare property-read types | — |
| KLIO_BARRIER_TRACE | set | The type-safe collection bridge (erased-bound check on generic members called through an erased signature): each refusal reason | [barrier] |
| KLIO_CFN_TRACE | <substr of a fn name> | Named-argument call binding: the declared parameter list vs the supplied names, on both the named and the typed entry | [cfn], [cft] |
| KLIO_CHAIN_TRACE | set | Enclosing-this chain activation per frame: enter/activate with thread id, frame pointers, and chain base (high volume) | [chain] |
| KLIO_DCS_TRACE | set | The declaring-class scan for a FuncId — the per-class method walk feeding the owner cache (very high volume) | [dcs] |
| KLIO_DRAIN_TRACE | set | Each Iterable receiver drained to a list by the collection fallback, with the caller and call-site span | [drain] |
| KLIO_FASTPLAN_TRACE | <substr of a fn name> | Why a function is ineligible for the monomorphic fast call plan (no body, inline, extension, defaults, sibling overloads, ...) | [fastplan] |
| KLIO_ITER_TRACE | set | The builtin iterator's next() element kind per call | [iter-next] |
| KLIO_KTYPE_TRACE | set | Each synthetic KType materialized for a reified type name, with the enclosing function | [ktype] |
| KLIO_MEOI_TRACE | <owner class> | Member-extension dispatch-receiver selection: the enclosing entries walked and each owner-identity verdict | [meoi] |
| KLIO_NOINST_TRACE | set | Each virtual slot resolved by member name against the runtime class of a host-backed (non-Instance) receiver | [noinst] |
| KLIO_PICK_TRACE | <name> | The runtime overload re-pick: the base candidate's applicability score and every sibling's | [pick] |
| KLIO_QT_TRACE | <substr of a qualifier> | The qualified this@Qualifier receiver walk: each candidate class and the match | [qt] |
| KLIO_REDIR_TRACE | set | Value-shaped redirect-target resolution among same-arity expect/actual siblings | [redir] |
| KLIO_RFP_DUMP | set | Dumps every registered receiver-fn-property (receiver, name) pair when the gate masks build | [rfp] |
| KLIO_ROUTE | <name> | Which runtime arm bound each *OrGlobal execution of that name (member@depth, overload, global-id, global, the fallback variants), plus dispatch-ladder route markers | [route] |
| KLIO_SLOT_RECV | set (needs KLIO_SLOT_BYNAME) | The slot-byname note prints the receiver's runtime type instead of the slot root | [slot-recv] |
| KLIO_THIS_TRAP | set | Every frame entry that binds a Bool or Int into a this parameter — the ext-receiver misbind signature — with the caller | [this-trap] |
| KLIO_VFLAT_TRACE | set | One line per declined virtual flat prepare, with the reason a slot population stays recursive | [vflat] |
| KLIO_WALK_TRACE | set | Each by-name IR method walk and extension-fallback walk entry, with the receiver and cache-key state | [ir-walk], [extfb-walk] |
Resolution audits and the eager front end¶
The resolver + type checker always run ahead of lowering; their overload
picks and type heads feed it. There is no switch — KLIO_EAGER was removed
once validation was identical with and without the evidence. A
resolver/typeck failure still falls back to AST evidence alone, so a program
that defeats the front end runs.
The audit switches emit machine-readable divergence records the
sweep scripts grep; see
Testing and verification for the
resolve_audit_sweep.py cycle.
| Variable | Values | What it shows/does | Output tag |
|---|---|---|---|
KLIO_EAGER_AUDIT |
set | Eager-pipeline bookkeeping (skip reasons, record counts) and eager-vs-lazy pick disagreements | [EAGER], [EAGER-AUDIT] |
KLIO_EAGER_HITS |
set | Per-call eager record/probe/hit/miss logging (high volume) | [EAGER-REC], [REC-MSC], [EAGER-PROBE], [EAGER-HIT], [EAGER-MISS2] |
KLIO_RESOLVE_AUDIT |
set (any value, even 0, enables) |
One record per bare call / inline target / value ref comparing the symbol index against the order-based heuristic, with a divergence grade | [KLIO_RESOLVE_AUDIT] |
KLIO_RESOLVE_STRICT |
set; 0/empty off |
Turns an unexplained index-vs-heuristic divergence into a panic instead of a log line | none (panics) |
KLIO_OR_AUDIT |
set; 0/empty off |
Member-vs-global audit: each *OrGlobal emission decision and the runtime arm that actually bound (scripts/or_audit_sweep.py asserts the lenient-arm residue) |
[KLIO_OR_AUDIT] |
KLIO_LINK_AUDIT |
set (any value enables) | Re-derives what the deleted per-call dispatch ladder would have chosen and logs any disagreement with the link-settled tables | [KLIO_LINK_AUDIT] |
KLIO_RECVHEAD_AUDIT |
set | Whether the type checker's recorded receiver-lambda head can answer the membership walk | [RECVHEAD-AUDIT] |
KLIO_TYPEHEAD_AUDIT |
set | The type checker's per-argument type head vs the AST-derived declared type (fills and disagreements) | [TYPEHEAD-FILL], [TYPEHEAD-AUDIT] |
KLIO_DECL_AUDIT |
1 |
Completeness audit of the no-holes symbol table after the run: every intrinsic FQN paired with whether the module declares it, tallied per package with hole samples. Program-scoped — the lazy IR only declares what the program reached, so the number is a lower bound | [decl-audit] |
scripts/commontest-sweep.py accepts --eager for compatibility and
ignores it: there is only one pipeline, so both just runs the corpus
twice and reports any run-to-run divergence (useful for catching
nondeterminism, not modes).
Errors, throws, and hangs¶
| Variable | Values | What it shows/does | Output tag |
|---|---|---|---|
KLIO_ERR_TRACE |
set | On otherwise-traceless Vm failures: the live frame chain plus a site-specific miss line (unresolved field get, uninvokable call value, unmatched this@label). In klio test it also renders the full throwable (type, message, frames, causes) instead of the terse summary |
[errtrace], [getfield-miss], [callvalue-miss], [labeled-this] |
KLIO_THROW_TRACE |
set | One line per exception as it is thrown (including failed casts that raise without a Throw) |
[throw-trace] |
KLIO_THROW_STACK |
set (needs KLIO_THROW_TRACE) |
Adds the full frame chain at each throw site | [errtrace] |
KLIO_LR_TRACE |
set | Labeled-return propagation through interpreter frames (raise, pass, exit) | [lr-raise], [lr], [lr-exit] |
KLIO_AMP_TRACE |
<substr> |
A resolution-class error about to be re-tagged as CalleeFailed whose message contains the substring; dumps the frames before they are torn down |
[amp] |
KLIO_SPIN_TRACE |
seconds (unparsable values fall back to 30) | Every N seconds of wall time, dumps the live frame chain and the innermost frames' registers, so a run that never returns names its loop | [spin] |
KLIO_SEGV_TRACE |
set | Installs a segfault handler at startup so SIGSEGV/SIGBUS prints a native backtrace | native backtrace |
KLIO_MAX_EVAL_DEPTH |
number (default 2000) | Caps interpreter recursion depth; on breach returns a catchable StackOverflow instead of faulting the native stack |
none |
KLIO_RUN_TIMEOUT_S |
seconds (0/unset off) |
Wall-clock deadline for the whole run; a watchdog thread aborts the process when it expires | [klio] |
KLIO_TEST_WALL_CAP |
seconds (default 300; 0 disables) |
Per-test wall cap in klio test: a wedged test fails "test wall-clock deadline exceeded" instead of hanging the run |
none |
Coroutines and the pump¶
| Variable | Values | What it shows/does | Output tag |
|---|---|---|---|
KLIO_PUMP_DIAG |
set | The cooperative pump: loop/slot/parked dumps, the park/adopt/persist/take token lifecycle, stalled-pump dumps, idle-streak and exit-time sleep attribution | [PUMP], [tok], [pump-streak], [wall-timer], [pump-sleep] |
KLIO_RESUME_TRACE |
set | The resumer's identity and frame chain, then one line per frame a resume drive re-runs, with path:line, the delivery route (via=pump-ready, inline-claim, persisted-on-top, ...), and the activation id. Catches double delivery |
[resume-call], [resume-frame] |
KLIO_SCOPE_DIAG |
set | The coroutine active-scope stack lifecycle: capture/restore on park and resume, guard enter/leave, push/pop | [scope] |
KLIO_NO_INLINE_RESUME |
set disables | Forces every continuation resume to queue on the pump instead of running inline on the caller's stack | none |
KLIO_PUMP_NOSLEEP |
set | Skips the 1 ms sleep slice in the wall-clock timer drain (busy-loops instead) | none |
KLIO_SYNC_RESUME |
1 to enable (default off) |
A cross-thread Kotlin resumeWith post waits (bounded) for the owner pump to run the routed step before the caller continues, instead of the fire-and-forget default |
none |
KLIO_SUSPEND_STATS |
set | Running counters of suspension snapshots (dense, slots, saved, params, captures, receivers), printed every 50k snapshots | [suspend-stats] |
kotlinx_coroutines_test_default_timeout |
Duration, e.g. 10s (default 60s) |
The runTest timeout. This is the env alias for the kotlinx.coroutines.test.default_timeout property: the property shim retries a dots-to-underscores form of any property name against the environment |
none |
KLIO_RACE_JITTER |
set | Widens object-cell lock acquisition windows (spin + yield) so genuine data races reproduce reliably under test | none |
KLIO_PUMP_DIAG=1 KLIO_RESUME_TRACE=1 kotlinx_coroutines_test_default_timeout=10s \
./zig-out/bin/klio test HangingTest.kt
Compose plugin¶
The @Composable lowering plugin + upstream engine runtime is the only compose
path — it always runs. These knobs bisect its two emissions.
| Variable | Values | What it shows/does | Output tag |
|---|---|---|---|
KLIO_COMPOSE_DBG |
set | One activation summary line (oracle sizes) plus group-emission debug inside the pass | [compose-pass] |
KLIO_COMPOSER_BIND_TRACE |
set | Each call that threads the $composer, $changed pair: the owning declaration and the composer's class (a non-Composer instance in the pair slot also dumps the frame chain) |
[composer-bind-fn], [composer-bind] |
KLIO_RSS_LOG |
set | Prints process RSS on each rendered Compose UI frame | [rss] |
KLIO_FJ_ESCAPE |
1 enables |
Lets a compiled body call back into the interpreter for one instruction instead of declining the whole function. Off: it widened acceptance with execution time unchanged, because an escaped instruction costs what the interpreter would have | — |
KLIO_FJ_SELF_INLINE |
0 disables |
The whole-function tier splices a self call (this.helper(...)) whose callee is scalar control flow over its own registers, reading and writing this only through the field sites. On by default: the receiver Move such a call needs is otherwise what makes the whole method uncompilable |
[jit] inlining self-call |
KLIO_FJ_DIRECT |
0 disables |
A self call the splice cannot take goes STRAIGHT into the callee's compiled code — a deopt-free method body over the same receiver, so no frame, no boxing, no host callback. On by default; without it such a call is a trampoline site, which also costs the caller its frameless seam | [jit] direct call |
KLIO_CTOR_TRACE |
set | Every secondary-constructor side-table lookup: the key, how many entries it found, and each entry's parameter/default counts. The table that decides whether a defaulted secondary constructor can take a call | [ctor] |
KLIO_RUN_STATS |
set | One line when the program's main returns: the boot/exec time split, RSS at main and at exit, RSS + mapped bytes after a forced final collection, and the live cell count that collection kept. Works the same for klio run, a bundle, and a transpiled binary, so the three are comparable |
[run-stats] |
./zig-out/bin/klio run scene.kt # plugin lowering
KLIO_COMPOSE_SKIP=0 ./zig-out/bin/klio run scene.kt # bisect the skip calculus
Compose UI and Skia¶
| Variable | Values | What it shows/does | Output tag |
|---|---|---|---|
KLIO_SKIA_LIB |
path | The Skia shim library to load at runtime (first in the search order) and to embed when bundling | none |
KLIO_SKIA_GPU |
set | Requests a GPU (Ganesh+EGL) surface for offscreen render; falls back to raster on failure | none |
KLIO_SKIA_VERBOSE |
set | One-line backend notes: window backend chosen, dump writeback result | [klio-skia] |
KLIO_SKIA_DUMP |
path | Writes the first presented GPU frame to the given PNG path (Metal builds) | none |
KLIO_SKIA_FONT |
path | Typeface override for text painting (checked before the bundled and system fonts) | none |
KLIO_COMPOSE_DEBUG |
set | Traces the SDL+GL / Skia GPU-window bring-up path in the C++ shim | [klio-compose] |
KLIO_PARA_TRACE |
set | Traces SkParagraph text-layout construction (font/unicode readiness, lengths) | [para] |
KLIO_DRAW_TRACE |
set | Each canvas rect draw with its surface, geometry, and color | [draw] |
Performance profile, JIT, and profiler¶
The profile itself (--opt / KLIO_OPT) is documented in
Performance; the granular
variables override individual fields on top of it.
| Variable | Values | What it shows/does | Output tag |
|---|---|---|---|
KLIO_OPT |
fast/safe/off (aliases: full, on, balanced, none, interp) |
Selects the performance profile: JIT tiers plus memory backend. klio run defaults to fast, klio test to safe |
none |
KLIO_JIT |
1 on, 0 off |
Loop-tier JIT override on top of the profile (on by default under fast) |
none |
KLIO_FLAT |
0 off (default on) |
The flat call driver; 0 falls back to native recursion for every call (bisect) |
none |
KLIO_FLAT_VCALL |
0 off (default on) |
The fused virtual flat path; 0 keeps slot-bound and lowering-resolved member calls on the recursive invoker (bisect) |
none |
KLIO_MEMBER_SITE |
0 off (default on) |
The CallMember instruction-site memo — the by-name replay path; 0 disables for bisection |
none |
KLIO_NATIVE_TRACE |
set | In a transpiled binary (klio transpile + libklio_rt): one line per native activation with its entry block and outcome, plus a miss line for user-package functions with no (or an fqn-guarded) native table entry — the engagement oracle: output parity alone cannot distinguish native execution from silent fallback to interpretation |
[native], [native-miss] |
KLIO_CM_TRACE |
<member name> |
At every CallMember execution of that name: the executing frame, whether lowering resolved it, and the full enclosing-this chain with entry kinds — the receiver-visibility debugger for member-extension dispatch |
[cmarm] |
KLIO_GF_TRACE |
<substr of a field name> |
Every GetField execution whose field name matches: field, receiver class, executing frame — pairs with KLIO_CM_TRACE to separate a wrong read from a wrong dispatch |
[gfarm] |
KLIO_RSEL_TRACE |
set | Every compatibility receiver re-selection at a receiver-lambda invoke: the recorded head, the passed receiver, and what was selected | [rsel] |
KLIO_FUNC_JIT |
1 on, 0 off |
Whole-function JIT override; turning it on also forces the loop tier on | none |
KLIO_JIT_DEBUG |
set; 0/empty off |
Per-decision JIT tracing: compile, bail, inline, evict | [jit] |
KLIO_RECLAIM |
gc, arena, smp/free/1, debug, 0 |
Memory backend override (and whether refcount teardown is active); the profile default is the tracing GC | none |
KLIO_PROF |
set; value = sampling interval in microseconds (default 1000, floor 100) | Statistical SIGPROF profiler; prints a by-function sample histogram to stderr at the end of the run (Linux) | [prof] |
KLIO_PROF_ALL |
set (needs KLIO_PROF) |
Widens profiling to the whole process, including startup and image decode | [prof] |
KLIO_PROF_CALLERS |
<substr> |
After the histogram, folds the callers of every sampled leaf whose name contains the substring | [prof] |
KLIO_OP_PROF |
set; value = sampling interval in microseconds (default 1000, floor 100) | Opcode sampler: a SIGPROF histogram over the interpreter's currently executing opcode tag (with host-route sub-tags), printed at the end of the run as self-time by opcode | [op-prof] |
KLIO_FN_PROF |
set; value = sampling interval in microseconds (default 1000, floor 100) | Kotlin-function sampler: a SIGPROF histogram over the INTERPRETED program's currently executing function, printed as self-time per Kotlin function. KLIO_PROF attributes time to interpreter internals; this one names the library body to serve or splice. Self-time excludes a callee only when the callee gets its own frame: a leaf- or bytecode-served callee is attributed to its caller |
[fn-prof] |
KLIO_FRAME_COUNT / KLIO_FRAME_CENSUS / KLIO_FRAME_WATCH=<substr> |
set / substring | How many interpreted activations a workload runs (activations = register-bank acquisitions, one per real frame; entries = runFrameExec entries, higher because a flat call re-enters its caller's frame at the return block), with _CENSUS the top functions by activation count and _WATCH a line per activation of a matching function naming its caller. The frames-per-unit metric that separates "too many frames" from "frames too expensive" |
[frames], [framewatch] |
KLIO_CALL_STATS |
set | Counts every interpreted function invocation by FQN over the whole run; klio test prints the top entries after the summary. The workload census that separates "slow per call" from "more calls than the reference would make" (missed skipping, repeated recompose, un-inlined accessors) |
[call-stats] |
KLIO_CALLVALUE_TRACE |
set | Flat closure-call preparation on the value-call path: per-argument kinds and under-application | [flat-prep], [cvt-flat] |
KLIO_DUMP_FN |
<name> or a numeric FuncId |
Prints the named function's lowered instruction stream the first time it runs (and its block table at startup) — the only way to see what an emit path produced for a body inside a baked pack | [dumpfn] |
Memory: GC, allocators, and leak tracking¶
The KLIO_GC_* family, KLIO_GC_ALLOC, KLIO_LEAK_BY_FQN, and the
slab tracers take effect only when the run uses the tracing GC
backend (the default for fast/safe).
| Variable | Values | What it shows/does | Output tag |
|---|---|---|---|
KLIO_GC_DEBUG |
set; 0/empty off |
One summary line per collection: epoch, kind (minor/major), marked, live bytes, freed | [kgc] |
KLIO_GC_GEN |
1 on (default), 0 off |
Generational collection: minor (nursery-only) sweeps between Appel-scheduled majors; 0 forces every collection major |
none |
KLIO_GC_GROWTH |
integer, min 2 (default 2) | The Appel growth multiplier: the next collection fires after live * factor bytes |
none |
KLIO_GC_MINOR_STOP |
1 on (default), 0 off |
Whether a minor mark stops at tenured cells; 0 full-traces minors to bisect a missed-barrier suspicion |
none |
KLIO_GC_REMEMBER_TRACE |
set | Logs, with a native stack, any remembered-set cell as it is swept | [gc-freed-remembered] |
KLIO_GC_HIST |
set; 0/empty off |
Top-16 live-cell payload types per collection | [kgc-hist] |
KLIO_ENUM_INIT_TRACE |
set | VM-start enum entry construction: which entries are rebuilt through the class path and the header thunk chain per class | [enum-init], [chain] |
KLIO_CTOR_TRACE |
set | Each secondary-constructor default-argument thunk as it is evaluated (class, parameter, thunk id, argument count) | [ctor-default] |
KLIO_TOPPROP_TRACE |
set | A top-level property initializer that deferred to on-access during the startup pass, with its error tag | [topprop-defer] |
KLIO_BOX_FILTER / KLIO_BOX_JOBS / KLIO_BOX_TIMEOUT_MS |
substring / count / ms | The box conformance runner's test subset, worker width, and per-test wall | [box-fail], [box-excluded] |
KLIO_GC_STRESS |
set; 0/empty off |
Collects at every safe point; surfaces incomplete roots/tracers immediately | none |
KLIO_GC_STRESS_EVERY |
number (0 off) |
Collects every N safe points (cheaper sampled stress) | none |
KLIO_GC_THRESHOLD_KB |
KiB (default 8192) | The collection-trigger floor; a small floor collects frequently | none |
KLIO_GC_NOFREE |
set; 0/empty off |
Marks fully but never frees; if a crash disappears, it was a premature free, not a marking bug | none |
KLIO_GC_POISON |
set; 0/empty off |
Quarantines swept cells and traps the next trace through one, naming the swept-while-live type | [GC-POISON] (panics) |
KLIO_GC_GUARD |
set, or dbg |
Panics on absurd (>1 MB) allocations after program start, the signature of reading a corrupted length from a swept buffer; dbg uses the checking allocator instead |
panic |
KLIO_GC_EXT |
1 on, 0/empty off (default on) |
Counts external frame/snapshot heap growth toward the GC trigger | none |
KLIO_GC_ALLOC |
slab (default), smp, gpa, calloc, leaktrack |
The freeing backend the collector frees into; leaktrack wraps the slab in the leak locator and reports at exit |
[leaktrack] |
KLIO_LEAK_BY_FQN |
set (needs KLIO_GC_ALLOC=leaktrack) |
Attributes outstanding allocations by intrinsic FQN instead of by stack (much cheaper) | [leaktrack-by-fqn] |
KLIO_RC_DETECT |
set; 0/empty off |
Refcount double-free detector: leaks control blocks so a second decrement is observable, then dumps a stack trace | [RC DOUBLE-FREE] |
KLIO_BOXDIE_TRACE |
set | Logs, with a native stack, a boxed List view whose box dies while a backing value is still attached | [boxdie] |
KLIO_ALLOC_TRACK |
set; 0/empty off |
Global allocation counters, a size histogram, and named phase snapshots; whole-process report at exit | [alloc-track] |
KLIO_PAGE_TRACE |
set; 0/empty off |
Histogram of direct page allocations, with stacks for the 96 KB to 160 KB window | [page-trace] |
KLIO_SLAB_STAT |
set | Total bytes currently mapped from the OS, printed at exit | [slab] |
KLIO_SLAB_TRACE |
set | Capture stacks of every live slab/large mmap, dumped at exit or on SIGTERM/SIGINT | [slabtrace] |
KLIO_CELL_TRACE |
set | Sampled tracking of live small slab cells with their allocation stacks | [slabtrace] |
KLIO_DECODE_STATS |
set | Per-type decoded bytes/nodes while loading a stdlib/module image, top 25 by bytes | [decode-stats] |
KLIO_RSS_CAP_KB |
KiB (default 6 GiB) | The RSS watchdog cap; the process aborts the moment RSS exceeds it, forestalling the kernel OOM killer. 0/unset keeps the default (it does not disable the watchdog) |
[klio] |
KLIO_PARITY_RSS_CAP_KB |
KiB | Legacy alias for KLIO_RSS_CAP_KB, consulted only when the primary is unset |
[klio] |
Stdlib, packs, and bundles¶
The stdlib pack resolution order and the image cache are described in the CLI tour; these are the overrides and traces.
| Variable | Values | What it shows/does | Output tag |
|---|---|---|---|
KLIO_HOME |
path | The klio data home (packs, cache, registry, stubs); overrides the ~/.klio default |
none |
KLIO_STDLIB_PACK |
path | On-disk stdlib pack override, first in the resolution order (also folded into the image cache key) | none |
KLIO_STDLIB_IMAGE |
0 disables |
The stdlib image cache; disabled, every run lowers the full dependency set | none |
KLIO_TRACE_STDLIB_IMAGE |
set; 0/empty off |
One hit/baked/fallback line per run with the cache key and timing |
[stdlib-image] |
KLIO_PACK_DIAG |
set | Disables the image cache so the legacy loader runs, and turns on its diagnostics (per-source lex/parse error dumps) | [embed lex err] |
KLIO_AST_REBASE_TRACE |
set | Old-to-new FileId mapping when a cached AST bundle's spans are rebased | [ast-rebase] |
KLIO_BUNDLE_INSPECT |
1 (0 off) |
A bundled executable prints its manifest and payload table, then exits without running | manifest listing |
KLIO_BUNDLE_PROGRAM_IMAGE |
0 disables (default on) |
Whether bundling attempts the whole-program image bake; 0 forces the program-source boot path |
none |
KLIO_STUB_DIR |
directory | Local source for cross-target runtime stubs and Skia shims (<dir>/<target>/<name>), checked before the download cache |
none |
KLIO_STDLIB_CHECK |
0 disables |
Checking the stdlib base's own sources while an image is built (publishes extern decls and eager call resolutions for the checker) | none |
KLIO_ENUM_INIT_TRACE |
set | Names any enum-instance field APPENDED rather than replaced in place during baked-enum init — the signature of a bake that dropped a field | [enum-init-append] |
Libraries and the front end¶
| Variable | Values | What it shows/does | Output tag |
|---|---|---|---|
KLIO_TRACE_HTTP |
set | Logs each outbound ktor HTTP request (method + URL) before the transport runs | [HTTP] |
KLIO_SERVE_MAX |
number (0/unset unlimited) |
Caps the embedded ktor server to N requests so a leak-checking run reaches its exit report | none |
KLIO_DOLLAR_TRACE |
set | Lexer trace for multi-dollar string-template arming (file, position, source window) | [dollar-arm] |
KLIO_REPEAT_DBG |
set | The String.repeat intrinsic's argument tags per call |
[srep] |
KLIO_SEQ_DIAG |
set | A sequence drain whose iterator lacks hasNext, with the iterator's kind and FQN |
[seq-diag] |
KLIO_UCTOR_TRACE |
set | An unsigned constructor refusing its argument shape, with the argument tags and a frame dump | [uctor] |
Test harness and dev tooling¶
These are honored by the itest binaries, the parity harness, and
the scripts, not by klio run itself.
| Variable | Values | What it shows/does | Output tag |
|---|---|---|---|
KLIO_ITEST_BIN |
path (default zig-out/bin/klio) |
The klio binary child-spawning itests run; zig build points it at the installed harness |
none |
KLIO_ITEST_VERBOSE |
set | Surfaces the differential itest's otherwise-suppressed progress lines | none |
KLIO_TEST_FILE_TRACE |
set | klio test prints each selected file's path-to-FileId mapping |
[test-file] |
KLIO_COMMONTEST_SHARD |
K/N |
Runs shard K of N of the commontest target list (weighted split; set by CI) | none |
KLIO_E2E_SHARD |
K/N |
Runs only corpus programs whose name hashes into shard K of N | none |
KLIO_E2E_FILTER |
<substr> |
Restricts the e2e corpus to programs whose file stem contains the substring | none |
KLIO_E2E_TRACE |
set | One line per e2e program as it runs, with the JIT state | e2e RUN |
KLIO_TRACE_STDLIB_BASE |
set; 0/empty off (Linux) |
One fast/fallback line per program: was the baked dependency base reused or rebuilt |
[stdlib-base] |
KLIO_PARITY_BASE_IMAGES |
directory | Where the parity/e2e/bench harness loads the baked base images from (zig-out/parity-base when running an itest binary by hand) |
none |
KLIO_PARITY_JOBS |
number (default: CPU count, cap 6) | Parity sweep worker count | none |
KLIO_PARITY_JAVA_XMX_MB |
MB (default 2048) | JVM heap ceiling for the kotlinc oracle | none |
KLIO_PARITY_JAVA_TIMEOUT_SECS |
seconds (default 60) | Wall-clock timeout for the kotlinc oracle | none |
KLIO_KOTLINC_JVM_HOME |
path | Existing JVM kotlinc distribution (or binary) for the parity oracle | none |
KLIO_KOTLINC_NATIVE |
path | Native kotlinc override | none |
KONAN_DATA_DIR |
path (default ~/.konan) |
Where the parity harness looks for Kotlin/Native distributions | none |
KLIO_NO_AUTO_INSTALL_KOTLINC |
1 (0 off) |
Never auto-install kotlinc; parity tests skip when none is found | none |
KLIO_SKIP_KOTLINC_PARITY |
1 (0 off) |
Skips the kotlinc leg of parity/differential checks entirely | none |
KLIO_FUZZ_SEED |
u64, decimal or 0x hex |
Base seed for the closures/suspend fuzzer (a failure prints the repro seed) | none |
KLIO_FUZZ_SEEDS |
u64 | How many seeds the fuzzer sweeps | none |
KLIO_SWEEP_DEBUG |
set | scripts/commontest-sweep.py prints each child's argv before spawning |
ARGV |
KLIO_BIN |
path | The binary scripts/klio-smoke.sh sweeps with |
none |
KLIO_SKIA_OS / KLIO_SKIA_ARCH |
linux/macos/windows, x64/arm64 |
Target selection for scripts/fetch-skia.sh |
none |
Harness / test-infrastructure variables¶
Plumbing read only by test code — not user debugging knobs.
| Variable | Values | What it does |
|---|---|---|
KLIO_ITEST_WALL_CAP |
seconds (default 60; 0 off) |
Per-program wall cap for the in-process itest harnesses; a spinning program fails "test wall-clock deadline exceeded" and names itself instead of hanging the binary |
KLIO_ENVONCE_SELFTEST_A / KLIO_ENVONCE_SELFTEST_B |
never set | Sentinel names the envOnce unit tests probe to prove distinct cache slots and unset-miss behavior |
KLIO_DEFINITELY_NOT_SET_XYZZY |
never set | Sentinel name the proc_env isSet unit test probes |
Workflow recipes¶
Tracing a coroutine hang. Turn on the pump and resume traces,
cap runTest so the hang fails fast, and add a spin dump in case
the hang is a busy loop rather than a parked pump:
KLIO_PUMP_DIAG=1 KLIO_RESUME_TRACE=1 KLIO_SPIN_TRACE=10 \
kotlinx_coroutines_test_default_timeout=10s \
./zig-out/bin/klio test kotlin-klio/klio-kotlinx-coroutines --filter FlowTest
Read the [tok] lifecycle to see which continuation parked and was
never taken; [resume-frame] lines show every frame each resume
re-ran and by which route.
Tracing a wrong overload pick. Pair the static and dynamic views for the one name that misbehaves:
KLIO_BARE_TRACE=encodeToString KLIO_MISS_TRACE=encodeToString \
KLIO_NU_TRACE=encodeToString \
./zig-out/bin/klio run repro.kt
[bare] shows what lowering bound (or NONE); the [extfb] /
[member-miss] tail shows which runtime candidates were skipped and
why; [strictext] / [mev] add visibility detail. Add
KLIO_CMG_TRACE=<name> for the dispatch preconditions at the call
instruction.
Root-causing an exception. When a failure surfaces as a bare error with no trace, or a teardown masks the original throw:
[throw-trace] names every throw as it happens (first one is
usually the root cause), [errtrace] dumps the frame chain, and in
klio test the failure detail becomes the fully rendered throwable.
Bisecting the compose plugin. The plugin always runs; bisect its two emissions:
./zig-out/bin/klio run scene.kt # plugin (always on)
KLIO_COMPOSE_SKIP=0 ./zig-out/bin/klio run scene.kt # no skip calculus
Rebuild any baked pack between flips (the flag is part of the pack
cache key), and add KLIO_COMPOSE_DBG=1 to confirm the pass
activated.
Per-thread state and the owner fast path¶
Darwin resolves every threadlocal access through a _tlv_get_addr CALL rather
than a register-relative load, and LLVM can only hoist that call within a
function — so in an interpreter the cost lands on every hot helper. It measured
25% of samples on a member-call loop, with the JIT on and off alike.
src/runtime/tls_fast.zig answers it: the thread that calls claimOwner() at
process entry reads the hot per-thread structures (the fused walker's banks,
host_fields' caches, the keepalive stack) from ordinary globals, and every
other thread keeps its threadlocal. The owner never changes, so no state
migrates between the two storages — a thread reads the same object for the
process's life. A binary that never claims an owner (the test harnesses) behaves
exactly as before.
Two things to know before extending it:
- It is NOT a win everywhere. The evaluator's own
EvalTlsis read on the JIT's per-call seam, where the compare that replaces the call costs more than the call did; it is deliberately left a plain threadlocal, and the comment at its declaration says so with the numbers. - Grouping threadlocals into one struct does nothing on its own. The compiler already reuses a repeated access within a function; the cost is one resolution per hot helper CALL, so only removing the resolution helps.
Measuring peak RSS (and the spin-loop trap)¶
Use scripts/measure-rss.sh -- <cmd>; it prints PEAK_KB=<n> RC=<rc>
WALL=<s>. For zig build itest-* you usually do not need it at all — the
build runner already prints MaxRSS: per run step under --summary all.
Do NOT hand-roll the sampler. The obvious inline version leaks a process that spins forever:
( <cmd> ) &
TP=$(pgrep -f "<name>" | head -1) # matches THIS shell too
while kill -0 $TP; do ...; sleep 10; done # never terminates
pgrep -f matches full command lines, and the wrapper's own command line
contains the pattern, so TP is the wrapper itself, kill -0 is always
true, and the loop runs at 0% CPU with a sleep child until something
reaps it. Three of these leaked in one session, two for sixteen hours,
because a run that prints nothing reads as "the measurement failed" rather
than "it is still going".
measure-rss.sh removes both failure modes by construction: the PID comes
from $! so no name matching happens, the loop is bounded by --max-wall
(default 900s) as well as by the child exiting, and an EXIT trap kills the
child on every path out.
The same self-match bites VERIFICATION code: pgrep -f "sleep 600" run
from a shell whose command line contains sleep 600 reports a leak that
is not there. Check with ps -eo pid,comm,args and match on comm
instead.