generator memthod: COUNT()
This commit is contained in:
@@ -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')
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,17 +85,17 @@ 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>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
protected internal static bool IsCharWord(
|
protected internal static bool IsCharWord(
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user