Step 1 (TCO) is done. #69
No reviewers
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
seq-lisp-2-backport
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
navicore/seq-lisp!69
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix-tco-self-hosting"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
PR Review: Step 1 (TCO) via Trampoline
Overall this is a clean, well-structured implementation of the trampoline pattern for TCO. The approach is sound and the key changes are easy to follow.
Correctness: Thunk Tag Collision Risk
The sentinel
__thunk__is just a symbol in user-visible data. If any user program returns a list starting with that symbol,thunk?will silently misidentify it as a deferred thunk and corrupt the result. For example,'(__thunk__ a b)in user code would cause incorrect behavior.A more robust approach: use a tagged wrapper type, a vector/record if available, or a gensym-style unique sentinel that cannot appear in user-written source. This is the highest-risk correctness issue in the PR.
Correctness:
trampolineSelf-Recursion is Subtle but Correcttrampolineis a seq-lisp closure whose tail-recursive call goes throughapply-closure(which now returns a thunk). The outer trampoline loop insevalre-enters for each thunk, so the stack stays bounded. This is correct, but subtle enough to warrant a comment explaining whytrampolinedoes not need to be a built-in primitive to avoid stack overflow.Test Coverage
The single new test only covers the
iftail-call path in a simple linear recursion. Worth adding tests for:condtail position (both a matched clause and theelsebranch)beginwith a tail call in the final expressionletbody as a tail callWithout these, regressions in
do-cond,do-begin, ordo-letwon't be caught.Minor: Removed Documentation Comment
In
do-if, the original comment; One-armed if: return '() when else branch is missingwas dropped. That comment explained a non-obvious semantic choice and is worth keeping.Minor: Macro Path Double-Thunking
apply-closurealways returns a thunk now, sotrampolineunwraps it immediately, then we re-wrap the result in a new thunk. This is correct but one allocation is always transient. A brief comment — "trampoline resolves the macro body; then we tail-call into the expansion" — would clarify the intent.Performance Note
Each tail call allocates a 3-element list
(__thunk__ env expr). For tight loops this is O(n) short-lived allocations, which is expected for a trampoline. Worth watching if GC pressure shows up later.Summary
Good foundational work. The thunk collision issue is the one I'd want addressed before merge.
PR Review: Step 1 (TCO) via Trampoline
The trampoline approach is a clean, idiomatic solution for adding TCO to a self-hosted evaluator. The design is sound and the implementation is mostly correct. A few things worth addressing:
Bug:
do-definemissing trampolinedo-define-allwas updated, but its siblingdo-definewas not. The non-function branch on line ~257 still callsevdirectly without trampolining:vs.
do-define-all(correctly updated):This means
(seval '(define x <tail-recursive-expr>))could return a raw thunk instead of the resolved value. Example that would silently misbehave:trampolineis itself host-recursiveThe trampoline bounces via self-recursion, which still consumes host (Rust) stack frames for each bounce. For the current tests (depth 10000) this should be fine, but if SeqLisp's host interpreter doesn't have TCO for
trampoline's own tail call, extremely deep loops could still overflow.This is an inherent limitation of the bootstrap approach and may be intentional as "Step 1" — just worth noting in a comment so it's not forgotten.
bootstrap-evalnot trampolined (minor)Lines ~585 and ~589 use raw
(ev ev env expr). Bootstrap functions (map,filter,fold,not) are not tail-recursive in their current form so this is harmless now, but it creates a subtle inconsistency — if bootstrap functions are ever extended with tail-recursive patterns, they'd silently get thunks back.Test coverage
The four new TCO tests are well-chosen and cover the main paths. A couple of gaps:
Mutual recursion — two functions bouncing between each other:
TCO through macro expansion — the macro path in
do-applynow returns a thunk viamake-thunk env expanded; a test verifying this works for a tail-call inside a macro body would be valuable.Minor observations
thunk?can false-positive on user lists starting with'__thunk__. Already documented — just confirming the risk is understood.thunk-env/thunk-exprhave no guard; calling them on a non-thunk would return garbage silently. Low priority sincethunk?gates all call sites.; TAIL POSITIONcomments are helpful — consistent and appreciated.Summary: The core trampoline design is correct and well-executed. The
do-definemissing trampoline is the one actual bug I'd want fixed before merging. Everything else is either a known limitation or a nice-to-have improvement.PR Review: Step 1 (TCO) via Trampoline
Overall this is a clean, well-structured implementation of tail-call optimization. The trampoline pattern is the right choice for a self-hosting Lisp evaluator that can't rely on host-level TCO, and the execution follows through consistently.
Code Quality
Thunk representation — using a tagged list
(__thunk__ env expr)is idiomatic and consistent with how closures and macros are already represented in this evaluator. Good.thunk?predicate — the guard(if (list? x) (if (null? x) #f ...))is correct and handles edge cases (non-list values, empty list). No issues.Accessor names —
thunk-envandthunk-exprare clear. Consider whetherthunk-bodywould read more naturally thanthunk-expr(to mirrorclosure-body), but this is minor.; TAIL POSITIONcomments — very helpful for future maintainers trying to understand why a thunk is returned instead of a direct eval call.Macro path — the double-trampoline pattern for macros is subtle but correct:
apply-closurereturns a thunk for the macro bodytrampolinedrives that to completion → produces the expanded formmake-thunk env expandeddefers evaluation of the expansionThe comment explaining this is appreciated.
Potential Issues
do-condrecursion —do-condcalls itself directly (not trampolined) when the test is false and there are more clauses. This is fine for reasonableconddepths, but worth noting as a host-stack consumer for pathologically long clause lists. Not a blocker.thunk?name collision — acknowledged in the reserved names comment, but worth emphasizing:__thunk__is only reserved by convention. Any user-created list starting with the__thunk__symbol will be misidentified as a thunk. This is consistent with how__closure__and__macro__work today, so it's an acceptable tradeoff for a bootstrap evaluator.trampolinetakesevas a parameter — this couples the trampoline to the evaluator's internal calling convention. It works correctly, but a future refactor might consider a zero-argument thunk wrapper to decouple them. Not urgent.Tests
Good coverage of the four tail positions:
if,cond,begin,let. The mutual-recursion limitation is clearly documented as a known constraint tied to immutable environments.One suggestion: the
sumtest usesn=10000with an accumulator — this is the canonical TCO test and 10,000 iterations is plenty to verify the trampoline fires. The test value50005000is correct (10000 * 10001 / 2). ✓The macro TCO test (
when→if) is a nice addition that exercises the double-trampoline path.Summary
__thunk__name collision) are properly documented.Nice work getting TCO in — this is a meaningful correctness improvement for any seq-lisp program that relies on tail recursion.