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
6 changes: 6 additions & 0 deletions changelog.d/9123-prototype-method-delete-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### fix(runtime): honor prototype method deletion in typed direct guards

Typed-feedback direct-method guards now consult the same per-name prototype
invalidation latch as inline shape guards. Deleting a declared prototype
method therefore falls back to ordinary dispatch and throws TypeError
instead of invoking the stale compiled method body. Fixes #9123.
5 changes: 5 additions & 0 deletions crates/perry-runtime/src/typed_feedback/guards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ fn method_direct_call_contract(
);
};
let name_hash = hash_bytes(method_bytes);
let method_guard_slot = crate::object::class_prototype_method_guard_slot(method_name);
if crate::object::class_prototype_fast_guard_invalidated_for_method(method_guard_slot) {
return (shape_addr, class_id, gc_type, name_hash, false);
}

if object_addr == 0
|| expected_class_id == 0
|| !crate::object::shapes::is_shape_id(expected_shape_id)
Expand Down
40 changes: 40 additions & 0 deletions crates/perry-runtime/src/typed_feedback/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2251,6 +2251,46 @@ fn typed_feedback_method_direct_guard_fails_for_own_method_replacement() {
assert_eq!(site.fallback_calls, 1);
}

#[test]
fn typed_feedback_method_direct_guard_fails_after_method_invalidation() {
let _guard = typed_feedback_test_lock();
reset_typed_feedback_for_tests();
register(
9123,
TypedFeedbackSiteKind::MethodCall,
"obj.deleted_9123()",
);

let class_id = 0x7EED_9123;
let method_name = b"deleted_9123";
let (obj, _, _, receiver) = class_instance(class_id, b"x");
let expected_shape_id = shape_id(obj);
unsafe { register_test_method(class_id, method_name) };

let guard = || unsafe {
js_typed_feedback_method_direct_call_guard(
9123,
receiver,
class_id,
expected_shape_id,
method_name.as_ptr() as *const i8,
method_name.len(),
test_direct_method_ptr(),
)
};
assert_eq!(guard(), 1);

// A delete has no replacement value for the contract to discover. The
// sticky per-name latch is the authoritative evidence that the declared
// vtable method may no longer be callable.
crate::object::invalidate_class_prototype_fast_guards_for_method("deleted_9123");
assert_eq!(guard(), 0);

let site = &typed_feedback_snapshot().sites[0];
assert_eq!(site.guard_passes, 1);
assert_eq!(site.guard_failures, 1);
}

#[test]
fn typed_feedback_method_direct_guard_fails_for_prototype_method_registration() {
let _guard = typed_feedback_test_lock();
Expand Down
73 changes: 73 additions & 0 deletions crates/perry/tests/issue_9123_method_delete_invalidation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! Regression coverage for #9123: deleting a declared prototype method must
//! invalidate compiler-emitted direct-method guards.

use std::path::PathBuf;
use std::process::Command;

fn perry_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_perry"))
}

#[test]
fn prototype_method_delete_invalidates_direct_dispatch() {
let dir = tempfile::tempdir().expect("tempdir");
let entry = dir.path().join("main.ts");
let output = dir.path().join("main_bin");
std::fs::write(
&entry,
r#"
class C {
a = 5;

inc(): number {
return this.a + 1;
}
}

function viaParam(c: C, n: number): number {
let last = 0;
for (let i = 0; i < n; i++) {
last = c.inc();
}
return last;
}

delete (C.prototype as any).inc;
try {
console.log(viaParam(new C(), 1));
} catch (error) {
console.log("threw:", (error as Error).constructor.name);
}
"#,
)
.expect("write source");

let compile = Command::new(perry_bin())
.current_dir(dir.path())
.arg("compile")
.arg(&entry)
.arg("-o")
.arg(&output)
.arg("--no-cache")
.arg("--no-auto-optimize")
.output()
.expect("run perry compile");
assert!(
compile.status.success(),
"perry compile failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&compile.stdout),
String::from_utf8_lossy(&compile.stderr)
);

let run = Command::new(&output)
.current_dir(dir.path())
.output()
.expect("run compiled binary");
assert!(
run.status.success(),
"compiled binary failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&run.stdout),
String::from_utf8_lossy(&run.stderr)
);
assert_eq!(String::from_utf8_lossy(&run.stdout), "threw: TypeError\n");
}
Loading