Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog.d/9242-release-parity-blockers.md
Original file line number Diff line number Diff line change
@@ -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.
38 changes: 38 additions & 0 deletions crates/perry-codegen/src/lower_call/method_override.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -892,6 +923,13 @@ pub(super) fn emit_guarded_direct_method_call(
shape_only_guard: bool,
subclass_arms: &[SubclassDispatchArm],
) -> Option<String> {
// `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))
Expand Down
10 changes: 10 additions & 0 deletions crates/perry-codegen/tests/native_proof_regressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
10 changes: 6 additions & 4 deletions test-files/test_parity_webassembly_graceful_fail_default.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Loading