generator memthod: COUNT()

This commit is contained in:
Youmu Liu
2026-09-01 18:12:11 +08:00
parent e52d0ac6f3
commit 4218baad48
5 changed files with 504 additions and 68 deletions
@@ -139,9 +139,44 @@ Span<char> required = stackalloc char[min];
量词来自用户输入,`a{1000000}` 会让源生成器尝试进行巨大的栈分配,并生成巨型字符串字面量。建议设置较小阈值,例如 32 或 64: 量词来自用户输入,`a{1000000}` 会让源生成器尝试进行巨大的栈分配,并生成巨型字符串字面量。建议设置较小阈值,例如 32 或 64:
- `min <= threshold`:摊平为字面量并调用 `StartsWith`; - `min <= threshold`:摊平为字面量并调用 `StartsWith`;
- `min > threshold`:生成有界字符循环,或在前期分析中选择专用 IR; - `min > threshold`:循环比较固定长度的摊平块,最后比较不足一个块的余数;
- 非法或超过生成器限制的数量:报告 Roslyn diagnostic,而不是让生成器崩溃。 - 非法或超过生成器限制的数量:报告 Roslyn diagnostic,而不是让生成器崩溃。
以阈值 64 为例,`a{150}` 被拆成两个 64 字符块和一个 22 字符余数块。运行时使用局部 span 逐块调用 `StartsWith`,全部成功后才一次性推进正式 cursor。这样失败时无需恢复已经提交的 `__remain`:
```csharp
if (__remain.Length < 150)
{
break;
}
ReadOnlySpan<char> __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 安全选择子节点。 1. 从 `node` 读取 `RegexCountPayload`,并用 Empty sentinel 安全选择子节点。
@@ -8,6 +8,7 @@
using StgSharp.RegularAnalysis.Abstraction; using StgSharp.RegularAnalysis.Abstraction;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Permissions; using System.Security.Permissions;
@@ -56,14 +57,24 @@ namespace StgSharp.RegularAnalysis.Text
} }
} else if (_source[pos + 1] == 'u') } else if (_source[pos + 1] == 'u')
{ {
string value = _source[(pos + 1)..(pos + 5)]; const int unicodeEscapeLength = 6;
pos += 6; if (pos > _source.Length - unicodeEscapeLength)
if (uint.TryParse(value, out uint code) && Rune.TryCreate(code, out Rune result))
{ {
return new Token<RegexElementLabel>(result.ToString(), 0, pos, RegexElementLabel.UNIT); throw new InvalidCastException(
"A Unicode escape must contain exactly four hexadecimal digits.");
}
ReadOnlySpan<char> value = _source.AsSpan(pos + 2, 4);
if (ushort.TryParse(value, NumberStyles.AllowHexSpecifier,
CultureInfo.InvariantCulture, out ushort code))
{
position = pos + unicodeEscapeLength;
return new Token<RegexElementLabel>(((char)code).ToString(), 0, pos,
RegexElementLabel.UNIT);
} else } 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') } else if (_source[pos + 1] == 'p')
{ {
+98 -10
View File
@@ -6,6 +6,7 @@
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
using System; using System;
using System.Collections.Frozen;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@@ -84,15 +85,15 @@ namespace StgSharp.RegularAnalysis.Text
/// <see langword="false" />. /// <see langword="false" />.
/// </returns> /// </returns>
/// <remarks> /// <remarks>
/// <para>/// This method operates on a single <see cref="char" />, and therefore /// on a /// <para> /// This method operates on a single <see cref="char" />, and therefore /// on
/// single UTF-16 code unit rather than on a complete Unicode scalar /// value. /// /// a single UTF-16 code unit rather than on a complete Unicode scalar /// value.
/// </para> <para>/// Characters in the Unicode Basic Multilingual Plane are classified /// ///</para> <para> /// Characters in the Unicode Basic Multilingual Plane are
/// /// normally. A supplementary-plane character is represented by a /// surrogate pair /// classified /// normally. A supplementary-plane character is represented by a ///
/// and is therefore presented to this method as two /// separate surrogate code units. /// surrogate pair and is therefore presented to this method as two /// separate surrogate
/// Neither surrogate is independently /// treated as a word character. This follows the /// code units. Neither surrogate is independently /// treated as a word character. This
/// char-based behavior of /// the corresponding .NET Regex helper. /// </para> <para>/// /// follows the char-based behavior of /// the corresponding .NET Regex helper. ///</para>
/// For ASCII input, the Unicode-category lookup is avoided. In ASCII, /// the accepted /// <para> /// For ASCII input, the Unicode-category lookup is avoided. In ASCII, /// the
/// set is exactly: /// <c> [A-Za-z0-9_] </c>. /// </para> <para>/// This method /// accepted set is exactly: /// <c> [A-Za-z0-9_] </c>. ///</para> <para> /// This method
/// implements ordinary .NET <c> \w </c> semantics. It does /// not implement the ASCII- /// implements ordinary .NET <c> \w </c> semantics. It does /// not implement the ASCII-
/// oriented behavior enabled by /// <c> RegexOptions.ECMAScript </c>. ///</para> /// oriented behavior enabled by /// <c> RegexOptions.ECMAScript </c>. ///</para>
/// </remarks> /// </remarks>
@@ -139,5 +140,92 @@ namespace StgSharp.RegularAnalysis.Text
#endregion #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<string, int>? _namedGroups;
private readonly ReadOnlySpan<char> _text;
internal MatchResult(
ReadOnlySpan<char> text,
FrozenDictionary<string, int> namedGroups,
CaptureRange[] groupRanges
)
{
ArgumentNullException.ThrowIfNull(namedGroups);
ArgumentNullException.ThrowIfNull(groupRanges);
_text = text;
_namedGroups = namedGroups;
_groupRanges = groupRanges;
}
public ReadOnlySpan<char> 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<char> 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;
}
}
} }
@@ -13,6 +13,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Reflection.Metadata.Ecma335;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -112,23 +113,23 @@ namespace StgSharp.RegularAnalysis.Text
RegexCharSet set RegexCharSet set
) )
{ {
string remainSpan = SourceGenContext._remain_span; string remain_span = SourceGenContext._remain_span;
string accept = set.Accept ? string.Empty : "!"; string accept = set.Accept ? string.Empty : "!";
switch (set.Type) switch (set.Type)
{ {
case RegexCharSetType.Single: 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: 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: case RegexCharSetType.Set:
switch (set.Value[0]) switch (set.Value[0])
{ {
case 's': case 's':
return $"{accept}char.IsWhiteSpace({remainSpan}[0])"; return $"{accept}char.IsWhiteSpace({remain_span}[0])";
case 'w': case 'w':
return $"{accept}{nameof(TextRegex.IsCharWord)}({remainSpan}[0])"; return $"{accept}{nameof(TextRegex.IsCharWord)}({remain_span}[0])";
case 'd': case 'd':
return $"{accept}char.IsDigit({remainSpan}[0])"; return $"{accept}char.IsDigit({remain_span}[0])";
default: default:
break; 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<string> 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<string> 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<RegexCharSetPayload>();
IReadOnlyList<RegexCharSet> 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<string> 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<char> 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<char> 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<char> 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( private static void GenerateCount(
RegexAstNode node, RegexAstNode node,
[NotNull] in SequenceEmitter<string> sc, [NotNull] in SequenceEmitter<string> sc,
@@ -341,9 +651,10 @@ namespace StgSharp.RegularAnalysis.Text
in SourceGenContext context in SourceGenContext context
) )
{ {
// TODO(NGRA): Resolve the COUNT body using Empty-sentinel ownership and read the RegexAstNode value = RegexAstNode.IsNullOrEmpty(node.Left) ? node.Right : node.Left;
// RegexCountPayload from the COUNT node rather than assuming a nullable child layout. if (RegexAstNode.IsNullOrEmpty(value)) {
RegexAstNode value = node.Left ?? node.Right; throw new InvalidOperationException("COUNT requires an operand.");
}
RegexCountPayload payload = node.PayloadAs<RegexCountPayload>(); RegexCountPayload payload = node.PayloadAs<RegexCountPayload>();
int min = payload.Min; int min = payload.Min;
int max = payload.Max; int max = payload.Max;
@@ -359,38 +670,7 @@ namespace StgSharp.RegularAnalysis.Text
switch (value.Label & RegexElementLabel.SEQUENCE) switch (value.Label & RegexElementLabel.SEQUENCE)
{ {
case RegexElementLabel.UNIT: case RegexElementLabel.UNIT:
GenerateUnitCount(value, min, max, is_greedy, sc, context);
Span<char> 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);");
break; break;
case RegexElementLabel.UNIT_SET: case RegexElementLabel.UNIT_SET:
@@ -398,14 +678,7 @@ namespace StgSharp.RegularAnalysis.Text
// using IndexOfAnyExcept where the charset shape permits it. // using IndexOfAnyExcept where the charset shape permits it.
break; break;
case RegexElementLabel.UNIT_SPAN: case RegexElementLabel.UNIT_SPAN:
GenerateUnitSpanCount(value, min, max, is_greedy, sc, context);
// 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 { }
break; break;
default: default:
@@ -26,8 +26,8 @@ namespace StgSharp.RegularAnalysis.Text
/// <remarks> /// <remarks>
/// Method level state is limited to the active cursor and the result of the most recent /// 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 /// 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 /// a block scoped scratch variable. Scratch indices remain unique while rented and are
/// always declare its own without colliding with an enclosing one. /// recycled after their generated lexical scope has closed.
/// </remarks> /// </remarks>
internal sealed class SourceGenContext internal sealed class SourceGenContext
{ {
@@ -38,6 +38,10 @@ namespace StgSharp.RegularAnalysis.Text
private int _exit_alt_count; private int _exit_alt_count;
private int _scratch_scope_count;
private readonly Stack<int> _free_scratch_scope_indices = [];
public string RemainSpan { get; } = _remain_span; public string RemainSpan { get; } = _remain_span;
public string FirstOfRemain { get; } = $@"{_remain_span}[0]"; public string FirstOfRemain { get; } = $@"{_remain_span}[0]";
@@ -58,6 +62,31 @@ namespace StgSharp.RegularAnalysis.Text
return code; 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);
}
} }
} }