Read a spread's elements before an earlier argument converts

When the Math functions evaluate their arguments into temporaries before
converting (an earlier argument whose ToNumber can run valueOf), a spread
argument's array is now copied where it is evaluated. Before, the
temporary held the array itself and its elements were read after the
conversions, so a valueOf that changed the array changed the values
passed, while JavaScript reads a spread's elements during argument
evaluation.
This commit is contained in:
Chris Tate
2026-10-21 16:34:37 -05:00
parent 349ab75298
commit bbb37ae03b
2 changed files with 44 additions and 6 deletions
@@ -1429,9 +1429,12 @@ export function lowerToNumberArgument(lowerer: Lowerer, node: ts.Expression): Ir
* any. When an argument's conversion can run program code (valueOf or
* toString on a dynamic value) and a later argument follows, the arguments
* evaluate into temporaries first and convert in order after. `lowerOther`
* lowers an argument that takes no conversion (a spread), or returns null
* for a ToNumber argument. Returns the temporaries' statements (empty when
* none are needed) and the converted values. */
* lowers an argument that takes no conversion (a spread's array), or returns
* null for a ToNumber argument; with temporaries, such an array is copied
* where it is evaluated, since JavaScript reads a spread's elements then,
* before a later conversion's valueOf could change them. Returns the
* temporaries' statements (empty when none are needed) and the converted
* values. */
export function lowerToNumberArguments(
lowerer: Lowerer,
nodes: readonly ts.Expression[],
@@ -1439,7 +1442,9 @@ export function lowerToNumberArguments(
): { stmts: IrStmt[]; values: IrExpr[] } {
const operands = nodes.map((node): NumberOperand => {
const other = lowerOther?.(node);
return other ? { value: other, convert: null, runsCode: false } : numberOperand(lowerer, node);
return other
? { value: other, convert: null, runsCode: false, spread: true }
: numberOperand(lowerer, node);
});
const late = operands.slice(0, -1).some((operand) => operand.runsCode);
if (!late) {
@@ -1451,7 +1456,18 @@ export function lowerToNumberArguments(
if (operand.value.kind === "numLit") return operand.value;
const temp = lowerer.declareHiddenLocal("%numberArgument", operand.value.type);
const loc = operand.value.loc;
stmts.push({ kind: "varDecl", localId: temp.id, init: operand.value, loc });
const init: IrExpr =
operand.spread && operand.value.type.kind === "array"
? {
kind: "arrIntrinsic",
method: "slice",
receiver: operand.value,
args: [numLit(0, loc), numLit(Infinity, loc)],
type: operand.value.type,
loc,
}
: operand.value;
stmts.push({ kind: "varDecl", localId: temp.id, init, loc });
const read = varRef(temp.id, operand.value.type, loc);
return operand.convert ? operand.convert(read) : read;
});
@@ -1460,11 +1476,13 @@ export function lowerToNumberArguments(
/** An argument a ToNumber site lowers: its evaluated value and the
* conversion that makes it a number (null when the value already is one).
* `runsCode`: the conversion can call program code. */
* `runsCode`: the conversion can call program code. `spread`: the value is
* a spread's array (lowerToNumberArguments' `lowerOther`). */
interface NumberOperand {
value: IrExpr;
convert: ((value: IrExpr) => IrExpr) | null;
runsCode: boolean;
spread?: boolean;
}
function numberOperand(lowerer: Lowerer, node: ts.Expression): NumberOperand {
+20
View File
@@ -75,3 +75,23 @@ try {
} catch (e) {
console.log(e.message, order);
}
// A spread reads its elements where it is evaluated, before an earlier
// argument's valueOf runs.
const values = [1, 2];
const mutator = {
valueOf() {
values[0] = 100;
values.push(50);
return 3;
},
};
console.log(Math.max(mutator, ...values), values.join(","));
const more = [4];
const resetter = {
valueOf() {
more[0] = -1;
return 0;
},
};
console.log(Math.hypot(resetter, ...more, 3), more.join(","));