From 58c3d9cac66494628fed22250258eac25995e781 Mon Sep 17 00:00:00 2001 From: Reuben Brooks Date: Mon, 27 Jul 2026 15:18:08 -0500 Subject: [PATCH] fasl: record shen.*lambdatable* deltas by name instead of serializing live lambdas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the S41.2 kernel every (define ...) in a loaded file runs shen.update-lambdatable, which (set)s shen.*lambdatable* to an assoc list whose entries hold live curried lambda functions. The fasl recorder captured that as an ordinary "g" (set) record, kdata_ser hit the closure ("unserializable KDATA value: function"), and the whole file became fasl-uncacheable — in practice ~every stdlib and user file, so the ~1s stdlib load re-compiled on every boot and every (load) paid full reader+macro+typecheck cost on every run. Record a new "lt" record instead: diff the new table against the current global (the recorder runs before the original set, so the old table is still live) and store only the NAMES whose entry was added/replaced. On replay, rebuild each entry from the live defun the same way shen.update-lambdatable does — shen.lambda-entry (which reads the arity property, already replayed by the preceding "p" record in stream order) + shen.assoc->. Mirrors the existing "lf" special case for the older kernel's shen.lambda-form put path. Bump FASL_FORMAT to SHENFASL5 so stale caches miss cleanly. Measured (arm64 macOS, LuaJIT 2.1 rolling): trivial-script startup 1.5s -> 0.34s warm; all 21 StLib files now fasl-hit (previously 19/21 uncacheable). make test 500/0, run-kernel-tests 134/0, warm/cold suite stdout identical modulo the by-design 'run time' banner drop. Co-Authored-By: Claude Fable 5 --- boot.lua | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/boot.lua b/boot.lua index 7520f31..dadc2d6 100644 --- a/boot.lua +++ b/boot.lua @@ -520,7 +520,8 @@ end -- Known: (destroy ...) at the REPL between loads is not in the key. -- SHEN_FASL=off disables; SHEN_FASL_DIR overrides ~/.cache/shen-lua-fasl; -- SHEN_FASL_DEBUG=1 logs hits/misses to stderr. -local FASL_FORMAT = "SHENFASL4" -- 4: added "e" (per-form value/type echo) +local FASL_FORMAT = "SHENFASL5" -- 5: "lt" (shen.*lambdatable* delta by name); + -- 4: "e" (per-form value/type echo) -- records; 3: "pc" (shen.compile-prolog) local FASL_STACK = {} local FASL_ROLL = 2166136261 @@ -569,6 +570,8 @@ end -- | M\n shen.record-macro (fn rebuilt by name) -- | P\n (put ... *property-vector*) -- | E\n #bytes\n bytes per-form value/type echo (stoutput) +-- | A\n shen.*lambdatable* delta (entries +-- rebuilt by name via shen.lambda-entry) -- | G\n }* (set ...) outside any chunk -- narity\n {ar SP name\n}* kbase\n nkdata\n entries gensym\n local function fasl_write(path, rec, arity0) @@ -588,6 +591,9 @@ local function fasl_write(path, rec, arity0) elseif r.k == "lf" then parts[#parts+1] = "L\n" kdata_ser(r.name, parts) + elseif r.k == "lt" then + parts[#parts+1] = "A\n" + kdata_ser(r.names, parts) elseif r.k == "dt" then parts[#parts+1] = "T\n" kdata_ser(r.name, parts) @@ -670,6 +676,9 @@ local function fasl_read(path) elseif k == "L" then local v = de_n(1); if not v then return nil end recs[i] = { k = "lf", name = v[1] } + elseif k == "A" then + local v = de_n(1); if not v then return nil end + recs[i] = { k = "lt", names = v[1] } elseif k == "T" then local v = de_n(2); if not v then return nil end recs[i] = { k = "dt", name = v[1], rules = v[2] } @@ -729,6 +738,23 @@ local function fasl_replay(cached) local val = R.is_cons(entry) and entry[2] or entry P.F["put"](r.name, R.intern("shen.lambda-form"), val, P.GLOBALS["*property-vector*"]) + elseif r.k == "lt" then + -- 41.2 kernel path: (set shen.*lambdatable* ...) carries live curried + -- lambdas, so the recording stored only the NAMES whose entries the set + -- added/replaced. Rebuild each entry from the live defun exactly the way + -- shen.update-lambdatable does (shen.lambda-entry reads the arity + -- property, replayed by the preceding "p" record in stream order). + local names = r.names + while R.is_cons(names) do + local name = names[1] + local entry = P.F["shen.lambda-entry"](name) + if R.is_cons(entry) then + P.F["set"](R.intern("shen.*lambdatable*"), + P.F["shen.assoc->"](name, entry[2], + P.GLOBALS["shen.*lambdatable*"])) + end + names = names[2] + end elseif r.k == "dt" then P.F["shen.process-datatype"](r.name, r.rules) elseif r.k == "sy" then @@ -848,6 +874,32 @@ local function install_fasl() -- the gensym counter churns on every expansion-time gensym; the replay -- fast-forward (max) covers it without hundreds of noise records if nm == "shen.*gensym*" then return nil end + if nm == "shen.*lambdatable*" then + -- 41.2 kernel: shen.update-lambdatable / update-lambda-table set the + -- whole assoc list, whose entries are (name . live-curried-lambda) — + -- unserializable, and the reason every stdlib/user load used to be + -- fasl-uncacheable. The recorder runs BEFORE the original set, so the + -- global still holds the old table: diff it against `val` and record + -- only the names whose entry was added/replaced (an "lt" record; the + -- fns are rebuilt on replay via shen.lambda-entry). + local old = {} + local t = P.GLOBALS["shen.*lambdatable*"] + while R.is_cons(t) do + local e = t[1] + if R.is_cons(e) and R.is_symbol(e[1]) then old[e[1].name] = e[2] end + t = t[2] + end + local names = R.NIL + t = val + while R.is_cons(t) do + local e = t[1] + if R.is_cons(e) and R.is_symbol(e[1]) and old[e[1].name] ~= e[2] then + names = R.cons(e[1], names) + end + t = t[2] + end + return { k = "lt", names = names } + end return { k = "g", name = name, val = val } end)