Repository navigation
fix(prolog): run deep proofs on fresh coroutine stacks (#85) - #90
Merged
Merged
Conversation
The native engine is CPS on the Lua stack: a successful proof returns only when the whole query has succeeded, so proof depth is Lua stack depth, and LuaJIT's stack is a fixed ~65500 slots. A typecheck overflowed at ~375 nested peano rules and at an ~800-element [list number]; the legacy engine reached ~1000 and shen-go has no limit. The issue's suggested fix (tail-call thawH from unify/bind1 and rely on choice points to unwind) does not hold: the translated predicates try their alternatives without trail marks or undo calls, so the undo inside unify_core and bind1 is what restores bindings on failure. The lock-on-fail frame in cut and the unlock after a clause sequence cannot be tail calls either. Instead, every coroutine has its own stack, so a deep proof continues on a fresh one (a segment): - unify, bind1 and cut count their non-tail thaws (inline, no extra frame); the translated predicates call the new counted E.thawN (THAWN) for every thaw that is not their return value, and tail_return keeps a thaw in return position a plain tail-called THAW, so constant-stack loops never count. - Every 96 counted frames the next thaw runs in a new coroutine; errors are rethrown unchanged and a host's yield is passed through. - query_end restores the count, which an error skips past. Past 1000 nested segments a query raises 'Prolog depth limit exceeded'. Typechecks of 1500-deep peano terms and 3000-element lists now succeed; deeper ones stop at the kernel's recursive shen.curry preprocessing (~2000 nested [s N] / ~4600 elements), which is outside the engine. Fixes #85 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
thaw_segment passed a host's coroutine.yield up through the segments, but two cases were wrong: - With no host coroutine to yield to, the error skipped the decrement of the nesting count in every suspended inner segment, so later deep queries had less headroom. - unpack(res, 2) truncated the yielded values at the first nil. Each segment now restores the count it saw on entry (by assignment, not a decrement), query_end restores it too, and values are packed with their count. New checks: a yield from 3000 levels deep reaches the host and resumes, and a yield with no host coroutine raises and leaves no segment open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 19, 2026
…depth # Conflicts: # CHANGELOG.md
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.
Fixes #85
Problem
The native Prolog engine runs in continuation-passing style on the Lua stack. A successful proof returns only after the whole query has succeeded, so proof depth equals Lua stack depth. LuaJIT's stack is fixed at about 65500 slots, and on main (e3dec8b) that gives these limits:
(shen.typecheck (mk 400) peano)fails withshen: stack overflow.[list number]fails the same way.Why not the issue's suggested fix
The issue suggests tail-calling
thawHfromunify/bind1and letting the choice points unwind. That doesn't hold. The translated predicates try their alternatives asif r == false then <next clause>, with no trail marks and no undo (dumpedpeano#type/shen.system-S), so the undo insideunify_coreandbind1is what restores bindings when a clause fails. Two other frames can't be tail calls either:cutsets the lock when its continuation fails.(unlock N)after failure.Removing these frames would change semantics.
Fix: stack segments
Every coroutine has its own Lua stack, so a deep proof now continues on a fresh one.
unify,bind1andcutcount their non-tail thaws. The counting is inlined, so no frame is added.E.thawN(THAWN) for every thaw that isn't their return value.tail_returnturns a thaw in return position back into a plain tail-calledTHAW. A tail call therefore never counts, and a constant-stack loop never opens a segment.thaw_segment). Errors are rethrown unchanged witherror(e, 0), so Shen exception objects survive. A host'scoroutine.yieldfrom inside is passed through to the enclosing coroutine.query_endrestores the count, because an error unwinds past the decrements. A nested query keeps counting its caller's stack.Prolog depth limit exceeded (1000 stack segments)instead of overflowing.E.segments()andE.segment_limits()are exposed for tests and diagnostics.Results (repros from the issue)
Run at head 8f50b50 with
luajit bin/shen --hush-load <file>:mkfrom the issue)shen: stack overflowpeano[list number])shen: stack overflow[list number][string "shen:mk"]:1: stack overflowmk, before the typecheck starts. shen-go has a growable stack. This is a general limit of the port.mk, typechecks now succeed up to 2000 nested[s N]and 4000 list elements. Past that they stop in the kernel's own recursive preprocessing, not in the engine. The frame histogram is 4673 ×shen.curry+ 4673 ×shen.map-h. That limit is the same class as Reader fails on list literals over ~1550 elements with a misleadingreader error near here:(suspected Lua stack overflow) #88's tree walkers and is outside this PR.e-draftworkload typechecks a system specification. On base it dies withshen: .../prolog_engine.lua:213: stack overflow. On this branch alone its payload is identical to shen-go 1f587de (sha256 bc80629a233e75af…).b-typedworkload also needs this fix as well as Generated Lua exceeds LuaJIT's 200-locals limit: datatype rules with nested conclusion patterns, list literals with 200+ elements #84 (fix: 200-locals shapes and long list literals (#84, #88; builds on #78) #91). Its typed-authority step wrapsshen.typecheckintrap-error, so a stack overflow silently became a refused judgement. With Generated Lua exceeds LuaJIT's 200-locals limit: datatype rules with nested conclusion patterns, list literals with 200+ elements #84 alone, 7 of 46cc-contractsjudgements come outok:false; withSHEN_PROLOG_ENGINE=legacy, 5 do; shen-go has 4. With Generated Lua exceeds LuaJIT's 200-locals limit: datatype rules with nested conclusion patterns, list literals with 200+ elements #84 and this PR together, the payload is identical to shen-go (dd877be7df09f341…).Cost
make certifyopens 0 segments: I counted withE.segments()aroundrun-kernel-tests.lua, and it printedSEGMENTS 0.luajitbuild). Everything is within noise:Tests
test/prolog_semantics_spec.luagets 13 new checks:(list number)typecheck, both of which open segments;(is R bottom)before(is R top);coroutine.yieldfrom 3000 levels deep reaching the host and resuming, and a yield with no host coroutine raising without leaving a segment open (4456af0);segment_limits, raising a trappableProlog depth limit exceeded, with the next query still working.On base, the spec dies at the first deep typecheck with
stack overflow.make testmake certify(43.1 suite)TOTAL: 1210 pass, 0 fail across 23 specspassed ... 154/failed ... 0TOTAL: 1223 pass, 0 fail across 23 specspassed ... 154/failed ... 0Both were run with
GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILEunset and a privateTMPDIR.Merge note
This merges cleanly with #78 (which also edits
prolog_compile.lua/prolog_engine.lua) and with #91 (#84/#88) and #89 (#86/#87), apart from a trivialCHANGELOG.mdconflict. On a local merge of all three (not pushed),make testgaveTOTAL: 1706 pass, 0 fail across 26 specsand certify gavepassed ... 154/failed ... 0.🤖 Generated with Claude Code