# NGRA 字符串正则 IR 设计补充建议 基于当前 `ngra-regex-ir.md` 的设计,我认为整体方向是成立的:它已经不是“完全低级的执行字节码”,而是保留了 `COUNT`、`COUNT_SEQ`、`COUNT_SET`、`TRY`、`PACK` 等高层结构,因此可以继续承担后续 match-opt 的输入。 不过,为了让规则更容易扩展、避免语义误判,我建议把优化明确拆成三类,并把当前 IR 分成“语义 IR / HIR”和“计划 IR / LIR”。 --- ## 1. 总体判断 当前 IR 最大的优点: 1. **足够紧凑**:`COUNT_SEQ`、`COUNT_SET`、`TRY` 等复合指令可以避免过早打碎成 `LoadChar + Compare + Branch`。 2. **保留 regex 结构**:量词、字符集、确定序列、组打包等信息仍然可见。 3. **适合继续做 match planning**:后续可以从这些指令中识别 `.*literal`、`[set]*`、bounded repeat、固定长度 block 等结构。 当前 IR 最大的风险: 1. `SEARCH` 这个名字容易误导。当前 HIR 中的 `SEARCH` 实际应当是 **MATCH_AT_CURRENT**,不是允许跳过字符的 `FIND`。 2. 检查点目前只描述为锚点指针栈,但真正的 backtracking frame 至少还要保存 `pos`、capture watermark、symbol watermark、repeat state 和 continuation。 3. `COUNT_*` 的语义必须明确是否保留 retry/backtracking,否则 `a*.`、`[abc]*abc` 等场景会被错误优化。 4. 字符集的语义表示和物理表示要分开:语义上 `[0-9a-z]` 是一个 set,物理上可以用 inline set-region 的多条 range-test 表示,避免每个 set 挂 `List`。 --- ## 2. 建议的分层 我建议仍然区分 HIR 和 LIR,不是因为机器必须分,而是因为人写规则时边界更清楚。 ```text AST ↓ AST Opt ↓ HIR Gen ↓ HIR Analysis ↓ HIR → LIR / MatchPlan Lowering ↓ LIR ↓ SourceGen / RuntimeGen / Interpreter ``` ### HIR:表达 regex 语义 HIR 保留接近正则结构的指令: ```text SEARCH_SEQ // 语义上是当前位置 MATCH_SEQ SEARCH_SET // 语义上是当前位置 MATCH_SET SEARCH_DOT // 或 UNIT_DOT,不应和 literal "." 混淆 COUNT COUNT_SEQ COUNT_SET TRY POP PACK ``` 注意:HIR 中可以继续叫 `SEARCH`,但文档里要明确: ```text HIR 的 SEARCH_* 不表示向后查找; 它表示从当前活动指针开始匹配。 真正允许跳过字符的搜索操作只在 LIR / MatchPlan 阶段引入,命名为 FIND_*。 ``` ### LIR:表达匹配计划 LIR 不只是更低级,而是“已经做过策略规划”的 IR: ```text MATCH_SEQ MATCH_CHAR MATCH_DOT MATCH_SET FIND_SEQ FIND_CHAR FIND_SET FIND_LAST_SEQ FIND_LAST_SET CONSUME_WHILE_SET CONSUME_UNTIL_CHAR CONSUME_UNTIL_SET COUNT_SET COUNT_SEQ COUNT_BLOCK CHECK_MIN_LENGTH CHECK_FIXED_OFFSET PUSH_BACKTRACK POP_BACKTRACK COMMIT FAIL CAPTURE_BEGIN CAPTURE_END PACK ``` LIR 中应该已经包含: ```text LoweredSetId SearchMode RepeatMode RetryMode CaptureMode EmitHint ``` --- ## 3. 三类优化策略 建议把所有优化分成三类: ```text 1. Regex algebra rewrite 改正则结构。 2. Match plan rewrite 改匹配策略。 3. Backend / C# span trick 改 C# 代码写法。 ``` 这个分类很重要,因为它能直接决定优化应该放在哪个阶段。 --- # 4. Regex algebra rewrite 这类优化应该放在 AST Opt 或 HIR 生成前后。它们改变的是正则结构,而不是 C# 写法。 ## 4.1 Literal 合并 ```regex a b c ``` 变成: ```regex abc ``` HIR: ```text SEARCH_SEQ "abc" ``` 不要生成三条 `SEARCH_CHAR`。 --- ## 4.2 Concat / Alternation flatten ```regex a(?:b(?:c)) ``` 变成: ```regex abc ``` ```regex a|(?:b|c) ``` 变成: ```regex a|b|c ``` --- ## 4.3 Alternation factoring / trie 这个你已经放在 AST Opt 做,位置是对的。 ```regex foobar|foobaz|foozip ``` 变成: ```regex foo(?:bar|baz|zip) ``` 或者 trie AST: ```text SEQ "foo" ALT SEQ "bar" SEQ "baz" SEQ "zip" ``` 这类结构改写越早越好。后续 IR opt 只需要识别这个结构并决定是否 `switch` / first-set dispatch。 --- ## 4.4 Bounded repeat 展开 ```regex a{2,3} ``` 可以变成: ```regex aa a? ``` 或: ```text SEARCH_SEQ "aa" OPTIONAL SEARCH_CHAR 'a' ``` 对于 lazy: ```regex a{2,3}?b ``` 要注意尝试顺序。它等价于: ```regex aa(?:b|ab) ``` 而不是: ```regex aa(?:a|ab) ``` Greedy 版本: ```regex a{2,3}b ``` 如果展开成显式分支,尝试顺序更像: ```regex aa(?:ab|b) ``` 语言集合相同,但 backtracking 尝试顺序不同。 --- ## 4.5 `a+b` 不建议改成 `a*(ab|b)` 当前文档中写: ```text a+b 可以优化为 a*(ab|b) ``` 这个需要修正。 `a+b` 的语言是: ```text a^n b, n >= 1 ``` 而 `a*(ab|b)` 会额外匹配: ```text b ``` 因为第二个分支 `a*b` 可以在 `a*` 取 0 时匹配 `b`。 更安全的结构是: ```regex aa*b ``` 或者: ```text SEARCH_CHAR 'a' COUNT_SEQ 'a' 0..∞ SEARCH_CHAR 'b' ``` 如果要利用后继 `b` 做策略优化,应在 MatchPlan 阶段处理,而不是做这个不等价的 algebra rewrite。 --- ## 4.6 Charset 合并 ```regex [abc]|[def] ``` 可以变成: ```regex [abcdef] ``` ```regex [a-f]|[d-z] ``` 可以变成: ```regex [a-z] ``` 这属于 AST/HIR 层的字符类代数。 --- ## 4.7 Charset 展开为 alternation 要谨慎 文档中写: ```text abc[de] 可以展开为 (abcd|abce) ``` 这个是可行的,但不一定总是好。建议规则改成: ```text 当 charset 很小,并且展开后能形成更长 literal / trie / branch dispatch 时,可以展开; 否则保留为 MATCH_SET。 ``` 例如: ```regex abc[de] ``` 既可以是: ```text SEARCH_SEQ "abc" SEARCH_SET [de] ``` 也可以是: ```text ALT SEARCH_SEQ "abcd" SEARCH_SEQ "abce" ``` 前者代码更短,后者可能更适合 required literal 或 trie。需要 code size budget。 --- # 5. Match plan rewrite 这类优化应该放在 HIR Analysis / HIR → LIR lowering。 它们不一定改成另一条正则,而是把匹配过程改成更高效的搜索/消费计划。 --- ## 5.1 长度预测应该在 HIR Analysis 做,SourceGen 只负责发代码 当前文档写“长度预测在 source-gen 阶段做”。我建议改成: ```text minLength / maxLength 的计算在 HIR Analysis 做; SourceGen 只负责把 CHECK_MIN_LENGTH 发成 C#。 ``` 例如: ```regex abc[0-9]{2}xyz ``` 分析结果: ```text MinLength = 8 ``` LIR: ```text CHECK_MIN_LENGTH 8 MATCH_SEQ "abc" ... ``` SourceGen: ```csharp if ((uint)(end - pos) < 8) goto Fail; ``` --- ## 5.2 `.*` 优化 HIR: ```text COUNT_DOT min=0 max=∞ greedy=true MATCH_CHAR 'a' ``` 可以 lower 成: ```text FIND_LAST_CHAR 'a' ``` lazy: ```text COUNT_DOT min=0 max=∞ greedy=false MATCH_CHAR 'a' ``` lower 成: ```text FIND_CHAR 'a' ``` 但要注意: 1. 默认 `.` 不匹配 `\n`,搜索范围应限制在当前行。 2. Singleline 模式下才可以跨整个 span。 3. 如果 `.*` 内部或后继结构涉及 capture/backtracking,不能简单“删掉 COUNT”,而应转换为带 retry 的 find plan。 4. 后继如果是 `MATCH_SEQ "abc"`,则变成 `FIND_SEQ` / `FIND_LAST_SEQ`。 5. 后继如果是 `MATCH_SET`,则变成 `FIND_SET` / `FIND_LAST_SET`。 --- ## 5.3 `[set]*` 优化 文档中写 `[set]*` 优化在 source-gen 阶段做。建议改成: ```text 是否将 COUNT_SET 变成 CONSUME_WHILE / CONSUME_UNTIL,在 lowering 做; 具体使用 IndexOfAnyExcept / SearchValues / manual loop,在 SourceGen 做。 ``` 例如: ```regex [a-f0-9]* ``` LIR: ```text CONSUME_WHILE_SET LowerHex ``` SourceGen: ```csharp int n = span.IndexOfAnyExcept(s_lowerHex); ``` 或者手写 loop / range compare。 --- ## 5.4 First-set 搜索 如果非锚定匹配: ```regex abc|defg|xyz ``` first-set 是: ```text 'a', 'd', 'x' ``` LIR 可以是: ```text FIND_SET { 'a', 'd', 'x' } VERIFY_BRANCH ``` 你文档里说“这个不好做,因为 IR 直接把 ALT 打碎成 if-else 串了”。这说明 AST Opt / HIR 里最好保留 ALT region 或者至少保留 branch summary。 即使 AST 已经 trie 化,IR 也应该尽量不要把它完全打碎成不可恢复的 if-else 串。至少给每个 region 计算 summary: ```text FirstSet MinLength RequiredLiteral CanBacktrack CanCapture ``` --- ## 5.5 Required literal 搜索 例如: ```regex .*ERROR:[0-9]+ ``` 可以直接规划: ```text FIND_SEQ "ERROR:" VERIFY_AFTER_ANCHOR ``` 这通常比只做 first-set 更强。 --- ## 5.6 Fixed-offset check ```regex [a-z]{20}XYZ ``` 可以 lower 成: ```text CHECK_MIN_LENGTH 23 CHECK_AT_OFFSET 20 MATCH_SEQ "XYZ" MATCH_SET_REPEAT [a-z] count=20 MATCH_SEQ "XYZ" ``` 这个属于 match plan,不属于 sourcegen。 --- ## 5.7 Auto-atomic / possessive 化 ```regex [0-9]*[A-Z] ``` 由于: ```text [0-9] ∩ [A-Z] = ∅ ``` 所以 `[0-9]*` 不需要回退,可以 lower 成: ```text COUNT_SET [0-9] greedy atomic MATCH_SET [A-Z] ``` 如果后继 first-set 和 repeat set 有交集,就不能这么做。 --- ## 5.8 Repeat retry mode LIR 应该明确 repeat 的 retry 策略: ```text RetryMode.None RetryMode.BackoffBy1 RetryMode.BackoffByStride RetryMode.SavedPositions RetryMode.GeneralBacktrack ``` 例如: ```regex (abc[cd])+ ``` 每轮固定长度 4,所以可以: ```text RetryMode.BackoffByStride stride=4 ``` 不需要保存每个内部指令的回溯点。 --- # 6. Backend / C# span trick 这类优化只放 SourceGen。它不再改变 regex 或 match plan。 --- ## 6.1 `StartsWith` LIR: ```text MATCH_SEQ "sample" ``` SourceGen 决定: ```csharp slice.StartsWith("sample", StringComparison.Ordinal) ``` 还是短串展开: ```csharp slice.Length >= 3 && slice[0] == 'a' && slice[1] == 'b' && slice[2] == 'c' ``` 建议规则: ```text 长度 1: char compare 长度 2~3: 直接 char compare 中长 literal: StartsWith IgnoreCase: StartsWith(..., OrdinalIgnoreCase) ``` --- ## 6.2 `[0-9]` 不建议写成 `IndexOf("0123456789")` 用于当前位置测试 文档中写: ```text [0-9] 展开为 IndexOf("0123456789") ``` 这个要分两种场景: ### 当前位置匹配 如果是: ```text MATCH_SET [0-9] ``` 更好是: ```csharp (uint)(ch - '0') <= 9 ``` ### span 中搜索 如果是: ```text FIND_SET [0-9] ``` 可以考虑: ```csharp span.IndexOfAnyInRange('0', '9') ``` 或: ```csharp span.IndexOfAny(s_digits) ``` 所以规则应写成: ```text MATCH_SET [0-9] → unsigned range compare FIND_SET [0-9] → IndexOfAnyInRange / SearchValues ``` --- ## 6.3 SearchValues 对于多字符集合: ```text FIND_SET [0-9a-f] ``` SourceGen 可生成: ```csharp private static readonly SearchValues s_lowerHex = SearchValues.Create("0123456789abcdef"); ``` 然后: ```csharp span.IndexOfAny(s_lowerHex); ``` 当前位置测试不一定要用 `SearchValues`,可能 range compare 或 bitmap 更快。 --- ## 6.4 Inline vs helper 复杂 Unicode set: ```text MATCH_SET UnicodeWord ``` SourceGen 决定: ```csharp if (!IsUnicodeWord(ch)) goto Fail; ``` 还是: ```csharp if (ch < 128) { if (!AsciiWordContains(ch)) goto Fail; } else { if (!IsUnicodeWord(ch)) goto Fail; } ``` 这个应由代码尺寸、热度、是否 source-gen 决定。 --- ## 6.5 Code size budget SourceGen 应负责最终代码膨胀控制: ```text 单方法最大语句数 literal 展开阈值 bounded repeat 展开阈值 是否生成 helper 是否抽 cold path SearchValues 字段数量 ``` --- # 7. 字符集的物理表示建议 为了避免每个 set 挂 `List`,可以用线性 IR region 表示同一个字符测试。 例如语义: ```regex [0-9a-z] ``` 物理 IR: ```text MATCH_SET_BEGIN default=false end=13 TEST_RANGE '0' '9' accept=true TEST_RANGE 'a' 'z' accept=true MATCH_SET_END ``` 语义: ```regex [^0-9a-z] ``` 物理 IR: ```text MATCH_SET_BEGIN default=true end=13 TEST_RANGE '0' '9' accept=false TEST_RANGE 'a' 'z' accept=false MATCH_SET_END ``` 要点是: ```text 这些 TEST_RANGE 测的是同一个字符; 整个 MATCH_SET 只推进 pos 一次。 ``` 不能把它误解成连续匹配多个字符。 复杂 Unicode set 可以走: ```text MATCH_SET_REF setId=42 ``` `setId=42` 指向全局 pooled table,而不是每个节点一个 List。 --- # 8. 检查点语义建议 当前文档把检查点描述为锚点指针栈,这对 capture 起点有用,但不够覆盖 backtracking。 建议把 checkpoint / backtrack frame 定义成: ```text BacktrackFrame: pos anchor captureTop symbolTop repeatState continuation ``` 其中: ```text pos: 活动指针恢复位置 anchor: 区间开始位置 captureTop: 捕获记录栈回滚点 symbolTop: 符号区回滚点 repeatState: 当前 COUNT/loop 的计数、起止位置、retry mode continuation: 失败后跳到哪里继续尝试 ``` 如果暂时是 tokenizer / no-capture 模式,可以裁掉 captureTop,但 IR 语义最好预留。 --- # 9. 建议修改原文的几个点 ## 9.1 “HIT” 应改为 “HIR” 原文: ```text 将HIT进行进一步优化 ``` 应为: ```text 将 HIR 进行进一步优化 ``` ## 9.2 `SEARCH` 的定义应改写 原文说“向后搜索确定长度序列”。建议改为: ```text HIR 中 SEARCH_* 表示从当前活动指针开始匹配; 它不是允许跳过字符的 FIND。 真正的 FIND_* 在 HIR→LIR 阶段由 match planner 引入。 ``` ## 9.3 `a+b → a*(ab|b)` 不等价 建议删除或改成: ```regex a+b → aa*b ``` 或者: ```text MATCH_CHAR 'a' COUNT_SEQ 'a' 0..∞ MATCH_CHAR 'b' ``` ## 9.4 `[0-9] → IndexOf("0123456789")` 应细分 改成: ```text 当前位置 MATCH_SET: 用 unsigned range compare。 span 搜索 FIND_SET: 用 IndexOfAnyInRange / SearchValues / IndexOfAny。 ``` ## 9.5 `.*` 优化不要说 “COUNT 消失” 建议改成: ```text COUNT_DOT 与后继匹配项合并为 FIND / FIND_LAST 计划。 ``` 这样更准确,因为有时还需要保留 retry / range 限制 / newline 限制。 --- # 10. 推荐最终管线 ```text Tokenize - 区分 Dot 与 EscapedLiteralDot - 区分 raw charset / literal / quantifier / group AST Build - 建立 regex 语义树 AST Opt - literal merge - concat flatten - alternation factoring / trie - bounded repeat normalization - simple group elimination - charset algebra merge HIR Gen - SEARCH_* as MATCH_AT_CURRENT - COUNT_* - TRY / POP - PACK - raw set / raw dot HIR Analysis - minLength / maxLength - nullable - firstSet - lastSet - required literal - fixed length - capture effect - repeat overlap HIR → LIR / MatchPlan - raw set → lowered set - dot → dot mode - introduce FIND - introduce CONSUME - choose retry mode - choose atomic/possessive - emit check-min-length / fixed-offset checks LIR - explicit MATCH / FIND / CONSUME - explicit lowered set - explicit backtracking plan - explicit capture plan SourceGen - StartsWith / SequenceEqual - IndexOf / IndexOfAny / IndexOfAnyExcept - SearchValues - range compare / bitmap - inline/helper split - code size budget ``` --- # 11. 总结 我建议保留你当前 IR 的核心设计,但把文档里的边界收紧: ```text AST Opt: 做 Regex algebra rewrite。 HIR: 表示从当前位置开始的 regex 语义。 HIR Analysis: 提取结构属性。 HIR → LIR: 做 Match plan rewrite。 LIR: 表示明确的匹配计划。 SourceGen: 做 C# span trick。 ``` 最重要的改动是: ```text SEARCH 在 HIR 中应被定义为 MATCH_AT_CURRENT; FIND 只能由后续优化阶段引入。 ``` 以及: ```text 字符集可以用线性 set-region 表示,避免 List 分配; 但 set-region 内的多条 range-test 必须测试同一个字符,不能变成多个连续 MATCH。 ``` 这样你的 IR 会同时满足三个目标: 1. 对人来说可读; 2. 对优化规则来说容易扩展; 3. 对 GC 和 sourcegen 来说物理表示足够紧凑。