Repository navigation
feat(types): infer _ wildcard in type-arg and throws positions (B-230, B-247) - #3887
Conversation
…-230, B-247) Adds a `_` inference-hole wildcard usable as a generic type argument and as a `throws`-clause member, and corrects the stdlib `to_json`/`serialize` throw sets. B-230: `Future<int, _>` now infers the error type from the spawn body's transitive throws while keeping the value type explicit, instead of forcing the developer to spell out the full error union or fall into the `never` trap. The hole is filled from the initializer's actual type, so it stays exact (not erased to `unknown`) and `await` sees the precise error type. B-247: `throws T | _` is an opt-in open contract — `_` absorbs the body's inferred (e.g. stdlib) throws while `T` stays named, and callers still see the full union (sound). Plain `throws T` stays exhaustive. Also drops the spurious `JsonParseError` from `baml.json.to_json`/`serialize` (a pure serializer cannot raise a parse error). `_` lowers to a single compile-time-only `Ty::Infer` hole that never reaches runtime; using it where it cannot be inferred (parameter/return/field/alias types) is a clean error (E0147) rather than a compiler panic.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds ChangesWildcard
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes 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 |
⏭️ 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):
|
Binary size checks passed✅ 7 passed
Generated by |
There was a problem hiding this comment.
Actionable comments posted: 4
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_ast/src/lower_cst.rs (1)
1311-1388: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winValidate
_in interfacerequiresclauses too.This adds E0147 coverage for interface fields, but
lower_interfacestill lowersrequiresparents withoutcheck_wildcard_type(...). That lets unsupported cases likeinterface I requires _orrequires Foo<_>slip past the new wildcard restriction.Suggested fix
let requires: Vec<TypeExpr> = parent_type_nodes .into_iter() .map(|te| { let expr = lower_type_expr::lower_type_expr_node(&te); let te_span = te.syntax().span_range(); - check_unknown_type( - &expr, - format!("requires clause of interface `{iface_name}`"), - te_span, - diags, - ); + let context = format!("requires clause of interface `{iface_name}`"); + check_unknown_type(&expr, context.clone(), te_span, diags); + lower_type_expr::check_wildcard_type(&expr, &context, te_span, diags); expr.with_span(te_span) }) .collect();🤖 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_ast/src/lower_cst.rs` around lines 1311 - 1388, `lower_interface` is missing wildcard validation in its `requires` clause lowering, so `_` can still be accepted there even though interface fields now reject it. Update the `requires` parent handling in `lower_interface` to run `check_wildcard_type(...)` on each lowered parent `TypeExpr`, alongside the existing `check_unknown_type(...)`, using the same span/context so cases like `interface I requires _` and `requires Foo<_>` are reported consistently.
🧹 Nitpick comments (1)
baml_language/crates/baml_tests/tests/wildcard_type_inference.rs (1)
187-205: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a
serialize<T>throws regression too.This locks down
baml.json.to_json, but the stdlib change inbaml_language/crates/baml_builtins2/baml_std/baml/ns_json/json.baml:177narrowsserialize<T>in the same way. A small compile-pass test forthrows baml.json.JsonSerializationErrorwould keep that wrapper from drifting independently.Suggested follow-up test
+/// `baml.json.serialize` should also expose only `JsonSerializationError`. +#[tokio::test] +async fn json_serialize_is_serialize_only() { + let out = baml_test!( + r#" + function dump(x: int) -> string throws baml.json.JsonSerializationError { + baml.json.serialize(x) + } + function main() -> int { 0 } + "# + ); + assert!( + matches!(out.result, Ok(BexExternalValue::Int(0))), + "expected 0, got {:?}", + out.result + ); +}🤖 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/wildcard_type_inference.rs` around lines 187 - 205, Add a regression test for the `serialize<T>` wrapper in the same test module, alongside `json_to_json_is_serialize_only`, to verify it can declare exactly `throws baml.json.JsonSerializationError` without triggering an exhaustiveness issue. Reuse the existing `baml_test!` pattern and reference the `serialize` path in the stdlib wrapper so the compile-pass coverage matches the `baml.json.to_json` case and prevents the two implementations from drifting independently.
🤖 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.
Inline comments:
In `@baml_language/crates/baml_compiler2_ast/src/lower_type_expr.rs`:
- Around line 609-671: Reject wildcard `_` in interface requires clauses and
generic bounds by invoking check_wildcard_type from the lowering paths in
lower_cst.rs that build those TypeExprs, including the requires parent handling
and the T extends bound handling. Ensure the new validation catches
TypeExprKind::Infer in those locations and emits the existing
LoweringDiagnostic::WildcardTypeNotAllowed with the right context, consistent
with the checks already used in lower_type_expr::check_wildcard_type.
In `@baml_language/crates/baml_compiler2_tir/src/builder.rs`:
- Around line 6834-6850: The infer-hole fallback in builder.rs is emitting
duplicate diagnostics because the speculative infer_expr probe and the
subsequent check_expr both append the same initializer errors. Update this
branch in the check path to snapshot the current diagnostic count before calling
infer_expr, then truncate any probe-generated diagnostics before running the
re-check with the filled type, matching the speculative handling used elsewhere
in this file. Keep the fix localized around the infer-hole fallback and the
related InferContext::report* accumulation behavior.
- Around line 8384-8455: `fill_infer_holes` is missing support for the same type
shapes that `ty_has_infer_hole` treats as containing holes, so `Ty::Union` and
`Ty::Function` can keep unresolved `Ty::Infer` values. Update `fill_infer_holes`
in `builder.rs` to recursively rewrite those shapes the same way it already
handles `Ty::Future`, `Ty::List`, `Ty::Map`, `Ty::Class`, and `Ty::Interface`,
or else narrow `ty_has_infer_hole` so it only reports cases this helper can
actually fill. Keep the behavior consistent between the two helpers and ensure
no `Ty::Infer` survives in the resolved type.
In `@baml_language/crates/baml_compiler2_tir/src/throw_inference.rs`:
- Around line 663-669: The `throws` lowering path is missing wildcard validation
for nested generic types, so `throws Wrapper<_>` can slip through as closed
while still containing an inferred hole. Update the `throws` handling in
`throw_inference.rs` to run `check_wildcard_type` before lowering, and make
`throws_ty_has_infer_hole` recursively inspect nested type structure beyond only
top-level `Ty::Infer` and `Ty::Union` so embedded infer holes are detected
consistently.
---
Outside diff comments:
In `@baml_language/crates/baml_compiler2_ast/src/lower_cst.rs`:
- Around line 1311-1388: `lower_interface` is missing wildcard validation in its
`requires` clause lowering, so `_` can still be accepted there even though
interface fields now reject it. Update the `requires` parent handling in
`lower_interface` to run `check_wildcard_type(...)` on each lowered parent
`TypeExpr`, alongside the existing `check_unknown_type(...)`, using the same
span/context so cases like `interface I requires _` and `requires Foo<_>` are
reported consistently.
---
Nitpick comments:
In `@baml_language/crates/baml_tests/tests/wildcard_type_inference.rs`:
- Around line 187-205: Add a regression test for the `serialize<T>` wrapper in
the same test module, alongside `json_to_json_is_serialize_only`, to verify it
can declare exactly `throws baml.json.JsonSerializationError` without triggering
an exhaustiveness issue. Reuse the existing `baml_test!` pattern and reference
the `serialize` path in the stdlib wrapper so the compile-pass coverage matches
the `baml.json.to_json` case and prevents the two implementations from drifting
independently.
🪄 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: 940047df-0103-4986-a162-ccc735c991eb
⛔ Files ignored due to path filters (1)
baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_tir.snapis excluded by!**/*.snap
📒 Files selected for processing (27)
baml_language/TYPE_SYSTEM.mdbaml_language/crates/baml_builtins2/baml_std/baml/ns_json/json.bamlbaml_language/crates/baml_builtins2_codegen/src/extract.rsbaml_language/crates/baml_compiler2_ast/src/ast.rsbaml_language/crates/baml_compiler2_ast/src/lib.rsbaml_language/crates/baml_compiler2_ast/src/lower_cst.rsbaml_language/crates/baml_compiler2_ast/src/lower_type_expr.rsbaml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rsbaml_language/crates/baml_compiler2_hir/src/builder.rsbaml_language/crates/baml_compiler2_ppir/src/ty.rsbaml_language/crates/baml_compiler2_tir/src/builder.rsbaml_language/crates/baml_compiler2_tir/src/callable.rsbaml_language/crates/baml_compiler2_tir/src/exhaustiveness.rsbaml_language/crates/baml_compiler2_tir/src/interfaces/coherence.rsbaml_language/crates/baml_compiler2_tir/src/lower_type_expr.rsbaml_language/crates/baml_compiler2_tir/src/normalize.rsbaml_language/crates/baml_compiler2_tir/src/throw_inference.rsbaml_language/crates/baml_compiler_diagnostics/src/diagnostic.rsbaml_language/crates/baml_lsp2_actions/src/check.rsbaml_language/crates/baml_lsp2_actions/src/utils.rsbaml_language/crates/baml_project/src/client_codegen.rsbaml_language/crates/baml_tests/src/compiler2_tir/mod.rsbaml_language/crates/baml_tests/tests/wildcard_type_inference.rsbaml_language/crates/baml_type/src/family.rsbaml_language/crates/baml_type/src/lib.rsbaml_language/crates/baml_type/src/normalize.rsbaml_language/crates/baml_type/src/runtime_ty.rs
…iagnostics Addresses the CodeRabbit review on #3887: - Reject `_` in interface `requires` clauses and generic bounds (`<T extends _>`), matching the existing E0147 gate for parameter/return/field/alias types. - Reject a `_` nested inside a thrown type (`throws Err<_>`); only a top-level `_` union member is the open-contract marker (`throws AppError | _`). - Add a safety net so a `_` that `fill_infer_holes` cannot structurally align (a union member, a function component, or a shape mismatch — e.g. `Box<int | _>`, `() -> _`) becomes a clean `unresolved type: _` error instead of a compiler panic at runtime lowering. The pattern ascription is the single reporting site, so the error is emitted exactly once. - Roll back the speculative free-inference probe's diagnostics in the infer-hole `let` path (`diagnostic_count`/`truncate_diagnostics`) so an ill-formed initializer is reported once, not twice. Adds regression tests for each case.
There was a problem hiding this comment.
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_ast/src/lower_cst.rs (1)
1542-1546: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winGeneric-param bounds in
lower_interfaceandlower_method_sigstill skip the wildcard check.
lower_functionandlower_classalready rejectT extends _, but these two lowering paths collectgeneric_param_boundswithout runningcheck_wildcard_type, so_can still fall through the generic unresolved-type path. Add the same loop after each bound collection.🤖 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_ast/src/lower_cst.rs` around lines 1542 - 1546, The generic parameter bound handling in lower_interface and lower_method_sig is missing the wildcard validation that lower_function and lower_class already perform. After collecting generic_param_bounds in those paths, run the same check_wildcard_type loop on each bound using the existing parameter_context so that T extends _ is rejected consistently. Update the logic near generic_param_bounds in lower_interface and lower_method_sig to mirror the validation already present in the other lowering functions.
🤖 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_ast/src/lower_cst.rs`:
- Around line 1542-1546: The generic parameter bound handling in lower_interface
and lower_method_sig is missing the wildcard validation that lower_function and
lower_class already perform. After collecting generic_param_bounds in those
paths, run the same check_wildcard_type loop on each bound using the existing
parameter_context so that T extends _ is rejected consistently. Update the logic
near generic_param_bounds in lower_interface and lower_method_sig to mirror the
validation already present in the other lowering functions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: faa608ab-6c1b-48c7-a848-72f405703100
📒 Files selected for processing (4)
baml_language/crates/baml_compiler2_ast/src/lower_cst.rsbaml_language/crates/baml_compiler2_ast/src/lower_type_expr.rsbaml_language/crates/baml_compiler2_tir/src/builder.rsbaml_language/crates/baml_tests/tests/wildcard_type_inference.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- baml_language/crates/baml_tests/tests/wildcard_type_inference.rs
- baml_language/crates/baml_compiler2_tir/src/builder.rs
… (Kai review) Kai noted that mapping `Ty::Infer` to a "matches-anything" sentinel during normalization breaks equivalence transitivity — `Box<int> == Box<_>` and `Box<string> == Box<_>` would both hold, transitively (and falsely) equating `Box<int>` with `Box<string>`. There is no sound normal form for an inference hole (Unknown *and* Error break transitivity identically), so the only correct design is to guarantee a hole never reaches normalization. Enforcing that immediately surfaced the leak Kai suspected: a `_` in a signature position (`-> _`, `(x: _)`, field/alias/bound/`requires` types) was E0147-reported but still lowered to `Ty::Infer` and reached the body-vs-return subtype check. - A `Ty::Infer` reaching `baml_type::normalize` / `compiler2_tir::normalize` is now an `unreachable!` — a hole there is a compiler bug, not a case to tolerate. - The AST wildcard gate now *rewrites* an illegal `_` to `TypeExprKind::Error` (in addition to reporting E0147), so signature/field/alias/bound/`requires` positions lower to the error-recovery `Ty::Unknown` sentinel, never `Ty::Infer`. - `Ty::Infer` now originates only from the two contexts that resolve it before any check: `let`-binding ascriptions (filled from the initializer) and top-level `throws` members (flattened by the firewall). The full suite passing with the `unreachable!` in place validates the invariant. Also extends `_` rejection to interface and method generic bounds.
Addressing @2kai2kai2's normalization-soundness feedbackKai raised (offline) that mapping He's right, and there is no sound normal form for a hole (Unknown and Error break transitivity identically), so I enforced the invariant rather than papering over it:
Agreed that moving the checking path to |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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.
Inline comments:
In `@baml_language/crates/baml_compiler2_ast/src/lower_cst.rs`:
- Around line 1584-1588: In lower_method_sig, required interface method
signatures must not allow a top-level `_` in the throws clause because there is
no body to infer from. Update the throws handling in lower_cst.rs to reject
wildcard throws for sig.throws_clause() by using check_wildcard_type (or
equivalent concrete-type validation) instead of check_throws_wildcard, and keep
the lowering path in lower_type_expr::lower_type_expr_node and
expr.with_span(te_span) for valid explicit throws types.
🪄 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: f38f0716-1ff6-4d3f-a72b-078d32fc4170
📒 Files selected for processing (4)
baml_language/crates/baml_compiler2_ast/src/lower_cst.rsbaml_language/crates/baml_compiler2_ast/src/lower_type_expr.rsbaml_language/crates/baml_compiler2_tir/src/normalize.rsbaml_language/crates/baml_type/src/normalize.rs
🚧 Files skipped from review as they are similar to previous changes (1)
- baml_language/crates/baml_compiler2_ast/src/lower_type_expr.rs
A bodyless method signature (interface required method) has no body to infer an open `throws … | _` from, and its declared throws is compared structurally during conformance checking — so a top-level `_` there would leak `Ty::Infer` into normalization (the new `unreachable!` guard). Reject any `_` in a method signature's throws clause with E0147, like other non-inferable positions. Adds a regression test.
…aryML#3910) ## Summary Extends the `_` wildcard feature from BoundaryML#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)** ```baml 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** ```baml 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)** ```baml 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)** ```baml 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](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
What
Introduces a
_inference-hole wildcard in type positions, fixing two related typechecker friction bugs, and corrects an imprecise stdlib throw set. Both tickets are fixed by a single shared mechanism (Ty::Infer), which is why they're landed together.Fixes B-230. Fixes B-247.
B-230 —
spawnannotation forced an exact error typeAnnotating a
spawnresultFuture<int, never>failed to compile when the body transitively threw (e.g. viabaml.sys.sleep throws baml.errors.Io), and there was no way to write a partial annotation:Future<V, E>is an invariant container, so the mismatch is correct —nevergenuinely means "throws nothing". The real gap was the missing partial annotation thatTYPE_SYSTEM.mdalready reserved_for. Now:The hole is filled from the initializer's actual type, so it stays exact (not erased to
unknown):await fs[0]sees the precise error type, and a wrong explicit arg (Future<string, _>against anintbody) is still a type error.B-247 —
throws Tforced exhaustive re-declaration of stdlib throwsA function declaring
throws BadInputthat also called anybaml.json.*function had to re-list the stdlib throws or dropthrowsentirely:throwsis exhaustive by design (TYPE_SYSTEM.md), so rather than silently weakening that, this adds an explicit opt-in: a_member opens the contract.throws AppError | _keepsAppErrornamed and lets_absorb the body's inferred throws. It stays sound: callers see the full union (AppError | JsonParseError | JsonDecodeError), because the firewall no longer treats a hole-bearing clause as a closed contract — the call-graph merge still runs and the hole is filled from the body's effective throw set. Plainthrows AppError(no_) remains exhaustive.Separately, the stdlib imprecision the ticket described has shifted since it was filed:
baml.json.stringifyis alreadythrows never; the real culprit wasbaml.json.to_json/serializedeclaring a spurious| JsonParseErroron a pure serializer. The native impl (render_to_json_honoring_overrides) only ever raisesJsonSerializationError, so those declarations are corrected.Implementation notes
_lowers to a single compile-time-onlyTy::Inferhole (#[axis(tir)]); it never reachesRuntimeTy/RealizedTy/MIR.letwith a hole-bearing annotation, the initializer is inferred freely (pushing the hole down as an expected type would poison the array/map container), the annotation's holes are filled from the actual type, then re-checked against the filled type so non-hole positions are still validated element-wise.throw_inference.rs), the callable-type builder (callable.rs, fills the hole from the body's effective throws so callers see the union), and the declared-vs-effective check (builder.rs)._where it cannot be inferred (parameter / return / field / alias types) is a clean error (E0147) rather than a compiler panic.Testing
crates/baml_tests/tests/wildcard_type_inference.rs(12 tests): B-230 inference + precision + wrong-arg rejection + general type-arg holes; B-247 open-slot + caller soundness + exhaustive-default + stdlib precision; E0147 rejection in bad positions.baml_testssuite green; clippy clean; stdlib TIR snapshot updated for theto_json/serializethrows change.Note for reviewers
This adds a small piece of surface to the documented type system (
TYPE_SYSTEM.mdupdated for both_cases). @2kai2kai2 — thethrows T | _open-slot semantics and the_type-arg hole touch your type-system design; would value your eye on whether the wording/semantics match your intent.Summary by CodeRabbit
New Features
_wildcard inference for generic type arguments and improved_support inthrows-clause typing, including “partial/open” throws contracts._across type rendering.Bug Fixes
to_jsonandserializenow expose onlyJsonSerializationError._usage in non-inferable positions (E0147).Tests
_inference, open vs closedthrowsbehavior, and JSON serialization rules.Documentation
throwstyping and_inference holes.