diff --git a/Jint.Tests.Test262/Test262Harness.settings.json b/Jint.Tests.Test262/Test262Harness.settings.json index 08d8a7aedb..e97144f5d0 100644 --- a/Jint.Tests.Test262/Test262Harness.settings.json +++ b/Jint.Tests.Test262/Test262Harness.settings.json @@ -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", diff --git a/Jint.Tests/Runtime/NumberTests.cs b/Jint.Tests/Runtime/NumberTests.cs index b43ae7fb57..031718a054 100644 --- a/Jint.Tests/Runtime/NumberTests.cs +++ b/Jint.Tests/Runtime/NumberTests.cs @@ -40,10 +40,10 @@ public void ToFixed(int fractionDigits, string result) } [Fact] - public void ToFixedWith100FractionDigitsThrows() + public void ToFixedWith100FractionDigitsWorks() { - var ex = Assert.Throws(() => _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] diff --git a/Jint/Native/Function/ClassDefinition.cs b/Jint/Native/Function/ClassDefinition.cs index 3e8943e224..d171f9d56b 100644 --- a/Jint/Native/Function/ClassDefinition.cs +++ b/Jint/Native/Function/ClassDefinition.cs @@ -49,7 +49,7 @@ public ClassDefinition( /// /// https://tc39.es/ecma262/#sec-runtime-semantics-classdefinitionevaluation /// - 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); @@ -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; diff --git a/Jint/Native/JsMap.cs b/Jint/Native/JsMap.cs index bffc8b865b..9b9926e22c 100644 --- a/Jint/Native/JsMap.cs +++ b/Jint/Native/JsMap.cs @@ -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); diff --git a/Jint/Native/JsSet.cs b/Jint/Native/JsSet.cs index f7c74c54fe..4ccd60981d 100644 --- a/Jint/Native/JsSet.cs +++ b/Jint/Native/JsSet.cs @@ -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); diff --git a/Jint/Native/Map/MapIteratorPrototype.cs b/Jint/Native/Map/MapIteratorPrototype.cs index d0871fd0f5..3cecc85a86 100644 --- a/Jint/Native/Map/MapIteratorPrototype.cs +++ b/Jint/Native/Map/MapIteratorPrototype.cs @@ -67,8 +67,9 @@ internal IteratorInstance ConstructValueIterator(JsMap map) private sealed class MapIterator : IteratorInstance { private readonly JintOrderedDictionary _map; - private int _position; + private JsValue? _lastKey; + private bool _done; public MapIterator(Engine engine, JsMap map) : base(engine) { @@ -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; } diff --git a/Jint/Native/Number/Dtoa/DtoaNumberFormatter.cs b/Jint/Native/Number/Dtoa/DtoaNumberFormatter.cs index 8bfcec2273..85b6e99091 100644 --- a/Jint/Native/Number/Dtoa/DtoaNumberFormatter.cs +++ b/Jint/Native/Number/Dtoa/DtoaNumberFormatter.cs @@ -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); diff --git a/Jint/Native/Number/NumberPrototype.cs b/Jint/Native/Number/NumberPrototype.cs index 348b8a8075..8ae7a35c70 100644 --- a/Jint/Native/Number/NumberPrototype.cs +++ b/Jint/Native/Number/NumberPrototype.cs @@ -94,12 +94,6 @@ 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)) @@ -107,18 +101,111 @@ private JsValue ToFixed(JsValue thisObject, JsCallArguments arguments) 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(); } /// diff --git a/Jint/Native/Object/ObjectInstance.cs b/Jint/Native/Object/ObjectInstance.cs index cfa085b458..207e057fd5 100644 --- a/Jint/Native/Object/ObjectInstance.cs +++ b/Jint/Native/Object/ObjectInstance.cs @@ -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); diff --git a/Jint/Native/Set/SetIteratorPrototype.cs b/Jint/Native/Set/SetIteratorPrototype.cs index 9be10ceb1f..fbf4b4e895 100644 --- a/Jint/Native/Set/SetIteratorPrototype.cs +++ b/Jint/Native/Set/SetIteratorPrototype.cs @@ -42,7 +42,7 @@ internal IteratorInstance ConstructEntryIterator(JsSet set) internal IteratorInstance ConstructValueIterator(JsSet set) { - var instance = new SetValueIterator(Engine, set._set._list); + var instance = new SetValueIterator(Engine, set); return instance; } @@ -50,6 +50,8 @@ private sealed class SetEntryIterator : IteratorInstance { private readonly JsSet _set; private int _position; + private JsValue? _lastValue; + private bool _done; public SetEntryIterator(Engine engine, JsSet set) : base(engine) { @@ -60,14 +62,25 @@ public SetEntryIterator(Engine engine, JsSet set) : base(engine) public override bool TryIteratorStep(out ObjectInstance nextItem) { + if (_done) + { + nextItem = IteratorResult.CreateKeyValueIteratorPosition(_engine); + return false; + } + + // Adjust position for mutations since last step + AdjustPosition(ref _position, _lastValue, _set._set); + if (_position < _set._set._list.Count) { var value = _set._set[_position]; + _lastValue = value; _position++; nextItem = IteratorResult.CreateKeyValueIteratorPosition(_engine, value, value); return true; } + _done = true; nextItem = IteratorResult.CreateKeyValueIteratorPosition(_engine); return false; } @@ -75,30 +88,69 @@ public override bool TryIteratorStep(out ObjectInstance nextItem) private sealed class SetValueIterator : IteratorInstance { - private readonly List _values; + private readonly JsSet _set; private int _position; - private bool _closed; + private JsValue? _lastValue; + private bool _done; - public SetValueIterator(Engine engine, List values) : base(engine) + public SetValueIterator(Engine engine, JsSet set) : base(engine) { _prototype = engine.Realm.Intrinsics.SetIteratorPrototype; - _values = values; + _set = set; _position = 0; } public override bool TryIteratorStep(out ObjectInstance nextItem) { - if (!_closed && _position < _values.Count) + if (_done) { - var value = _values[_position]; + nextItem = IteratorResult.CreateKeyValueIteratorPosition(_engine); + return false; + } + + // Adjust position for mutations since last step + AdjustPosition(ref _position, _lastValue, _set._set); + + if (_position < _set._set._list.Count) + { + var value = _set._set[_position]; + _lastValue = value; _position++; nextItem = IteratorResult.CreateValueIteratorPosition(_engine, value); return true; } - _closed = true; + _done = true; nextItem = IteratorResult.CreateKeyValueIteratorPosition(_engine); return false; } } + + private static void AdjustPosition(ref int position, JsValue? lastValue, OrderedSet set) + { + if (lastValue is null) + { + return; + } + + if (position > 0 && position - 1 < set._list.Count && ReferenceEquals(set._list[position - 1], lastValue)) + { + // Common fast path: lastValue still at expected position + } + else if (set.Contains(lastValue)) + { + var newIndex = set._list.IndexOf(lastValue); + if (newIndex < position - 1) + { + // Value moved backward (entries before it were deleted) + position = newIndex + 1; + } + // else: value was deleted and re-added at end, keep position + } + else + { + // Value deleted, entries shifted left + position = System.Math.Max(0, position - 1); + } + } } diff --git a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs index 943b68bb4f..bd2a05d187 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs @@ -440,13 +440,20 @@ protected override object EvaluateInternal(EvaluationContext context) private JsValue NamedEvaluation(EvaluationContext context, JintExpression expression) { - var rval = expression.GetValue(context); if (expression._expression.IsAnonymousFunctionDefinition() && _left._expression.Type == NodeType.Identifier) { - ((Function) rval).SetFunctionName(((Identifier) _left._expression).Name); + var name = ((Identifier) _left._expression).Name; + if (expression is JintClassExpression classExpression) + { + return classExpression.EvaluateWithName(context, name); + } + + var rval = expression.GetValue(context); + ((Function) rval).SetFunctionName(name); + return rval; } - return rval; + return expression.GetValue(context); } internal sealed class SimpleAssignmentExpression : JintExpression @@ -537,7 +544,16 @@ private JsValue SetValue(EvaluationContext context) Throw.SyntaxError(engine.Realm, "Invalid assignment target"); } - var completion = right.GetValue(context); + JsValue completion; + if (right is JintClassExpression classExpression && right._expression.IsAnonymousFunctionDefinition()) + { + completion = classExpression.EvaluateWithName(context, identifier.Value.ToString()); + } + else + { + completion = right.GetValue(context); + } + if (context.IsAbrupt()) { return completion; @@ -551,7 +567,7 @@ private JsValue SetValue(EvaluationContext context) var rval = completion.Clone(); - if (right._expression.IsFunctionDefinition()) + if (right._expression.IsFunctionDefinition() && right is not JintClassExpression) { ((Function) rval).SetFunctionName(identifier.Value); } diff --git a/Jint/Runtime/Interpreter/Expressions/JintClassExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintClassExpression.cs index 8b91ec14ef..7b27082597 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintClassExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintClassExpression.cs @@ -1,3 +1,4 @@ +using Jint.Native; using Jint.Native.Function; namespace Jint.Runtime.Interpreter.Expressions; @@ -16,4 +17,10 @@ protected override object EvaluateInternal(EvaluationContext context) var env = context.Engine.ExecutionContext.LexicalEnvironment; return _classDefinition.BuildConstructor(context, env); } + + internal JsValue EvaluateWithName(EvaluationContext context, string classBinding) + { + var env = context.Engine.ExecutionContext.LexicalEnvironment; + return _classDefinition.BuildConstructor(context, env, classBinding); + } } diff --git a/Jint/Runtime/Interpreter/Statements/JintVariableDeclaration.cs b/Jint/Runtime/Interpreter/Statements/JintVariableDeclaration.cs index 5756751865..44158b3106 100644 --- a/Jint/Runtime/Interpreter/Statements/JintVariableDeclaration.cs +++ b/Jint/Runtime/Interpreter/Statements/JintVariableDeclaration.cs @@ -69,7 +69,14 @@ protected override Completion ExecuteInternal(EvaluationContext context) var value = JsValue.Undefined; if (declaration.Init != null) { - value = declaration.Init.GetValue(context).Clone(); + if (declaration.Init is JintClassExpression classExpr && declaration.Init._expression.IsAnonymousFunctionDefinition()) + { + value = classExpr.EvaluateWithName(context, lhs.ReferencedName.ToString()).Clone(); + } + else + { + value = declaration.Init.GetValue(context).Clone(); + } // Check for generator suspension after evaluating initializer if (context.IsSuspended()) @@ -78,7 +85,7 @@ protected override Completion ExecuteInternal(EvaluationContext context) return new Completion(CompletionType.Normal, value, _statement); } - if (declaration.Init._expression.IsFunctionDefinition()) + if (declaration.Init._expression.IsFunctionDefinition() && declaration.Init is not JintClassExpression) { ((Function) value).SetFunctionName(lhs.ReferencedName); } @@ -127,7 +134,15 @@ protected override Completion ExecuteInternal(EvaluationContext context) var lhs = (Reference) declaration.Left!.Evaluate(context); lhs.AssertValid(engine.Realm); - var value = declaration.Init.GetValue(context).Clone(); + JsValue value; + if (declaration.Init is JintClassExpression classExpr && declaration.Init._expression.IsAnonymousFunctionDefinition()) + { + value = classExpr.EvaluateWithName(context, lhs.ReferencedName.ToString()).Clone(); + } + else + { + value = declaration.Init.GetValue(context).Clone(); + } // Check for generator suspension after evaluating initializer if (context.IsSuspended()) @@ -136,7 +151,7 @@ protected override Completion ExecuteInternal(EvaluationContext context) return new Completion(CompletionType.Normal, value, _statement); } - if (declaration.Init._expression.IsFunctionDefinition()) + if (declaration.Init._expression.IsFunctionDefinition() && declaration.Init is not JintClassExpression) { ((Function) value).SetFunctionName(lhs.ReferencedName); }