// ----------------------------------------------------------------------------- // 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 /// /// Generates one Match method from a fully prefixed IR stream. /// /// /// Not implemented yet. Returns an empty emitter so the generator pipeline stays runnable /// while the backend is being rebuilt. /// internal static SequenceEmitter 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 se = new(); List> sub_match_list = []; SourceGenContext sc = new(); RegexInfo info = source.Info; List ir = source.IRs; ReadOnlySpan ir_sp = CollectionsMarshal.AsSpan(ir); SequenceEmitter match_e = new(); int index = 0; int end = FindRegionEnd(ir_sp, index); ReadOnlySpan 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 _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 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 source, out bool accept ) { // range or single HashSet single_char = []; if (source[0] == '[') { int idx; accept = source[1] != '^'; idx = accept ? 1 : 2; List list = []; ReadOnlySpan 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 single_char, ReadOnlySpan seq, List sets ) { if (seq.Length == 0 || sets == null) { return; } int pos; ReadOnlySpan remain = seq; while ((pos = remain.IndexOf('\\')) != -1 || remain.Length > 0) { if (pos != 0) { ReadOnlySpan 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 } }