From ba9844db2e217afb11472b155361244e0d815e8d Mon Sep 17 00:00:00 2001 From: Andrew DiZenzo Date: Wed, 3 Jun 2026 16:22:00 +0000 Subject: [PATCH] Align function prototype value reads --- crates/perry-codegen/src/expr/property_get.rs | 26 ++++++--------- .../src/runtime_decls/strings.rs | 1 + .../src/object/class_registry.rs | 31 +++++++++++++++++ .../object/function-prototype-value.ts | 33 +++++++++++++++++++ 4 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 test-parity/node-suite/object/function-prototype-value.ts diff --git a/crates/perry-codegen/src/expr/property_get.rs b/crates/perry-codegen/src/expr/property_get.rs index 6ca0670145..4f970110f3 100644 --- a/crates/perry-codegen/src/expr/property_get.rs +++ b/crates/perry-codegen/src/expr/property_get.rs @@ -452,23 +452,17 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let i32_v = blk.call(I32, "js_url_search_params_size", &[(I64, &recv_handle)]); Ok(blk.sitofp(I32, &i32_v, DOUBLE)) } - - // `arr[i] = v` — typed-Number array element write. - // - // INLINE FAST PATH: - // - // load length from arr_ptr+0 - // if idx < length: inline store, done - // else if idx < capacity: inline store + bump length, done - // else: call js_array_set_f64_extend (slow realloc path) - // - // The ArrayHeader layout is `{ length: u32, capacity: u32, ... }` - // (8 bytes), followed by `[f64; N]` elements at offset 8. - // - // For non-LocalGet receivers we still use bounds-checked - // `js_array_set_f64` (no return value, no realloc) since there's - // no local to write a possibly-realloc'd pointer back to. Expr::PropertyGet { object, property } => { + if property == "prototype" + && matches!(object.as_ref(), Expr::FuncRef(_) | Expr::Closure { .. }) + { + let func_value = lower_expr(ctx, object)?; + return Ok(ctx.block().call( + DOUBLE, + "js_function_prototype_value_for_read", + &[(DOUBLE, &func_value)], + )); + } if let Some((builtin_name, method_name)) = builtin_prototype_method_read(object, property) { diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index 6b2addbe50..7337e739cc 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -1101,6 +1101,7 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { DOUBLE, &[DOUBLE, PTR, I64], ); + module.declare_function("js_function_prototype_value_for_read", DOUBLE, &[DOUBLE]); module.declare_function("js_typeerror_new", I64, &[I64]); module.declare_function("js_rangeerror_new", I64, &[I64]); module.declare_function("js_syntaxerror_new", I64, &[I64]); diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index bea798421c..cdc6661870 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -1906,6 +1906,37 @@ pub(crate) fn ordinary_function_prototype_value_for_read(func_value: f64) -> Opt Some(crate::value::js_nanbox_pointer(proto as i64)) } +#[no_mangle] +pub extern "C" fn js_function_prototype_value_for_read(func_value: f64) -> f64 { + let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + let jv = crate::value::JSValue::from_bits(func_value.to_bits()); + if !jv.is_pointer() { + return undef; + } + let ptr = jv.as_pointer() as *const crate::closure::ClosureHeader; + if ptr.is_null() || !is_valid_obj_ptr(ptr as *const u8) { + return undef; + } + unsafe { + if (*ptr).type_tag != crate::closure::CLOSURE_MAGIC { + return undef; + } + } + + let closure_addr = ptr as usize; + if crate::closure::closure_is_key_deleted(closure_addr, "prototype") { + return undef; + } + let dynamic = crate::closure::closure_get_dynamic_prop(closure_addr, "prototype"); + if dynamic.to_bits() != crate::value::TAG_UNDEFINED { + return dynamic; + } + if let Some(proto) = generator_function_prototype_of(closure_addr) { + return proto; + } + ordinary_function_prototype_value_for_read(func_value).unwrap_or(undef) +} + /// Lookup helper: returns the registered prototype-method value for /// `(class_id, name)`, or None if no assignment matched. Walks the /// parent-class chain so methods registered on a base class are found diff --git a/test-parity/node-suite/object/function-prototype-value.ts b/test-parity/node-suite/object/function-prototype-value.ts new file mode 100644 index 0000000000..1705a54946 --- /dev/null +++ b/test-parity/node-suite/object/function-prototype-value.ts @@ -0,0 +1,33 @@ +function show(label: string, value: unknown) { + console.log(label, JSON.stringify(value)); +} + +function Foo() {} + +const fooProto: any = (Foo as any).prototype; +show("function prototype object", [ + typeof fooProto, + fooProto !== null, + fooProto.constructor === Foo, + (Foo as any).prototype === fooProto, +]); + +const child: any = Object.create((Foo as any).prototype); +show("object create accepts function prototype", [ + typeof child, + child !== null, +]); + +function Bar() {} +(Bar as any).prototype = Object.create((Foo as any).prototype); +(Bar as any).prototype.constructor = Bar; + +const barProto: any = (Bar as any).prototype; +show("assigned function prototype object", [ + typeof barProto, + barProto !== null, + (Bar as any).prototype === barProto, +]); + +const arrow = () => {}; +show("arrow prototype missing", (arrow as any).prototype === undefined);