From 37f7e4921188898eb0cba8b8959367cd906dfef8 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Thu, 26 Mar 2026 09:56:36 +0200 Subject: [PATCH] Fix compat-table failures: Array.at, AnnexB if-decl, Proxy observable gets - Fix Array.prototype.at() returning wrong value for negative out-of-bounds indices by changing ulong to long (C# casts negative double to ulong as 0) - Fix AnnexB if-decl-else-decl with same function name in both branches by not adding to effectiveLexicalNames for IfStatement siblings - Use RegExpCreate instead of RegExp.Construct in String.prototype.match/search to avoid extra IsRegExp call (observable Symbol.match access through Proxy) - Remove observable sticky property read in RegExp.prototype[Symbol.replace] fast path by deriving from already-read flags string - Fix String.prototype.split to use direct JsRegExp check instead of IsRegExp to avoid observable Symbol.match access Co-Authored-By: Claude Opus 4.6 (1M context) --- Jint/HoistingScope.cs | 19 ++++++++++++------- Jint/Native/Array/ArrayPrototype.cs | 10 +++++----- Jint/Native/RegExp/RegExpConstructor.cs | 11 +++++++++++ Jint/Native/RegExp/RegExpPrototype.cs | 3 ++- Jint/Native/String/StringPrototype.cs | 8 +++++--- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/Jint/HoistingScope.cs b/Jint/HoistingScope.cs index eb61fa7d42..6bba4e2dc6 100644 --- a/Jint/HoistingScope.cs +++ b/Jint/HoistingScope.cs @@ -319,15 +319,20 @@ private void VisitCore(Node node, Node? parent, HashSet? enclosingLexica // Track this function name as a lexical binding so inner scopes // can't AnnexB-hoist a same-named function (replacing with var // would conflict with this block-level lexical binding). - if (effectiveLexicalNames is null) + // Skip for IfStatement branches - sibling branches (consequent/alternate) + // are not nested scopes and should not block each other. + if (node.Type is not NodeType.IfStatement) { - effectiveLexicalNames = new HashSet(StringComparer.Ordinal); + if (effectiveLexicalNames is null) + { + effectiveLexicalNames = new HashSet(StringComparer.Ordinal); + } + else if (ReferenceEquals(effectiveLexicalNames, enclosingLexicalNames)) + { + effectiveLexicalNames = new HashSet(enclosingLexicalNames, StringComparer.Ordinal); + } + effectiveLexicalNames.Add(fnName); } - else if (ReferenceEquals(effectiveLexicalNames, enclosingLexicalNames)) - { - effectiveLexicalNames = new HashSet(enclosingLexicalNames, StringComparer.Ordinal); - } - effectiveLexicalNames.Add(fnName); } } } diff --git a/Jint/Native/Array/ArrayPrototype.cs b/Jint/Native/Array/ArrayPrototype.cs index 7055712260..80edc2a9dd 100644 --- a/Jint/Native/Array/ArrayPrototype.cs +++ b/Jint/Native/Array/ArrayPrototype.cs @@ -890,22 +890,22 @@ private JsValue At(JsValue thisObject, JsCallArguments arguments) var len = target.GetLength(); var relativeIndex = TypeConverter.ToInteger(arguments.At(0)); - ulong actualIndex; + long actualIndex; if (relativeIndex < 0) { - actualIndex = (ulong) (len + relativeIndex); + actualIndex = (long) (len + relativeIndex); } else { - actualIndex = (ulong) relativeIndex; + actualIndex = (long) relativeIndex; } - if (actualIndex < 0 || actualIndex >= len) + if (actualIndex < 0 || (ulong) actualIndex >= len) { return Undefined; } - return target.Get(actualIndex); + return target.Get((ulong) actualIndex); } /// diff --git a/Jint/Native/RegExp/RegExpConstructor.cs b/Jint/Native/RegExp/RegExpConstructor.cs index 8b30481fb8..d1a7c922d1 100644 --- a/Jint/Native/RegExp/RegExpConstructor.cs +++ b/Jint/Native/RegExp/RegExpConstructor.cs @@ -479,6 +479,17 @@ internal JsRegExp RegExpInitialize(JsRegExp r, JsValue pattern, JsValue flags, b return r; } + /// + /// https://tc39.es/ecma262/#sec-regexpcreate + /// RegExpCreate(P, F) - creates a new RegExp without calling IsRegExp on the pattern. + /// Used by String.prototype.match/search/split per spec. + /// + internal JsRegExp RegExpCreate(JsValue pattern, JsValue flags) + { + var r = RegExpAlloc(this); + return RegExpInitialize(r, pattern, flags); + } + private JsRegExp RegExpAlloc(JsValue newTarget) { if (ReferenceEquals(newTarget, this)) diff --git a/Jint/Native/RegExp/RegExpPrototype.cs b/Jint/Native/RegExp/RegExpPrototype.cs index 63b37118ff..b667b3d7ba 100644 --- a/Jint/Native/RegExp/RegExpPrototype.cs +++ b/Jint/Native/RegExp/RegExpPrototype.cs @@ -161,9 +161,10 @@ private JsValue Replace(JsValue thisObject, JsCallArguments arguments) } // check if we can access fast path + // Derive sticky from already-read flags string to avoid extra observable property access if (!fullUnicode && !mayHaveNamedCaptures - && !TypeConverter.ToBoolean(rx.Get(PropertySticky)) + && !flags.Contains('y') && rx is JsRegExp rei && rei.HasDefaultRegExpExec) { var count = global ? int.MaxValue : 1; diff --git a/Jint/Native/String/StringPrototype.cs b/Jint/Native/String/StringPrototype.cs index 67c21c9b2c..913e80c1b4 100644 --- a/Jint/Native/String/StringPrototype.cs +++ b/Jint/Native/String/StringPrototype.cs @@ -865,13 +865,15 @@ private JsValue Split(JsValue thisObject, JsCallArguments arguments) // Coerce into a number, true will become 1 var lim = limit.IsUndefined() ? uint.MaxValue : TypeConverter.ToUint32(limit); + // Per spec, if we got here the separator didn't have @@split, so just ToString it. + // Don't call IsRegExp - it would be an observable extra property access (@@match). if (separator.IsNull()) { separator = "null"; } else if (!separator.IsUndefined()) { - if (!separator.IsRegExp()) + if (separator is not JsRegExp) { separator = TypeConverter.ToJsString(separator); // Coerce into a string, for an object call toString() } @@ -1014,7 +1016,7 @@ private JsValue Search(JsValue thisObject, JsCallArguments arguments) } } - var rx = (JsRegExp) _realm.Intrinsics.RegExp.Construct([regex]); + var rx = _realm.Intrinsics.RegExp.RegExpCreate(regex, JsValue.Undefined); var s = TypeConverter.ToJsString(thisObject); return _engine.Invoke(rx, GlobalSymbolRegistry.Search, [s]); } @@ -1187,7 +1189,7 @@ private JsValue Match(JsValue thisObject, JsCallArguments arguments) } } - var rx = (JsRegExp) _realm.Intrinsics.RegExp.Construct([regex]); + var rx = _realm.Intrinsics.RegExp.RegExpCreate(regex, JsValue.Undefined); var s = TypeConverter.ToJsString(thisObject); return _engine.Invoke(rx, GlobalSymbolRegistry.Match, [s]);