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;