Skip to content

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

Description

@proggeramlug

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 && ./dynamic

Expected (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:65 declares var stringify = function stringify(object, prefix, ..., sideChannel) with
    18 parameters and calls itself recursively with 18 arguments (lib/stringify.js:183). No program that imports qs
    compiles. Packages that use qs for query strings are likely affected too (inferred, not tested), for example
    express's extended query parser, body-parser urlencoded({ 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 the js_closure_call0..16 runtime
    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_array for more than 16 args (crates/perry-runtime/src/closure/dispatch/value_call.rs:398-406).
    dispatch_with_arity has explicit arms up to 32 params (crates/perry-runtime/src/closure/registry.rs:1309). So the codegen
    ceiling looks like a leftover of the fixed js_closure_callN family, 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 of args.
    Reflect.apply(Math.max, null, [1,2,3,4,5,6]) gives 4, and Node gives 6.

Activity

  1. added
    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindings
    on Sep 17, 2026
  2. proggeramlug commented on Sep 17, 2026

    @proggeramlug
    ContributorAuthor

    Fixed on main by PR #10532, landed in merge train #10559 (v0.5.1592) — the train closes its source PRs instead of merging them, so the Fixes #10420 keyword in #10532 never evaluated. Changelog fragment changelog.d/10532-*.md is on main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindings

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions