Skip to content

Fix type checker literal union upcasting issues#3299

Merged
hellovai merged 4 commits intocanaryfrom
hellovai/fix-type-checker-literal-union-upcasting-issues
Mar 28, 2026
Merged

Fix type checker literal union upcasting issues#3299
hellovai merged 4 commits intocanaryfrom
hellovai/fix-type-checker-literal-union-upcasting-issues

Conversation

@hellovai
Copy link
Copy Markdown
Contributor

@hellovai hellovai commented Mar 28, 2026

Artifacts | Task

What problems was I solving

The BAML type checker had two categories of failures around literal union types:

  1. Arithmetic on unions of literals failed: Expressions like 1 + if (true) { 2 } else { 3 } produced InvalidBinaryOp errors because infer_arithmetic's base_ty closure only handled Ty::Primitive and Ty::LiteralTy::Union fell through to None. This blocked six tests that were marked #[ignore].

  2. Container inference didn't widen fresh literals: [1, 2, 3] at an unannotated let-binding inferred as (1 | 2 | 3)[] instead of int[], because widen_fresh() only matched top-level Ty::Literal and didn't recurse into compound types (Union, List, Map, Optional).

After this PR, union-of-literal arithmetic works correctly, container literals widen as expected, and all six previously-ignored tests pass.

What user-facing changes did I ship

How I implemented it

TIR: Fix arithmetic on union types (builder.rs)

Refactored infer_arithmetic's base_ty from a closure to a local fn and added a Ty::Union arm that recursively extracts the base primitive from each union member. Added a promote helper that handles same-type identity and Int/Float promotion. This means 1 + if (true) { 2 } else { 3 } now correctly resolves both operands to Int and produces an Int result.

TIR: Recursive widen_fresh() (ty.rs)

Extended widen_fresh() to recurse into Ty::Union, Ty::List, Ty::Map, and Ty::Optional, following the recursive transformation pattern from substitute_ty in generics.rs. Added a dedup_and_collapse helper that flattens nested unions, deduplicates members, and collapses singletons. This means [1, 2, 3] at a let-binding now widens each Fresh literal inside the union, deduplicates three Primitive(Int) values into one, and produces int[].

MIR: Guard constant propagation for phi-like temps (cleanup.rs)

Added count_local_defs to count assignment sites per local. The propagate_copies pass now skips constant propagation for locals defined in multiple branches (e.g., a temp assigned in both arms of an if-else). This prevents the MIR optimizer from incorrectly collapsing phi-like temporaries — a bug that surfaced when the arithmetic fixes produced correct TIR that the optimizer then mishandled.

Tests: New projects + un-ignored tests

  • Created literal_union_arithmetic/main.baml with 5 functions exercising union arithmetic (if-else add, match add, both-sides union, subtract, loop match accumulator)
  • Created literal_union_widening/main.baml with 4 functions exercising recursive widening (array of ints, map of ints, nested array, annotated literal union preservation)
  • Un-ignored all 6 previously-failing tests across soundness.rs, if_else.rs, and match_basics.rs
  • Added 3 subtype unit tests in normalize.rs for Union([Lit(1), Lit(2)]) <: Int, List(Union(...)) <: List(Int), and mixed int/float literal union subtyping

Deviations from the plan

Implemented as planned

  • Phase 0: Both test projects created verbatim as specified
  • Phase 1: infer_arithmetic refactored with base_ty fn + promote helper + Ty::Union arm; all 6 tests un-ignored
  • Phase 2: widen_fresh() made recursive with Union/List/Map/Optional arms + dedup_and_collapse helper
  • Phase 3: All 3 subtype unit tests added in normalize.rs

Deviations/surprises

  • promote takes b by reference (&PrimitiveType) instead of by value — minor ergonomic adjustment, semantically identical
  • normalize.rs tests use HashMap::new() instead of FxHashMap::default() — matches the existing test module imports

Additions not in plan

  • cleanup.rscount_local_defs + phi-like temp guard in propagate_copies: The MIR constant propagation pass was incorrectly collapsing phi-like temporaries (locals assigned in multiple if-else branches). This bug was latent but surfaced when the arithmetic fixes produced correct TIR that the optimizer then mishandled. The fix counts definition sites per local and skips propagation for multi-definition locals.
  • Snapshot updates for existing test projects (catch_throw, format_checks, function_call, headers_edge_cases, parser_statements): Downstream effects of both the widen_fresh recursion (literal types in TIR now widen correctly) and the propagate_copies fix (MIR local numbering shifts).

Items planned but not implemented

  • None — all phases fully implemented

How to verify it

Setup

git fetch
scripts/create_worktree.sh hellovai/fix-type-checker-literal-union-upcasting-issues
cd ~/wt/baml/fix-type-checker-literal-union-upcasting-issues/baml_language

Manual Testing

  • Review crates/baml_tests/projects/literal_union_arithmetic/main.baml — confirm the 5 arithmetic test cases cover the intended scenarios
  • Review crates/baml_tests/projects/literal_union_widening/main.baml — confirm the 4 widening test cases cover arrays, maps, nested arrays, and annotated preservation
  • Review the cleanup.rs changes — confirm the count_local_defs logic correctly counts assignments and call destinations

Automated Tests

cargo test -p baml_tests
cargo test -p baml_compiler2_tir

Description for the changelog

Fix type checker to support arithmetic on unions of literal types and recursive widening of fresh literals in containers.

Summary by CodeRabbit

  • New Features

    • Arithmetic now handles union-typed operands and promotes numeric combinations; widening of fresh literal types now propagates through unions and container types.
  • Bug Fixes

    • Copy/constant propagation tightened to avoid unsafe inlining into locals with multiple definitions.
  • Tests

    • Added literal-union subtyping/widening tests.
    • Re-enabled and updated bytecode snapshots for control-flow + union-arithmetic cases.
    • Updated editor/LSP test expectations to relax prior diagnostics.

- Extend infer_arithmetic's base_ty to handle Ty::Union by folding over
  members with a promote helper, fixing InvalidBinaryOp for union-of-literal
  arithmetic (e.g. `1 + if(true){2} else {3}`)
- Make widen_fresh() recursive into Union, List, Map, Optional so container
  literals at unannotated let-bindings widen correctly (e.g. `[1,2,3]` infers
  as int[] instead of (1|2|3)[])
- Fix propagate_copies in MIR cleanup to guard against inlining phi-like
  temps that have multiple definitions
- Add subtype unit tests for union-of-literals scenarios
- Un-ignore 6 tests across soundness, if_else, and match_basics
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 28, 2026

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

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Mar 28, 2026 6:39pm
promptfiddle Ready Ready Preview, Comment Mar 28, 2026 6:39pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 28, 2026

📝 Walkthrough

Walkthrough

Added local-definition counting to MIR cleanup to restrict copy/constant propagation; extended TIR arithmetic inference to handle unions and promote numeric literals; normalized/deduplicated unions during widening; added literal-union tests and BAML examples; updated many test snapshots and LSP2 action expectations.

Changes

Cohort / File(s) Summary
MIR Copy Propagation
baml_language/crates/baml_compiler2_mir/src/cleanup.rs
Added private count_local_defs to compute per-local assignment counts and require single-definition (defs[dest] == 1) before performing copy/constant propagation; updated comments.
TIR Arithmetic Inference
baml_language/crates/baml_compiler2_tir/src/builder.rs
infer_arithmetic extended to extract primitive members from Ty::Union, promote numeric combinations (e.g., Int+Float → Float), and added helpers (promote, base_ty) to compute result types.
Union Widening & Normalization
baml_language/crates/baml_compiler2_tir/src/ty.rs
Added dedup_and_collapse(types: Vec<Ty>, attr: TyAttr) and extended Ty::widen_fresh to recurse into Union, List, Map, and Optional, widening members and collapsing/deduplicating union results.
Union Subtyping Tests
baml_language/crates/baml_compiler2_tir/src/normalize.rs
Added unit tests asserting literal-union subtyping to primitives (including inside lists) and mixed literal numeric unions subtype to Float where applicable.
BAML Test Projects (examples)
baml_language/crates/baml_tests/projects/literal_union_arithmetic/main.baml, baml_language/crates/baml_tests/projects/literal_union_widening/main.baml
Added example/test BAML files exercising arithmetic over control-flow-produced unions and widening behavior (multiple new functions).
Test Snapshots & Expectations
baml_language/crates/baml_tests/tests/if_else.rs, .../match_basics.rs, .../soundness.rs
Removed several #[ignore = "..."] markers and updated insta bytecode snapshots: temporary variable slots and jump/label arrangements changed; some tests un-ignored.
LSP/Action Test Expectations
baml_language/crates/baml_lsp2_actions_tests/test_files/...
Updated multiple LSP2 action test files to remove previously-expected diagnostics for operations on literal-union types (replaced with // <no-diagnostics-expected> or adjusted expected type text).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix type checker literal union upcasting issues' clearly and directly summarizes the main change: addressing type-checking problems with literal union upcasting, which aligns with the PR's core objective of fixing literal-union type handling in the compiler.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 hellovai/fix-type-checker-literal-union-upcasting-issues

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.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 28, 2026

Binary size checks passed

7 passed

Artifact Platform Gzip Baseline Delta Status
bridge_cffi Linux 4.0 MB 5.7 MB -1.6 MB (-29.1%) OK
bridge_cffi-stripped Linux 2.5 MB 4.3 MB -1.8 MB (-41.4%) OK
bridge_cffi macOS 3.3 MB 4.6 MB -1.3 MB (-29.0%) OK
bridge_cffi-stripped macOS 2.0 MB 3.5 MB -1.5 MB (-42.1%) OK
bridge_cffi Windows 3.3 MB 4.6 MB -1.4 MB (-29.6%) OK
bridge_cffi-stripped Windows 2.1 MB 3.5 MB -1.5 MB (-41.8%) OK
bridge_wasm WASM 2.0 MB 3.0 MB -939.9 KB (-31.7%) OK

Generated by cargo size-gate · workflow run

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: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ec5f0226-428c-41a1-a440-eab77f29ec4b

📥 Commits

Reviewing files that changed from the base of the PR and between ccd9110 and 18e6da8.

⛔ Files ignored due to path filters (29)
  • baml_language/crates/baml_tests/snapshots/catch_throw/baml_tests__catch_throw__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/catch_throw/baml_tests__catch_throw__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/format_checks/baml_tests__format_checks__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/format_checks/baml_tests__format_checks__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/format_checks/baml_tests__format_checks__05_diagnostics.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/format_checks/baml_tests__format_checks__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/function_call/baml_tests__function_call__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/function_call/baml_tests__function_call__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/headers_edge_cases/baml_tests__headers_edge_cases__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/headers_edge_cases/baml_tests__headers_edge_cases__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/literal_union_arithmetic/baml_tests__literal_union_arithmetic__01_lexer__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/literal_union_arithmetic/baml_tests__literal_union_arithmetic__02_parser__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/literal_union_arithmetic/baml_tests__literal_union_arithmetic__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/literal_union_arithmetic/baml_tests__literal_union_arithmetic__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/literal_union_arithmetic/baml_tests__literal_union_arithmetic__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/literal_union_arithmetic/baml_tests__literal_union_arithmetic__05_diagnostics.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/literal_union_arithmetic/baml_tests__literal_union_arithmetic__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/literal_union_arithmetic/baml_tests__literal_union_arithmetic__10_formatter__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/literal_union_widening/baml_tests__literal_union_widening__01_lexer__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/literal_union_widening/baml_tests__literal_union_widening__02_parser__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/literal_union_widening/baml_tests__literal_union_widening__03_hir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/literal_union_widening/baml_tests__literal_union_widening__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/literal_union_widening/baml_tests__literal_union_widening__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/literal_union_widening/baml_tests__literal_union_widening__05_diagnostics.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/literal_union_widening/baml_tests__literal_union_widening__06_codegen.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/literal_union_widening/baml_tests__literal_union_widening__10_formatter__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/parser_statements/baml_tests__parser_statements__04_5_mir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/parser_statements/baml_tests__parser_statements__04_tir.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/parser_statements/baml_tests__parser_statements__05_diagnostics.snap is excluded by !**/*.snap
📒 Files selected for processing (9)
  • baml_language/crates/baml_compiler2_mir/src/cleanup.rs
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_compiler2_tir/src/normalize.rs
  • baml_language/crates/baml_compiler2_tir/src/ty.rs
  • baml_language/crates/baml_tests/projects/literal_union_arithmetic/main.baml
  • baml_language/crates/baml_tests/projects/literal_union_widening/main.baml
  • baml_language/crates/baml_tests/tests/if_else.rs
  • baml_language/crates/baml_tests/tests/match_basics.rs
  • baml_language/crates/baml_tests/tests/soundness.rs

…r defs

Document why widen_fresh drops _attr on Union (always default in TIR layer)
and why count_local_defs omits DispatchFuture/Await (lowering never reuses
locals across Assign and async terminators).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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

♻️ Duplicate comments (1)
baml_language/crates/baml_compiler2_mir/src/cleanup.rs (1)

304-311: 🛠️ Refactor suggestion | 🟠 Major

Make the sync-vs-async def invariant executable.

The new defs[dest] == 1 gate is only sound because count_local_defs() intentionally ignores Await/DispatchFuture writes. Right now that contract lives only in comments, so a future lowering tweak can silently re-open the same phi-like miscompile on mixed sync/async branches. Please add a debug assertion/verifier check for “no local is written by both Assign and async terminators”, or count those terminators here.

Also applies to: 506-510, 523-526


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1bc131f2-19c9-43d5-b41b-cb1f8177b4ed

📥 Commits

Reviewing files that changed from the base of the PR and between 18e6da8 and 704ed82.

📒 Files selected for processing (2)
  • baml_language/crates/baml_compiler2_mir/src/cleanup.rs
  • baml_language/crates/baml_compiler2_tir/src/ty.rs

Literal unions like `1 | 2 | 3` are now widened to their primitive type
(e.g. `int`), so error messages and diagnostics reflect the widened types.
Spurious operator errors on literal unions (Add, Mul) are now resolved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…thread TyAttr

- cleanup.rs: Use Place::base_local() instead of hand-rolled projection
  walk. Count DispatchFuture/Await destinations as definition sites.
- ty.rs: Thread the union's TyAttr through dedup_and_collapse instead of
  dropping it and rebuilding with TyAttr::default().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 55433123-bfcc-4d78-b97f-8702f48c44d0

📥 Commits

Reviewing files that changed from the base of the PR and between 62e8420 and a027d0e.

📒 Files selected for processing (2)
  • baml_language/crates/baml_compiler2_mir/src/cleanup.rs
  • baml_language/crates/baml_compiler2_tir/src/ty.rs

@hellovai hellovai added this pull request to the merge queue Mar 28, 2026
Merged via the queue into canary with commit 1a2ed81 Mar 28, 2026
43 checks passed
@hellovai hellovai deleted the hellovai/fix-type-checker-literal-union-upcasting-issues branch March 28, 2026 19:23
codeshaunted pushed a commit that referenced this pull request Mar 30, 2026
[Artifacts](https://cloud.codelayer.cloud/tasks/019d3357-d681-74d6-8d5a-02e6fe7c4038/artifacts)
|
[Task](https://cloud.codelayer.cloud/deep/tasks/019d3357-d681-74d6-8d5a-02e6fe7c4038)

## What problems was I solving

The BAML type checker had two categories of failures around literal
union types:

1. **Arithmetic on unions of literals failed**: Expressions like `1 + if
(true) { 2 } else { 3 }` produced `InvalidBinaryOp` errors because
`infer_arithmetic`'s `base_ty` closure only handled `Ty::Primitive` and
`Ty::Literal` — `Ty::Union` fell through to `None`. This blocked six
tests that were marked `#[ignore]`.

2. **Container inference didn't widen fresh literals**: `[1, 2, 3]` at
an unannotated `let`-binding inferred as `(1 | 2 | 3)[]` instead of
`int[]`, because `widen_fresh()` only matched top-level `Ty::Literal`
and didn't recurse into compound types (`Union`, `List`, `Map`,
`Optional`).

After this PR, union-of-literal arithmetic works correctly, container
literals widen as expected, and all six previously-ignored tests pass.

## What user-facing changes did I ship

-
[cleanup.rs](https://github.com/BoundaryML/baml/pull/3299/files#diff-baml_language/crates/baml_compiler2_mir/src/cleanup.rs)
— MIR constant propagation guard for multi-definition locals (phi-like
temps)
-
[builder.rs](https://github.com/BoundaryML/baml/pull/3299/files#diff-baml_language/crates/baml_compiler2_tir/src/builder.rs)
— Extended `infer_arithmetic` to handle `Ty::Union` with recursive
`base_ty` + `promote` helper
-
[ty.rs](https://github.com/BoundaryML/baml/pull/3299/files#diff-baml_language/crates/baml_compiler2_tir/src/ty.rs)
— Recursive `widen_fresh()` for compound types + `dedup_and_collapse`
helper
-
[normalize.rs](https://github.com/BoundaryML/baml/pull/3299/files#diff-baml_language/crates/baml_compiler2_tir/src/normalize.rs)
— New subtype unit tests for union-of-literals scenarios
-
[literal_union_arithmetic/main.baml](https://github.com/BoundaryML/baml/pull/3299/files#diff-baml_language/crates/baml_tests/projects/literal_union_arithmetic/main.baml)
— New test project for union-of-literal arithmetic
-
[literal_union_widening/main.baml](https://github.com/BoundaryML/baml/pull/3299/files#diff-baml_language/crates/baml_tests/projects/literal_union_widening/main.baml)
— New test project for recursive widening
-
[if_else.rs](https://github.com/BoundaryML/baml/pull/3299/files#diff-baml_language/crates/baml_tests/tests/if_else.rs)
— Un-ignored 3 arithmetic tests
-
[match_basics.rs](https://github.com/BoundaryML/baml/pull/3299/files#diff-baml_language/crates/baml_tests/tests/match_basics.rs)
— Un-ignored 2 arithmetic tests
-
[soundness.rs](https://github.com/BoundaryML/baml/pull/3299/files#diff-baml_language/crates/baml_tests/tests/soundness.rs)
— Un-ignored 1 arithmetic test

## How I implemented it

### TIR: Fix arithmetic on union types (`builder.rs`)

Refactored `infer_arithmetic`'s `base_ty` from a closure to a local `fn`
and added a `Ty::Union` arm that recursively extracts the base primitive
from each union member. Added a `promote` helper that handles same-type
identity and `Int`/`Float` promotion. This means `1 + if (true) { 2 }
else { 3 }` now correctly resolves both operands to `Int` and produces
an `Int` result.

### TIR: Recursive `widen_fresh()` (`ty.rs`)

Extended `widen_fresh()` to recurse into `Ty::Union`, `Ty::List`,
`Ty::Map`, and `Ty::Optional`, following the recursive transformation
pattern from `substitute_ty` in `generics.rs`. Added a
`dedup_and_collapse` helper that flattens nested unions, deduplicates
members, and collapses singletons. This means `[1, 2, 3]` at a
`let`-binding now widens each `Fresh` literal inside the union,
deduplicates three `Primitive(Int)` values into one, and produces
`int[]`.

### MIR: Guard constant propagation for phi-like temps (`cleanup.rs`)

Added `count_local_defs` to count assignment sites per local. The
`propagate_copies` pass now skips constant propagation for locals
defined in multiple branches (e.g., a temp assigned in both arms of an
if-else). This prevents the MIR optimizer from incorrectly collapsing
phi-like temporaries — a bug that surfaced when the arithmetic fixes
produced correct TIR that the optimizer then mishandled.

### Tests: New projects + un-ignored tests

- Created `literal_union_arithmetic/main.baml` with 5 functions
exercising union arithmetic (if-else add, match add, both-sides union,
subtract, loop match accumulator)
- Created `literal_union_widening/main.baml` with 4 functions exercising
recursive widening (array of ints, map of ints, nested array, annotated
literal union preservation)
- Un-ignored all 6 previously-failing tests across `soundness.rs`,
`if_else.rs`, and `match_basics.rs`
- Added 3 subtype unit tests in `normalize.rs` for `Union([Lit(1),
Lit(2)]) <: Int`, `List(Union(...)) <: List(Int)`, and mixed int/float
literal union subtyping

## Deviations from the plan

### Implemented as planned
- Phase 0: Both test projects created verbatim as specified
- Phase 1: `infer_arithmetic` refactored with `base_ty` fn + `promote`
helper + `Ty::Union` arm; all 6 tests un-ignored
- Phase 2: `widen_fresh()` made recursive with
`Union`/`List`/`Map`/`Optional` arms + `dedup_and_collapse` helper
- Phase 3: All 3 subtype unit tests added in `normalize.rs`

### Deviations/surprises
- `promote` takes `b` by reference (`&PrimitiveType`) instead of by
value — minor ergonomic adjustment, semantically identical
- `normalize.rs` tests use `HashMap::new()` instead of
`FxHashMap::default()` — matches the existing test module imports

### Additions not in plan
- **`cleanup.rs` — `count_local_defs` + phi-like temp guard in
`propagate_copies`**: The MIR constant propagation pass was incorrectly
collapsing phi-like temporaries (locals assigned in multiple if-else
branches). This bug was latent but surfaced when the arithmetic fixes
produced correct TIR that the optimizer then mishandled. The fix counts
definition sites per local and skips propagation for multi-definition
locals.
- **Snapshot updates for existing test projects** (`catch_throw`,
`format_checks`, `function_call`, `headers_edge_cases`,
`parser_statements`): Downstream effects of both the `widen_fresh`
recursion (literal types in TIR now widen correctly) and the
`propagate_copies` fix (MIR local numbering shifts).

### Items planned but not implemented
- None — all phases fully implemented

## How to verify it

### Setup
```bash
git fetch
scripts/create_worktree.sh hellovai/fix-type-checker-literal-union-upcasting-issues
cd ~/wt/baml/fix-type-checker-literal-union-upcasting-issues/baml_language
```

### Manual Testing
- [ ] Review
`crates/baml_tests/projects/literal_union_arithmetic/main.baml` —
confirm the 5 arithmetic test cases cover the intended scenarios
- [ ] Review
`crates/baml_tests/projects/literal_union_widening/main.baml` — confirm
the 4 widening test cases cover arrays, maps, nested arrays, and
annotated preservation
- [ ] Review the `cleanup.rs` changes — confirm the `count_local_defs`
logic correctly counts assignments and call destinations

### Automated Tests
```bash
cargo test -p baml_tests
cargo test -p baml_compiler2_tir
```

## Description for the changelog

Fix type checker to support arithmetic on unions of literal types and
recursive widening of fresh literals in containers.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Arithmetic now handles union-typed operands and promotes numeric
combinations; widening of fresh literal types now propagates through
unions and container types.

* **Bug Fixes**
* Copy/constant propagation tightened to avoid unsafe inlining into
locals with multiple definitions.

* **Tests**
  * Added literal-union subtyping/widening tests.
* Re-enabled and updated bytecode snapshots for control-flow +
union-arithmetic cases.
  * Updated editor/LSP test expectations to relax prior diagnostics.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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