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
34 changes: 34 additions & 0 deletions crates/perry-codegen/src/codegen/string_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,11 @@ pub(super) fn emit_string_pool(
// crash (Next.js `new c.AppPageRouteModule({...})`). Mirrors the
// closure-rest registration but keyed by the `_constructor` symbol.
let mut ctor_rest_regs: Vec<(String, usize)> = Vec::new();
// Per-class-id ctor synth/rest flags (has_synthetic_arguments, has_rest) so
// the `super(...spread)` runtime apply path packs a pass-through parent
// ctor's `arguments` / rest slot correctly (a zero-declared-param parent
// that reads `arguments`, e.g. tsc's emitted pass-through ctor).
let mut ctor_flag_regs: Vec<(u32, bool, bool)> = Vec::new();
for (class_name, class) in classes.iter() {
// Refs #486: skip alias keys (class_table now contains both the
// canonical name and self-binding aliases like `_X` from
Expand Down Expand Up @@ -662,6 +667,20 @@ pub(super) fn emit_string_pool(
{
ctor_rest_regs.push((ctor_symbol.clone(), rest_idx));
}
// Record the ctor's trailing-param shape so the `super(...spread)`
// apply path forwards the flat spread args and packs the trailing slot:
// a synthesized `arguments` slot receives ALL args (from index 0), a
// user rest param only the args from the rest position onward.
{
let last = class.constructor.as_ref().and_then(|c| c.params.last());
let ctor_has_synth = last.map(|p| p.arguments_object.is_some()).unwrap_or(false);
let ctor_has_rest = last
.map(|p| p.is_rest && p.arguments_object.is_none())
.unwrap_or(false);
if ctor_has_synth || ctor_has_rest {
ctor_flag_regs.push((cid, ctor_has_synth, ctor_has_rest));
}
}
ctor_triples.push((cid, ctor_symbol, ctor_params));
}
method_triples.sort_unstable();
Expand Down Expand Up @@ -785,6 +804,21 @@ pub(super) fn emit_string_pool(
&[(PTR, &func_ref), (I32, &rest_idx.to_string())],
);
}
// Register ctor synth/rest flags so `super(...spread)` packs the parent
// ctor's trailing `arguments` / rest slot correctly.
ctor_flag_regs.sort_unstable();
for (cid, has_synth, has_rest) in ctor_flag_regs {
chunker.roll_if_full();
let blk = chunker.current_block();
blk.call_void(
"js_register_class_constructor_flags",
&[
(I64, &cid.to_string()),
(I64, if has_synth { "1" } else { "0" }),
(I64, if has_rest { "1" } else { "0" }),
],
);
}

// Refs #618 / #420: register every class id with the runtime so
// `js_value_typeof` can distinguish a class ref (NaN-boxed INT32 with
Expand Down
16 changes: 12 additions & 4 deletions crates/perry-codegen/src/expr/this_super_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,21 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
);
}
perry_hir::CallArg::Spread(e) => {
// `js_array_push_spread_any` also handles the
// arguments OBJECT (array-like, not ArrayHeader) —
// the `super(...arguments)` source.
// Route every spread operand through the full iterator
// protocol (`js_array_spread_append` -> `array_from_
// spread_value`): it drives a custom `[Symbol.iterator]`
// (`super(...iter)`), spreads the arguments OBJECT
// (`super(...arguments)`), arrays, sets/maps, typed
// arrays, and strings, AND propagates an abrupt
// completion from a throwing iterator step/value — the
// `call-spread-*-iter` / `call-spread-err-*` cases. The
// old `js_array_push_spread_any` only handled arrays and
// array-like (`.length`) objects, so a plain iterable
// (no `.length`) contributed zero args.
let v = lower_expr(ctx, e)?;
arr = ctx.block().call(
I64,
"js_array_push_spread_any",
"js_array_spread_append",
&[(I64, &arr), (DOUBLE, &v)],
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,14 @@ pub(crate) fn declare_core(module: &mut LlModule) {
// #1787: register a class's standalone constructor so `new
// <classObjectValue>()` can replay it on a dynamically-allocated instance.
module.declare_function("js_register_class_constructor", VOID, &[I64, I64, I64]);
// Constructor synth/rest flags: (class_id, has_synthetic_arguments,
// has_rest) — consulted by the `super(...spread)` apply path so it packs a
// pass-through parent ctor's `arguments` / rest slot correctly.
module.declare_function(
"js_register_class_constructor_flags",
VOID,
&[I64, I64, I64],
);
// #1788: register a class STATIC method + dispatch an inherited static
// method on a class value (subclass extends a class-expression value).
module.declare_function(
Expand Down
99 changes: 89 additions & 10 deletions crates/perry-runtime/src/object/class_constructors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,53 @@ fn lookup_class_constructor(class_id: u32) -> Option<(usize, u32)> {
.copied()
}

/// Per-class-id flags for a registered standalone constructor: whether its
/// trailing param is the HIR-synthesized `arguments` slot and/or a user rest
/// param (`constructor(a, ...rest)`). The `arguments` slot must receive ALL
/// call args (packed from index 0) whereas a user rest slot receives only the
/// args from the rest position onward — the same distinction `call_vtable_
/// method` draws via `has_synthetic_arguments` / `has_rest`. Registered by
/// codegen (`js_register_class_constructor_flags`) alongside the ctor itself so
/// the `super(...spread)` apply path (`js_super_construct_apply`) can forward
/// the flat spread args and let `call_vtable_method` pack the trailing slot
/// correctly. Absent entry ⇒ neither flag (a plain fixed-arity ctor).
static CLASS_CONSTRUCTOR_FLAGS: RwLock<Option<HashMap<u32, (bool, bool)>>> = RwLock::new(None);

/// Codegen FFI: record `(has_synthetic_arguments, has_rest)` for a class ctor.
/// See [`CLASS_CONSTRUCTOR_FLAGS`].
#[no_mangle]
pub extern "C" fn js_register_class_constructor_flags(
class_id: i64,
has_synthetic_arguments: i64,
has_rest: i64,
) {
if class_id == 0 {
return;
}
let mut guard = CLASS_CONSTRUCTOR_FLAGS.write().unwrap();
if guard.is_none() {
*guard = Some(HashMap::new());
}
guard.as_mut().unwrap().insert(
class_id as u32,
(has_synthetic_arguments != 0, has_rest != 0),
);
}

/// Keepalive anchor (generated-code-only callee).
#[used]
static KEEP_JS_REGISTER_CLASS_CONSTRUCTOR_FLAGS: extern "C" fn(i64, i64, i64) =
js_register_class_constructor_flags;

/// Look up a class ctor's `(has_synthetic_arguments, has_rest)` flags.
fn lookup_class_constructor_flags(class_id: u32) -> (bool, bool) {
CLASS_CONSTRUCTOR_FLAGS
.read()
.ok()
.and_then(|g| g.as_ref().and_then(|m| m.get(&class_id).copied()))
.unwrap_or((false, false))
}

thread_local! {
/// Decl-site snapshots of a function-nested class DECLARATION's captured
/// outer locals, keyed by class_id. Filled by the codegen-emitted
Expand Down Expand Up @@ -350,25 +397,57 @@ pub unsafe extern "C" fn js_super_construct_apply(
} else {
crate::array::js_array_length(arr)
} as usize;
let mut final_args: Vec<f64> = Vec::with_capacity(total_params as usize);
for i in 0..user_params {
if i < n {
// Flatten every SPREAD-expanded arg into a contiguous f64 buffer and
// let `call_vtable_method` pack the trailing param. When the parent
// ctor uses `arguments` (a zero-declared-param pass-through ctor:
// `super(...[3,4,5])` → parent reads `arguments.length`), the
// synthesized `arguments` slot must hold ALL args (packed from index
// 0); a user rest param (`constructor(a, ...rest)`) holds only the
// args from the rest position onward. The old truncation to
// `user_params` (and `has_synth=false`/`has_rest=false`) dropped the
// extra spread args and left `arguments.length == 0`. `caps` (the
// decl-site capture snapshot for a function-nested class) are
// appended AFTER the user args, mirroring the fixed-arity super path.
// `call_vtable_method`'s synth/rest packing bundles the trailing
// param from the flat arg buffer — but it packs from index 0 for
// synthesized `arguments` and would swallow any trailing capture
// params. Function-nested capturing ctors never combine caps with a
// synth/rest trailing param in practice, so only take the flat-
// forward path when there are no caps; otherwise keep the original
// fixed-arity truncation (caps appended after user args).
let (mut has_synth, mut has_rest_flag) = lookup_class_constructor_flags(cur);
if !caps.is_empty() {
has_synth = false;
has_rest_flag = false;
}
let mut final_args: Vec<f64> = Vec::with_capacity(user_params.max(n) + caps.len());
if has_synth || has_rest_flag {
// Forward all n spread args flat; call_vtable_method packs the
// trailing synthesized-`arguments` / rest slot (from index 0 for
// `arguments`, from the rest position for a user rest param).
for i in 0..n {
final_args.push(crate::array::js_array_get_f64(arr, i as u32));
} else {
final_args.push(undef);
}
}
for bits in &caps {
final_args.push(f64::from_bits(*bits));
} else {
for i in 0..user_params {
if i < n {
final_args.push(crate::array::js_array_get_f64(arr, i as u32));
} else {
final_args.push(undef);
}
}
for bits in &caps {
final_args.push(f64::from_bits(*bits));
}
}
let _ = call_vtable_method(
ctor_ptr,
this_raw,
final_args.as_ptr(),
final_args.len(),
total_params,
false,
false,
has_synth,
has_rest_flag,
);
return undef;
}
Expand Down
Loading