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
162 changes: 162 additions & 0 deletions Jint.Tests/Runtime/SumOfProductsLaneTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
namespace Jint.Tests.Runtime;

/// <summary>
/// Pins the sum-of-products arithmetic lane (JintBinaryExpression.SumOfProductsLane): assignments
/// whose right-hand side is a ± tree of products over numeric leaves compute on raw doubles and
/// box once, and must match the generic tree evaluation.
/// </summary>
public class SumOfProductsLaneTests
{
[Fact]
public void MatrixKernelMatchesManualComputation()
{
var engine = new Engine();
// the MMulti shape: 4 products over chained dense reads, computed-member store
var result = engine.Evaluate("""
(function () {
var m1 = [[1.5, 2, 3, 4], [5, 6, 7, 8]];
var m2 = [[0.5, 1], [2, 3], [4, 5], [6, 7]];
var m = [[], []];
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
m[i][j] = m1[i][0] * m2[0][j] + m1[i][1] * m2[1][j] + m1[i][2] * m2[2][j] + m1[i][3] * m2[3][j];
}
}
return m.map(r => r.join(',')).join(';');
})()
""").AsString();

// row0: [1.5*0.5+2*2+3*4+4*6, 1.5*1+2*3+3*5+4*7] = [40.75, 50.5]
// row1: [5*0.5+6*2+7*4+8*6, 5*1+6*3+7*5+8*7] = [90.5, 114]
Assert.Equal("40.75,50.5;90.5,114", result);
}

[Fact]
public void MinusTermsAndConstantsAndScalarsWork()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
var v = [2, 3, 4];
var cos = 0.5, sin = 0.25;
var out1 = [];
out1[0] = v[1] * cos - v[2] * sin;
out1[1] = v[1] * sin + v[2] * cos;
out1[2] = v[0] * 2 - v[1] * 3 + v[2] * 0.5;
return out1.join(',');
})()
""").AsString();

Assert.Equal("0.5,2.75,-3", result); // 1.5-1=0.5; 0.75+2=2.75; 4-9+2=-3
}

[Fact]
public void SpecialValuesFlowThroughIeeeSemantics()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
var a = [NaN, Infinity, -Infinity, 0, 5];
var r = [];
r[0] = a[0] * 2 + a[4] * 1; // NaN
r[1] = a[1] * 2 + a[4] * 1; // Infinity
r[2] = a[1] * 1 + a[2] * 1; // Infinity + -Infinity = NaN
r[3] = a[3] * 1 + a[3] * 1; // 0
return r.map(x => '' + x).join(',');
})()
""").AsString();

Assert.Equal("NaN,Infinity,NaN,0", result);
}

[Fact]
public void IntegerLeavesStayExact()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
var a = [3, 7, 11, 13];
var r = [];
r[0] = a[0] * a[1] + a[2] * a[3]; // 21 + 143 = 164
r[1] = a[3] * 1000000 + a[2] * 1000; // 13011000
return r.join(',') + ':' + (r[0] === 164) + ':' + Number.isInteger(r[1]);
})()
""").AsString();

Assert.Equal("164,13011000:true:true", result);
}

[Fact]
public void NonNumericLeafDeclinesWithSingleEvaluation()
{
var engine = new Engine();
// the array element is a string: the lane's pure probe declines before any effect and the
// generic path performs the spec concatenation exactly once
var result = engine.Evaluate("""
(function () {
var a = [2, 'x'];
var r = [];
r[0] = a[0] * 3 + a[1] * 2; // 6 + NaN ('x'*2) = NaN
r[1] = a[0] * 3 + a[0] * 2; // 10
return r.map(x => '' + x).join(',');
})()
""").AsString();

Assert.Equal("NaN,10", result);
}

[Fact]
public void GetterBaseDeclinesAndFiresExactlyOnce()
{
var engine = new Engine();
// a Proxy-backed "array" can't take the dense fast read: the lane must decline BEFORE
// touching it, so the generic evaluation's trap count stays exactly one per element read
var result = engine.Evaluate("""
(function () {
var reads = 0;
var target = [4, 5];
var p = new Proxy(target, { get(t, k, r) { if (k === '0' || k === '1') { reads++; } return Reflect.get(t, k, r); } });
var sink = [];
sink[0] = p[0] * 2 + p[1] * 3; // 8 + 15 = 23
return sink[0] + ':' + reads;
})()
""").AsString();

Assert.Equal("23:2", result);
}

[Fact]
public void HoleyAndOutOfRangeReadsDecline()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
var a = [1, , 3]; // hole at 1 → undefined → NaN through multiplication
var r = [];
r[0] = a[0] * 2 + a[1] * 3;
r[1] = a[0] * 2 + a[5] * 3; // out of range → undefined
return r.map(x => '' + x).join(',');
})()
""").AsString();

Assert.Equal("NaN,NaN", result);
}

[Fact]
public void NegativeZeroProductsFollowSpec()
{
var engine = new Engine();
// 0 * -1 is -0 per spec; summing two negative zeros keeps -0. The raw-double lane follows
// the spec exactly (Number::multiply is double multiplication).
var result = engine.Evaluate("""
(function () {
var a = [0, -1];
var r = [];
r[0] = a[0] * a[1] + a[0] * a[1];
return Object.is(r[0], -0) + ':' + Object.is(a[0] * a[1] + a[0] * 0, 0);
})()
""").AsString();

Assert.Equal("true:true", result);
}
}
20 changes: 20 additions & 0 deletions Jint/Runtime/Environments/DeclarativeEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,26 @@ internal bool TryGetNumberSlotForRead(int slotIndex, out double value)
return false;
}

/// <summary>
/// Pure slot read returning the binding's value object for the arithmetic-lane leaves
/// (an unboxed number materializes with the standard write-back). Returns false for
/// out-of-range/stale indices and uninitialized (TDZ) bindings so the caller declines
/// to the generic path before any side effect.
/// </summary>
internal bool TryGetSlotValueForRead(int slotIndex, [NotNullWhen(true)] out JsValue? value)
{
var slots = _slots;
if (slots is null || (uint) slotIndex >= (uint) slots.Length)
{
value = null;
return false;
}

ref var binding = ref slots[slotIndex];
value = binding.HasReferenceValue ? binding.Value : MaterializeUnboxedOrNull(ref binding);
return value is not null;
}

/// <summary>
/// Stores a raw number into a slot previously validated by <see cref="TryGetNumberSlot"/>
/// without materializing a JsNumber.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,10 @@ internal sealed class SimpleAssignmentExpression : JintExpression
// only routes this shape through EvaluateAndDiscard; every other assignment keeps its exact path.
private readonly bool _structurallyNumeric;

// Sum-of-products right-hand sides (the 3d-cube matrix kernels) evaluate on raw doubles
// and box once instead of per binary node; see JintBinaryExpression.SumOfProductsLane.
private JintBinaryExpression.SumOfProductsLane? _rightSumOfProducts;

public SimpleAssignmentExpression(AssignmentExpression expression) : base(expression)
{
_structurallyNumeric = IsNumericAssignmentShape(expression);
Expand Down Expand Up @@ -657,6 +661,7 @@ private void Initialize()
_right = Build(assignmentExpression.Right);

TryEnableNumericFastPath(assignmentExpression);
_rightSumOfProducts = JintBinaryExpression.SumOfProductsLane.TryBuild(_right);
}

private void TryEnableNumericFastPath(AssignmentExpression assignmentExpression)
Expand Down Expand Up @@ -916,7 +921,10 @@ private JsValue SetValue(EvaluationContext context)

lref.AssertValid(engine.Realm);

var rval = _right.GetValue(context);
// sum-of-products right-hand sides compute on raw doubles and box once
var rval = _rightSumOfProducts is not null && _rightSumOfProducts.TryEvaluate(context, out var unboxedProductSum)
? JsNumber.Create(unboxedProductSum)
: _right.GetValue(context);

// If generator suspended or return requested during right-hand side evaluation, don't assign
if (context.IsGeneratorAborted())
Expand Down
Loading