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
9 changes: 0 additions & 9 deletions Jint.Tests.Test262/Test262Harness.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,6 @@

// requires investigation how to process complex function name evaluation for property
"built-ins/Function/prototype/toString/method-computed-property-name.js",
"language/expressions/class/elements/class-name-static-initializer-anonymous.js",

// delete/add detection not implemented for map iterator during iteration
"built-ins/Map/prototype/forEach/iterates-values-deleted-then-readded.js",
"built-ins/MapIteratorPrototype/next/iteration-mutable.js",
"built-ins/Set/prototype/forEach/iterates-values-revisits-after-delete-re-add.js",

// 100 fraction digits is not supported due to .NET format specifier limitation
"built-ins/Number/prototype/toFixed/range.js",

// special casing data
"built-ins/**/special_casing*.js",
Expand Down
6 changes: 3 additions & 3 deletions Jint.Tests/Runtime/NumberTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ public void ToFixed(int fractionDigits, string result)
}

[Fact]
public void ToFixedWith100FractionDigitsThrows()
public void ToFixedWith100FractionDigitsWorks()
{
var ex = Assert.Throws<JavaScriptException>(() => _engine.Evaluate($"(3).toFixed(100)"));
Assert.Equal("100 fraction digits is not supported due to .NET format specifier limitation", ex.Message);
var value = _engine.Evaluate("(3).toFixed(100)").AsString();
Assert.Equal("3." + new string('0', 100), value);
}

[Theory]
Expand Down
4 changes: 2 additions & 2 deletions Jint/Native/Function/ClassDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public ClassDefinition(
/// <summary>
/// https://tc39.es/ecma262/#sec-runtime-semantics-classdefinitionevaluation
/// </summary>
public JsValue BuildConstructor(EvaluationContext context, Environment env)
public JsValue BuildConstructor(EvaluationContext context, Environment env, string? classBinding = null)
{
// A class definition is always strict mode code.
using var _ = new StrictModeScope(true, true);
Expand Down Expand Up @@ -143,7 +143,7 @@ public JsValue BuildConstructor(EvaluationContext context, Environment env)
var constructorInfo = constructor.DefineMethod(proto, constructorParent);
F = constructorInfo.Closure;

F.SetFunctionName(_className ?? "");
F.SetFunctionName(_className ?? classBinding ?? "");

F.MakeConstructor(writableProperty: false, proto);
F._constructorKind = _superClass is null ? ConstructorKind.Base : ConstructorKind.Derived;
Expand Down
26 changes: 23 additions & 3 deletions Jint/Native/JsMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,31 @@ internal void ForEach(ICallable callable, JsValue thisArg)
var args = _engine._jsValueArrayPool.RentArray(3);
args[2] = this;

for (var i = 0; i < _map.Count; i++)
var i = 0;
while (i < _map.Count)
{
args[0] = _map[i];
args[1] = _map.GetKey(i);
var key = _map.GetKey(i);
args[0] = _map[key];
args[1] = key;
callable.Call(thisArg, args);

// Adjust position for mutations during callback
if (i < _map.Count && ReferenceEquals(_map.GetKey(i), key))
{
// Common fast path: key still at same position
i++;
}
else if (_map.ContainsKey(key))
{
var newIndex = _map.IndexOf(key);
if (newIndex < i)
{
// Key moved backward (entries before it were deleted)
i = newIndex + 1;
}
// else: key was deleted and re-added at end, keep i (entries shifted left)
}
// else: key was deleted, entries shifted left so i now points to next entry
}

_engine._jsValueArrayPool.ReturnArray(args);
Expand Down
38 changes: 37 additions & 1 deletion Jint/Native/JsSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,53 @@ internal void ForEach(ICallable callable, JsValue thisArg)
var args = _engine._jsValueArrayPool.RentArray(3);
args[2] = this;

for (var i = 0; i < _set._list.Count; i++)
var i = 0;
while (i < _set._list.Count)
{
var value = _set._list[i];
args[0] = value;
args[1] = value;
callable.Call(thisArg, args);

// Adjust position for mutations during callback
if (i < _set._list.Count && SameComparison(_set._list[i], value))
{
// Common fast path: value still at same position
i++;
}
else if (_set.Contains(value))
{
var newIndex = _set._list.IndexOf(value);
if (newIndex < i)
{
// Value moved backward (entries before it were deleted)
i = newIndex + 1;
}
// else: value was deleted and re-added at end, keep i (entries shifted left)
}
// else: value was deleted, entries shifted left so i now points to next entry
}

_engine._jsValueArrayPool.ReturnArray(args);
}

private static bool SameComparison(JsValue a, JsValue b)
{
// Use reference equality for most values, SameValueZero for numbers
if (ReferenceEquals(a, b))
{
return true;
}

// Handle the case where JsNumber instances may not be reference equal
if (a is JsNumber na && b is JsNumber nb)
{
return na._value == nb._value || (double.IsNaN(na._value) && double.IsNaN(nb._value));
}

return false;
}

internal ObjectInstance Entries() => _engine.Realm.Intrinsics.SetIteratorPrototype.ConstructEntryIterator(this);

internal ObjectInstance Values() => _engine.Realm.Intrinsics.SetIteratorPrototype.ConstructValueIterator(this);
Expand Down
36 changes: 34 additions & 2 deletions Jint/Native/Map/MapIteratorPrototype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ internal IteratorInstance ConstructValueIterator(JsMap map)
private sealed class MapIterator : IteratorInstance
{
private readonly JintOrderedDictionary<JsValue, JsValue> _map;

private int _position;
private JsValue? _lastKey;
private bool _done;

public MapIterator(Engine engine, JsMap map) : base(engine)
{
Expand All @@ -78,16 +79,47 @@ public MapIterator(Engine engine, JsMap map) : base(engine)

public override bool TryIteratorStep(out ObjectInstance nextItem)
{
if (_done)
{
nextItem = IteratorResult.CreateKeyValueIteratorPosition(_engine);
return false;
}

// Adjust position for mutations since last step
if (_lastKey is not null)
{
if (_position > 0 && _position - 1 < _map.Count && ReferenceEquals(_map.GetKey(_position - 1), _lastKey))
{
// Common fast path: lastKey still at expected position
}
else if (_map.ContainsKey(_lastKey))
{
var newIndex = _map.IndexOf(_lastKey);
if (newIndex < _position - 1)
{
// Key moved backward (entries before it were deleted)
_position = newIndex + 1;
}
// else: key was deleted and re-added at end, keep position
}
else
{
// Key deleted, entries shifted left
_position = System.Math.Max(0, _position - 1);
}
}

if (_position < _map.Count)
{
var key = _map.GetKey(_position);
var value = _map[key];

_lastKey = key;
_position++;
nextItem = IteratorResult.CreateKeyValueIteratorPosition(_engine, key, value);
return true;
}

_done = true;
nextItem = IteratorResult.CreateKeyValueIteratorPosition(_engine);
return false;
}
Expand Down
3 changes: 1 addition & 2 deletions Jint/Native/Number/Dtoa/DtoaNumberFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ public static void DoubleToAscii(
fast_worked = FastDtoa.NumberToString(v, DtoaMode.Shortest, 0, out point, ref buffer);
break;
case DtoaMode.Fixed:
//fast_worked = FastFixedDtoa(v, requested_digits, buffer, length, point);
Throw.NotImplementedException();
// No fast path implemented; fall through to bignum.
break;
case DtoaMode.Precision:
fast_worked = FastDtoa.NumberToString(v, DtoaMode.Precision, requested_digits, out point, ref buffer);
Expand Down
109 changes: 98 additions & 11 deletions Jint/Native/Number/NumberPrototype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,31 +94,118 @@ private JsValue ToFixed(JsValue thisObject, JsCallArguments arguments)
Throw.RangeError(_realm, "fractionDigits argument must be between 0 and 100");
}

// limitation with .NET, max is 99
if (f == 100)
{
Throw.RangeError(_realm, "100 fraction digits is not supported due to .NET format specifier limitation");
}

var x = TypeConverter.ToNumber(thisObject);

if (double.IsNaN(x))
{
return "NaN";
}

if (x >= Ten21)
if (x >= Ten21 || x <= -Ten21)
{
return ToNumberString(x);
}

// handle non-decimal with greater precision
if (System.Math.Abs(x - (long) x) < JsNumber.DoubleIsIntegerTolerance)
bool negative = false;
if (x < 0)
{
negative = true;
x = -x;
}

if (f == 0)
{
// Fast path: no fractional digits
var rounded = System.Math.Round(x, MidpointRounding.AwayFromZero);
var result = negative ? "-" + ((long) rounded).ToString(CultureInfo.InvariantCulture) : ((long) rounded).ToString(CultureInfo.InvariantCulture);
return result;
}

// Use .NET formatting for f <= 99 (fast path)
if (f <= 99)
{
// handle non-decimal with greater precision
if (System.Math.Abs(x - (long) x) < JsNumber.DoubleIsIntegerTolerance)
{
var result = ((long) x).ToString("f" + f, CultureInfo.InvariantCulture);
return negative ? "-" + result : result;
}

var formatted = x.ToString("f" + f, CultureInfo.InvariantCulture);
return negative ? "-" + formatted : formatted;
}

// Use Dtoa infrastructure for f == 100 (avoids .NET format specifier limitation)
return ToFixedDtoa(x, f, negative);
}

private static string ToFixedDtoa(double x, int fractionDigits, bool negative)
{
if (x == 0)
{
var sb = new ValueStringBuilder(stackalloc char[128]);
if (negative)
{
sb.Append('-');
}
sb.Append("0.");
sb.Append('0', fractionDigits);
return sb.ToString();
}

var dtoaBuilder = new DtoaBuilder(stackalloc char[fractionDigits + 50]);
DtoaNumberFormatter.DoubleToAscii(
ref dtoaBuilder,
x,
DtoaMode.Fixed,
fractionDigits,
out _,
out var decimalPoint);

var result2 = new ValueStringBuilder(stackalloc char[fractionDigits + 50]);
if (negative)
{
return ((long) x).ToString("f" + f, CultureInfo.InvariantCulture);
result2.Append('-');
}

if (decimalPoint <= 0)
{
// 0.000...digits
result2.Append("0.");
result2.Append('0', -decimalPoint);
result2.Append(dtoaBuilder._chars.Slice(0, dtoaBuilder.Length));
int remaining = fractionDigits - (-decimalPoint + dtoaBuilder.Length);
if (remaining > 0)
{
result2.Append('0', remaining);
}
}
else if (decimalPoint >= dtoaBuilder.Length)
{
// Integer part only, pad with zeros
result2.Append(dtoaBuilder._chars.Slice(0, dtoaBuilder.Length));
result2.Append('0', decimalPoint - dtoaBuilder.Length);
if (fractionDigits > 0)
{
result2.Append('.');
result2.Append('0', fractionDigits);
}
}
else
{
// digits split across integer and fractional part
result2.Append(dtoaBuilder._chars.Slice(0, decimalPoint));
result2.Append('.');
int fracDigitsFromDtoa = dtoaBuilder.Length - decimalPoint;
result2.Append(dtoaBuilder._chars.Slice(decimalPoint, fracDigitsFromDtoa));
int remaining = fractionDigits - fracDigitsFromDtoa;
if (remaining > 0)
{
result2.Append('0', remaining);
}
}

return x.ToString("f" + f, CultureInfo.InvariantCulture);
return result2.ToString();
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion Jint/Native/Object/ObjectInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1683,7 +1683,7 @@ internal static void DefineField(ObjectInstance receiver, ClassFieldDefinition f
var initValue = Undefined;
if (initializer is not null)
{
initValue = receiver._engine.Call(initializer, receiver);
initValue = receiver._engine.Call(initializer, thisObject: receiver, Arguments.Empty);
if (initValue is Function.Function functionInstance)
{
functionInstance.SetFunctionName(fieldName);
Expand Down
Loading