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
16 changes: 16 additions & 0 deletions crates/perry-hir/src/lower/expr_call/static_and_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,22 @@ pub(super) fn try_static_method_and_instance(
// Fall through — let the regular method-call
// dispatch further down handle the user-class
// method.
} else if static_call_has_spread {
// A spread argument (`recv.method(...arr)`) cannot be
// represented in `NativeMethodCall`'s positional `args`:
// the pre-lowered `args` already collapsed the spread
// SOURCE into a single element, and the codegen
// `NA_VARARGS` packer then pushes that array as ONE
// argument instead of flattening it. The static-method
// arms above already guard on `static_call_has_spread`
// for the same reason; the instance arm was missing it.
// Concretely, `AsyncLocalStorage.run(store, cb, ...args)`
// handed the callback a single array instead of its
// forwarded arguments. Fall through to the generic
// `CallSpread` path (function tail `Ok(Err(args))`),
// which materialises + flattens the spread via
// `js_array_concat` before dispatching the native method
// by name.
} else {
// Get the object expression (the instance variable)
let object_expr = lower_expr(ctx, &member.obj)?;
Expand Down
47 changes: 47 additions & 0 deletions test-files/test_gap_als_run_spread_args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// A spread argument forwarded into a native-instance method with a
// `NA_VARARGS` tail (`AsyncLocalStorage.prototype.run(store, cb, ...args)`)
// must be FLATTENED into individual arguments, not passed as a single array.
//
// The HIR instance-method arm produced a `NativeMethodCall` for the spread
// call using pre-lowered positional args (which collapse the spread source
// into one element); the `NA_VARARGS` packer then pushed that array as ONE
// argument. So `als.run(store, cb, ...[1, 2, 3])` handed `cb` a single
// `[1,2,3]` array instead of `1, 2, 3`. The static-method arms already
// guarded on `static_call_has_spread`; the instance arm was missing it, so
// spread instance calls now fall through to the flattening `CallSpread` path.
//
// Next.js wraps the whole app render in
// `workUnitAsyncStorage.run(store, renderFn, ...args)`, so a collapsed spread
// silently corrupted the forwarded arguments.
//
// Validated byte-for-byte against `node --experimental-strip-types`.

import { AsyncLocalStorage } from "async_hooks";

const als = new AsyncLocalStorage<{ s: number }>();
const arr = [1, 2, 3];

// spread of an array literal
console.log(als.run({ s: 1 }, (...a: number[]) => a.length, ...[1, 2, 3]));

// spread of a variable
console.log(als.run({ s: 1 }, (...a: number[]) => a.length, ...arr));

// mixed literal + spread + literal
console.log(als.run({ s: 1 }, (...a: number[]) => a.length, 0, ...arr, 9));

// forwarded args reach the callback in order and with correct values
console.log(als.run({ s: 1 }, (x: number, y: number, z: number) => x + y + z, ...arr));

// nested run(store, fn, ...restParam): the classic Next render shape
const outer = new AsyncLocalStorage<{ o: number }>();
const inner = new AsyncLocalStorage<{ i: number }>();
const nested = outer.run(
{ o: 1 },
(fn: (a: number, b: number) => number, ...args: number[]) =>
inner.run({ i: 2 }, fn, ...args),
(a: number, b: number) => a + b,
100,
200,
);
console.log(nested);
Loading