remove old generatoer, use new generator
This commit is contained in:
@@ -8,101 +8,162 @@
|
||||
using StgSharp.RegularAnalysis.Abstraction;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace StgSharp.RegularAnalysis.Text
|
||||
{
|
||||
internal partial class TextRegexSourceGen
|
||||
{
|
||||
/*
|
||||
* =======================================================================================
|
||||
* L1, several regions plus a tail
|
||||
* =======================================================================================
|
||||
*
|
||||
* Highest consumption in the dispatch chain, so these are offered the window first. Each
|
||||
* shape here was already recognised once during prefix emission, so a generator should
|
||||
* mirror the matching RegexAnalyzer.PrefixEmit recogniser rather than re-derive the shape.
|
||||
*
|
||||
* L1.2 ALT chain with a shared suffix
|
||||
* window L1.1 followed by linear instructions in the parent region
|
||||
* sample (ab|[cd]|.)x
|
||||
* nature flattening, may return -1
|
||||
*
|
||||
* L1.3 group tail
|
||||
* window region, POP FAIL with Prev_Fail, PACK SUCCESS with Prev_Success
|
||||
* mirrors TryEmitTryGroupTail
|
||||
* shape region block, capture written on the success branch
|
||||
* sample (?<name>abc)
|
||||
* nature flattening, may return -1
|
||||
*
|
||||
* L1.4 group tail wrapping an ALT chain
|
||||
* sample (?<a>abcd|a(bc)c)
|
||||
* nature flattening, may return -1
|
||||
*
|
||||
* L1.5 COUNT_COMPLEX plus body
|
||||
* window RegexCountComplexIR, then CommandCount entries
|
||||
* mirrors TryEmitComplexIR
|
||||
* shape loop wrapping the generated body
|
||||
* sample (ab){2,3}
|
||||
* nature REQUIRED, no fallback exists
|
||||
*
|
||||
* L1.6 COUNT_COMPLEX whose body contains an ALT chain, plus a linear suffix
|
||||
* sample (123(4|5)67){3,4}114514
|
||||
* nature REQUIRED, no fallback exists
|
||||
*
|
||||
* L1.7 FIND_COMPLEX plus body
|
||||
* window RegexFindComplexIR, then CommandCount entries
|
||||
* shape search loop wrapping the generated body
|
||||
* sample .*(ab|cd)
|
||||
* nature REQUIRED, no fallback exists
|
||||
*
|
||||
* L1.5 to L1.7 are required because a complex body is not a region of its own. L2 cannot
|
||||
* pick it up, so there is no degradation path if these are missing.
|
||||
*
|
||||
* CommandCount must be read from the prefixed IR. Prefix emission rebuilds these counts,
|
||||
* so the value carried by the pre-prefix IR no longer applies.
|
||||
*
|
||||
* Open questions to settle while implementing:
|
||||
* - how the cursor is restored between ALT cases
|
||||
* - which successful outcome a PACK uses as its capture span
|
||||
* - whether a region body ever deserves its own generated method instead of inlining
|
||||
*
|
||||
* =======================================================================================
|
||||
* L2, one region
|
||||
* =======================================================================================
|
||||
*
|
||||
* Consumes CommandCount + 1, the whole block a TRY lowers to. Together with L4 and L5 this
|
||||
* is the correctness core: once these three exist, every expression can be generated, just
|
||||
* with deeper nesting than L1 and L3 would produce.
|
||||
*
|
||||
* L2.1 RegexTryIR plus its body sample (abc)
|
||||
* L2.2 the same, body containing a nested region sample (a(bc))
|
||||
* L2.3 a single instruction region folded by prefix emission sample (a)
|
||||
*
|
||||
* Target shape, names owned by SourceGenContext:
|
||||
*
|
||||
* __rN_begin = __cur;
|
||||
* __rN_ok = false;
|
||||
* do
|
||||
* {
|
||||
* // body, a POP breaks out here
|
||||
* __rN_ok = __ok;
|
||||
* } while (false);
|
||||
* if (!__rN_ok) { __cur = __rN_begin; }
|
||||
* __ok = __rN_ok;
|
||||
*
|
||||
* Points the implementation has to settle:
|
||||
* - the failure path restores the cursor to the region entry
|
||||
* - break only leaves the nearest enclosing do block, so a nested region must not rely
|
||||
* on it to exit an outer one
|
||||
* - the whole match body is itself region zero, which is what makes a top level POP
|
||||
* legal
|
||||
*
|
||||
* =======================================================================================
|
||||
* L3, fixed multi instruction windows
|
||||
* =======================================================================================
|
||||
*
|
||||
* Consumes two to eight entries. Pure code quality work, never required for correctness.
|
||||
* Every one of these must return -1 cleanly when the window does not match, so the entry
|
||||
* falls through to L4.
|
||||
*
|
||||
* L3.1 FIND_COMPLEX FURTHEST COUNT:-1, PACK, MATCH_SEQ 3 (?<name>.*)234
|
||||
* L3.2 the same with a single element anchor 3 (?<name>.*)a
|
||||
* L3.3 FIND_SEQ, MATCH_SEQ, merged into one search 2 .*abc
|
||||
* L3.4 MATCH_SEQ, COUNT_SEQ, the expanded a+ shape 2 a{3,5}, a+
|
||||
* L3.5 a run of MATCH_SEQ and MATCH_SET, merged into one condition 2 to 8 a[abc]d
|
||||
* L3.6 trailing COUNT_SET over a dot, fast success 1 to 2 abc.*
|
||||
*
|
||||
* Two hazards to check for each of these:
|
||||
* - merging must not break the line indices an Observe_Line prefix refers to
|
||||
* - a merged window still has to leave __ok describing the whole window
|
||||
*/
|
||||
|
||||
private static int GenerateNamedCountAny(
|
||||
ReadOnlySpan<RegexIR> region_ir,
|
||||
private static int TryGenerateAltChain(
|
||||
int outer_level,
|
||||
ReadOnlySpan<RegexIR> ir_span,
|
||||
SourceGenContext sc,
|
||||
in SequenceEmitter<string> main_emitter,
|
||||
in List<SequenceEmitter<string>> sub_emitter_list
|
||||
in SequenceEmitter<string> code,
|
||||
in List<SequenceEmitter<string>> local_func
|
||||
)
|
||||
{
|
||||
if (region_ir[0] is not RegexCountSetIR { IsDotStar:true} ||
|
||||
region_ir[1] is not RegexPackIR pack) {
|
||||
/*
|
||||
* L1.1 ALT chain
|
||||
* window region, then further regions carrying Prev_Fail
|
||||
* mirrors TryEmitAltChain
|
||||
* shape if / else if / else
|
||||
* sample a|b, ab|cd|ef, a|bc|def
|
||||
* nature flattening, may return -1
|
||||
*/
|
||||
|
||||
/*
|
||||
* [004] 0001, DEF , DEF , NONE, 0000, MATCH_SEQ "abc"
|
||||
* [005] 0002, DEF , Sccs, NONE, 0000, MATCH_SEQ "d" <- an alt chain begins here
|
||||
* [006] 0002, Fail, Sccs, LINE, 0005, MATCH_SEQ "c" <- an alt chain ends here
|
||||
* [007] 0001, Fail, DEF , LINE, 0006, POP FAIL
|
||||
*/
|
||||
|
||||
const RegexIRPrefixMask simple_alt = RegexIRPrefixMask.Prev_Fail | RegexIRPrefixMask.Pop_Success;
|
||||
|
||||
int cur_level = ir_span[0].Prefix.WorkingRegion;
|
||||
if (cur_level == outer_level) {
|
||||
return -1;
|
||||
}
|
||||
if (region_ir[2] is RegexMatchSeqIR) { }
|
||||
_ = main_emitter.AppendLine($@"do").AppendLine($@"{{");
|
||||
_ = main_emitter.AppendLine($@"}}").AppendLine($@"while(true);");
|
||||
string group_idx = sc.MakeNewNamedGroup(pack.GroupName);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void GenerateRegion(
|
||||
ReadOnlySpan<RegexIR> region_ir,
|
||||
SourceGenContext sc,
|
||||
in SequenceEmitter<string> main_emitter,
|
||||
in List<SequenceEmitter<string>> sub_emitter_list
|
||||
)
|
||||
{
|
||||
int current_region = region_ir[0].Prefix.WorkingRegion;
|
||||
ReadOnlySpan<RegexIR> ir_sp = region_ir;
|
||||
ReadOnlySpan<RegexIR> scan_win;
|
||||
int index = 0;
|
||||
do
|
||||
int i = 1;
|
||||
RegexIR ir;
|
||||
for (; i < ir_span.Length; i++)
|
||||
{
|
||||
int result;
|
||||
int count = 0;
|
||||
scan_win = ir_sp[index..];
|
||||
if ((result =
|
||||
TryGenerateFindSeq(scan_win, sc, in main_emitter,
|
||||
in sub_emitter_list)) > 0)
|
||||
ir = ir_span[i];
|
||||
if ((ir.Prefix.Mask & simple_alt) == simple_alt)
|
||||
{
|
||||
count += result;
|
||||
} else
|
||||
if (TryGenerateMatchSeq(scan_win, main_emitter, sc) > 0)
|
||||
// simple match
|
||||
} else if (ir is RegexTryIR _try && (ir.Prefix.Mask & simple_alt) == simple_alt)
|
||||
{
|
||||
count += 1;
|
||||
} else if (TryGenerateMatchSet(scan_win, main_emitter, sc) > 0)
|
||||
{
|
||||
count++;
|
||||
} else if (TryGenerateFindSeq(scan_win, main_emitter, sc) > 0)
|
||||
{
|
||||
count++;
|
||||
} else { }
|
||||
index += count;
|
||||
} while (index < ir_sp.Length);
|
||||
}
|
||||
|
||||
private static int TryGenerateFindSeq(
|
||||
ReadOnlySpan<RegexIR> scan_win,
|
||||
SourceGenContext sc,
|
||||
in SequenceEmitter<string> main_emitter,
|
||||
in List<SequenceEmitter<string>> sub_emitter_list
|
||||
)
|
||||
{
|
||||
if (scan_win[0] is not RegexFindSeqIR find) {
|
||||
return -1;
|
||||
// match a try region
|
||||
ReadOnlySpan<RegexIR> body_span = ir_span.Slice(i + 1, i + _try.CommandCount);
|
||||
i += _try.CommandCount;
|
||||
}
|
||||
}
|
||||
const string cur_ptr = "cur_ptr",
|
||||
begin_ptr = "begin_ptr";
|
||||
_ = main_emitter.AppendLine()
|
||||
.AppendLine(@$"//Find sequence:{FormatStringLiteral(find.Pattern)} at any pos")
|
||||
.AppendLine(@"do")
|
||||
.AppendLine(@"{")
|
||||
.AppendLine(@$"int {cur_ptr}, {begin_ptr} = {current_ptr};")
|
||||
.AppendLine($@"{ROS_char} rest = ");
|
||||
|
||||
string finder = find.IsFirst ? "FirstIndexOf" : "LastIndexOf";
|
||||
|
||||
if (scan_win[1] is RegexMatchSeqIR seq)
|
||||
{
|
||||
string seq_pat = FormatStringLiteral($"{find.Pattern}{seq.Pattern}");
|
||||
_ = main_emitter.AppendLine(@$"//Match sequence:{seq_pat} at current pos")
|
||||
.AppendLine($@"if( )");
|
||||
}
|
||||
|
||||
_ = main_emitter.AppendLine("}").AppendLine(string.Empty);
|
||||
return 1;
|
||||
return i;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// file="TextRegexSourceGen.GenClass.Prefix"
|
||||
// Project: StgSharp
|
||||
// Copyright (c) Nitload.
|
||||
// SPDX-License-Identifier: MIT
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StgSharp.RegularAnalysis.Text
|
||||
{
|
||||
internal partial class TextRegexSourceGen
|
||||
{
|
||||
|
||||
/*
|
||||
* =======================================================================================
|
||||
* L5, prefix decoration
|
||||
* =======================================================================================
|
||||
*
|
||||
* A prefix consumes no IR of its own. It wraps whatever an L1 to L4 body generator
|
||||
* produced:
|
||||
*
|
||||
* [prefix open ] if (<observed result>) {
|
||||
* [ body ] ... sets __ok, advances __cur
|
||||
* [ line slot ] __lK_ok = __ok;
|
||||
* [ pop ] if (__ok) { __rN_ok = true; break; }
|
||||
* [prefix close] }
|
||||
*
|
||||
* Keeping this outside the body generators is what lets L2 and L4 stay small. They only
|
||||
* describe what an instruction does, never when it runs or who reads the outcome. Without
|
||||
* this layer the other layers cannot be assembled into correct control flow at all, so L5
|
||||
* is part of the correctness core rather than an optimisation.
|
||||
*
|
||||
* Checklist, RegexIRPrefixMask on the left:
|
||||
*
|
||||
* L5.1 ExeCondition none run unconditionally
|
||||
* L5.2 ExeCondition Prev_Success wrap in if (observed)
|
||||
* L5.3 ExeCondition Prev_Fail wrap in if (!observed)
|
||||
* L5.4 ExeCondition Always run unconditionally, deliberately detached from
|
||||
* the surrounding condition chain
|
||||
* L5.5 ObserveMethod none read the result of the preceding instruction
|
||||
* L5.6 ObserveMethod Observe_Line read the slot named by ObserveSource
|
||||
* L5.7 ObserveMethod Observe_Region read the slot named by ObserveSource
|
||||
* L5.8 PopCondition Pop_Success write the region result true, then leave it
|
||||
* L5.9 PopCondition Pop_Fail write the region result false, then leave it
|
||||
* L5.10 WorkingRegion selects which region scope the code lands in
|
||||
*
|
||||
* Rules the implementation has to hold:
|
||||
*
|
||||
* - Observe_Line and Observe_Region must read the slot ObserveSource names. Neither may
|
||||
* degrade into reading the previous instruction, which is what an absent observe
|
||||
* method means and nothing else.
|
||||
* - Line slots are allocated only for lines some prefix actually observes. That demand
|
||||
* has to be collected in a pass before body generation, because an observer can be
|
||||
* emitted before the line it reads, for instance when a nested region observes a line
|
||||
* of its parent.
|
||||
* - A pop lowers to break, which leaves the nearest enclosing do while(false) block.
|
||||
* That block is the region the instruction works in, because every region is emitted
|
||||
* as one and the whole match body is wrapped as region zero.
|
||||
* - Open and close must be paired. A body generator that declines the window must be
|
||||
* detected before the opening brace is emitted, so bodies are generated into a
|
||||
* detached emitter first.
|
||||
*
|
||||
* The open call should report how many braces it opened, so the close call knows how many
|
||||
* to emit rather than recomputing the condition.
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// file="TextRegexSourceGen.GenClass.SingleIR"
|
||||
// Project: StgSharp
|
||||
// Copyright (c) Nitload.
|
||||
// SPDX-License-Identifier: MIT
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StgSharp.RegularAnalysis.Text
|
||||
{
|
||||
internal partial class TextRegexSourceGen
|
||||
{
|
||||
|
||||
/*
|
||||
* =======================================================================================
|
||||
* L4, one instruction
|
||||
* =======================================================================================
|
||||
*
|
||||
* The fallback of the dispatch chain. Every RegexIRCommand needs a generator here, because
|
||||
* this is the only layer guaranteed to be offered every window. L1 and L3 may decline, L4
|
||||
* may not.
|
||||
*
|
||||
* Each generator consumes exactly one entry and describes only what the instruction does.
|
||||
* When it runs, which result it reads and whether it leaves the region early are all added
|
||||
* by L5, so nothing here should inspect RegexIR.Prefix beyond WorkingRegion.
|
||||
*
|
||||
* Contract for a generated body, names owned by SourceGenContext:
|
||||
*
|
||||
* - assign the outcome to __ok
|
||||
* - advance __cur only on success
|
||||
* - never index __cur past the end of the input
|
||||
* - declare scratch variables inside a block of their own, so a repeated shape does not
|
||||
* collide with itself
|
||||
*
|
||||
* A body must be generated into a detached emitter and only spliced in once it is known to
|
||||
* be claimed. Emitting a prefix for an instruction that turns out to have no generator
|
||||
* would open a brace nobody closes.
|
||||
*
|
||||
* Checklist:
|
||||
*
|
||||
* L4.1 MATCH_SEQ abc, \., a\*b
|
||||
* L4.2 MATCH_SET, discrete [abc], [123abc], [.]
|
||||
* L4.3 MATCH_SET, range [1-9]
|
||||
* L4.4 MATCH_SET, escape class \w
|
||||
* L4.5 MATCH_SET, any element .
|
||||
* L4.6 COUNT_SEQ a*, a?
|
||||
* L4.7 COUNT_SET [abc]*, \w+, .{11,13}, [.]*
|
||||
* L4.8 FIND_SEQ degradation path of .*abc
|
||||
* L4.9 FIND_SET -
|
||||
* L4.10 POP ALT tail
|
||||
* L4.11 PACK (abc), ordinal group
|
||||
*
|
||||
* Semantics worth pinning down before writing these:
|
||||
*
|
||||
* - [.] and . are different sets. The former is a literal dot, the latter is any
|
||||
* element, and [.]* must not be folded into the dot star optimisation.
|
||||
* - \w has to keep full .NET semantics. TextRegex.IsCharWord is inherited by the
|
||||
* generated class and already carries them, so the generated code can call it
|
||||
* directly instead of approximating with an ASCII test.
|
||||
* - This backend does not backtrack. A lazy repetition therefore consumes exactly its
|
||||
* lower bound and leaves the rest to the following anchor. Lazy forms needing more
|
||||
* are expected to have been rewritten into a find by the IR optimizer, so confirm
|
||||
* that before adding backtracking here.
|
||||
* - An epsilon sequence and an empty repetition unit both reach these generators.
|
||||
* Decide whether they are vacuous successes or an optimizer bug worth reporting.
|
||||
* - PACK decides nothing, so it should leave __ok untouched. Whether it runs at all is
|
||||
* already expressed by its execution condition.
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -6,32 +6,104 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using StgSharp.RegularAnalysis.Abstraction;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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 and L5 together are sufficient for correctness. L1 and L3 only flatten 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.
|
||||
*
|
||||
* 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";
|
||||
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.4 Emit the whole pattern guard from RegexInfo.MinPredictLength.
|
||||
// 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();
|
||||
@@ -47,7 +119,8 @@ namespace StgSharp.RegularAnalysis.Text
|
||||
|
||||
while (true)
|
||||
{
|
||||
GenerateRegion(region, sc, match_e, sub_match_list);
|
||||
// GenerateRegion(region, sc, match_e, sub_match_list);
|
||||
_ = TryGenerateAltChain(0, region, sc, match_e, sub_match_list);
|
||||
if (end >= ir_sp.Length)
|
||||
{
|
||||
break;
|
||||
@@ -72,7 +145,7 @@ namespace StgSharp.RegularAnalysis.Text
|
||||
.AppendLine($@"int {offset} = 0;")
|
||||
.AppendLine();
|
||||
|
||||
_ = sc.GenVarDefine(se);
|
||||
// _ = sc.GenVarDefine(se);
|
||||
|
||||
_ = se.Append(match_e);
|
||||
|
||||
@@ -87,6 +160,10 @@ namespace StgSharp.RegularAnalysis.Text
|
||||
return se;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region shared helpers
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int FindRegionEnd(
|
||||
ReadOnlySpan<RegexIR> ir_sp,
|
||||
@@ -111,151 +188,11 @@ namespace StgSharp.RegularAnalysis.Text
|
||||
return SyntaxFactory.Literal(str).Text;
|
||||
}
|
||||
|
||||
#region sub generator
|
||||
|
||||
private static int TryGenerateFindSeq(
|
||||
ReadOnlySpan<RegexIR> scan_win,
|
||||
SequenceEmitter<string> se,
|
||||
SourceGenContext ng
|
||||
private static string FormatCharLiteral(
|
||||
char value
|
||||
)
|
||||
{
|
||||
if (scan_win[0] is not RegexFindSeqIR seq) {
|
||||
return -1;
|
||||
}
|
||||
string pattern = seq.Pattern;
|
||||
int length = pattern.Length;
|
||||
string index_caller = seq.IsFirst ? "IndexOf" : "LastIndexOf";
|
||||
|
||||
_ = se.AppendLine()
|
||||
.AppendLine($@"if(({offset} = [{current_ptr}..].{index_caller}({pattern})) != -1)")
|
||||
.AppendLine($@"{{")
|
||||
.AppendLine($@"{current_ptr} += {offset} + {length};")
|
||||
.AppendLine($@"}}");
|
||||
return length;
|
||||
}
|
||||
|
||||
private static int TryGenerateMatchSeq(
|
||||
ReadOnlySpan<RegexIR> scan_win,
|
||||
SequenceEmitter<string> se,
|
||||
SourceGenContext ng
|
||||
)
|
||||
{
|
||||
if (scan_win[0] is not RegexFindSeqIR seq) {
|
||||
return -1;
|
||||
}
|
||||
string pattern = FormatStringLiteral(seq.Pattern);
|
||||
int length = pattern.Length;
|
||||
|
||||
_ = se.AppendLine()
|
||||
.AppendLine($@"if([{current_ptr}..({current_ptr} + {length})] == {pattern})")
|
||||
.AppendLine($@"{{")
|
||||
.AppendLine($@"{current_ptr} += {offset} + {length};")
|
||||
.AppendLine($@"}}");
|
||||
return length;
|
||||
}
|
||||
|
||||
private static int TryGenerateMatchSet(
|
||||
ReadOnlySpan<RegexIR> scan_win,
|
||||
SequenceEmitter<string> se,
|
||||
SourceGenContext ng
|
||||
)
|
||||
{
|
||||
if (scan_win[0] is not RegexMatchSeqIR seq) {
|
||||
return -1;
|
||||
}
|
||||
string pattern = seq.Pattern;
|
||||
RegexCharSet[] set_pattern = FigureCharSetType(pattern, out bool accept);
|
||||
|
||||
string var_char = ng.CurChar;
|
||||
_ = se.AppendLine($@"// match charset of {pattern}")
|
||||
.AppendLine($@"char {var_char} = {orig_text}[{current_ptr}];")
|
||||
.AppendLine();
|
||||
|
||||
_ = se.AppendLine("if(");
|
||||
RegexCharSet p = set_pattern[0];
|
||||
string _case = GenerateCase(p);
|
||||
string _case_begin = accept ? string.Empty : "!(";
|
||||
_ = se.AppendLine($@"if ({_case_begin}{_case}||");
|
||||
|
||||
for (int i = 1; i < set_pattern.Length - 1; i++)
|
||||
{
|
||||
p = set_pattern[i];
|
||||
_case = GenerateCase(p);
|
||||
if (string.IsNullOrEmpty(_case))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
_ = se.AppendLine($@" {_case} ||");
|
||||
}
|
||||
|
||||
p = set_pattern[^1];
|
||||
_case = GenerateCase(p);
|
||||
string _case_end = accept ? string.Empty : ")";
|
||||
_ = se.AppendLine($@"{_case}{_case_end})")
|
||||
.AppendLine("{")
|
||||
.AppendLine($@"{current_ptr} += 1;")
|
||||
.AppendLine("}");
|
||||
|
||||
return 1;
|
||||
|
||||
|
||||
string GenerateCase(
|
||||
RegexCharSet c
|
||||
)
|
||||
{
|
||||
switch (c.Type)
|
||||
{
|
||||
// Handle single character
|
||||
case CharSetType.Single:
|
||||
string set = c.Value;
|
||||
if (set.Length == 1)
|
||||
{
|
||||
return $@"{var_char} == '{c.Value}'";
|
||||
} else if (set.Length < 5)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
_ = sb.Append('(');
|
||||
foreach (char item in set) {
|
||||
_ = sb.Append(CultureInfo.InvariantCulture, $@"item == '{item}' or ");
|
||||
}
|
||||
_ = sb.Append(')');
|
||||
return sb.ToString();
|
||||
} else
|
||||
{
|
||||
return $@"""{c.Value}"".AsSpan().Contains({var_char})";
|
||||
}
|
||||
|
||||
// Handle range of characters
|
||||
case CharSetType.Range:
|
||||
return $@"({var_char} >= '{c.Value[0]}' && {var_char} <= '{c.Value[1]}')";
|
||||
|
||||
// Handle set of characters
|
||||
case CharSetType.Set:
|
||||
if (c.Accept)
|
||||
{
|
||||
return c.Value switch
|
||||
{
|
||||
"s" => $@"char.IsWhiteSpace({var_char})",
|
||||
"w" => $@"char.IsLetterOrDigit({var_char}) || {var_char} == '_'",
|
||||
"d" => $@"char.IsDigit({var_char})",
|
||||
_ => throw new InvalidCastException(),
|
||||
};
|
||||
} else
|
||||
{
|
||||
return c.Value switch
|
||||
{
|
||||
"s" => $@"!char.IsWhiteSpace({var_char})",
|
||||
"w" => $@"!char.IsLetterOrDigit({var_char}) && {var_char} != '_'",
|
||||
"d" => $@"!char.IsDigit({var_char})",
|
||||
_ => throw new InvalidCastException(),
|
||||
};
|
||||
}
|
||||
case CharSetType.None:
|
||||
return string.Empty;
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
return SyntaxFactory.Literal(value).Text;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -19,86 +19,177 @@ namespace StgSharp.RegularAnalysis.Text
|
||||
|
||||
#region var naming
|
||||
|
||||
private class SourceGenContext
|
||||
/// <summary>
|
||||
/// Tracks variable naming and declaration demand while generating one match method.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Method level state is limited to the active cursor and the result of the most recent
|
||||
/// instruction. Everything else is either a region slot, a line slot, a capture slot, or
|
||||
/// a block scoped scratch variable. Scratch names are never reused, so a nested block can
|
||||
/// always declare its own without colliding with an enclosing one.
|
||||
/// </remarks>
|
||||
private sealed class SourceGenContext
|
||||
{
|
||||
|
||||
public bool Need_char_cur { get; set; }
|
||||
/// <summary>
|
||||
/// Active pointer. Indexes the next unconsumed element of the input.
|
||||
/// </summary>
|
||||
public const string Cursor = "__cur";
|
||||
|
||||
public bool Need_remain_cur { get; set; }
|
||||
/// <summary>
|
||||
/// Name of the match method parameter holding the input sequence.
|
||||
/// </summary>
|
||||
public const string InputSpan = "text";
|
||||
|
||||
public bool Need_find_result { get; set; }
|
||||
/// <summary>
|
||||
/// Result of the instruction emitted immediately before the current one.
|
||||
/// </summary>
|
||||
public const string StepResult = "__ok";
|
||||
|
||||
private const string __char_cur = "__char_cur";
|
||||
private const string __remain_cur = "__remain_cur";
|
||||
private const string __find_result = "__find_result";
|
||||
private readonly Dictionary<string, int> _groupIndex = [];
|
||||
private readonly SortedSet<int> _lineSlots = [];
|
||||
private readonly HashSet<int> _observedLines = [];
|
||||
private readonly SortedSet<int> _regionSlots = [];
|
||||
|
||||
private int _min_region_begin_index;
|
||||
private int _anonymousGroup;
|
||||
private int _scratchIndex;
|
||||
|
||||
public string MakeNewRegionBegin()
|
||||
/// <summary>
|
||||
/// Records that some prefix observes the result of the instruction at line
|
||||
/// <paramref name="line" />.
|
||||
/// </summary>
|
||||
public void MarkLineObserved(
|
||||
int line
|
||||
)
|
||||
{
|
||||
string name = $"__region_begin_{_min_region_begin_index}";
|
||||
_min_region_begin_index++;
|
||||
return name;
|
||||
_ = _observedLines.Add(line);
|
||||
}
|
||||
|
||||
public string CurChar
|
||||
/// <summary>
|
||||
/// Indicates whether the instruction at line <paramref name="line" /> has to persist
|
||||
/// its result into a line slot.
|
||||
/// </summary>
|
||||
public bool IsLineObserved(
|
||||
int line
|
||||
)
|
||||
{
|
||||
get
|
||||
{
|
||||
Need_char_cur = true;
|
||||
return __char_cur;
|
||||
}
|
||||
return _observedLines.Contains(line);
|
||||
}
|
||||
|
||||
#region namig group
|
||||
/// <summary>
|
||||
/// Allocates a fresh block scoped scratch variable name.
|
||||
/// </summary>
|
||||
/// <param name="tag">
|
||||
/// Short role marker, for example <c> n </c> for a counter or <c> ch </c> for a
|
||||
/// character.
|
||||
/// </param>
|
||||
public string NewScratch(
|
||||
string tag
|
||||
)
|
||||
{
|
||||
return $"__{tag}{_scratchIndex++}";
|
||||
}
|
||||
|
||||
private int _min_named_group;
|
||||
private Dictionary<string, int> _name_idx_map = [];
|
||||
/// <summary>
|
||||
/// Name of the slot holding the cursor value captured when region
|
||||
/// <paramref name="region" /> was entered.
|
||||
/// </summary>
|
||||
public string RegionAnchor(
|
||||
int region
|
||||
)
|
||||
{
|
||||
_ = _regionSlots.Add(region);
|
||||
return $"__r{region}_begin";
|
||||
}
|
||||
|
||||
public string MakeNewNamedGroup(
|
||||
/// <summary>
|
||||
/// Name of the slot holding the match result of region <paramref name="region" />.
|
||||
/// </summary>
|
||||
public string RegionResult(
|
||||
int region
|
||||
)
|
||||
{
|
||||
_ = _regionSlots.Add(region);
|
||||
return $"__r{region}_ok";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Name of the slot holding the match result of the instruction at line
|
||||
/// <paramref name="line" />. Only lines actually referenced by an
|
||||
/// <see cref="RegexIRPrefixMask.Observe_Line" /> prefix get a slot.
|
||||
/// </summary>
|
||||
public string LineResult(
|
||||
int line
|
||||
)
|
||||
{
|
||||
_ = _lineSlots.Add(line);
|
||||
return $"__l{line}_ok";
|
||||
}
|
||||
|
||||
public string GroupBegin(
|
||||
string name
|
||||
)
|
||||
{
|
||||
if (_name_idx_map.TryGetValue(name, out int idx)) {
|
||||
return $"__named_group_{idx}";
|
||||
}
|
||||
idx = _min_named_group;
|
||||
_name_idx_map[name] = idx;
|
||||
_min_named_group++;
|
||||
return $"__named_group_{idx}";
|
||||
return $"__g{ResolveGroup(name)}_begin";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public string FindResult
|
||||
{
|
||||
get
|
||||
{
|
||||
Need_find_result = true;
|
||||
return __find_result;
|
||||
}
|
||||
}
|
||||
|
||||
public string CurRemain
|
||||
{
|
||||
get
|
||||
{
|
||||
Need_remain_cur = true;
|
||||
return __remain_cur;
|
||||
}
|
||||
}
|
||||
|
||||
public SequenceEmitter<string> GenVarDefine(
|
||||
SequenceEmitter<string> se
|
||||
public string GroupEnd(
|
||||
string name
|
||||
)
|
||||
{
|
||||
if (se is null) {
|
||||
return $"__g{ResolveGroup(name)}_end";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits declarations for every slot requested during body generation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Must be called after the method body has been generated, but the produced lines
|
||||
/// have to be placed before it.
|
||||
/// </remarks>
|
||||
public SequenceEmitter<string> EmitDeclarations(
|
||||
SequenceEmitter<string> emitter
|
||||
)
|
||||
{
|
||||
if (emitter is null) {
|
||||
return null!;
|
||||
}
|
||||
_ = se.AppendLine($@"int {__char_cur}", Need_char_cur)
|
||||
.AppendLine($@"int {__remain_cur}", Need_remain_cur);
|
||||
|
||||
return se;
|
||||
_ = emitter.AppendLine($@"int {Cursor} = 0;")
|
||||
.AppendLine($@"bool {StepResult} = true;");
|
||||
|
||||
foreach (int region in _regionSlots)
|
||||
{
|
||||
_ = emitter.AppendLine($@"int {RegionAnchor(region)} = 0;")
|
||||
.AppendLine($@"bool {RegionResult(region)} = false;");
|
||||
}
|
||||
|
||||
foreach (int line in _lineSlots) {
|
||||
_ = emitter.AppendLine($@"bool {LineResult(line)} = false;");
|
||||
}
|
||||
|
||||
foreach (int group in _groupIndex.Values)
|
||||
{
|
||||
_ = emitter.AppendLine($@"int __g{group}_begin = -1;")
|
||||
.AppendLine($@"int __g{group}_end = -1;");
|
||||
}
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
private int ResolveGroup(
|
||||
string name
|
||||
)
|
||||
{
|
||||
string key = string.IsNullOrEmpty(name) ?
|
||||
$"#{_anonymousGroup++}" :
|
||||
name;
|
||||
if (_groupIndex.TryGetValue(key, out int index)) {
|
||||
return index;
|
||||
}
|
||||
index = _groupIndex.Count;
|
||||
_groupIndex[key] = index;
|
||||
return index;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user