Skip to content

Commit dfddac0

Browse files
committed
[JSC] DFG should preserve ToNumber side-effect for Math.pow with one argument
https://bugs.webkit.org/show_bug.cgi?id=309541 Reviewed by Justin Michaud. When Math.pow(x) is called with a single argument, DFG ByteCodeParser unconditionally folds it to a NaN constant and drops the argument entirely. However, the runtime implementation (mathProtoFuncPow) always calls argument(0).toNumber(), which has observable side effects: valueOf/Symbol.toPrimitive invocations, or throwing TypeError for Symbol/BigInt. This causes tier-inconsistent behavior: before DFG tier-up, valueOf is invoked on every call; after tier-up, the side effect silently disappears. Fix by splitting the < 3 argument check: for the 1-argument case, emit a Phantom with NumberUse edge on the argument before returning NaN. This forces OSR exit when a non-number is passed, falling back to baseline where ToNumber runs correctly. This mirrors the existing pattern in handleMinMax for Math.max(x)/Math.min(x). Test: JSTests/stress/math-pow-one-argument-to-number.js * JSTests/stress/math-pow-one-argument-to-number.js: Added. (shouldBe): (let.obj.valueOf): (test): (testThrow): (let.throwObj.valueOf): (catch): * Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp: (JSC::DFG::ByteCodeParser::handleIntrinsicCall): Canonical link: https://commits.webkit.org/308954@main
1 parent 8b67517 commit dfddac0

2 files changed

Lines changed: 47 additions & 2 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
function shouldBe(actual, expected) {
2+
if (actual !== expected)
3+
throw new Error(`expected ${expected} but got ${actual}`);
4+
}
5+
6+
let counter = 0;
7+
let obj = { valueOf() { counter++; return 1; } };
8+
9+
function test(x) {
10+
return Math.pow(x);
11+
}
12+
noInline(test);
13+
14+
for (let i = 0; i < testLoopCount; i++)
15+
shouldBe(Number.isNaN(test(obj)), true);
16+
17+
shouldBe(counter, testLoopCount);
18+
19+
for (let i = 0; i < testLoopCount; i++)
20+
shouldBe(Number.isNaN(test(42)), true);
21+
22+
function testThrow(x) {
23+
return Math.pow(x);
24+
}
25+
noInline(testThrow);
26+
27+
for (let i = 0; i < testLoopCount; i++)
28+
shouldBe(Number.isNaN(testThrow(1.5)), true);
29+
30+
let throwObj = { valueOf() { throw new Error("ok"); } };
31+
let caught = false;
32+
try {
33+
testThrow(throwObj);
34+
} catch (e) {
35+
caught = true;
36+
shouldBe(e.message, "ok");
37+
}
38+
shouldBe(caught, true);

Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2630,9 +2630,16 @@ auto ByteCodeParser::handleIntrinsicCall(Node* callee, Operand resultOperand, Ca
26302630
}
26312631

26322632
case PowIntrinsic: {
2633-
if (argumentCountIncludingThis < 3) {
2634-
// Math.pow() and Math.pow(x) return NaN.
2633+
if (argumentCountIncludingThis == 1) {
2634+
// Math.pow() returns NaN.
2635+
insertChecks();
2636+
setResult(addToGraph(JSConstant, OpInfo(m_constantNaN)));
2637+
return CallOptimizationResult::Inlined;
2638+
}
2639+
if (argumentCountIncludingThis == 2) {
2640+
// Math.pow(x) returns NaN, but ToNumber(x) may have side effects.
26352641
insertChecks();
2642+
addToGraph(Phantom, Edge(get(virtualRegisterForArgumentIncludingThis(1, registerOffset)), NumberUse));
26362643
setResult(addToGraph(JSConstant, OpInfo(m_constantNaN)));
26372644
return CallOptimizationResult::Inlined;
26382645
}

0 commit comments

Comments
 (0)