From 4218baad481dfd611b88a5930f65a268579f0535 Mon Sep 17 00:00:00 2001 From: Youmu Liu <105042599+Nitload-NSI@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:12:11 +0800 Subject: [PATCH] generator memthod: COUNT() --- .../COUNT_UNIT_GENERATION.md | 37 +- .../Text/RegexTokenReader.cs | 21 +- .../Text/TextRegex.cs | 110 +++++- .../Text/TextRegexSourceGen.GenClass.cs | 371 +++++++++++++++--- .../Text/TextRegexSourceGen.GenContext.cs | 33 +- 5 files changed, 504 insertions(+), 68 deletions(-) diff --git a/src/StgSharp.RegularAnalysis/COUNT_UNIT_GENERATION.md b/src/StgSharp.RegularAnalysis/COUNT_UNIT_GENERATION.md index cf014775..0206f291 100644 --- a/src/StgSharp.RegularAnalysis/COUNT_UNIT_GENERATION.md +++ b/src/StgSharp.RegularAnalysis/COUNT_UNIT_GENERATION.md @@ -139,9 +139,44 @@ Span required = stackalloc char[min]; 量词来自用户输入,`a{1000000}` 会让源生成器尝试进行巨大的栈分配,并生成巨型字符串字面量。建议设置较小阈值,例如 32 或 64: - `min <= threshold`:摊平为字面量并调用 `StartsWith`; -- `min > threshold`:生成有界字符循环,或在前期分析中选择专用 IR; +- `min > threshold`:循环比较固定长度的摊平块,最后比较不足一个块的余数; - 非法或超过生成器限制的数量:报告 Roslyn diagnostic,而不是让生成器崩溃。 +以阈值 64 为例,`a{150}` 被拆成两个 64 字符块和一个 22 字符余数块。运行时使用局部 span 逐块调用 `StartsWith`,全部成功后才一次性推进正式 cursor。这样失败时无需恢复已经提交的 `__remain`: + +```csharp +if (__remain.Length < 150) +{ + break; +} + +ReadOnlySpan __requiredRest_0 = __remain; +bool __requiredMatched_0 = true; + +for (int __requiredChunkIndex_0 = 0; __requiredChunkIndex_0 < 2; __requiredChunkIndex_0++) +{ + if (!__requiredRest_0.StartsWith("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + { + __requiredMatched_0 = false; + break; + } + + __requiredRest_0 = __requiredRest_0.Slice(64); +} + +if (__requiredMatched_0 && !__requiredRest_0.StartsWith("aaaaaaaaaaaaaaaaaaaaaa")) +{ + __requiredMatched_0 = false; +} + +if (!__requiredMatched_0) +{ + break; +} + +__remain = __remain.Slice(150); +``` + ## 后续实现顺序 1. 从 `node` 读取 `RegexCountPayload`,并用 Empty sentinel 安全选择子节点。 diff --git a/src/StgSharp.RegularAnalysis/Text/RegexTokenReader.cs b/src/StgSharp.RegularAnalysis/Text/RegexTokenReader.cs index 8743bea5..5c1887f0 100644 --- a/src/StgSharp.RegularAnalysis/Text/RegexTokenReader.cs +++ b/src/StgSharp.RegularAnalysis/Text/RegexTokenReader.cs @@ -8,6 +8,7 @@ using StgSharp.RegularAnalysis.Abstraction; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Runtime.InteropServices; using System.Security.Permissions; @@ -56,14 +57,24 @@ namespace StgSharp.RegularAnalysis.Text } } else if (_source[pos + 1] == 'u') { - string value = _source[(pos + 1)..(pos + 5)]; - pos += 6; - if (uint.TryParse(value, out uint code) && Rune.TryCreate(code, out Rune result)) + const int unicodeEscapeLength = 6; + if (pos > _source.Length - unicodeEscapeLength) { - return new Token(result.ToString(), 0, pos, RegexElementLabel.UNIT); + throw new InvalidCastException( + "A Unicode escape must contain exactly four hexadecimal digits."); + } + + ReadOnlySpan value = _source.AsSpan(pos + 2, 4); + if (ushort.TryParse(value, NumberStyles.AllowHexSpecifier, + CultureInfo.InvariantCulture, out ushort code)) + { + position = pos + unicodeEscapeLength; + return new Token(((char)code).ToString(), 0, pos, + RegexElementLabel.UNIT); } else { - throw new InvalidCastException($"Failed to convert {value} to a valid unicode code point."); + throw new InvalidCastException( + $"Failed to convert {value.ToString()} to a UTF-16 code unit."); } } else if (_source[pos + 1] == 'p') { diff --git a/src/StgSharp.RegularAnalysis/Text/TextRegex.cs b/src/StgSharp.RegularAnalysis/Text/TextRegex.cs index 3b4cdbc1..bd693402 100644 --- a/src/StgSharp.RegularAnalysis/Text/TextRegex.cs +++ b/src/StgSharp.RegularAnalysis/Text/TextRegex.cs @@ -6,6 +6,7 @@ // ----------------------------------------------------------------------------- using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -84,17 +85,17 @@ namespace StgSharp.RegularAnalysis.Text /// . /// /// - /// /// This method operates on a single , and therefore /// on a - /// single UTF-16 code unit rather than on a complete Unicode scalar /// value. /// - /// /// Characters in the Unicode Basic Multilingual Plane are classified - /// /// normally. A supplementary-plane character is represented by a /// surrogate pair - /// and is therefore presented to this method as two /// separate surrogate code units. - /// Neither surrogate is independently /// treated as a word character. This follows the - /// char-based behavior of /// the corresponding .NET Regex helper. /// /// - /// For ASCII input, the Unicode-category lookup is avoided. In ASCII, /// the accepted - /// set is exactly: /// [A-Za-z0-9_] . /// /// This method + /// /// This method operates on a single , and therefore /// on + /// a single UTF-16 code unit rather than on a complete Unicode scalar /// value. + /// /// /// Characters in the Unicode Basic Multilingual Plane are + /// classified /// normally. A supplementary-plane character is represented by a /// + /// surrogate pair and is therefore presented to this method as two /// separate surrogate + /// code units. Neither surrogate is independently /// treated as a word character. This + /// follows the char-based behavior of /// the corresponding .NET Regex helper. /// + /// /// For ASCII input, the Unicode-category lookup is avoided. In ASCII, /// the + /// accepted set is exactly: /// [A-Za-z0-9_] . /// /// This method /// implements ordinary .NET \w semantics. It does /// not implement the ASCII- - /// oriented behavior enabled by /// RegexOptions.ECMAScript . /// + /// oriented behavior enabled by /// RegexOptions.ECMAScript . /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected internal static bool IsCharWord( @@ -139,5 +140,92 @@ namespace StgSharp.RegularAnalysis.Text #endregion } - public ref struct MatchResult { } + internal readonly struct CaptureRange + { + + private readonly uint _encodedStart; + + public CaptureRange( + int start, + int length + ) + { + ArgumentOutOfRangeException.ThrowIfNegative(start); + ArgumentOutOfRangeException.ThrowIfNegative(length); + + _encodedStart = (uint)start + 1u; + Length = length; + } + + public int Length { get; } + + public int Start => unchecked((int)(_encodedStart - 1u)); + + public bool Success => _encodedStart != 0; + + } + + public readonly ref struct MatchResult + { + + private readonly CaptureRange[]? _groupRanges; + private readonly FrozenDictionary? _namedGroups; + private readonly ReadOnlySpan _text; + + internal MatchResult( + ReadOnlySpan text, + FrozenDictionary namedGroups, + CaptureRange[] groupRanges + ) + { + ArgumentNullException.ThrowIfNull(namedGroups); + ArgumentNullException.ThrowIfNull(groupRanges); + + _text = text; + _namedGroups = namedGroups; + _groupRanges = groupRanges; + } + + public ReadOnlySpan this[ + string name + ] + { + get + { + ArgumentNullException.ThrowIfNull(name); + + if (_namedGroups is null || + !_namedGroups.TryGetValue(name, out int index)) + { + throw new KeyNotFoundException($"The match result does not define a group named '{name}'."); + } + + CaptureRange range = _groupRanges![index]; + return range.Success ? _text.Slice(range.Start, range.Length) : default; + } + } + + public bool TryGetGroup( + string name, + out ReadOnlySpan value + ) + { + ArgumentNullException.ThrowIfNull(name); + + if (_namedGroups is not null && + _namedGroups.TryGetValue(name, out int index)) + { + CaptureRange range = _groupRanges![index]; + if (range.Success) + { + value = _text.Slice(range.Start, range.Length); + return true; + } + } + + value = default; + return false; + } + + } } diff --git a/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.GenClass.cs b/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.GenClass.cs index 96a67995..046cab63 100644 --- a/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.GenClass.cs +++ b/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.GenClass.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Reflection.Metadata.Ecma335; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -112,23 +113,23 @@ namespace StgSharp.RegularAnalysis.Text RegexCharSet set ) { - string remainSpan = SourceGenContext._remain_span; + string remain_span = SourceGenContext._remain_span; string accept = set.Accept ? string.Empty : "!"; switch (set.Type) { case RegexCharSetType.Single: - return $"{accept}({FormatStringLiteral(set.Value)}.{nameof(MemoryExtensions.Contains)}({remainSpan}[0]))"; + return $"{accept}({FormatStringLiteral(set.Value)}.{nameof(MemoryExtensions.Contains)}({remain_span}[0]))"; case RegexCharSetType.Range: - return $"{accept}({remainSpan}[0] >= {FormatCharLiteral(set.Value[0])} && {remainSpan}[0] <= {FormatCharLiteral(set.Value[1])})"; + return $"{accept}({remain_span}[0] >= {FormatCharLiteral(set.Value[0])} && {remain_span}[0] <= {FormatCharLiteral(set.Value[1])})"; case RegexCharSetType.Set: switch (set.Value[0]) { case 's': - return $"{accept}char.IsWhiteSpace({remainSpan}[0])"; + return $"{accept}char.IsWhiteSpace({remain_span}[0])"; case 'w': - return $"{accept}{nameof(TextRegex.IsCharWord)}({remainSpan}[0])"; + return $"{accept}{nameof(TextRegex.IsCharWord)}({remain_span}[0])"; case 'd': - return $"{accept}char.IsDigit({remainSpan}[0])"; + return $"{accept}char.IsDigit({remain_span}[0])"; default: break; } @@ -334,6 +335,315 @@ namespace StgSharp.RegularAnalysis.Text } } + private const int flattened_unit_count_threshold = 32; + + private static void GenerateUnitCount( + RegexAstNode value, + int min, + int max, + bool is_greedy, + [NotNull] in SequenceEmitter sc, + in SourceGenContext context + ) + { + if (min < 0 || (max >= 0 && max < min)) { + throw new InvalidOperationException($"Invalid COUNT range: {min}..{max}."); + } + + string literal = value.Value; + if (literal.Length != 1) { + throw new InvalidOperationException( + $"COUNT(UNIT) requires one decoded character, but received {literal.Length}."); + } + + char expected = literal[0]; + string expected_literal = FormatCharLiteral(expected); + string remain_span = context.RemainSpan; + int scratch_scope_index = context.RentScratchScopeIndex(); + string required_rest = $"__required_rest_{scratch_scope_index}"; + string required_matched = $"__required_matched_{scratch_scope_index}"; + string required_chunk_index = $"__required_chunk_index_{scratch_scope_index}"; + string additional_count = $"__additional_count_{scratch_scope_index}"; + + // TODO(NGRA): Move ownership of this work region to the enclosing CONCAT or match + // method. Until then, break demonstrates the local failure path but cannot prevent a + // later sibling node from being emitted and executed. + _ = sc.AppendLine("do").AppendLine("{"); + + if (min <= flattened_unit_count_threshold) + { + if (min > 0) + { + string required_prefix = new(expected, min); + _ = sc.AppendLine($"if (!{remain_span}.{nameof(MemoryExtensions.StartsWith)}({FormatStringLiteral(required_prefix)}))") + .AppendLine("{") + .AppendLine("break;") + .AppendLine("}") + .AppendLine($"{remain_span} = {remain_span}.Slice({min});"); + } + } else + { + // Keep large lower bounds vectorizable without emitting a giant literal. Validate + // repeated threshold-sized chunks against a local span and commit the cursor only + // after every chunk has matched. + string required_chunk = new(expected, flattened_unit_count_threshold); + int required_chunk_count = min / flattened_unit_count_threshold; + int required_remainder_length = min % flattened_unit_count_threshold; + + _ = sc.AppendLine($"if ({remain_span}.Length < {min})") + .AppendLine("{") + .AppendLine("break;") + .AppendLine("}") + .AppendLine($"{ROS_char} {required_rest} = {remain_span};") + .AppendLine($"bool {required_matched} = true;") + .AppendLine($"for (int {required_chunk_index} = 0; {required_chunk_index} < {required_chunk_count}; {required_chunk_index}++)") + .AppendLine("{") + .AppendLine($"if (!{required_rest}.{nameof(MemoryExtensions.StartsWith)}({FormatStringLiteral(required_chunk)}))") + .AppendLine("{") + .AppendLine($"{required_matched} = false;") + .AppendLine("break;") + .AppendLine("}") + .AppendLine($"{required_rest} = {required_rest}.Slice({flattened_unit_count_threshold});") + .AppendLine("}"); + + if (required_remainder_length > 0) + { + string required_remainder = new(expected, required_remainder_length); + _ = sc.AppendLine($"if ({required_matched} && !{required_rest}.{nameof(MemoryExtensions.StartsWith)}({FormatStringLiteral(required_remainder)}))") + .AppendLine("{") + .AppendLine($"{required_matched} = false;") + .AppendLine("}"); + } + + _ = sc.AppendLine($"if (!{required_matched})") + .AppendLine("{") + .AppendLine("break;") + .AppendLine("}") + .AppendLine($"{remain_span} = {remain_span}.Slice({min});"); + } + + int additional_limit = max < 0 ? -1 : max - min; + if (is_greedy && additional_limit != 0) + { + if (additional_limit < 0) + { + _ = sc.AppendLine($"while (!{remain_span}.IsEmpty && {remain_span}[0] == {expected_literal})") + .AppendLine("{") + .AppendLine($"{remain_span} = {remain_span}.Slice(1);") + .AppendLine("}"); + } else + { + _ = sc.AppendLine($"for (int {additional_count} = 0; {additional_count} < {additional_limit} && !{remain_span}.IsEmpty && {remain_span}[0] == {expected_literal}; {additional_count}++)") + .AppendLine("{") + .AppendLine($"{remain_span} = {remain_span}.Slice(1);") + .AppendLine("}"); + } + } else if (!is_greedy && additional_limit != 0) + { + // Lazy COUNT deliberately stops at Min. Its optional tail is consumed only when + // a future continuation attempt fails and requests another repetition. + _ = sc.AppendLine("// TODO(NGRA): Retry the lazy COUNT tail from its continuation."); + } + + _ = sc.AppendLine("} while (false);"); + context.ReturnScratchScopeIndex(scratch_scope_index); + } + + private static void GenerateUnitSetCount( + RegexAstNode value, + int min, + int max, + bool is_greedy, + [NotNull] in SequenceEmitter sc, + in SourceGenContext context + ) + { + if (min < 0 || (max >= 0 && max < min)) { + throw new InvalidOperationException($"Invalid COUNT range: {min}..{max}."); + } + + string literal = value.Value; + if (literal.Length != 1) { + throw new InvalidOperationException( + $"COUNT(UNIT) requires one decoded character, but received {literal.Length}."); + } + + RegexCharSetPayload payload = value.PayloadAs(); + + IReadOnlyList charset = payload.Set; + string remain_span = context.RemainSpan; + int scratch_scope_index = context.RentScratchScopeIndex(); + string required_rest = $"__required_rest_{scratch_scope_index}"; + string required_matched = $"__required_matched_{scratch_scope_index}"; + string required_chunk_index = $"__required_chunk_index_{scratch_scope_index}"; + string additional_count = $"__additional_count_{scratch_scope_index}"; + + + // TODO(NGRA): Move ownership of this work region to the enclosing CONCAT or match + // method. Until then, break demonstrates the local failure path but cannot prevent a + // later sibling node from being emitted and executed. + bool accept = payload.Accept; + RegexCharSet _set = charset[0]; + _ = sc.AppendLine($"{ROS_char} {required_matched} = {remain_span}") + .AppendLine($"int {additional_count} = 0;") + .AppendLine() + .AppendLine() //TODO loop head + .AppendLine($"if({(accept ? string.Empty : "!(")} {GenerateCharSetExpression(_set)} ||"); + for (int i = 1; i < charset.Count - 1; i++) + { + _set = charset[i]; + _ = sc.AppendLine($" {GenerateCharSetExpression(_set)} ||"); + } + _set = charset[^1]; + _ = sc.AppendLine($" {GenerateCharSetExpression(_set)} ){(accept ? string.Empty : ')')}") + .AppendLine("{") + .AppendLine($@"{context.RemainSpan} = {context.RemainSpan}.Slice(1);") + .AppendLine($@"break;") + .AppendLine("}"); + } + + private static void GenerateUnitSpanCount( + RegexAstNode value, + int min, + int max, + bool is_greedy, + [NotNull] in SequenceEmitter sc, + in SourceGenContext context + ) + { + if (min < 0 || (max >= 0 && max < min)) { + throw new InvalidOperationException($"Invalid COUNT range: {min}..{max}."); + } + + string literal = value.Value; + if (literal.Length == 0) { + throw new InvalidOperationException( + "COUNT(UNIT_SPAN) requires a non-empty literal."); + } + + string expected_literal = FormatStringLiteral(literal); + string remain_span = context.RemainSpan; + int scratch_scope_index = context.RentScratchScopeIndex(); + string required_rest = $"__required_rest_{scratch_scope_index}"; + string required_matched = $"__required_matched_{scratch_scope_index}"; + string required_chunk_index = $"__required_chunk_index_{scratch_scope_index}"; + string required_count = $"__required_count_{scratch_scope_index}"; + string additional_count = $"__additional_count_{scratch_scope_index}"; + string starts_with = $"{nameof(MemoryExtensions.StartsWith)}"; + + // TODO(NGRA): Move ownership of this work region to the enclosing CONCAT or match + // method. Until then, break demonstrates the local failure path but cannot prevent a + // later sibling node from being emitted and executed. + _ = sc.AppendLine("do").AppendLine("{"); + + if (literal.Length > flattened_unit_count_threshold) + { + // One repetition is already larger than the flattening threshold. Match the + // original literal directly and keep all cursor mutations local until Min has + // succeeded; the common tail below remains responsible for Min..Max. + _ = sc.AppendLine($"int {required_count} = 0;") + .AppendLine($"{ROS_char} {required_rest} = {remain_span};") + .AppendLine($"for (; {required_count} < {min} && {required_rest}.{starts_with}({expected_literal}); {required_count}++)") + .AppendLine("{") + .AppendLine($"{required_rest} = {required_rest}.Slice({literal.Length});") + .AppendLine("}") + .AppendLine($"if ({required_count} != {min})") + .AppendLine("{") + .AppendLine("break;") + .AppendLine("}") + .AppendLine($"{remain_span} = {required_rest};"); + } else if (min <= flattened_unit_count_threshold / literal.Length) + { + if (min > 0) + { + Span required_prefix_span = stackalloc char[literal.Length * min]; + for (int i = 0; i < min; i++) { + literal.AsSpan().CopyTo(required_prefix_span[(i * literal.Length)..]); + } + string required_prefix = required_prefix_span.ToString(); + _ = sc.AppendLine($"if (!{remain_span}.{starts_with}({FormatStringLiteral(required_prefix)}))") + .AppendLine("{") + .AppendLine("break;") + .AppendLine("}") + .AppendLine($"{remain_span} = {remain_span}.Slice({min * literal.Length});"); + } + } else + { + // Keep large lower bounds vectorizable without emitting a giant literal. Validate + // repeated threshold-sized chunks against a local span and commit the cursor only + // after every chunk has matched. + int count = flattened_unit_count_threshold / literal.Length; + Span required_prefix_span = stackalloc char[literal.Length * count]; + for (int i = 0; i < count; i++) { + literal.AsSpan().CopyTo(required_prefix_span[(i * literal.Length)..]); + } + string required_chunk = required_prefix_span.ToString(); + int required_chunk_count = min / count; + int required_remainder_length = min % count; + + _ = sc.AppendLine($"{ROS_char} {required_rest} = {remain_span};") + .AppendLine($"bool {required_matched} = true;") + .AppendLine($"for (int {required_chunk_index} = 0; {required_chunk_index} < {required_chunk_count}; {required_chunk_index}++)") + .AppendLine("{") + .AppendLine($"if (!{required_rest}.{starts_with}({FormatStringLiteral(required_chunk)}))") + .AppendLine("{") + .AppendLine($"{required_matched} = false;") + .AppendLine("break;") + .AppendLine("}") + .AppendLine($"{required_rest} = {required_rest}.Slice({count * literal.Length});") + .AppendLine("}"); + + if (required_remainder_length > 0) + { + Span required_remainder_span = stackalloc char[literal.Length * required_remainder_length]; + for (int i = 0; i < required_remainder_length; i++) { + literal.AsSpan().CopyTo(required_remainder_span[(i * literal.Length)..]); + } + string required_remainder = required_remainder_span.ToString(); + _ = sc.AppendLine($"if ({required_matched} && !{required_rest}.{starts_with}({FormatStringLiteral(required_remainder)}))") + .AppendLine("{") + .AppendLine($"{required_matched} = false;") + .AppendLine("}") + .AppendLine($"if ({required_matched})") + .AppendLine("{") + .AppendLine($"{required_rest} = {required_rest}.Slice({literal.Length * required_remainder_length});") + .AppendLine("}"); + } + + _ = sc.AppendLine($"if (!{required_matched})") + .AppendLine("{") + .AppendLine("break;") + .AppendLine("}") + .AppendLine($"{remain_span} = {required_rest};"); + } + + int additional_limit = max < 0 ? -1 : max - min; + if (is_greedy && additional_limit != 0) + { + if (additional_limit < 0) + { + _ = sc.AppendLine($"while ({remain_span}.{starts_with}({expected_literal}))") + .AppendLine("{") + .AppendLine($"{remain_span} = {remain_span}.Slice({literal.Length});") + .AppendLine("}"); + } else + { + _ = sc.AppendLine($"for (int {additional_count} = 0; {additional_count} < {additional_limit} && {remain_span}.{starts_with}({expected_literal}); {additional_count}++)") + .AppendLine("{") + .AppendLine($"{remain_span} = {remain_span}.Slice({literal.Length});") + .AppendLine("}"); + } + } else if (!is_greedy && additional_limit != 0) + { + // Lazy COUNT deliberately stops at Min. Its optional tail is consumed only when + // a future continuation attempt fails and requests another repetition. + _ = sc.AppendLine("// TODO(NGRA): Retry the lazy COUNT tail from its continuation."); + } + + _ = sc.AppendLine("} while (false);"); + context.ReturnScratchScopeIndex(scratch_scope_index); + } + private static void GenerateCount( RegexAstNode node, [NotNull] in SequenceEmitter sc, @@ -341,9 +651,10 @@ namespace StgSharp.RegularAnalysis.Text in SourceGenContext context ) { - // TODO(NGRA): Resolve the COUNT body using Empty-sentinel ownership and read the - // RegexCountPayload from the COUNT node rather than assuming a nullable child layout. - RegexAstNode value = node.Left ?? node.Right; + RegexAstNode value = RegexAstNode.IsNullOrEmpty(node.Left) ? node.Right : node.Left; + if (RegexAstNode.IsNullOrEmpty(value)) { + throw new InvalidOperationException("COUNT requires an operand."); + } RegexCountPayload payload = node.PayloadAs(); int min = payload.Min; int max = payload.Max; @@ -359,38 +670,7 @@ namespace StgSharp.RegularAnalysis.Text switch (value.Label & RegexElementLabel.SEQUENCE) { case RegexElementLabel.UNIT: - - Span long_seq = stackalloc char[min]; - long_seq.Fill(value.Source.Source[0]); - - // TODO(NGRA): Move ownership of this work region to the enclosing CONCAT - // or match method once failure propagation is wired. A region local to - // COUNT is only a structural trial because break cannot skip later nodes. - _ = sc.AppendLine("do") - .AppendLine("{") - .AppendLine($"if({context.RemainSpan}.{nameof(MemoryExtensions.StartsWith)}({FormatStringLiteral(long_seq)}))") - .AppendLine(@"{") - /* TODO(NGRA): The flattened literal has already matched Min copies. - * Consume that prefix, then emit only the optional tail: at most - * Max - Min additional copies, or an unbounded tail when Max < 0. - * Greedy and lazy modes must order the continuation differently. - */ - .AppendLine($"int remained_count = {max} - {min};") - .AppendLine($@"{context.RemainSpan} = {context.RemainSpan}.Slice({min});") - .AppendLine($"for(int i = 0; i < remained_count && {context.RemainSpan}[0] == {FormatCharLiteral(value.Source.Source[0])}; i++)") - .AppendLine("{") - .AppendLine($@"{context.RemainSpan} = {context.RemainSpan}.Slice(1);") - .AppendLine("}") - .AppendLine(@"}") - .AppendLine("else") - .AppendLine("{") - /* StartsWith has not mutated the cursor, so this branch needs no - * local restoration. The enclosing work region must own its entry - * cursor/capture snapshot and interpret this break as failure. - */ - .AppendLine("break;") - .AppendLine("}") - .AppendLine("} while (false);"); + GenerateUnitCount(value, min, max, is_greedy, sc, context); break; case RegexElementLabel.UNIT_SET: @@ -398,14 +678,7 @@ namespace StgSharp.RegularAnalysis.Text // using IndexOfAnyExcept where the charset shape permits it. break; case RegexElementLabel.UNIT_SPAN: - - // TODO(NGRA): Emit repeated full-span matching with min/max enforcement and - // cursor rollback when the minimum cannot be satisfied. - string pattern = value.Source.Source; - if (pattern.Length > 32) - { - _ = sc.AppendLine("do").AppendLine("{").AppendLine("} while(false);"); - } else { } + GenerateUnitSpanCount(value, min, max, is_greedy, sc, context); break; default: diff --git a/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.GenContext.cs b/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.GenContext.cs index 110e57c8..cba128af 100644 --- a/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.GenContext.cs +++ b/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.GenContext.cs @@ -26,8 +26,8 @@ namespace StgSharp.RegularAnalysis.Text /// /// Method level state is limited to the active cursor and the result of the most recent /// instruction. Everything else is either a region slot, a line slot, a capture slot, or - /// a block scoped scratch variable. Scratch names are never reused, so a nested block can - /// always declare its own without colliding with an enclosing one. + /// a block scoped scratch variable. Scratch indices remain unique while rented and are + /// recycled after their generated lexical scope has closed. /// internal sealed class SourceGenContext { @@ -38,6 +38,10 @@ namespace StgSharp.RegularAnalysis.Text private int _exit_alt_count; + private int _scratch_scope_count; + + private readonly Stack _free_scratch_scope_indices = []; + public string RemainSpan { get; } = _remain_span; public string FirstOfRemain { get; } = $@"{_remain_span}[0]"; @@ -58,6 +62,31 @@ namespace StgSharp.RegularAnalysis.Text return code; } + public int RentScratchScopeIndex() + { + if (_free_scratch_scope_indices.TryPop(out int recycled_index)) { + return recycled_index; + } + + int index = _scratch_scope_count; + _scratch_scope_count++; + return index; + } + + public void ReturnScratchScopeIndex( + int index + ) + { + if ((uint)index >= (uint)_scratch_scope_count) { + throw new ArgumentOutOfRangeException(nameof(index)); + } + if (_free_scratch_scope_indices.Contains(index)) { + throw new InvalidOperationException($"Scratch scope {index} was returned twice."); + } + + _free_scratch_scope_indices.Push(index); + } + } }