Skip to content

Solve _ inference holes in expression-context type positions - #3910

Merged
antoniosarosi merged 1 commit into
canaryfrom
hellovai/fix-compiler-error
Jul 7, 2026
Merged

antoniosarosi merged 1 commit into
canaryfrom
hellovai/fix-compiler-error

Conversation

@hellovai

@hellovai hellovai commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Extends the _ wildcard feature from #3887 (B-230/B-247) to the expression-context type positions. Before this, _ was solvable in a let annotation and a throws clause, but writing it anywhere in an expression (a call turbofish, an object literal, a generic value, an upcast) either panicked the compiler or was silently un-handled.

cc @antoniosarosi, since this builds directly on your _ inference work and changes the invariant your firewall documented.

Root cause

Your check_wildcard_type firewall rewrites _ to Ty::Error at every declaration site (params, returns, fields, aliases, bounds), which is why the doc there said "a Ty::Infer only ever originates from the two hole-handling contexts."

That invariant was actually already violated: there are four expression positions that carry a written type and never pass through the firewall, so a _ there reached type checking as a live Ty::Infer:

  1. call turbofish, foo<T, _>(x)
  2. object construction, Box<_> { ... }
  3. generic-apply value, id<_>
  4. upcast target, c.as<Show<_>>

A surviving Ty::Infer then tripped an unreachable! at one of the two boundaries that deliberately refuse to guess a meaning for a hole:

  • baml_type/src/runtime_ty.rs (MIR runtime lowering): `Infer` is not a valid `RuntimeTy`
  • baml_compiler2_tir/src/normalize.rs:566 (structural normalization): inference hole _ reached structural normalization

These were latent panics, not something a user had to do anything exotic to hit.

Approach

Same principle as your let/throws handling: fill the hole from a local source where one exists, reject it cleanly where none does. Never let a Ty::Infer escape the site.

Position Fill source Behavior
Call turbofish race<T, _>(...) the call arguments solved
Object Box<_> { v: 5 } the field values solved
Generic-apply value id<_> none (a bare value has nothing to infer from) clean diagnostic
Upcast c.as<Show<_>> none (explicit ascription) clean diagnostic

For the two solve sites, a hole that still cannot be resolved from its source (e.g. pick<int, _>(5) where U is in neither an argument nor the return) is reported as cannot infer type parameter, not accepted or panicked.

I deliberately left upcast as a reject rather than solving _ from the receiver's implements list. That is a bigger feature and matches how is Show<_> already behaves. Happy to revisit if you think upcast should solve.

Before / after

1. Call turbofish (the original report)

function with_timeout<T>(millis: int, f: () -> T) -> T {
    let work = spawn { f() };
    let timer = spawn {
        baml.sys.sleep(baml.time.Duration.from_milliseconds(millis));
        work.cancel();
        throw TimeoutError { message: `timed out` };
    };
    let result = baml.future.race<T, _>([work, timer]);
    await result
}
  • Before: panic, `Infer` is not a valid `RuntimeTy` (runtime lowering).
  • After: compiles and runs. _ (the future's error type) is inferred from [work, timer], while T stays pinned. Note the no-turbofish form race([work, timer]) cannot type-check here (the two futures do not unify), which is exactly why the partial <T, _> is needed.

2. Object construction

class Box<T> { v T }
let b = Box<_> { v: 5 }        // was: panic (normalize.rs:566)  ->  now: T = int, runs

class Pair<A, B> { a A  b B }
let p = Pair<int, _> { a: 1, b: "hi" }   // pins A = int, infers B = string from the field

3. Generic-apply value (no source, reject)

function id<T>(x: T) -> T { x }
let f = id<_>                  // was: panic (normalize.rs:566)  ->  now: E0002 cannot infer type parameter `T`

4. Upcast target (no source, reject)

c.as<Show<_>>                  // was: panic (normalize.rs:566)  ->  now: E0002 unresolved type: _  (matches `is Show<_>`)

Centralized policy (so a 5th position does not reintroduce this)

The enforcement is necessarily per-site (a hole has no sound normal form, so it must be resolved before that site's own subtype/normalize checks; the unreachable! is the intentional tripwire). To avoid four ad-hoc copies, the fill-or-reject rule now lives in one documented place in builder.rs:

  • report_uninferable_type_hole(param, at): the single reject-and-neutralize path. CannotInferTypeParameter when a parameter is named, UnresolvedType for a nested ascription hole.
  • fill_expression_type_arg_hole(partial, candidate, param, at): keep-if-concrete, else fill from the site's inferred candidate, else reject. candidate: None degenerates to pure reject, so the same helper serves both solve and reject sites.

Call, generic-apply, and upcast all route through these. Object keeps its own field-inference loop (it has a richer policy: an unconstrained phantom param recovers to BuiltinUnknown instead of erroring) but is documented as the field-sourced case of the same rule.

On the MIR side, the two hole detectors were merged into one (type_arg_is_infer_hole), which asks the actual conversion boundary (lower_to_runtime(..).is_err()) so it can never drift from what the runtime accepts.

I also corrected the check_wildcard_type doc, which claimed the two-context invariant that is no longer true.

What "handled" means here

Every _ position now either solves or produces a clean diagnostic. It does not mean every theoretically-inferable _ is inferred (upcast is a deliberate reject). The guarantee is: no Ty::Infer reaches normalization or runtime lowering from a supported syntactic position.

Files

  • baml_compiler2_tir/src/builder.rs: policy helpers + the four sites (call, object, generic-apply, upcast).
  • baml_compiler2_mir/src/lower.rs: substitute the TIR-solved type into hole slots for call and object type-args instead of re-lowering the raw _; single hole detector.
  • baml_compiler2_ast/src/lower_type_expr.rs: corrected check_wildcard_type invariant docs.
  • baml_tests/tests/wildcard_expression_holes.rs: 9 regression tests covering all four positions (solve, mixed-partial, nested, phantom, and each reject).

Testing

New file green (9). No regressions across wildcard (17), interfaces (35 / 150), instantiation, spawn, match/typevar, json interfaces (12 / 11), and tir/mir unit (175). Clean cargo clippy / cargo fmt.


🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • _ type holes in expression-contexts are now handled more consistently, producing clear compile-time errors instead of runtime panics.
    • Improved inference for partial/explicit generic type arguments, including mixed inferred and written holes.
    • Object construction, upcast/ascription targets, and call-site generics now resolve _ reliably using available context.
  • Tests
    • Added regression coverage for wildcard holes across function calls, timeouts/race-style expressions, nested constructors, and interface upcasts.

@hellovai
hellovai requested a review from antoniosarosi July 3, 2026 22:11
@vercel

vercel Bot commented Jul 3, 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 7, 2026 5:09pm
promptfiddle Ready Ready Preview, Comment Jul 7, 2026 5:09pm
promptfiddle2 Ready Ready Preview, Comment Jul 7, 2026 5:09pm

Request Review

@github-actions

github-actions Bot commented Jul 3, 2026

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.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: edfd22d9-bf13-4477-ab4f-2ac40e679b9c

📥 Commits

Reviewing files that changed from the base of the PR and between 5d7c683 and 9e70aa9.

📒 Files selected for processing (4)
  • baml_language/crates/baml_compiler2_ast/src/lower_type_expr.rs
  • baml_language/crates/baml_compiler2_mir/src/lower.rs
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_tests/tests/wildcard_expression_holes.rs
✅ Files skipped from review due to trivial changes (1)
  • baml_language/crates/baml_compiler2_ast/src/lower_type_expr.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_tests/tests/wildcard_expression_holes.rs
  • baml_language/crates/baml_compiler2_mir/src/lower.rs

📝 Walkthrough

Walkthrough

This PR refines how _ inference holes are handled in expression-context type positions. TIR builder logic now fills or rejects holes during generic instantiation, call-site lowering, upcasts, and object construction. MIR lowering uses solved types before runtime conversion, and tests cover the new behavior.

Changes

Expression-context wildcard hole resolution

Layer / File(s) Summary
Declaration-site wildcard documentation update
baml_language/crates/baml_compiler2_ast/src/lower_type_expr.rs
Comment expanded to clarify where _ is firewalled at declarations versus surviving in expression contexts.
TIR hole-filling and reporting helpers
baml_language/crates/baml_compiler2_tir/src/builder.rs
Adds report_uninferable_type_hole and fill_expression_type_arg_hole helpers to diagnose or fill unresolved _ holes.
TIR call-site and generic instantiation handling
baml_language/crates/baml_compiler2_tir/src/builder.rs
Fills holes in bare generic instantiations and partial explicit call type-arg lists, records solved call type args, and rejects holes in upcast/ascription targets.
TIR object construction inference
baml_language/crates/baml_compiler2_tir/src/builder.rs
Preserves explicit non-hole type args and infers holes from field values during object construction, emitting CannotInferTypeParameter when unresolvable.
MIR object/reflect template lowering
baml_language/crates/baml_compiler2_mir/src/lower.rs
Detects infer-holes in object type args and reflect.type_of args, using TIR-inferred templates or a new lower_type_arg_to_tir helper instead of raw lowering.
MIR call type-argument substitution
baml_language/crates/baml_compiler2_mir/src/lower.rs
lower_call_type_args/lower_explicit_type_args accept solved Tir2Ty args and substitute solved templates for infer-hole type args.
Regression tests
baml_language/crates/baml_tests/tests/wildcard_expression_holes.rs
New tests cover race<T,_> inference, rejected unconstrained turbofish, object construction holes, bare generic instantiation, and upcast target rejection.

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

Possibly related PRs

  • BoundaryML/baml#3750: Related to preventing unresolved inference placeholders from crossing the Ty→RuntimeTy boundary.
  • BoundaryML/baml#3887: Builds on the same _ wildcard-in-type-arg/throws implementation.

Poem

A wildcard hopped where types were thin,
_ marks the spot where doubt creeps in.
I filled each hole with care and cheer,
so runtime never sheds a tear.
Hop, infer, and test anew —
🐇 clean types for me and you!

🚥 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 accurately summarizes the main change: handling _ inference holes in expression-context type positions.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hellovai/fix-compiler-error

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.

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 3, 2026 22:18 Inactive
@github-actions

github-actions Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

Binary size checks passed

✅ 7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
✅ baml-cli Linux 🔒 21.6 MB 9.2 MB file 21.5 MB +91.7 KB (+0.4%) OK
✅ packed-program Linux 🔒 15.8 MB 6.6 MB file 15.6 MB +145.8 KB (+0.9%) OK
✅ baml-cli macOS 🔒 16.6 MB 8.0 MB file 16.6 MB +82.8 KB (+0.5%) OK
✅ packed-program macOS 🔒 12.2 MB 5.8 MB file 12.1 MB +99.7 KB (+0.8%) OK
✅ baml-cli Windows 🔒 18.2 MB 8.3 MB file 18.1 MB +71.7 KB (+0.4%) OK
✅ packed-program Windows 🔒 13.1 MB 5.9 MB file 13.0 MB +113.5 KB (+0.9%) OK
✅ bridge_wasm WASM 14.5 MB 🔒 4.1 MB gzip 4.1 MB +56.5 KB (+1.4%) 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.

Caution

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

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

4895-4953: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Move the _-hole guard before associated-type completion. resolve_deep can recurse into the target’s interface args, and Ty::Infer still hits normalization’s unreachable path, so .as<Show<_>> can panic before report_uninferable_type_hole runs.

🤖 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_tir/src/builder.rs` around lines 4895 -
4953, The `_`-hole check in the target handling path is happening too late,
after associated-type completion via AssociatedProjectionResolver::resolve_deep
can recurse into interface args and trigger normalization on Ty::Infer. Move the
ty_has_infer_hole guard in the builder logic before the interface
associated-binding completion block so expressions like `.as<Show<_>>` are
rejected by report_uninferable_type_hole before any deep resolution runs. Keep
the change localized to the target_ty handling around lower_type_expr_in_ns and
the associated type projection completion branch.
🤖 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.

Outside diff comments:
In `@baml_language/crates/baml_compiler2_tir/src/builder.rs`:
- Around line 4895-4953: The `_`-hole check in the target handling path is
happening too late, after associated-type completion via
AssociatedProjectionResolver::resolve_deep can recurse into interface args and
trigger normalization on Ty::Infer. Move the ty_has_infer_hole guard in the
builder logic before the interface associated-binding completion block so
expressions like `.as<Show<_>>` are rejected by report_uninferable_type_hole
before any deep resolution runs. Keep the change localized to the target_ty
handling around lower_type_expr_in_ns and the associated type projection
completion branch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 44c36d0f-fdeb-421b-a64e-df4fe651bf40

📥 Commits

Reviewing files that changed from the base of the PR and between 680dc42 and 5d7c683.

📒 Files selected for processing (4)
  • baml_language/crates/baml_compiler2_ast/src/lower_type_expr.rs
  • baml_language/crates/baml_compiler2_mir/src/lower.rs
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_tests/tests/wildcard_expression_holes.rs

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle July 3, 2026 22:29 Inactive
@antoniosarosi
antoniosarosi added this pull request to the merge queue Jul 6, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 6, 2026
@antoniosarosi
antoniosarosi added this pull request to the merge queue Jul 6, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 6, 2026
@antoniosarosi
antoniosarosi added this pull request to the merge queue Jul 6, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 6, 2026
@antoniosarosi
antoniosarosi added this pull request to the merge queue Jul 7, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 7, 2026
@antoniosarosi
antoniosarosi added this pull request to the merge queue Jul 7, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 7, 2026
`_` was firewalled to `Ty::Error` only at declaration sites. The four
expression positions that admit a written type (call turbofish, object
construction, generic-apply value, upcast target) bypassed that firewall,
so a `_` there survived as a raw `Ty::Infer` and hit an `unreachable!` at
structural normalization or runtime lowering.

Handle the hole at each site instead: solve it from local context where a
source exists (call arguments, object field values), reject it cleanly
where none does (bare generic value, upcast ascription). Centralize the
fill-or-reject rule in one documented policy (`fill_expression_type_arg_hole`
/ `report_uninferable_type_hole`) that the sites share, and correct the now
stale `check_wildcard_type` invariant docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@antoniosarosi
antoniosarosi force-pushed the hellovai/fix-compiler-error branch from 5d7c683 to 9e70aa9 Compare July 7, 2026 16:47
@vercel
vercel Bot temporarily deployed to Preview – beps July 7, 2026 16:48 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 7, 2026 16:55 Inactive
@antoniosarosi
antoniosarosi added this pull request to the merge queue Jul 7, 2026
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle July 7, 2026 17:09 Inactive
Merged via the queue into canary with commit 41f0d02 Jul 7, 2026
53 checks passed
@antoniosarosi
antoniosarosi deleted the hellovai/fix-compiler-error branch July 7, 2026 17:25

This branch was previously deployed

3 inactive deployments
Preview – promptfiddle — 9e70aa9f Deployed Jul 7, 2026 by vercel[bot]
Preview – promptfiddle2 — 9e70aa9f Deployed Jul 7, 2026 by vercel[bot]
Preview – beps — 9e70aa9f Deployed Jul 7, 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.

2 participants