Skip to content

fix: expand spread args to Math.max/min/hypot (#951)#957

Merged
nickna merged 1 commit into
mainfrom
wrk/issue-951-math-spread
Jun 26, 2026
Merged

fix: expand spread args to Math.max/min/hypot (#951)#957
nickna merged 1 commit into
mainfrom
wrk/issue-951-math-spread

Conversation

@nickna

@nickna nickna commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Closes #951.

Problem

In compiled mode, Math.max(...arr) / Math.min(...arr) / Math.hypot(...arr) returned NaN. The variadic Math emitters iterated arguments and emitted each via EmitExpression, but a spread argument ...arr is an Expr.Spread whose emit just yields the inner array object — so ToNumber(array)NaN.

While fixing it, differential-vs-Node testing showed the bug was broader than the issue described (the "compiled-only; interpreter unaffected" note was wrong): the interpreter also failed, and there were two adjacent Math.* spec bugs.

Changes

Compiled (the reported bug)

  • MathStaticEmittermin/max/hypot detect spread args and route to the variadic object[] adapters with a spread-expanded argument array. The hypot guard runs before the general ToNumber loop so it doesn't corrupt the stack.
  • ExpressionEmitterBase.CallHelpers — extracted the existing f(...arr) arg-array-with-spread logic into a reusable EmitArgsArrayWithSpread (exposed on IEmitterContext), keeping the working generic-call path as the single source of truth.
  • RuntimeEmitter.MathAdapters — added the ECMA-262 21.3.2.16 Infinity-first check to the hypot adapter so value-form (const h = Math.hypot) and spread paths agree.

Interpreter (also affected)

  • Interpreter.Calls — the built-in static call path (CallBuiltInSync + async CallBuiltInWithPooledArgs) never expanded spreads, leaking the array through as a single Object-kind arg so AsNumber() threw. Both now expand spreads into a flat arg list before dispatch.
  • MathBuiltIns — pre-existing bugs surfaced by testing: Math.max/min(1, NaN, 3) returned 1/3 (now NaN); Math.hypot(NaN, Infinity) returned NaN (now Infinity).

Tests

  • MathSpreadArgumentTests — basic / mixed / empty / boxed any[] / NaN-Infinity / async spreads, run in both interpreter and compiled modes (interpreter as the Node-matching oracle).

Verification

  • The issue repro + edge cases produce Node-identical output in both modes.
  • Full suite: 14338 passing. The only failures are the 2 in-sln Test262 baseline tests (untracked local scaffolding with a stale baseline — fails identically on clean main) and one load-flaky unit test (differs each run, passes in isolation; the known .NET 10 tier-0 QuickJit issue).

Compiled `Math.max(...arr)` / `min` / `hypot` returned NaN: the variadic
Math emitters emitted each arg via EmitExpression, but a spread `...arr`
is an Expr.Spread whose emit yields the array object, so ToNumber(array)
→ NaN. Route spread calls through the variadic object[] adapters with a
spread-expanded args array (reusing the f(...arr) expansion path,
extracted as EmitArgsArrayWithSpread on IEmitterContext).

The interpreter was also affected (contrary to the issue text): the
built-in static call path (CallBuiltInSync / CallBuiltInWithPooledArgs)
never expanded spreads, leaking the array as a single Object-kind arg so
AsNumber() threw. Both paths now expand spreads before dispatch.

Also fixes two adjacent interpreter spec bugs surfaced by differential
testing: Math.max/min ignored NaN args (now propagate NaN), and
Math.hypot lacked the ECMA-262 Infinity-before-NaN check (now returns
Infinity). The compiled hypot adapter gains the same Infinity-first
check so value-form and spread paths agree.

Tests: MathSpreadArgumentTests covers basic/mixed/empty/boxed-array/
NaN-Infinity/async spreads across interpreter and compiled modes.
@nickna nickna merged commit 1447f95 into main Jun 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

compiled: Math.max/min/hypot don't support spread args (Math.max(...arr) → NaN)

1 participant