Phase 3 delivered: #67
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!67
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "self-hosting-io-and-load"
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?
Note: load evaluates in the host's environment, so definitions from loaded files aren't accessible to the caller. This will be naturally resolved in Phase
4 when we build the self-hosted evaluator.
PR Review: Phase 3 - File I/O Builtins and Self-Hosted Loader
Good progress on Phase 3. The implementation is generally solid, but there are a few issues worth addressing before merge.
Critical: Accidental artifact file committed
A file named 'hello from seqlisp' (with spaces) was accidentally committed. Its contents are '/tmp/seqlisp-test.txt' - clearly a test artifact that should not be in the repo. Remove before merge.
Bug: Broken test - load file with syntax error
In tests/lisp/reader/load.slisp, the test writes the file after trying to load it. The load call is in the let-result binding, but the file does not exist at that point - this always fails with file-not-found, not a syntax error. The test also unconditionally returns true, so it always passes regardless of actual behavior. Fix: write the file first, then assert-error on the load.
Minor: write-file evaluates content before path
In eval-write-file-with-env, the stack manipulation evaluates content before path. Lisp convention is left-to-right evaluation, so users expect path evaluated first. No practical impact for string-literal args, but inconsistent with convention.
Minor: No test for write-file permission failure
The suite covers the happy path and read-of-nonexistent-file, but has no test for write-file failing (e.g., writing to a read-only path). Worth adding if file.spit surfaces a meaningful error in that case.
Minor: Temp files not cleaned up
Tests write to /tmp but never remove files. The file-exists-false assertion relies on /tmp/seqlisp-nonexistent-file.txt not existing - a prior run that created that file would cause a spurious failure.
Implementation looks correct
Stack manipulation in eval-read-file-with-env and eval-file-exists-with-env follows existing patterns cleanly. The load function in lib/reader/load.slisp is minimal and clear. The +3 then count in the dispatch chain is correct. The acknowledged environment-isolation limitation of load is reasonable for this phase.
Code Review: Phase 3 — File I/O Builtins & Self-Hosted Loader
Overall this is a solid, well-tested addition. The implementation is consistent with existing patterns in
eval.seq, and the test coverage is thorough. A few issues need attention before merging.🔴 Must Fix
Accidentally committed file:
hello from seqlispA file named
hello from seqlisp(with spaces) was added to the repo root. Its contents are just the string/tmp/seqlisp-test.txt— this looks like a leftover from manual testing that was accidentally staged. It must be removed before merging.🟡 Potential Bugs
eval-write-file-with-env: argument evaluation order vs.file.spitargument orderThe implementation evaluates
contentbeforepath, leaving the stack asContentStr PathStrwhenfile.spitis called (PathStr on top). This only works correctly iffile.spitexpects( content path -- bool )with path on top of stack. If the convention is( path content -- bool ), the arguments are swapped and everywrite-filecall silently writes to the wrong location.This is worth verifying against the Seq stdlib definition of
file.spit, since the bug would be silent (the call succeeds but writes to the content string interpreted as a path).Empty
ifbranch ineval-read-file-with-envandeval-file-exists-with-envWhen
eval-err?is true, control falls through the emptyifbranch and the error result is left on the stack implicitly. While this probably works (the error propagates), the empty branch is fragile and easy to misread. The existing pattern elsewhere uses explicit handling. Consider:🟡 Design / Consistency
Asymmetric error handling in
loadloadhas two distinct failure modes with different shapes:read-filefails → returns anEvalResulterror (caught byok?/assert-error)eval-all-with-errorsfails → returns a raw(error msg ...)list (requires(car result)inspection)This is confirmed by the test using
(assert-eq (car result) 'error)rather than(assert-error ...). Callers must know to handle both shapes. A comment inlib/reader/load.slispdocumenting this contract would help — or, ifeval-all-with-errorscan be wrapped into anEvalResult, the API would be more uniform.🟢 Minor / Suggestions
Test files hardcode
/tmp/paths with no cleanupThe tests write 5+ files to
/tmp/and leave them behind. This is fine for local runs, but in CI or on macOS (where/tmpis a symlink to/private/tmp) it could cause subtle issues. Consider:file.spit/file.slurpwork correctly through symlinks in the CI environmenteval-write-file-with-envstack manipulation is denseThe argument-juggling section (
dup rot swap,rot rot, etc.) is harder to follow than the single-argument builtins. A few inline stack-effect comments at key points would make it easier to audit and maintain.Parenthesis count in
tests/lisp/all.slispThe closing paren count was bumped from
))))))))))))))))))))))))to))))))))))))))))))))))))— this looks correct to match the added(append file-io-tests, but worth double-checking the nesting depth is balanced.✅ What's Working Well
check-arityand the existing dispatch patternfile-exists?correctly returns#t/#fsymbols rather than propagating an error for the "not found" case — that's the right behaviorloadis clean and minimalCode Review: Phase 3 — File I/O Builtins & Self-hosted Loader
Overall this is a clean, well-scoped addition. The implementation is straightforward and the tests cover the important cases. A few things worth discussing:
Argument Evaluation Order in
write-fileIn
eval-write-file-with-env, content is evaluated before path (see the comment "Evaluate content first"). Standard Lisp convention evaluates arguments left-to-right. This reversal won't matter for side-effect-free expressions, but it's a semantic inconsistency worth noting. Consider whether left-to-right order is achievable without adding significant stack complexity, or at least add a comment explicitly calling out the deviation.Asymmetric Error Handling in
loadThe error contract documented in
lib/reader/load.slispis accurate but creates a real footgun:Callers cannot uniformly handle errors from
load— they need different strategies depending on whether the failure was at the file-read or eval stage. This is understandable giveneval-all-with-errors's contract, and the comment flags that Phase 4 will unify it. But the test"load file with eval error returns error list"implicitly validates this asymmetry as correct behavior. It might be worth naming that test more explicitly (e.g.,"load eval error returns data not exception") so future readers understand this is a known asymmetry, not an oversight.Test Isolation
All tests write to predictable
/tmppaths:The
file-exists-falsetest will fail flakily if a previous run of the test suite was interrupted before cleanup — or if two test suite runs happen in parallel. Minor concern for CI, but worth a thought. Also, none of the test files are cleaned up, which accumulates noise in/tmpover repeated runs.file-exists-falseFragilityThis relies on a file not existing, but
/tmp/seqlisp-nonexistent-file.txtis a perfectly plausible path that could exist on a shared CI runner. A safer pattern would be to write a file, delete it, then assert it no longer exists — though this requires adelete-filebuiltin that doesn't exist yet. Alternatively, use a more unique path (UUID-based or process-id-based). Low priority, but worth flagging.Stack Manipulation Readability in
eval-write-file-with-envThe write-file implementation has several multi-step stack dances (
rot rot,dup rot swap) that are hard to follow without careful annotation. The existing comments help, but intermediate stack-state comments would make future maintenance easier:Consider adding
( -- stack-state )annotations at the tricky spots. The read-file and file-exists? implementations are much simpler and easy to follow.Minor:
then then thencountAfter adding the three new file I/O dispatches, the dispatch chain now has:
Nine
thens — this is already fragile to count. Not blocking, but a local word or a comment marking the count would help catch off-by-one errors when the next builtin is added.What's Good
check-arity+ early return on error) matches the existing codebase conventions.loadcorrectly propagates file-read errors upward and returns eval errors as data pereval-all-with-errorscontract.Good work overall. The main things to address before merge are the arg-evaluation order question and test isolation. The asymmetric error handling in
loadis an acceptable Phase 3 trade-off as long as the asymmetry stays documented.Code Review: Phase 3 — File I/O Builtins & Self-hosted Loader
Overall this is a solid, well-structured addition. The stack manipulation is dense but appears correct, and the error documentation in
load.slispis a nice touch. A few items worth noting:Bug / Behavioral Issue
write-fileevaluates arguments right-to-left (eval.seq~line 2430)Due to stack management, content is evaluated before path:
This deviates from the left-to-right evaluation order that most Lisps guarantee. For pure expressions it's invisible, but
(write-file (side-effecting-path) (side-effecting-content))will evaluate content before path. If the evaluator ever documents evaluation order guarantees, this will need to be consistent or explicitly noted.Error Message Quality
Both
read-fileandwrite-fileproduce opaque errors:Including the path in the message (e.g.,
"read-file: could not read file: /some/path") would make debugging significantly easier, especially whenloadcallsread-fileinternally and the error propagates up.Test Isolation Concerns (
tests/lisp/core/file_io.slisp)file-exists-falseis flaky: It asserts(file-exists? "/tmp/seqlisp-nonexistent-file.txt")returns#f, but there's no guarantee that file doesn't exist (e.g., left over from a crashed run). A more uniquely named or randomly-suffixed path would be safer.No cleanup: Test files in
/tmp/accumulate across runs. For CI this is fine, but consider whether the test suite should clean up after itself.Missing edge cases:
(write-file "/tmp/..." "")— empty string content(write-file (string-append "/tmp/" "test.txt") "data")— path via expression (would also exercise the evaluation order issue above)(write-file 42 "data")— non-string path at eval time (not just a literal)Minor: Silent empty branch pattern
In
eval-read-file-with-envandeval-file-exists-with-env:The "propagate" comment is doing heavy lifting — the intent is that the error stays on the stack and naturally falls through. This is idiomatic Forth, but the comment could be clearer:
# leave EvalResult on stack; falls through to ;.Acknowledged Debt
The asymmetry in
load.slisp(file error → evaluator error; eval error → error-list data) is clearly documented with a Phase 4 note. The test"load file with eval error returns error list"correctly covers this behavior. No action needed here, just confirming the documentation is sufficient.Dispatch Chain Length
The
then then then then then then then then thenchain is getting long. Not a blocker, but as builtins accumulate this O(n) string comparison dispatch may be worth addressing (e.g., a lookup table or hash dispatch) before it becomes painful. This isn't new to this PR — just worth flagging as accumulating tech debt.Good coverage on the happy-path tests and error cases. The
loadtests inreader/load.slispare particularly thorough. Nice work on Phase 3.