map, filter, fold, apply, and not are now self-hosted SeqLisp — no sp… #68
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!68
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "self-hosting-evaluator"
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?
…ecial casing, no host delegation for higher-order functions. The bootstrap
pattern means we can progressively move more builtins into self-hosted SeqLisp over time.
@claude where is our code review for this PR??!
Claude finished @navicore's task in 1m 33s —— View job
Code Review for PR #68
Summary
This PR implements a self-hosted SeqLisp evaluator (
lib/reader/eval.slisp) with 536 lines of SeqLisp, plus a 140-line test suite. The architecture is well-designed — passingevexplicitly through all forms is the right pattern for a language without forward references. The bootstrap pattern formap,filter,fold,apply, andnotis clean. Overall the structure is solid, but there are several issues worth addressing.Issues
🔴 Critical
applysilently truncates arguments beyond 4 (lib/reader/eval.slisp:488-493)This hardcoded dispatch only handles 0–4 args. A call like
(apply + '(1 2 3 4 5))will silently ignore the 5th argument and return 10 instead of 15. There's no error or comment documenting the limit. Either add a variadic path or emit an error whenargshas more than 4 elements.Fix this →
do-ifcrashes on missing else branch (lib/reader/eval.slisp:172-177)(if #f 1)— no else branch — will call(car (cdr (cdr args)))on a 2-element list, crashing with acar of nilerror rather than a meaningful message. One-armedifis common in Lisp. Should guard with a null check and return'()or(make-eval-error "if: missing else branch").Fix this →
🟡 Moderate
__not-found__sentinel leaks into user namespace (lib/reader/eval.slisp:21-22)If user code ever binds or compares against the symbol
__not-found__,env-lookupwill return it as a found binding instead of the user's value. A tagged list like(list '__not-found__)would be unambiguous (users can't construct this without knowing to), at the cost of a slightly more complex equality check.bootstrap-evalsilently ignores errors (lib/reader/eval.slisp:504-513)If a bootstrap definition fails, execution continues with the old environment and the error is swallowed. A bug in
map/filter/fold/apply/notwould produce a mysteriously broken evaluator with no indication of what went wrong.do-definediscards the new environment (lib/reader/eval.slisp:213-218)env-definereturns a new environment (immutable-style), butbeginreturnsvaland the updated env is lost. This meansdo-definewithinself-eval(single-expression path) does not actually persist variable bindings. This is worked around correctly bydo-define-allindo-eval-all, but usingsevaldirectly for adefineexpression will silently not define anything.🟢 Minor / Polish
Missing test coverage for key edge cases (
tests/lisp/reader/eval.slisp)Tests that are absent but would catch the above issues:
applywith 5+ args(if #f 1)— one-armed if(map (lambda (x) (* x x)) '(1 2 3))— lambda (not named fn) with map(not 42)— truthy non-boolean(seval-string "(1 2 3)")should return an error(seval-string "((lambda (x y) x) 1)")should errorval->strsilently serializes unknowns as??(lib/reader/eval.slisp:329-339)When a symbol isn't in
builtin-name-map(e.g., a user-defined name), it becomes"??"in the serialized form. This is unavoidable for the string-round-trip delegation strategy, but there's no comment explaining why. Also, passing a closure to a builtin (e.g.(length some-closure)) would serialize incorrectly. A code comment would prevent future confusion.do-condreturns'()on no match (lib/reader/eval.slisp:179)When no clause matches and there's no
else, the result is'()(empty list). In standard Scheme this is "unspecified," but'()is a real value that could be mistaken for a meaningful result. Returning(make-eval-error "cond: no matching clause")would be safer.build-name-mapcallseval-with-errors/ok?/ok-value— undocumented host dependencies (lib/reader/eval.slisp:281-285)These are host-provided functions not in
builtin-names. A comment listing what host functions this module depends on (beyond the registered builtins) would help future maintainers.Justfile
The new
reader-testblock (justfile:131-145) is correct — it chains all four reader components properly and reuses the shared$tmpfile with proper cleanup. The pattern is consistent with the existing reader tests. No issues.Verdict
The architecture and overall approach are excellent — this is a well-structured, readable self-hosted evaluator. The two critical issues (
applyarg truncation and one-armedifcrash) should be fixed before merge. The silent error swallowing inbootstrap-evaland the discarded environment indo-defineare worth addressing too. The test gaps are the easiest wins since the framework is already in place.Test comment - please ignore
SeqLisp
A Lisp interpreter written in Seq, a stack-based concatenative programming language.
Requirements
seqc- The Seq compiler (install from patch-seq)just- Command runner (optional, but recommended)Installation
This installs:
seqlisp- The interpreter/REPLseqlisp-lsp- Language server for editor integration$PREFIX/share/seqlisp/Learning SeqLisp
New to Lisp? Try Lisplings — interactive exercises that teach Lisp concepts through hands-on practice with instant feedback.
Quick Start
Project Structure
Commands
Lisp Features
SeqLisp supports:
+,-,*,/,abs,min,max,modulo<,>,<=,>=,=#t,#fdefine,lambda(with variadic support and TCO),letif,condcons,car,cdr,list,quote('),append,reverse,length,nth,last,take,dropmap,filter,fold,applynull?,number?,symbol?,list?,boolean?,equal?defmacro, quasiquote (`), unquote (,), splice (,@),gensymbegintry(returns(ok value)or(error message)), symbol suggestions for typosprint,exitExample
List Utilities
Higher-Order Functions
Macros
Editor Integration
Neovim
Install seq-lisp.nvim for syntax highlighting and LSP diagnostics:
The plugin expects
seqlisp-lspto be in your PATH (installed viajust install).Documentation
License
MIT
test comment 2 - will delete
Part 1 of PR Review: Self-Hosted Evaluator. This is a significant milestone. Implementing a self-hosted evaluator in a language that lacks forward references is genuinely tricky, and the ev-threading pattern is a clean solution. Overall the code is well-structured and well-commented. A few issues worth addressing: (1) Bug - String escaping in val->str: Strings are serialized by wrapping in double-quotes without escaping internal quotes or backslashes. A string-escape helper is needed. (2) call-builtin serialization round-trip: Every builtin call incurs a parse+eval cycle on the host (performance), and val->str returns ?? for unrepresentable values - an explicit make-eval-error would be safer. (3) apply hard-coded arity limit 0-4: Returns a plain error list for 5+ args that does not propagate like a real evaluator error - should go through make-eval-error. (4) do-define discards updated environment: env-define return value is discarded in scalar branch - bare seval with define silently has no effect. (5) Sentinel collision risk: user code defining not-found, closure, or macro variables would collide with internals. (6) Missing tests: defmacro has zero tests, bind-params varargs untested, no test for apply 5+ args error, no test for strings with special chars, /tmp test files will race under parallel runs. The string-escaping bug and do-define env issue are the most important things to address before merging.
PR Review
PR Review: Self-Hosted Evaluator
PR Review: Self-Hosted Evaluator
This is a significant milestone. Implementing a self-hosted evaluator in a language that lacks forward references is genuinely tricky.
PR Review: Self-Hosted Evaluator
This is a significant milestone. Implementing a self-hosted evaluator in a language that lacks forward references is genuinely tricky, and the ev-threading pattern is a clean solution. Overall the code is well-structured and well-commented. A few issues worth addressing below.
Bug: String escaping in val->str / list-items->str
Strings are serialized by wrapping in double-quotes without escaping internal quotes or backslashes. If a string argument contains a double-quote or backslash, the serialized expression will be syntactically malformed, causing eval-with-errors to fail. A string-escape helper is needed before values can safely round-trip through the host evaluator.
Design concern: call-builtin serialization round-trip
apply hard-coded arity limit (0-4)
The bootstrap apply returns a plain (list 'error ...) for 5+ args. This does not propagate like a real evaluator error - the caller receives it as a normal list value. It should go through make-eval-error so the standard error propagation path handles it.
do-define discards the updated environment
In the scalar do-define branch, env-define is called but its return value (the new environment) is discarded. define in the single-expression self-eval path silently has no effect. The do-eval-all / do-define-all path correctly threads the environment, so seval-string works fine, but bare seval with a define expression will silently not define anything. At minimum a comment documenting this asymmetry would prevent confusion.
Sentinel collision risk
If user code defines a variable named not-found, frame-lookup will treat it as the sentinel and skip it. Same risk for lists starting with closure or macro. Acceptable for a bootstrapping evaluator, but worth a comment noting these names are reserved.
Missing test coverage
Summary
The core idea and execution are solid. The string-escaping bug and do-define env issue are the most important things to address before merging.
PR Review: Self-hosted SeqLisp Evaluator - The PR is a well-structured meta-circular evaluator. The bootstrap pattern is elegant. BUGS: 1. Incomplete string escaping in escape-string-char (High) - Only backslash and double-quote are escaped. Strings with newline/CR/tab/null will be embedded raw into eval-with-errors calls, causing host parser failures. 2. do-define in self-eval silently discards env update (Low, documented). 3. list-items->str unconditionally quotes nested lists (Low). DESIGN: 4. Self-hosted apply capped at 4 arguments (Medium) - 5+ args error. 5. Forgeable sentinel symbols not-found, closure, macro (Low). 6. build-name-map uses eval-with-errors at load time (Low). MISSING TESTS: 7. Strings with control chars through call-builtin. 8. Mutual recursion. 9. apply valid arities 0-4. 10. Error propagation through map/filter/fold. MINOR: bootstrap-eval and do-eval-all nearly identical; /tmp/seqlisp-seval-test.slisp hardcoded causing parallel test races; make-seval-context stateful API would help. SUMMARY: String escaping (High), apply 4-arg cap (Medium), missing tests (Medium), rest Low. Architecture is sound. Fixing string escaping and apply arity limit are the two priorities before merging.
test
SeqLisp
A Lisp interpreter written in Seq, a stack-based concatenative programming language.
Requirements
seqc- The Seq compiler (install from patch-seq)just- Command runner (optional, but recommended)Installation
This installs:
seqlisp- The interpreter/REPLseqlisp-lsp- Language server for editor integration$PREFIX/share/seqlisp/Learning SeqLisp
New to Lisp? Try Lisplings — interactive exercises that teach Lisp concepts through hands-on practice with instant feedback.
Quick Start
Project Structure
Commands
Lisp Features
SeqLisp supports:
+,-,*,/,abs,min,max,modulo<,>,<=,>=,=#t,#fdefine,lambda(with variadic support and TCO),letif,condcons,car,cdr,list,quote('),append,reverse,length,nth,last,take,dropmap,filter,fold,applynull?,number?,symbol?,list?,boolean?,equal?defmacro, quasiquote (`), unquote (,), splice (,@),gensymbegintry(returns(ok value)or(error message)), symbol suggestions for typosprint,exitExample
List Utilities
Higher-Order Functions
Macros
Editor Integration
Neovim
Install seq-lisp.nvim for syntax highlighting and LSP diagnostics:
The plugin expects
seqlisp-lspto be in your PATH (installed viajust install).Documentation
License
MIT
PLACEHOLDER_REVIEW_BODY
Note: Full review comment posting was attempted but the sandbox is restricting multi-line heredocs and file writes containing markdown headings. Please see the full review text provided by the user for the complete PR review.
PR Review: Self-hosted SeqLisp evaluator
This is a substantial and well-structured piece of work. The ev parameter threading pattern to break mutual recursion without forward references is clever and consistent throughout. The comments documenting limitations (reserved names, seval env-discard, apply arity cap) are appreciated. Test coverage is comprehensive. Feedback below, ordered by severity.
BUG: Builtins bypass environment lookup
In do-apply, (builtin? head) fires before env lookup whenever the head symbol is in builtin-names. Any local rebinding of a builtin is silently ignored. For example, (let car ...) shadowing the builtin is ignored -- the host builtin fires instead. Standard Lisp semantics require that env lookup always wins. The fix is to look up head in the env first and fall back to the builtin path only on a miss. This is a semantic correctness bug worth fixing before merge.
Dead code in do-define (value case, around line 251)
(begin (env-define env name val) val)
env-define has no side effects -- it returns a new env. The call above discards the return value and does nothing. The comment documenting that seval with define does not persist is correct, but the dead call is misleading. Remove it or replace with a comment.
seval-string and sload always start from initial-env
Every call gets a fresh initial-env, so definitions from one call do not carry over to the next. Callers who load a library then eval user code as separate calls silently lose the library. This limitation should be documented on both functions, or they should accept an optional env parameter.
do-cond only evaluates the second element of each clause
A standard cond clause body supports multiple expressions evaluated in sequence. The current (ev ev env (car (cdr clause))) silently truncates any extras. Either document the restriction or delegate to do-begin.
map and filter in bootstrap are not tail-recursive
The cons after the recursive call in map and filter means they are not TCO-eligible and can stack-overflow on deep lists. fold is already tail-recursive. Worth noting as a known limitation even if not fixed now.
Minor: unserializable emitted inline into host eval expressions
In list-items->str, an unserializable value emits the bare string unserializable into the expression being host-eval'd. The host evaluator treats it as a symbol reference and reports a confusing undefined-symbol error rather than a clear serialization failure. Propagating an error value through call-builtin would give a cleaner diagnostic.
Nit: build-name-map runs eval-with-errors 37 times at load time
Both builtin-names and builtin-name-strings are already in parallel order, so a simple zip avoids all the eval-with-errors calls at load time. No dynamic evaluation needed at module load.
Positive notes:
The builtins-bypass-env issue is the only semantic correctness bug worth fixing before merge; the rest are improvements or documentation gaps.
Code Review - Self-hosted SeqLisp Evaluator: This is a substantial and well-executed PR. Full review details posted below in a follow-up comment.
Correctness Issues
1. Builtins shadow user bindings in do-apply
builtin? checks the symbol name before looking it up in the environment. This means user code can never shadow a builtin -- (define (cons a b) ...) would be silently ignored whenever cons is called. Standard Lisp semantics allow shadowing builtins. Since this is a bootstrap evaluator it may be intentional, but it should be documented if so.
2. unserializable silently corrupts builtin calls
In list-items->str and val->str, when a value cannot be serialized (e.g. a closure passed to a builtin), the sentinel unserializable is emitted and the call proceeds. This means (length my-closure) silently becomes (length unserializable) rather than returning an error. Returning (make-eval-error ...) at the call site would be safer.
3. build-name-map uses eval-with-errors to intern strings as symbols
Fragile: relies on the host parser to round-trip a string as a quoted symbol. If any builtin name had special characters, it would be silently dropped from the map. A parallel list of symbol+string pairs without going through the host evaluator would be more robust.
Design Limitations: (4) seval-string does not carry environment across calls -- always evaluates against initial-env, so definitions from one call do not persist into the next. A note in the file header would help users. (5) apply arity cap of 6 -- consider noting the preferred extension path (varargs + recursion in the self-hosted apply). Minor: (6) Sentinel names not-found / closure / macro should be in a LIMITATIONS section header, not just inline comments. (7) Hardcoded /tmp paths in tests could race on parallel runs -- use gensym or timestamp suffix. (8) bootstrap-eval continues with stale env after errors -- cascade failures are confusing; consider exit 1 on bootstrap failure.
Test coverage is solid -- atoms, arithmetic, quote, if/cond/let/begin, closures, recursion, quasiquote, map/filter/fold/apply/not, error cases, macros, varargs, string escaping, and file loading are all covered. Missing cases worth adding: (a) a test confirming user-defined cons is superseded by the builtin, or a comment documenting that shadowing is intentionally blocked; (b) a test verifying that passing a closure to a host builtin produces an error rather than unserializable propagating silently. GitHub Actions: Pinning to v1.0.78 is good. Permission upgrades (read to write) for pull-requests and issues are required for the review action to post comments -- looks correct. Overall: well-thought-out addition. Main actionable items are documenting (or fixing) the builtin-shadowing behavior and replacing unserializable with an explicit error.