Skip to content

fix(compiler2): subtype-reduce control-flow joins and establish empty collections (B-236) - #4030

Closed
codeshaunted wants to merge 2 commits into
canaryfrom
avery/b-236-recheck
Closed

codeshaunted wants to merge 2 commits into
canaryfrom
avery/b-236-recheck

Conversation

@codeshaunted

@codeshaunted codeshaunted commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Fixes B-236. Supersedes #3781.

The bug

function f(xs: string[], m: int) -> string {
  let top = if (m > 0) { xs.slice(0, m) } else { [] };
  top.join(" ")   // E0007: type `string[] | _[]` has no member `join`
}

The if/else joined string[] with the empty-array EvolvingList(never) into a union. The unestablished evolving sentinel is a dead end inside a union (no later use can establish a union arm), so member resolution fails - and before the E0007 guard existed, .join mis-dispatched to the Map impl and aborted the VM with expected map, got array.

The fix

Port of typescript-go's flow-join simplification (getUnionOrEvolvingArrayType + removeSubtypes, the UnionReduction.Subtype mode applied to unions synthesized at expression joins), adapted to BAML's invariant containers:

  • Subtype reduction at joins: union members that are subtypes of other members are dropped. 1 | int -> int, Color.Red | Color -> Color, int | unknown -> unknown. Declared (user-written) unions are untouched, as in TS.
  • Empty-collection establishment: an unestablished [] / {} branch is absorbed by any same-category container member. It cannot be a subtype of one (lists/maps are invariant), but the join is an establishing use exactly like a push, and the Evolving wrapper proves the value is a fresh literal no committed alias can observe. This is what TS gets from never[] covariance, restricted to the sound fresh-literal case: a user-annotated committed never[] is NOT absorbed.
  • Two still-evolving containers join element-wise and stay evolving, so let x = if c { [] } else { [] }; x.push(5) still establishes.
  • Deliberately NOT ported from fix(compiler2): widen empty-collection branches in control-flow joins (B-236) #3781: two committed lists of different element types do not element-join to (a | b)[] - that would let a write through the joined view corrupt an aliased a[]. They stay a union, matching TS (which keeps string[] | number[] and rejects mutation through it).
  • Also fixes empty Expr::Map literals committing to map<never, never> instead of an evolving map (the Expr::Array arm already had the special case).

Tests

Six regression tests in type_error_repro.rs: the B-236 repro (both runtime paths, including the else-branch's actual empty array flowing through the string[] binding), the map analogue, both-empty-stays-establishable, a soundness guard that committed int[]/string[] branches stay a union with mutation rejected, and literal-base reduction.

Local: type_error_repro suite green (18/18); full workspace suite left to CI. TIR/corpus snapshot diffs may surface there (e.g. stdlib OrchestrationStep[] | never[] -> OrchestrationStep[]); flagging for review rather than pre-accepting.

Summary by CodeRabbit

  • Bug Fixes

    • Improved type inference for conditional branches, pattern matching, and exception flows.
    • Empty lists and maps now combine correctly with typed collections, enabling expected operations such as joining strings and indexing maps.
    • Simplified inferred unions by removing redundant subtype entries.
    • Empty collections can now establish their element types when later mutated.
    • Preserved type safety when combining collections with incompatible element types.
  • Tests

    • Added regression coverage for collection inference, runtime behavior, mutation, and subtype reduction.

… collections (B-236)

Port of typescript-go's flow-join simplification (getUnionOrEvolvingArrayType +
removeSubtypes). At a control-flow join (if/else, match arms, array/map element
joins), union members that are subtypes of other members are now dropped
(1 | int -> int, Color.Red | Color -> Color), and an unestablished empty [] / {}
is absorbed by any same-category container member: the join is an establishing
use exactly like a push, so 'if c { xs.slice(0, m) } else { [] }' types as
string[] instead of the dead-end union 'string[] | _[]' (E0007 on .join, and
before that a VM abort: expected map, got array).

Unlike the earlier attempt (#3781), two committed lists of different element
types do NOT element-join to (a | b)[], which would let a write through the
joined view corrupt an aliased a[]; they stay a union, as in TypeScript. Two
still-evolving containers join element-wise and stay evolving so a later push
can still establish the result.

Also fixes the empty Expr::Map literal committing to map<never, never> instead
of an evolving map (the Expr::Array arm already had the special case).
@linear

linear Bot commented Jul 14, 2026

Copy link
Copy Markdown

B-236

@vercel

vercel Bot commented Jul 14, 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 Jul 15, 2026 12:01am
promptfiddle Ready Ready Preview, Comment Jul 15, 2026 12:01am
promptfiddle2 Ready Ready Preview, Comment Jul 15, 2026 12:01am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (15)
  • baml_language/crates/baml_tests/snapshots/baml_src/patterns_new_runtime.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/__testing_std__/baml_tests__compiles____testing_std____04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/catch_all_keyword/baml_tests__compiles__catch_all_keyword__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/catch_all_panics/baml_tests__compiles__catch_all_panics__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/catch_throw/baml_tests__compiles__catch_throw__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/json_parse_stringify_intrinsics/baml_tests__compiles__json_parse_stringify_intrinsics__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/lambda_fat_arrow/baml_tests__compiles__lambda_fat_arrow__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/patterns_class_destructure_namespaces/baml_tests__compiles__patterns_class_destructure_namespaces__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/patterns_new/baml_tests__compiles__patterns_new__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_throw_regressions/baml_tests__diagnostic_errors__catch_throw_regressions__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/headers_edge_cases/baml_tests__diagnostic_errors__headers_edge_cases__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/diagnostic_errors/match_exhaustiveness/baml_tests__diagnostic_errors__match_exhaustiveness__04_tir.snap is excluded by !**/*.snap

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 756eb32e-4037-401a-9b88-98803f548ffc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The TIR builder now performs subtype-aware joins, preserves evolving list/map inference, and uses instance-based join helpers across control-flow, throws, and pattern typing. New B-236 tests cover container establishment, incompatible unions, and literal subtype reduction.

Changes

Type joining and regression coverage

Layer / File(s) Summary
Subtype-aware join algorithm
baml_language/crates/baml_compiler2_tir/src/builder.rs
Empty maps remain evolving, evolving containers join recursively, and redundant union members are removed using subtype and establishment checks.
Join helper propagation
baml_language/crates/baml_compiler2_tir/src/builder.rs
Control-flow, throws, bounds, and pattern typing paths use instance-based join_types, join_all, and facts_to_ty helpers.
Control-flow join regression tests
baml_language/crates/baml_tests/tests/type_error_repro.rs
Tests cover list/map joins, delayed evolution, incompatible committed lists, and integer literal subtype reduction.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • BoundaryML/baml#3642: Uses join_all in pattern dispatch, connecting it to this PR’s updated join behavior.
  • BoundaryML/baml#3887: Relates to Infer handling in type joins and type-argument or throws positions.

Suggested reviewers: 2kai2kai2, aaronvg

Poem

A bunny hops through unions bright,
Empty maps now grow just right.
Lists join softly, types align,
Subtypes vanish from the line.
Tests thump happily in the snow—
“Push and index!” the rabbits go.

🚥 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 main compiler2 fix: subtype-reducing control-flow joins and handling empty collections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 avery/b-236-recheck

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.

@vercel
vercel Bot temporarily deployed to Preview – beps July 14, 2026 22:53 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 14, 2026 23:02 Inactive
@github-actions

github-actions Bot commented Jul 14, 2026 •

Copy link
Copy Markdown

Binary size checks passed

✅ 7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
✅ baml-cli Linux 🔒 22.1 MB 9.5 MB file 22.1 MB +42.1 KB (+0.2%) OK
✅ packed-program Linux 🔒 15.9 MB 6.7 MB file 15.8 MB +40.9 KB (+0.3%) OK
✅ baml-cli macOS 🔒 17.0 MB 8.2 MB file 17.0 MB +33.1 KB (+0.2%) OK
✅ packed-program macOS 🔒 12.2 MB 5.9 MB file 12.2 MB +16.6 KB (+0.1%) OK
✅ baml-cli Windows 🔒 18.6 MB 8.4 MB file 18.5 MB +36.4 KB (+0.2%) OK
✅ packed-program Windows 🔒 13.1 MB 6.0 MB file 13.1 MB +36.8 KB (+0.3%) OK
✅ bridge_wasm WASM 14.7 MB 🔒 4.2 MB gzip 4.1 MB +12.7 KB (+0.3%) 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

@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.

🧹 Nitpick comments (2)
baml_language/crates/baml_tests/tests/type_error_repro.rs (2)

258-378: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Regression tests are internally consistent with the described join semantics; all six manually traced correctly.

One nit: per coding guidelines, unit tests are preferred over integration tests where possible. Several of these (esp. the two if_else_literal_and_base_reduce_to_base / subtype-reduction-only cases) test a pure function (join_types) and could additionally live as fast #[cfg(test)] unit tests directly against Ty values in builder.rs, rather than going through full compile+VM execution — though the runtime-behavior tests (branch-taken, evolving-establish-then-push, panic-on-push) do need the integration harness. Given this mirrors the codebase's existing convention for TIR regression coverage, treat this as optional.

As per coding guidelines: "**/*.rs: Prefer writing Rust unit tests over integration tests where possible."

🤖 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/tests/type_error_repro.rs` around lines 258 -
378, Optionally add focused #[cfg(test)] unit coverage in builder.rs for the
pure join_types behavior, especially subtype reduction such as literal-plus-base
and other type-only cases, using direct Ty values. Keep the existing integration
tests for runtime branch execution, evolving container inference, and mutation
rejection, since those require the full harness.

343-354: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a Map-typed analog of this test for symmetry.

This covers "committed lists of different element types stay a union, mutation rejected" for List. join_establishes/remove_subtype_members treat List and Map symmetrically in builder.rs, so an equivalent map<string,int> | map<string,string> case (mutation via index-assign rejected) would round out coverage for the map side of the same guarantee.

🤖 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/tests/type_error_repro.rs` around lines 343 -
354, Add a map-typed counterpart to
if_else_committed_lists_of_different_elements_stay_a_union, using an if/else
expression that produces map<string, int> and map<string, string> values.
Attempt mutation through index assignment and assert the compilation panic
reports the appropriate rejected map mutation/member behavior, preserving the
existing union-typing coverage pattern.
🤖 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/tests/type_error_repro.rs`:
- Around line 258-378: Optionally add focused #[cfg(test)] unit coverage in
builder.rs for the pure join_types behavior, especially subtype reduction such
as literal-plus-base and other type-only cases, using direct Ty values. Keep the
existing integration tests for runtime branch execution, evolving container
inference, and mutation rejection, since those require the full harness.
- Around line 343-354: Add a map-typed counterpart to
if_else_committed_lists_of_different_elements_stay_a_union, using an if/else
expression that produces map<string, int> and map<string, string> values.
Attempt mutation through index assignment and assert the compilation panic
reports the appropriate rejected map mutation/member behavior, preserving the
existing union-typing coverage pattern.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c5d5db46-2b92-4d66-b861-26e1af477bb5

📥 Commits

Reviewing files that changed from the base of the PR and between 187304b and f606f87.

📒 Files selected for processing (2)
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_tests/tests/type_error_repro.rs

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle July 14, 2026 23:14 Inactive
…btype reduction

All 15 diffs are the intended reduction from the previous commit:
- 14 x 04_tir: literal-vs-base unions at if/match/catch joins collapse to the
  base type (1 | int -> int, true | bool -> bool, 0n | bigint -> bigint,
  string | "caught" -> string).
- patterns_new_runtime bytecode: int[] | never[] -> int[] in load_type and
  baml.iter.Iterable/Iterator instantiations - the B-236 shape occurring in
  corpus code.
@vercel
vercel Bot temporarily deployed to Preview – beps July 14, 2026 23:40 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 14, 2026 23:47 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle July 15, 2026 00:01 Inactive
@codeshaunted
codeshaunted added this pull request to the merge queue Jul 15, 2026
@codeshaunted
codeshaunted removed this pull request from the merge queue due to a manual request Jul 15, 2026

This branch was previously deployed

3 inactive deployments
Preview – promptfiddle — 48dfed08 Deployed Jul 15, 2026 by vercel[bot]
Preview – promptfiddle2 — 48dfed08 Deployed Jul 14, 2026 by vercel[bot]
Preview – beps — 48dfed08 Deployed Jul 14, 2026 by vercel[bot]
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