diff --git a/Directory.Packages.props b/Directory.Packages.props index c59f121edb..2489c94f9e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,14 +8,15 @@ - + + - + @@ -28,6 +29,6 @@ - + \ No newline at end of file diff --git a/Jint.Benchmark/README.md b/Jint.Benchmark/README.md index e2d10c98c4..7a2a8cf90f 100644 --- a/Jint.Benchmark/README.md +++ b/Jint.Benchmark/README.md @@ -9,7 +9,7 @@ dotnet run -c Release --allCategories EngineComparison * tests are run in global engine strict mode, as YantraJS always uses strict mode which improves performance * `Jint` and `Jint_ParsedScript` shows the difference between always parsing the script source file and reusing parsed `Script` instance. -Last updated 2023-11-05 +Last updated 2023-11-24 * Jint main * Jurassic 3.2.7 diff --git a/Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj b/Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj index b5340a5658..2896385681 100644 --- a/Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj +++ b/Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj @@ -26,6 +26,7 @@ + diff --git a/Jint.Tests.PublicInterface/TimeSystemTests.cs b/Jint.Tests.PublicInterface/TimeSystemTests.cs index 7d4df5bcd1..049f594536 100644 --- a/Jint.Tests.PublicInterface/TimeSystemTests.cs +++ b/Jint.Tests.PublicInterface/TimeSystemTests.cs @@ -1,5 +1,6 @@ using System.Globalization; using Jint.Runtime; +using Microsoft.Extensions.Time.Testing; using NodaTime; namespace Jint.Tests.PublicInterface; @@ -33,6 +34,29 @@ public void CanProduceValidDatesUsingNodaTimeIntegration(string input, long expe Assert.Equal(expected, engine.Evaluate($"new Date({input}) * 1").AsNumber()); } + + [Fact] + public void CanUseTimeProvider() + { + var defaultEngine = new Engine(); + var defaultNow = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + var defaultScriptNow = defaultEngine.Evaluate("new Date() * 1").AsNumber(); + Assert.InRange(defaultScriptNow, defaultNow, defaultNow + 100); + + var timeProvider = new FakeTimeProvider(); + timeProvider.SetUtcNow(new DateTimeOffset(2023, 11, 6, 0, 0, 0, 0, TimeSpan.Zero)); + + var timeProviderEngine = new Engine(options => + { + options.TimeSystem = new TimeProviderTimeSystem(timeProvider); + }); + + var timeProviderNow = timeProvider.GetUtcNow().ToUnixTimeMilliseconds(); + var timeProviderScriptNow = timeProviderEngine.Evaluate("new Date() * 1").AsNumber(); + Assert.InRange(timeProviderNow, timeProviderScriptNow, timeProviderScriptNow + 100); + + Assert.NotInRange(timeProviderScriptNow, defaultScriptNow - 10000, defaultScriptNow + 10000); + } } file sealed class NodaTimeSystem : DefaultTimeSystem @@ -52,3 +76,18 @@ public override TimeSpan GetUtcOffset(long epochMilliseconds) return offset.ToTimeSpan(); } } + +file sealed class TimeProviderTimeSystem : DefaultTimeSystem +{ + private readonly TimeProvider _timeProvider; + + public TimeProviderTimeSystem(TimeProvider timeProvider) : base(TimeZoneInfo.Utc, CultureInfo.CurrentCulture) + { + _timeProvider = timeProvider; + } + + public override DateTimeOffset GetUtcNow() + { + return _timeProvider.GetUtcNow(); + } +} diff --git a/Jint.Tests.Test262/Test262Harness.settings.json b/Jint.Tests.Test262/Test262Harness.settings.json index f6ac59bf09..d5c7c99fa5 100644 --- a/Jint.Tests.Test262/Test262Harness.settings.json +++ b/Jint.Tests.Test262/Test262Harness.settings.json @@ -1,5 +1,5 @@ { - "SuiteGitSha": "6396ebde0316639292530460d1ef961fd9bbe0d4", + "SuiteGitSha": "2060494f280ba89d71a0f51d4ff171bafe476a05", //"SuiteDirectory": "//mnt/c/work/test262", "TargetPath": "./Generated", "Namespace": "Jint.Tests.Test262", @@ -12,6 +12,7 @@ "decorators", "generators", "import-assertions", + "import-attributes", "iterator-helpers", "regexp-duplicate-named-groups", "regexp-lookbehind", @@ -47,6 +48,7 @@ "built-ins/RegExp/prototype/exec/S15.10.6.2_A1_T6.js", "language/literals/regexp/u-case-mapping.js", "built-ins/RegExp/lookahead-quantifier-match-groups.js", + "built-ins/RegExp/unicode_full_case_folding.js", // requires investigation how to process complex function name evaluation for property "built-ins/Function/prototype/toString/method-computed-property-name.js", @@ -85,6 +87,8 @@ "language/expressions/object/method-definition/name-prop-name-yield-id.js", "language/statements/class/elements/*-generator-method-*.js", "language/expressions/class/elements/*-generator-method-*.js", + "built-ins/Set/prototype/union/allows-set-like-class.js", + "built-ins/Set/prototype/union/allows-set-like-object.js", // generators not implemented "built-ins/Object/prototype/toString/proxy-function.js", diff --git a/Jint.Tests/Runtime/EngineLimitTests.cs b/Jint.Tests/Runtime/EngineLimitTests.cs index fe0577883b..9f839e1cc4 100644 --- a/Jint.Tests/Runtime/EngineLimitTests.cs +++ b/Jint.Tests/Runtime/EngineLimitTests.cs @@ -9,7 +9,7 @@ public class EngineLimitTests { #if RELEASE - const int FunctionNestingCount = 840; + const int FunctionNestingCount = 1010; #else const int FunctionNestingCount = 510; #endif diff --git a/Jint/Extensions/Polyfills.cs b/Jint/Extensions/Polyfills.cs index 0117c84606..0fdb9dc89a 100644 --- a/Jint/Extensions/Polyfills.cs +++ b/Jint/Extensions/Polyfills.cs @@ -1,21 +1,17 @@ -using System.Runtime.CompilerServices; - namespace Jint; internal static class Polyfills { #if NETFRAMEWORK || NETSTANDARD2_0 - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] internal static bool Contains(this string source, char c) => source.IndexOf(c) != -1; + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + internal static bool StartsWith(this string source, char c) => source.Length > 0 && source[0] == c; #endif #if NETFRAMEWORK - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] internal static bool Contains(this ReadOnlySpan source, string c) => source.IndexOf(c) != -1; #endif - -#if NETFRAMEWORK || NETSTANDARD2_0 - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool StartsWith(this string source, char c) => source.Length > 0 && source[0] == c; -#endif } diff --git a/Jint/Native/Array/ArrayPrototype.cs b/Jint/Native/Array/ArrayPrototype.cs index f1e16039d6..697487f766 100644 --- a/Jint/Native/Array/ArrayPrototype.cs +++ b/Jint/Native/Array/ArrayPrototype.cs @@ -1,12 +1,12 @@ #pragma warning disable CA1859 // Use concrete types when possible for improved performance -- most of prototype methods return JsValue using System.Linq; +using System.Text; using Jint.Collections; using Jint.Native.Iterator; using Jint.Native.Number; using Jint.Native.Object; using Jint.Native.Symbol; -using Jint.Pooling; using Jint.Runtime; using Jint.Runtime.Descriptors; using Jint.Runtime.Descriptors.Specialized; @@ -1265,15 +1265,15 @@ static string StringFromJsValue(JsValue value) return s; } - using var sb = StringBuilderPool.Rent(); - sb.Builder.Append(s); + using var sb = new ValueStringBuilder(stackalloc char[256]); + sb.Append(s); for (uint k = 1; k < len; k++) { if (sep != "") { - sb.Builder.Append(sep); + sb.Append(sep); } - sb.Builder.Append(StringFromJsValue(o.Get(k))); + sb.Append(StringFromJsValue(o.Get(k))); } return sb.ToString(); diff --git a/Jint/Native/BigInt/BigIntPrototype.cs b/Jint/Native/BigInt/BigIntPrototype.cs index 86875afaed..8b5311afc4 100644 --- a/Jint/Native/BigInt/BigIntPrototype.cs +++ b/Jint/Native/BigInt/BigIntPrototype.cs @@ -1,9 +1,9 @@ using System.Globalization; using System.Numerics; +using System.Text; using Jint.Collections; using Jint.Native.Object; using Jint.Native.Symbol; -using Jint.Pooling; using Jint.Runtime; using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; @@ -114,22 +114,24 @@ private JsValue ToBigIntString(JsValue thisObject, JsValue[] arguments) value = -value; } - const string digits = "0123456789abcdefghijklmnopqrstuvwxyz"; + const string Digits = "0123456789abcdefghijklmnopqrstuvwxyz"; - using var builder = StringBuilderPool.Rent(); - var sb = builder.Builder; + var sb = new ValueStringBuilder(stackalloc char[64]); for (; value > 0; value /= radixMV) { var d = (int) (value % radixMV); - sb.Append(digits[d]); + sb.Append(Digits[d]); } - var charArray = new char[sb.Length]; - sb.CopyTo(0, charArray, 0, charArray.Length); - System.Array.Reverse(charArray); + if (negative) + { + sb.Append('-'); + } + + sb.Reverse(); - return (negative ? "-" : "") + new string(charArray); + return sb.ToString(); } /// diff --git a/Jint/Native/Date/DateConstructor.cs b/Jint/Native/Date/DateConstructor.cs index ac89e19ccb..449589cd7e 100644 --- a/Jint/Native/Date/DateConstructor.cs +++ b/Jint/Native/Date/DateConstructor.cs @@ -98,9 +98,9 @@ private static JsValue Utc(JsValue thisObject, JsValue[] arguments) return finalDate.TimeClip().ToJsValue(); } - private static JsValue Now(JsValue thisObject, JsValue[] arguments) + private JsValue Now(JsValue thisObject, JsValue[] arguments) { - return (long) (DateTime.UtcNow - Epoch).TotalMilliseconds; + return (long) (_timeSystem.GetUtcNow().DateTime - Epoch).TotalMilliseconds; } protected internal override JsValue Call(JsValue thisObject, JsValue[] arguments) @@ -120,7 +120,7 @@ public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget) newTarget, static intrinsics => intrinsics.Date.PrototypeObject, static (engine, _, dateValue) => new JsDate(engine, dateValue), - (DateTime.UtcNow - Epoch).TotalMilliseconds); + (_timeSystem.GetUtcNow().DateTime - Epoch).TotalMilliseconds); } return ConstructUnlikely(arguments, newTarget); diff --git a/Jint/Native/JsValue.cs b/Jint/Native/JsValue.cs index cfab33dbab..f23f8b187a 100644 --- a/Jint/Native/JsValue.cs +++ b/Jint/Native/JsValue.cs @@ -54,7 +54,18 @@ internal IteratorInstance GetIterator(Realm realm, GeneratorKind hint = Generato } [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal IteratorInstance GetIteratorFromMethod(Realm realm, ICallable method) + { + var iterator = method.Call(this); + if (iterator is not ObjectInstance objectInstance) + { + ExceptionHelper.ThrowTypeError(realm); + return null!; + } + return new IteratorInstance.ObjectIterator(objectInstance); + } + + [Pure] internal bool TryGetIterator(Realm realm, [NotNullWhen(true)] out IteratorInstance? iterator, GeneratorKind hint = GeneratorKind.Sync, ICallable? method = null) { var obj = TypeConverter.ToObject(realm, this); diff --git a/Jint/Native/Json/JsonParser.cs b/Jint/Native/Json/JsonParser.cs index d1173854e0..3dc380781e 100644 --- a/Jint/Native/Json/JsonParser.cs +++ b/Jint/Native/Json/JsonParser.cs @@ -5,8 +5,6 @@ using System.Runtime.InteropServices; using System.Text; using Esprima; -using Jint.Native.Object; -using Jint.Pooling; using Jint.Runtime; namespace Jint.Native.Json @@ -172,9 +170,9 @@ private string ScanPunctuatorValue(int start, char code) } } - private Token ScanNumericLiteral(ref State state) + private Token ScanNumericLiteral() { - var sb = state.TokenBuffer; + using var sb = new ValueStringBuilder(stackalloc char[64]); var start = _index; var ch = _source.CharCodeAt(_index); var canBeInteger = true; @@ -249,7 +247,6 @@ private Token ScanNumericLiteral(ref State state) } var number = sb.ToString(); - sb.Clear(); JsNumber value; if (canBeInteger && long.TryParse(number, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longResult) && longResult != -0) @@ -312,7 +309,7 @@ private Token ScanStringLiteral(ref State state) int start = _index; ++_index; - var sb = state.TokenBuffer; + using var sb = new ValueStringBuilder(stackalloc char[64]); while (_index < _length) { char ch = _source[_index++]; @@ -382,8 +379,7 @@ private Token ScanStringLiteral(ref State state) ThrowError(_index, Messages.UnexpectedEOS); } - string value = sb.ToString(); - sb.Clear(); + var value = sb.ToString(); return CreateToken(Tokens.String, value, '\"', new JsString(value), new TextRange(start, _index)); } @@ -407,14 +403,14 @@ private Token Advance(ref State state) { if (IsDecimalDigit(_source.CharCodeAt(_index + 1))) { - return ScanNumericLiteral(ref state); + return ScanNumericLiteral(); } return ScanPunctuator(); } if (IsDecimalDigit(ch)) { - return ScanNumericLiteral(ref state); + return ScanNumericLiteral(); } if (ch == 't' || ch == 'f') @@ -704,8 +700,7 @@ public JsValue Parse(string code, ParserOptions? options) _length = _source.Length; _lookahead = null!; - using var wrapper = StringBuilderPool.Rent(); - State state = new State(wrapper.Builder); + State state = new State(); Peek(ref state); JsValue jsv = ParseJsonValue(ref state); @@ -719,22 +714,9 @@ public JsValue Parse(string code, ParserOptions? options) return jsv; } + [StructLayout(LayoutKind.Auto)] private ref struct State { - public State(StringBuilder tokenBuffer) - { - TokenBuffer = tokenBuffer; - CurrentDepth = 0; - } - - /// - /// StringBuilder instance which can be used to collect - /// characters into a single string. Must only be used - /// when no child-parser gets called. Must be cleared - /// after usage. - /// - public StringBuilder TokenBuffer { get; } - /// /// The current recursion depth /// diff --git a/Jint/Native/Json/JsonSerializer.cs b/Jint/Native/Json/JsonSerializer.cs index c13da3cb97..9b011b80ee 100644 --- a/Jint/Native/Json/JsonSerializer.cs +++ b/Jint/Native/Json/JsonSerializer.cs @@ -5,11 +5,9 @@ using Jint.Native.BigInt; using Jint.Native.Boolean; using Jint.Native.Number; -using Jint.Native.Number.Dtoa; using Jint.Native.Object; using Jint.Native.Proxy; using Jint.Native.String; -using Jint.Pooling; using Jint.Runtime; using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; @@ -54,15 +52,20 @@ public JsValue Serialize(JsValue value, JsValue replacer, JsValue space) var wrapper = _engine.Realm.Intrinsics.Object.Construct(Arguments.Empty); wrapper.DefineOwnProperty(JsString.Empty, new PropertyDescriptor(value, PropertyFlag.ConfigurableEnumerableWritable)); - using var jsonBuilder = StringBuilderPool.Rent(); - - var target = new SerializerState(jsonBuilder.Builder); - if (SerializeJSONProperty(JsString.Empty, wrapper, ref target) == SerializeResult.Undefined) + string result; + var json = new ValueStringBuilder(); + try { - return JsValue.Undefined; + if (SerializeJSONProperty(JsString.Empty, wrapper, ref json) == SerializeResult.Undefined) + { + return JsValue.Undefined; + } } - - return new JsString(target.Json.ToString()); + finally + { + result = json.ToString(); + } + return new JsString(result); } private void SetupReplacer(JsValue replacer) @@ -154,25 +157,25 @@ private static string BuildSpacingGap(JsValue space) /// /// https://tc39.es/ecma262/#sec-serializejsonproperty /// - private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref SerializerState target) + private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref ValueStringBuilder json) { var value = ReadUnwrappedValue(key, holder); if (ReferenceEquals(value, JsValue.Null)) { - target.Json.Append("null"); + json.Append("null"); return SerializeResult.NotUndefined; } if (value.IsBoolean()) { - target.Json.Append(((JsBoolean) value)._value ? "true" : "false"); + json.Append(((JsBoolean) value)._value ? "true" : "false"); return SerializeResult.NotUndefined; } if (value.IsString()) { - QuoteJSONString(value.ToString(), target.Json); + QuoteJSONString(value.ToString(), ref json); return SerializeResult.NotUndefined; } @@ -182,7 +185,7 @@ private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref S if (value.IsInteger()) { - target.Json.Append((long) doubleValue); + json.Append(((long) doubleValue).ToString(CultureInfo.InvariantCulture)); return SerializeResult.NotUndefined; } @@ -191,16 +194,15 @@ private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref S { if (TypeConverter.CanBeStringifiedAsLong(doubleValue)) { - target.Json.Append((long) doubleValue); + json.Append(((long) doubleValue).ToString(CultureInfo.InvariantCulture)); return SerializeResult.NotUndefined; } - target.DtoaBuilder.Reset(); - NumberPrototype.NumberToString(doubleValue, target.DtoaBuilder, target.Json); + json.Append(NumberPrototype.ToNumberString(doubleValue)); return SerializeResult.NotUndefined; } - target.Json.Append(JsString.NullString); + json.Append("null"); return SerializeResult.NotUndefined; } @@ -213,18 +215,18 @@ private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref S { if (CanSerializesAsArray(objectInstance)) { - SerializeJSONArray(objectInstance, ref target); + SerializeJSONArray(objectInstance, ref json); return SerializeResult.NotUndefined; } if (objectInstance is IObjectWrapper wrapper && _engine.Options.Interop.SerializeToJson is { } serialize) { - target.Json.Append(serialize(wrapper.Target)); + json.Append(serialize(wrapper.Target)); return SerializeResult.NotUndefined; } - SerializeJSONObject(objectInstance, ref target); + SerializeJSONObject(objectInstance, ref json); return SerializeResult.NotUndefined; } @@ -305,16 +307,16 @@ private static bool CanSerializesAsArray(ObjectInstance value) /// /// MethodImplOptions.AggressiveOptimization = 512 which is only exposed in .NET Core. /// - [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)512)] - private static unsafe void QuoteJSONString(string value, StringBuilder target) + [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions) 512)] + private static unsafe void QuoteJSONString(string value, ref ValueStringBuilder json) { if (value.Length == 0) { - target.Append("\"\""); + json.Append("\"\""); return; } - target.Append('"'); + json.Append('"'); #if NETCOREAPP1_0_OR_GREATER fixed (char* ptr = value) @@ -327,7 +329,7 @@ private static unsafe void QuoteJSONString(string value, StringBuilder target) if (index < 0) { // append the remaining text which doesn't need any encoding. - target.Append(value.AsSpan(offset)); + json.Append(value.AsSpan(offset)); break; } @@ -335,10 +337,10 @@ private static unsafe void QuoteJSONString(string value, StringBuilder target) if (index - offset > 0) { // append everything which does not need any encoding until the found index. - target.Append(value.AsSpan(offset, index - offset)); + json.Append(value.AsSpan(offset, index - offset)); } - AppendJsonStringCharacter(value, ref index, target); + AppendJsonStringCharacter(value, ref index, ref json); offset = index + 1; remainingLength = value.Length - offset; @@ -351,59 +353,59 @@ private static unsafe void QuoteJSONString(string value, StringBuilder target) #else for (var i = 0; i < value.Length; i++) { - AppendJsonStringCharacter(value, ref i, target); + AppendJsonStringCharacter(value, ref i, ref json); } #endif - target.Append('"'); + json.Append('"'); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AppendJsonStringCharacter(string value, ref int index, StringBuilder target) + private static void AppendJsonStringCharacter(string value, ref int index, ref ValueStringBuilder json) { var c = value[index]; switch (c) { case '\"': - target.Append("\\\""); + json.Append("\\\""); break; case '\\': - target.Append("\\\\"); + json.Append("\\\\"); break; case '\b': - target.Append("\\b"); + json.Append("\\b"); break; case '\f': - target.Append("\\f"); + json.Append("\\f"); break; case '\n': - target.Append("\\n"); + json.Append("\\n"); break; case '\r': - target.Append("\\r"); + json.Append("\\r"); break; case '\t': - target.Append("\\t"); + json.Append("\\t"); break; default: if (char.IsSurrogatePair(value, index)) { #if NETCOREAPP1_0_OR_GREATER - target.Append(value.AsSpan(index, 2)); + json.Append(value.AsSpan(index, 2)); index++; #else - target.Append(c); + json.Append(c); index++; - target.Append(value[index]); + json.Append(value[index]); #endif } else if (c < 0x20 || char.IsSurrogate(c)) { - target.Append("\\u"); - target.Append(((int) c).ToString("x4", CultureInfo.InvariantCulture)); + json.Append("\\u"); + json.Append(((int) c).ToString("x4", CultureInfo.InvariantCulture)); } else { - target.Append(c); + json.Append(c); } break; } @@ -412,12 +414,12 @@ private static void AppendJsonStringCharacter(string value, ref int index, Strin /// /// https://tc39.es/ecma262/#sec-serializejsonarray /// - private void SerializeJSONArray(ObjectInstance value, ref SerializerState target) + private void SerializeJSONArray(ObjectInstance value, ref ValueStringBuilder json) { var len = TypeConverter.ToUint32(value.Get(CommonProperties.Length)); if (len == 0) { - target.Json.Append("[]"); + json.Append("[]"); return; } @@ -435,22 +437,22 @@ private void SerializeJSONArray(ObjectInstance value, ref SerializerState target { if (hasPrevious) { - target.Json.Append(separator); + json.Append(separator); } else { - target.Json.Append('['); + json.Append('['); } if (_gap.Length > 0) { - target.Json.Append('\n'); - target.Json.Append(_indent); + json.Append('\n'); + json.Append(_indent); } - if (SerializeJSONProperty(i, value, ref target) == SerializeResult.Undefined) + if (SerializeJSONProperty(i, value, ref json) == SerializeResult.Undefined) { - target.Json.Append(JsString.NullString); + json.Append("null"); } hasPrevious = true; @@ -460,16 +462,16 @@ private void SerializeJSONArray(ObjectInstance value, ref SerializerState target { _stack.Exit(); _indent = stepback; - target.Json.Append("[]"); + json.Append("[]"); return; } if (_gap.Length > 0) { - target.Json.Append('\n'); - target.Json.Append(stepback); + json.Append('\n'); + json.Append(stepback); } - target.Json.Append(']'); + json.Append(']'); _stack.Exit(); _indent = stepback; @@ -478,14 +480,14 @@ private void SerializeJSONArray(ObjectInstance value, ref SerializerState target /// /// https://tc39.es/ecma262/#sec-serializejsonobject /// - private void SerializeJSONObject(ObjectInstance value, ref SerializerState target) + private void SerializeJSONObject(ObjectInstance value, ref ValueStringBuilder json) { var enumeration = _propertyList is null ? PropertyEnumeration.FromObjectInstance(value) : PropertyEnumeration.FromList(_propertyList); if (enumeration.IsEmpty) { - target.Json.Append("{}"); + json.Append("{}"); return; } @@ -501,33 +503,33 @@ private void SerializeJSONObject(ObjectInstance value, ref SerializerState targe for (var i = 0; i < enumeration.Keys.Count; i++) { var p = enumeration.Keys[i]; - int position = target.Json.Length; + int position = json.Length; if (hasPrevious) { - target.Json.Append(separator); + json.Append(separator); } else { - target.Json.Append('{'); + json.Append('{'); } if (_gap.Length > 0) { - target.Json.Append('\n'); - target.Json.Append(_indent); + json.Append('\n'); + json.Append(_indent); } - QuoteJSONString(p.ToString(), target.Json); - target.Json.Append(':'); + QuoteJSONString(p.ToString(), ref json); + json.Append(':'); if (_gap.Length > 0) { - target.Json.Append(' '); + json.Append(' '); } - if (SerializeJSONProperty(p, value, ref target) == SerializeResult.Undefined) + if (SerializeJSONProperty(p, value, ref json) == SerializeResult.Undefined) { - target.Json.Length = position; + json.Length = position; } else { @@ -539,33 +541,21 @@ private void SerializeJSONObject(ObjectInstance value, ref SerializerState targe { _stack.Exit(); _indent = stepback; - target.Json.Append("{}"); + json.Append("{}"); return; } if (_gap.Length > 0) { - target.Json.Append('\n'); - target.Json.Append(stepback); + json.Append('\n'); + json.Append(stepback); } - target.Json.Append('}'); + json.Append('}'); _stack.Exit(); _indent = stepback; } - private readonly ref struct SerializerState - { - public SerializerState(StringBuilder jsonBuilder) - { - Json = jsonBuilder; - DtoaBuilder = TypeConverter.CreateDtoaBuilderForDouble(); - } - - public readonly StringBuilder Json; - public readonly DtoaBuilder DtoaBuilder; - } - private enum SerializeResult { NotUndefined, diff --git a/Jint/Native/Number/Dtoa/BignumDtoa.cs b/Jint/Native/Number/Dtoa/BignumDtoa.cs index 098f4cedf9..4389c538f5 100644 --- a/Jint/Native/Number/Dtoa/BignumDtoa.cs +++ b/Jint/Native/Number/Dtoa/BignumDtoa.cs @@ -11,7 +11,7 @@ public static void NumberToString( double v, DtoaMode mode, int requested_digits, - DtoaBuilder builder, + ref DtoaBuilder builder, out int decimal_point) { var bits = (ulong) BitConverter.DoubleToInt64Bits(v); @@ -73,7 +73,7 @@ public static void NumberToString( delta_minus, delta_plus, is_even, - builder); + ref builder); break; case DtoaMode.Fixed: BignumToFixed( @@ -81,7 +81,7 @@ public static void NumberToString( ref decimal_point, numerator, denominator, - builder); + ref builder); break; case DtoaMode.Precision: GenerateCountedDigits( @@ -89,7 +89,7 @@ public static void NumberToString( ref decimal_point, numerator, denominator, - builder); + ref builder); break; default: ExceptionHelper.ThrowArgumentOutOfRangeException(); @@ -117,7 +117,7 @@ private static void GenerateShortestDigits( Bignum delta_minus, Bignum delta_plus, bool is_even, - DtoaBuilder buffer) + ref DtoaBuilder buffer) { // Small optimization: if delta_minus and delta_plus are the same just reuse // one of the two bignums. @@ -239,7 +239,7 @@ static void GenerateCountedDigits( ref int decimal_point, Bignum numerator, Bignum denominator, - DtoaBuilder buffer) + ref DtoaBuilder buffer) { Debug.Assert(count >= 0); for (int i = 0; i < count - 1; ++i) @@ -286,7 +286,7 @@ static void BignumToFixed( ref int decimal_point, Bignum numerator, Bignum denominator, - DtoaBuilder buffer) + ref DtoaBuilder buffer) { // Note that we have to look at more than just the requested_digits, since // a number could be rounded up. Example: v=0.5 with requested_digits=0. @@ -329,7 +329,7 @@ static void BignumToFixed( // The requested digits correspond to the digits after the point. // The variable 'needed_digits' includes the digits before the point. int needed_digits = (decimal_point) + requested_digits; - GenerateCountedDigits(needed_digits, ref decimal_point, numerator, denominator, buffer); + GenerateCountedDigits(needed_digits, ref decimal_point, numerator, denominator, ref buffer); } } diff --git a/Jint/Native/Number/Dtoa/DtoaBuilder.cs b/Jint/Native/Number/Dtoa/DtoaBuilder.cs index 07998053d0..f0bc39c504 100644 --- a/Jint/Native/Number/Dtoa/DtoaBuilder.cs +++ b/Jint/Native/Number/Dtoa/DtoaBuilder.cs @@ -1,55 +1,53 @@ -#nullable disable - /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -namespace Jint.Native.Number.Dtoa -{ - internal sealed class DtoaBuilder - { - // allocate buffer for generated digits + extra notation + padding zeroes - internal readonly char[] _chars; - internal int Length; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; - public DtoaBuilder(int size) - { - _chars = new char[size]; - } +namespace Jint.Native.Number.Dtoa; - public DtoaBuilder() : this(FastDtoa.KFastDtoaMaximalLength + 8) - { - } +[StructLayout(LayoutKind.Auto)] +internal ref struct DtoaBuilder +{ + // allocate buffer for generated digits + extra notation + padding zeroes + internal readonly Span _chars; + internal int Length; - internal void Append(char c) - { - _chars[Length++] = c; - } + public DtoaBuilder(Span initialBuffer) + { + _chars = initialBuffer; + } - internal void DecreaseLast() - { - _chars[Length - 1]--; - } + internal void Append(char c) + { + _chars[Length++] = c; + } - public void Reset() - { - Length = 0; - System.Array.Clear(_chars, 0, _chars.Length); - } + internal void DecreaseLast() + { + _chars[Length - 1]--; + } - public char this[int i] - { - get => _chars[i]; - set - { - _chars[i] = value; - Length = System.Math.Max(Length, i + 1); - } - } + public void Reset() + { + Length = 0; + _chars.Clear(); + } - public override string ToString() + public char this[int i] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _chars[i]; + set { - return "[chars:" + new string(_chars, 0, Length) + "]"; + _chars[i] = value; + Length = System.Math.Max(Length, i + 1); } } -} \ No newline at end of file + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan Slice(int start, int length) => _chars.Slice(start, length); + + public override string ToString() => "[chars:" + _chars.Slice(0, Length).ToString() + "]"; +} diff --git a/Jint/Native/Number/Dtoa/DtoaNumberFormatter.cs b/Jint/Native/Number/Dtoa/DtoaNumberFormatter.cs index 06b9a2b07a..310f72391a 100644 --- a/Jint/Native/Number/Dtoa/DtoaNumberFormatter.cs +++ b/Jint/Native/Number/Dtoa/DtoaNumberFormatter.cs @@ -8,7 +8,7 @@ namespace Jint.Native.Number.Dtoa internal static class DtoaNumberFormatter { public static void DoubleToAscii( - DtoaBuilder buffer, + ref DtoaBuilder buffer, double v, DtoaMode mode, int requested_digits, @@ -44,14 +44,14 @@ public static void DoubleToAscii( bool fast_worked = false; switch (mode) { case DtoaMode.Shortest: - fast_worked = FastDtoa.NumberToString(v, DtoaMode.Shortest, 0, out point, buffer); + 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); ExceptionHelper.ThrowNotImplementedException(); break; case DtoaMode.Precision: - fast_worked = FastDtoa.NumberToString(v, DtoaMode.Precision, requested_digits, out point, buffer); + fast_worked = FastDtoa.NumberToString(v, DtoaMode.Precision, requested_digits, out point, ref buffer); break; default: ExceptionHelper.ThrowArgumentOutOfRangeException(); @@ -65,7 +65,7 @@ public static void DoubleToAscii( // If the fast dtoa didn't succeed use the slower bignum version. buffer.Reset(); - BignumDtoa.NumberToString(v, mode, requested_digits, buffer, out point); + BignumDtoa.NumberToString(v, mode, requested_digits, ref buffer, out point); } } -} \ No newline at end of file +} diff --git a/Jint/Native/Number/Dtoa/FastDtoa.cs b/Jint/Native/Number/Dtoa/FastDtoa.cs index fed935103c..8846eb4b83 100644 --- a/Jint/Native/Number/Dtoa/FastDtoa.cs +++ b/Jint/Native/Number/Dtoa/FastDtoa.cs @@ -64,7 +64,7 @@ internal sealed class FastDtoa // representable number to the input. // Modifies the generated digits in the buffer to approach (round towards) w. private static bool RoundWeed( - DtoaBuilder buffer, + ref DtoaBuilder buffer, ulong distanceTooHighW, ulong unsafeInterval, ulong rest, @@ -183,7 +183,7 @@ private static bool RoundWeed( // // Precondition: rest < ten_kappa. static bool RoundWeedCounted( - DtoaBuilder buffer, + ref DtoaBuilder buffer, ulong rest, ulong ten_kappa, ulong unit, @@ -413,7 +413,7 @@ private static bool DigitGen( in DiyFp low, in DiyFp w, in DiyFp high, - DtoaBuilder buffer, + ref DtoaBuilder buffer, int mk, out int kappa) { @@ -473,7 +473,7 @@ private static bool DigitGen( // Rounding down (by not emitting the remaining digits) yields a number // that lies within the unsafe interval. return RoundWeed( - buffer, + ref buffer, DiyFp.Minus(tooHigh, w).F, unsafeInterval.F, rest, @@ -511,7 +511,7 @@ private static bool DigitGen( if (fractionals < unsafeInterval.F) { return RoundWeed( - buffer, + ref buffer, DiyFp.Minus(tooHigh, w).F*unit, unsafeInterval.F, fractionals, @@ -552,7 +552,7 @@ private static bool DigitGen( static bool DigitGenCounted( in DiyFp w, int requested_digits, - DtoaBuilder buffer, + ref DtoaBuilder buffer, out int kappa) { Debug.Assert(MinimalTargetExponent <= w.E && w.E <= MaximalTargetExponent); @@ -592,7 +592,7 @@ static bool DigitGenCounted( if (requested_digits == 0) { ulong rest = (((ulong) integrals) << -one.E) + fractionals; - return RoundWeedCounted(buffer, rest,(ulong) divisor << -one.E, w_error, ref kappa); + return RoundWeedCounted(ref buffer, rest,(ulong) divisor << -one.E, w_error, ref kappa); } // The integrals have been generated. We are at the point of the decimal @@ -615,7 +615,7 @@ static bool DigitGenCounted( (kappa)--; } if (requested_digits != 0) return false; - return RoundWeedCounted(buffer, fractionals, one.F, w_error, ref kappa); + return RoundWeedCounted(ref buffer, fractionals, one.F, w_error, ref kappa); } // Provides a decimal representation of v. @@ -629,7 +629,7 @@ static bool DigitGenCounted( // The last digit will be closest to the actual v. That is, even if several // digits might correctly yield 'v' when read again, the closest will be // computed. - private static bool Grisu3(double v, DtoaBuilder buffer, out int decimal_exponent) + private static bool Grisu3(double v, ref DtoaBuilder buffer, out int decimal_exponent) { ulong bits = (ulong) BitConverter.DoubleToInt64Bits(v); DiyFp w = DoubleHelper.AsNormalizedDiyFp(bits); @@ -681,7 +681,7 @@ private static bool Grisu3(double v, DtoaBuilder buffer, out int decimal_exponen // the buffer will be filled with "123" und the decimal_exponent will be // decreased by 2. int kappa; - var digitGen = DigitGen(scaledBoundaryMinus, scaledW, scaledBoundaryPlus, buffer, mk, out kappa); + var digitGen = DigitGen(scaledBoundaryMinus, scaledW, scaledBoundaryPlus, ref buffer, mk, out kappa); decimal_exponent = -mk + kappa; return digitGen; } @@ -695,7 +695,7 @@ private static bool Grisu3(double v, DtoaBuilder buffer, out int decimal_exponen static bool Grisu3Counted( double v, int requested_digits, - DtoaBuilder buffer, + ref DtoaBuilder buffer, out int decimal_exponent) { ulong bits = (ulong) BitConverter.DoubleToInt64Bits(v); @@ -725,7 +725,7 @@ static bool Grisu3Counted( // return together with a kappa such that scaled_w ~= buffer * 10^kappa. (It // will not always be exactly the same since DigitGenCounted only produces a // limited number of digits.) - bool result = DigitGenCounted(scaled_w, requested_digits, buffer, out var kappa); + bool result = DigitGenCounted(scaled_w, requested_digits, ref buffer, out var kappa); decimal_exponent = -mk + kappa; return result; } @@ -735,7 +735,7 @@ public static bool NumberToString( DtoaMode mode, int requested_digits, out int decimal_point, - DtoaBuilder buffer) + ref DtoaBuilder buffer) { Debug.Assert(v > 0); Debug.Assert(!double.IsNaN(v)); @@ -746,10 +746,10 @@ public static bool NumberToString( switch (mode) { case DtoaMode.Shortest: - result = Grisu3(v, buffer, out decimal_exponent); + result = Grisu3(v, ref buffer, out decimal_exponent); break; case DtoaMode.Precision: - result = Grisu3Counted(v, requested_digits, buffer, out decimal_exponent); + result = Grisu3Counted(v, requested_digits, ref buffer, out decimal_exponent); break; default: ExceptionHelper.ThrowArgumentOutOfRangeException(); diff --git a/Jint/Native/Number/NumberPrototype.cs b/Jint/Native/Number/NumberPrototype.cs index 186be11282..8e4f8cdeba 100644 --- a/Jint/Native/Number/NumberPrototype.cs +++ b/Jint/Native/Number/NumberPrototype.cs @@ -4,7 +4,6 @@ using Jint.Collections; using Jint.Native.Number.Dtoa; using Jint.Native.Object; -using Jint.Pooling; using Jint.Runtime; using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; @@ -16,6 +15,9 @@ namespace Jint.Native.Number /// internal sealed class NumberPrototype : NumberInstance { + private const int SmallDtoaLength = FastDtoa.KFastDtoaMaximalLength + 8; + private const int LargeDtoaLength = 101; + private readonly Realm _realm; private readonly NumberConstructor _constructor; @@ -201,12 +203,12 @@ private JsValue ToExponential(JsValue thisObject, JsValue[] arguments) } int decimalPoint; - DtoaBuilder dtoaBuilder; + var dtoaBuilder = new DtoaBuilder(stackalloc char[f == -1 ? SmallDtoaLength : LargeDtoaLength]); + if (f == -1) { - dtoaBuilder = new DtoaBuilder(); DtoaNumberFormatter.DoubleToAscii( - dtoaBuilder, + ref dtoaBuilder, x, DtoaMode.Shortest, requested_digits: 0, @@ -216,9 +218,8 @@ private JsValue ToExponential(JsValue thisObject, JsValue[] arguments) } else { - dtoaBuilder = new DtoaBuilder(101); DtoaNumberFormatter.DoubleToAscii( - dtoaBuilder, + ref dtoaBuilder, x, DtoaMode.Precision, requested_digits: f + 1, @@ -230,7 +231,7 @@ private JsValue ToExponential(JsValue thisObject, JsValue[] arguments) Debug.Assert(dtoaBuilder.Length <= f + 1); int exponent = decimalPoint - 1; - var result = CreateExponentialRepresentation(dtoaBuilder, exponent, negative, f+1); + var result = CreateExponentialRepresentation(ref dtoaBuilder, exponent, negative, f+1); return result; } @@ -266,9 +267,9 @@ private JsValue ToPrecision(JsValue thisObject, JsValue[] arguments) ExceptionHelper.ThrowRangeError(_realm, "precision must be between 1 and 100"); } - var dtoaBuilder = new DtoaBuilder(101); + var dtoaBuilder = new DtoaBuilder(stackalloc char[LargeDtoaLength]); DtoaNumberFormatter.DoubleToAscii( - dtoaBuilder, + ref dtoaBuilder, x, DtoaMode.Precision, p, @@ -279,50 +280,49 @@ private JsValue ToPrecision(JsValue thisObject, JsValue[] arguments) int exponent = decimalPoint - 1; if (exponent < -6 || exponent >= p) { - return CreateExponentialRepresentation(dtoaBuilder, exponent, negative, p); + return CreateExponentialRepresentation(ref dtoaBuilder, exponent, negative, p); } - using (var builder = StringBuilderPool.Rent()) + var sb = new ValueStringBuilder(stackalloc char[128]); + + // Use fixed notation. + if (negative) { - // Use fixed notation. - if (negative) - { - builder.Builder.Append('-'); - } + sb.Append('-'); + } - if (decimalPoint <= 0) - { - builder.Builder.Append("0."); - builder.Builder.Append('0', -decimalPoint); - builder.Builder.Append(dtoaBuilder._chars, 0, dtoaBuilder.Length); - builder.Builder.Append('0', p - dtoaBuilder.Length); - } - else + if (decimalPoint <= 0) + { + sb.Append("0."); + sb.Append('0', -decimalPoint); + sb.Append(dtoaBuilder._chars.Slice(0, dtoaBuilder.Length)); + sb.Append('0', p - dtoaBuilder.Length); + } + else + { + int m = System.Math.Min(dtoaBuilder.Length, decimalPoint); + sb.Append(dtoaBuilder._chars.Slice(0, m)); + sb.Append('0', System.Math.Max(0, decimalPoint - dtoaBuilder.Length)); + if (decimalPoint < p) { - int m = System.Math.Min(dtoaBuilder.Length, decimalPoint); - builder.Builder.Append(dtoaBuilder._chars, 0, m); - builder.Builder.Append('0', System.Math.Max(0, decimalPoint - dtoaBuilder.Length)); - if (decimalPoint < p) + sb.Append('.'); + var extra = negative ? 2 : 1; + if (dtoaBuilder.Length > decimalPoint) { - builder.Builder.Append('.'); - var extra = negative ? 2 : 1; - if (dtoaBuilder.Length > decimalPoint) - { - int len = dtoaBuilder.Length - decimalPoint; - int n = System.Math.Min(len, p - (builder.Builder.Length - extra)); - builder.Builder.Append(dtoaBuilder._chars, decimalPoint, n); - } - - builder.Builder.Append('0', System.Math.Max(0, extra + (p - builder.Builder.Length))); + int len = dtoaBuilder.Length - decimalPoint; + int n = System.Math.Min(len, p - (sb.Length - extra)); + sb.Append(dtoaBuilder._chars.Slice(decimalPoint, n)); } - } - return builder.ToString(); + sb.Append('0', System.Math.Max(0, extra + (p - sb.Length))); + } } + + return sb.ToString(); } private static string CreateExponentialRepresentation( - DtoaBuilder buffer, + ref DtoaBuilder buffer, int exponent, bool negative, int significantDigits) @@ -334,26 +334,25 @@ private static string CreateExponentialRepresentation( exponent = -exponent; } - using (var builder = StringBuilderPool.Rent()) + var sb = new ValueStringBuilder(stackalloc char[128]); + if (negative) { - if (negative) - { - builder.Builder.Append('-'); - } - builder.Builder.Append(buffer._chars[0]); - if (significantDigits != 1) - { - builder.Builder.Append('.'); - builder.Builder.Append(buffer._chars, 1, buffer.Length - 1); - int length = buffer.Length; - builder.Builder.Append('0', significantDigits - length); - } - - builder.Builder.Append('e'); - builder.Builder.Append(negativeExponent ? '-' : '+'); - builder.Builder.Append(exponent); - return builder.ToString(); + sb.Append('-'); } + sb.Append(buffer[0]); + if (significantDigits != 1) + { + sb.Append('.'); + sb.Append(buffer.Slice(1, buffer.Length - 1)); + int length = buffer.Length; + sb.Append('0', significantDigits - length); + } + + sb.Append('e'); + sb.Append(negativeExponent ? '-' : '+'); + sb.Append(exponent.ToString(CultureInfo.InvariantCulture)); + + return sb.ToString(); } private JsValue ToNumberString(JsValue thisObject, JsValue[] arguments) @@ -419,15 +418,15 @@ internal static string ToBase(long n, int radix) return "0"; } - using var result = StringBuilderPool.Rent(); + var sb = new ValueStringBuilder(stackalloc char[64]); while (n > 0) { var digit = (int) (n % radix); - n = n / radix; - result.Builder.Insert(0, Digits[digit]); + n /= radix; + sb.Append(Digits[digit]); } - - return result.ToString(); + sb.Reverse(); + return sb.ToString(); } internal static string ToFractionBase(double n, int radix) @@ -441,57 +440,48 @@ internal static string ToFractionBase(double n, int radix) return "0"; } - using var result = StringBuilderPool.Rent(); + var result = new ValueStringBuilder(stackalloc char[64]); while (n > 0 && result.Length < 50) // arbitrary limit { var c = n*radix; var d = (int) c; n = c - d; - result.Builder.Append(Digits[d]); + result.Append(Digits[d]); } return result.ToString(); } - private static string ToNumberString(double m) - { - using var stringBuilder = StringBuilderPool.Rent(); - NumberToString(m, new DtoaBuilder(), stringBuilder.Builder); - return stringBuilder.Builder.ToString(); - } - - internal static void NumberToString( - double m, - DtoaBuilder builder, - StringBuilder stringBuilder) + internal static string ToNumberString(double m) { if (double.IsNaN(m)) { - stringBuilder.Append("NaN"); - return; + return "NaN"; } if (m == 0) { - stringBuilder.Append('0'); - return; + return "0"; } if (double.IsInfinity(m)) { - stringBuilder.Append(double.IsNegativeInfinity(m) ? "-Infinity" : "Infinity"); - return; + return double.IsNegativeInfinity(m) ? "-Infinity" : "Infinity"; } + var builder = new DtoaBuilder(stackalloc char[SmallDtoaLength]); + DtoaNumberFormatter.DoubleToAscii( - builder, + ref builder, m, DtoaMode.Shortest, 0, out var negative, out var decimal_point); + + var stringBuilder = new ValueStringBuilder(stackalloc char[64]); if (negative) { stringBuilder.Append('-'); @@ -500,22 +490,22 @@ internal static void NumberToString( if (builder.Length <= decimal_point && decimal_point <= 21) { // ECMA-262 section 9.8.1 step 6. - stringBuilder.Append(builder._chars, 0, builder.Length); + stringBuilder.Append(builder._chars.Slice(0, builder.Length)); stringBuilder.Append('0', decimal_point - builder.Length); } else if (0 < decimal_point && decimal_point <= 21) { // ECMA-262 section 9.8.1 step 7. - stringBuilder.Append(builder._chars, 0, decimal_point); + stringBuilder.Append(builder._chars.Slice(0, decimal_point)); stringBuilder.Append('.'); - stringBuilder.Append(builder._chars, decimal_point, builder.Length - decimal_point); + stringBuilder.Append(builder._chars.Slice(decimal_point, builder.Length - decimal_point)); } else if (decimal_point <= 0 && decimal_point > -6) { // ECMA-262 section 9.8.1 step 8. stringBuilder.Append("0."); stringBuilder.Append('0', -decimal_point); - stringBuilder.Append(builder._chars, 0, builder.Length); + stringBuilder.Append(builder._chars.Slice(0, builder.Length)); } else { @@ -524,7 +514,7 @@ internal static void NumberToString( if (builder.Length != 1) { stringBuilder.Append('.'); - stringBuilder.Append(builder._chars, 1, builder.Length - 1); + stringBuilder.Append(builder._chars.Slice(1, builder.Length - 1)); } stringBuilder.Append('e'); @@ -535,8 +525,10 @@ internal static void NumberToString( exponent = -exponent; } - stringBuilder.Append(exponent); + stringBuilder.Append(exponent.ToString(CultureInfo.InvariantCulture)); } + + return stringBuilder.ToString(); } } } diff --git a/Jint/Native/RegExp/RegExpPrototype.cs b/Jint/Native/RegExp/RegExpPrototype.cs index 20f989a52b..53c4bdff7c 100644 --- a/Jint/Native/RegExp/RegExpPrototype.cs +++ b/Jint/Native/RegExp/RegExpPrototype.cs @@ -1,12 +1,12 @@ #pragma warning disable CA1859 // Use concrete types when possible for improved performance -- most of prototype methods return JsValue +using System.Text; using System.Text.RegularExpressions; using Jint.Collections; using Jint.Native.Number; using Jint.Native.Object; using Jint.Native.String; using Jint.Native.Symbol; -using Jint.Pooling; using Jint.Runtime; using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; @@ -337,8 +337,7 @@ internal static string GetSubstitution( // $` Inserts the portion of the string that precedes the matched substring. // $' Inserts the portion of the string that follows the matched substring. // $n or $nn Where n or nn are decimal digits, inserts the nth parenthesized submatch string, provided the first argument was a RegExp object. - using var replacementBuilder = StringBuilderPool.Rent(); - var sb = replacementBuilder.Builder; + using var sb = new ValueStringBuilder(stackalloc char[128]); for (var i = 0; i < replacement.Length; i++) { char c = replacement[i]; @@ -353,14 +352,12 @@ internal static string GetSubstitution( case '&': sb.Append(matched); break; -#pragma warning disable CA1846 case '`': - sb.Append(str.Substring(0, position)); + sb.Append(str.AsSpan(0, position)); break; case '\'': - sb.Append(str.Substring(position + matched.Length)); + sb.Append(str.AsSpan(position + matched.Length)); break; -#pragma warning restore CA1846 case '<': var gtPos = replacement.IndexOf('>', i + 1); if (gtPos == -1 || namedCaptures.IsUndefined()) @@ -430,7 +427,7 @@ internal static string GetSubstitution( } } - return replacementBuilder.ToString(); + return sb.ToString(); } /// diff --git a/Jint/Native/Set/JsSet.cs b/Jint/Native/Set/JsSet.cs index b5fb4d4553..ea95fccecb 100644 --- a/Jint/Native/Set/JsSet.cs +++ b/Jint/Native/Set/JsSet.cs @@ -9,9 +9,13 @@ internal sealed class JsSet : ObjectInstance { internal readonly OrderedSet _set; - public JsSet(Engine engine) : base(engine) + public JsSet(Engine engine) : this(engine, new OrderedSet(SameValueZeroComparer.Instance)) { - _set = new OrderedSet(SameValueZeroComparer.Instance); + } + + public JsSet(Engine engine, OrderedSet set) : base(engine) + { + _set = set; } public override PropertyDescriptor GetOwnProperty(JsValue property) diff --git a/Jint/Native/Set/SetConstructor.cs b/Jint/Native/Set/SetConstructor.cs index 2335432519..75b3656cbd 100644 --- a/Jint/Native/Set/SetConstructor.cs +++ b/Jint/Native/Set/SetConstructor.cs @@ -25,7 +25,7 @@ internal SetConstructor( _prototypeDescriptor = new PropertyDescriptor(PrototypeObject, PropertyFlag.AllForbidden); } - private SetPrototype PrototypeObject { get; } + internal SetPrototype PrototypeObject { get; } protected override void Initialize() { diff --git a/Jint/Native/Set/SetPrototype.cs b/Jint/Native/Set/SetPrototype.cs index 11e0753c52..b67d5d55e2 100644 --- a/Jint/Native/Set/SetPrototype.cs +++ b/Jint/Native/Set/SetPrototype.cs @@ -32,15 +32,16 @@ protected override void Initialize() { ["length"] = new PropertyDescriptor(0, PropertyFlag.Configurable), ["constructor"] = new PropertyDescriptor(_constructor, PropertyFlag.NonEnumerable), - ["add"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "add", Add, 1, PropertyFlag.Configurable), true, false, true), - ["clear"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "clear", Clear, 0, PropertyFlag.Configurable), true, false, true), - ["delete"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "delete", Delete, 1, PropertyFlag.Configurable), true, false, true), - ["entries"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "entries", Entries, 0, PropertyFlag.Configurable), true, false, true), - ["forEach"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "forEach", ForEach, 1, PropertyFlag.Configurable), true, false, true), - ["has"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "has", Has, 1, PropertyFlag.Configurable), true, false, true), - ["keys"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "keys", Values, 0, PropertyFlag.Configurable), true, false, true), - ["values"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "values", Values, 0, PropertyFlag.Configurable), true, false, true), - ["size"] = new GetSetPropertyDescriptor(get: new ClrFunctionInstance(Engine, "get size", Size, 0, PropertyFlag.Configurable), set: null, PropertyFlag.Configurable) + ["add"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "add", Add, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["clear"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "clear", Clear, 0, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["delete"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "delete", Delete, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["entries"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "entries", Entries, 0, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["forEach"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "forEach", ForEach, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["has"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "has", Has, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["keys"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "keys", Values, 0, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["values"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "values", Values, 0, PropertyFlag.Configurable), PropertyFlag.NonEnumerable), + ["size"] = new GetSetPropertyDescriptor(get: new ClrFunctionInstance(Engine, "get size", Size, 0, PropertyFlag.Configurable), set: null, PropertyFlag.Configurable), + ["union"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "union", Union, 1, PropertyFlag.Configurable), PropertyFlag.NonEnumerable) }; SetProperties(properties); @@ -112,6 +113,68 @@ private JsValue ForEach(JsValue thisObject, JsValue[] arguments) return Undefined; } + private JsValue Union(JsValue thisObject, JsValue[] arguments) + { + var set = AssertSetInstance(thisObject); + var other = arguments.At(0); + var otherRec = GetSetRecord(other); + var keysIter = otherRec.Set.GetIteratorFromMethod(_realm, otherRec.Keys); + var resultSetData = set._set.Clone(); + while (keysIter.TryIteratorStep(out var next)) + { + var nextValue = next.Get(CommonProperties.Value); + if (nextValue == JsNumber.NegativeZero) + { + nextValue = JsNumber.PositiveZero; + } + resultSetData.Add(nextValue); + } + + var result = new JsSet(_engine, resultSetData) + { + _prototype = _engine.Realm.Intrinsics.Set.PrototypeObject + }; + return result; + + } + + private readonly record struct SetRecord(JsValue Set, double Size, ICallable Has, ICallable Keys); + + private SetRecord GetSetRecord(JsValue obj) + { + if (obj is not ObjectInstance) + { + ExceptionHelper.ThrowTypeError(_realm); + } + + var rawSize = obj.Get("size"); + var numSize = TypeConverter.ToNumber(rawSize); + if (double.IsNaN(numSize)) + { + ExceptionHelper.ThrowTypeError(_realm); + } + + var intSize = TypeConverter.ToIntegerOrInfinity(numSize); + if (intSize < 0) + { + ExceptionHelper.ThrowRangeError(_realm); + } + + var has = obj.Get(CommonProperties.Has); + if (!has.IsCallable) + { + ExceptionHelper.ThrowTypeError(_realm); + } + + var keys = obj.Get(CommonProperties.Keys); + if (!keys.IsCallable) + { + ExceptionHelper.ThrowTypeError(_realm); + } + + return new SetRecord(Set: obj, Size: intSize, Has: (ICallable) has, Keys: (ICallable) keys); + } + private ObjectInstance Values(JsValue thisObject, JsValue[] arguments) { var set = AssertSetInstance(thisObject); diff --git a/Jint/Native/String/StringConstructor.cs b/Jint/Native/String/StringConstructor.cs index 5a30c3df72..36063335aa 100644 --- a/Jint/Native/String/StringConstructor.cs +++ b/Jint/Native/String/StringConstructor.cs @@ -1,10 +1,10 @@ #pragma warning disable CA1859 // Use concrete types when possible for improved performance -- most of prototype methods return JsValue +using System.Text; using Jint.Collections; using Jint.Native.Array; using Jint.Native.Function; using Jint.Native.Object; -using Jint.Pooling; using Jint.Runtime; using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; @@ -76,48 +76,60 @@ private static JsValue FromCharCode(JsValue? thisObj, JsValue[] arguments) return JsString.Create(new string(elements)); } + /// + /// https://tc39.es/ecma262/#sec-string.fromcodepoint + /// private JsValue FromCodePoint(JsValue thisObject, JsValue[] arguments) { - var codeUnits = new List(); - string result = ""; + JsNumber codePoint; + using var result = new ValueStringBuilder(stackalloc char[128]); foreach (var a in arguments) { - var codePoint = TypeConverter.ToNumber(a); - if (codePoint < 0 - || codePoint > 0x10FFFF - || double.IsInfinity(codePoint) - || double.IsNaN(codePoint) - || TypeConverter.ToInt32(codePoint) != codePoint) + int point; + codePoint = TypeConverter.ToJsNumber(a); + if (codePoint.IsInteger()) { - ExceptionHelper.ThrowRangeError(_realm, "Invalid code point " + codePoint); + point = (int) codePoint._value; + if (point is < 0 or > 0x10FFFF) + { + goto rangeError; + } + } + else + { + var pointTemp = codePoint._value; + if (pointTemp < 0 || pointTemp > 0x10FFFF || double.IsInfinity(pointTemp) || double.IsNaN(pointTemp) || TypeConverter.ToInt32(pointTemp) != pointTemp) + { + goto rangeError; + } + + point = (int) pointTemp; } - var point = (uint) codePoint; if (point <= 0xFFFF) { // BMP code point - codeUnits.Add(JsNumber.Create(point)); + result.Append((char) point); } else { // Astral code point; split in surrogate halves // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae point -= 0x10000; - codeUnits.Add(JsNumber.Create((point >> 10) + 0xD800)); // highSurrogate - codeUnits.Add(JsNumber.Create((point % 0x400) + 0xDC00)); // lowSurrogate - } - if (codeUnits.Count >= 0x3fff) - { - result += FromCharCode(null, codeUnits.ToArray()); - codeUnits.Clear(); + result.Append((char) ((point >> 10) + 0xD800)); // highSurrogate + result.Append((char) (point % 0x400 + 0xDC00)); // lowSurrogate } } - return result + FromCharCode(null, codeUnits.ToArray()); + return JsString.Create(result.ToString()); + + rangeError: + _engine.SignalError(ExceptionHelper.CreateRangeError(_realm, "Invalid code point " + codePoint)); + return null!; } /// - /// https://www.ecma-international.org/ecma-262/6.0/#sec-string.raw + /// https://tc39.es/ecma262/#sec-string.raw /// private JsValue Raw(JsValue thisObject, JsValue[] arguments) { @@ -132,18 +144,18 @@ private JsValue Raw(JsValue thisObject, JsValue[] arguments) return JsString.Empty; } - using var result = StringBuilderPool.Rent(); + using var result = new ValueStringBuilder(); for (var i = 0; i < length; i++) { if (i > 0) { if (i < arguments.Length && !arguments[i].IsUndefined()) { - result.Builder.Append(TypeConverter.ToString(arguments[i])); + result.Append(TypeConverter.ToString(arguments[i])); } } - result.Builder.Append(TypeConverter.ToString(operations.Get((ulong) i))); + result.Append(TypeConverter.ToString(operations.Get((ulong) i))); } return result.ToString(); diff --git a/Jint/Native/String/StringPrototype.cs b/Jint/Native/String/StringPrototype.cs index 1cf2201bef..f203b4837e 100644 --- a/Jint/Native/String/StringPrototype.cs +++ b/Jint/Native/String/StringPrototype.cs @@ -9,7 +9,6 @@ using Jint.Native.Object; using Jint.Native.RegExp; using Jint.Native.Symbol; -using Jint.Pooling; using Jint.Runtime; using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; @@ -643,8 +642,7 @@ static int StringIndexOf(string s, string search, int fromIndex) var advanceBy = System.Math.Max(1, searchLength); var endOfLastMatch = 0; - using var pool = StringBuilderPool.Rent(); - var result = pool.Builder; + using var result = new ValueStringBuilder(); var position = StringIndexOf(thisString, searchString, 0); while (position != -1) @@ -662,7 +660,9 @@ static int StringIndexOf(string s, string search, int fromIndex) replacement = RegExpPrototype.GetSubstitution(searchString, thisString, position, captures, Undefined, TypeConverter.ToString(replaceValue)); } - result.Append(preserved).Append(replacement); + result.Append(preserved); + result.Append(replacement); + endOfLastMatch = position + searchLength; position = StringIndexOf(thisString, searchString, position + advanceBy); @@ -671,7 +671,7 @@ static int StringIndexOf(string s, string search, int fromIndex) if (endOfLastMatch < thisString.Length) { #if NETFRAMEWORK - result.Append(thisString.Substring(endOfLastMatch)); + result.Append(thisString.AsSpan(endOfLastMatch)); #else result.Append(thisString[endOfLastMatch..]); #endif @@ -1144,11 +1144,10 @@ private JsValue Repeat(JsValue thisObject, JsValue[] arguments) return new string(s[0], (int) n); } - using var sb = StringBuilderPool.Rent(); - sb.Builder.EnsureCapacity((int) (n * s.Length)); + var sb = new ValueStringBuilder((int) (n * s.Length)); for (var i = 0; i < n; ++i) { - sb.Builder.Append(s); + sb.Append(s); } return sb.ToString(); @@ -1170,8 +1169,7 @@ private JsValue ToWellFormed(JsValue thisObject, JsValue[] arguments) var strLen = s.Length; var k = 0; - using var builder = StringBuilderPool.Rent(); - var result = builder.Builder; + var result = new ValueStringBuilder(); while (k < strLen) { var cp = CodePointAt(s, k); @@ -1182,7 +1180,7 @@ private JsValue ToWellFormed(JsValue thisObject, JsValue[] arguments) } else { - result.Append(s, k, cp.CodeUnitCount); + result.Append(s.AsSpan(k, cp.CodeUnitCount)); } k += cp.CodeUnitCount; } diff --git a/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs b/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs index dff4978054..b494970002 100644 --- a/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs +++ b/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs @@ -1,6 +1,7 @@ #pragma warning disable CA1859 // Use concrete types when possible for improved performance -- most of prototype methods return JsValue using System.Linq; +using System.Text; using Jint.Collections; using Jint.Native.Array; using Jint.Native.ArrayBuffer; @@ -8,7 +9,6 @@ using Jint.Native.Number; using Jint.Native.Object; using Jint.Native.Symbol; -using Jint.Pooling; using Jint.Runtime; using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; @@ -637,15 +637,15 @@ static string StringFromJsValue(JsValue value) return s; } - using var sb = StringBuilderPool.Rent(); - sb.Builder.Append(s); + using var result = new ValueStringBuilder(); + result.Append(s); for (var k = 1; k < len; k++) { - sb.Builder.Append(sep); - sb.Builder.Append(StringFromJsValue(o[k])); + result.Append(sep); + result.Append(StringFromJsValue(o[k])); } - return sb.ToString(); + return result.ToString(); } /// diff --git a/Jint/Pooling/StringBuilderPool.cs b/Jint/Pooling/StringBuilderPool.cs deleted file mode 100644 index c5184d7ac0..0000000000 --- a/Jint/Pooling/StringBuilderPool.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Diagnostics; -using System.Text; - -namespace Jint.Pooling -{ - /// - /// Pooling of StringBuilder instances. - /// - internal static class StringBuilderPool - { - private static readonly ConcurrentObjectPool _pool; - - static StringBuilderPool() - { - _pool = new ConcurrentObjectPool(() => new StringBuilder()); - } - - public static BuilderWrapper Rent() - { - var builder = _pool.Allocate(); - Debug.Assert(builder.Length == 0); - return new BuilderWrapper(builder, _pool); - } - - internal readonly struct BuilderWrapper : IDisposable - { - public readonly StringBuilder Builder; - private readonly ConcurrentObjectPool _pool; - - public BuilderWrapper(StringBuilder builder, ConcurrentObjectPool pool) - { - Builder = builder; - _pool = pool; - } - - public int Length => Builder.Length; - - public override string ToString() - { - return Builder.ToString(); - } - - public void Dispose() - { - var builder = Builder; - - // do not store builders that are too large. - if (builder.Capacity <= 1024 * 1024) - { - builder.Clear(); - _pool.Free(builder); - } - else - { - _pool.ForgetTrackedObject(builder); - } - } - } - } -} diff --git a/Jint/Pooling/ValueStringBuilder.cs b/Jint/Pooling/ValueStringBuilder.cs new file mode 100644 index 0000000000..f5bd679329 --- /dev/null +++ b/Jint/Pooling/ValueStringBuilder.cs @@ -0,0 +1,336 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// ReSharper disable once CheckNamespace +namespace System.Text; + +internal ref struct ValueStringBuilder +{ + private char[]? _arrayToReturnToPool; + private Span _chars; + private int _pos; + + public ValueStringBuilder(Span initialBuffer) + { + _arrayToReturnToPool = null; + _chars = initialBuffer; + _pos = 0; + } + + public ValueStringBuilder(int initialCapacity) + { + _arrayToReturnToPool = ArrayPool.Shared.Rent(initialCapacity); + _chars = _arrayToReturnToPool; + _pos = 0; + } + + public int Length + { + get => _pos; + set + { + Debug.Assert(value >= 0); + Debug.Assert(value <= _chars.Length); + _pos = value; + } + } + + public int Capacity => _chars.Length; + + public void EnsureCapacity(int capacity) + { + // This is not expected to be called this with negative capacity + Debug.Assert(capacity >= 0); + + // If the caller has a bug and calls this with negative capacity, make sure to call Grow to throw an exception. + if ((uint)capacity > (uint)_chars.Length) + Grow(capacity - _pos); + } + + /// + /// Get a pinnable reference to the builder. + /// Does not ensure there is a null char after + /// This overload is pattern matched in the C# 7.3+ compiler so you can omit + /// the explicit method call, and write eg "fixed (char* c = builder)" + /// + public ref char GetPinnableReference() + { + return ref MemoryMarshal.GetReference(_chars); + } + + /// + /// Get a pinnable reference to the builder. + /// + /// Ensures that the builder has a null char after + public ref char GetPinnableReference(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return ref MemoryMarshal.GetReference(_chars); + } + + public ref char this[int index] + { + get + { + Debug.Assert(index < _pos); + return ref _chars[index]; + } + } + + public override string ToString() + { + string s = _chars.Slice(0, _pos).ToString(); + Dispose(); + return s; + } + + /// Returns the underlying storage of the builder. + public Span RawChars => _chars; + + /// + /// Returns a span around the contents of the builder. + /// + /// Ensures that the builder has a null char after + public ReadOnlySpan AsSpan(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return _chars.Slice(0, _pos); + } + + public ReadOnlySpan AsSpan() => _chars.Slice(0, _pos); + public ReadOnlySpan AsSpan(int start) => _chars.Slice(start, _pos - start); + public ReadOnlySpan AsSpan(int start, int length) => _chars.Slice(start, length); + + public void Reverse() + { + _chars.Slice(0, _pos).Reverse(); + } + + public bool TryCopyTo(Span destination, out int charsWritten) + { + if (_chars.Slice(0, _pos).TryCopyTo(destination)) + { + charsWritten = _pos; + Dispose(); + return true; + } + else + { + charsWritten = 0; + Dispose(); + return false; + } + } + + public void Insert(int index, char value, int count) + { + if (_pos > _chars.Length - count) + { + Grow(count); + } + + int remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + _chars.Slice(index, count).Fill(value); + _pos += count; + } + + public void Insert(int index, string? s) + { + if (s == null) + { + return; + } + + int count = s.Length; + + if (_pos > (_chars.Length - count)) + { + Grow(count); + } + + int remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + s +#if !NETCOREAPP + .AsSpan() +#endif + .CopyTo(_chars.Slice(index)); + _pos += count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(char c) + { + int pos = _pos; + Span chars = _chars; + if ((uint)pos < (uint)chars.Length) + { + chars[pos] = c; + _pos = pos + 1; + } + else + { + GrowAndAppend(c); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(string? s) + { + if (s == null) + { + return; + } + + int pos = _pos; + if (s.Length == 1 && (uint)pos < (uint)_chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc. + { + _chars[pos] = s[0]; + _pos = pos + 1; + } + else + { + AppendSlow(s); + } + } + + private void AppendSlow(string s) + { + int pos = _pos; + if (pos > _chars.Length - s.Length) + { + Grow(s.Length); + } + + s +#if !NETCOREAPP + .AsSpan() +#endif + .CopyTo(_chars.Slice(pos)); + _pos += s.Length; + } + + public void Append(char c, int count) + { + if (_pos > _chars.Length - count) + { + Grow(count); + } + + Span dst = _chars.Slice(_pos, count); + for (int i = 0; i < dst.Length; i++) + { + dst[i] = c; + } + _pos += count; + } + + public unsafe void Append(char* value, int length) + { + int pos = _pos; + if (pos > _chars.Length - length) + { + Grow(length); + } + + Span dst = _chars.Slice(_pos, length); + for (int i = 0; i < dst.Length; i++) + { + dst[i] = *value++; + } + _pos += length; + } + + public void Append(scoped ReadOnlySpan value) + { + int pos = _pos; + if (pos > _chars.Length - value.Length) + { + Grow(value.Length); + } + + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AppendSpan(int length) + { + int origPos = _pos; + if (origPos > _chars.Length - length) + { + Grow(length); + } + + _pos = origPos + length; + return _chars.Slice(origPos, length); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowAndAppend(char c) + { + Grow(1); + Append(c); + } + + /// + /// Resize the internal buffer either by doubling current buffer size or + /// by adding to + /// whichever is greater. + /// + /// + /// Number of chars requested beyond current position. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void Grow(int additionalCapacityBeyondPos) + { + Debug.Assert(additionalCapacityBeyondPos > 0); + Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); + + const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength + + // Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try + // to double the size if possible, bounding the doubling to not go beyond the max array length. + int newCapacity = (int)Math.Max( + (uint)(_pos + additionalCapacityBeyondPos), + Math.Min((uint)_chars.Length * 2, ArrayMaxLength)); + + // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative. + // This could also go negative if the actual required length wraps around. + char[] poolArray = ArrayPool.Shared.Rent(newCapacity); + + _chars.Slice(0, _pos).CopyTo(poolArray); + + char[]? toReturn = _arrayToReturnToPool; + _chars = _arrayToReturnToPool = poolArray; + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + char[]? toReturn = _arrayToReturnToPool; + this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } +} diff --git a/Jint/Runtime/CallStack/JintCallStack.cs b/Jint/Runtime/CallStack/JintCallStack.cs index d6a234791d..a44317c114 100644 --- a/Jint/Runtime/CallStack/JintCallStack.cs +++ b/Jint/Runtime/CallStack/JintCallStack.cs @@ -1,11 +1,11 @@ using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using System.Text; using Esprima; using Esprima.Ast; using Jint.Collections; using Jint.Native.Function; -using Jint.Pooling; using Jint.Runtime.Environments; using Jint.Runtime.Interpreter.Expressions; @@ -119,19 +119,17 @@ public override string ToString() internal string BuildCallStackString(Location location, int excludeTop = 0) { static void AppendLocation( - StringBuilder sb, + ref ValueStringBuilder sb, string shortDescription, in Location loc, in CallStackElement? element) { - sb - .Append(" at"); + sb.Append(" at"); if (!string.IsNullOrWhiteSpace(shortDescription)) { - sb - .Append(' ') - .Append(shortDescription); + sb.Append(' '); + sb.Append(shortDescription); } if (element?.Arguments is not null) @@ -151,24 +149,23 @@ static void AppendLocation( sb.Append(')'); } - sb - .Append(' ') - .Append(loc.Source) - .Append(':') - .Append(loc.End.Line) - .Append(':') - .Append(loc.Start.Column + 1) // report column number instead of index - .AppendLine(); + sb.Append(' '); + sb.Append(loc.Source); + sb.Append(':'); + sb.Append(loc.End.Line.ToString(CultureInfo.InvariantCulture)); + sb.Append(':'); + sb.Append((loc.Start.Column + 1).ToString(CultureInfo.InvariantCulture)); // report column number instead of index + sb.Append(Environment.NewLine); } - using var sb = StringBuilderPool.Rent(); + var builder = new ValueStringBuilder(); // stack is one frame behind function-wise when we start to process it from expression level var index = _stack._size - 1 - excludeTop; var element = index >= 0 ? _stack[index] : (CallStackElement?) null; var shortDescription = element?.ToString() ?? ""; - AppendLocation(sb.Builder, shortDescription, location, element); + AppendLocation(ref builder, shortDescription, location, element); location = element?.Location ?? default; index--; @@ -178,13 +175,17 @@ static void AppendLocation( element = index >= 0 ? _stack[index] : null; shortDescription = element?.ToString() ?? ""; - AppendLocation(sb.Builder, shortDescription, location, element); + AppendLocation(ref builder, shortDescription, location, element); location = element?.Location ?? default; index--; } - return sb.ToString().TrimEnd(); + var result = builder.AsSpan().TrimEnd().ToString(); + + builder.Dispose(); + + return result; } /// diff --git a/Jint/Runtime/CommonProperties.cs b/Jint/Runtime/CommonProperties.cs index ee2e6bcd08..e7537d75e4 100644 --- a/Jint/Runtime/CommonProperties.cs +++ b/Jint/Runtime/CommonProperties.cs @@ -1,29 +1,30 @@ using Jint.Native; -namespace Jint.Runtime +namespace Jint.Runtime; + +internal static class CommonProperties { - internal static class CommonProperties - { - internal static readonly JsString Arguments = JsString.CachedCreate("arguments"); - internal static readonly JsString Caller = JsString.CachedCreate("caller"); - internal static readonly JsString Callee = JsString.CachedCreate("callee"); - internal static readonly JsString Constructor = JsString.CachedCreate("constructor"); - internal static readonly JsString Eval = JsString.CachedCreate("eval"); - internal static readonly JsString Infinity = JsString.CachedCreate("Infinity"); - internal static readonly JsString Length = JsString.CachedCreate("length"); - internal static readonly JsString Name = JsString.CachedCreate("name"); - internal static readonly JsString Prototype = JsString.CachedCreate("prototype"); - internal static readonly JsString Size = JsString.CachedCreate("size"); - internal static readonly JsString Next = JsString.CachedCreate("next"); - internal static readonly JsString Done = JsString.CachedCreate("done"); - internal static readonly JsString Value = JsString.CachedCreate("value"); - internal static readonly JsString Return = JsString.CachedCreate("return"); - internal static readonly JsString Set = JsString.CachedCreate("set"); - internal static readonly JsString Get = JsString.CachedCreate("get"); - internal static readonly JsString Writable = JsString.CachedCreate("writable"); - internal static readonly JsString Enumerable = JsString.CachedCreate("enumerable"); - internal static readonly JsString Configurable = JsString.CachedCreate("configurable"); - internal static readonly JsString Stack = JsString.CachedCreate("stack"); - internal static readonly JsString Message = JsString.CachedCreate("message"); - } + internal static readonly JsString Arguments = JsString.CachedCreate("arguments"); + internal static readonly JsString Callee = JsString.CachedCreate("callee"); + internal static readonly JsString Caller = JsString.CachedCreate("caller"); + internal static readonly JsString Configurable = JsString.CachedCreate("configurable"); + internal static readonly JsString Constructor = JsString.CachedCreate("constructor"); + internal static readonly JsString Done = JsString.CachedCreate("done"); + internal static readonly JsString Enumerable = JsString.CachedCreate("enumerable"); + internal static readonly JsString Eval = JsString.CachedCreate("eval"); + internal static readonly JsString Get = JsString.CachedCreate("get"); + internal static readonly JsString Has = JsString.CachedCreate("has"); + internal static readonly JsString Infinity = JsString.CachedCreate("Infinity"); + internal static readonly JsString Keys = JsString.CachedCreate("keys"); + internal static readonly JsString Length = JsString.CachedCreate("length"); + internal static readonly JsString Message = JsString.CachedCreate("message"); + internal static readonly JsString Name = JsString.CachedCreate("name"); + internal static readonly JsString Next = JsString.CachedCreate("next"); + internal static readonly JsString Prototype = JsString.CachedCreate("prototype"); + internal static readonly JsString Return = JsString.CachedCreate("return"); + internal static readonly JsString Set = JsString.CachedCreate("set"); + internal static readonly JsString Size = JsString.CachedCreate("size"); + internal static readonly JsString Stack = JsString.CachedCreate("stack"); + internal static readonly JsString Value = JsString.CachedCreate("value"); + internal static readonly JsString Writable = JsString.CachedCreate("writable"); } diff --git a/Jint/Runtime/DefaultTimeSystem.cs b/Jint/Runtime/DefaultTimeSystem.cs index 72f348abe2..001a2ecd68 100644 --- a/Jint/Runtime/DefaultTimeSystem.cs +++ b/Jint/Runtime/DefaultTimeSystem.cs @@ -51,6 +51,11 @@ public DefaultTimeSystem(TimeZoneInfo timeZoneInfo, CultureInfo parsingCulture) DefaultTimeZone = timeZoneInfo; } + public virtual DateTimeOffset GetUtcNow() + { + return DateTimeOffset.UtcNow; + } + public TimeZoneInfo DefaultTimeZone { get; } public virtual bool TryParse(string date, out long epochMilliseconds) diff --git a/Jint/Runtime/ExceptionHelper.cs b/Jint/Runtime/ExceptionHelper.cs index e47bb5a518..475edbdd70 100644 --- a/Jint/Runtime/ExceptionHelper.cs +++ b/Jint/Runtime/ExceptionHelper.cs @@ -87,6 +87,11 @@ public static ErrorDispatchInfo CreateUriError(Realm realm, string message) return new ErrorDispatchInfo(realm.Intrinsics.UriError, message); } + public static ErrorDispatchInfo CreateRangeError(Realm realm, string message) + { + return new ErrorDispatchInfo(realm.Intrinsics.RangeError, message); + } + [DoesNotReturn] public static void ThrowNotImplementedException(string? message = null) { diff --git a/Jint/Runtime/ITimeSystem.cs b/Jint/Runtime/ITimeSystem.cs index e691a4c7c2..07729dec1a 100644 --- a/Jint/Runtime/ITimeSystem.cs +++ b/Jint/Runtime/ITimeSystem.cs @@ -10,6 +10,12 @@ namespace Jint.Runtime; /// public interface ITimeSystem { + /// + /// Retrieves current UTC time. + /// + /// Current UTC time. + DateTimeOffset GetUtcNow(); + /// /// Return the default time zone system is using. Usually , but can be altered via /// engine configuration, see . diff --git a/Jint/Runtime/Interpreter/Expressions/JintExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintExpression.cs index fe78d83ef8..a4269ca8e1 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintExpression.cs @@ -34,7 +34,7 @@ public virtual JsValue GetValue(EvaluationContext context) return context.Engine.GetValue(reference, true); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions) 512)] public object Evaluate(EvaluationContext context) { var oldSyntaxElement = context.LastSyntaxElement; diff --git a/Jint/Runtime/Interpreter/Expressions/JintTemplateLiteralExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintTemplateLiteralExpression.cs index e3b7688ac4..8b0b2157bf 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintTemplateLiteralExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintTemplateLiteralExpression.cs @@ -1,6 +1,6 @@ +using System.Text; using Esprima.Ast; using Jint.Native; -using Jint.Pooling; namespace Jint.Runtime.Interpreter.Expressions; @@ -40,16 +40,16 @@ protected override object EvaluateInternal(EvaluationContext context) _initialized = true; } - using var sb = StringBuilderPool.Rent(); + using var sb = new ValueStringBuilder(); ref readonly var elements = ref _templateLiteralExpression.Quasis; for (var i = 0; i < elements.Count; i++) { var quasi = elements[i]; - sb.Builder.Append(quasi.Value.Cooked); + sb.Append(quasi.Value.Cooked); if (i < _expressions.Length) { var value = _expressions[i].GetValue(context); - sb.Builder.Append(TypeConverter.ToString(value)); + sb.Append(TypeConverter.ToString(value)); } } diff --git a/Jint/Runtime/Interpreter/JintFunctionDefinition.cs b/Jint/Runtime/Interpreter/JintFunctionDefinition.cs index 774ca65364..fadc34018b 100644 --- a/Jint/Runtime/Interpreter/JintFunctionDefinition.cs +++ b/Jint/Runtime/Interpreter/JintFunctionDefinition.cs @@ -33,7 +33,7 @@ public JintFunctionDefinition(IFunction function) /// /// https://tc39.es/ecma262/#sec-ordinarycallevaluatebody /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions) 512)] internal Completion EvaluateBody(EvaluationContext context, FunctionInstance functionObject, JsValue[] argumentsList) { Completion result; diff --git a/Jint/Runtime/Interpreter/JintStatementList.cs b/Jint/Runtime/Interpreter/JintStatementList.cs index 52bd55b0a0..0b199d4008 100644 --- a/Jint/Runtime/Interpreter/JintStatementList.cs +++ b/Jint/Runtime/Interpreter/JintStatementList.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using Esprima.Ast; using Jint.Native; using Jint.Native.Error; @@ -63,6 +64,8 @@ private void Initialize(EvaluationContext context) _jintStatements = jintStatements; } + + [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions) 512)] public Completion Execute(EvaluationContext context) { if (!_initialized) diff --git a/Jint/Runtime/Interpreter/Statements/JintStatement.cs b/Jint/Runtime/Interpreter/Statements/JintStatement.cs index 1f59a6640d..9832e62d47 100644 --- a/Jint/Runtime/Interpreter/Statements/JintStatement.cs +++ b/Jint/Runtime/Interpreter/Statements/JintStatement.cs @@ -26,7 +26,7 @@ protected JintStatement(Statement statement) _statement = statement; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions) 512)] public Completion Execute(EvaluationContext context) { if (_statement.Type != Nodes.BlockStatement) diff --git a/Jint/Runtime/JavaScriptException.cs b/Jint/Runtime/JavaScriptException.cs index d285606da4..d79b546062 100644 --- a/Jint/Runtime/JavaScriptException.cs +++ b/Jint/Runtime/JavaScriptException.cs @@ -1,8 +1,8 @@ +using System.Text; using Esprima; using Jint.Native; using Jint.Native.Error; using Jint.Native.Object; -using Jint.Pooling; using Jint.Runtime.Descriptors; namespace Jint.Runtime; @@ -132,8 +132,7 @@ public override string? StackTrace public override string ToString() { - using var rent = StringBuilderPool.Rent(); - var sb = rent.Builder; + var sb = new ValueStringBuilder(); sb.Append("Error"); var message = Message; @@ -150,7 +149,7 @@ public override string ToString() sb.Append(stackTrace); } - return rent.ToString(); + return sb.ToString(); } } } diff --git a/Jint/Runtime/OrderedSet.cs b/Jint/Runtime/OrderedSet.cs index 251cdd61a9..9db1ca1220 100644 --- a/Jint/Runtime/OrderedSet.cs +++ b/Jint/Runtime/OrderedSet.cs @@ -1,50 +1,58 @@ -namespace Jint.Runtime +namespace Jint.Runtime; + +internal sealed class OrderedSet { - internal sealed class OrderedSet - { - internal readonly List _list; - private readonly HashSet _set; + internal List _list; + private HashSet _set; - public OrderedSet(IEqualityComparer comparer) - { - _list = new List(); - _set = new HashSet(comparer); - } + public OrderedSet(IEqualityComparer comparer) + { + _list = new List(); + _set = new HashSet(comparer); + } - public T this[int index] + public T this[int index] + { + get => _list[index]; + set { - get => _list[index]; - set + if (_set.Add(value)) { - if (_set.Add(value)) - { - _list[index] = value; - } + _list[index] = value; } } + } - public void Add(T item) + public OrderedSet Clone() + { + return new OrderedSet(EqualityComparer.Default) { - if (_set.Add(item)) - { - _list.Add(item); - } - } + _set = new HashSet(this._set, this._set.Comparer), + _list = new List(this._list) + }; + } - public void Clear() + public void Add(T item) + { + if (_set.Add(item)) { - _list.Clear(); - _set.Clear(); + _list.Add(item); } + } + + public void Clear() + { + _list.Clear(); + _set.Clear(); + } - public bool Contains(T item) => _set.Contains(item); + public bool Contains(T item) => _set.Contains(item); - public int Count => _list.Count; + public int Count => _list.Count; - public bool Remove(T item) - { - _set.Remove(item); - return _list.Remove(item); - } + public bool Remove(T item) + { + _set.Remove(item); + return _list.Remove(item); } } diff --git a/Jint/Runtime/TypeConverter.cs b/Jint/Runtime/TypeConverter.cs index 381f22bf4d..f434d79a33 100644 --- a/Jint/Runtime/TypeConverter.cs +++ b/Jint/Runtime/TypeConverter.cs @@ -7,11 +7,9 @@ using Jint.Extensions; using Jint.Native; using Jint.Native.Number; -using Jint.Native.Number.Dtoa; using Jint.Native.Object; using Jint.Native.String; using Jint.Native.Symbol; -using Jint.Pooling; using Jint.Runtime.Interop; namespace Jint.Runtime @@ -896,20 +894,7 @@ internal static string ToString(double d) return ToString((long) d); } - using var stringBuilder = StringBuilderPool.Rent(); - // we can create smaller array as we know the format to be short - NumberPrototype.NumberToString(d, CreateDtoaBuilderForDouble(), stringBuilder.Builder); - return stringBuilder.Builder.ToString(); - } - - /// - /// Creates a new with the default buffer - /// size for - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static DtoaBuilder CreateDtoaBuilderForDouble() - { - return new DtoaBuilder(17); + return NumberPrototype.ToNumberString(d); } ///