diff --git a/Jint.Tests/Runtime/NumberTests.cs b/Jint.Tests/Runtime/NumberTests.cs index 5b6dea5c7..f677ad63b 100644 --- a/Jint.Tests/Runtime/NumberTests.cs +++ b/Jint.Tests/Runtime/NumberTests.cs @@ -250,11 +250,20 @@ public void IntegerMultiplicationPreservesNegativeZero() r.push(Object.is(acc2, -0)); var acc3 = 0; acc3 *= 5; r.push(Object.is(acc3, 0)); + // discard-mode numeric-assignment shape (x = a * b as a statement, LHS already + // a number so the unboxed-slot store engages) and a first-assignment variant + // that exercises the value-producing binary lane instead + var prod = 1; prod = zero * negFive; + r.push(Object.is(prod, -0)); + var prod2 = 1; prod2 = negFive * zero; + r.push(Object.is(prod2, -0)); + var prod3; prod3 = zero * five; + r.push(Object.is(prod3, 0)); return r.join(','); })() """).AsString(); - Assert.Equal("true,true,true,true,true,true,true,true,true,true,true", result); + Assert.Equal("true,true,true,true,true,true,true,true,true,true,true,true,true,true", result); } // The following tests guard the primitive-receiver method resolution that skips allocating a diff --git a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs index 449e2ea55..d7b6c07d9 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs @@ -633,14 +633,14 @@ private static bool IsNumericAssignmentShape(AssignmentExpression expression) return false; } - // Multiplication is intentionally excluded: the boxed binary path computes 0 * negative as - // integers and yields +0, whereas raw-double multiplication yields the spec-correct -0, so a - // double-based fast path would change observable behaviour. Addition/subtraction/division/ - // remainder produce identical results on both paths, keeping this a pure optimization. + // All five operators produce identical results on the raw-double and boxed paths: + // IEEE arithmetic is the spec algorithm, the boxed integer-multiply arm routes zero + // products through double math to preserve -0, and an unboxed slot stores -0 exactly. switch (binary.Operator) { case Operator.Addition: case Operator.Subtraction: + case Operator.Multiplication: case Operator.Division: case Operator.Remainder: return IsIdentifierOrNumericLiteral(binary.Left) && IsIdentifierOrNumericLiteral(binary.Right); @@ -681,6 +681,7 @@ private void TryEnableNumericFastPath(AssignmentExpression assignmentExpression) { case Operator.Addition: case Operator.Subtraction: + case Operator.Multiplication: case Operator.Division: case Operator.Remainder: break; @@ -876,6 +877,11 @@ private bool TryAssignNumeric(EvaluationContext context) case Operator.Subtraction: result = left - right; break; + case Operator.Multiplication: + // IEEE 754 multiplication is Number::multiply, including -0 for zero products + // of opposite signs; the unboxed slot stores -0 exactly + result = left * right; + break; case Operator.Division: // IEEE 754 division matches the ECMAScript algorithm for all special cases result = left / right; diff --git a/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs index 85748e4a6..c3dc0797f 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs @@ -268,6 +268,137 @@ public bool TryGetInt32Operands(EvaluationContext context, out int left, out int } } + /// + /// Unboxed operand lane for the arithmetic operators (+ - * /) over identifier and + /// numeric-constant leaf shapes (`a * b`, `x - 1`, `2 * x`). Both operands are read as raw + /// doubles through the slot/global caches — pure reads, so a decline (non-number binding, + /// TDZ, slot miss) falls into the generic path which replays left-then-right evaluation with + /// no observable double effect. For two runtime-proven Numbers the spec operator degenerates + /// to the Number:: op, which IEEE double arithmetic implements exactly (including the + /// sign-of-zero rules), and JsNumber.Create normalizes integral results to the same + /// Integer-typed instances the boxed arms produce, so downstream integer fast paths stay + /// armed. Embedded directly as a struct in Minus/Times/Divide (almost always numeric — + /// same precedent as the comparison classes' embedded lane); Plus wraps it in + /// because string-concat Plus nodes are numerous and + /// should pay one reference field, not an embedded slot-cache pair, for a lane they never + /// arm. The embedded form matters on hit-heavy loops: a separate lane object costs an extra + /// dereference (cache line) per evaluation. + /// + private protected struct NumericOperandLane + { + private JintIdentifierExpression? _leftIdentifier; // null => _leftConstant + private JintIdentifierExpression? _rightIdentifier; // null => _rightConstant + private double _leftConstant; + private double _rightConstant; + private bool _armed; + private SlotLocationCache _leftSlotCache; + private SlotLocationCache _rightSlotCache; + + public readonly bool IsArmed => _armed; + + public void Initialize(JintExpression left, JintExpression right) + { + var leftIdentifier = left as JintIdentifierExpression; + var rightIdentifier = right as JintIdentifierExpression; + + // constant-op-constant is folded before these nodes are built; requiring at least one + // identifier keeps the lane off shapes other machinery owns + if (leftIdentifier is null && rightIdentifier is null) + { + return; + } + + double leftConstant = 0; + if (leftIdentifier is null) + { + if (left is not JintConstantExpression { Value: JsNumber leftNumber }) + { + return; + } + leftConstant = leftNumber._value; + } + + double rightConstant = 0; + if (rightIdentifier is null) + { + if (right is not JintConstantExpression { Value: JsNumber rightNumber }) + { + return; + } + rightConstant = rightNumber._value; + } + + _leftIdentifier = leftIdentifier; + _leftConstant = leftConstant; + _rightIdentifier = rightIdentifier; + _rightConstant = rightConstant; + _armed = true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetOperands(EvaluationContext context, out double left, out double right) + { + left = _leftConstant; + right = _rightConstant; + + if (!_armed || context.OperatorOverloadingAllowed) + { + return false; + } + + var engine = context.Engine; + if (engine.ExecutionContext.Suspendable is not null) + { + // generator/async resume replays a saved left operand through the suspend + // protocol; live slot re-reads could observe a different value + return false; + } + + var env = engine.ExecutionContext.LexicalEnvironment; + + var leftIdentifier = _leftIdentifier; + if (leftIdentifier is not null + && (!_leftSlotCache.TryResolve(engine, env, leftIdentifier.Identifier, out var leftEnvironment, out var leftSlotIndex) + || !leftEnvironment.TryGetNumberSlotForRead(leftSlotIndex, out left))) + { + if (leftIdentifier._cachedGlobalEnv is null || !TryReadGlobalNumber(leftIdentifier, engine, env, out left)) + { + return false; + } + } + + var rightIdentifier = _rightIdentifier; + if (rightIdentifier is not null + && (!_rightSlotCache.TryResolve(engine, env, rightIdentifier.Identifier, out var rightEnvironment, out var rightSlotIndex) + || !rightEnvironment.TryGetNumberSlotForRead(rightSlotIndex, out right))) + { + if (rightIdentifier._cachedGlobalEnv is null || !TryReadGlobalNumber(rightIdentifier, engine, env, out right)) + { + return false; + } + } + + return true; + } + } + + /// + /// Heap-allocated for PlusBinaryExpression: allocated only + /// when the operand shape matches at build time, so the many string-concat Plus nodes carry + /// a single null reference field instead of the embedded lane struct. + /// + private protected sealed class BoxedNumericOperandLane + { + public NumericOperandLane Lane; + + public static BoxedNumericOperandLane? TryBuild(JintExpression left, JintExpression right) + { + var lane = new NumericOperandLane(); + lane.Initialize(left, right); + return lane.IsArmed ? new BoxedNumericOperandLane { Lane = lane } : null; + } + } + /// /// Whole-tree lane for sum-of-products arithmetic over pure-readable numeric leaves — /// `M1[i][0]*M2[0][j] + M1[i][1]*M2[1][j] + …`, the dromaeo-3d-cube MMulti/VMulti kernel shape, @@ -1201,12 +1332,20 @@ public override bool GetBooleanValue(EvaluationContext context) private sealed class PlusBinaryExpression : JintBinaryExpression { + private readonly BoxedNumericOperandLane? _numericLane; + public PlusBinaryExpression(NonLogicalBinaryExpression expression) : base(expression) { + _numericLane = BoxedNumericOperandLane.TryBuild(_left, _right); } protected override object EvaluateInternal(EvaluationContext context) { + if (_numericLane is not null && _numericLane.Lane.TryGetOperands(context, out var unboxedLeft, out var unboxedRight)) + { + return JsNumber.Create(unboxedLeft + unboxedRight); + } + if (!TryEvaluateOperands(context, out var left, out var right)) { return JsValue.Undefined; @@ -1335,12 +1474,20 @@ protected override object EvaluateInternal(EvaluationContext context) private sealed class MinusBinaryExpression : JintBinaryExpression { + private NumericOperandLane _numericLane; + public MinusBinaryExpression(NonLogicalBinaryExpression expression) : base(expression) { + _numericLane.Initialize(_left, _right); } protected override object EvaluateInternal(EvaluationContext context) { + if (_numericLane.TryGetOperands(context, out var unboxedLeft, out var unboxedRight)) + { + return JsNumber.Create(unboxedLeft - unboxedRight); + } + if (!TryEvaluateOperands(context, out var left, out var right)) { return JsValue.Undefined; @@ -1382,12 +1529,22 @@ protected override object EvaluateInternal(EvaluationContext context) private sealed class TimesBinaryExpression : JintBinaryExpression { + private NumericOperandLane _numericLane; + public TimesBinaryExpression(NonLogicalBinaryExpression expression) : base(expression) { + _numericLane.Initialize(_left, _right); } protected override object EvaluateInternal(EvaluationContext context) { + if (_numericLane.TryGetOperands(context, out var unboxedLeft, out var unboxedRight)) + { + // raw-double multiply is Number::multiply bit for bit, including -0 for zero + // products of opposite signs + return JsNumber.Create(unboxedLeft * unboxedRight); + } + if (!TryEvaluateOperands(context, out var left, out var right)) { return JsValue.Undefined; @@ -1434,12 +1591,20 @@ protected override object EvaluateInternal(EvaluationContext context) private sealed class DivideBinaryExpression : JintBinaryExpression { + private NumericOperandLane _numericLane; + public DivideBinaryExpression(NonLogicalBinaryExpression expression) : base(expression) { + _numericLane.Initialize(_left, _right); } protected override object EvaluateInternal(EvaluationContext context) { + if (_numericLane.TryGetOperands(context, out var unboxedLeft, out var unboxedRight)) + { + return JsNumber.Create(unboxedLeft / unboxedRight); + } + if (!TryEvaluateOperands(context, out var left, out var right)) { return JsValue.Undefined;