737 lines
35 KiB
C#
737 lines
35 KiB
C#
// -----------------------------------------------------------------------------
|
|
// file="TextRegexSourceGen.GenClass"
|
|
// Project: StgSharp
|
|
// Copyright (c) Nitload.
|
|
// SPDX-License-Identifier: MIT
|
|
// -----------------------------------------------------------------------------
|
|
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Nitload.RegularAnalysis.Abstraction;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
|
namespace Nitload.RegularAnalysis.Text
|
|
{
|
|
internal partial class TextRegexSourceGen
|
|
{
|
|
|
|
internal static void GenerateMethodSource(
|
|
RegexAstNode ast,
|
|
in SequenceEmitter<string> sc,
|
|
in List<SequenceEmitter<string>> func_list,
|
|
in SourceGenContext context,
|
|
in ExitLabel exit_label = default,
|
|
bool is_find = false
|
|
)
|
|
{
|
|
RegexAstNode root = ast;
|
|
if (root.Label == RegexElementLabel.EPSILON) {
|
|
return;
|
|
}
|
|
if (TryGenerateSpecialSource(root, sc, func_list, context, exit_label)) {
|
|
return;
|
|
}
|
|
RegexElementLabel op = root.Label & (RegexElementLabel.VAST_OPERATOR);
|
|
if (op != 0)
|
|
{
|
|
switch (op)
|
|
{
|
|
case RegexElementLabel.CONCAT:
|
|
GenerateConcat(root, sc, func_list, context, exit_label);
|
|
break;
|
|
case RegexElementLabel.COUNT:
|
|
GenerateCount(root, sc, func_list, context);
|
|
break;
|
|
case RegexElementLabel.GROUP_BEGIN:
|
|
GenerateGroup(root, sc, func_list, context);
|
|
break;
|
|
case RegexElementLabel.ALT:
|
|
GenerateAlt(root, sc, func_list, context);
|
|
break;
|
|
default:
|
|
|
|
// TODO(NGRA): Report an unsupported operator instead of silently emitting
|
|
// an incomplete matcher.
|
|
break;
|
|
}
|
|
} else
|
|
{
|
|
op = root.Label & (RegexElementLabel.SEQUENCE);
|
|
switch (op)
|
|
{
|
|
case RegexElementLabel.UNIT:
|
|
GenerateUnit(root, sc, context, exit_label);
|
|
break;
|
|
case RegexElementLabel.UNIT_SET:
|
|
GenerateUnitSet(root, sc, context, exit_label);
|
|
break;
|
|
case RegexElementLabel.UNIT_SPAN:
|
|
GenerateUnitSpan(root, sc, context, exit_label);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// TODO(NGRA): Integrate is_find candidate search and continuation retries.
|
|
// Leaf emission currently matches only at the active cursor.
|
|
}
|
|
}
|
|
|
|
#region supporting method
|
|
|
|
private static string FormatStringLiteral(
|
|
string value
|
|
)
|
|
{
|
|
return SyntaxFactory.Literal(value).Text;
|
|
}
|
|
|
|
private static string FormatStringLiteral(
|
|
ReadOnlySpan<char> value
|
|
)
|
|
{
|
|
return SyntaxFactory.Literal(value.ToString()).Text;
|
|
}
|
|
|
|
private static string FormatCharLiteral(
|
|
char value
|
|
)
|
|
{
|
|
return SyntaxFactory.Literal(value).Text;
|
|
}
|
|
|
|
private static string GenerateCharSetExpression(
|
|
RegexCharSet set
|
|
)
|
|
{
|
|
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)}({remain_span}[0]))";
|
|
case RegexCharSetType.Range:
|
|
if (set.Value.Length != 2) {
|
|
throw new InvalidOperationException("A charset range requires two endpoints.");
|
|
}
|
|
return $"{accept}({remain_span}[0] >= {FormatCharLiteral(set.Value[0])} && {remain_span}[0] <= {FormatCharLiteral(set.Value[1])})";
|
|
case RegexCharSetType.Set:
|
|
switch (set.Value)
|
|
{
|
|
case "s":
|
|
return $"{accept}char.IsWhiteSpace({remain_span}[0])";
|
|
case "w":
|
|
return $"{accept}{Text_Regex}.{nameof(TextRegex.IsCharWord)}({remain_span}[0])";
|
|
case "d":
|
|
return $"{accept}char.IsDigit({remain_span}[0])";
|
|
default:
|
|
break;
|
|
}
|
|
goto default;
|
|
case RegexCharSetType.Any:
|
|
return set.Accept ? "true" : "false";
|
|
default:
|
|
throw new NotSupportedException($"Unsupported charset rule: {set.Type} ({set.Value}).");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region UNIT direct matching
|
|
|
|
private static void GenerateUnit(
|
|
RegexAstNode value,
|
|
[NotNull] in SequenceEmitter<string> sc,
|
|
in SourceGenContext context,
|
|
in ExitLabel exit
|
|
)
|
|
{
|
|
string literal = value.Value;
|
|
if (literal.Length != 1) {
|
|
throw new InvalidOperationException(
|
|
$"UNIT requires one decoded character, but received {literal.Length}.");
|
|
}
|
|
|
|
string remain_span = context.RemainSpan;
|
|
_ = sc.AppendLine($"if ({remain_span}.IsEmpty || {remain_span}[0] != {FormatCharLiteral(literal[0])})")
|
|
.AppendLine("{")
|
|
.AppendLine($"goto {exit.GetLabelName()};")
|
|
.AppendLine("}")
|
|
.AppendLine($"{remain_span} = {remain_span}.Slice(1);");
|
|
}
|
|
|
|
private static void GenerateUnitSet(
|
|
RegexAstNode value,
|
|
[NotNull] in SequenceEmitter<string> sc,
|
|
in SourceGenContext context,
|
|
in ExitLabel exit
|
|
)
|
|
{
|
|
RegexCharSetPayload payload = value.PayloadAs<RegexCharSetPayload>();
|
|
string[] rules = new string[payload.Set.Count];
|
|
for (int i = 0; i < rules.Length; i++) {
|
|
rules[i] = GenerateCharSetExpression(payload.Set[i]);
|
|
}
|
|
|
|
// Combine rule-level complements before applying the enclosing charset complement.
|
|
// An empty union accepts nothing; its complement accepts any available character.
|
|
string predicate = rules.Length == 0 ? "false" : string.Join(" || ", rules);
|
|
string mismatch = payload.Accept ? $"!({predicate})" : $"({predicate})";
|
|
string remain_span = context.RemainSpan;
|
|
_ = sc.AppendLine($"if ({remain_span}.IsEmpty || {mismatch})")
|
|
.AppendLine("{")
|
|
.AppendLine($"goto {exit.GetLabelName()};")
|
|
.AppendLine("}")
|
|
.AppendLine($"{remain_span} = {remain_span}.Slice(1);");
|
|
}
|
|
|
|
private static void GenerateUnitSpan(
|
|
RegexAstNode value,
|
|
[NotNull] in SequenceEmitter<string> sc,
|
|
in SourceGenContext context,
|
|
in ExitLabel exit
|
|
)
|
|
{
|
|
string literal = value.Value;
|
|
if (literal.Length == 0) {
|
|
throw new InvalidOperationException("UNIT_SPAN requires a non-empty literal.");
|
|
}
|
|
|
|
// StartsWith also rejects input shorter than the decoded literal.
|
|
string remain_span = context.RemainSpan;
|
|
_ = sc.AppendLine($"if (!{remain_span}.{nameof(MemoryExtensions.StartsWith)}({FormatStringLiteral(literal)}))")
|
|
.AppendLine("{")
|
|
.AppendLine($"goto {exit.GetLabelName()};")
|
|
.AppendLine("}")
|
|
.AppendLine($"{remain_span} = {remain_span}.Slice({literal.Length});");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region UNIT count matching
|
|
|
|
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,
|
|
in ExitLabel exit
|
|
)
|
|
{
|
|
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;
|
|
|
|
// TODO(NGRA-NAMING): Allocate required_rest, required_matched, required_chunk_index,
|
|
// and additional_count through the exclusive-name pool, including loop-local names.
|
|
int scratch_scope_index = context.RequestScratchScopeIndex();
|
|
string exit_label_node = exit.GetLabelName();
|
|
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): Replace this local COUNT exit with the inherited failure ExitLabel.
|
|
// The temporary goto preserves the former local exit but still lets siblings execute.
|
|
|
|
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($"goto {exit_label_node};")
|
|
.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($"goto {exit_label_node};")
|
|
.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($"goto {exit_label_node};")
|
|
.AppendLine("}")
|
|
.AppendLine($"{remain_span} = {remain_span}.Slice({min});");
|
|
}
|
|
|
|
int additional_limit = max < 0 ? -1 : max - min;
|
|
if (is_greedy && additional_limit != 0)
|
|
{
|
|
_ = additional_limit < 0 ?
|
|
sc.AppendLine($"while (!{remain_span}.IsEmpty && {remain_span}[0] == {expected_literal})")
|
|
.AppendLine("{")
|
|
.AppendLine($"{remain_span} = {remain_span}.Slice(1);")
|
|
.AppendLine("}") :
|
|
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.");
|
|
}
|
|
}
|
|
|
|
private static void GenerateUnitSetCount(
|
|
RegexAstNode value,
|
|
int min,
|
|
int max,
|
|
bool is_greedy,
|
|
[NotNull] in SequenceEmitter<string> sc,
|
|
in SourceGenContext context,
|
|
in ExitLabel exit_label
|
|
)
|
|
{
|
|
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;
|
|
|
|
// TODO(NGRA-NAMING): Allocate required_matched and additional_count through the
|
|
// exclusive-name pool. Reserve required_rest/required_chunk_index if emitted later.
|
|
int scratch_scope_index = context.RequestScratchScopeIndex();
|
|
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): Route a failed minimum check through the inherited failure ExitLabel.
|
|
// The break below only stops charset scanning and must remain local to that loop.
|
|
bool accept = payload.Accept;
|
|
RegexCharSet _set = charset[0];
|
|
_ = sc.AppendLine($"{ROS_char} {required_matched} = {remain_span}")
|
|
.AppendLine($"int {additional_count} = 0;")
|
|
.AppendLine($"for(; {additional_count} < {max}; {additional_count} ++)")
|
|
.AppendLine("{");
|
|
if (charset.Count == 1)
|
|
{
|
|
// single charset rule
|
|
_ = sc.AppendLine($"if({(accept ? string.Empty : "!(")} {GenerateCharSetExpression(_set)})");
|
|
} else
|
|
{
|
|
// multi charset rule
|
|
_ = sc.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 : ')')}");
|
|
}
|
|
_ = sc .AppendLine("{")
|
|
.AppendLine($@"{context.RemainSpan} = {context.RemainSpan}.Slice(1);")
|
|
.AppendLine("}")
|
|
.AppendLine("else")
|
|
.AppendLine("{")
|
|
.AppendLine($@"break;")
|
|
.AppendLine("}")
|
|
.AppendLine("}")
|
|
.AppendLine($"if({additional_count} < {min})")
|
|
.AppendLine("{")
|
|
.AppendLine("}")
|
|
.AppendLine("else")
|
|
.AppendLine("{")
|
|
.AppendLine("}");
|
|
}
|
|
|
|
private static void GenerateUnitSpanCount(
|
|
RegexAstNode value,
|
|
int min,
|
|
int max,
|
|
bool is_greedy,
|
|
[NotNull] in SequenceEmitter<string> sc,
|
|
in SourceGenContext context,
|
|
in ExitLabel exit
|
|
)
|
|
{
|
|
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;
|
|
|
|
// TODO(NGRA-NAMING): Allocate required_rest, required_matched, required_chunk_index,
|
|
// required_count, and additional_count through the exclusive-name pool. Flattened
|
|
// sibling COUNT nodes share the declaration space even after their exit labels.
|
|
int scratch_scope_index = context.RequestScratchScopeIndex();
|
|
string exit_label_node = exit.GetLabelName();
|
|
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): Replace this local COUNT exit with the inherited failure ExitLabel.
|
|
// The temporary goto preserves the former local exit but still lets siblings execute.
|
|
|
|
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($"goto {exit_label_node};")
|
|
.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($"goto {exit_label_node};")
|
|
.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($"goto {exit_label_node};")
|
|
.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.");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region UNIT find matching
|
|
|
|
// TODO(NGRA): Add UNIT, UNIT_SET, and UNIT_SPAN candidate search emitters here.
|
|
// Wire is_find dispatch and continuation retries when FIND matching is implemented.
|
|
|
|
#endregion
|
|
|
|
#region Operator generation
|
|
|
|
private static void GenerateAlt(
|
|
RegexAstNode node,
|
|
[NotNull] in SequenceEmitter<string> sc,
|
|
in List<SequenceEmitter<string>> func_list,
|
|
in SourceGenContext context
|
|
)
|
|
{
|
|
RegexAstNode current = node.Right;
|
|
|
|
// TODO(NGRA-NAMING): Reserve exclusive names for future ALT cursor/capture snapshots;
|
|
// their lifetime extends through retry labels, not just the candidate's emitted block.
|
|
ExitLabel exit_label = context.RequestExitLabel(ExitMode.Goto);
|
|
|
|
// TODO(NGRA): Walk every normalized ALT candidate exactly once. The final traversal
|
|
// shape and failure continuation are not wired yet.
|
|
while (RegexAstNode.IsNullOrEmpty(current))
|
|
{
|
|
RegexElementLabel op = current.Label & RegexElementLabel.SEQUENCE;
|
|
if (op != 0)
|
|
{
|
|
string code = current.SourceCode;
|
|
switch (op)
|
|
{
|
|
case RegexElementLabel.UNIT:
|
|
|
|
// TODO(NGRA): Guard against an empty remaining span before indexing it.
|
|
_ = sc.AppendLine(@$"if({context.RemainSpan}[0] == {FormatCharLiteral(code[0])})")
|
|
.AppendLine("{")
|
|
.AppendLine($@"{context.RemainSpan} = {context.RemainSpan}.Slice(1);")
|
|
.AppendLine($@"goto {exit_label};")
|
|
.AppendLine("}");
|
|
break;
|
|
case RegexElementLabel.UNIT_SET:
|
|
|
|
// TODO(NGRA): Guard the remaining span, handle empty/Any sets, and
|
|
// validate negated-set composition before emitting this predicate.
|
|
RegexCharSetPayload payload = current.PayloadAs<RegexCharSetPayload>();
|
|
IReadOnlyList<RegexCharSet> split = payload.Set;
|
|
bool accept = payload.Accept;
|
|
RegexCharSet _set = split[0];
|
|
_ = sc.AppendLine($"if({(accept ? string.Empty : "!(")} {GenerateCharSetExpression(_set)} ||");
|
|
for (int i = 1; i < split.Count - 1; i++)
|
|
{
|
|
_set = split[i];
|
|
_ = sc.AppendLine($" {GenerateCharSetExpression(_set)} ||");
|
|
}
|
|
_set = split[^1];
|
|
_ = sc.AppendLine($" {GenerateCharSetExpression(_set)} ){(accept ? string.Empty : ')')}")
|
|
.AppendLine("{")
|
|
.AppendLine($@"{context.RemainSpan} = {context.RemainSpan}.Slice(1);")
|
|
.AppendLine($@"goto {exit_label};")
|
|
.AppendLine("}");
|
|
break;
|
|
case RegexElementLabel.UNIT_SPAN:
|
|
|
|
// TODO(NGRA): Advance by code.Length rather than one character and
|
|
// preserve the ALT rollback cursor on failure.
|
|
string literal = FormatStringLiteral(code);
|
|
_ = sc.AppendLine(@$"if({context.RemainSpan}.{MemoryExtensions.StartsWith}({literal}))")
|
|
.AppendLine("{")
|
|
.AppendLine($@"{context.RemainSpan} = {context.RemainSpan}.Slice({code.Length});")
|
|
.AppendLine($@"goto {exit_label};")
|
|
.AppendLine("}");
|
|
|
|
break;
|
|
default:
|
|
|
|
// TODO(NGRA): Route unsupported ALT candidates through the recursive
|
|
// matcher or report a source-generation diagnostic.
|
|
break;
|
|
}
|
|
|
|
// TODO(NGRA): Advance to the next ALT candidate after a scalar candidate
|
|
// misses; the current sequence branch does not update current.
|
|
} else
|
|
{
|
|
GenerateMethodSource(current, sc, func_list, context);
|
|
current = current.Next;
|
|
}
|
|
}
|
|
|
|
// TODO(NGRA): Emit the all-alternatives-failed continuation before the shared success
|
|
// label, including restoration of the entry cursor and capture state.
|
|
_ = sc.AppendLine($@"{exit_label}: {{ }}");
|
|
}
|
|
|
|
private static void GenerateGroup(
|
|
RegexAstNode node,
|
|
[NotNull] in SequenceEmitter<string> sc,
|
|
in List<SequenceEmitter<string>> func_list,
|
|
in SourceGenContext context
|
|
)
|
|
{
|
|
RegexAstNode left = node.Left;
|
|
RegexAstNode right = node.Right;
|
|
|
|
RegexGroupPayload config = node.PayloadAs<RegexGroupPayload>();
|
|
|
|
// TODO(NGRA-NAMING): Reserve exclusive names for generated group snapshots and capture
|
|
// temporaries. A capture's user-provided name is not a unique generated local name.
|
|
|
|
// TODO(NGRA): Select the owned group body using Empty-sentinel checks, snapshot its
|
|
// start cursor, and restore it when the child matcher fails.
|
|
if (left is not null)
|
|
{
|
|
GenerateMethodSource(left, sc, func_list, context);
|
|
} else
|
|
if (right is not null) {
|
|
GenerateMethodSource(right, sc, func_list, context);
|
|
}
|
|
|
|
string group_name = config.Name;
|
|
|
|
// TODO(NGRA): Allocate numeric/named capture slots and emit the successful capture
|
|
// span. Non-capturing groups still need a rollback scope but no stored capture.
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
private static void GenerateConcat(
|
|
RegexAstNode node,
|
|
[NotNull] in SequenceEmitter<string> sc,
|
|
in List<SequenceEmitter<string>> func_list,
|
|
in SourceGenContext context,
|
|
in ExitLabel exit_label = default
|
|
)
|
|
{
|
|
if (!RegexAstNode.IsNullOrEmpty(node.Left)) {
|
|
GenerateMethodSource(node.Left, sc, func_list, context, exit_label);
|
|
}
|
|
if (!RegexAstNode.IsNullOrEmpty(node.Right)) {
|
|
GenerateMethodSource(node.Right, sc, func_list, context, exit_label);
|
|
}
|
|
}
|
|
|
|
private static void GenerateCount(
|
|
RegexAstNode node,
|
|
[NotNull] in SequenceEmitter<string> sc,
|
|
in List<SequenceEmitter<string>> func_list,
|
|
in SourceGenContext context
|
|
)
|
|
{
|
|
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;
|
|
bool is_greedy = payload.IsGreedy;
|
|
|
|
// TODO(NGRA-NAMING): Reserve exclusive names for future repetition/rollback state
|
|
// across complex COUNT retries; recursive children must use the same method pool.
|
|
ExitLabel exit_label = context.RequestExitLabel(ExitMode.Goto);
|
|
if ((value.Label & RegexElementLabel.SEQUENCE) == 0)
|
|
{
|
|
// TODO(NGRA): Emit min/max repetition, zero-width progress protection, greedy vs.
|
|
// lazy ordering, and per-attempt rollback for a complex COUNT body.
|
|
GenerateMethodSource(value, sc, func_list, context);
|
|
} else
|
|
{
|
|
switch (value.Label & RegexElementLabel.SEQUENCE)
|
|
{
|
|
case RegexElementLabel.UNIT:
|
|
GenerateUnitCount(value, min, max, is_greedy, sc, context, exit_label);
|
|
break;
|
|
case RegexElementLabel.UNIT_SET:
|
|
GenerateUnitSetCount(value, min, max, is_greedy, sc, context, exit_label);
|
|
break;
|
|
case RegexElementLabel.UNIT_SPAN:
|
|
GenerateUnitSpanCount(value, min, max, is_greedy, sc, context, exit_label);
|
|
break;
|
|
default:
|
|
|
|
// TODO(NGRA): Diagnose malformed COUNT children instead of emitting an
|
|
// empty branch.
|
|
break;
|
|
}
|
|
}
|
|
|
|
// TODO(NGRA): Connect COUNT success/failure continuations to the surrounding rollback
|
|
// scope; the label is currently only a placeholder.
|
|
_ = sc.AppendLine($@"{exit_label}: {{ }}");
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|