Files
stgsharp/future/ngra-regex-sourcegen-goldens.md
T

12 KiB
Raw Blame History

NGRA Regex 源生成 Checklist

本文件按一次扫描消耗的前缀 IR 数量分层,记录源生成器需要实现的形态。 消耗量从高到低排列,从整个 region 的识别一直降到单条 MATCH 指令。

为什么按 IR 消耗量分层

早期版本按正则语法分批(基础匹配 / 计数 / ALT / 捕获组……),这描述的是表达式形态。 但 EmitPrefix 之后,生成器面对的不再是表达式,而是带前缀的指令窗口:

  • a|b 与 (?<a>abcd|a(bc)c) 在语法上分属两批,在前缀 IR 里却共用同一个 「region 链 + Prev_Fail 切换」骨架;
  • abc 与 a[abc]d 在语法上同属一批,消耗的 IR 条数却不同;
  • WorkingRegion / ExeCondition / ObserveMethod / PopCondition 是横切在每条指令上的,任何按语法组织的清单都无法表达它们。

因此清单以「一个 TryGenerateXxx 一次吞掉几条 IR」为轴,与 RegexAnalyzer.PrefixEmit 里 TryEmitXxx 的调度链保持同构。

生成器统一约定

这些约定是所有层级的前提,先固定下来再写具体层:

  1. 返回值语义:所有 TryGenerateXxx 返回本次消耗的 IR 条数,-1 表示窗口不匹配。 不得返回 pattern 长度、字符数等其它含义的整数。
  2. 调度链顺序:调度链的 if / else if 严格按 L1 → L2 → L3 → L4 排列, 层号即消耗量的递减顺序,高消耗形态优先尝试,落空后自然降级到单指令。
  3. 窗口约定:扫描窗口为 ir_sp[index..],长度不足时由各 TryGenerate 自行短路返回 -1。 窥探深度参照 IR 优化器的 8 条窗口。
  4. 前缀不参与消耗计数:前缀装饰(L5)不单独消耗 IR,它包裹在 L1–L4 产出的代码外层。
  5. 作用域归属:代码块的作用域由 Prefix.WorkingRegion 决定,不由表达式嵌套决定。
  6. 控制流形态:region 一律生成 do { ... } while(false) + break, 不使用 .NET generated regex 的 goto + 标签方案。

正确性核心与纯优化的分界

  • L2 + L4 + L5 是正确性核心:region 作用域、单指令主体、前缀装饰三者齐备, 任意表达式即可端到端生成出语义正确的代码。
  • L1 与 L3 是纯优化:它们只把 L2/L4 会生成的嵌套结构压平成更好的代码形状。 未实现时必须干净地返回 -1,让窗口自然降级,不得影响正确性。

这条分界决定了实现顺序:先把 L2/L4/L5 打通拿到可运行基线,再回头做 L1/L3。

记录模板

层级:

触发窗口:

消耗 IR 数:

覆盖样本:

期望代码形状:

Optimized IR:

IR -> Code 规则:

前缀交互:

备注:

L0 方法级

一次消耗整个 IR 列表 + RegexInfo,只产出骨架,不产出任何匹配逻辑。

# 触发 消耗 产出 状态
L0.1 TextRegexSource 入口 全部 Match(ReadOnlySpan<char>) 签名与 MatchResult 返回 [ ]
L0.2 RegionCount 元数据 每个 region 的 start / end / success 槽 [ ]
L0.3 SingleLineResultCount 元数据 Observe_Line 所需的行结果槽 [ ]
L0.4 MinPredictLength 元数据 入口处最小长度断言(可延后) [ ]
L0.5 — — 活动指针、锚点指针、SourceGenContext 按需变量声明 [ ]

样本:任意,用 abc 验证骨架能编译即可。

关注点:region 槽是否必须全量分配、行结果槽能否按 ObserveSource 稀疏分配。

L1 Region 组合级(纯优化)

一次消耗多个 region + 尾部控制指令,是窗口消耗量最高的形态。 这些组合在 EmitPrefix 里已经被识别过一次 (TryEmitAltChain / TryEmitTryGroupTail / TryEmitComplexIR),生成器应与之一一对应。

除 L1.5–L1.7 外,本层全部是代码形状优化:不实现时窗口会降级到 L2 + L5, 生成出语义等价但嵌套更深的代码。

# 触发窗口 消耗 覆盖样本 性质 状态
L1.1 region 链,后继 region 带 Prev_Fail Σ region a|b、ab|cd|ef、a|bc|def 压平 [ ]
L1.2 同上 + 公共后缀 Σ region + 后缀 (ab|[cd]|.)x 压平 [ ]
L1.3 region + POP FAIL(Prev_Fail) + PACK SUCCESS(Prev_Success) region + 2 (?<name>abc) 压平 [ ]
L1.4 L1.3 且 region 内部是 ALT 链 嵌套 (?<a>abcd|a(bc)c) 压平 [ ]
L1.5 COUNT_COMPLEX + body CommandCount + 1 (ab){2,3} 必需 [ ]
L1.6 COUNT_COMPLEX + body(含 ALT) + 线性后缀 嵌套 + 后缀 (123(4|5)67){3,4}114514 必需 [ ]
L1.7 FIND_COMPLEX + body CommandCount + 1 .*(ab|cd) 必需 [ ]

期望代码形状:L1.1/L1.2 压平为 if / else if / else 链;L1.3/L1.4 在 region 成功分支写捕获区间; L1.5–L1.7 为循环 / 搜索包裹 body,CommandCount 必须按前缀化后的实际条数重算。

L1.5–L1.7 标记为必需,是因为 COUNT_COMPLEX / FIND_COMPLEX 的 body 不是独立 region, L2 接不住它们,没有替代降级路径。

关注点:ALT 各 case 之间活动指针如何恢复、PACK 取哪一个成功结果作为捕获区间。

L2 Region 级(正确性核心)

一次消耗 CommandCount + 1,即一个 TRY 归约出的完整 region。 决定作用域与失败复位策略,是所有嵌套结构的承载体。

# 触发窗口 消耗 覆盖样本 状态
L2.1 RegexTryIR + 其 body CommandCount + 1 (abc) [ ]
L2.2 同上,body 内含嵌套 region 递归 (a(bc)) [ ]
L2.3 单指令 region(被折叠的 TRY) 1 (a) [ ]

期望代码形状:

__rN_begin = __cur;
__rN_ok = false;
do
{
    // body,POP 在此 break
    __rN_ok = __ok;
} while (false);
if (!__rN_ok) { __cur = __rN_begin; }
__ok = __rN_ok;

失败路径必须把活动指针复位到 region 入口。POP 直接编译为 break,不引入标签。

关注点:region 全部内联展开、嵌套 region 的指针复位链、break 只能跳出最近一层 do。

L3 多指令定式(纯优化)

一次消耗 2–8 条,滑动窗口内的固定组合。全部属于代码质量优化,非正确性必需。 每条都必须能在窗口不匹配时干净地降级到 L4。

# 触发窗口 消耗 覆盖样本 状态
L3.1 FIND_COMPLEX FURTHEST COUNT:-1 + PACK + MATCH_SEQ 3 (?<name>.*)234 [ ]
L3.2 同上,锚点为单字符 3 (?<name>.*)a [ ]
L3.3 FIND_SEQ + MATCH_SEQ,合并为一次 IndexOf 2 .*abc [ ]
L3.4 MATCH_SEQ + COUNT_SEQ(a+、a{m,n} 展开形态) 2 a{3,5}、a+ [ ]
L3.5 连续 MATCH_SEQ / MATCH_SET,合流为单个 if 与表达式 2–8 a[abc]d [ ]
L3.6 尾部 COUNT_SET .*,快速成功 1–2 abc.* [ ]

关注点:合并后失败诊断是否还能定位到原指令、合并是否破坏 Observe_Line 的行号引用。

L4 单指令级

一次消耗 1 条。这是调度链的兜底层,必须对每种 RegexIRCommand 都有实现。

# 指令 覆盖样本 状态
L4.1 MATCH_SEQ abc、\.、a\*b [ ]
L4.2 MATCH_SET 离散 [abc]、[123abc]、[.] [ ]
L4.3 MATCH_SET 范围 [1-9] [ ]
L4.4 MATCH_SET 转义类 \w [ ]
L4.5 MATCH_SET 任意字符 . [ ]
L4.6 COUNT_SEQ a*、a? [ ]
L4.7 COUNT_SET [abc]*、\w+、.{11,13}、[.]* [ ]
L4.8 FIND_SEQ .*abc 降级路径 [ ]
L4.9 FIND_SET — [ ]
L4.10 POP ALT 尾 [ ]
L4.11 PACK (abc) 默认序号组 [ ]

关注点:[.] 与 . 必须走不同后端语义;[.]* 不得被当作 .* 优化。

L5 前缀装饰(横切,消耗 0)

前缀不消耗 IR,而是包裹在 L1–L4 产出的代码外层。 这一层不实现,L1–L4 的产出就无法拼装成正确的控制流。

# 前缀字段 取值 生成形态 状态
L5.1 ExeCondition DEF 无条件顺序执行 [ ]
L5.2 ExeCondition Prev_Success 包裹 if (前驱成功) [ ]
L5.3 ExeCondition Prev_Fail 包裹 else / if (!前驱成功) [ ]
L5.4 ExeCondition Always 脱离条件链,始终执行 [ ]
L5.5 ObserveMethod NONE 读紧邻前驱的隐式结果 [ ]
L5.6 ObserveMethod Observe_Line + ObserveSource 读指定行结果槽 [ ]
L5.7 ObserveMethod Observe_Region + ObserveSource 读指定 region 结果槽 [ ]
L5.8 PopCondition Pop_Success 写回 region 成功并提前退出 [ ]
L5.9 PopCondition Pop_Fail 写回 region 失败并提前退出 [ ]
L5.10 WorkingRegion 任意 决定代码落入哪个 region 作用域 [ ]

关注点:Observe_Line 只读目标行结果,Observe_Region 只读目标 region 结果, 二者都不得退化成「读上一条」。行结果槽仅在被 Observe_Line 引用时才分配。

实现顺序

调度链按 L1 → L4 排列,但实现先拿正确性核心(L2/L4/L5),再回头填纯优化层(L1/L3):

  1. L0 — 生成的空骨架能通过编译,TextRegexSourceGen.GenFile 的两处 TODO 闭合。
  2. L2.1 — (abc) 产出正确的 do{}while(false) 作用域与复位逻辑。
  3. L5.1–L5.3、L5.5 — 顺序与成功/失败链能拼装,此时 L2 的 region 才真正可用。
  4. L4 全部 — 兜底层补齐,任意线性表达式可端到端生成。
  5. L5.6–L5.9 — 跨行 / 跨 region 观察与提前退出,a|b 与 (?<name>abc) 自然跑通(嵌套形态)。 此时获得可运行基线,任何表达式都能生成正确代码。
  6. L1.5、L1.7 — 复杂计数与复杂搜索,无降级路径,必须实现。
  7. L1.1–L1.4 — 把 ALT 链与捕获组的嵌套形态压平。
  8. L3 全部 — 代码质量优化,可持续追加。

第一阶段验收口径(沿用原优先级):MATCH_SEQ、MATCH_SET、COUNT_SEQ、COUNT_SET、 简单 ALT、PACK、FIND_SEQ、FIND_COMPLEX COUNT:-1 全部稳定。

低优先级,暂不纳入 checklist:

  1. 逐 region 最短长度断言。
  2. 复杂字符集完全内联。
  3. 公共前缀提取和代码树生成。
  4. 对所有正则语义做完整兼容。

手写代码关注点

每个样本的手写代码都应尽量确认这些点:

  1. 活动指针如何推进。
  2. 失败时是否需要恢复活动指针。
  3. region result 和 single-line result 是否必须分配。
  4. Observe_Line 是否只读取目标行结果。
  5. Observe_Region 是否只读取目标 region 结果。
  6. PACK 使用哪一个成功结果作为捕获区间。
  7. 最小长度检查是否值得在当前样本中生成。
  8. 本形态消耗几条 IR,落空时降级到哪一层。
  9. 前缀的执行条件与观察源是否被正确外提,而不是硬编码进指令主体。

已手写样本索引

test/SingleFile/regex.cs 中已有的样本与层级对应:

函数 表达式 对应层级
Match_ABC abc L4.1
Match_A_charset_ABC_C a[abc]d L3.5 / L4.1 / L4.2
Match_charset_ABC [abc] L4.2
Match_Dot . L4.5
Match_A_3_5 a{3,5} L3.4
Match_AB_2_3 (ab){2,3} L1.5
MatchDiffCase (ab|[cd]|.)x L1.2
MatchCountAnyGroup (?<name>.*)x L3.1

待补:L2.1 (abc)、L1.1 a\|b、L1.3 (?<name>abc)、L5 各前缀组合的最小样本。 其中 L5 的样本最关键:需要分别写出 Prev_Success / Prev_Fail 包裹、 Observe_Line / Observe_Region 读取、Pop_Success / Pop_Fail 写回三组最小代码形状。

综合验收样本

a*.[abc]*(?<a>abcd|a(bc)c)(?<name>.*)234

覆盖 L1.1、L1.3、L1.4、L3.1、L4.6、L4.7,作为全链路收口用例,最后再手写。