Skip to content

Preserve let/let* side effects - #118

Merged
shsms merged 6 commits into
mainfrom
fmt-core
May 13, 2026
Merged

Preserve let/let* side effects#118
shsms merged 6 commits into
mainfrom
fmt-core

Conversation

@shsms

@shsms shsms commented May 13, 2026

Copy link
Copy Markdown
Owner

Also expose parse_file + etags polish

shsms added 6 commits May 13, 2026 21:21
Exposes the AST without evaluating it — wanted for tooling (linters,
analyzers, source-rewriters) that need to read a Lisp file's forms
without running them. The function already existed; this only
changes visibility, plus a doc comment covering the filename-table
side effect (re-parsing the same path is idempotent).
`compile_fn_let_star` short-circuited the whole form when the body
compiled to zero instructions — which happens whenever the let's
result is discarded and the body is a literal or a binding lookup.
The early return threw away `result`, which already held the
binding-init computations and their `BeginScope`s, so any
side-effecting init in `(let ((x (mutate))) t)` silently disappeared.

Drop the shortcut. The body is appended (empty is fine — values
pushed by the inits are consumed by `BeginScope`, the trailing
`EndScope`s leave the stack balanced) and the inits run. Add four
regression tests covering both `let` and `let*`, body that ignores
the binding, and sequential lets in one progn.
`(progn)`, `(let ((x 5)))`, and `(let* ((x 5)))` are well-formed in
Emacs and evaluate to nil. Tulisp's bytecode `compile_progn` returned
an empty instruction sequence regardless of `keep_result`, so a
caller wanting a value (e.g. inside `(princ …)`) found nothing on the
VM stack and panicked with `attempt to subtract with overflow`. The
tree-walker `impl_let` rejected empty body outright with a
TypeMismatch.

Push nil from `compile_progn` when the form list is empty and a
result is wanted, and drop the `body.consp()` check in `impl_let` —
its `eval_progn` already returns nil on an empty form list. Adds
four regression cases covering progn, let, let*, and an empty progn
nested in a position that consumes a value.
The recorded location for a Rust-side defun is the source line of
the ctx.defun() call (via #[track_caller]). For multi-line
registrations like

    ctx.defun(
        "set-voltage-per-phase",
        move |...| { ... },
    );

the recorded line is just `    ctx.defun(` -- the name lives one
line later. tags_table previously used that line as the etags
preamble, so emacs's tag-find couldn't locate the name on the
recorded line and fell back to a forward search through the file,
landing on the wrong occurrence (often a call site rather than the
registration).

Walk forward up to 8 lines looking for one that contains the name
literal. If found, use it as the preamble (and adjust the line
number / byte offset accordingly). Single-line registrations and
.lisp source's `(defun NAME ...)` forms hit the fast path on the
recorded line itself, so they're unaffected.
The follow-load-files arm of the parser called `inner.cadr()?.as_string()?`
on every `(load X)` it saw — propagating an error up if X was a
variable / expression rather than a literal path. That error
aborted the parse of the enclosing form, so any defun whose body
contained `(load some-var)` got dropped from the tags table along
with every defun that came after it in the file.

Concretely, sim/common.lisp's load-overrides has

    (let ((path (overrides-path)))
      (when (file-exists-p path)
        (load path)))

— and that single dynamic load form was killing tags for both
load-overrides itself and scenario-end-after that followed it.

Gate the recursive descent on `as_string` succeeding via let-chain;
fall through silently if not. We can't statically know the path
of a dynamic load anyway, so skipping is the right behaviour.
The string-literal case still gets followed.
Cleans up three rustfmt drifts that were sitting on main: a use-list
sort in src/bytecode/interpreter.rs, a stray blank line at the end of
src/lists.rs, and a function signature in src/plist.rs that fits on
one line at the current width.
@shsms
shsms merged commit bb901e3 into main May 13, 2026
2 checks passed
@shsms
shsms deleted the fmt-core branch May 13, 2026 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant