From 69c98cfbfea851020eb1eff8235e0509bc699d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 31 Aug 2026 06:12:00 +0200 Subject: [PATCH 1/4] fix(codegen): preserve declared method field shadows --- .../src/lower_call/method_override.rs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) 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)) From 735d0918156a4ea675cb9b5e99f2088c996ae650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 31 Aug 2026 06:18:11 +0200 Subject: [PATCH 2/4] test(wasm): keep default-runtime fixture out of host mode --- .../test_parity_webassembly_graceful_fail_default.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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, From d0e8bf050d24f0478226a74a1bd7f696b9763972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 31 Aug 2026 06:24:11 +0200 Subject: [PATCH 3/4] docs(changelog): record release parity fixes --- changelog.d/9242-release-parity-blockers.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog.d/9242-release-parity-blockers.md 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. From 7160a34b008a2190e2cf6a910b06964aa0b23e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 31 Aug 2026 06:34:32 +0200 Subject: [PATCH 4/4] test(codegen): expect own-value dispatch for field shadows --- crates/perry-codegen/tests/native_proof_regressions.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) 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"),