generator memthod: COUNT()
This commit is contained in:
@@ -139,9 +139,44 @@ Span<char> 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<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 安全选择子节点。
|
||||
|
||||
@@ -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<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
|
||||
{
|
||||
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')
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@@ -84,15 +85,15 @@ namespace StgSharp.RegularAnalysis.Text
|
||||
/// <see langword="false" />.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// <para>/// This method operates on a single <see cref="char" />, and therefore /// on 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
|
||||
/// /// 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. /// </para> <para>///
|
||||
/// For ASCII input, the Unicode-category lookup is avoided. In ASCII, /// the accepted
|
||||
/// set is exactly: /// <c> [A-Za-z0-9_] </c>. /// </para> <para>/// This method
|
||||
/// <para> /// This method operates on a single <see cref="char" />, and therefore /// on
|
||||
/// 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 /// 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. ///</para>
|
||||
/// <para> /// For ASCII input, the Unicode-category lookup is avoided. In ASCII, /// the
|
||||
/// 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-
|
||||
/// oriented behavior enabled by /// <c> RegexOptions.ECMAScript </c>. ///</para>
|
||||
/// </remarks>
|
||||
@@ -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<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.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<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(
|
||||
RegexAstNode node,
|
||||
[NotNull] in SequenceEmitter<string> 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<RegexCountPayload>();
|
||||
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<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);");
|
||||
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:
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ namespace StgSharp.RegularAnalysis.Text
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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<int> _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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user