Files
stgsharp/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.GenClass.cs
T

366 lines
14 KiB
C#

// -----------------------------------------------------------------------------
// file="TextRegexSourceGen.GenClass"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using Microsoft.CodeAnalysis.CSharp;
using StgSharp.RegularAnalysis.Abstraction;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace StgSharp.RegularAnalysis.Text
{
internal partial class TextRegexSourceGen
{
/*
* ---------------------------------------------------------------------------------------
* NGRA text regex source generation backend
* ---------------------------------------------------------------------------------------
*
* The backend turns a fully prefixed IR stream into the body of one Match method. It is
* organised in layers, ordered by how many IR entries a single generator consumes:
*
* L0 whole IR stream method skeleton, slot declarations, entry guards
* L1 several regions ALT chain, group tail, complex count, complex find
* L2 one region the block a TRY lowers to
* L3 two to eight IR fixed windows worth collapsing into better code
* L4 one IR the fallback, one generator per RegexIRCommand
* L5 zero IR prefix decoration wrapped around L1 to L4 output
*
* File layout:
*
* TextRegexSourceGen.GenClass.cs this file, L0 and shared helpers
* TextRegexSourceGen.GenClass.MultiIR.cs L1, L2 and L3
* TextRegexSourceGen.GenClass.SingleIR.cs L4
* TextRegexSourceGen.GenClass.Prefix.cs L5
* TextRegexSourceGen.GenContext.cs variable naming and slot allocation
*
* Contract every generator honours:
*
* 1. A TryGenerateXxx returns how many IR entries it consumed, or -1 when the window
* does not match its shape. It returns an entry count, never a character count or a
* pattern length.
* 2. The dispatch chain offers a window to L1, then L2, then L3, then L4. Higher
* consumption wins, so a shape spanning a whole region plus a tail is never split by
* a smaller generator.
* 3. L2, L4, L5 and the required entries of L1 together are sufficient for correctness.
* The flattening entries of L1 and all of L3 only improve the shape of what those
* would already emit, so an unimplemented one must return -1 cleanly rather than
* emit a partial result.
* 4. A prefix consumes no IR. L1 to L4 describe what an instruction does, L5 decides
* when it runs and who reads the outcome.
* 5. Scope follows Prefix.WorkingRegion, not expression nesting.
*
* Control flow shape:
*
* A region lowers to do { ... } while(false), and POP lowers to break. The goto plus
* label scheme used by the .NET generated regex backend is deliberately not adopted.
*
* A FIND lowers to a candidate retry loop nested inside the region block. That loop is
* nearer than the region do block, so a POP inside it needs the __rN_done flag described
* in TextRegexSourceGen.GenClass.MultiIR.cs rather than a plain break.
*
* Generated variable names are owned by SourceGenContext. No generator invents one.
*
* The per shape checklist this backend is rebuilt against lives in
* future/ngra-regex-sourcegen-goldens.md, and the handwritten target code for each shape
* lives in test/SingleFile/regex.cs.
*/
// private const string current_ptr = "cur_ptr_global"; // global match cursor
// private const string offset = "offset"; //
// private const string orig_text = "text"; //
#region L0 method level
/// <summary>
/// Generates one <c> Match </c> method from a fully prefixed IR stream.
/// </summary>
/// <remarks>
/// Not implemented yet. Returns an empty emitter so the generator pipeline stays runnable
/// while the backend is being rebuilt.
/// </remarks>
internal static SequenceEmitter<string> GenerateSource(
TextRegexSource source
)
{
//
// L0, method skeleton
//
// TODO L0.1 Emit the Match signature from ROS_char and SourceGenContext.InputSpan.
// TODO L0.2 Emit region anchor and region result slots, count from RegexInfo.RegionCount.
// TODO L0.3 Emit line result slots, only for lines an Observe_Line prefix refers to.
// TODO L0.5 Emit the cursor, the step result and any scratch declaration the context collected.
//
/*
* Slot demand is only known once the body exists, so the body has to be generated
* into a detached emitter first and spliced in after the declaration block.
*
* The dispatch chain that walks the IR stream belongs here too. It offers each window
* to L1, L2, L3 then L4, and adds the returned consumption count to the cursor. An
* unclaimed window must produce a hard build break rather than silently emit nothing,
* because code that compiles but matches incorrectly is far harder to diagnose.
*/
SequenceEmitter<string> se = new();
List<SequenceEmitter<string>> sub_match_list = [];
SourceGenContext sc = new();
RegexInfo info = source.Info;
List<RegexIR> ir = source.IRs;
ReadOnlySpan<RegexIR> ir_sp = CollectionsMarshal.AsSpan(ir);
SequenceEmitter<string> match_e = new();
int index = 0;
int end = FindRegionEnd(ir_sp, index);
ReadOnlySpan<RegexIR> region = ir_sp[0..end];
while (true)
{
// GenerateRegion(region, sc, match_e, sub_match_list);
_ = TryGenerateAltChain(0, region, sc, match_e, sub_match_list);
_ = TryGenerateTryBlock(0, region, sc, match_e, sub_match_list);
_ = TryGenerateNamedCountAny(0, region, sc, match_e, sub_match_list);
if (end >= ir_sp.Length)
{
break;
}
index = end;
end = FindRegionEnd(ir_sp, end);
region = ir_sp[index..end];
}
_ = se.AppendLine($@"public override MatchResult Match({ROS_char} text)")//
.AppendLine(@"{");
int min_predict_length = info.MinPredictLength;
if (min_predict_length > 0) {
_ = se.AppendLine($@"if (text.Length < {min_predict_length})")
.AppendLine(@"{")
.AppendLine(@"return default;")
.AppendLine(@"}")
.AppendLine();
}
int region_count = info.RegionCount;
for (int i = 0; i < region_count; i++)
{
_ = se.AppendLine($@"int region_{i}_start = -1;")//
.AppendLine($@"int region_{i}_end = -1;")
.AppendLine($@"bool region_{i}_success;");
}
/*
_ = se.AppendLine($@"int {current_ptr} = 0;")
.AppendLine($@"int {offset} = 0;")
.AppendLine();
/**/
// _ = sc.GenVarDefine(se);
_ = se.Append(match_e);
foreach (SequenceEmitter<string> _sub in sub_match_list)
{
_ = se.AppendLine() //
.Append(_sub);
}
_ = se.Append(@"}");
return se;
}
#endregion
private enum RegionExitMethod
{
Break,
Continue
}
#region shared helpers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int FindRegionEnd(
ReadOnlySpan<RegexIR> ir_sp,
int current
)
{
int cur_depth = ir_sp[current].Prefix.WorkingRegion;
int end = ir_sp.Length;
for (; end > 0; end--)
{
if (ir_sp[end].Prefix.WorkingRegion == cur_depth) {
return end;
}
}
return 1;
}
private static string FormatStringLiteral(
string str
)
{
return SyntaxFactory.Literal(str).Text;
}
private static string FormatCharLiteral(
char value
)
{
return SyntaxFactory.Literal(value).Text;
}
#endregion
#region charset config
internal enum CharSetType
{
None,
Single,
Range,
Set
}
internal record class RegexCharSet(
string Value,
CharSetType Type,
bool Accept
)
{
public override string ToString()
{
return $"{{value:{Value}, type:{Type}, accept:{Accept}}}";
}
}
private static RegexCharSet[] FigureCharSetType(
ReadOnlySpan<char> source,
out bool accept
)
{
// range or single
HashSet<char> single_char = [];
if (source[0] == '[')
{
int idx;
accept = source[1] != '^';
idx = accept ? 1 : 2;
List<RegexCharSet> list = [];
ReadOnlySpan<char> seq = source.Slice(idx, source.Length - idx - 1);
if (seq.IndexOf('-') == -1)
{
SplitSet(single_char, seq, list);
StringBuilder sb = new();
foreach (char c in single_char) {
_ = sb.Append(c);
}
list.Add(new(sb.ToString(), CharSetType.Single, true));
} else
{
for (; seq.Length > 0; )
{
int pos = seq.IndexOf('-');
if (pos == -1)
{
// has no range
SplitSet(single_char, seq, list);
break;
} else if (pos > 1)
{
// has other sequence before range
SplitSet(single_char, seq[0..(pos - 1)], list);
}
list.Add(new($"{seq[pos - 1]}{seq[pos + 1]}", CharSetType.Range, true));
seq = seq[(pos + 2)..];
}
StringBuilder sb = new();
foreach (char c in single_char) {
_ = sb.Append(c);
}
list.Add(new(sb.ToString(), CharSetType.Single, true));
return [.. list];
}
} else if (source[0] == '\\')
{
// set
accept = char.IsAsciiLetterUpper(source[0]);
return char.ToLower(source[0], CultureInfo.InvariantCulture)
switch
{
's' => [new("s", CharSetType.Set, accept)],
'w' => [new("w", CharSetType.Set, accept)],
'd' => [new("d", CharSetType.Set, accept)],
_ => throw new NotSupportedException()
};
} else
{
// unknown, throw
throw new InvalidCastException($"Unknown char set: {source}");
}
static void SplitSet(
HashSet<char> single_char,
ReadOnlySpan<char> seq,
List<RegexCharSet> sets
)
{
if (seq.Length == 0 || sets == null) {
return;
}
int pos;
ReadOnlySpan<char> remain = seq;
while ((pos = remain.IndexOf('\\')) != -1 || remain.Length > 0)
{
if (pos != 0)
{
ReadOnlySpan<char> singles = remain[0..pos];
sets.Add(new RegexCharSet(singles.ToString(), CharSetType.Single, true));
}
bool accept = char.IsAsciiLetterUpper(remain[pos + 1]);
switch (char.ToLower(remain[pos + 1], CultureInfo.InvariantCulture))
{
case 's':
sets.Add(new("s", CharSetType.Set, accept));
break;
case 'w':
sets.Add(new("w", CharSetType.Set, accept));
break;
case 'd':
sets.Add(new("d", CharSetType.Set, accept));
break;
case '['or'('or')'or']'or'\\'or'\"':
_ = single_char.Add(remain[pos + 1]);
break;
default:
throw new InvalidCastException($"Unknown char or charset {remain[0..1]}");
}
remain = remain[(pos + 2)..];
}
if (remain.Length > 0)
{
foreach (char c in remain) {
_ = single_char.Add(c);
}
}
}
}
#endregion
}
}