baml_language: add if-let, while-let, and let-else#3525
Conversation
Adds Rust-style pattern-matching control flow on top of the existing
match infrastructure. Each construct desugars at AST lowering so HIR /
TIR / MIR / codegen see only match expressions.
- `if let <pat> = <expr> { then } [else { else }]`
→ match (<expr>) { <pat> => <then>, _ => <else_or_empty> }
- `while let <pat> = <expr> { body }`
→ while (true) { match (<expr>) { <pat> => <body>, _ => { break } } }
- `let <pat> = <expr> else { div };`
→ let <pat> = match (<expr>) { <pat> => <binding>, _ => { div } };
(or a stand-alone match statement when the pattern binds no name)
Parser adds two new syntax kinds (IF_LET_EXPR, WHILE_LET_STMT) and
extends LET_STMT with an optional `else { ... }` block. The
parse_let_header_no_else helper keeps the if-let/while-let header from
swallowing an `else` that belongs to the enclosing construct.
The desugared synthetic AST nodes use zero-width spans at the end of
the enclosing statement so HIR's position-based scope_at_offset doesn't
let them shadow user-written body scopes. Without this, references to
pattern bindings inside the body would fail to resolve and codegen
would silently emit Constant::Null.
Formatter is updated end-to-end: LetStmt gains an optional
`let_else: Option<(Else, BlockExpr)>` and a shared `print_header`
helper so IfLetExpr / WhileLetStmt can reuse the trivia-preserving
header printing instead of re-implementing it (which would silently
drop comments around `=`). ElseExpr gets an `IfLet` arm so `else if let`
chains print correctly.
Tests
-----
- 17 compile-tier .baml files exercising the new constructs end-to-
end (lexer → parser → HIR → TIR → MIR → codegen → formatter).
- 61 runtime tests in baml_tests/tests/{if_let,while_let,let_else,
let_constructs_extra}.rs covering: success/mismatch paths,
shadowing, nesting, lambda capture, else-if-let chains, scrutinee
evaluation order (once vs. per-iteration), binding mutability,
null/optional and user-class narrows, for-loop interaction,
throwing scrutinee.
- 13 formatter trivia tests in baml_fmt/tests/let_constructs_fmt.rs
covering line + block comments at every header/body position,
blank-line preservation, and format-is-idempotent for each
construct.
- Scope-bleed regressions in diagnostic_errors/let_bindings_scope/
(bindings leaking past `}`, into else, into sibling arms) — each
produces `unresolved name`.
- Refutability assertions in diagnostic_errors/let_refutability/ —
eight cases violating Rust's rule that if-let / while-let /
let-else require refutable patterns. Currently caught via BAML's
match-arm exhaustiveness as `unreachable arm`; snapshots act as a
migration hook for when a dedicated diagnostic gets wired up.
- Malformed-syntax recovery cases in broken_syntax/let_constructs.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis pull request introduces Rust-inspired pattern-matching constructs to BAML: ChangesPattern matching constructs (if let, while let, let...else)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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_compiler_parser/src/parser.rs`:
- Around line 2932-2940: parse_let_header_no_else currently calls
parse_let_stmt_inner which eats a trailing semicolon (via
p.eat(TokenKind::Semicolon)), causing header parsing used by if/while/for to
incorrectly accept statement-level semicolons; change the code so
parse_let_header_no_else does not trigger that semicolon consumption — either
add a parameter to parse_let_stmt_inner (e.g., allow_trailing_semicolon: bool)
and pass false from parse_let_header_no_else, or extract the header-parsing
portion into a new helper (e.g., parse_let_header_inner) that omits the
p.eat(TokenKind::Semicolon) call; update callers accordingly (including the
other header caller around lines ~2974-2984) so only statement-level let parsing
consumes the semicolon.
In `@baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs`:
- Around line 3267-3276: The else-arm of a `let ... else` is being lowered into
a normal match arm (via lower_expr, alloc_pattern, MatchArm, alloc_match_arm)
without ensuring it actually diverges, so non-diverging else blocks slip
through; before constructing arm_div call into the divergence-check logic (e.g.,
check that the lowered else_block_node is syntactically/semantically divergent
or that self.lower_expr(else_block_node) produces a Divergent kind) and if it
does not, emit a compile error rejecting the `let ... else` form, otherwise
proceed to allocate the wildcard pattern and match arm as now; ensure the check
happens in the same lowering branch that creates arm_div so downstream phases
retain the guarantee that the else arm must diverge.
- Around line 3287-3307: The code drops WATCH_LET semantics by always emitting
Stmt::Let { is_watched: false, ... } in the binding branch and returning a plain
Stmt::Expr in the no-binding branch; fix by propagating the "watched" flag into
both branches: determine whether this lowering came from a WATCH_LET (e.g., a
boolean like is_watched or by inspecting the incoming node from
lower_block_expr), then use that flag when constructing the outer let (set
Stmt::Let { is_watched: true/flag, ... } instead of false) and, in the
no-binding path, do not return Stmt::Expr(match_expr) — allocate a Stmt::Let
with outer_pattern and initializer Some(match_expr) and is_watched set to the
same flag (or otherwise ensure a watched-let sentinel is emitted) so WATCH_LET
semantics are preserved; update uses of alloc_stmt, outer_let, match_stmt,
binding_name, outer_pattern, and Stmt::Let accordingly.
🪄 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: 02989fac-5daf-420b-b43c-a0739a969954
⛔ Files ignored due to path filters (38)
baml_language/crates/baml_tests/snapshots/broken_syntax/let_constructs/baml_tests__broken_syntax__let_constructs__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/broken_syntax/let_constructs/baml_tests__broken_syntax__let_constructs__02_parser__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/broken_syntax/let_constructs/baml_tests__broken_syntax__let_constructs__05_diagnostics.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__01_lexer__if_let.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__01_lexer__if_let_edge_cases.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__01_lexer__let_else.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__01_lexer__let_else_edge_cases.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__01_lexer__while_let.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__01_lexer__while_let_edge_cases.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__02_parser__if_let.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__02_parser__if_let_edge_cases.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__02_parser__let_else.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__02_parser__let_else_edge_cases.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__02_parser__while_let.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__02_parser__while_let_edge_cases.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__05_diagnostics.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__10_formatter__if_let.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__10_formatter__if_let_edge_cases.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__10_formatter__let_else.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__10_formatter__let_else_edge_cases.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__10_formatter__while_let.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/parser_statements/baml_tests__compiles__parser_statements__10_formatter__while_let_edge_cases.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/let_bindings_scope/baml_tests__diagnostic_errors__let_bindings_scope__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/let_bindings_scope/baml_tests__diagnostic_errors__let_bindings_scope__02_parser__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/let_bindings_scope/baml_tests__diagnostic_errors__let_bindings_scope__03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/let_bindings_scope/baml_tests__diagnostic_errors__let_bindings_scope__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/let_bindings_scope/baml_tests__diagnostic_errors__let_bindings_scope__05_diagnostics.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/let_bindings_scope/baml_tests__diagnostic_errors__let_bindings_scope__10_formatter__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/let_refutability/baml_tests__diagnostic_errors__let_refutability__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/let_refutability/baml_tests__diagnostic_errors__let_refutability__02_parser__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/let_refutability/baml_tests__diagnostic_errors__let_refutability__03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/let_refutability/baml_tests__diagnostic_errors__let_refutability__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/let_refutability/baml_tests__diagnostic_errors__let_refutability__05_diagnostics.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/let_refutability/baml_tests__diagnostic_errors__let_refutability__10_formatter__main.snapis excluded by!**/*.snap
📒 Files selected for processing (20)
baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rsbaml_language/crates/baml_compiler_parser/src/parser.rsbaml_language/crates/baml_compiler_syntax/src/ast.rsbaml_language/crates/baml_compiler_syntax/src/syntax_kind.rsbaml_language/crates/baml_fmt/src/ast/expressions.rsbaml_language/crates/baml_fmt/src/ast/statements.rsbaml_language/crates/baml_fmt/tests/let_constructs_fmt.rsbaml_language/crates/baml_tests/projects/broken_syntax/let_constructs/main.bamlbaml_language/crates/baml_tests/projects/compiles/parser_statements/if_let.bamlbaml_language/crates/baml_tests/projects/compiles/parser_statements/if_let_edge_cases.bamlbaml_language/crates/baml_tests/projects/compiles/parser_statements/let_else.bamlbaml_language/crates/baml_tests/projects/compiles/parser_statements/let_else_edge_cases.bamlbaml_language/crates/baml_tests/projects/compiles/parser_statements/while_let.bamlbaml_language/crates/baml_tests/projects/compiles/parser_statements/while_let_edge_cases.bamlbaml_language/crates/baml_tests/projects/diagnostic_errors/let_bindings_scope/main.bamlbaml_language/crates/baml_tests/projects/diagnostic_errors/let_refutability/main.bamlbaml_language/crates/baml_tests/tests/if_let.rsbaml_language/crates/baml_tests/tests/let_constructs_extra.rsbaml_language/crates/baml_tests/tests/let_else.rsbaml_language/crates/baml_tests/tests/while_let.rs
| /// Parse a let-header (pattern + initializer) wrapped in a `LET_STMT` node. | ||
| /// | ||
| /// Used by `if let`, `while let`, and for-in to embed a pattern + value | ||
| /// without the surrounding statement-level semantics. In those contexts a | ||
| /// trailing `else` always belongs to the enclosing construct, never to a | ||
| /// let-else binding. | ||
| fn parse_let_header_no_else(&mut self) { | ||
| self.parse_let_stmt_inner(/* allow_let_else = */ false); | ||
| } |
There was a problem hiding this comment.
Don't let header parsing consume statement semicolons.
parse_let_header_no_else() still routes through parse_let_stmt_inner(), so if let x = y; { ... } / while let x = y; { ... } are accepted even though the helper is supposed to strip statement-level syntax. The same shared p.eat(TokenKind::Semicolon) also makes let ... else { ... } accept a missing trailing ;, which contradicts the grammar described in this PR.
Suggested fix
fn parse_let_stmt(&mut self) {
- self.parse_let_stmt_inner(/* allow_let_else = */ true);
+ self.parse_let_stmt_inner(/* allow_let_else = */ true, /* allow_semicolon = */ true);
}
fn parse_let_header_no_else(&mut self) {
- self.parse_let_stmt_inner(/* allow_let_else = */ false);
+ self.parse_let_stmt_inner(/* allow_let_else = */ false, /* allow_semicolon = */ false);
}
-fn parse_let_stmt_inner(&mut self, allow_let_else: bool) {
+fn parse_let_stmt_inner(&mut self, allow_let_else: bool, allow_semicolon: bool) {
self.with_node(SyntaxKind::LET_STMT, |p| {
+ let mut saw_let_else = false;
+
// pattern / initializer...
- if allow_let_else && p.at(TokenKind::Else) {
+ if allow_let_else && p.at(TokenKind::Else) {
+ saw_let_else = true;
p.bump(); // else
if p.at(TokenKind::LBrace) {
p.parse_block_expr();
} else {
p.error_unexpected_token("block after 'else' in let-else".to_string());
}
}
- p.eat(TokenKind::Semicolon);
+ if allow_semicolon {
+ if saw_let_else {
+ p.expect(TokenKind::Semicolon);
+ } else {
+ p.eat(TokenKind::Semicolon);
+ }
+ }
});
}Also applies to: 2974-2984
🤖 Prompt for 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.
In `@baml_language/crates/baml_compiler_parser/src/parser.rs` around lines 2932 -
2940, parse_let_header_no_else currently calls parse_let_stmt_inner which eats a
trailing semicolon (via p.eat(TokenKind::Semicolon)), causing header parsing
used by if/while/for to incorrectly accept statement-level semicolons; change
the code so parse_let_header_no_else does not trigger that semicolon consumption
— either add a parameter to parse_let_stmt_inner (e.g.,
allow_trailing_semicolon: bool) and pass false from parse_let_header_no_else, or
extract the header-parsing portion into a new helper (e.g.,
parse_let_header_inner) that omits the p.eat(TokenKind::Semicolon) call; update
callers accordingly (including the other header caller around lines ~2974-2984)
so only statement-level let parsing consumes the semicolon.
| // Match-diverge arm: lowers the user's else block. Expected to | ||
| // diverge (return / throw / break / continue) — same as Rust. | ||
| let div_body = self.lower_expr(else_block_node); | ||
| let wildcard = self.alloc_pattern(Pattern::Wildcard, synth_range); | ||
| let arm_div = MatchArm { | ||
| pattern: wildcard, | ||
| guard: None, | ||
| body: div_body, | ||
| }; | ||
| let arm_div_id = self.alloc_match_arm(arm_div, else_block_node.text_range()); |
There was a problem hiding this comment.
Reject non-diverging let ... else blocks before desugaring.
Once this branch is lowered into a plain match, downstream phases can no longer tell that the else arm was required to diverge. That means forms like let x: int = value else { 0 }; become normal value-producing matches and keep executing, which breaks the feature's stated Rust-style semantics.
🤖 Prompt for 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.
In `@baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs` around lines
3267 - 3276, The else-arm of a `let ... else` is being lowered into a normal
match arm (via lower_expr, alloc_pattern, MatchArm, alloc_match_arm) without
ensuring it actually diverges, so non-diverging else blocks slip through; before
constructing arm_div call into the divergence-check logic (e.g., check that the
lowered else_block_node is syntactically/semantically divergent or that
self.lower_expr(else_block_node) produces a Divergent kind) and if it does not,
emit a compile error rejecting the `let ... else` form, otherwise proceed to
allocate the wildcard pattern and match arm as now; ensure the check happens in
the same lowering branch that creates arm_div so downstream phases retain the
guarantee that the else arm must diverge.
| if binding_name.is_some() { | ||
| // Binding case — outer let binds the pattern's names from the | ||
| // match result. | ||
| let outer_let = self.alloc_stmt( | ||
| Stmt::Let { | ||
| pattern: outer_pattern, | ||
| initializer: Some(match_expr), | ||
| is_watched: false, | ||
| origin: LetOrigin::Source, | ||
| }, | ||
| range, | ||
| ); | ||
| vec![outer_let] | ||
| } else { | ||
| // No-binding case — the match is a statement on its own; we | ||
| // discard `outer_pattern` since it doesn't introduce any names | ||
| // and its only purpose was the type/wildcard check that the arm | ||
| // pattern already performs. | ||
| let _ = outer_pattern; | ||
| let match_stmt = self.alloc_stmt(Stmt::Expr(match_expr), range); | ||
| vec![match_stmt] |
There was a problem hiding this comment.
Preserve WATCH_LET semantics in the let-else path.
lower_block_expr routes WATCH_LET nodes here, but the binding case always emits Stmt::Let { is_watched: false, ... }, and the no-binding branch drops the watched binding entirely by returning only a Stmt::Expr. watch let ... else will therefore behave like a plain let/match instead of a watched binding.
🤖 Prompt for 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.
In `@baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs` around lines
3287 - 3307, The code drops WATCH_LET semantics by always emitting Stmt::Let {
is_watched: false, ... } in the binding branch and returning a plain Stmt::Expr
in the no-binding branch; fix by propagating the "watched" flag into both
branches: determine whether this lowering came from a WATCH_LET (e.g., a boolean
like is_watched or by inspecting the incoming node from lower_block_expr), then
use that flag when constructing the outer let (set Stmt::Let { is_watched:
true/flag, ... } instead of false) and, in the no-binding path, do not return
Stmt::Expr(match_expr) — allocate a Stmt::Let with outer_pattern and initializer
Some(match_expr) and is_watched set to the same flag (or otherwise ensure a
watched-let sentinel is emitted) so WATCH_LET semantics are preserved; update
uses of alloc_stmt, outer_let, match_stmt, binding_name, outer_pattern, and
Stmt::Let accordingly.
Binary size checks failed❌ 1 violations · ✅ 6 passed
Details & how to fixViolations:
Add/update baselines:
[artifacts.bridge_cffi]
file_bytes = 17666544
stripped_bytes = 17666536
gzip_bytes = 6592737Generated by |
Summary
Adds Rust-style pattern-matching control flow on top of the existing match infrastructure. Each construct desugars at AST lowering so HIR / TIR / MIR / codegen see only match expressions.
if let <pat> = <expr> { then } [else { else }]match (<expr>) { <pat> => <then>, _ => <else_or_empty> }while let <pat> = <expr> { body }while (true) { match (<expr>) { <pat> => <body>, _ => { break } } }let <pat> = <expr> else { div };let <pat> = match (<expr>) { <pat> => <binding>, _ => { div } };(or stand-alone match for wildcard patterns)The parser gets two new syntax kinds (
IF_LET_EXPR,WHILE_LET_STMT) andLET_STMTis extended with an optionalelse { ... }block. Aparse_let_header_no_elsehelper keeps the if-let / while-let header from accidentally swallowing anelsethat belongs to the enclosing construct.Synthetic AST nodes get zero-width spans at the end of the enclosing statement so HIR's position-based
scope_at_offsetdoesn't let them shadow user-written body scopes — without this, references to pattern bindings inside the body would fail to resolve and codegen would silently emitConstant::Null.Formatter
LetStmt::print_headeris factored out and shared withIfLetExpr::print/WhileLetStmt::printso comment trivia aroundlet <pat> = <expr>survives the formatter.ElseExprgets anIfLetarm soelse if letchains print correctly.LetStmtgains an optionallet_else: Option<(Else, BlockExpr)>.Tests
crates/baml_tests/tests/{if_let,while_let,let_else,let_constructs_extra}.rs. These execute the compiled bytecode and assert the produced value — covering success / mismatch paths, shadowing, nesting, lambda capture,else if letchains, scrutinee evaluation order (once for if-let / let-else, once-per-iteration for while-let), binding mutability, null / optional and user-class narrows, throwing scrutinee, for-loop interaction.crates/baml_fmt/tests/let_constructs_fmt.rscovering line + block comments at every position around the header and body, blank-line preservation, andformat(format(x)) == format(x)idempotency for each construct.crates/baml_tests/projects/compiles/parser_statements/running through lexer → parser → HIR → TIR → MIR → codegen → formatter.diagnostic_errors/let_bindings_scope/(binding leaks past}, into else, into sibling arms — all produceunresolved name).diagnostic_errors/let_refutability/— 8 cases violating Rust's rule that if-let / while-let / let-else require refutable patterns. Currently caught via BAML's match-arm exhaustiveness asunreachable arm; the snapshots act as a migration hook for when a dedicated diagnostic gets wired up.broken_syntax/let_constructs/covering missing pattern, missing=, missing initializer, missing body, missing else block, non-block else, missing semicolon,else if letwithout=.Test plan
Summary by CodeRabbit
New Features
if letexpressions enabling type-safe pattern matching with optionalelsebranches for fallthrough handlingwhile letloops supporting pattern-based conditional iteration that terminates when patterns no longer matchlet ... elsestatements for robust pattern binding with mandatory divergence paths (return, throw, break, continue)