perf: cache the recomputable half of a warm boot; standard-library boot image (#46) - #57
Merged
Conversation
Warm start-up was 0.16 s of which nothing was compilation: the kernel bytecode cache and the user fasl cache were both hitting, and the time went entirely on state that the caches had no way to express and so rebuilt from scratch on every single start. Four such items, each replaced by a cache of the state it produces: * klambda/types.kl's trailing 161 `(declare Name Type)` forms. `declare` runs the type theory for real on each one — shen.variancy under the Prolog machine plus (eval-kl (shen.prolog-abstraction Type)), a full KL->Lua compile and loadstring per signature — ~43 ms, the largest single item in a warm boot. The variance check is a static check of the kernel's own signatures against sources the cache key already covers, and the abstraction is deterministic, so hoist the block out of the concatenated chunk (only ever a contiguous TRAILING run, so no reordering is possible) and cache one dumped abstraction per signature. Cache format SHENKC2 -> SHENKC3, which also carries the gensym and inference counters `declare` would have advanced, so a cached boot is indistinguishable from an uncached one at the Shen level. * shen.process-datatype, re-executed by the fasl "dt" record because shen.*datatypes* entries hold unserializable closures. They are all (TypeName . (fn TypeName)) (sequent.kl shen.remember-datatype), so the new "dv" record stores the NAMES in order and rebuilds the list — whole-list, not a delta, so (preclude-all-but ...) shrinking the table replays correctly. The datatype's own work then replays from its recorded chunks: ~17 ms per datatype, 2 in the standard library. * shen.lambda-entry ran the whole compiler to produce a curried chain over a late-bound F lookup and nothing else — one chunk compile per lambda table entry, i.e. per `define` and per name a fasl replay rebuilds (285 in a warm stdlib load). Build the chain directly; the innermost call goes through APP on the name symbol, matching the codegen for a name whose arity is unknown at compile time. * shen.assoc->, the update primitive behind shen.*lambdatable*, shen.*sigf* and shen.*datatypes*, was the KL non-tail recursion: a stack frame and an F-table lookup per entry scanned, over lists several hundred entries long. Native iterative prefix copy. Semantics: cold and warm boots produce byte-identical shen.*sigf* (contents and order), shen.*lambdatable*, shen.*alldatatypes*, shen.*gensym* and (inferences), and both match main. Differential-tested shen.assoc-> and shen.lambda-entry against the compiled-KL originals. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Everything in the previous commit caches a PIECE of the boot. This caches the standard library PHASE — the closest a Lua host gets to shen-cl's save-lisp-and-die, which is why shen-cl starts in 0.01 s: it does not rebuild anything. A warm stdlib load was ~20 separate fasl hits plus the install.shen driver around them, and the driver is not free: reading install.shen through the kernel reader alone is ~12 ms, and the trailing (external stlib) / systemf / (preclude-all-but []) block another ~11 ms. No per-file cache can ever capture that, because it does not happen inside a file. The image records the ENTIRE span — driver forms and nested loads alike, in stream order — as one fasl record stream and replays it in one go. The nested loads splice their own streams into the enclosing recording (splice_into_outer), so a load that was itself a fasl hit still contributes its records; a fasl REPLAY is marked in-chunk for the enclosing span, since a replay drives the very functions the recorder wraps and would otherwise be captured a second time and captured wrong (it reinstates chunks through load_chunk, so it contributes no "c" records). A top-level (load "X") in install.shen is dispatched straight to `load` instead of through `eval` — what it compiles to anyway — so the image cannot contain a chunk that re-runs the load it replaced. Invalidation is by construction: the file name keys on the kernel key, the record format, the Prolog engine and the on-disk install.shen text, and the image itself carries the path and content hash of EVERY file the recorded span loaded, all of which are re-verified on each boot. The per-file fasl caches are still written on a miss, so an image that cannot be serialized costs nothing beyond not existing. FASL_ROLL is folded from the same contents in the same order, so a user program's fasl key is identical whether the stdlib came from the image or from its 20 files. SHEN_STDLIB_IMAGE=off disables it. A single-file bundle materialises the stdlib to a fresh temp directory every boot, so it opts out rather than leave a megabyte behind per run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
declarations.kl ends with (shen.build-lambda-table (external shen)), which maps shen.lambda-entry over ~280 external symbols — and shen.lambda-entry ran a full KL->Lua compile and loadstring per name. That one form was two thirds of what remained of a cached kernel load after the type-signature block was cached. The native shen.lambda-entry added in the previous commit could not help it: install_native_stdlib runs after the whole kernel is loaded, long after declarations.kl. So it moves to its own installer with no dependency on the compiled original (`arity` returns an integer or -1 for every input, so every branch is reachable natively), and boot.lua installs it at the point where declarations.kl has defined `arity` and is about to build the table. Reaching that point means the form has to be outside the file's concatenated chunk, so hoist_declares generalises to hoist_tail: a kernel file's trailing run of non-defun top-level forms becomes a separate ":init" chunk, cached like any other, run immediately after the body — so no reordering is possible — with the trailing declare block, if any, after it. Exactly two files in the 41.2 kernel have such a run: types.kl (the 161 signatures) and declarations.kl (this one form). Anything less tidy than [defuns][inits][declares] is left inline and cached as before. shen.*gensym* after boot is now lower than main's by the ~1300 names the skipped abstractions and lambda-function terms used to consume. The counter only ever grows within a session, so no name it can still produce was ever handed out; cached and uncached boots agree exactly, which is the invariant that matters. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…46) test/boot_cache_spec.lua (19 assertions) locks in the property that makes the whole series legitimate: a cached boot must be indistinguishable from an uncached one at the Shen level. It boots subprocesses in every cache configuration — nothing cached, kernel bytecode cache cold and warm, stdlib image miss and hit, image explicitly off — and diffs a state fingerprint covering shen.*sigf* by name AND order, the lambda table, both datatype tables, shen.*gensym*, (inferences), and a real typecheck of a user definition. It also asserts the image is genuinely exercised (an image hit is observed, not assumed) and that editing a standard-library file invalidates it, against a COPY of lib/StLib via SHEN_STDLIB_DIR so the checkout is never mutated. Part 2 differential-tests the two natives against the compiled-KL definitions they replace, compiled fresh out of klambda/ under an alias: shen.assoc-> over 11 shapes (empty, replace, append, duplicate keys, non-pair entries, numeric/string keys, improper tail, non-list) and shen.lambda-entry over 8 live arities including partial application and the arity 0 / unknown cases. README: the cache section becomes three caches, with what the bytecode cache now additionally carries, what the boot image is and how it invalidates, SHEN_STDLIB_IMAGE=off, and the cached-equals-uncached guarantee the new spec enforces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Warm start-up was ~0.16 s and, as issue #46's last re-measurement established, none of it was compilation — the kernel bytecode cache and the user fasl cache were both hitting. The time went on state that the caches had no way to express and therefore rebuilt from scratch on every single start. This PR caches that state.
What was actually being recomputed
Measured on arm64 LuaJIT 2.1.1774638290, warm,
os.clockinside the process (a quiet-machine reference boot:require0.002 +load_kernel0.046 +initialise0.095 = 0.143 s):klambda/types.kl's 161(declare …)declareruns the type theory for real:shen.variancyunder the Prolog machine +(eval-kl (shen.prolog-abstraction Type)), a full KL→Lua compile andloadstringper signature(shen.build-lambda-table (external shen))shen.lambda-entryruns the whole compiler once per external symbol (~280)shen.process-datatype×2 (stdlib)"dt"record re-ran theshen.<datatype>yacc parser + type theory, becauseshen.*datatypes*entries hold closuresshen.*lambdatable*rebuild ×285shen.lambda-entrycompile per entry, eachshen.assoc->'d into a 568-entry list by non-tail KL recursioninstall.shendriver(external stlib)/systemf/preclude-all-butblock — outside any file, so no per-file cache could ever hold itWhat this PR does
Five changes, each replacing a recomputation with a cache of the state it produces.
1. Kernel type signatures in the bytecode cache (
SHENKC2→SHENKC3). types.kl's trailing declare block is hoisted out of the concatenated chunk soboot.luaruns it, and one dumped prolog abstraction per signature goes in the cache. The variance check is a static check of the kernel's own signatures against sources the cache key already covers; the abstraction is deterministic. Only ever a contiguous trailing run is hoisted, so hoisting cannot reorder a file's effects.2.
"dv"fasl record replaces"dt".shen.*datatypes*/shen.*alldatatypes*entries are all(TypeName . (fn TypeName))(sequent.klshen.remember-datatype), so the record stores the names in order and rebuilds the list; the datatype's own work then replays from its recorded chunks. Whole-list rather than a delta, so(preclude-all-but [])shrinking the table replays correctly.3. Native
shen.lambda-entryandshen.assoc->.shen.lambda-entrycompiled a Lua chunk to produce a curried chain over a late-boundFlookup and nothing else — built directly instead. It is installed early (its own installer, no dependency on the compiled original) so it is in place beforedeclarations.klbuilds the lambda table; reaching that point is what generaliseshoist_declaresintohoist_tail, which puts a file's trailing non-defun run into a separate":init"chunk.shen.assoc->was non-tail KL recursion over lists hundreds of entries long — native iterative prefix copy.4. Standard-library boot image. The whole stdlib phase — install.shen's own forms and the ~20 nested loads alike — is recorded as one fasl record stream and replayed in one go: the nearest a Lua host gets to shen-cl's
save-lisp-and-die, which is the actual reason shen-cl starts in 0.01 s. Nested loads splice their record streams into the enclosing recording; a fasl replay is marked in-chunk for the enclosing span, since a replay drives the very functions the recorder wraps.Invalidation and semantics
The image is keyed on the kernel key + record format + Prolog engine + the on-disk
install.shen, and it stores the path and content hash of every file the recorded span loaded, all re-verified on each boot. The per-file fasl caches are still written on a miss, so an image that cannot be serialized costs nothing beyond not existing.FASL_ROLLis folded from the same contents in the same order, so a user program's fasl key is identical either way.SHEN_STDLIB_IMAGE=offdisables it; a single-file bundle opts out (it materialises the stdlib to a fresh temp dir each boot).A cached boot is indistinguishable from an uncached one at the Shen level: same
shen.*sigf*contents and order, same lambda table, same datatypes, sameshen.*gensym*and(inferences), same typechecking.shen.*gensym*is lower than main's by the ~1300 names the skipped abstractions used to consume — the counter only grows within a session, so no name it can still produce was ever handed out, and cached/uncached agree exactly, which is the invariant that matters.Results
Interleaved A/B against
main@ 67e2f43, min-of-N child CPU. This box has heavy, bursty background load (load average 13-14 during these runs), which inflates every absolute number by roughly 2×; the ratios are what hold. Reference quiet-machine numbers formainreproduce issue #46's 0.154 s.bin/shen -e '(output "hi~%")', warm, min of 40 interleaved:scriptIn-process phase split, min of 25 interleaved:
load_kernelinitialise(stdlib)Cold start (
rmboth caches first, min of 5) is unchanged: 1.89 s → 1.93 s CPU — the cold path does the same work plus writing the image.Gates
luajit run-kernel-tests.lua— 134 passed / 0 failed, okmake test— 896 pass / 0 fail across 19 specs (19 new)luajit examples/{openresty,openresty-authz,envoy}/selftest.lua— all OKpython3 bifrost.py --impls shen-lua— 31 pass, 0 diverge, 0 FAILbuild/make-bundle.luarebuilt and exercised (kernel cache format changed)New
test/boot_cache_spec.lua(19 assertions) pins cached-boot == uncached-boot across every configuration, asserts the image is genuinely hit (not vacuously skipped), asserts editing a standard-library file invalidates it (against a copy viaSHEN_STDLIB_DIR, so the checkout is never mutated), and differential-tests both natives against the compiled-KL definitions compiled fresh out ofklambda/under an alias.