Skip to content

fix(lang): support return as a diverging expression in catch/match arms#3881

Merged
antoniosarosi merged 2 commits into
canaryfrom
antonio/catch-arm-return-error
Jun 29, 2026
Merged

fix(lang): support return as a diverging expression in catch/match arms#3881
antoniosarosi merged 2 commits into
canaryfrom
antonio/catch-arm-return-error

Conversation

@antoniosarosi

@antoniosarosi antoniosarosi commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Fixes B-246.

Problem

Writing a braceless return as a catch/match arm value fails because return is a statement, not an expression. The compiler emits a cascade of misleading errors where only the first identifies the real problem:

function foo() -> float {
    let x = risky() catch (e) {
        _ => return 0.0,   // <- braceless return
    };
    x + 1.0
}
error: Expected expression, found return            (E0010)  <- the real issue
error: Float literal values are not supported: 0.0  (E0010)  <- recovery artifact
error: Expected '=>' after catch pattern, found ','  (E0010) <- recovery artifact
error: operator `Add` cannot be applied to ...       (E0004) <- recovery artifact

The last three are downstream parse failures from recovery misalignment, not real bugs — they read as "this expression has four bugs."

Fix

Adopt the Rust model the ticket recommends: return now also parses as a RETURN_EXPR — a diverging expression of type never, exactly mirroring how throw already works. Since never unifies with any arm type, the arm type-checks without special-casing, while the returned value is still checked against the function's declared return type (identical typing to the block form => { return x }).

throw was already a diverging expression and worked braceless in arm position; this makes return symmetric.

Pipeline (mirrors throw at every stage)

  • Parserparse_return_expr produces RETURN_EXPR, recognized in parse_primary_expr. Statement-position return is still taken by parse_stmt first, so it stays a RETURN_STMT and no existing snapshots move.
  • AST — new Expr::Return { value: Option<ExprId> }; value extraction shared with Stmt::Return.
  • HIR / TIR / MIR — walk, type as never (+ check value against the declared return type, + defer-escape), and lower to a function-exit return (not routed through catch_context, so it returns from the function rather than being caught).
  • Formatter — wraps a braceless arm body into a block (as it already does for throw) and appends the ; a block-position return needs, so output round-trips (idempotent) for both catch and match arms.

Tests

  • baml_src/ns_catch_arm_return — executable catch- and match-arm cases asserting runtime behavior (early-return fires on throw; non-throw path flows normally; mixed arm types).
  • projects/compiles/catch_arm_return — clean compile + pins the formatter wrap for both arm kinds.
  • projects/diagnostic_errors/catch_arm_braceless_return — a wrong-typed braceless return now yields a single clear type mismatch: expected bool, got Errors, identical to the block form, instead of the four-error cascade.

Full baml_tests lib suite (1703) green; baml_src suite (1954 tests) green; parser/fmt/defer/exceptions suites green; clippy clean on all touched crates.

Summary by CodeRabbit

  • New Features

    • Added support for using return in expression positions, including match and catch arms, with correct control-flow behavior.
    • Braceless return arm bodies are now formatted appropriately when wrapped into blocks.
  • Bug Fixes

    • Improved scope traversal, throw handling, type-checking, and control-flow lowering for return expressions.
    • Enhanced diagnostics to avoid cascading errors and report clearer return-type mismatches in braceless return catch arms.
  • Tests

    • Added new compile/format and diagnostic regression coverage for return used as arm values.

…arms

Writing a braceless `return` as a `catch`/`match` arm value
(`_ => return 0`) used to fail: `return` was a statement, so the
expression parser rejected it and emitted a cascade of misleading
downstream errors (B-246):

    error: Expected expression, found return
    error: Float literal values are not supported: 0.0
    error: Expected '=>' after catch pattern, found ','
    error: operator `Add` cannot be applied to ...

Only the first pointed at the real problem; the rest were recovery
artifacts.

`return` now also parses as a `RETURN_EXPR` — a diverging expression of
type `never`, mirroring `throw`. `never` unifies with any arm type, so
the arm type-checks without special-casing, while the returned value is
still checked against the function's declared return type (identical
typing to the block form `=> { return x }`). Statement-position `return`
is unchanged (still a `RETURN_STMT`); the expression path only fires when
`return` is reached through expression parsing, so no existing snapshots
move.

The formatter wraps a braceless arm body into a block (as it already does
for `throw`) and appends the `;` a block-position `return` needs, so the
output round-trips (idempotent) for both catch and match arms.

Tests:
- ns_catch_arm_return: executable catch/match-arm cases (runtime behavior)
- compiles/catch_arm_return: clean compile + pins the formatter wrap
- diagnostic_errors/catch_arm_braceless_return: a wrong-typed braceless
  `return` now yields a single clear `type mismatch`, not the cascade
@vercel

vercel Bot commented Jun 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Jun 29, 2026 12:25am
promptfiddle Ready Ready Preview, Comment Jun 29, 2026 12:25am
promptfiddle2 Ready Ready Preview, Comment Jun 29, 2026 12:25am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 95178e45-87cf-4dfc-a144-ffc1b9325a13

📥 Commits

Reviewing files that changed from the base of the PR and between 1824fcd and 5719ccc.

⛔ Files ignored due to path filters (6)
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_arm_braceless_return/baml_tests__diagnostic_errors__catch_arm_braceless_return__01_lexer__repro.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_arm_braceless_return/baml_tests__diagnostic_errors__catch_arm_braceless_return__02_parser__repro.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_arm_braceless_return/baml_tests__diagnostic_errors__catch_arm_braceless_return__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_arm_braceless_return/baml_tests__diagnostic_errors__catch_arm_braceless_return__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_arm_braceless_return/baml_tests__diagnostic_errors__catch_arm_braceless_return__05_diagnostics.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_arm_braceless_return/baml_tests__diagnostic_errors__catch_arm_braceless_return__10_formatter__repro.snap is excluded by !**/*.snap
📒 Files selected for processing (1)
  • baml_language/crates/baml_tests/projects/diagnostic_errors/catch_arm_braceless_return/repro.baml

📝 Walkthrough

Walkthrough

Adds return as a diverging expression usable in catch and match arm values. The parser, AST, lowering, analysis, formatter, tests, diagnostics, and tooling output are updated to recognize and preserve expression-position returns.

Changes

Expression-position return in catch/match arms

Layer / File(s) Summary
Syntax and parser
baml_language/crates/baml_compiler_syntax/src/syntax_kind.rs, baml_language/crates/baml_compiler_syntax/src/ast.rs, baml_language/crates/baml_compiler_parser/src/parser.rs
RETURN_EXPR and ReturnExpr are added, along with parser support for return in expression position.
AST lowering
baml_language/crates/baml_compiler2_ast/src/ast.rs, baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs
Expr::Return { value } is added and RETURN_EXPR lowers through shared optional-value extraction into expression-position return nodes.
HIR and MIR flow
baml_language/crates/baml_compiler2_hir/src/builder.rs, baml_language/crates/baml_compiler2_mir/src/lower.rs
Return values are walked in HIR, and MIR lowering stores the value in _0, replays defers, unwinds watched locals, and jumps to the function exit.
TIR typing and analysis
baml_language/crates/baml_compiler2_tir/src/builder.rs, baml_language/crates/baml_compiler2_tir/src/throws_analysis.rs, baml_language/crates/baml_compiler2_visualization/src/control_flow/from_ast.rs
Expr::Return is handled in forward-reference collection, typing, throw-fact collection, throws analysis, and CFG callee-name collection.
Formatter support
baml_language/crates/baml_fmt/src/ast/expressions.rs
RETURN_EXPR maps to Expression::Return, printing preserves source ranges, never uses single-line formatting, and inserts semicolons when wrapping braceless return into arm blocks.
Tests, diagnostics, and renderers
baml_language/crates/baml_tests/baml_src/ns_catch_arm_return/catch_arm_return.baml, baml_language/crates/baml_tests/projects/compiles/catch_arm_return/catch_arm_return.baml, baml_language/crates/baml_tests/projects/diagnostic_errors/catch_arm_braceless_return/repro.baml, baml_language/crates/baml_tests/src/compiler2_tir/mod.rs, baml_language/crates/tools_onionskin/src/compiler.rs
Runtime and compile fixtures, a diagnostic repro, and expression renderers are updated for braceless return.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • BoundaryML/baml#3719: Shares formatter work in baml_fmt/src/ast/expressions.rs around catch-arm rendering.
  • BoundaryML/baml#3785: Shares TIR defer control-flow escape handling in baml_compiler2_tir/src/builder.rs.

Poem

I nibble on syntax, hop by hop,
A braceless return that won’t stop.
Through catch and match I dart away,
Then land in _0 and fade to gray.
The formatter tucks me in with care,
🐇 return now leaps through every layer.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the core change: enabling return as a diverging expression in catch/match arms.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch antonio/catch-arm-return-error

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@baml_language/crates/baml_tests/projects/diagnostic_errors/catch_arm_braceless_return/repro.baml`:
- Around line 23-26: The repro in caller() is not exhaustive, so it can trigger
an extra non-exhaustive/throws diagnostic in addition to the intended
return-type mismatch on return e. Update the catch arm handling in caller() by
either adding a fallback arm in the catch expression or narrowing callee() so it
throws only Errors.AuthenticationError, ensuring the fixture isolates the
return-type mismatch alone.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4813c0ad-b63c-46cd-8b18-477812b47f11

📥 Commits

Reviewing files that changed from the base of the PR and between 6b7d7bd and 1824fcd.

⛔ Files ignored due to path filters (16)
  • baml_language/crates/baml_tests/snapshots/baml_src/_root.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/baml_src/catch_arm_return.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/catch_arm_return/baml_tests__compiles__catch_arm_return__01_lexer__catch_arm_return.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/catch_arm_return/baml_tests__compiles__catch_arm_return__02_parser__catch_arm_return.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/catch_arm_return/baml_tests__compiles__catch_arm_return__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/catch_arm_return/baml_tests__compiles__catch_arm_return__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/catch_arm_return/baml_tests__compiles__catch_arm_return__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/catch_arm_return/baml_tests__compiles__catch_arm_return__05_diagnostics.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/catch_arm_return/baml_tests__compiles__catch_arm_return__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/catch_arm_return/baml_tests__compiles__catch_arm_return__10_formatter__catch_arm_return.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_arm_braceless_return/baml_tests__diagnostic_errors__catch_arm_braceless_return__01_lexer__repro.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_arm_braceless_return/baml_tests__diagnostic_errors__catch_arm_braceless_return__02_parser__repro.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_arm_braceless_return/baml_tests__diagnostic_errors__catch_arm_braceless_return__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_arm_braceless_return/baml_tests__diagnostic_errors__catch_arm_braceless_return__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_arm_braceless_return/baml_tests__diagnostic_errors__catch_arm_braceless_return__05_diagnostics.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_arm_braceless_return/baml_tests__diagnostic_errors__catch_arm_braceless_return__10_formatter__repro.snap is excluded by !**/*.snap
📒 Files selected for processing (16)
  • baml_language/crates/baml_compiler2_ast/src/ast.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs
  • baml_language/crates/baml_compiler2_hir/src/builder.rs
  • baml_language/crates/baml_compiler2_mir/src/lower.rs
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_compiler2_tir/src/throws_analysis.rs
  • baml_language/crates/baml_compiler2_visualization/src/control_flow/from_ast.rs
  • baml_language/crates/baml_compiler_parser/src/parser.rs
  • baml_language/crates/baml_compiler_syntax/src/ast.rs
  • baml_language/crates/baml_compiler_syntax/src/syntax_kind.rs
  • baml_language/crates/baml_fmt/src/ast/expressions.rs
  • baml_language/crates/baml_tests/baml_src/ns_catch_arm_return/catch_arm_return.baml
  • baml_language/crates/baml_tests/projects/compiles/catch_arm_return/catch_arm_return.baml
  • baml_language/crates/baml_tests/projects/diagnostic_errors/catch_arm_braceless_return/repro.baml
  • baml_language/crates/baml_tests/src/compiler2_tir/mod.rs
  • baml_language/crates/tools_onionskin/src/compiler.rs

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown

Binary size checks passed

7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
baml-cli Linux 🔒 21.5 MB 9.2 MB file 21.5 MB +8.0 KB (+0.0%) OK
packed-program Linux 🔒 15.6 MB 6.6 MB file 15.6 MB +6.9 KB (+0.0%) OK
baml-cli macOS 🔒 16.6 MB 8.0 MB file 16.6 MB +0 B (+0.0%) OK
packed-program macOS 🔒 12.1 MB 5.7 MB file 12.1 MB +16 B (+0.0%) OK
baml-cli Windows 🔒 18.1 MB 8.2 MB file 18.1 MB +6.7 KB (+0.0%) OK
packed-program Windows 🔒 13.0 MB 5.8 MB file 13.0 MB +4.8 KB (+0.0%) OK
bridge_wasm WASM 14.3 MB 🔒 4.1 MB gzip 4.1 MB +4.6 KB (+0.1%) OK

🔒 = the size this artifact is GATED on (ceiling + delta). Binaries gate on file size (installed binary); WASM gates on gzip (download size). The other size is shown for information only.


Generated by cargo size-gate · workflow run

CodeRabbit: the `caller()` repro caught only one of `Errors`' four variants,
so the fixture emitted an `extraneous throws` warning alongside the intended
return-type mismatch. Make the catch arm exhaustive (`throws string` + a
`string =>` arm) so the fixture isolates exactly one diagnostic — the
`type mismatch: expected bool, got string` on `return e` — which is the point
the regression is meant to pin (no cascade, single clear error).
@antoniosarosi antoniosarosi enabled auto-merge June 29, 2026 00:13
@antoniosarosi antoniosarosi added this pull request to the merge queue Jun 29, 2026
Merged via the queue into canary with commit 8305061 Jun 29, 2026
86 of 87 checks passed
@antoniosarosi antoniosarosi deleted the antonio/catch-arm-return-error branch June 29, 2026 01:01
codeshaunted added a commit that referenced this pull request Jun 29, 2026
Latest canary (catch/match-arm `return` support, #3881). Clean merge with
no conflicts; workspace compiles and 750 affected-crate tests pass.
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