Repository navigation
Conversation
A datatype rule with six or more premises over another user type compiled a shen.consume helper whose continuation captured 17 or more locals (two per premise variable, plus the assumption and engine temps). mkhandle refused past 16 captures, so the helper was never registered in NativePred and the caller died with "shen.consumeN is undefined". Five premises captured 15 and fit NEWCONT16. Wider handles now use the existing NEWCONTV path. Co-authored-by: pyrex41 <pyrex41@users.noreply.github.com>
cursor Bot
pushed a commit
to pyrex41/unii
that referenced
this pull request
Oct 9, 2026
* README.md: what unii is, clone with submodules, every suite's command. * LICENSE: BSD-3-Clause, the terms unii had inside shen-lua. * HANDOFF.md: provenance and preserved history, a Runtime pin section (submodule, flake input, manifest; how the pin is verified), root-relative paths and module names, layout, verification counts, and that pyrex41/shen-lua#78 fixes the six-premise datatype bug so the nested-record workaround can be dropped once the pin moves past it (pin not moved). * MANIFEST.md: pin mechanisms and flake narHash, dev shell contents, test tool dependencies; correct the stale bundle hash and storage_format. * Contracts and READMEs: paths relative to the new root; shen-lua PR references qualified so they link to shen-lua. Co-authored-by: pyrex41 <pyrex41@users.noreply.github.com>
An inline freeze-let thawed inside a value-position control form is expanded inside the hoisted KC function with the freeze's own env, but the KC took only the control form's free variables as parameters. A freeze variable the form did not mention itself resolved to a global: (let G (freeze Z) (let Y (if (= X 0) (thaw G) 5) Y)) returned nil for X = 0. kc_params now adds the free variables of every inline record the form thaws (transitively). Kernel codegen is unchanged. The KC hoisting is factored into hoist_kc for reuse. Co-authored-by: pyrex41 <pyrex41@users.noreply.github.com>
ctail emitted one Lua local per let, so a single function with a long
let chain failed to load ("function at line 1 has more than 200 local
variables"). The kernel's pattern-matching KL for a sequent rule spends
about six lets per premise, so a datatype rule with ~30 premises over a
user type failed to load at all.
Each Lua function now tracks the locals it has declared (params, lets,
chain-hoist steps, snapshot captures, fN refs). Past 120, the rest of the
let chain continues in a fresh KC function via a proper tail call
(return KC[i](live vars)). The split is refused while a goto-kind
failure continuation is active (its label must stay in this function)
or when the continuation itself would need 120+ parameters. Kernel
codegen is unchanged; no kernel function reaches the budget.
Co-authored-by: pyrex41 <pyrex41@users.noreply.github.com>
Two limits in the native Prolog translator left NativePred[shen.consumeN]
unset for wide sequent rules, so typechecks walking the rule died with
"shen.consumeN is undefined":
* every let chained a new env table via __index, and from 16 premises
the consume helper nested past LuaJIT's 100-table chain limit ("loop
in gettable"). extend_env collapses the chain every 32 levels into
one table with the same root lookup.
* the generated Lua then exceeded 200 locals in one function. When
loadstring reports that, the predicate is translated again with let
splitting: past 120 locals the rest of a let chain is lifted into a
static (the existing let-bound freeze closure conversion) and called.
Predicates that already loaded keep byte-identical code, so the
split costs nothing on existing hot paths.
Lifted statics and continuations now keep the vec/lock bindings in their
env, so a misuse refuses as it would in the enclosing scope instead of
compiling the name as a symbol. mkhandle and lift_cont share
newcont_ctor (NEWCONT0..16, NEWCONTV past that).
datatype_consume_spec covers 5..37 premises: every consume helper and
the rule translate natively, the last element is unpacked, and misses
still miss. engine_spec pins NEWCONTV's capture layout and undo.
Co-authored-by: pyrex41 <pyrex41@users.noreply.github.com>
A defun whose ordinary Lua does not load (200 locals, 60 upvalues, 250 registers) is recompiled once with captures passed as tables: hoisted bodies and freezes past 24 captures take one table, lambdas closing over more than 40 names become LAMW closures, let chains split at 80 locals whatever the live set, goto failure continuations stay freeze/thaw, flattened call chains reuse one local, and very wide calls pass UNPACK. Curried lambda chains deeper than 32 compile to CURRY in every mode. Code that loads today is unchanged. Co-authored-by: pyrex41 <pyrex41@users.noreply.github.com>
When the split translation still passes a Lua limit, translate once more with wide statics (free vars as one table), CAPREF reads in wide continuations, NEWCONTT handles, CONSL cons chains and lifted functions in one LT table. Predicates that translate today are unchanged. Co-authored-by: pyrex41 <pyrex41@users.noreply.github.com>
Co-authored-by: pyrex41 <pyrex41@users.noreply.github.com>
Merge origin/main into the wide-premise branch. compiler.lua and prims.lua combined cleanly: the string->n fusion and the Unicode primitives sit next to the wide-mode retry. The test runner lists both the new UTF-8 spec and the wide-compile spec. Co-authored-by: pyrex41 <pyrex41@users.noreply.github.com>
pyrex41
marked this pull request as ready for review
October 12, 2026 03:34
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.
How sequent premises compile (background for review)
Take a rule whose conclusion unpacks a list into N assumptions:
1. The rule becomes a Prolog predicate.
row#type Goal Hypsmakes N fresh Prolog variables W1..WN. It then calls a helper to find a matching hypothesis, and continues by provingGoalwith the N new assumptionsWi : pieceadded in front of the remaining hypotheses.2. The helper is
shen.consume<N>.shen.specialise-consume(kernelsequent.kl) gensyms one helper per rule. Its signature is(shen.consumeN Hyps Rest W1 .. WN B L K C), so it takes N+6 arguments: the hypothesis list, the leftover hypotheses, the N output variables, and the four engine arguments (bindings, lock, cut key, continuation). It has two clauses:[cons E1 [cons E2 ... []]] : row, onehd/tl/lazydereftest per list element. That is about 6 nestedlets per premise. It then binds the outputs in a chain:(bind W1 E1 .. (freeze (bind W2 E2 .. (freeze ... (bind Rest Tail .. C))))).(lambda Z1 (lambda Z2 ...))that closes over every argument.The kernel also registers a lambda entry for the helper: N+6 nested
lambdas around one call.3. Continuations are where the width comes from. Each
(freeze ...)in that bind chain is a continuation. When it runs, it needs every value the rest of the chain still uses: the remainingWiandEi,Rest,Tail, andB L K C. So the first freeze captures roughly 2N values, and with the helper's own arguments threaded through it is roughly 3N. The continuation thatrow#typehands toconsumeNcaptures all N fresh variables and builds the N-element assumption list.4. Two compilers, each with its own continuation representation. Every predicate is compiled twice:
compiler.lua, always used). It compiles the KLdefunto Lua. Afreezebecomes a constant functionKC[i] = function(cap1..capk) ... end. The use site isBIND(KC[i], cap1..capk), which snapshots the captures into a thunk. A value-positionif/letis hoisted the same way and called asKC[i](caps). Alambdabecomes a Lua closure, so its captures are upvalues.prolog_compile.lua, used by the typechecker). It translates the same KL into direct Lua over the integer term arena. A continuation is a handle into a capture buffer:NEWCONTk(fn, c1..ck)for k ≤ 16 andNEWCONTVabove that, read back withCAPREF(base, i). Statics and helpers are lifted to chunk-level functions that take their free variables as parameters.5. Where each limit came from. Every limit is a Lua or LuaJIT ceiling hit by a function or call that grows with N. The rows are in the order a growing rule reaches them:
mkhandlerefused more than 16NEWCONTV__indextable perletlets per premise in one functionCURRYrow#typebuilds the N-element assumption list as nestedCONScallsCONSL(wide mode)row#type's chain of Nshen.gccalls, flattened one local per frame(shen.consumeN ...)with N+6 argumentsUNPACK(wide mode)From 38 premises on, the first error on both compilers is the 250-register limit. The rows below it only appear once that is fixed. The upvalue limit (60 per closure, clause 2's lambda) and the native translator's cap of 150 lifted functions never surface first, because wide mode already handles them (
LAMW, anLTtable). None of this involves the Shen value representation. It is all about how many names one generated Lua function holds.Summary
Loading a Shen datatype rule with six or more premises over another user-defined type failed with
shen.consume<N> is undefined, and wider rules hit the limits above in turn. Rules now work up to 194 premises on both engines (previously 5). Generated Lua is unchanged for everything that compiled before.Rebased onto current main by merging it in (ShenOSKernel 42.2, #81, and the UTF-8 string primitives, #82).
shen.specialise-consumeandshen.consume-clauseare the same code as in 42.0 apart from gensym numbers andsimple-errorbecomingshen.f-erroron partial-function paths. The helper shape did not change, so none of the wide-mode work became unnecessary and none of it had to be rewritten for 42.2.Changes
Earlier commits (6 to 37 premises):
mkhandle→NEWCONTVpast 16 captures.NEWCONTVwrites the same contiguouscapbuflayout and the samecontFn/contBaseentries asNEWCONT0..16.letlevels, keeping the same root lookup.loadstringrejects a translated predicate for too many locals.letchain continues in a freshKCfunction via a tail call.Wide mode (38 to 194 premises):
Legacy wide mode (
compiler.lua,prims.lua).P.evalloads each defun as before. If Lua rejects the code for locals, upvalues, or "too complex", that one defun is recompiled withC.compile_top(form, true):Tand readsT[i]. Narrower hoists inside a wide body get fresh parameter names.T.BIND/THAWclosures instead of agotolabel, so no label pins a let chain to one function.LAMW(KC[i], {...}).UNPACK({...}, 1, n).Captured values are immutable KL bindings, so copying them into
Tat the use site observes exactly what the parameter form did.CURRYfor curried lambda chains deeper than 32, in every mode (no kernel chain is that deep). Each application returns a fresh closure over its own copy of the arguments so far.Native wide retry (
prolog_compile.lua,prolog_engine.lua). Used when the split translation still passes a Lua limit:CAPREF(base, i)at each use;NEWCONTT(fn, {...}), which has the same capture-buffer layout;CONSL({...}, tail);LTtable.The merge with main touched three files.
compiler.luaandprims.luacombined without conflict markers: thestring->nofpos/hdstrfusion and the Unicode string primitives sit next to the wide-mode retry.scripts/run-tests.luaneeded both new specs (utf8_strings_spec.luaandwide_compile_spec.lua).Performance (against current main, ShenOSKernel 42.2)
germanon both): main 3.75–5.31 ms, this branch 3.64–4.24 ms. Parity; the 5.31 ms figure is one noisy main run.Tests (on 42.2)
make test: 1511 pass, 0 fail across 25 specs (includestest/utf8_strings_spec.luafrom Make string primitives count Unicode code points (UTF-8) #82 andtest/wide_compile_spec.lua).make certify: 143 passed, 0 failed (the 42.2 suite).test/datatype_consume_spec.lua(insidemake test): 5, 6, 8, 12, 15, 16, 20, 24, 30, 32, 37, 38, 40, 64, 100, and 128 premises, on the native engine and the legacy KLrowN#type. 194 premises by hand: 16 pass, 0 fail. 195 still fails to load: the consume helper's own signature has 201 parameters (function at line 1 has more than 200 local variables).test/wide_compile_spec.lua: 27 checks. A 210-let chain with every binding live, the same chain under a two-thaw failure path, a lambda over 70 bindings, and a 40-deep curried chain. Each case first shows the ordinary code is rejected.test/engine_spec.lua:newcontTreads back and reclaims 1, 16, 49, and 261 captures;cons_listmatches nestedcons.Remaining limits
function(...)with the parameters in a table) for very wide defuns, in both compilers.