Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Jint.Tests/Runtime/NumberTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 10 additions & 4 deletions Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
165 changes: 165 additions & 0 deletions Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,137 @@ public bool TryGetInt32Operands(EvaluationContext context, out int left, out int
}
}

/// <summary>
/// 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
/// <see cref="BoxedNumericOperandLane"/> 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.
/// </summary>
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;
}
}

/// <summary>
/// Heap-allocated <see cref="NumericOperandLane"/> 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.
/// </summary>
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;
}
}

/// <summary>
/// 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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading