Repository navigation
Calls with more than 16 arguments: closure-value calls fail to compile ("closure call with 18 args (max 16)"), dynamic calls get 0 for params 17+ #10420
Copy link
Copy link
Closed
Labels
package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindingsFound by the 2026 package audit: compiling real npm packages from source instead of native bindings
Description
Found by the package audit (compiling real npm packages from source instead of Perry's native bindings) on
Perry e6dcb62 (v0.5.1587), Linux x64. Calling a function stored in a variable with more than 16 arguments is a hard
codegen error. Calling a function that declares more than 16 parameters through apply/call/spread, or as an
object-literal method, compiles but passes 0 for every parameter after the 16th.
Reproduction
main.js (same result as .ts):
// qs's shape: a recursive named function expression bound to a var
var f = function f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18) {
return a1 > 0 ? f(a1 - 1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18) : a18;
};
console.log(f(3, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, "ok18"));dynamic.js (compiles, wrong values):
function p18(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18) {
return [a16, a17, a18].join(",");
}
const g = p18;
const args = Array.from({ length: 18 }, (_, i) => i + 1);
console.log("apply ", g.apply(null, args));
console.log("call ", g.call(null, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18));
console.log("spread", g(...args));
const o = { m(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18) { return [a16, a17, a18].join(","); } };
console.log("objlit", o.m(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18));node main.js; node dynamic.js
perry compile main.js -o main
perry compile dynamic.js -o dynamic && ./dynamicExpected (Node 26.5.1)
ok18
apply 16,17,18
call 16,17,18
spread 16,17,18
objlit 16,17,18
Actual (Perry)
main.js fails to compile (same with and without PERRY_NO_AUTO_OPTIMIZE=1):
Error compiling module 'main.js' (...) with --backend llvm: lowering entry of module 'main.js': lowering init statements of module 'main.js': perry-codegen Phase D.1: closure call with 18 args (max 16)
dynamic.js (same with and without PERRY_NO_AUTO_OPTIMIZE=1):
apply 16,0,0
call 16,0,0
spread 16,0,0
objlit 16,0,0
Impact
- qs 6.15.3:
lib/stringify.js:65declaresvar stringify = function stringify(object, prefix, ..., sideChannel)with
18 parameters and calls itself recursively with 18 arguments (lib/stringify.js:183). No program that importsqs
compiles. Packages that use qs for query strings are likely affected too (inferred, not tested), for example
express'sextendedquery parser, body-parserurlencoded({ extended: true })and superagent. - Other code with a 17+ argument call through a function value (generated code, curried helpers, minified output where
parameters became callee locals) fails the same way. The silent-zero case can corrupt data without any error.
Notes
Results (.js, 18 args unless noted; compile errors are the same in auto mode):
| call shape | Perry |
|---|---|
function f(...18) declaration, direct call (also recursive) |
OK |
imported export function f(...18), direct call |
OK |
class method new C().m(...18), class constructor new C(...18) |
OK |
function passed as a parameter and called fn(...18) inside |
OK |
const f = (...18) => ...; f(...18) |
compile error |
| same with 17 params/args | compile error (closure call with 17 args (max 16)) |
var f = function f(...18) {...} (qs) |
compile error |
import { f } where export var f = function f(...18) {...} |
compile error |
const f = (...xs) => xs.length; f(...18 args) (rest param) |
compile error |
g.apply(null, arr18), g.call(null, ...18), g(...arr18) where g has 17 or 18 declared params |
runs; params 17+ are 0 |
g.apply(null, arr18) where g has 18 declared params and returns arguments.length + ":" + a18 |
returns undefined (Node: 18:18) |
object-literal method o.m(...18) with 18 declared params |
runs; params 17+ are 0 |
g.apply/spread where g declares no params and reads arguments (up to 20 args tested) |
OK |
- Compile error sites (verified): three
bail!s with the fixed ceiling 16, matching thejs_closure_call0..16runtime
entry points:crates/perry-codegen/src/lower_call/early_branches.rs:449-452(local closure value)crates/perry-codegen/src/lower_call/extern_func.rs:1869-1873(imported var holding a closure)crates/perry-codegen/src/lower_call/early_branches.rs:354-357(CurrentStepClosure)
- Runtime side (inferred): the value-call dispatcher already falls back to
js_closure_call_arrayfor more than 16 args (crates/perry-runtime/src/closure/dispatch/value_call.rs:398-406).
dispatch_with_arityhas explicit arms up to 32 params (crates/perry-runtime/src/closure/registry.rs:1309). So the codegen
ceiling looks like a leftover of the fixedjs_closure_callNfamily, not a hard runtime limit. The zeros in params
17+ suggest the array path forwards only 16 values into a closure body whose native signature has more parameters.
This was not traced. - Related: closed perry-codegen Phase D.1: closure-call arity ceiling at 5 args blocks Effect #314 raised the closure-call ceiling from 5 to 16 for Effect.
- Unrelated, not covered here:
Reflect.apply(fn, null, args)passes only the first 4 elements ofargs.
Reflect.apply(Math.max, null, [1,2,3,4,5,6])gives4, and Node gives6.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindingsFound by the 2026 package audit: compiling real npm packages from source instead of native bindings
Activity
proggeramlug commentedon Sep 17, 2026
Fixed on
mainby PR #10532, landed in merge train #10559 (v0.5.1592) — the train closes its source PRs instead of merging them, so theFixes #10420keyword in #10532 never evaluated. Changelog fragmentchangelog.d/10532-*.mdis onmain.