diff --git a/src/Nitload.RegularAnalysis/Text/TextRegexSourceGen.GenClass.cs b/src/Nitload.RegularAnalysis/Text/TextRegexSourceGen.GenClass.cs index 39047e19..6791bf37 100644 --- a/src/Nitload.RegularAnalysis/Text/TextRegexSourceGen.GenClass.cs +++ b/src/Nitload.RegularAnalysis/Text/TextRegexSourceGen.GenClass.cs @@ -220,8 +220,11 @@ namespace Nitload.RegularAnalysis.Text int max, bool is_greedy, [NotNull] in SequenceEmitter sc, + in List> func_list, in SourceGenContext context, - in ExitLabel exit + in ExitLabel exit, + IReadOnlyList? continuation = null, + int next = 0 ) { if (min < 0 || (max >= 0 && max < min)) { @@ -300,22 +303,60 @@ namespace Nitload.RegularAnalysis.Text } int additional_limit = max < 0 ? -1 : max - min; + bool has_continuation = continuation is not null && next < continuation.Count; + bool can_retry = has_continuation && additional_limit != 0; + string additional_entry = $"__additional_entry_{scratch_scope_index}"; + if (can_retry) { + _ = sc.AppendLine($"{ROS_char} {additional_entry} = {remain_span};"); + } + if (can_retry || (is_greedy && additional_limit > 0)) { + _ = sc.AppendLine($"int {additional_count} = 0;"); + } if (is_greedy && additional_limit != 0) { - _ = additional_limit < 0 ? - sc.AppendLine($"while (!{remain_span}.IsEmpty && {remain_span}[0] == {expected_literal})") + string limit = additional_limit < 0 ? string.Empty : + $"{additional_count} < {additional_limit} && "; + _ = sc.AppendLine($"while ({limit}!{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) + .AppendLine($"{remain_span} = {remain_span}.Slice(1);"); + if (can_retry || additional_limit > 0) { + _ = sc.AppendLine($"{additional_count}++;"); + } + _ = sc.AppendLine("}"); + } + if (can_retry) { - // 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."); + // The same optional count drives scanning and retries. Required units stay consumed. + ExitLabel retry = context.RequestExitLabel(ExitMode.Goto); + _ = sc.AppendLine("while (true)") + .AppendLine("{") + .AppendLine($"{remain_span} = {additional_entry}.Slice({additional_count});"); + GenerateConcatTail(continuation!, next, sc, func_list, context, retry); + _ = sc.AppendLine("break;") + .AppendLine($"{retry.GetLabelName()}: {{}}"); + if (is_greedy) + { + _ = sc.AppendLine($"if ({additional_count} == 0)") + .AppendLine("{") + .AppendLine($"{remain_span} = {additional_entry};") + .AppendLine($"goto {exit_label_node};") + .AppendLine("}") + .AppendLine($"{additional_count}--;"); + } else + { + string limit = additional_limit < 0 ? string.Empty : + $"{additional_count} == {additional_limit} || "; + _ = sc.AppendLine($"{remain_span} = {additional_entry}.Slice({additional_count});") + .AppendLine($"if ({limit}{remain_span}.IsEmpty || {remain_span}[0] != {expected_literal})") + .AppendLine("{") + .AppendLine($"{remain_span} = {additional_entry};") + .AppendLine($"goto {exit_label_node};") + .AppendLine("}") + .AppendLine($"{additional_count}++;"); + } + _ = sc.AppendLine("}"); + } else if (has_continuation) { + GenerateConcatTail(continuation!, next, sc, func_list, context, exit); } } @@ -399,8 +440,11 @@ namespace Nitload.RegularAnalysis.Text int max, bool is_greedy, [NotNull] in SequenceEmitter sc, + in List> func_list, in SourceGenContext context, - in ExitLabel exit + in ExitLabel exit, + IReadOnlyList? continuation = null, + int next = 0 ) { if (min < 0 || (max >= 0 && max < min)) { @@ -510,26 +554,60 @@ namespace Nitload.RegularAnalysis.Text } int additional_limit = max < 0 ? -1 : max - min; + bool has_continuation = continuation is not null && next < continuation.Count; + bool can_retry = has_continuation && additional_limit != 0; + string additional_entry = $"__additional_entry_{scratch_scope_index}"; + if (can_retry) { + _ = sc.AppendLine($"{ROS_char} {additional_entry} = {remain_span};"); + } + if (can_retry || (is_greedy && additional_limit > 0)) { + _ = sc.AppendLine($"int {additional_count} = 0;"); + } if (is_greedy && additional_limit != 0) { - if (additional_limit < 0) + string limit = additional_limit < 0 ? string.Empty : + $"{additional_count} < {additional_limit} && "; + _ = sc.AppendLine($"while ({limit}{remain_span}.{starts_with}({expected_literal}))") + .AppendLine("{") + .AppendLine($"{remain_span} = {remain_span}.Slice({literal.Length});"); + if (can_retry || additional_limit > 0) { + _ = sc.AppendLine($"{additional_count}++;"); + } + _ = sc.AppendLine("}"); + } + if (can_retry) + { + // Each retry moves by a complete literal using the scan's own optional counter. + ExitLabel retry = context.RequestExitLabel(ExitMode.Goto); + _ = sc.AppendLine("while (true)") + .AppendLine("{") + .AppendLine($"{remain_span} = {additional_entry}.Slice({additional_count} * {literal.Length});"); + GenerateConcatTail(continuation!, next, sc, func_list, context, retry); + _ = sc.AppendLine("break;") + .AppendLine($"{retry.GetLabelName()}: {{}}"); + if (is_greedy) { - _ = sc.AppendLine($"while ({remain_span}.{starts_with}({expected_literal}))") + _ = sc.AppendLine($"if ({additional_count} == 0)") .AppendLine("{") - .AppendLine($"{remain_span} = {remain_span}.Slice({literal.Length});") - .AppendLine("}"); + .AppendLine($"{remain_span} = {additional_entry};") + .AppendLine($"goto {exit_label_node};") + .AppendLine("}") + .AppendLine($"{additional_count}--;"); } else { - _ = sc.AppendLine($"for (int {additional_count} = 0; {additional_count} < {additional_limit} && {remain_span}.{starts_with}({expected_literal}); {additional_count}++)") + string limit = additional_limit < 0 ? string.Empty : + $"{additional_count} == {additional_limit} || "; + _ = sc.AppendLine($"{remain_span} = {additional_entry}.Slice({additional_count} * {literal.Length});") + .AppendLine($"if ({limit}!{remain_span}.{starts_with}({expected_literal}))") .AppendLine("{") - .AppendLine($"{remain_span} = {remain_span}.Slice({literal.Length});") - .AppendLine("}"); + .AppendLine($"{remain_span} = {additional_entry};") + .AppendLine($"goto {exit_label_node};") + .AppendLine("}") + .AppendLine($"{additional_count}++;"); } - } 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("}"); + } else if (has_continuation) { + GenerateConcatTail(continuation!, next, sc, func_list, context, exit); } } @@ -604,12 +682,10 @@ namespace Nitload.RegularAnalysis.Text 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); - } + // Keep the complete suffix visible even when CONCAT is left-associated. + List sequence = []; + AppendConcatNodes(node, sequence); + GenerateConcatTail(sequence, 0, sc, func_list, context, exit_label); } private static void GenerateCount( @@ -617,13 +693,25 @@ namespace Nitload.RegularAnalysis.Text [NotNull] in SequenceEmitter sc, in List> func_list, in SourceGenContext context, - in ExitLabel exit_label + in ExitLabel exit_label, + IReadOnlyList? continuation = null, + int next = 0 ) { RegexAstNode value = RegexAstNode.IsNullOrEmpty(node.Left) ? node.Right : node.Left; if (RegexAstNode.IsNullOrEmpty(value)) { throw new InvalidOperationException("COUNT requires an operand."); } + // Inspect transparent CONCAT wrappers without rewriting the repetition body. + while (value.Label == RegexElementLabel.CONCAT) + { + bool left_empty = RegexAstNode.IsNullOrEmpty(value.Left); + bool right_empty = RegexAstNode.IsNullOrEmpty(value.Right); + if (left_empty == right_empty) { + break; + } + value = left_empty ? value.Right : value.Left; + } RegexCountPayload payload = node.PayloadAs(); int min = payload.Min; int max = payload.Max; @@ -641,14 +729,16 @@ namespace Nitload.RegularAnalysis.Text switch (value.Label & RegexElementLabel.SEQUENCE) { case RegexElementLabel.UNIT: - GenerateUnitCount(value, min, max, is_greedy, sc, context, exit_label); - break; + GenerateUnitCount(value, min, max, is_greedy, sc, func_list, context, + exit_label, continuation, next); + return; 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; + GenerateUnitSpanCount(value, min, max, is_greedy, sc, func_list, context, + exit_label, continuation, next); + return; default: // TODO(NGRA): Diagnose malformed COUNT children instead of emitting an @@ -656,7 +746,9 @@ namespace Nitload.RegularAnalysis.Text break; } } - + if (continuation is not null) { + GenerateConcatTail(continuation, next, sc, func_list, context, exit_label); + } } #endregion diff --git a/src/Nitload.RegularAnalysis/Text/TextRegexSourceGen.GenSpecial.cs b/src/Nitload.RegularAnalysis/Text/TextRegexSourceGen.GenSpecial.cs index 9deefd61..d0724085 100644 --- a/src/Nitload.RegularAnalysis/Text/TextRegexSourceGen.GenSpecial.cs +++ b/src/Nitload.RegularAnalysis/Text/TextRegexSourceGen.GenSpecial.cs @@ -180,6 +180,50 @@ namespace Nitload.RegularAnalysis.Text #endregion + #region CONCAT continuation + + private static void AppendConcatNodes( + RegexAstNode node, + List sequence + ) + { + if (RegexAstNode.IsNullOrEmpty(node)) { + return; + } + if (node.Label == RegexElementLabel.CONCAT && + !RegexAstNode.IsDotStar(node.Left) && !IsNamedFind(node)) + { + AppendConcatNodes(node.Left, sequence); + AppendConcatNodes(node.Right, sequence); + } else { + sequence.Add(node); + } + } + + private static void GenerateConcatTail( + IReadOnlyList sequence, + int start, + in SequenceEmitter sc, + in List> func_list, + in SourceGenContext context, + in ExitLabel exit_label + ) + { + for (int i = start; i < sequence.Count; i++) + { + RegexAstNode node = sequence[i]; + if (node.Label == RegexElementLabel.COUNT) + { + // COUNT owns both its repetition and the continuation that may retry it. + GenerateCount(node, sc, func_list, context, exit_label, sequence, i + 1); + return; + } + GenerateMethodSource(node, sc, func_list, context, exit_label); + } + } + + #endregion + #region FIND concatenation private static bool TryGenerateFindConcat( diff --git a/test/StgSharpDebug/Program.cs b/test/StgSharpDebug/Program.cs index 0ff354d0..0406adfe 100644 --- a/test/StgSharpDebug/Program.cs +++ b/test/StgSharpDebug/Program.cs @@ -24,17 +24,8 @@ namespace Nitload private static string testExpPath = @"D:\Programmer\stepcode\data\ap203\ap203.exp"; - internal static unsafe void Main( - string[] args - ) + internal static unsafe void Main() { - // Select the managed-only emitter preview before initializing the normal runtime. - if (args.Length > 0 && args[0] == "--regex-preview") - { - Environment.ExitCode = TestRegexGenerate.Run(args[1..]); - return; - } - try { World.Initialize(new() @@ -47,7 +38,7 @@ namespace Nitload .InitializeModule(); // TestRegexAnalyse.Run(); - _ = TestRegexGenerate.Run("abc|123"); + _ = TestRegexGenerate.Run("abc1{1,2}23"); // TestHlsf(); diff --git a/test/StgSharpDebug/TestRegexGenerate.cs b/test/StgSharpDebug/TestRegexGenerate.cs index 6a53a4a1..4e09804b 100644 --- a/test/StgSharpDebug/TestRegexGenerate.cs +++ b/test/StgSharpDebug/TestRegexGenerate.cs @@ -1,6 +1,6 @@ // ----------------------------------------------------------------------------- // file="TestRegexGenerate" -// Project: Nitload +// Project: StgSharp // Copyright (c) Nitload. // SPDX-License-Identifier: MIT // ----------------------------------------------------------------------------- @@ -16,29 +16,19 @@ namespace Nitload { internal static class TestRegexGenerate { - public static int Run(params string[] args) - { - if (args.Length > 1) { - throw new ArgumentException("Expected at most one regex pattern.", nameof(args)); - } - if (args.Length == 1) - { - Print(args[0]); - return 0; - } - while (true) - { - Console.Write("regex> "); - string? pattern = Console.ReadLine(); - if (pattern is null or ":exit") { - return 0; - } - Print(pattern); - } + public static int Run( + string arg + ) + { + Console.WriteLine($"Text regex to be tested: {arg} :\n\n\n"); + Print(arg); + return 0; } - private static void Print(string pattern) + private static void Print( + string pattern + ) { TextRegexSource analysis = RegexAnalyzer.Analyze(pattern); if (analysis.AnalyseException is not null) { @@ -61,5 +51,6 @@ namespace Nitload } } } + } }