Skip to content

Support bare return - #29

Merged
wtholliday merged 1 commit into
fix-return-type-checkfrom
bare-return
Aug 8, 2026
Merged

Support bare return#29
wtholliday merged 1 commit into
fix-return-type-checkfrom
bare-return

Conversation

@wtholliday

Copy link
Copy Markdown
Collaborator

Fixes the remaining half of #23.

Stacked on #28 — based on fix-return-type-check, since a bare return needs the return-type constraint that PR adds in order to be checked against the enclosing function. Please merge #28 first; the base will retarget to main automatically.

The problem

return with no value was a parse error. parse_stmt unconditionally parsed an operand, and Expr::Return held a non-optional ExprID, so there was no way to represent it:

do_work(done: bool) {
    if done {
        return
    }
    value = value + 1
}
do_work.lyte:6: Expected expression

The error pointed at the line after the return, and in a larger file the desynced parser cascaded into a run of bogus Expected declaration errors — which is what made this hard to diagnose from the reporter's side.

The fix

Expr::Return(Option<ExprID>). The parser treats return as bare when nothing is left in the statement — Endl, Rbrace, or end of input — and the checker types a bare return as void, so it unifies with the enclosing function's return type through the machinery from #28.

The issue asked for either support or a clearer diagnostic. This gives both: bare return works as an early exit, and using one in a value-returning function is now a clear type error rather than a parse failure.

❌ f.lyte:2:5: return type must match function return type: void vs i32
    return
    ^

Each backend emits its existing void-return path for the bare case: return_(&[]) in Cranelift, build_return(None) in LLVM, ReturnVoid in the stack VM, and a zero placeholder in r0 for the register VM (whose void epilogue ignores it).

Two bugs fixed along the way

  • return g() where g is itself void fed a placeholder value to a void signature in the Cranelift and stack backends. Both now emit the void return. LLVM already special-cased this.
  • In the stack backend, Expr::Return in void context fell through to the generic "translate then drop" path, emitting a Drop after the return terminator. Return never falls through, so there is nothing to drop.

Testing

New golden tests: checker/bare_return (the issue's example, plus unreachable code after a bare return), checker/bare_return_in_value_fn (the error case), loops/bare_return_in_loop (returns from the function, not just the loop). Plus a pretty_print unit test for the bare form.

cargo test --workspace passes: 352 lib tests and 313 golden tests across all four backend suites.

I also built with --features llvm and ran the bare-return cases through the LLVM backend by hand, since it isn't part of the default test run. All four backends — Cranelift JIT, stack VM, register VM, LLVM — produce identical output for bare returns inside loops, inside if/else, in lambdas, and with unreachable code following.

No new cargo fmt drift or clippy warnings relative to the base (both verified by diffing against a stashed working tree; the repo has pre-existing drift in other files).

🤖 Generated with Claude Code

https://claude.ai/code/session_01BX9w4k8qecsH9LMgBP8hXc

Fixes the remaining half of #23. `return` with no value was a parse error,
because `parse_stmt` unconditionally parsed an operand and `Expr::Return`
held a non-optional `ExprID`:

    do_work(done: bool) {
        if done {
            return
        }
        value = value + 1
    }

    do_work.lyte:6: Expected expression

The diagnostic pointed at the line *after* the return, and in a larger file
the desynced parser cascaded into a run of bogus `Expected declaration`
errors, which is what made the failure hard to read.

Make the operand optional: `Expr::Return(Option<ExprID>)`. The parser treats
`return` as bare when nothing is left in the statement — `Endl`, `Rbrace`, or
end of input — and the checker types a bare return as void, so it unifies with
the enclosing function's return type through the machinery added in the
previous commit. A bare return in a value-returning function is now a clear
error rather than a parse failure:

    ❌ f.lyte:2:5: return type must match function return type: void vs i32
        return
        ^

Backends emit their existing void-return path for the bare case: `return_(&[])`
in Cranelift, `build_return(None)` in LLVM, `ReturnVoid` in the stack VM, and a
zero placeholder in r0 for the register VM, whose void epilogue ignores it.

Two things fixed along the way:

- `return g()` where `g` is itself void previously fed a placeholder value to
  a void signature in the Cranelift and stack backends. Both now emit the
  void return. (LLVM already special-cased this.)
- In the stack backend, `Expr::Return` in void context fell through to the
  generic "translate then drop" path, emitting a Drop after the return
  terminator. Return never falls through, so there is nothing to drop.

Verified on all four backends — Cranelift JIT, stack VM, register VM, and
LLVM — including bare returns inside loops, inside if/else, in lambdas, and
with unreachable code following.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BX9w4k8qecsH9LMgBP8hXc
@wtholliday
wtholliday merged commit 204f953 into fix-return-type-check Aug 8, 2026
8 checks passed
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