-
-
Notifications
You must be signed in to change notification settings - Fork 161
perf(runtime): make Function.prototype.bind's name/length metadata lazy #10119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| ### Performance | ||
|
|
||
| - **`Function.prototype.bind` no longer eagerly builds `"bound " + name` or its | ||
| `name`/`length` property-attribute records on every call.** A bind whose | ||
| result never reads `.name`/`.length` now performs neither the runtime-string | ||
| allocation nor the two `set_builtin_property_attrs` side-table inserts — | ||
| redundant, since a closure with no dynamic-prop entry for those keys already | ||
| defaults correctly everywhere it's observed. `.name`'s string is built and | ||
| cached lazily on first actual read, through the same seam every other reader | ||
| of a closure's `.name` already goes through, so `Object. | ||
| getOwnPropertyDescriptor`, `console.log`, and a chained `.bind().bind()` all | ||
| still see the right value. `Get(Target, "name")` still runs synchronously at | ||
| bind time, so a throwing `name` getter on the target still fails `bind()` | ||
| itself. Refs #10084. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -290,13 +290,31 @@ fn format_function_for_console(closure_ptr: *const crate::closure::ClosureHeader | |
| registered_name_string(func_ptr as usize).filter(|n| !n.is_empty()) | ||
| } | ||
| }; | ||
| let label = match registry_name.or_else(|| { | ||
| props | ||
| .iter() | ||
| .find(|(k, _)| k == "name") | ||
| .and_then(|(_, v)| jsvalue_string_content(*v)) | ||
| .filter(|n| !n.is_empty()) | ||
| }) { | ||
| let label = match registry_name | ||
| .or_else(|| { | ||
| props | ||
| .iter() | ||
| .find(|(k, _)| k == "name") | ||
| .and_then(|(_, v)| jsvalue_string_content(*v)) | ||
| .filter(|n| !n.is_empty()) | ||
| }) | ||
| .or_else(|| { | ||
| // #10084: a `Function.prototype.bind` result's `.name` is built | ||
| // lazily and so may be absent from both the func-ptr registry | ||
| // (bound closures share the `BOUND_FUNCTION_FUNC_PTR` sentinel, | ||
| // never registered with a per-instance name) and the `props` | ||
| // snapshot above (taken before any read materialized it). | ||
| // Synthesize (and cache) it the same way any other reader of | ||
| // `.name` would. | ||
| unsafe { | ||
| ((*closure_ptr).func_ptr == crate::closure::BOUND_FUNCTION_FUNC_PTR).then(|| { | ||
| jsvalue_string_content(crate::closure::bound_function_lazy_name( | ||
| closure_ptr as usize, | ||
| )) | ||
| }) | ||
| } | ||
| .flatten() | ||
|
Comment on lines
+301
to
+316
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Gate the lazy name fallback on own-key presence.
🤖 Prompt for AI Agents |
||
| }) { | ||
| Some(name) => format!("[Function: {name}]"), | ||
| None => "[Function (anonymous)]".to_string(), | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -435,20 +435,79 @@ pub(crate) fn rebind_explicit_this(target: f64, this_arg: f64) -> f64 { | |
| f64::from_bits(crate::closure::clone_closure_rebind_this(bits, this_arg)) | ||
| } | ||
|
|
||
| /// Read a callable's own `name` *property* as a Rust `String`, if present and a | ||
| /// String value. Covers names installed by `Object.defineProperty(fn, "name", | ||
| /// …)` and the `"bound …"` name a prior `.bind()` stores, neither of which is | ||
| /// visible through the declared-name func-ptr registry. Returns `None` when no | ||
| /// such property exists or it isn't a String. | ||
| unsafe fn read_function_name_property(closure_ptr: usize) -> Option<String> { | ||
| /// Fallback target name for [`bound_function_lazy_name`]: the target-name | ||
| /// snapshot captured at bind time (capture slot 3) was not a String (no | ||
| /// override, or an explicit non-String `Object.defineProperty` value — both | ||
| /// collapse to the same declared-name fallback, matching the prior eager | ||
| /// behavior), so fall back to the target's *declared* name — the func-ptr | ||
| /// registry for a closure, or the class registry for a class ref. Both | ||
| /// registries are immutable for the life of the program, so resolving them | ||
| /// lazily here instead of at bind time is observationally identical. | ||
| unsafe fn bound_target_declared_name(target_value: f64) -> String { | ||
| use crate::value::JSValue; | ||
| let name_val = crate::closure::closure_get_dynamic_prop(closure_ptr, "name"); | ||
| let name_jv = JSValue::from_bits(name_val.to_bits()); | ||
| if !name_jv.is_any_string() { | ||
| return None; | ||
| let target_jv = JSValue::from_bits(target_value.to_bits()); | ||
| if target_jv.is_pointer() { | ||
| let target_closure = target_jv.as_pointer::<ClosureHeader>(); | ||
| if !target_closure.is_null() && (*target_closure).type_tag == CLOSURE_MAGIC { | ||
| return crate::builtins::function_name_for_ptr((*target_closure).func_ptr as usize) | ||
| .unwrap_or_default(); | ||
| } | ||
| return String::new(); | ||
| } | ||
| let hdr = crate::builtins::js_string_coerce(name_val); | ||
| crate::object::has_own_helpers::str_from_string_header(hdr).map(str::to_owned) | ||
| let target_class_id = crate::object::class_ref_id(target_value).or_else(|| { | ||
| ((target_value.to_bits() >> 48) == 0x7FFE | ||
| && crate::object::class_prototype_ref_id(target_value).is_none()) | ||
| .then_some((target_value.to_bits() & 0xFFFF_FFFF) as u32) | ||
| }); | ||
| target_class_id | ||
| .and_then(crate::object::class_name_for_id) | ||
| .unwrap_or_default() | ||
| } | ||
|
|
||
| /// Lazily synthesize and cache a bound function's `.name`. `ptr` must be a | ||
| /// live `BOUND_FUNCTION_FUNC_PTR` closure with no `"name"` entry in its own | ||
| /// dynamic-prop table yet (the caller — [`closure_get_dynamic_prop`], | ||
| /// `Object.getOwnPropertyDescriptor`, and the console-formatting path — all | ||
| /// check that first). Refs #10084: `js_function_bind` no longer builds | ||
| /// `"bound " + targetName` or writes it to the dynamic-prop table on every | ||
| /// call; that work happens here, once, on first actual `.name` read, and the | ||
| /// result is cached via `closure_set_dynamic_prop` so repeat reads are O(1). | ||
| /// | ||
| /// Capture slot 3 holds the raw `Get(Target, "name")` value snapshotted at | ||
| /// bind time (a String value, or a non-String sentinel — see | ||
| /// `bound_target_declared_name`); capture slot 0 holds the original bind | ||
| /// target, used for the declared-name fallback and, transitively, for a | ||
| /// chained `f.bind().bind()` (reading slot 3 of an inner bound closure | ||
| /// recurses into this same function through `closure_get_dynamic_prop`). | ||
| /// | ||
| /// GC safety: `ptr`'s address must not be trusted across the allocating | ||
| /// `js_string_coerce`/`js_string_from_bytes` calls below, so it is rooted and | ||
| /// re-derived afterward before the final cache write (mirrors | ||
| /// `js_object_get_own_property_descriptor`'s closure arm, #6943). | ||
| pub(crate) unsafe fn bound_function_lazy_name(ptr: usize) -> f64 { | ||
| use crate::value::JSValue; | ||
|
|
||
| let scope = crate::gc::RuntimeHandleScope::new(); | ||
| let ptr_handle = scope.root_raw_mut_ptr(ptr as *mut u8); | ||
| let (name_value, ptr_raw) = ptr_handle.across_mut::<u8, _>(|| { | ||
| let closure = ptr as *const ClosureHeader; | ||
| let name_hint = js_closure_get_capture_f64(closure, 3); | ||
| let target_name = if JSValue::from_bits(name_hint.to_bits()).is_any_string() { | ||
| let hdr = crate::builtins::js_string_coerce(name_hint); | ||
| crate::object::has_own_helpers::str_from_string_header(hdr) | ||
| .map(str::to_owned) | ||
| .unwrap_or_default() | ||
| } else { | ||
| bound_target_declared_name(js_closure_get_capture_f64(closure, 0)) | ||
| }; | ||
|
Comment on lines
+500
to
+502
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- target hunk ---'
sed -n '450,525p' crates/perry-runtime/src/closure/dispatch/bound.rs
printf '%s\n' '--- direct definitions/callers ---'
rg -n -C 5 'bound_target_declared_name|bound_function_lazy_name|js_function_bind|closure_set_dynamic_prop|js_closure_get_capture_f64' crates/perry-runtime/srcRepository: PerryTS/perry Length of output: 50370 🤖 get_repo_knowledge executed:
Length of output: 27850 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- bound.rs target and bind flow ---'
sed -n '360,525p' crates/perry-runtime/src/closure/dispatch/bound.rs
printf '%s\n' '--- exact bind-related symbols in closure sources ---'
rg -n '^(pub )?(unsafe )?extern "C" fn js_function_bind|^(pub )?fn (bound_target_declared_name|bound_function_lazy_name)|bound_target_declared_name|bound_function_lazy_name' crates/perry-runtime/src/closure
printf '%s\n' '--- dynamic property definition ---'
rg -n -l 'closure_set_dynamic_prop' crates/perry-runtime/src/closure
rg -n -C 12 'fn closure_set_dynamic_prop|pub fn closure_set_dynamic_prop' crates/perry-runtime/src/closureRepository: PerryTS/perry Length of output: 12953 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- js_function_bind implementation ---'
sed -n '525,625p' crates/perry-runtime/src/closure/dispatch/bound.rs
printf '%s\n' '--- lazy name call site ---'
sed -n '820,870p' crates/perry-runtime/src/closure/dynamic_props.rs
printf '%s\n' '--- relevant tests ---'
rg -n -C 5 'bind.*name|name.*bind|bound .*name|undefined.*bind' crates/perry-runtime test-parity 2>/dev/null | head -n 240Repository: PerryTS/perry Length of output: 28164 🌐 Web query:
💡 Result: <search_synthesis> <source_evidence> Citations:
Use an empty target name for non-String
🤖 Prompt for AI Agents |
||
| let bound_name = format!("bound {target_name}"); | ||
| let name_ptr = | ||
| crate::string::js_string_from_bytes(bound_name.as_ptr(), bound_name.len() as u32); | ||
| f64::from_bits(JSValue::string_ptr(name_ptr).bits()) | ||
| }); | ||
| let ptr = ptr_raw as usize; | ||
| crate::closure::closure_set_dynamic_prop(ptr, "name", name_value); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C12 \
'fn closure_set_dynamic_prop|closure_set_dynamic_prop\s*\(' \
crates/perry-runtime/srcRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: set -euo pipefail
printf '%s\n' '--- bound_function_lazy_name ---'
sed -n '460,515p' crates/perry-runtime/src/closure/dispatch/bound.rs
printf '%s\n' '--- closure_set_dynamic_prop and helpers ---'
sed -n '1020,1110p' crates/perry-runtime/src/closure/dynamic_props.rs
rg -n -C8 'fn note_young_closure_owner|fn barrier_closure_dynamic_props|RuntimeHandleScope|root_nanbox_f64' crates/perry-runtime/src/closure crates/perry-runtime/src/gc.rsRepository: PerryTS/perry Length of output: 44033 🏁 Script executed: set -euo pipefail
printf '%s\n' '--- GC module files ---'
fd -t f -i 'gc' crates/perry-runtime/src | head -40
printf '%s\n' '--- RuntimeHandleScope definitions ---'
rg -n -C12 'struct RuntimeHandleScope|impl RuntimeHandleScope|root_nanbox_f64|root_raw_mut_ptr' crates/perry-runtime/src
printf '%s\n' '--- external slot barrier definition ---'
rg -n -C15 'runtime_write_barrier_external_slot' crates/perry-runtime/srcRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: set -euo pipefail
scope_file=$(rg -l 'pub struct RuntimeHandleScope|struct RuntimeHandleScope' crates/perry-runtime/src | head -1)
printf '%s\n' "--- scope definition: $scope_file ---"
rg -n -C18 'pub struct RuntimeHandleScope|impl RuntimeHandleScope|pub fn root_nanbox_f64|pub fn root_raw_mut_ptr|pub fn root_heap_word_u64' "$scope_file"
printf '%s\n' '--- external barrier definitions ---'
rg -l 'fn runtime_write_barrier_external_slot|runtime_write_barrier_external_slot' crates/perry-runtime/src | head -10
rg -n -C18 'fn runtime_write_barrier_external_slot|pub fn runtime_write_barrier_external_slot' crates/perry-runtime/src/gc* crates/perry-runtime/src 2>/dev/null | head -120Repository: PerryTS/perry Length of output: 15090 Root 🤖 Prompt for AI AgentsSource: Learnings |
||
| name_value | ||
| } | ||
|
|
||
| /// `Function.prototype.bind(thisArg, ...boundArgs)` — create a distinct bound | ||
|
|
@@ -457,8 +516,18 @@ unsafe fn read_function_name_property(closure_ptr: usize) -> Option<String> { | |
| /// the BOUND_FUNCTION_FUNC_PTR sentinel; `js_closure_callN` / | ||
| /// `js_native_call_value` route it through `dispatch_bound_function`. | ||
| /// | ||
| /// `.name` is set to `"bound " + target.name` and `.length` to | ||
| /// `max(0, target.length - boundArgs.length)`, matching Node. Refs #2840. | ||
| /// `.name` reads as `"bound " + target.name` and `.length` as | ||
| /// `max(0, target.length - boundArgs.length)`, matching Node — but neither is | ||
| /// built eagerly (#10084). `.length`'s numeric value is cheap to compute | ||
| /// (no string work) and is stored eagerly as before; `.name`'s `"bound "` | ||
| /// string and the `set_builtin_property_attrs` calls a prior version made | ||
| /// unconditionally for both are gone from this function entirely — absent a | ||
| /// dynamic-prop table entry, a closure's `name`/`length` already default to | ||
| /// `{writable:false, enumerable:false, configurable:true}` everywhere they're | ||
| /// observed (`closure_dynamic_enumerable_props`, | ||
| /// `js_object_get_own_property_descriptor`, `closure_set_field_by_name`), so | ||
| /// those calls were redundant. `.name`'s string is built lazily by | ||
| /// `bound_function_lazy_name`, on first actual read. Refs #2840. | ||
| #[no_mangle] | ||
| pub unsafe extern "C" fn js_function_bind( | ||
| target_value: f64, | ||
|
|
@@ -485,28 +554,67 @@ pub unsafe extern "C" fn js_function_bind( | |
| && crate::object::class_prototype_ref_id(target_value).is_none()) | ||
| .then_some((target_value.to_bits() & 0xFFFF_FFFF) as u32) | ||
| }); | ||
| let target_closure = if target_jv.is_pointer() { | ||
| let target_is_closure = if target_jv.is_pointer() { | ||
| let ptr = target_jv.as_pointer::<ClosureHeader>(); | ||
| if ptr.is_null() || (*ptr).type_tag != CLOSURE_MAGIC { | ||
| // Preserve the existing conservative pass-through for callable | ||
| // native handles that do not use the closure representation. | ||
| return target_value; | ||
| } | ||
| Some(ptr) | ||
| true | ||
| } else if target_class_id.is_some() { | ||
| // ClassRefs are callable/constructable INT32-tagged values rather | ||
| // than heap closures. They still need a real BoundFunction wrapper | ||
| // so `new C.bind(_, ...args)()` prepends its captured arguments. | ||
| None | ||
| false | ||
| } else { | ||
| return target_value; | ||
| }; | ||
|
|
||
| // Root the bind target across every allocating call below (`this` | ||
| // boxing, `Get(Target, "name")` — which may run a user getter — the | ||
| // partial-args array, and the bound closure itself) so none of them can | ||
| // leave a stale address in the bound closure's own capture slots after a | ||
| // copying minor. | ||
| let scope = crate::gc::RuntimeHandleScope::new(); | ||
| let target_h = scope.root_nanbox_f64(target_value); | ||
|
|
||
| let bound_this = if args_len >= 1 && !args_ptr.is_null() { | ||
| coerce_call_this(target_value, *args_ptr) | ||
| let arg0 = *args_ptr; | ||
| target_h | ||
| .across_nanbox(|| coerce_call_this(target_h.get_nanbox_f64(), arg0)) | ||
| .0 | ||
| } else { | ||
| f64::from_bits(crate::value::TAG_UNDEFINED) | ||
| }; | ||
| let this_h = scope.root_nanbox_f64(bound_this); | ||
|
|
||
| // Spec step 12-13: `Get(Target, "name")` must run now, synchronously — a | ||
| // target whose `name` getter throws must fail `bind()` itself (Test262 | ||
| // bind/instance-name-error.js), not a later `.name` read on the bound | ||
| // function. A class target has no analogous accessor path, so | ||
| // TAG_UNDEFINED (the "no override" sentinel `bound_function_lazy_name` | ||
| // recognizes via `bound_target_declared_name`) is captured directly. This | ||
| // is the ONLY work `.name` does at bind time now — see | ||
| // `bound_function_lazy_name` for the deferred "bound " + name build. | ||
| let name_hint = if target_is_closure { | ||
| target_h | ||
| .across_nanbox(|| { | ||
| let tclosure = | ||
| JSValue::from_bits(target_h.get_nanbox_f64().to_bits()).as_pointer::<u8>(); | ||
| crate::closure::closure_get_dynamic_prop(tclosure as usize, "name") | ||
| }) | ||
| .0 | ||
|
Comment on lines
+600
to
+607
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline crates/perry-runtime/src/closure/dispatch/bound.rs \
--match js_function_bind --view expanded
rg -n -C5 \
'js_function_bind\s*\(|root_nanbox|args_ptr|RuntimeHandleScope' \
cratesRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: set -euo pipefail
printf '%s\n' '--- bind implementation ---'
rg -n -C35 'pub extern "C" fn js_function_bind|fn js_function_bind|args_ptr\[1\.\.' crates/perry-runtime/src/closure/dispatch/bound.rs
printf '%s\n' '--- exact bind callers ---'
rg -n -C8 'js_function_bind\s*\(' crates/perry-runtime crates/perry-codegen crates/perry-compiler 2>/dev/null | head -n 300
printf '%s\n' '--- ABI/context definitions ---'
rg -n -C12 'struct .*Args|args_ptr:|args_len:|RuntimeHandleScope|root_nanbox' crates/perry-runtime/src/closure crates/perry-runtime/src/gc crates/perry-runtime/src 2>/dev/null | head -n 500Repository: PerryTS/perry Length of output: 12128 🤖 get_repo_knowledge executed:
Length of output: 22503 🏁 Script executed: set -euo pipefail
printf '%s\n' '--- complete bind body ---'
sed -n '532,700p' crates/perry-runtime/src/closure/dispatch/bound.rs
printf '%s\n' '--- bind dispatch context and caller ---'
sed -n '1,120p' crates/perry-runtime/src/object/native_call_method/common_methods.rs
rg -n -C12 'fn .*dispatch.*args_ptr|args_ptr: \*const f64|args_ptr:.*f64|NmCtx|NativeCall' crates/perry-runtime/src/closure crates/perry-runtime/src/object/native_call_method crates/perry-runtime/src/object/native_module_dispatch
printf '%s\n' '--- handle-scope API ---'
rg -n -C8 'pub struct RuntimeHandleScope|impl RuntimeHandleScope|root_nanbox|root_raw|struct RuntimeHandle' crates/perry-runtime/src/gcRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: set -euo pipefail
printf '%s\n' '--- bind body ---'
sed -n '532,660p' crates/perry-runtime/src/closure/dispatch/bound.rs
printf '%s\n' '--- direct bind dispatch callers ---'
sed -n '1,110p' crates/perry-runtime/src/object/native_call_method/common_methods.rs
sed -n '1,105p' crates/perry-runtime/src/object/native_call_method/primitive_methods.rs
printf '%s\n' '--- dispatch definitions ---'
rg -l 'dispatch_common|args_ptr: \*const f64|pub.*args_ptr' crates/perry-runtime/src/object/native_call_method
for f in $(rg -l 'dispatch_common|args_ptr: \*const f64|pub.*args_ptr' crates/perry-runtime/src/object/native_call_method); do
echo "--- $f ---"
rg -n -C10 'dispatch_common|args_ptr: \*const f64|pub.*args_ptr' "$f"
done
printf '%s\n' '--- handle API definitions ---'
rg -n -C12 'pub struct RuntimeHandleScope|impl RuntimeHandleScope|pub fn root_nanbox|pub fn root_raw' crates/perry-runtime/src/gc --glob '*.rs'Repository: PerryTS/perry Length of output: 42946 🏁 Script executed: set -euo pipefail
printf '%s\n' '--- native method dispatch root and argument construction ---'
rg -n -C18 'arg_handles|dispatch_common\(|dispatch_primitive\(' crates/perry-runtime/src/object/native_call_method --glob '*.rs'
printf '%s\n' '--- all js_function_bind call sites ---'
rg -n -C6 'js_function_bind\s*\(' crates/perry-runtime/src --glob '*.rs'
printf '%s\n' '--- remaining bind body ---'
sed -n '660,735p' crates/perry-runtime/src/closure/dispatch/bound.rsRepository: PerryTS/perry Length of output: 50369 🏁 Script executed: set -euo pipefail
printf '%s\n' '--- name-property helper ---'
rg -n -C25 'fn closure_get_dynamic_prop|pub.*closure_get_dynamic_prop|closure_get_dynamic_prop\s*\(' crates/perry-runtime/src/closure crates/perry-runtime/src/object --glob '*.rs'
printf '%s\n' '--- native-call entry point ---'
rg -l 'js_native_call_method|arg_handles.*root_nanbox' crates/perry-runtime/src --glob '*.rs'
for f in $(rg -l 'js_native_call_method|arg_handles.*root_nanbox' crates/perry-runtime/src --glob '*.rs' | head -n 10); do
echo "--- $f ---"
rg -n -C18 'js_native_call_method|arg_handles.*root_nanbox|root_nanbox_f64_slice|args_ptr' "$f" | head -n 240
doneRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: set -euo pipefail
printf '%s\n' '--- exact closure helper definition ---'
rg -n 'closure_get_dynamic_prop' crates/perry-runtime/src/closure --glob '*.rs'
printf '%s\n' '--- exact property-get implementation used for closure names ---'
rg -n -C35 'pub.*closure_get_dynamic_prop|unsafe fn closure_get_dynamic_prop|fn closure_get_dynamic_prop' crates/perry-runtime/src/closure --glob '*.rs'
printf '%s\n' '--- native-call argument roots ---'
rg -n 'arg_handles|root_nanbox_f64_slice|RuntimeHandleScope::new' crates/perry-runtime/src/object/native_call_method/mod.rs crates/perry-runtime/src/object/native_call_method.rs 2>/dev/nullRepository: PerryTS/perry Length of output: 11602 🏁 Script executed: set -euo pipefail
sed -n '1288,1330p' crates/perry-runtime/src/object/native_call_method.rs
sed -n '1755,1785p' crates/perry-runtime/src/object/native_call_method.rs
sed -n '1938,2035p' crates/perry-runtime/src/object/native_call_method.rs
sed -n '806,825p' crates/perry-runtime/src/closure/dynamic_props.rsRepository: PerryTS/perry Length of output: 7437 Root bind arguments across the target name lookup
🤖 Prompt for AI AgentsSource: Learnings |
||
| } else { | ||
| f64::from_bits(crate::value::TAG_UNDEFINED) | ||
| }; | ||
| // `name_hint` may itself be a heap string pointer (a real `Get(Target, | ||
| // "name")` result) — root it too, or it would go stale across the | ||
| // partial-args array / bound-closure allocations below, and we'd write a | ||
| // dangling capture slot 3 (exactly the "lazily-derived name retains a | ||
| // stale address" failure mode this fix must avoid). | ||
| let name_h = scope.root_nanbox_f64(name_hint); | ||
|
|
||
| let bound_arg_count = args_len.saturating_sub(1); | ||
|
|
||
| // Build the partial-args array (NaN-boxed values copied as-is). | ||
|
|
@@ -520,17 +628,38 @@ pub unsafe extern "C" fn js_function_bind( | |
| } else { | ||
| std::ptr::null_mut() | ||
| }; | ||
| let args_h = (!bound_args_arr.is_null()).then(|| scope.root_raw_mut_ptr(bound_args_arr)); | ||
|
|
||
| // Allocate the bound closure with 3 capture slots. | ||
| let bound = crate::closure::js_closure_alloc(BOUND_FUNCTION_FUNC_PTR, 3); | ||
| // Allocate the bound closure with 4 capture slots: target, bound this, | ||
| // partial-args array, and the `.name` snapshot above. | ||
| let bound = crate::closure::js_closure_alloc(BOUND_FUNCTION_FUNC_PTR, 4); | ||
| let bound_h = scope.root_raw_mut_ptr(bound as *mut u8); | ||
| let bound = bound_h.get_raw_mut_ptr::<ClosureHeader>(); | ||
| let target_value = target_h.get_nanbox_f64(); | ||
| let bound_this = this_h.get_nanbox_f64(); | ||
| let name_hint = name_h.get_nanbox_f64(); | ||
| let bound_args_arr = args_h | ||
| .as_ref() | ||
| .map(|h| h.get_raw_mut_ptr::<crate::array::ArrayHeader>()) | ||
| .unwrap_or(std::ptr::null_mut()); | ||
| js_closure_set_capture_f64(bound, 0, target_value); | ||
| js_closure_set_capture_f64(bound, 1, bound_this); | ||
| js_closure_set_capture_ptr(bound, 2, bound_args_arr as i64); | ||
| js_closure_set_capture_f64(bound, 3, name_hint); | ||
|
|
||
| // Re-derive the target closure pointer from the (possibly refreshed) | ||
| // `target_value` for the `.length` read below — `target_is_closure`'s | ||
| // classification doesn't change, but the address might have. | ||
| let target_closure = target_is_closure | ||
| .then(|| JSValue::from_bits(target_value.to_bits()).as_pointer::<ClosureHeader>()); | ||
|
|
||
| // Spec `.length` = max(0, ToIntegerOrInfinity(Get(target, "length")) - | ||
| // boundArgs.length). An `Object.defineProperty(fn, "length", {value})` | ||
| // override (own dynamic prop) wins over the registered declared length, | ||
| // and the value may be NaN (→ 0), ±Infinity, or beyond int32. | ||
| // and the value may be NaN (→ 0), ±Infinity, or beyond int32. This read | ||
| // is an own-data-property lookup only (no accessor/getter support), so | ||
| // unlike `.name` above it cannot run arbitrary code and needs no | ||
| // rooting of its own. | ||
| let target_len_f = if let Some(target_closure) = target_closure { | ||
| match crate::closure::closure_get_own_dynamic_prop(target_closure as usize, "length") { | ||
| Some(v) => { | ||
|
|
@@ -569,42 +698,6 @@ pub unsafe extern "C" fn js_function_bind( | |
| ); | ||
| } | ||
|
|
||
| // Spec `.name` = "bound " + targetName, where targetName is `Get(Target, | ||
| // "name")` (the empty string when that is not a String). Read the target's | ||
| // `name` *property* first — it reflects an `Object.defineProperty(fn, | ||
| // "name", …)` override and a previous `.bind()`'s `"bound …"` name (so | ||
| // `f.bind().bind().name` chains to `"bound bound …"`). Fall back to the | ||
| // declared name from the func-ptr registry for plain named functions, which | ||
| // don't materialize a `name` data property. | ||
| let target_name = if let Some(target_closure) = target_closure { | ||
| read_function_name_property(target_closure as usize) | ||
| .or_else(|| crate::builtins::function_name_for_ptr((*target_closure).func_ptr as usize)) | ||
| .unwrap_or_default() | ||
| } else { | ||
| target_class_id | ||
| .and_then(crate::object::class_name_for_id) | ||
| .unwrap_or_default() | ||
| }; | ||
| let bound_name = format!("bound {target_name}"); | ||
| let name_ptr = | ||
| crate::string::js_string_from_bytes(bound_name.as_ptr(), bound_name.len() as u32); | ||
| let name_value = f64::from_bits(JSValue::string_ptr(name_ptr).bits()); | ||
| crate::closure::closure_set_dynamic_prop(bound as usize, "name", name_value); | ||
| // Spec attributes for a function's own `name`/`length`: | ||
| // { writable: false, enumerable: false, configurable: true }. Without | ||
| // these the dynamic-prop `name` slot defaults to enumerable and shows | ||
| // up in for-in / Object.keys (Test262 bind/instance-name*). | ||
| crate::object::set_builtin_property_attrs( | ||
| bound as usize, | ||
| "name".to_string(), | ||
| crate::object::PropertyAttrs::new(false, false, true), | ||
| ); | ||
| crate::object::set_builtin_property_attrs( | ||
| bound as usize, | ||
| "length".to_string(), | ||
| crate::object::PropertyAttrs::new(false, false, true), | ||
| ); | ||
|
|
||
| crate::gc::runtime_write_barrier_root_heap_word(bound as u64); | ||
| f64::from_bits(JSValue::pointer(bound as *mut u8).bits()) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Keep
Object.getOwnPropertyDescriptoron one source line.The fragment is copied directly into GitHub Release notes. Markdown converts this code-span newline to a space, publishing
Object. getOwnPropertyDescriptorinstead of the valid API name.Proposed fix
🤖 Prompt for AI Agents