fix: suppress top-level auto-print when entry body ends in a loop#231
Merged
Conversation
Print-loops like `@x xs{prnt x}` at the function tail were double-printing
at the top level: each item from the loop body via `prnt`, then the loop's
last-body-value bubbled up as the program result and got auto-printed
again. The well-known workaround was a trailing `+0 0` sentinel after
every print-loop, which every persona had to learn.
This is a print-layer-only fix in main.rs. When the program's syntactic
entry-function body ends with `@` or `wh` AND has no early-return path
(no `ret`, no braceless guard, no nested `ret` in guards/match/loops),
the plain-text top-level auto-print is suppressed.
The check is syntactic, not value-based: a function that explicitly
returns `nil` from a non-loop tail still prints "nil", because the user
asked for it. Loop-as-expression semantics inside functions are unchanged,
so a caller that consumes such a function's return value still sees the
loop's last body value. JSON mode is untouched so machine-readable
consumers always get a structured result line.
The early-return guard is conservative: if the body contains any `ret` or
braceless guard, we can't tell at print time whether the program's value
came from the loop tail or from an explicit return, so we err on the side
of printing.
Three pre-existing tests asserted that a function whose body ends with a bare loop auto-prints the loop's last-body-value at top level. That shape is now suppressed (it's the same shape that caused the print-loop double- output). The tests still want to assert the loop value reaches stdout, just via the trailing-expression pattern the rest of the codebase already uses (see examples/loops.ilo: `range-sum`, `range-list`, `brk-at-3`, `cnt-evens` all bind the loop value and end with a trailing expression). - examples/lists.ilo:sq-last binds the loop value into `r` and ends with `+r 0` so the function tail is an expression, not a loop. - tests/eval_inline.rs:range_basic and range_with_arg get the same treatment with inline comments explaining the convention.
Pins the new behaviour across tree, VM, and Cranelift: - top-level print-loop emits only the loop body's prints (no trailing dupe) - nested print-loop: caller still observes the loop's last body value as the callee's return, so loop-as-expression semantics inside functions are unchanged - empty-list loop at top level produces no output (previously leaked `nil`) - brk-loop and while-loop tails are suppressed - trailing-expression sentinel still auto-prints (non-regression) - early-return present + loop tail: value still prints (conservative rule) - explicit `nil` return from a non-loop tail still prints (suppression is syntactic loop-tail, never blanket Nil) - `--json` mode is unaffected: always emits a structured result `examples/print-loop.ilo` documents the same scenarios with `-- run:`/ `-- out:` annotations so tests/examples_engines.rs exercises them as higher-level engine-parity checks.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
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.
Summary
Print-loops like
@x xs{prnt x}at the function tail used to double-print at the top level: each item from the loop body viaprnt, then the loop's last-body-value bubbled up as the program result and got auto-printed again. The well-known persona workaround was a trailing+0 0sentinel after every print-loop, called out as a "consistent irritant across all 5 programs" in the assessment doc (line 834) and as one of the top friction items at line 854.Print-layer-only fix, scoped to
src/main.rs. When the program's syntactic entry-function body ends with@/whAND has no early-return path, the plain-text top-level auto-print is suppressed. Loop-as-expression semantics inside functions are completely unchanged - a caller that consumes such a function's return value still sees the loop's last body value. JSON mode is untouched so machine-readable consumers always get a structured result.The check is syntactic, not value-based: a function that explicitly returns
nilfrom a non-loop tail (e.g.nothing>O n;nil) still prints "nil" because the user asked for it.Repro
Before:
After:
Cross-engine: tree, VM, and Cranelift all agree.
What's in the diff
Three commits, each one logical step:
suppress top-level auto-print when entry body ends in a loop- the helper (program_result_should_suppress) walks the entry function's AST tail and decides whether to suppress;print_valuegains asuppress_loop_tailflag, plain mode honours it, JSON mode ignores it. All four engine runners (run_default,run_cranelift_engine,run_vm_with_provider,run_interp_with_provider) compute the flag once at dispatch and thread it toprint_value. A separatebody_has_early_returnwalker is the conservative safety valve: if the body contains anyretor braceless guard anywhere (including inside nested guards/match/loops), suppression is off, because we can't tell at print time whether the program's value came from the loop tail or from an explicit return.update bare-loop-tail tests and example to use sentinel form- three pre-existing tests asserted the previous "function ends with a bare loop, auto-print the loop value" shape. They're updated to bind the loop value and end with a trailing expression, the same patternexamples/loops.ilouses for every existing loop example. Inline comments explain the convention so future readers don't think the rewrite was cosmetic.cross-engine regression test and example for print-loop suppression-tests/regression_loop_print.rsruns nine scenarios across tree/VM/Cranelift: top-level print-loop, nested print-loop (must still return the value), empty-loop, brk-loop, while-loop, trailing-sentinel non-regression, early-return + loop tail (conservative rule), explicit-nil return (non-regression), and JSON mode (non-regression).examples/print-loop.ilodocuments the same scenarios with-- run:/-- out:annotations sotests/examples_engines.rsexercises them as higher-level engine-parity checks.Test plan
cargo fmt --all -- --checkcleancargo clippy --workspace --all-targets -- -D warningscleancargo test --release --features cranelift- full suite passestests/regression_loop_print.rspasses on all three enginestests/examples_engines.rspicks up the newexamples/print-loop.iloand runs it on tree + VM1\n2\n3\ninstead of1\n2\n3\n3\nacross all three engines--jsonstill emits{"ok":3}for the print-loop case and{"ok":null}for empty-loopouter>n;v=inner();+v 0whereinnerends in a print-loop still returns 3 toouterFollow-ups
None blocking. If we ever want to revisit the early-return-present-plus-loop-tail edge case, the right place is runtime-flagging "did this value come from the loop tail?" inside the interpreter/VM/Cranelift - significant cross-backend plumbing, deferred unless a persona report surfaces the pain.