Fix generic match exhaustiveness so null can't leak into a non-nullable T (B-633)#3904
Fix generic match exhaustiveness so null can't leak into a non-nullable T (B-633)#3904antoniosarosi wants to merge 1 commit into
null can't leak into a non-nullable T (B-633)#3904Conversation
…able `T` (B-633)
A `match` arm `let x: T` over a union scrutinee `T | <concrete>` (T a rigid
type variable) was treated as covering the concrete member too, so
`match (v: T?) { let x: T => x }` was wrongly deemed exhaustive and `null`
could bind into a non-nullable `T` at runtime — while the concrete control
`match (v: int?) { let x: int => x }` correctly errored E0062.
Static root cause: `union_targets_for_pattern` used a symmetric overlap where
`atoms_overlap` reports a `TypeVar` as overlapping everything, so a bare
`let x: T` pattern claimed concrete union members. Fix: make the pattern-side
overlap directional — a bare top-level `TypeVar` pattern claims only the
union's `TypeVar` member(s), never `null`/`string`. This also removes a
symmetric false E0063 "unreachable arm" on an explicit `null =>` arm.
Runtime root cause: once `let x: T` is no longer an over-claiming catch-all,
its runtime type test was emitted as constant-`false` (a bare `TypeArgRef`
erased to `Void`), so the arm never matched. Fix: reify the test — resolve
`T` against the frame's realized type args and test the value against the
resolved type. A class value matches an interface `T` it implements (via
`type_implements`), so nullable-`T` array iteration and interface-typed
iterators keep working.
- baml_compiler2_tir/src/builder.rs: pattern-side directional overlap.
- baml_compiler2_mir/src/lower.rs: route a bare `let x: T` through
`tir2_to_dispatch_guard_template`, matching B-634's class-arm reified path.
- baml_compiler2_emit/src/emit.rs: emit a reified `TypeArgRef` `is_type` test
instead of constant-false.
- bex_vm/src/vm.rs: `IsType` resolves the `Type` template against the frame
and runs an interface-aware value-membership test.
- stdlib ns_iter/iter.baml: ArrayIterator.next gains the now-required `null`
arm (unreachable at runtime — stored nulls match `let value: T`).
- Tests: diagnostic_errors/generic_typevar_match (E0062), compiles/patterns_new
(both arm orders), ns_patterns_new_runtime e2e, match_union_typevar
reachability.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
⏭️ Performance benchmarks were skippedPerf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to To run them on this PR, do any of the following, then push a commit (or re-run CI):
|
📝 WalkthroughWalkthroughThis PR fixes generic ChangesGeneric TypeVar pattern-match fix
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Lower as lower_pattern_test
participant Emit as emit.rs is_type
participant VM as bex_vm IsType
Lower->>Lower: detect TypeVar in scope for let x: T
Lower->>Emit: emit_is_type_template_branch(TyTemplate)
Emit->>Emit: build ConstValue::Type(template)
Emit->>VM: IsType instruction with ConstValue::Type operand
VM->>VM: substitute frame.type_args into template
VM->>VM: value_matches_reified_ty(value, resolved type)
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ 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.
🧹 Nitpick comments (1)
baml_language/crates/baml_tests/projects/compiles/patterns_new/patterns_new.baml (1)
289-294: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd the
IterDonelet-first ordering.This only covers
IterDonebeforelet x: T; add the mirror case so the compile test locks the concrete-arm-after-TypeVar reachability regression for non-null sentinels too.Proposed test addition
function typevar_done_first<T>(v: T | IterDone, fallback: T) -> T { match (v) { IterDone => fallback, let x: T => x, } } + +function typevar_done_let_first<T>(v: T | IterDone, fallback: T) -> T { + match (v) { + let x: T => x, + IterDone => fallback, + } +}🤖 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_tests/projects/compiles/patterns_new/patterns_new.baml` around lines 289 - 294, The compile test for typevar_done_first only covers the pattern order where the concrete IterDone arm comes before the let binding; add the mirror case using the same function-style pattern so it also tests let x: T before IterDone. Update the patterns_new test content to include a matching function or match block that exercises the reversed ordering, using IterDone and typevar_done_first as reference points, so the regression is locked for non-null sentinels too.
🤖 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.
Nitpick comments:
In
`@baml_language/crates/baml_tests/projects/compiles/patterns_new/patterns_new.baml`:
- Around line 289-294: The compile test for typevar_done_first only covers the
pattern order where the concrete IterDone arm comes before the let binding; add
the mirror case using the same function-style pattern so it also tests let x: T
before IterDone. Update the patterns_new test content to include a matching
function or match block that exercises the reversed ordering, using IterDone and
typevar_done_first as reference points, so the regression is locked for non-null
sentinels too.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 78cd9649-7227-4912-ba1e-8118e48bd28b
⛔ Files ignored due to path filters (20)
baml_language/crates/baml_tests/snapshots/baml_src/patterns_new_runtime.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/patterns_new/baml_tests__compiles__patterns_new__01_lexer__patterns_new.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/patterns_new/baml_tests__compiles__patterns_new__02_parser__patterns_new.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/patterns_new/baml_tests__compiles__patterns_new__03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/patterns_new/baml_tests__compiles__patterns_new__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/patterns_new/baml_tests__compiles__patterns_new__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/patterns_new/baml_tests__compiles__patterns_new__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/patterns_new/baml_tests__compiles__patterns_new__10_formatter__patterns_new.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/generic_typevar_match/baml_tests__diagnostic_errors__generic_typevar_match__01_lexer__generic_typevar_match.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/generic_typevar_match/baml_tests__diagnostic_errors__generic_typevar_match__02_parser__generic_typevar_match.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/generic_typevar_match/baml_tests__diagnostic_errors__generic_typevar_match__03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/generic_typevar_match/baml_tests__diagnostic_errors__generic_typevar_match__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/generic_typevar_match/baml_tests__diagnostic_errors__generic_typevar_match__05_diagnostics.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/generic_typevar_match/baml_tests__diagnostic_errors__generic_typevar_match__10_formatter__generic_typevar_match.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/generics/baml_tests__diagnostic_errors__generics__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/generics/baml_tests__diagnostic_errors__generics__05_diagnostics.snapis excluded by!**/*.snap
📒 Files selected for processing (9)
baml_language/crates/baml_builtins2/baml_std/baml/ns_iter/iter.bamlbaml_language/crates/baml_compiler2_emit/src/emit.rsbaml_language/crates/baml_compiler2_mir/src/lower.rsbaml_language/crates/baml_compiler2_tir/src/builder.rsbaml_language/crates/baml_tests/baml_src/ns_patterns_new_runtime/patterns_new_runtime.bamlbaml_language/crates/baml_tests/projects/compiles/patterns_new/patterns_new.bamlbaml_language/crates/baml_tests/projects/diagnostic_errors/generic_typevar_match/generic_typevar_match.bamlbaml_language/crates/baml_tests/tests/match_union_typevar.rsbaml_language/crates/bex_vm/src/vm.rs
Binary size checks passed✅ 7 passed
Generated by |
|
Parking as draft — holding until @2kai2kai2's type-algebra / interface-soundness rework series lands. This is a TIR exhaustiveness fix ( |
📍 Status — where we stopped (resume from here)
The approach is CodeRabbit‑approved. Pushed commit
ea1446c53implements the two‑part fix (documented below). Two items remain, both saved as uncommitted WIP in the worktree~/orca/workspaces/baml/antonio-b-633:Closure‑value edge in the reified
⚠️ Still needs verification that this makes the 2 tests pass with correct results (not merely stop panicking) — i.e. confirm the closure reaching the test is a legit refutable
is_typetest — WIP fix on disk, not yet committed / verified.Running the full CI suite (not just
baml_tests) surfaced a real panic:value_matches_reified_tyreusedvalue_concrete_runtime_ty, which is interface‑receiver‑only andunreachable!‑panics on Function/Closure/Future values (bex_vm/src/vm.rs~1996). Twobex_enginetests hit it —while_let_closure_captures_fresh_cell_per_iterationandtest_closures_in_loop_vars— because a closure value reaches the reified test.WIP fix (uncommitted,
vm.rs+46/−7): make the test total via a newis_reified_type_subjectguard (only primitives / class‑instances / enum‑variants /type/ array / map are validvalue_concrete_runtime_tysubjects) plus type‑tag checks forFunction/Futuretargets.let x: T, and not an irrefutable bind that shouldn't emit a type test at all (if the latter, gate it out in lowering instead of tolerating it in the VM).baml_clidescribe snapshot regen. The stdlibiter.bamlchange (addednull => Done {}arm toArrayIterator.next) shifts thedescribe bamllisting;baml_cli__describe_command_tests__render_builtin_package_listing.snapneeds regenerating (a.snap.newis staged).Resume checklist (once Kai's series lands):
antonio/b-633onto the newcanary; reconcileunion_targets_for_patternwith the reworked TIR (the fix may simplify or partly disappear).E0062, no null‑leak) + no‑regression (iter_array_nullable_elements→[3,1,4,3],iter_flatten→[1,2,3,4]).cargo nextest run --all-features -E 'not package(baml_tests) and not package(/^sdk_test_/) and not binary(=pack_e2e)'andcargo test -p baml_tests -- --skip parser_stress→ 0 failed; regenbaml_cli(+ any flagged) snapshots;cargo clippy --all-targets --all-features -- -D warningsclean;cargo fmt.gh pr ready 3904(un‑draft) and enqueue.Bug
A generic
matcharmlet x: Tover a union scrutineeT | <concrete>(whereTis a rigid type variable) was treated as covering the concrete member too. So this was wrongly deemed exhaustive and compiled, lettingnullbind into a non-nullableTat runtime:The concrete control
match (v: int?) { let x: int => x }correctly erroredE0062. The bug was specific to a type-variable arm. It also produced a symmetric falseE0063"unreachable arm": inmatch (v: T?) { let x: T => …, null => … }the explicitnull =>arm was reported unreachable.(Note: the ticket title "pop() erases the ?" is inaccurate —
pop()/.at()correctly returnT | null; the defect was in exhaustiveness/reachability dispatch.)Root cause
Static —
union_targets_for_pattern(baml_compiler2_tir/src/builder.rs) decides which union members a pattern targets via a symmetric structural overlap, andatoms_overlapreports aTypeVaras overlapping everything. So a barelet x: Tpattern claimed the concretenull/stringmember, making the match exhaustive and shadowing the explicit concrete arm.Runtime — once
let x: Tno longer over-claims (so it is not the exhaustive-last catch-all), its runtime type test was emitted, but a bareTypeArgReffell through to a constant-falsein codegen (emit.rs, "these don't arise from pattern matching today"). With the test always false, thelet x: Tarm never matched — which would break every array iterator (ArrayIterator.nextrelied on the old catch-all).Fix
builder.rs: a bare top-levelTypeVarpattern claims only the union'sTypeVarmember(s), never a concrete member. Mirrors the existing member-side rule. This restoresE0062and removes the falseE0063.let x: Tarm now resolvesTagainst the frame's realized type args and tests the value against the resolved type, reusing the machinery B-634 (Honor subtyping in generic class match arms' reified type-arg check (B-634) #3902) introduced for class match arms:lower.rs: route a barelet x: Tthroughtir2_to_dispatch_guard_template(covariantTypeArgRefOrWildcard), the same path as theClass<T>arm.emit.rs: emit a reifiedis_type(aTypeconstant carrying the template) instead of constant-false.vm.rs:IsTypesubstitutes the template fromframe.type_argsand runs an interface-aware value-membership test (is_subtype_offor concrete/union targets;type_implementswhenTresolves to an interface).This single approach fixes both the soundness hole and keeps generic iteration working:
is_type(null, int?)→ true →let x: Tbinds it (nullableTyields its storednull).is_type(null, int)→ false → falls to thenullarm (no leak intoint).is_type(ArrayIterator, Iterable)→ true viatype_implements(a class value matches an interfaceTit implements) →flatten/interface-typed iterators keep working.The one stdlib change:
ArrayIterator.nextgains the now-requirednullarm (unreachable at runtime — stored nulls matchlet value: T).Tests
diagnostic_errors/generic_typevar_match—E0062forlet x: ToverT?/T | stringvia.pop(),.at(), bareT?, andT | string.compiles/patterns_new— both arm orders (let x: Tfirst andnullfirst) andDone-first compile with noE0062/E0063.ns_patterns_new_runtimee2e — both arm orders +.pop()/.at()round-trip;nullroutes to thenullarm for non-nullableTand binds tolet x: Tfor nullableT.match_union_typevar— updated the reachability regression to assert concrete arms after a barelet x: Tstay reachable.Before / after
match (v: T?) { let x: T => x }E0062missing nullmatch (v: T?) { let x: T => …, null => … }E0063iter_array_nullable_elements(int?[])[3, 1, 4, 3]iter_flatten(interface-typed)[][1, 2, 3, 4]Verification
cargo test -p baml_tests -- --skip parser_stress→ 2796 passed, 0 failed (incl. B-634'sns_generic_match_subtype/generic_match_typevar_arm).cargo clippy --all-targets --all-features -- -D warningsclean onbaml_compiler2_tir,baml_compiler2_mir,baml_compiler2_emit,bex_vm.canary(after B-634 Honor subtyping in generic class match arms' reified type-arg check (B-634) #3902 and B-611 Preserve the ErrorContext cause chain across a defer on the unwind path (B-611) #3903).https://linear.app/boundaryml2/issue/B-633
Summary by CodeRabbit
let x: Tnow works correctly alongsidenulland other concrete cases.