process cases of ALT as special rules

This commit is contained in:
Youmu Liu
2026-09-10 18:17:09 +08:00
parent 8755185e78
commit f372f83171
18 changed files with 486 additions and 166 deletions
@@ -39,13 +39,13 @@ namespace Nitload.RegularAnalysis.Text
GenerateConcat(root, sc, func_list, context, exit_label);
break;
case RegexElementLabel.COUNT:
GenerateCount(root, sc, func_list, context);
GenerateCount(root, sc, func_list, context, exit_label);
break;
case RegexElementLabel.GROUP_BEGIN:
GenerateGroup(root, sc, func_list, context);
GenerateGroup(root, sc, func_list, context, exit_label);
break;
case RegexElementLabel.ALT:
GenerateAlt(root, sc, func_list, context);
GenerateAlt(root, sc, func_list, context, exit_label);
break;
default:
@@ -167,17 +167,20 @@ namespace Nitload.RegularAnalysis.Text
)
{
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})")
_ = sc.AppendLine($"if ({remain_span}.IsEmpty ||")
.AppendLine(payload.Accept ? " !(" : " (");
if (payload.Set.Count == 0) {
_ = sc.AppendLine(" false");
}
for (int i = 0; i < payload.Set.Count; i++)
{
string suffix = i + 1 < payload.Set.Count ? " ||" : string.Empty;
_ = sc.AppendLine($" {GenerateCharSetExpression(payload.Set[i])}{suffix}");
}
_ = sc.AppendLine(" ))")
.AppendLine("{")
.AppendLine($"goto {exit.GetLabelName()};")
.AppendLine("}")
@@ -244,9 +247,6 @@ namespace Nitload.RegularAnalysis.Text
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)
@@ -428,9 +428,6 @@ namespace Nitload.RegularAnalysis.Text
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
@@ -551,93 +548,28 @@ namespace Nitload.RegularAnalysis.Text
RegexAstNode node,
[NotNull] in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context
in SourceGenContext context,
in ExitLabel exit_label
)
{
ExitLabel success = context.RequestExitLabel(ExitMode.Goto);
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))
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;
}
GenerateAltCandidate(current, sc, func_list, context, success);
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}: {{ }}");
_ = sc.AppendLine($"goto {exit_label.GetLabelName()};")
.AppendLine($"{success.GetLabelName()}: ;");
}
private static void GenerateGroup(
RegexAstNode node,
[NotNull] in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context
in SourceGenContext context,
in ExitLabel exit_label
)
{
RegexAstNode left = node.Left;
@@ -648,14 +580,13 @@ namespace Nitload.RegularAnalysis.Text
// 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)
// TODO(NGRA): Snapshot the group start cursor and restore it when the child fails.
if (!RegexAstNode.IsNullOrEmpty(left))
{
GenerateMethodSource(left, sc, func_list, context);
GenerateMethodSource(left, sc, func_list, context, exit_label);
} else
if (right is not null) {
GenerateMethodSource(right, sc, func_list, context);
if (!RegexAstNode.IsNullOrEmpty(right)) {
GenerateMethodSource(right, sc, func_list, context, exit_label);
}
string group_name = config.Name;
@@ -685,7 +616,8 @@ namespace Nitload.RegularAnalysis.Text
RegexAstNode node,
[NotNull] in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context
in SourceGenContext context,
in ExitLabel exit_label
)
{
RegexAstNode value = RegexAstNode.IsNullOrEmpty(node.Left) ? node.Right : node.Left;
@@ -699,12 +631,11 @@ namespace Nitload.RegularAnalysis.Text
// 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);
GenerateMethodSource(value, sc, func_list, context, exit_label);
} else
{
switch (value.Label & RegexElementLabel.SEQUENCE)
@@ -726,9 +657,6 @@ namespace Nitload.RegularAnalysis.Text
}
}
// TODO(NGRA): Connect COUNT success/failure continuations to the surrounding rollback
// scope; the label is currently only a placeholder.
_ = sc.AppendLine($@"{exit_label}: {{ }}");
}
#endregion
@@ -1,6 +1,6 @@
// -----------------------------------------------------------------------------
// file="TextRegexSourceGen.GenSpecial"
// Project: Nitload
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
@@ -24,11 +24,162 @@ namespace Nitload.RegularAnalysis.Text
// to the general path; returning true prevents that subtree from being emitted twice.
return node.Label switch
{
RegexElementLabel.CONCAT => TryGenerateFindConcat(node, sc, func_list, context, exit_label),
RegexElementLabel.CONCAT => TryGenerateFindConcat(node, sc, func_list, context,
exit_label),
_ => false,
};
}
#region ALT candidate rules
private static bool TryGetSimpleAltNode(
RegexAstNode node,
out RegexAstNode simple
)
{
simple = node;
while (simple.Label == RegexElementLabel.CONCAT)
{
bool left_empty = RegexAstNode.IsNullOrEmpty(simple.Left);
bool right_empty = RegexAstNode.IsNullOrEmpty(simple.Right);
if (left_empty == right_empty) {
return false;
}
simple = left_empty ? simple.Right : simple.Left;
}
// Inspect transparent wrappers without changing ownership or the candidate Next link.
// COUNT and GROUP are never peeled: repetition and capture semantics must survive.
return simple.Label is RegexElementLabel.UNIT or RegexElementLabel.UNIT_SET or
RegexElementLabel.UNIT_SPAN;
}
private static void GenerateAltCandidate(
RegexAstNode node,
in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context,
in ExitLabel success
)
{
if (TryGetSimpleAltNode(node, out RegexAstNode simple)) {
node = simple;
}
switch (node.Label)
{
case RegexElementLabel.UNIT:
GenerateAltUnit(node, sc, context, success);
break;
case RegexElementLabel.UNIT_SET:
GenerateAltUnitSet(node, sc, context, success);
break;
case RegexElementLabel.UNIT_SPAN:
GenerateAltUnitSpan(node, sc, context, success);
break;
case RegexElementLabel.CONCAT when RegexAstNode.IsNullOrEmpty(node.Left) &&
RegexAstNode.IsNullOrEmpty(node.Right):
case RegexElementLabel.EPSILON:
// An empty expression succeeds without consuming input.
_ = sc.AppendLine($"goto {success.GetLabelName()};");
break;
case RegexElementLabel.CONCAT:
case RegexElementLabel.COUNT:
case RegexElementLabel.GROUP_BEGIN:
case RegexElementLabel.ALT:
GenerateAltOperator(node, sc, func_list, context, success);
break;
default:
throw new NotSupportedException($"Unsupported ALT candidate: {node.Label}.");
}
}
private static void GenerateAltUnit(
RegexAstNode node,
in SequenceEmitter<string> sc,
in SourceGenContext context,
in ExitLabel success
)
{
string literal = node.Value;
if (literal.Length != 1) {
throw new InvalidOperationException("ALT UNIT requires one decoded character.");
}
_ = sc.AppendLine($"if (!{context.RemainSpan}.IsEmpty && {context.RemainSpan}[0] == {FormatCharLiteral(literal[0])})")
.AppendLine("{")
.AppendLine($"{context.RemainSpan} = {context.RemainSpan}.Slice(1);")
.AppendLine($"goto {success.GetLabelName()};")
.AppendLine("}");
}
private static void GenerateAltUnitSet(
RegexAstNode node,
in SequenceEmitter<string> sc,
in SourceGenContext context,
in ExitLabel success
)
{
RegexCharSetPayload payload = node.PayloadAs<RegexCharSetPayload>();
_ = sc.AppendLine($"if (!{context.RemainSpan}.IsEmpty &&")
.AppendLine(payload.Accept ? " (" : " !(");
if (payload.Set.Count == 0) {
_ = sc.AppendLine(" false");
}
for (int i = 0; i < payload.Set.Count; i++)
{
string suffix = i + 1 < payload.Set.Count ? " ||" : string.Empty;
_ = sc.AppendLine($" {GenerateCharSetExpression(payload.Set[i])}{suffix}");
}
_ = sc.AppendLine(" ))")
.AppendLine("{")
.AppendLine($"{context.RemainSpan} = {context.RemainSpan}.Slice(1);")
.AppendLine($"goto {success.GetLabelName()};")
.AppendLine("}");
}
private static void GenerateAltUnitSpan(
RegexAstNode node,
in SequenceEmitter<string> sc,
in SourceGenContext context,
in ExitLabel success
)
{
string literal = node.Value;
if (literal.Length == 0) {
throw new InvalidOperationException("ALT UNIT_SPAN requires a non-empty literal.");
}
string predicate = $"{context.RemainSpan}.{nameof(MemoryExtensions.StartsWith)}({FormatStringLiteral(literal)})";
_ = sc.AppendLine($"if ({predicate})")
.AppendLine("{")
.AppendLine($"{context.RemainSpan} = {context.RemainSpan}.Slice({literal.Length});")
.AppendLine($"goto {success.GetLabelName()};")
.AppendLine("}");
}
private static void GenerateAltOperator(
RegexAstNode node,
in SequenceEmitter<string> sc,
in List<SequenceEmitter<string>> func_list,
in SourceGenContext context,
in ExitLabel success
)
{
ExitLabel failure = context.RequestExitLabel(ExitMode.Goto);
string entry = $"__alt_entry_{context.RequestScratchScopeIndex()}";
_ = sc.AppendLine($"{ROS_char} {entry} = {context.RemainSpan};");
GenerateMethodSource(node, sc, func_list, context, failure);
_ = sc.AppendLine($"goto {success.GetLabelName()};")
.AppendLine($"{failure.GetLabelName()}:")
.AppendLine($"{context.RemainSpan} = {entry};");
// TODO(NGRA): Restore capture state once GROUP emission supports captures.
// Retrying an already successful candidate after a later continuation fails is separate.
}
#endregion
#region FIND concatenation
private static bool TryGenerateFindConcat(
@@ -78,7 +229,8 @@ namespace Nitload.RegularAnalysis.Text
{
// (?<name>.*)abc
if (RegexAstNode.IsNullOrEmpty(node) || node.Label != RegexElementLabel.CONCAT) {
if (RegexAstNode.IsNullOrEmpty(node) || node.Label != RegexElementLabel.CONCAT)
{
return false;
}
RegexAstNode group = node.Left;
@@ -106,6 +258,5 @@ namespace Nitload.RegularAnalysis.Text
// An escaped star is a literal and must not be classified as dot-star repetition.
#endregion
}
}