specific file for special regex rule

This commit is contained in:
Youmu Liu
2026-09-09 18:11:40 +08:00
parent 8641fd4e08
commit 8755185e78
9 changed files with 406 additions and 261 deletions
@@ -7,15 +7,7 @@
using Microsoft.CodeAnalysis.CSharp;
using Nitload.RegularAnalysis.Abstraction;
using System;
using System.Buffers;
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;
namespace Nitload.RegularAnalysis.Text
{
@@ -27,21 +19,24 @@ namespace Nitload.RegularAnalysis.Text
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);
GenerateConcat(root, sc, func_list, context, exit_label);
break;
case RegexElementLabel.COUNT:
GenerateCount(root, sc, func_list, context);
@@ -64,25 +59,20 @@ namespace Nitload.RegularAnalysis.Text
switch (op)
{
case RegexElementLabel.UNIT:
// TODO(NGRA): Emit the remaining-span length guard, literal comparison,
// cursor advance, and failure transfer for one character.
GenerateUnit(root, sc, context, exit_label);
break;
case RegexElementLabel.UNIT_SET:
// TODO(NGRA): Emit the remaining-span length guard, charset predicate,
// cursor advance, and failure transfer.
GenerateUnitSet(root, sc, context, exit_label);
break;
case RegexElementLabel.UNIT_SPAN:
// TODO(NGRA): Emit StartsWith for the complete literal span and advance by
// the literal length; integrate is_find candidate retry semantics.
GenerateUnitSpan(root, sc, context, exit_label);
break;
default:
// TODO(NGRA): Diagnose malformed leaf nodes instead of generating no code.
break;
}
// TODO(NGRA): Integrate is_find candidate search and continuation retries.
// Leaf emission currently matches only at the active cursor.
}
}
@@ -120,228 +110,105 @@ namespace Nitload.RegularAnalysis.Text
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[0])
switch (set.Value)
{
case 's':
case "s":
return $"{accept}char.IsWhiteSpace({remain_span}[0])";
case 'w':
return $"{accept}{nameof(TextRegex.IsCharWord)}({remain_span}[0])";
case 'd':
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:
// TODO(NGRA): Unsupported/empty charset rules must become an analyzer
// diagnostic; returning an empty expression produces invalid generated C#.
return string.Empty;
throw new NotSupportedException($"Unsupported charset rule: {set.Type} ({set.Value}).");
}
}
private static bool IsNamedFind(
RegexAstNode node
)
{
// (?<name>.*)abc
if (node is null)
{
return false;
}
if ((node.Label & RegexElementLabel.CONCAT) != 0) {
return false;
}
RegexAstNode group = node.Left;
// TODO(NGRA): Validate the normalized CONCAT/GROUP/COUNT shape with Empty sentinels
// before dereferencing children; this helper currently assumes every edge exists.
if ((group.Label & RegexElementLabel.GROUP_BEGIN) == 0)
{
return false;
}
RegexAstNode count = group.Right;
// .*
return (count.Label & RegexElementLabel.COUNT) != 0 &&
(count.Right.Label & RegexElementLabel.UNIT_SINGLE) != 0 &&
count.Right.Source.Source == ".";
}
#endregion
#region basic verb generate
#region UNIT direct matching
private static void GenerateAlt(
RegexAstNode node,
private static void GenerateUnit(
RegexAstNode value,
[NotNull] in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context
in SourceGenContext context,
in ExitLabel exit
)
{
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 liteal = FormatStringLiteral(code);
_ = sc.AppendLine(@$"if({context.RemainSpan}.{MemoryExtensions.StartsWith}({liteal}))")
.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;
}
string literal = value.Value;
if (literal.Length != 1) {
throw new InvalidOperationException(
$"UNIT requires one decoded character, but received {literal.Length}.");
}
// 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}: {{ }}");
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 GenerateGroup(
RegexAstNode node,
private static void GenerateUnitSet(
RegexAstNode value,
[NotNull] in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context
in SourceGenContext context,
in ExitLabel exit
)
{
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);
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]);
}
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();
// 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 GenerateConcat(
RegexAstNode node,
private static void GenerateUnitSpan(
RegexAstNode value,
[NotNull] in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context
in SourceGenContext context,
in ExitLabel exit
)
{
RegexAstNode left = node.Left;
RegexAstNode right = node.Right;
if (left is not null)
{
// find
if ((left.Label & RegexElementLabel.COUNT) == RegexElementLabel.COUNT &&
(left.Right.Label & RegexElementLabel.UNIT_SINGLE) == RegexElementLabel.UNIT_SINGLE &&
left.Right.Source.Source == "." &&
right is not null)
{
// TODO(NGRA): Emit the unnamed FIND candidate loop and retry the right subtree
// until it succeeds or no anchor candidate remains.
GenerateMethodSource(right, sc, func_list, context, true);
} else if (IsNamedFind(node))
{
if (right is not null)
{
// TODO(NGRA): Emit named FIND retry and capture the text preceding the
// successful anchor candidate.
GenerateMethodSource(right, sc, func_list, context, true);
} else
{
// TODO(NGRA): Capture the remaining input for a terminal named dot-star.
}
} else
{
GenerateMethodSource(left, sc, func_list, context);
if (right is not null) {
GenerateMethodSource(right, sc, func_list, context);
}
}
string literal = value.Value;
if (literal.Length == 0) {
throw new InvalidOperationException("UNIT_SPAN requires a non-empty literal.");
}
// TODO(NGRA): Normalize ownership so the right subtree is emitted exactly once. It is
// currently reachable both inside the ordinary CONCAT branch and here.
if (right is not null)
{
GenerateMethodSource(right, sc, func_list, context);
}
// 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(
@@ -435,19 +302,15 @@ namespace Nitload.RegularAnalysis.Text
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("}");
}
_ = 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
@@ -673,12 +536,156 @@ namespace Nitload.RegularAnalysis.Text
}
}
private static void GenerateCount(
#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
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;