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
21 changes: 21 additions & 0 deletions changelog.d/11581-function-own-properties-in-the-function.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
A function's own properties now live in the function object: an ordinary
shaped property object hangs off the closure header, traced by the collector
and installed with a write barrier. The three process-global closure side
tables (own properties, deleted keys, recorded prototypes), their young log
and their rekey/prune/scan passes are deleted. A function with only data
properties gets a keyed Function shape that is canonical per its property
object's key list.

The method-call site serves `F.m()` on such a function from that property
object (a new entry kind), so a namespace-style function's methods are called
directly like an ordinary object's.

Fixed, matching node: `bind` reads `length` through a getter; a deleted
`name` / `length` is inherited from `Function.prototype`; after
`Object.setPrototypeOf(fn, p)`, `fn.call` / `apply` / `bind` use `p`'s; and
calling a method a function object does not have (never set, deleted, or
absent from its prototype) throws a TypeError instead of returning `{}`.

Measured against the method-call base (instructions, real release profile,
5 interleaved rounds, each arm linking its own runtime): the Zod workload
1.609G -> 1.143G (-29.0%), the tsc workload 79.57G -> 78.03G (-1.9%).
7 changes: 5 additions & 2 deletions crates/perry-abi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,11 @@ pub const fn method_site_padded_argc(argc: usize) -> usize {
}
/// The entry `slot` bit for an own key in the receiver's spill buffer.
pub const METHOD_SITE_SPILL: u64 = 1 << 62;
/// The index bits of an entry's `slot` word (bits 61 and 60 are reserved for
/// the function-bag and accessor entry kinds).
/// The entry `slot` bit for an own key of a function-object receiver: an
/// inline slot of the object at `ClosureHeader::props`.
pub const METHOD_SITE_FUNCTION_BAG: u64 = 1 << 61;
/// The index bits of an entry's `slot` word (bit 60 is reserved for the
/// accessor entry kind).
pub const METHOD_SITE_INDEX_MASK: u64 = (1 << 60) - 1;
/// `object::ObjectMeta::spill` (the object-owned overflow buffer).
pub const OBJECT_META_SPILL_OFFSET: usize = 32;
54 changes: 50 additions & 4 deletions crates/perry-codegen/src/expr/method_site.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
//! s < 0 (inherited): PERRY_PROTO_VALIDITY == site.gen else MISS
//! h = site.closure ; f = site.func
//! own: v = load [recv + HDR + 8*s] ; v is a heap pointer else MISS
//! fn: v = load [[recv + PROPS] + HDR + 8*s] (bit 61: a function's
//! own-property object; same checks as own)
//! [v-8] & 0x80FF == CLOSURE ; [v+8] == site.func else NEXT WAY
//! h = handle(v) ; f = site.func
//! CALL: this = recv ; r = f(h, args...) ; restore this
Expand Down Expand Up @@ -88,6 +90,7 @@ pub(crate) fn emit_method_site(
// live function object: type `GC_TYPE_CLOSURE` and not a forwarded stub
// (`closure::is_closure_ptr`'s kind term; there is no payload magic).
let func_offset = crate::runtime_abi::CLOSURE_FUNC_PTR_OFFSET as i64;
let props_offset = crate::runtime_abi::CLOSURE_PROPS_OFFSET as i64;
let kind_offset = -(crate::runtime_abi::GC_HEADER_SIZE as i64);
let kind_mask = (0xFFu16 | (u16::from(crate::runtime_abi::GC_FLAG_FORWARDED) << 8)).to_string();
let closure_kind = crate::runtime_abi::GC_TYPE_CLOSURE.to_string();
Expand Down Expand Up @@ -188,12 +191,18 @@ pub(crate) fn emit_method_site(
};
let other_idx = ctx.new_block("msite.other");
let other2_idx = ctx.new_block("msite.other2");
let other3_idx = ctx.new_block("msite.other3");
let bag_idx = ctx.new_block("msite.fn_bag");
let bag2_idx = ctx.new_block("msite.fn_bag_load");
let spill_idx = ctx.new_block("msite.spill");
let spill2_idx = ctx.new_block("msite.spill_buf");
let spill3_idx = ctx.new_block("msite.spill_check");
let spill4_idx = ctx.new_block("msite.spill_load");
let other_l = ctx.block_label(other_idx);
let other2_l = ctx.block_label(other2_idx);
let other3_l = ctx.block_label(other3_idx);
let bag_l = ctx.block_label(bag_idx);
let bag2_l = ctx.block_label(bag2_idx);
let spill_l = ctx.block_label(spill_idx);
let spill2_l = ctx.block_label(spill2_idx);
let spill3_l = ctx.block_label(spill3_idx);
Expand All @@ -207,8 +216,8 @@ pub(crate) fn emit_method_site(
blk.cond_br(&tagged, &other_l, &own_l);
s
};
// other: inherited (bit 63) or own spill (bit 62); any other kind bit is
// not one this site knows, and misses.
// other: inherited (bit 63), own spill (bit 62) or function bag (bit 61);
// any other kind bit is not one this site knows, and misses.
ctx.current_block = other_idx;
{
let blk = ctx.block();
Expand All @@ -221,9 +230,39 @@ pub(crate) fn emit_method_site(
let spill_bit = blk.lshr(I64, &slot, "62");
let is_spill = blk.icmp_ne(I64, &spill_bit, "0");
let index = blk.and(I64, &slot, &index_mask);
blk.cond_br(&is_spill, &spill_l, &miss_l);
blk.cond_br(&is_spill, &spill_l, &other3_l);
index
};
ctx.current_block = other3_idx;
{
let blk = ctx.block();
let bag_bit = blk.lshr(I64, &slot, "61");
let is_bag = blk.icmp_ne(I64, &bag_bit, "0");
blk.cond_br(&is_bag, &bag_l, &miss_l);
}
// function bag: the receiver's own-property object, then its inline slot.
// The keyed Function ShapeId the word matched is canonical per that
// object's key list, so the object exists; the null test is a guard.
ctx.current_block = bag_idx;
let bag = {
let blk = ctx.block();
let pp = emit_field_ptr(blk, &biased, props_offset);
let bag = blk.load(I64, &pp);
let has = blk.icmp_ne(I64, &bag, "0");
blk.cond_br(&has, &bag2_l, &miss_l);
bag
};
ctx.current_block = bag2_idx;
let (bag_v, bag_end) = {
let blk = ctx.block();
let bptr = blk.inttoptr(I64, &bag);
let base = blk.gep(crate::types::I8, &bptr, &[(I64, &header.to_string())]);
let vp = blk.gep(I64, &base, &[(I64, &index)]);
let v = blk.load(I64, &vp);
let end = blk.label.clone();
blk.br(&value_l);
(v, end)
};
// own inline: load the slot.
ctx.current_block = own_idx;
let (inline_v, inline_end) = {
Expand Down Expand Up @@ -279,7 +318,14 @@ pub(crate) fn emit_method_site(
ctx.current_block = value_idx;
let own_ub = {
let blk = ctx.block();
let v = blk.phi(I64, &[(&inline_v, &inline_end), (&spill_v, &spill_end)]);
let v = blk.phi(
I64,
&[
(&inline_v, &inline_end),
(&spill_v, &spill_end),
(&bag_v, &bag_end),
],
);
let u = blk.sub(I64, &v, &(RECEIVER_BIAS as i64).to_string());
let heap = blk.icmp_ult(I64, &u, &(RECEIVER_SPAN as i64).to_string());
blk.cond_br(&heap, &own_fn_l, &miss_l);
Expand Down
7 changes: 3 additions & 4 deletions crates/perry-runtime/src/closure/alloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,10 +367,9 @@ pub struct ClosureHeader {
pub shape_id: u32,
/// Function pointer (the actual compiled function).
pub func_ptr: *const u8,
/// Reserved for the function object's shaped own-property record (D1).
/// ALWAYS NULL in this stage and not yet enumerated by the collector:
/// the stage that first writes it must make it a traced, rewritten
/// raw-pointer child edge (and barrier the store) in the same change.
/// The function object's own-property bag (`closure::props`, D1): null
/// until the first own property, then a traced, rewritten raw-pointer
/// child edge (`gc::layout`'s `ClosureCaptures` arm).
pub props: *mut crate::object::ObjectHeader,
}

Expand Down
28 changes: 27 additions & 1 deletion crates/perry-runtime/src/closure/dispatch/bound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,33 @@ pub unsafe extern "C" fn js_function_bind(
let target_closure = target_is_closure.then(|| {
JSValue::from_bits(target_h.get_nanbox_f64().to_bits()).as_pointer::<ClosureHeader>()
});
let target_len_f = if let Some(target_closure) = target_closure {
// Spec step 5: `HasOwnProperty(Target, "length")` then `Get(Target,
// "length")` — a GETTER installed by `Object.defineProperty(fn, "length",
// {get})` runs. Only a FunctionDictionary target can carry one (every
// accessor install leaves the described shapes), so the common case stays
// the own-data / registered-length read below. The getter may allocate:
// everything live is rooted above, and the target is re-read after.
let accessor_len = target_closure.and_then(|t| unsafe {
if crate::closure::shape::closure_on_base_shape(t) {
return None;
}
crate::object::get_accessor_descriptor(t as usize, "length")?;
let v = crate::closure::closure_get_dynamic_prop(t as usize, "length");
let jv = JSValue::from_bits(v.to_bits());
Some(if jv.is_int32() {
jv.as_int32() as f64
} else if jv.is_number() {
jv.as_number()
} else {
0.0
})
});
let target_closure = target_is_closure.then(|| {
JSValue::from_bits(target_h.get_nanbox_f64().to_bits()).as_pointer::<ClosureHeader>()
});
let target_len_f = if let Some(len) = accessor_len {
len
} else if let Some(target_closure) = target_closure {
match crate::closure::closure_get_own_dynamic_prop(target_closure as usize, "length") {
Some(v) => {
let jv = JSValue::from_bits(v.to_bits());
Expand Down
Loading
Loading