Skip to content

Commit d19a0b4

Browse files
sxlijinclaude
andcommitted
feat(generics): inbound value-inference for generic BAML calls
Solve a generic call's TypeVars from argument values when the wire leaves them unbound, manufacturing the same type_args the explicit _types= channel would, then handing off to the unchanged substitute/Gate A/coerce/Gate B path. Engine-side only (RuntimeTy): - union_runtime_ty + infer_bindings_runtime: RuntimeTy ports of the TIR's union_ty / infer_bindings_inner (union-merge, null-strip, no union-arm routing per 00b3 G5). - synth_ty_from_value: value->RuntimeTy bridge; host-only values bind rust_type and ride through as Object::RustData (no materializer change). - driver in call_function_bound_args: infer before Gate A; explicit/self bindings win via or_insert. Tests: 16 unifier unit tests + 17 integration tests (tests/generics_inference.rs) covering 00b3 cases A/B/C/D/E/F/I. Full bex_engine suite green (412 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 85247f4 commit d19a0b4

12 files changed

Lines changed: 1393 additions & 317 deletions

File tree

baml_language/Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

baml_language/crates/baml_compiler2_tir/src/generics.rs

Lines changed: 11 additions & 262 deletions
Original file line numberDiff line numberDiff line change
@@ -532,268 +532,17 @@ pub fn contains_typevar_where(ty: &Ty, pred: &dyn Fn(&Name) -> bool) -> bool {
532532
}
533533
}
534534

535-
/// Infer type variable bindings by walking formal and actual types in parallel.
536-
///
537-
/// When `formal` is `Ty::TypeVar("T", TyAttr::default())` and `actual` is `Ty::Int { attr: TyAttr::default() }`,
538-
/// records `T → int` in `bindings`. For structural types, recurses into
539-
/// matching structures. Conflicting inferences are merged via `union_ty`.
540-
fn infer_bindings_inner(
541-
formal: &Ty,
542-
actual: &Ty,
543-
bindings: &mut FxHashMap<Name, Ty>,
544-
allow_typevar_actuals: bool,
545-
// A *rigid* type variable that must never be bound from an argument — the
546-
// pinned `Self` of an interface method call (mirrors rustc's `ty::Param`,
547-
// which unification never instantiates). `None` = no rigid variable (the
548-
// historical behavior). This is the only thing that distinguishes a
549-
// Self-pinned call from any other; ordinary calls pass `None`, so their
550-
// inference is completely unchanged.
551-
rigid: Option<&Name>,
552-
) {
553-
fn nullable_non_null_part(ty: &Ty) -> Option<Ty> {
554-
let Ty::Union(members, attr) = ty else {
555-
return None;
556-
};
557-
if !members.iter().any(Ty::is_null) {
558-
return None;
559-
}
560-
let non_null: Vec<Ty> = members
561-
.iter()
562-
.filter(|member| !member.is_null())
563-
.cloned()
564-
.collect();
565-
match non_null.as_slice() {
566-
[] => None,
567-
[single] => Some(single.clone()),
568-
_ => Some(Ty::Union(non_null, attr.clone())),
569-
}
570-
}
571-
572-
match (formal, actual) {
573-
(Ty::TypeVar(name, _), actual_ty) => {
574-
if rigid == Some(name) {
575-
return;
576-
}
577-
// Skip TypeVar-to-TypeVar bindings by default — they usually provide
578-
// no information for ordinary call inference. Some higher-order
579-
// callable-summary paths opt into preserving them explicitly.
580-
if !allow_typevar_actuals && matches!(actual_ty, Ty::TypeVar(_, _)) {
581-
return;
582-
}
583-
// An `Unknown` actual carries NO information: binding it (or
584-
// unioning it into an existing binding) only poisons the result —
585-
// e.g. an expected return of `SpawnParams<unknown, unknown>`
586-
// driving phase-0 must not turn a param-bound `T = int` into
587-
// `int | unknown`.
588-
if matches!(actual_ty, Ty::Unknown { .. }) {
589-
return;
590-
}
591-
bindings
592-
.entry(name.clone())
593-
.and_modify(|existing| *existing = union_ty(existing, actual_ty))
594-
.or_insert_with(|| actual_ty.clone());
595-
}
596-
(Ty::List(f, _), Ty::List(a, _)) => {
597-
infer_bindings_inner(f, a, bindings, allow_typevar_actuals, rigid);
598-
}
599-
(
600-
Ty::Map {
601-
key: fk, value: fv, ..
602-
},
603-
Ty::Map {
604-
key: ak, value: av, ..
605-
},
606-
) => {
607-
infer_bindings_inner(fk, ak, bindings, allow_typevar_actuals, rigid);
608-
infer_bindings_inner(fv, av, bindings, allow_typevar_actuals, rigid);
609-
}
610-
(Ty::Union(_, _), _) if nullable_non_null_part(formal).is_some() => {
611-
let formal_inner = nullable_non_null_part(formal).expect("checked above");
612-
let actual_inner = nullable_non_null_part(actual).unwrap_or_else(|| actual.clone());
613-
infer_bindings_inner(
614-
&formal_inner,
615-
&actual_inner,
616-
bindings,
617-
allow_typevar_actuals,
618-
rigid,
619-
);
620-
}
621-
(Ty::Union(f_members, _), Ty::Union(a_members, _))
622-
if f_members.len() == a_members.len() =>
623-
{
624-
for (formal_member, actual_member) in f_members.iter().zip(a_members.iter()) {
625-
infer_bindings_inner(
626-
formal_member,
627-
actual_member,
628-
bindings,
629-
allow_typevar_actuals,
630-
rigid,
631-
);
632-
}
633-
}
634-
(
635-
Ty::Function {
636-
params: fp,
637-
ret: fr,
638-
throws: fth,
639-
..
640-
},
641-
Ty::Function {
642-
params: ap,
643-
ret: ar,
644-
throws: ath,
645-
..
646-
},
647-
) => {
648-
for (fp, ap) in fp.iter().zip(ap.iter()) {
649-
infer_bindings_inner(&fp.ty, &ap.ty, bindings, allow_typevar_actuals, rigid);
650-
}
651-
infer_bindings_inner(fr, ar, bindings, allow_typevar_actuals, rigid);
652-
infer_bindings_inner(fth, ath, bindings, allow_typevar_actuals, rigid);
653-
}
654-
(Ty::Class(fn_name, f_args, _), Ty::Class(an_name, a_args, _)) if fn_name == an_name => {
655-
for (ft, at) in f_args.iter().zip(a_args.iter()) {
656-
infer_bindings_inner(ft, at, bindings, allow_typevar_actuals, rigid);
657-
}
658-
}
659-
// `Future<T, E>` is its own variant — descend into both params so the
660-
// future combinators can infer `<T, E>` from a `Future<T, E>[]` arg.
661-
(Ty::Future(f_value, f_error, _), Ty::Future(a_value, a_error, _)) => {
662-
infer_bindings_inner(f_value, a_value, bindings, allow_typevar_actuals, rigid);
663-
infer_bindings_inner(f_error, a_error, bindings, allow_typevar_actuals, rigid);
664-
}
665-
// A heterogeneous future array — e.g. `[spawn { 1 }, spawn { 2 }]` —
666-
// types as `(Future<A, EA> | Future<B, EB>)[]` because `Future` is
667-
// invariant. Match the `Future<T, E>` formal against each union member
668-
// so `T`/`E` bind to the union of the member value/error types (the
669-
// TypeVar arm merges the per-member bindings via `union_ty`).
670-
(Ty::Future(_, _, _), Ty::Union(members, _)) => {
671-
for member in members {
672-
infer_bindings_inner(formal, member, bindings, allow_typevar_actuals, rigid);
673-
}
674-
}
675-
(
676-
Ty::Interface(fn_name, f_args, f_assoc, _),
677-
Ty::Interface(an_name, a_args, a_assoc, _),
678-
) if fn_name == an_name => {
679-
for (ft, at) in f_args.iter().zip(a_args.iter()) {
680-
infer_bindings_inner(ft, at, bindings, allow_typevar_actuals, rigid);
681-
}
682-
for (formal_name, formal_ty) in f_assoc {
683-
if let Some((_, actual_ty)) = a_assoc
684-
.iter()
685-
.find(|(actual_name, _)| actual_name == formal_name)
686-
{
687-
infer_bindings_inner(
688-
formal_ty,
689-
actual_ty,
690-
bindings,
691-
allow_typevar_actuals,
692-
rigid,
693-
);
694-
}
695-
}
696-
}
697-
// Builtin container bridging: Array<T> ↔ List(T), Map<K,V> ↔ Map(K,V)
698-
// This enables UFCS calls like `Array.length(arr)` where the formal self
699-
// type is Class(Array, [T]) and the actual is List(int).
700-
(Ty::Class(class_name, f_args, _), Ty::List(actual_inner, _))
701-
if class_name.is_builtin_root_type("Array") && f_args.len() == 1 =>
702-
{
703-
infer_bindings_inner(
704-
&f_args[0],
705-
actual_inner,
706-
bindings,
707-
allow_typevar_actuals,
708-
rigid,
709-
);
710-
}
711-
(
712-
Ty::Class(class_name, f_args, _),
713-
Ty::Map {
714-
key: actual_key,
715-
value: actual_val,
716-
..
717-
},
718-
) if class_name.is_builtin_root_type("Map") && f_args.len() == 2 => {
719-
infer_bindings_inner(
720-
&f_args[0],
721-
actual_key,
722-
bindings,
723-
allow_typevar_actuals,
724-
rigid,
725-
);
726-
infer_bindings_inner(
727-
&f_args[1],
728-
actual_val,
729-
bindings,
730-
allow_typevar_actuals,
731-
rigid,
732-
);
733-
}
734-
_ => {} // Concrete types: nothing to infer
735-
}
736-
}
737-
738-
pub fn infer_bindings(formal: &Ty, actual: &Ty, bindings: &mut FxHashMap<Name, Ty>) {
739-
infer_bindings_inner(formal, actual, bindings, false, None);
740-
}
741-
742-
pub fn infer_bindings_allow_typevars(formal: &Ty, actual: &Ty, bindings: &mut FxHashMap<Name, Ty>) {
743-
infer_bindings_inner(formal, actual, bindings, true, None);
744-
}
745-
746-
/// Like [`infer_bindings`] but treats `rigid` (when `Some`) as a rigid type
747-
/// variable that is never bound from an argument — the pinned `Self` of an
748-
/// interface method call. Every other variable infers exactly as before.
749-
pub fn infer_bindings_rigid_self(
750-
formal: &Ty,
751-
actual: &Ty,
752-
bindings: &mut FxHashMap<Name, Ty>,
753-
rigid: Option<&Name>,
754-
) {
755-
infer_bindings_inner(formal, actual, bindings, false, rigid);
756-
}
757-
758-
/// Combine two types into a union, deduplicating members.
759-
///
760-
/// Used when the same type variable is inferred from multiple arguments
761-
/// (e.g., `deep_equals(myInt, myString)` → `T` gets `int` then `string`).
762-
pub fn union_ty(a: &Ty, b: &Ty) -> Ty {
763-
normalize_union_members([a.clone(), b.clone()], TyAttr::default())
764-
}
765-
766-
fn normalize_union_members(members: impl IntoIterator<Item = Ty>, attr: TyAttr) -> Ty {
767-
let mut normalized = Vec::new();
768-
for member in members {
769-
match member {
770-
Ty::Never { .. } => {}
771-
Ty::Union(inner, _) => {
772-
for inner_member in inner {
773-
if !matches!(inner_member, Ty::Never { .. })
774-
&& !normalized.contains(&inner_member)
775-
{
776-
normalized.push(inner_member);
777-
}
778-
}
779-
}
780-
other if !normalized.contains(&other) => normalized.push(other),
781-
_ => {}
782-
}
783-
}
784-
785-
match normalized.len() {
786-
0 => Ty::Never { attr },
787-
1 => normalized.pop().expect("length checked"),
788-
_ => {
789-
// TODO(TyAttr): This union is synthesized from multiple input types — there's no
790-
// single "original attr" to preserve. If inputs carry different attrs, which one
791-
// wins? May need a merge/lattice operation on TyAttr, or default may be correct if
792-
// attrs describe declaration sites rather than computed types.
793-
Ty::Union(normalized, attr)
794-
}
795-
}
796-
}
535+
// ── Type variable inference & union normalization ──────────────────────────────
536+
//
537+
// The pure `Ty`-walking inference/union primitives now live in `baml_type` so the
538+
// runtime engine can share the single algorithm (it widens `RuntimeTy` → `Ty`,
539+
// runs the unifier, narrows back) without a runtime → compiler dependency. They
540+
// are re-exported here so every existing `crate::generics::…` caller is
541+
// unchanged. See `01c-inbound-inference-reuse.md`.
542+
pub use baml_type::generics::{
543+
infer_bindings, infer_bindings_allow_typevars, infer_bindings_rigid_self,
544+
normalize_union_members, union_ty,
545+
};
797546

798547
/// Replace any remaining `Ty::TypeVar` with `Ty::Unknown` and emit diagnostics.
799548
///

baml_language/crates/baml_type/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ rust-version.workspace = true
88
baml_base = { workspace = true }
99
baml_type_macros = { workspace = true }
1010
borsh = { workspace = true }
11+
rustc-hash = { workspace = true }

0 commit comments

Comments
 (0)