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
41 changes: 40 additions & 1 deletion crates/perry-runtime/src/closure/dispatch/bound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,46 @@ pub unsafe fn dispatch_bound_method(closure: *const ClosureHeader, args: &[f64])
// the rebind targets the right object. Ordinary `obj.method(args)` calls do
// NOT reach here (they lower straight to `js_native_call_method`), so this
// only governs method-as-value invocations.
namespace_obj = crate::object::canonical_bound_method_receiver(namespace_obj);
// The captured slot-0 prototype-ref names the OWNER class. A method value is a
// FIXED function object (spec: reading `C.prototype.m` yields *that* function;
// invoking it later does not re-resolve `m` against whatever receiver it is
// called on), so dispatch the OWNER's body with the call-time `this` rather than
// re-resolving the name on the RECEIVER's class below.
//
// Re-resolution breaks as soon as the value is COPIED onto another class's
// prototype — `Execute.prototype.resultsetHeader = Query.prototype.resultsetHeader`,
// the block copy mysql2's Command state machine performs. Invoking it on an
// `Execute` then re-resolved "resultsetHeader" against Execute, found the copy
// (this very closure), and recursed until the call-depth guard returned the null
// object — user-visible as `[object Object]` instead of the method's result,
// exactly the failure the comment below describes for the self-shadowing `bind`
// case.
let owner_proto_ref = namespace_obj;
let call_receiver = crate::object::canonical_bound_method_receiver(owner_proto_ref);
if method_name_len > 0 && !method_name_ptr.is_null() {
if let Some(owner_id) = crate::object::class_prototype_ref_id(owner_proto_ref) {
if let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts(
method_name_ptr as *const u8,
method_name_len,
)) {
if let Some((func_ptr, param_count, has_synth_args, has_rest)) =
crate::object::lookup_class_method_in_chain(owner_id, name)
{
return crate::object::call_vtable_method(
func_ptr,
call_receiver.to_bits() as i64,
args.as_ptr(),
args.len(),
param_count,
has_synth_args,
has_rest,
);
}
}
}
}

namespace_obj = call_receiver;

// A bound-method VALUE (`const f = obj.method`) is resolved at READ time and
// must always invoke that method — even if `obj.method` is later reassigned.
Expand Down
65 changes: 65 additions & 0 deletions test-files/test_gap_cross_class_prototype_method_copy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copying a method value from one class's prototype onto another's — the block
// copy mysql2's Command state machine performs
// (`Execute.prototype.resultsetHeader = Query.prototype.resultsetHeader`).
//
// A method value is a FIXED function object: invoking it must run the OWNER's
// body with the call-time `this`, never re-resolve the method name against the
// receiver's own class. Re-resolving finds the copy itself and recurses until
// the call-depth guard yields the null object (`[object Object]`).

class Query {
tag = "Query";
resultsetHeader(this: any, n: number): any {
return "Q.rsh:" + this.tag + ":" + n;
}
done(this: any): any {
return "Q.done:" + this.tag;
}
}

class Execute {
tag = "Execute";
start(this: any): any {
return (Execute as any).prototype.resultsetHeader;
}
// Execute has its OWN readField; the copied Query method reads it off `this`,
// which must still resolve virtually to Execute's override.
readField(this: any): any {
return "Execute.readField";
}
}

(Execute as any).prototype.resultsetHeader = (Query as any).prototype.resultsetHeader;
(Execute as any).prototype.done = (Query as any).prototype.done;

const ex: any = new Execute();

// direct call through the copied prototype slot
console.log("direct :", ex.resultsetHeader(6));
console.log("copied done :", ex.done());

// as a state-machine value: read it, then invoke it later (mysql2's `this.next`)
const next = ex.start();
console.log("typeof next :", typeof next);
console.log("invoke next :", next.call(ex, 6));

// the value read straight off the source prototype
const q: any = new Query();
const m = (Query as any).prototype.resultsetHeader;
console.log("proto value :", typeof m, m.call(q, 1));

// an override on the receiver must NOT hijack the copied owner's body
class Sub extends Query {
resultsetHeader(this: any, n: number): any {
return "Sub.rsh:" + n;
}
}
const sub: any = new Sub();
console.log("override :", sub.resultsetHeader(2));
console.log("owner body :", (Query as any).prototype.resultsetHeader.call(sub, 3));

// method identity: reading a method twice yields the same function object
console.log(
"identity :",
(Query as any).prototype.done === (Query as any).prototype.done,
);