Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Acornima" Version="1.3.2" />
<PackageVersion Include="Acornima.Extras" Version="1.3.2" />
<PackageVersion Include="Acornima" Version="1.4.0" />
<PackageVersion Include="Acornima.Extras" Version="1.4.0" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="BenchmarkDotNet.TestAdapter" Version="0.13.12" />
<PackageVersion Include="FluentAssertions" Version="[7.2.2]" />
Expand Down
59 changes: 1 addition & 58 deletions Jint.Tests.Test262/Test262Harness.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
"ExcludedFeatures": [
"import-defer",
"import-text",
"regexp-unicode-property-escapes",
"regexp-v-flag",
"source-phase-imports",
"tail-call-optimization"
],
Expand All @@ -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",

Expand Down Expand Up @@ -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"

]
}
1 change: 0 additions & 1 deletion Jint.Tests.Test262/Test262Test.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#nullable enable

using Jint.Native;
using Jint.Native.Error;
using Jint.Native.Object;
using Jint.Runtime;
using Jint.Runtime.Interop;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ public void CanPreCompileRegex()
var declaration = Assert.IsType<VariableDeclaration>(script.Program.Body[0]);
var init = Assert.IsType<RegExpLiteral>(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);
}

Expand Down
3 changes: 3 additions & 0 deletions Jint/Engine.Ast.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public static Prepared<Script> PrepareScript(string code, string? source = null,
Script preparedScript;
var sourceOffset = options.ParsingOptions.SourceOffset;
var padding = AcornimaExtensions.CreateSourceOffsetPadding(sourceOffset);

if (padding.Length > 0)
{
var paddedCode = padding + code;
Expand All @@ -38,6 +39,7 @@ public static Prepared<Script> PrepareScript(string code, string? source = null,
{
preparedScript = parser.ParseScript(code, source, strict);
}

return new Prepared<Script>(preparedScript, parserOptions);
}
catch (Exception e)
Expand All @@ -64,6 +66,7 @@ public static Prepared<Module> PrepareModule(string code, string? source = null,
try
{
var preparedModule = parser.ParseModule(code, source);

return new Prepared<Module>(preparedModule, parserOptions);
}
catch (Exception e)
Expand Down
88 changes: 82 additions & 6 deletions Jint/Engine.Defaults.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,93 @@
using Jint.Native.RegExp;
using Jint.Runtime.RegExp;

namespace Jint;

public partial class Engine
{
internal const bool FoldConstantsOnPrepareByDefault = true;

private static readonly TimeSpan DefaultPrepareRegexTimeout = TimeSpan.FromSeconds(10);

/// <summary>
/// OnRegExp callback that validates regex syntax at parse time without converting.
/// Conversion is done at runtime in RegExpInitialize with fallback to the custom engine.
/// </summary>
internal static readonly OnRegExpHandler ValidateRegExpHandler = static (in RegExpParsingContext ctx) =>
{
ctx.Validate();
return RegExpParseResult.ForSuccess();
};

/// <summary>
/// OnRegExp callback that validates and converts regex at parse time (interpreted .NET Regex).
/// Used during script/module preparation when CompileRegex is false.
/// </summary>
internal static readonly OnRegExpHandler ConvertRegExpHandler = static (in RegExpParsingContext ctx) =>
{
ctx.Validate();
// Skip pre-compilation for patterns that need the custom QuickJS engine at runtime
if (!RegExpConstructor.NeedCustomEngine(ctx.Pattern, ctx.Flags)
&& JsRegExpConverter.TryConvert(ctx.Pattern, ctx.Flags, DefaultPrepareRegexTimeout, out var regex, out var groupCount))
{
return RegExpParseResult.ForSuccess(regex, groupCount);
}

return RegExpParseResult.ForSuccess();
};

/// <summary>
/// OnRegExp callback that validates and converts regex at parse time (compiled .NET Regex).
/// Used during script/module preparation when CompileRegex is true (default for preparation).
/// </summary>
internal static readonly OnRegExpHandler CompileRegExpHandler = static (in RegExpParsingContext ctx) =>
{
ctx.Validate();
// Skip pre-compilation for patterns that need the custom QuickJS engine at runtime
if (!RegExpConstructor.NeedCustomEngine(ctx.Pattern, ctx.Flags)
&& JsRegExpConverter.TryConvert(ctx.Pattern, ctx.Flags, DefaultPrepareRegexTimeout, out var regex, out var groupCount, compiled: true))
{
return RegExpParseResult.ForSuccess(regex, groupCount);
}

return RegExpParseResult.ForSuccess();
};

/// <summary>
/// Creates an OnRegExp handler with a caller-specified timeout (interpreted .NET Regex).
/// </summary>
internal static OnRegExpHandler CreateConvertRegExpHandler(TimeSpan timeout) => (in RegExpParsingContext ctx) =>
{
ctx.Validate();
if (!RegExpConstructor.NeedCustomEngine(ctx.Pattern, ctx.Flags)
&& JsRegExpConverter.TryConvert(ctx.Pattern, ctx.Flags, timeout, out var regex, out var groupCount))
{
return RegExpParseResult.ForSuccess(regex, groupCount);
}

return RegExpParseResult.ForSuccess();
};

/// <summary>
/// Creates an OnRegExp handler with a caller-specified timeout (compiled .NET Regex).
/// </summary>
internal static OnRegExpHandler CreateCompileRegExpHandler(TimeSpan timeout) => (in RegExpParsingContext ctx) =>
{
ctx.Validate();
if (!RegExpConstructor.NeedCustomEngine(ctx.Pattern, ctx.Flags)
&& JsRegExpConverter.TryConvert(ctx.Pattern, ctx.Flags, timeout, out var regex, out var groupCount, compiled: true))
{
return RegExpParseResult.ForSuccess(regex, groupCount);
}

return RegExpParseResult.ForSuccess();
};

internal static readonly ParserOptions BaseParserOptions = ParserOptions.Default with
{
EcmaVersion = EcmaVersion.ES2023,
ExperimentalESFeatures = ExperimentalESFeatures.ImportAttributes
| ExperimentalESFeatures.RegExpDuplicateNamedCapturingGroups
| ExperimentalESFeatures.RegExpModifiers
| ExperimentalESFeatures.ExplicitResourceManagement
| ExperimentalESFeatures.Decorators,
EcmaVersion = EcmaVersion.ES2026,
ExperimentalESFeatures = ExperimentalESFeatures.Decorators,
OnRegExp = ValidateRegExpHandler,
Tolerant = false,
};
}
2 changes: 1 addition & 1 deletion Jint/Native/Iterator/IteratorInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ public override bool TryIteratorStep(out ObjectInstance nextItem)
if (macthStr == "")
{
var thisIndex = TypeConverter.ToLength(_iteratingRegExp.Get(JsRegExp.PropertyLastIndex));
var nextIndex = thisIndex + 1;
var nextIndex = RegExpPrototype.AdvanceStringIndex(_s, thisIndex, _unicode);
_iteratingRegExp.Set(JsRegExp.PropertyLastIndex, nextIndex, true);
}
}
Expand Down
32 changes: 31 additions & 1 deletion Jint/Native/JsRegExp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Jint.Native.RegExp;
using Jint.Runtime;
using Jint.Runtime.Descriptors;
using Jint.Runtime.RegExp;

namespace Jint.Native;

Expand All @@ -24,6 +25,12 @@ public JsRegExp(Engine engine)
public Regex Value { get; set; } = null!;
public string Source { get; set; }

/// <summary>
/// Custom regex engine used when .NET Regex cannot handle the pattern.
/// When set, this takes priority over <see cref="Value"/>.
/// </summary>
internal JintRegExpEngine? CustomEngine { get; set; }

public string Flags
{
get => _flags;
Expand All @@ -37,6 +44,7 @@ public string Flags
IgnoreCase = false;
Multiline = false;
Sticky = false;
Unicode = false;
FullUnicode = false;
UnicodeSets = false;
foreach (var c in _flags)
Expand All @@ -62,29 +70,51 @@ public string Flags
Sticky = true;
break;
case 'u':
Unicode = true;
FullUnicode = true;
break;
case 'v':
UnicodeSets = true;
FullUnicode = true; // v-flag implies unicode semantics
break;
}
}
}
}

public RegExpParseResult ParseResult { get; set; }
/// <summary>
/// JS group count (including group 0 for full match) from Jint's converter.
/// 0 means not set — fall back to .NET Regex's own group count.
/// </summary>
internal int ConvertedGroupCount { get; set; }

/// <summary>
/// Provides backward-compatible access to the regex parse result.
/// </summary>
[Obsolete("The ParseResult property is no longer populated. Use Value to access the .NET Regex instance directly.")]
public RegExpParseResult ParseResult
{
get => UsesDotNetEngine && Value is not null ? RegExpParseResult.ForSuccess(Value) : default;
set { /* no-op for backward compatibility */ }
}

public bool DotAll { get; private set; }
public bool Global { get; private set; }
public bool Indices { get; private set; }
public bool IgnoreCase { get; private set; }
public bool Multiline { get; private set; }
public bool Sticky { get; private set; }
/// <summary>Whether the 'u' flag was explicitly set (for the unicode accessor).</summary>
public bool Unicode { get; private set; }
/// <summary>Whether unicode semantics apply (true for both 'u' and 'v' flags).</summary>
public bool FullUnicode { get; private set; }
public bool UnicodeSets { get; private set; }

internal bool HasDefaultRegExpExec => Properties == null && Prototype is RegExpPrototype { HasDefaultExec: true };

/// <summary>Whether this regex uses the .NET Regex engine (not the custom engine).</summary>
internal bool UsesDotNetEngine => CustomEngine is null;

public override PropertyDescriptor GetOwnProperty(JsValue property)
{
if (PropertyLastIndex.Equals(property))
Expand Down
Loading