Skip to content

Treat underscore patterns as explicit discards#3402

Open
rossirpaulo wants to merge 1 commit intocanaryfrom
feature/discard-bindings
Open

Treat underscore patterns as explicit discards#3402
rossirpaulo wants to merge 1 commit intocanaryfrom
feature/discard-bindings

Conversation

@rossirpaulo
Copy link
Copy Markdown
Contributor

@rossirpaulo rossirpaulo commented Apr 23, 2026

Summary

  • Introduce explicit discard pattern variants for _ and typed _, instead of modeling them as bindings named _.
  • Update AST lowering, HIR, MIR, and TIR to treat discard patterns as non-binding while still preserving type annotations and validation.
  • Fix control-flow, catch handling, and type inference so discard patterns do not create locals, duplicate definitions, or accidental name resolution for _.
  • Add syntax and compiler tests covering discard lets, typed discards, matches, catches, and unresolved _ references.

Testing

  • Added HIR tests for discard let bindings not creating duplicate definitions or visible scope bindings.
  • Added TIR tests for discard patterns in matches and catch clauses.
  • Added LSP syntax test coverage in discard_bindings.baml.
  • Not run

Summary by CodeRabbit

  • New Features

    • Added support for discard patterns to explicitly ignore values in let bindings, for loops, and match expressions
    • Added support for typed discard patterns with explicit type annotations
    • Discard patterns create no variable bindings and cannot be referenced
  • Tests

    • Added test coverage for discard binding functionality

- Add discard and typed-discard AST patterns
- Prevent `_` from creating bindings in lowering, HIR, TIR, and MIR
- Add syntax and compiler tests for discard bindings
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 23, 2026

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

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Apr 23, 2026 1:13pm
promptfiddle Ready Ready Preview, Comment Apr 23, 2026 1:13pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

The PR introduces discard pattern support (_ and typed _: T) to the BAML compiler's AST and updates all compilation phases to handle discards structurally, avoiding string-name comparisons and unnecessary local-variable binding.

Changes

Cohort / File(s) Summary
AST Pattern Variants
baml_language/crates/baml_compiler2_ast/src/ast.rs
Added Discard and TypedDiscard { ty: TypeExpr } variants to Pattern enum. Introduced binding_name() helper returning Option<&Name> (only for binding variants) and is_discard() boolean check.
Lowering & Type Validation
baml_language/crates/baml_compiler2_ast/src/disambiguate.rs, baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs
Updated pattern lowering to treat _ as explicit discard (not binding) across match arms, catch clauses, typed patterns, let statements, and for-loops. Extended type-annotation validation to handle typed discards.
HIR & TIR Builders
baml_language/crates/baml_compiler2_hir/src/builder.rs, baml_language/crates/baml_compiler2_tir/src/builder.rs
Refactored pattern binding extraction to use binding_name() helper. Extended match/catch exhaustiveness logic and type validation to account for discard patterns without materializing locals.
MIR Lowering
baml_language/crates/baml_compiler2_mir/src/lower.rs
Updated let/for/match/catch lowering to handle discard patterns structurally (no binding/local generation) instead of string-name comparisons. Extended wildcard detection in switch optimization to recognize AstPattern::Discard.
Catch Binding Collection
baml_language/crates/baml_compiler2_tir/src/throw_inference.rs
Replaced enum variant matching with binding_name() accessor for extracting catch-binding names.
Control Flow Visualization
baml_language/crates/baml_compiler2_visualization/src/control_flow/from_ast.rs
Added pattern-label rendering for untyped and typed discard patterns as _.
Tests
baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/expr/discard_bindings.baml, baml_language/crates/baml_tests/src/compiler2_hir.rs, baml_language/crates/baml_tests/src/compiler2_tir/mod.rs
Added syntax/semantic tests validating discard binding behavior (untyped let _, typed let _: T, for _ in), type checking, and snapshot rendering of discard patterns.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A discard named underscore, quite plain,
No binding, no local—a clean refrain!
Through HIR and MIR, through TIR it flies,
Pattern-matched, type-checked, before our eyes.
One wildcard pattern to rule them all... then drop 'em! ✨

🚥 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 and concisely summarizes the main change: introducing explicit discard patterns for underscore _ identifiers throughout the compiler pipeline.
Docstring Coverage ✅ Passed Docstring coverage is 86.84% 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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/discard-bindings

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
baml_language/crates/baml_compiler2_tir/src/builder.rs (1)

2876-2903: ⚠️ Potential issue | 🟡 Minor

Fix _ arm unreachable warnings in CatchAllPanics clauses.

Pattern::Discard (_) returns None for the panic subset, causing has_panic_component to be false. When a CatchAllPanics clause has no remaining throws to match, the check matches.may_match.is_empty() && !has_panic_component will incorrectly warn that a _ arm is unreachable, even though _ in CatchAllPanics should always be reachable (panics occur at runtime regardless of declared throws). Special-case _ patterns when clause.kind == CatchAllPanics to skip the unreachable warning. Add a regression test: catch all panics { _ => ... } with no declared throws should not emit UnreachableArm diagnostics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@baml_language/crates/baml_compiler2_tir/src/builder.rs` around lines 2876 -
2903, The unreachable-`_` warning arises because Pattern::Discard returns None
for ty_panic_subset, making has_panic_component false; modify the reachability
check used when validating clause arms to special-case the `Pattern::Discard`
(the `_` arm) when `clause.kind == CatchAllPanics` so it is treated as having a
panic component and thus never flagged unreachable. Concretely, in the
arm-reachability logic where you compute `has_panic_component` from
`ty_panic_subset` and check `matches.may_match.is_empty() &&
!has_panic_component`, change that condition to consider `(clause.kind ==
CatchAllPanics && pattern is Pattern::Discard)` as equivalent to
`has_panic_component` (or bypass the unreachable diagnostic for that case). Add
a regression test `catch all panics { _ => ... }` with no declared throws to
assert no UnreachableArm is emitted.
baml_language/crates/baml_compiler2_mir/src/lower.rs (1)

4977-4993: ⚠️ Potential issue | 🟡 Minor

Skip stack-trace local creation for discarded stack traces.

binding_name() returns None for _, but this map still declares an anonymous stack_trace_local and records it in the catch region. That keeps _ out of scope, but it still materializes a local for an explicit discard.

Proposed fix
         let stack_trace_local = clauses.first().and_then(|c| {
-            c.stack_trace_binding.map(|st_pat| {
-                let st_name = self.body.patterns[st_pat].binding_name().cloned();
+            c.stack_trace_binding.and_then(|st_pat| {
+                let st_pat = &self.body.patterns[st_pat];
+                if st_pat.is_discard() {
+                    return None;
+                }
+                let st_name = st_pat.binding_name().cloned()?;
                 let local = self.builder.declare_local(
-                    st_name.clone(),
+                    Some(st_name.clone()),
                     Ty::BuiltinUnknown {
                         attr: TyAttr::default(),
                     },
                     None,
                     false,
                 );
-                if let Some(name) = st_name {
-                    self.locals.insert(name, local);
-                }
-                local
+                self.locals.insert(st_name, local);
+                Some(local)
             })
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@baml_language/crates/baml_compiler2_mir/src/lower.rs` around lines 4977 -
4993, The code currently creates a stack_trace_local even when the pattern's
binding_name() is None (i.e., `_`), so change the closure used for
c.stack_trace_binding handling to only declare and insert a local when
binding_name() returns Some(name): fetch st_name =
self.body.patterns[st_pat].binding_name(), if st_name.is_none() return None,
otherwise call self.builder.declare_local(...) and then self.locals.insert(name,
local) and return local; update the stack_trace_local computation (the
map/and_then chain around clauses.first(), c.stack_trace_binding, declare_local,
and self.locals.insert) accordingly so discarded `_` stack traces do not
materialize a local.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs`:
- Around line 1048-1050: The code currently uses
self.patterns.alloc(Pattern::Discard) and self.exprs.alloc(Expr::Missing)
bypassing the arena helpers, which desynchronizes patterns/exprs from
source_map.pattern_spans; replace those direct alloc calls in the match-arm
construction with the proper helpers (call self.alloc_pattern(Pattern::Discard)
and self.alloc_expr(Expr::Missing) or the existing alloc_* methods used
elsewhere in lower_expr_body.rs) so recovered Pattern::Discard and Expr::Missing
are recorded in the source maps (keep references to patterns, exprs,
alloc_pattern, alloc_expr, Pattern::Discard, Expr::Missing and
source_map.pattern_spans).

---

Outside diff comments:
In `@baml_language/crates/baml_compiler2_mir/src/lower.rs`:
- Around line 4977-4993: The code currently creates a stack_trace_local even
when the pattern's binding_name() is None (i.e., `_`), so change the closure
used for c.stack_trace_binding handling to only declare and insert a local when
binding_name() returns Some(name): fetch st_name =
self.body.patterns[st_pat].binding_name(), if st_name.is_none() return None,
otherwise call self.builder.declare_local(...) and then self.locals.insert(name,
local) and return local; update the stack_trace_local computation (the
map/and_then chain around clauses.first(), c.stack_trace_binding, declare_local,
and self.locals.insert) accordingly so discarded `_` stack traces do not
materialize a local.

In `@baml_language/crates/baml_compiler2_tir/src/builder.rs`:
- Around line 2876-2903: The unreachable-`_` warning arises because
Pattern::Discard returns None for ty_panic_subset, making has_panic_component
false; modify the reachability check used when validating clause arms to
special-case the `Pattern::Discard` (the `_` arm) when `clause.kind ==
CatchAllPanics` so it is treated as having a panic component and thus never
flagged unreachable. Concretely, in the arm-reachability logic where you compute
`has_panic_component` from `ty_panic_subset` and check
`matches.may_match.is_empty() && !has_panic_component`, change that condition to
consider `(clause.kind == CatchAllPanics && pattern is Pattern::Discard)` as
equivalent to `has_panic_component` (or bypass the unreachable diagnostic for
that case). Add a regression test `catch all panics { _ => ... }` with no
declared throws to assert no UnreachableArm is emitted.
🪄 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: 16f37261-acec-421f-95da-2e3b9a6b3f29

📥 Commits

Reviewing files that changed from the base of the PR and between ea9547a and e199b9d.

📒 Files selected for processing (11)
  • baml_language/crates/baml_compiler2_ast/src/ast.rs
  • baml_language/crates/baml_compiler2_ast/src/disambiguate.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/throw_inference.rs
  • baml_language/crates/baml_compiler2_visualization/src/control_flow/from_ast.rs
  • baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/expr/discard_bindings.baml
  • baml_language/crates/baml_tests/src/compiler2_hir.rs
  • baml_language/crates/baml_tests/src/compiler2_tir/mod.rs

Comment on lines +1048 to 1050
pattern: pattern.unwrap_or_else(|| self.patterns.alloc(Pattern::Discard)),
guard,
body: body.unwrap_or_else(|| self.exprs.alloc(Expr::Missing)),
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.

⚠️ Potential issue | 🟡 Minor

Keep recovered match-arm nodes in the source maps.

Line 1048 bypasses alloc_pattern, so a missing-pattern recovery arm desynchronizes patterns from source_map.pattern_spans; later pattern spans in the same body can fall back to defaults. The adjacent missing-body fallback has the same arena/source-map hazard.

Proposed fix
         let arm = MatchArm {
-            pattern: pattern.unwrap_or_else(|| self.patterns.alloc(Pattern::Discard)),
+            pattern: pattern.unwrap_or_else(|| self.alloc_pattern(Pattern::Discard, arm_span)),
             guard,
-            body: body.unwrap_or_else(|| self.exprs.alloc(Expr::Missing)),
+            body: body.unwrap_or_else(|| self.alloc_expr(Expr::Missing, arm_span)),
         };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pattern: pattern.unwrap_or_else(|| self.patterns.alloc(Pattern::Discard)),
guard,
body: body.unwrap_or_else(|| self.exprs.alloc(Expr::Missing)),
pattern: pattern.unwrap_or_else(|| self.alloc_pattern(Pattern::Discard, arm_span)),
guard,
body: body.unwrap_or_else(|| self.alloc_expr(Expr::Missing, arm_span)),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs` around lines
1048 - 1050, The code currently uses self.patterns.alloc(Pattern::Discard) and
self.exprs.alloc(Expr::Missing) bypassing the arena helpers, which
desynchronizes patterns/exprs from source_map.pattern_spans; replace those
direct alloc calls in the match-arm construction with the proper helpers (call
self.alloc_pattern(Pattern::Discard) and self.alloc_expr(Expr::Missing) or the
existing alloc_* methods used elsewhere in lower_expr_body.rs) so recovered
Pattern::Discard and Expr::Missing are recorded in the source maps (keep
references to patterns, exprs, alloc_pattern, alloc_expr, Pattern::Discard,
Expr::Missing and source_map.pattern_spans).

@github-actions
Copy link
Copy Markdown

Binary size checks passed

7 passed

Artifact Platform Gzip Baseline Delta Status
bridge_cffi Linux 6.0 MB 5.7 MB +357.4 KB (+6.3%) OK
bridge_cffi-stripped Linux 6.0 MB 5.7 MB +325.4 KB (+5.7%) OK
bridge_cffi macOS 5.0 MB 4.6 MB +339.6 KB (+7.4%) OK
bridge_cffi-stripped macOS 4.9 MB 4.7 MB +272.2 KB (+5.8%) OK
bridge_cffi Windows 5.0 MB 4.6 MB +343.4 KB (+7.4%) OK
bridge_cffi-stripped Windows 4.9 MB 4.7 MB +285.9 KB (+6.1%) OK
bridge_wasm WASM 3.3 MB 3.2 MB +40.6 KB (+1.3%) OK

Generated by cargo size-gate · workflow run

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