diff --git a/baml_language/CHANGELOG.md b/baml_language/CHANGELOG.md index a483ce71916..b0c8fd6c290 100644 --- a/baml_language/CHANGELOG.md +++ b/baml_language/CHANGELOG.md @@ -29,6 +29,8 @@ This changelog covers the independent `baml_language` release line. It does not ### Fixes +- Carried a constructed type's identity through interface dispatch: `type.of()` inside an implements-block or inherited default method is now the exact `type` value the caller minted for a `reflect.class.new` or `reflect.enum.new` declaration, instead of an `==`-distinct re-derivation of the same type. Identity-keyed patterns — a registry keyed by type, a comparison against a stored type — match inside an impl body instead of silently missing. Statically instantiated type parameters, derived types (`t.array()`, `type.meta(…)`), and declarations from a compiled `reflect.Package` keep re-deriving their value, so they are unaffected. ([#4516](https://github.com/BoundaryML/baml/pull/4516)) - Antonio Sarosi +- Carried a runtime-minted type's identity through interface dispatch: `type.of()` inside an implements-block or inherited default method is now the exact `type` value the caller minted, instead of an `==`-distinct re-derivation of the same type. Identity-keyed patterns — a registry keyed by type, a comparison against a stored type — match inside an impl body instead of silently missing. Statically instantiated type parameters and derived types (`t.array()`, `type.meta(…)`) are unaffected. ([#4516](https://github.com/BoundaryML/baml/pull/4516)) - Antonio Sarosi - Rejected an inline `unreflect(value)` type argument whose runtime type would escape into the expression's published type — `Agent.new(...)`, `Holder { ... }`, any callee returning `Wrapper` or `T[]` — with E0168, which suggests naming it first (`type Out = unreflect(value)`). The class-literal spelling had reached MIR lowering with an error-recovery type argument and panicked. Results typed by the parameter itself (`sap.parse`, `Fn$parse`) and results that never mention it (`Fn$render_prompt`) are unchanged. ([#4518](https://github.com/BoundaryML/baml/pull/4518)) - Antonio Sarosi - Fixed optional-chained method calls dropping their type arguments: `x?.m()` — along with inferred, class-generic, and interface-dispatched forms — now seeds the callee's type-argument frame exactly like `x.m()` instead of failing at runtime with an internal frame type-arg error. An optional-chained call to a builtin I/O method (`file?.text()`) also takes the same inline sys-op path as the plain spelling, so its omitted defaulted arguments are materialized rather than reaching the engine unset. ([#4495](https://github.com/BoundaryML/baml/pull/4495)) - Antonio Sarosi - Rejected class-literal construction of the builtin companion carriers (`baml.Int`, `baml.Map`, `baml.String`, and the other zero-field method carriers) with E0166 instead of building a meaningless empty instance; `baml.Map {}` and `baml.Array {}` had reached MIR lowering with unsolved phantom type arguments and panicked. ([#4493](https://github.com/BoundaryML/baml/pull/4493)) - Antonio Sarosi diff --git a/baml_language/crates/baml_tests/tests/runtime_type_bindings.rs b/baml_language/crates/baml_tests/tests/runtime_type_bindings.rs index 80d1b631948..7c7722a053e 100644 --- a/baml_language/crates/baml_tests/tests/runtime_type_bindings.rs +++ b/baml_language/crates/baml_tests/tests/runtime_type_bindings.rs @@ -354,3 +354,412 @@ async fn agent_run_parses_a_reflected_output_type() { Ok(BexExternalValue::String("Pixel|Pixel".into())) ); } + +/// B-1582 follow-up: definitions crossed dispatch, but *identity* did not. +/// The resolver realizes an impl frame off the receiver's `Self`, which carries +/// realized types only, so `type.of()` in the body derived a fresh mint — +/// structurally the right type, `==`-wrong against the value the caller minted. +/// Every identity-keyed pattern (a registry, a stored type compared with `==`) +/// silently missed. Covered here: an implements-block method, an inherited +/// default method, and a second dispatch out of an impl body. +#[tokio::test] +async fn minted_type_identity_survives_interface_dispatch() { + let output = baml_test!( + r##" + interface Probe { + function same(self, t: type) -> bool throws never + function same_from_default(self, t: type) -> bool throws never { + type.of() == t + } + } + + interface Relay { + function relay(self, t: type) -> bool throws unknown + } + + class Holder { + function new() -> Holder throws never { + Holder {} + } + + implements Probe { + function same(self, t: type) -> bool throws never { + type.of() == t + } + } + + implements Relay { + function relay(self, t: type) -> bool throws unknown { + // Two-hop: the second interface operand is materialized + // inside this frame, so it has to carry the identity on. + Holder.new().same(t) + } + } + } + + function main() -> string throws unknown { + let output_type = reflect.class.new("RuntimeOutput", { + "name": type.of(), + }).as_type() + type Out = unreflect(output_type) + + let holder = Holder.new() + let impl_method = holder.same(output_type) + let default_method = holder.same_from_default(output_type) + let two_hop = holder.relay(output_type) + `${impl_method}|${default_method}|${two_hop}` + } + "## + ); + assert_eq!( + output.result, + Ok(BexExternalValue::String("true|true|true".into())) + ); +} + +/// The pattern the identity is *for*: a registry keyed by the type value at the +/// call site, looked up by `==` inside the impl. The second entry is the +/// control — a separately minted class of the same shape and source name must +/// stay a miss, so a passing lookup cannot be `==` degenerating to `true`. +#[tokio::test] +async fn interface_impl_methods_look_up_a_type_keyed_registry() { + let output = baml_test!( + r##" + class Entry { + key: type, + label: string, + } + + interface Named { + function label(self, first: Entry, second: Entry) -> string throws never + } + + class Holder { + function new() -> Holder throws never { + Holder {} + } + + implements Named { + function label(self, first: Entry, second: Entry) -> string throws never { + let wanted = type.of() + if (first.key == wanted) { + first.label + } else if (second.key == wanted) { + second.label + } else { + "missing" + } + } + } + } + + function main() -> string throws unknown { + let first_type = reflect.class.new("Shape", { + "name": type.of(), + }).as_type() + let second_type = reflect.class.new("Shape", { + "name": type.of(), + }).as_type() + type First = unreflect(first_type) + type Second = unreflect(second_type) + + let first_entry = Entry { key: first_type, label: "first" } + let second_entry = Entry { key: second_type, label: "second" } + + let first_hit = Holder.new().label(first_entry, second_entry) + let second_hit = Holder.new().label(first_entry, second_entry) + `${first_hit}|${second_hit}` + } + "## + ); + assert_eq!( + output.result, + Ok(BexExternalValue::String("first|second".into())) + ); +} + +/// Negative control: recovery reads the mint a definition records, so two +/// separate mints of the same shape stay distinct inside the impl, and a +/// statically instantiated type parameter keeps deriving its static digest — +/// the path is not taken at all when the interface operand carries no runtime +/// definitions. +#[tokio::test] +async fn dispatch_identity_separates_distinct_mints_and_leaves_static_generics_alone() { + let output = baml_test!( + r##" + interface Probe { + function same(self, t: type) -> bool throws never + } + + class Holder { + function new() -> Holder throws never { + Holder {} + } + + implements Probe { + function same(self, t: type) -> bool throws never { + type.of() == t + } + } + } + + function main() -> string throws unknown { + let mine = reflect.class.new("Shape", { + "name": type.of(), + }).as_type() + let other = reflect.class.new("Shape", { + "name": type.of(), + }).as_type() + type Mine = unreflect(mine) + + let holder = Holder.new() + let own_mint = holder.same(mine) + let foreign_mint = holder.same(other) + + let static_holder = Holder.new() + let static_match = static_holder.same(type.of()) + let static_miss = static_holder.same(type.of()) + `${own_mint}|${foreign_mint}|${static_match}|${static_miss}` + } + "## + ); + assert_eq!( + output.result, + Ok(BexExternalValue::String("true|false|true|false".into())) + ); +} + +/// A runtime *package*'s declarations do NOT keep identity across dispatch, and +/// that is deliberate. Recovery reads a definition back out of the interface +/// operand's overlay, which is keyed by qualified name — and a compiled +/// package's `Item` is plain `user.Item`, exactly like a static `Item` and like +/// every other package's `Item`. Answering from a name that several definitions +/// can spell would hand back a *wrong* mint (see the two regressions below), so +/// the recovery declines and the body re-derives a static value. Definitions +/// still travel (#4501), which is what `name()` pins: the impl body can still +/// read the type, it just does not hold the caller's identity token for it. +#[tokio::test] +async fn runtime_package_declarations_keep_definitions_but_not_identity() { + let output = baml_test!( + r##" + interface Probe { + function same(self, t: type) -> bool throws never + function name(self) -> string throws never { + type.of().to_string() + } + } + + class Holder { + function new() -> Holder throws never { + Holder {} + } + + implements Probe { + function same(self, t: type) -> bool throws never { + type.of() == t + } + } + } + + function main() -> string throws unknown { + let pkg = reflect.Package.compile({ "items.baml": #" +class Item { value string } + "# }) + let item_type = (pkg.get_class("root.Item") ?? throw "missing Item").as_type() + type Item = unreflect(item_type) + + let holder = Holder.new() + `${holder.same(item_type)}|${holder.name()}` + } + "## + ); + assert_eq!( + output.result, + Ok(BexExternalValue::String("false|Item".into())) + ); +} + +/// A STATIC class must not be answered from an overlay that happens to carry a +/// runtime definition of the same name. `LoadType` staples the whole frame +/// overlay onto anything materialized in a frame that touched a runtime type, +/// so binding a compiled package's `Item` puts `user.Item` in this frame's +/// overlay — and the static `Holder` dispatch below then sees it. Reading +/// the overlay by plain name reported `type.of() != type.of()` and +/// `== ` the *package's* mint, which no `$dyn`-named type can reproduce. +#[tokio::test] +async fn static_class_slots_are_not_answered_from_a_same_named_runtime_definition() { + let output = baml_test!( + r##" + class Item { + value: string, + } + + interface Probe { + function same(self, t: type) -> bool throws never + } + + class Holder { + function new() -> Holder throws never { + Holder {} + } + + implements Probe { + function same(self, t: type) -> bool throws never { + type.of() == t + } + } + } + + function main() -> string throws unknown { + let pkg = reflect.Package.compile({ "items.baml": #" +class Item { value string } + "# }) + let runtime_item = (pkg.get_class("root.Item") ?? throw "missing Item").as_type() + // Binding it merges `user.Item` into this frame's overlay, which is + // what every type materialized here from now on carries. + type Shadow = unreflect(runtime_item) + let _shadow_holder = Holder.new() + + let holder = Holder.new() + `${holder.same(type.of())}|${holder.same(runtime_item)}` + } + "## + ); + assert_eq!( + output.result, + Ok(BexExternalValue::String("true|false".into())) + ); +} + +/// Two compiled packages that each declare `Item` produce two distinct mints +/// under one qualified name. An overlay keeps the first pointer it sees per +/// name, so a by-name recovery answered `Holder` with A's mint — `==` true +/// against a type it is not, and false against the one it is. Declining is the +/// only honest answer available from a name alone. +#[tokio::test] +async fn same_named_declarations_from_two_packages_do_not_cross_match() { + let output = baml_test!( + r##" + interface Probe { + function same(self, t: type) -> bool throws never + } + + class Holder { + function new() -> Holder throws never { + Holder {} + } + + implements Probe { + function same(self, t: type) -> bool throws never { + type.of() == t + } + } + } + + function main() -> string throws unknown { + let first = reflect.Package.compile({ "a.baml": #" +class Item { value string } + "# }) + let second = reflect.Package.compile({ "b.baml": #" +class Item { value string } + "# }) + let first_item = (first.get_class("root.Item") ?? throw "missing A").as_type() + let second_item = (second.get_class("root.Item") ?? throw "missing B").as_type() + type First = unreflect(first_item) + type Second = unreflect(second_item) + + let _first_holder = Holder.new() + let holder = Holder.new() + // Neither is claimed. The failure this guards is the SECOND value + // being `true` — answering with a foreign package's identity. + `${holder.same(second_item)}|${holder.same(first_item)}` + } + "## + ); + assert_eq!( + output.result, + Ok(BexExternalValue::String("false|false".into())) + ); +} + +/// Owner slots and method-level slots share one lane, and the owner slots are +/// recovered while the method slots arrive as operands. End-to-end cover for the +/// alignment the unit tests pin: a generic method on a generic impl, each slot a +/// different runtime mint. +#[tokio::test] +async fn dispatch_identity_covers_owner_and_method_slots_together() { + let output = baml_test!( + r##" + interface Probe { + function pair(self, own: type, method: type) -> string throws never + } + + class Holder { + function new() -> Holder throws never { + Holder {} + } + + implements Probe { + function pair(self, own: type, method: type) -> string throws never { + `${type.of() == own}|${type.of() == method}|${type.of() == method}` + } + } + } + + function main() -> string throws unknown { + let owner_type = reflect.class.new("Owner", { + "name": type.of(), + }).as_type() + let method_type = reflect.class.new("Method", { + "name": type.of(), + }).as_type() + type Owner = unreflect(owner_type) + type Method = unreflect(method_type) + + Holder.new().pair(owner_type, method_type) + } + "## + ); + assert_eq!( + output.result, + Ok(BexExternalValue::String("true|true|false".into())) + ); +} + +/// A runtime *enum* in a class-level slot takes the same road as a runtime +/// class — the recovery has an enum arm, and nothing else exercises it. +#[tokio::test] +async fn dispatch_identity_covers_a_runtime_enum_slot() { + let output = baml_test!( + r##" + interface Probe { + function same(self, t: type) -> bool throws never + } + + class Holder { + function new() -> Holder throws never { + Holder {} + } + + implements Probe { + function same(self, t: type) -> bool throws never { + type.of() == t + } + } + } + + function main() -> string throws unknown { + let choice = reflect.enum.new("Choice", ["FIRST", "SECOND"]).as_type() + let other = reflect.enum.new("Choice", ["FIRST", "SECOND"]).as_type() + type Choice = unreflect(choice) + + let holder = Holder.new() + `${holder.same(choice)}|${holder.same(other)}` + } + "## + ); + assert_eq!( + output.result, + Ok(BexExternalValue::String("true|false".into())) + ); +} diff --git a/baml_language/crates/bex_vm/src/package_baml/type_class.rs b/baml_language/crates/bex_vm/src/package_baml/type_class.rs index d1afaf8aedd..2ee80a9d8df 100644 --- a/baml_language/crates/bex_vm/src/package_baml/type_class.rs +++ b/baml_language/crates/bex_vm/src/package_baml/type_class.rs @@ -17,46 +17,14 @@ impl BamlNamespaceType for PackageBamlImpl { fn of_value(vm: &mut BexVm, v: &Value) -> Result { if let Some(ptr) = v.as_object_ptr() { let nominal = match vm.get_object(ptr) { - Object::Instance(instance) => Some((instance.class, true)), - Object::Variant(variant) => Some((variant.enm, false)), + Object::Instance(instance) => Some(instance.class), + Object::Variant(variant) => Some(variant.enm), _ => None, }; - if let Some((definition_ptr, is_class)) = nominal { - let reconstructed = match vm.get_object(definition_ptr) { - Object::Class(class) if is_class => { - class.runtime_type.as_ref().map(|runtime| { - let mut defs = runtime.defs.clone(); - defs.classes.insert(class.name.clone(), definition_ptr); - let ty = baml_type::RealizedTy::Class( - class.name.clone(), - Vec::new(), - baml_type::TyAttr::default(), - ); - if runtime.owner.is_null() { - TypeValue::from_parts_with_defs(ty, runtime.mint, defs) - } else { - TypeValue::runtime_with_defs(ty, runtime.mint, defs, runtime.owner) - } - }) - } - Object::Enum(enm) if !is_class => enm.runtime_type.as_ref().map(|runtime| { - let mut defs = runtime.defs.clone(); - defs.enums.insert(enm.name.clone(), definition_ptr); - let ty = baml_type::RealizedTy::Enum( - enm.name.clone(), - baml_type::TyAttr::default(), - ); - if runtime.owner.is_null() { - TypeValue::from_parts_with_defs(ty, runtime.mint, defs) - } else { - TypeValue::runtime_with_defs(ty, runtime.mint, defs, runtime.owner) - } - }), - _ => None, - }; - if let Some(type_value) = reconstructed { - return Ok(Value::object(vm.tlab.alloc_type(type_value))); - } + if let Some(definition_ptr) = nominal + && let Some(type_value) = vm.runtime_declaration_identity(definition_ptr) + { + return Ok(Value::object(vm.tlab.alloc_type(type_value))); } } let ty = vm diff --git a/baml_language/crates/bex_vm/src/vm.rs b/baml_language/crates/bex_vm/src/vm.rs index 7c47220e2d9..00bfbc391b7 100644 --- a/baml_language/crates/bex_vm/src/vm.rs +++ b/baml_language/crates/bex_vm/src/vm.rs @@ -138,16 +138,16 @@ struct TakenTypeArgs { fn append_virtual_method_type_args( frame_type_args: &mut Vec, method_type_args: &TakenTypeArgs, + mut owner_type_values: Vec>, ) -> Vec> { - let mut type_values = Vec::new(); - if !method_type_args.values.is_empty() { + if !method_type_args.values.is_empty() || !owner_type_values.is_empty() { // The resolver-provided owner/impl slots precede method-level slots in // the callee frame. Preserve that sparse alignment for exact values. - type_values.resize(frame_type_args.len(), None); - type_values.extend_from_slice(&method_type_args.values); + owner_type_values.resize(frame_type_args.len(), None); + owner_type_values.extend_from_slice(&method_type_args.values); } frame_type_args.extend_from_slice(&method_type_args.tys); - type_values + owner_type_values } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -728,7 +728,7 @@ pub(crate) mod tests { }; let mut frame_type_args = vec![baml_type::RealizedTy::int()]; - let values = append_virtual_method_type_args(&mut frame_type_args, &method); + let values = append_virtual_method_type_args(&mut frame_type_args, &method, Vec::new()); assert_eq!( frame_type_args, @@ -738,9 +738,59 @@ pub(crate) mod tests { ] ); assert_eq!(values.len(), 2); - assert!(values[0].is_none(), "owner slot must remain reconstructed"); + assert!( + values[0].is_none(), + "an owner slot with no recovered identity stays reconstructed" + ); assert_eq!(values[1].as_ref().map(TypeValue::mint), Some(exact.mint())); } + + /// Owner slots recovered from the interface operand keep their positions + /// when method-level slots are appended after them. + #[test] + fn recovered_owner_type_values_precede_method_slots() { + let owner = TypeValue::from_parts(baml_type::RealizedTy::int(), MintId::Runtime(3)); + let method_exact = + TypeValue::from_parts(baml_type::RealizedTy::string(), MintId::Runtime(7)); + let method = TakenTypeArgs { + tys: vec![baml_type::RealizedTy::string()], + values: vec![Some(method_exact.clone())], + defs: DynTypeDefs::default(), + }; + let mut frame_type_args = vec![baml_type::RealizedTy::int()]; + + let values = append_virtual_method_type_args( + &mut frame_type_args, + &method, + vec![Some(owner.clone())], + ); + + assert_eq!(values.len(), 2); + assert_eq!(values[0].as_ref().map(TypeValue::mint), Some(owner.mint())); + assert_eq!( + values[1].as_ref().map(TypeValue::mint), + Some(method_exact.mint()) + ); + } + + /// A method with no type arguments of its own still hands the owner slots + /// down — the value lane is not gated on the method being generic. + #[test] + fn recovered_owner_type_values_survive_a_non_generic_method() { + let owner = TypeValue::from_parts(baml_type::RealizedTy::int(), MintId::Runtime(3)); + let method = TakenTypeArgs::default(); + let mut frame_type_args = vec![baml_type::RealizedTy::int()]; + + let values = append_virtual_method_type_args( + &mut frame_type_args, + &method, + vec![Some(owner.clone())], + ); + + assert_eq!(frame_type_args, vec![baml_type::RealizedTy::int()]); + assert_eq!(values.len(), 1); + assert_eq!(values[0].as_ref().map(TypeValue::mint), Some(owner.mint())); + } } impl RootHaver for Frame { @@ -2182,6 +2232,90 @@ impl BexVm { } } + /// Rebuild the `type` value a runtime nominal declaration was created as. + /// + /// A runtime definition records the mint it was created with, so its + /// identity is *read back* here rather than derived afresh — deriving would + /// hand out a different value for the same type (BEP-066 I-1). Provenance + /// deliberately excludes the definition's own pointer (nothing can name + /// itself through its own dependencies), so it is added back. + /// + /// `None` for a static declaration: its mint is the deterministic digest of + /// its spelling, so every materialization already agrees. + pub(crate) fn runtime_declaration_identity( + &self, + definition_ptr: HeapPtr, + ) -> Option { + let (ty, defs, runtime) = match self.get_object(definition_ptr) { + Object::Class(class) => { + let runtime = class.runtime_type.as_ref()?; + let mut defs = runtime.defs.clone(); + defs.classes.insert(class.name.clone(), definition_ptr); + let ty = baml_type::RealizedTy::Class( + class.name.clone(), + Vec::new(), + baml_type::TyAttr::default(), + ); + (ty, defs, runtime) + } + Object::Enum(enm) => { + let runtime = enm.runtime_type.as_ref()?; + let mut defs = runtime.defs.clone(); + defs.enums.insert(enm.name.clone(), definition_ptr); + let ty = + baml_type::RealizedTy::Enum(enm.name.clone(), baml_type::TyAttr::default()); + (ty, defs, runtime) + } + _ => return None, + }; + Some(if runtime.owner.is_null() { + TypeValue::from_parts_with_defs(ty, runtime.mint, defs) + } else { + TypeValue::runtime_with_defs(ty, runtime.mint, defs, runtime.owner) + }) + } + + /// The exact minted value for `ty`, when `ty` is the bare spelling of a + /// runtime declaration `defs` carries. + /// + /// **The name has to be mint-unique.** A `DynTypeDefs` is keyed by + /// `QualifiedTypeName`, and only a `runtime_local` name (`user.$dyn.N.Foo` + /// — `reflect.class.new` / `reflect.enum.new`) has its mint *in* the name. + /// A static declaration and a runtime *package*'s declaration are both + /// plain `user.Foo`, and an overlay reaches a frame whether or not the + /// spelling that pulled it in is the one being recovered — `LoadType` + /// staples the whole frame overlay onto anything materialized there. So + /// matching an ordinary name against the overlay would answer a *different* + /// definition's mint: a static `Holder` in a frame that also touched a + /// runtime package's `Item` would report `type.of() != type.of()`, + /// and two compiled packages that both declare `Item` would cross-match. + /// Handing back a wrong identity is worse than handing back none, so + /// everything but a mint-unique name declines and re-derives normally. + /// + /// A decorated or parameterized spelling is a *different* type value than + /// the definition was minted as, so the recovered value must also describe + /// the same type to be usable — the equality check below is that guard, and + /// it keeps the rule out of the attribute-by-attribute business. + fn minted_declaration_value( + &self, + ty: &baml_type::RealizedTy, + defs: &DynTypeDefs, + ) -> Option { + let definition_ptr = match ty { + baml_type::RealizedTy::Class(name, args, _) + if args.is_empty() && name.is_runtime_minted() => + { + defs.classes.get(name) + } + baml_type::RealizedTy::Enum(name, _) if name.is_runtime_minted() => { + defs.enums.get(name) + } + _ => None, + }?; + let value = self.runtime_declaration_identity(*definition_ptr)?; + (value.ty == *ty).then_some(value) + } + fn load_global_in(&self, package: HeapPtr, index: GlobalIndex) -> Value { if package.as_ptr().is_null() { return self.globals.get(self.proof(), index); @@ -6025,6 +6159,17 @@ impl BexVm { ); self.pending_call_type_args = previous_type_args; self.pending_call_type_values = previous_type_values; + // FOLLOW-UP (not a defect today): the rooted copy of the values is + // dropped one line above, and the writes below read `options`, which + // borrows a caller *local* that no GC root covers. A collection between + // the two would forward the rooted copy and leave these pointers stale. + // It is unreachable as written — `execute_call_from_locals_offset` only + // pushes a frame and sizes the eval stack, with no TLAB allocation, and + // the native path that can allocate pushes no bytecode frame, so the + // guard below declines. Recorded because this lane now carries recovered + // identities as well as method-level ones, so the day something on that + // path starts allocating, this is where it bites. + // // A definition overlay can arrive without any type-argument slots of its // own — interface dispatch hands one down for a method that declares no // generics — so the metadata lane is written whenever any of the three @@ -7454,6 +7599,15 @@ impl BexVm { // `user.$dyn.N.Out` as a name nothing defines — reflection, // rendering and SAP inside an interface method would fail on // a type the caller can use fine. + // + // The clone is `O(defs)` on every dispatch that carries any + // (a static interface operand short-circuits on `is_empty`). + // `Arc` would make it a refcount bump, but it + // is not a drop-in: GC forwarding rewrites the pointers + // *inside* a `DynTypeDefs` in place, so sharing would have + // to be unshared again (`Arc::make_mut`) exactly where it + // pays off. It stays the lever if an interface-heavy + // profile ever asks for it. let iface_defs = iface_value .as_object_ptr() @@ -7584,9 +7738,36 @@ impl BexVm { } (callee, frame) }; - let type_values = method_type_args.as_ref().map_or_else(Vec::new, |method| { - append_virtual_method_type_args(&mut type_args, method) + // Exact runtime identity for the receiver's class-level + // slots. The resolver realizes those slots off `Self`, + // which is realized types only, so `type.of()` in an + // impl or default-method body would derive a *fresh* mint + // for a type the caller minted — structurally right, + // `==`-wrong, which breaks every identity-keyed pattern + // (BEP-066 I-1). The interface operand already carries the + // definitions those slots name, and each runtime definition + // records the mint it was created with, so the caller's + // value is read back rather than re-derived. Only the + // definition-carrying operand pays: a static interface + // leaves `iface_defs` empty and skips the walk entirely. + let owner_type_values = iface_defs.as_ref().map_or_else(Vec::new, |defs| { + let mut values: Vec> = type_args + .iter() + .map(|ty| self.minted_declaration_value(ty, defs)) + .collect(); + if values.iter().all(Option::is_none) { + values.clear(); + } + values }); + let type_values = match method_type_args.as_ref() { + Some(method) => append_virtual_method_type_args( + &mut type_args, + method, + owner_type_values, + ), + None => owner_type_values, + }; let locals_offset = StackIndex::from_raw(args_offset);