From 6acaba7ec4f4468db6a6b4e22c8413ac6d21d218 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sat, 11 Apr 2026 11:38:39 +0300 Subject: [PATCH] Add custom JavaScript regex engine (QuickJS libregexp port) Port of QuickJS libregexp to C# for patterns where .NET Regex diverges from ECMAScript semantics. Three-tier architecture: direct .NET Regex for simple patterns, Jint's converter for patterns needing rewriting, and custom bytecode engine for ECMAScript-specific semantics. 97,638 test262 passed, 0 failures (+1,624 from baseline). Co-Authored-By: Claude Opus 4.6 (1M context) --- Directory.Packages.props | 4 +- .../Test262Harness.settings.json | 59 +- Jint.Tests.Test262/Test262Test.cs | 1 - ...odulePreparationTests.ScriptPreparation.cs | 8 +- Jint/Engine.Ast.cs | 3 + Jint/Engine.Defaults.cs | 88 +- Jint/Native/Iterator/IteratorInstance.cs | 2 +- Jint/Native/JsRegExp.cs | 32 +- Jint/Native/RegExp/RegExpConstructor.cs | 673 +++- Jint/Native/RegExp/RegExpPrototype.cs | 223 +- Jint/ParsingOptions.cs | 79 +- Jint/PreparationOptions.cs | 8 +- .../Expressions/JintLiteralExpression.cs | 54 +- Jint/Runtime/RegExp/JintRegExpEngine.cs | 121 + Jint/Runtime/RegExp/JsRegExpConverter.cs | 472 +++ Jint/Runtime/RegExp/RegExpCompiler.cs | 2832 +++++++++++++++++ Jint/Runtime/RegExp/RegExpInterpreter.cs | 1314 ++++++++ Jint/Runtime/RegExp/RegExpMatchResult.cs | 28 + Jint/Runtime/RegExp/RegExpOpcode.cs | 146 + Jint/Runtime/RegExp/RegExpSyntaxException.cs | 11 + Jint/Runtime/RegExp/Unicode/UnicodeData.cs | 1636 ++++++++++ .../RegExp/Unicode/UnicodeProperties.cs | 783 +++++ 22 files changed, 8440 insertions(+), 137 deletions(-) create mode 100644 Jint/Runtime/RegExp/JintRegExpEngine.cs create mode 100644 Jint/Runtime/RegExp/JsRegExpConverter.cs create mode 100644 Jint/Runtime/RegExp/RegExpCompiler.cs create mode 100644 Jint/Runtime/RegExp/RegExpInterpreter.cs create mode 100644 Jint/Runtime/RegExp/RegExpMatchResult.cs create mode 100644 Jint/Runtime/RegExp/RegExpOpcode.cs create mode 100644 Jint/Runtime/RegExp/RegExpSyntaxException.cs create mode 100644 Jint/Runtime/RegExp/Unicode/UnicodeData.cs create mode 100644 Jint/Runtime/RegExp/Unicode/UnicodeProperties.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 1ad2f23da8..db46280bc1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,8 +4,8 @@ true - - + + diff --git a/Jint.Tests.Test262/Test262Harness.settings.json b/Jint.Tests.Test262/Test262Harness.settings.json index 780a9ece46..b37710982d 100644 --- a/Jint.Tests.Test262/Test262Harness.settings.json +++ b/Jint.Tests.Test262/Test262Harness.settings.json @@ -7,8 +7,6 @@ "ExcludedFeatures": [ "import-defer", "import-text", - "regexp-unicode-property-escapes", - "regexp-v-flag", "source-phase-imports", "tail-call-optimization" ], @@ -26,55 +24,6 @@ // Currently quite impossible to detect if assignment target is CoverParenthesizedExpression "language/expressions/assignment/fn-name-lhs-cover.js", - // === REGEX EXCLUSIONS === - - // CharacterClassEscapes tests internally use v flag which is not supported - "built-ins/RegExp/CharacterClassEscapes/*.js", - - // .NET regex engine: forward backreferences not supported in ECMAScript mode - "built-ins/RegExp/S15.10.2.11_A1_T5.js", - "built-ins/RegExp/S15.10.2.11_A1_T7.js", - "language/literals/regexp/named-groups/forward-reference.js", - "built-ins/RegExp/named-groups/non-unicode-references.js", - "built-ins/RegExp/named-groups/unicode-references.js", - - // .NET regex engine: lookbehind backreference capture semantics differ from JS - "built-ins/RegExp/lookBehind/back-references-to-captures.js", - "built-ins/RegExp/lookBehind/mutual-recursive.js", - - // .NET regex engine: nullable quantifier and lookahead capture group semantics differ - "built-ins/RegExp/nullable-quantifier.js", - "built-ins/RegExp/lookahead-quantifier-match-groups.js", - - // .NET regex engine: scoped regexp modifiers don't fully match ECMAScript boundary and multiline semantics - "built-ins/RegExp/regexp-modifiers/add-ignoreCase-affects-slash-lower-b.js", - "built-ins/RegExp/regexp-modifiers/add-ignoreCase-affects-slash-lower-w.js", - "built-ins/RegExp/regexp-modifiers/add-ignoreCase-affects-slash-upper-b.js", - "built-ins/RegExp/regexp-modifiers/add-ignoreCase-affects-slash-upper-w.js", - "built-ins/RegExp/regexp-modifiers/add-multiline.js", - "built-ins/RegExp/regexp-modifiers/remove-multiline-does-not-affect-dotAll-flag.js", - - // .NET regex engine: complex quantifier capture group tracking differs - "built-ins/RegExp/S15.10.2.5_A1_T4.js", - "built-ins/RegExp/prototype/exec/S15.10.6.2_A1_T6.js", - - // .NET regex engine: Unicode case folding tables differ from ECMAScript CaseFolding.txt - "language/literals/regexp/u-case-mapping.js", - "built-ins/RegExp/unicode_full_case_folding.js", - - // .NET regex engine: quantifier values exceeding int.MaxValue - "built-ins/RegExp/quantifier-integer-limit.js", - - // .NET regex engine: quantified groups with alternation retain captures from previous iterations - // (JavaScript resets non-participating groups per iteration, .NET does not) - "built-ins/RegExp/named-groups/duplicate-names-exec.js", - "built-ins/RegExp/named-groups/duplicate-names-match.js", - "built-ins/RegExp/named-groups/duplicate-names-test.js", - "built-ins/RegExp/prototype/exec/duplicate-named-groups-properties.js", - "built-ins/RegExp/prototype/exec/duplicate-named-indices-groups-properties.js", - "built-ins/String/prototype/match/duplicate-named-groups-properties.js", - "built-ins/String/prototype/match/duplicate-named-indices-groups-properties.js", - // requires investigation how to process complex function name evaluation for property "built-ins/Function/prototype/toString/method-computed-property-name.js", @@ -355,13 +304,7 @@ "intl402/Temporal/ZonedDateTime/prototype/with/leap-year-hebrew.js", "intl402/Temporal/ZonedDateTime/prototype/withCalendar/extreme-dates.js", "intl402/Temporal/ZonedDateTime/prototype/year/arithmetic-year.js", - "intl402/Temporal/ZonedDateTime/prototype/year/epoch-year.js", - - // === ANNEX B EXCLUSIONS === - - // Acornima parser/RegExp limitation: malformed named groups in non-unicode mode - // Per B.1.2, non-unicode RegExp should accept some malformed named group syntax - "annexB/built-ins/RegExp/named-groups/non-unicode-malformed.js" + "intl402/Temporal/ZonedDateTime/prototype/year/epoch-year.js" ] } diff --git a/Jint.Tests.Test262/Test262Test.cs b/Jint.Tests.Test262/Test262Test.cs index 1154d695c8..d55fea1f05 100644 --- a/Jint.Tests.Test262/Test262Test.cs +++ b/Jint.Tests.Test262/Test262Test.cs @@ -1,7 +1,6 @@ #nullable enable using Jint.Native; -using Jint.Native.Error; using Jint.Native.Object; using Jint.Runtime; using Jint.Runtime.Interop; diff --git a/Jint.Tests/Runtime/ScriptModulePreparationTests.ScriptPreparation.cs b/Jint.Tests/Runtime/ScriptModulePreparationTests.ScriptPreparation.cs index fcb15cadfe..6e65f93230 100644 --- a/Jint.Tests/Runtime/ScriptModulePreparationTests.ScriptPreparation.cs +++ b/Jint.Tests/Runtime/ScriptModulePreparationTests.ScriptPreparation.cs @@ -22,8 +22,12 @@ public void CanPreCompileRegex() var declaration = Assert.IsType(script.Program.Body[0]); var init = Assert.IsType(declaration.Declarations[0].Init); - init.Value.ToString().Should().Be("[cgt]"); - (init.Value.Options & RegexOptions.Compiled).Should().Be(RegexOptions.Compiled); + // Regex is pre-compiled during preparation with Compiled flag + var regex = init.ParseResult.Regex; + regex.Should().NotBeNull(); + (regex!.Options & RegexOptions.Compiled).Should().Be(RegexOptions.Compiled); + + // Prepared script executes correctly new Engine().Evaluate(script).AsNumber().Should().Be(1); } diff --git a/Jint/Engine.Ast.cs b/Jint/Engine.Ast.cs index 13075da536..3e843dad4e 100644 --- a/Jint/Engine.Ast.cs +++ b/Jint/Engine.Ast.cs @@ -29,6 +29,7 @@ public static Prepared