diff --git a/changelog.d/9242-release-parity-blockers.md b/changelog.d/9242-release-parity-blockers.md new file mode 100644 index 0000000000..e3ff384c9d --- /dev/null +++ b/changelog.d/9242-release-parity-blockers.md @@ -0,0 +1,4 @@ +Method calls now preserve same-named instance fields declared on the receiver +class or an ancestor instead of bypassing them with direct prototype dispatch. +The default-runtime WebAssembly parity fixture also reliably stays out of the +auto-linked host mode, restoring coverage of graceful degradation. diff --git a/crates/perry-codegen/src/lower_call/method_override.rs b/crates/perry-codegen/src/lower_call/method_override.rs index 27e5d374d5..ff51517fde 100644 --- a/crates/perry-codegen/src/lower_call/method_override.rs +++ b/crates/perry-codegen/src/lower_call/method_override.rs @@ -867,6 +867,37 @@ pub(super) struct SubclassDispatchArm { pub target_fn: String, } +/// A declared instance field is an own property on every constructed object, +/// so it wins over a same-named prototype method. The direct-method guards +/// prove the receiver's class/shape and prototype stability, but that is not +/// enough to skip ordinary own-property lookup when the expected shape itself +/// contains the method name. Computed fields are conservatively treated as a +/// possible shadow because their runtime key is not available here. +fn class_chain_may_declare_method_field(ctx: &FnCtx<'_>, class_name: &str, property: &str) -> bool { + let mut current = Some(class_name.to_string()); + let mut seen = std::collections::HashSet::new(); + for _ in 0..64 { + let Some(name) = current else { + return false; + }; + if !seen.insert(name.clone()) { + return true; + } + let Some(class) = ctx.classes.get(&name).copied() else { + return true; + }; + if class + .fields + .iter() + .any(|field| field.key_expr.is_some() || (!field.is_private && field.name == property)) + { + return true; + } + current = class.extends_name.clone(); + } + true +} + /// Emit a typed-feedback runtime guard before a known class-method direct call. /// /// The guard validates that the receiver still has the expected class shape, @@ -892,6 +923,13 @@ pub(super) fn emit_guarded_direct_method_call( shape_only_guard: bool, subclass_arms: &[SubclassDispatchArm], ) -> Option { + // `class C { m = fn; m() {} }` and an inherited field with the same name + // both require ordinary own-property lookup. Falling back here reaches + // `emit_own_method_override_check` / dynamic dispatch in the caller. + if class_chain_may_declare_method_field(ctx, receiver_class_name, property) { + return None; + } + let truthy_result_kind = ctx .truthy_call_result_requested .then(|| constructive_method_truthiness(ctx, direct_fn)) diff --git a/crates/perry-codegen/tests/native_proof_regressions.rs b/crates/perry-codegen/tests/native_proof_regressions.rs index 1645262e07..68fdfdf815 100644 --- a/crates/perry-codegen/tests/native_proof_regressions.rs +++ b/crates/perry-codegen/tests/native_proof_regressions.rs @@ -13896,6 +13896,16 @@ fn scalar_method_boolean_predicate_rejects_mutation_call_accessor_and_dynamic_pr ), "mutation must dispatch directly to the resolved method on the heap receiver:\n{ir}" ); + } else if case == "inherited_field_shadow" { + // A declared field on the base class is an own property on the + // constructed child. The safe fallback therefore probes the own + // slot and invokes its value, rather than entering the prototype + // method dispatcher that would skip the shadow. + assert!( + ir.contains("call double @js_object_get_own_field_or_undef") + && ir.contains("call double @js_native_call_value"), + "inherited field shadow must probe and dispatch the own method value:\n{ir}" + ); } else { assert!( ir.contains("call double @js_native_call_method"), diff --git a/test-files/test_parity_webassembly_graceful_fail_default.ts b/test-files/test_parity_webassembly_graceful_fail_default.ts index 87ee4cafd9..492b71a7db 100644 --- a/test-files/test_parity_webassembly_graceful_fail_default.ts +++ b/test-files/test_parity_webassembly_graceful_fail_default.ts @@ -1,7 +1,9 @@ -// Resolve the namespace through a computed key so this test deliberately does -// not trigger the compiler's static WebAssembly host auto-linking. It checks -// the default runtime's honest graceful degradation instead. -const WA: any = (globalThis as any)["Web" + "Assembly"]; +// Resolve the namespace through a runtime-computed key so this test +// deliberately does not trigger WebAssembly host auto-linking. A `+` here is +// constant-folded before feature detection, which would turn this default- +// runtime fixture into a host-runtime test. +const namespaceKey = ["Web", "Assembly"].join(""); +const WA: any = (globalThis as any)[namespaceKey]; const validAdd = new Uint8Array([ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01,