switch C code prefix to NIF
This commit is contained in:
@@ -1,204 +0,0 @@
|
||||
# NGRA `COUNT(UNIT)` 生成说明
|
||||
|
||||
本文记录 `TextRegexSourceGen.GenerateCount` 中单字符重复分支的当前设计,方便后续继续实现。
|
||||
|
||||
相关代码:[`Text/TextRegexSourceGen.GenClass.cs`](Text/TextRegexSourceGen.GenClass.cs)。
|
||||
|
||||
## 目标
|
||||
|
||||
对于 `a{min,max}`,不要把必需的 `min` 次匹配生成成逐字符循环。生成器先把字符重复 `min` 次,形成一个字符串字面量,并使用一次 `StartsWith` 验证下界:
|
||||
|
||||
```csharp
|
||||
if (__remain.StartsWith("aaa"))
|
||||
{
|
||||
// 已经确认至少存在 min 个 a。
|
||||
}
|
||||
else
|
||||
{
|
||||
// 当前工作 region 失败。
|
||||
}
|
||||
```
|
||||
|
||||
匹配成功后一次性消费这个前缀,再只处理区间中可选的部分:
|
||||
|
||||
```text
|
||||
必需前缀:min 次
|
||||
可选尾部:max - min 次
|
||||
```
|
||||
|
||||
`max < 0` 表示尾部没有上限。
|
||||
|
||||
## AST 数据归属
|
||||
|
||||
`COUNT` 配置属于操作符节点,重复的字符属于其子节点:
|
||||
|
||||
```csharp
|
||||
RegexCountPayload payload = node.PayloadAs<RegexCountPayload>();
|
||||
RegexAstNode value = RegexAstNode.IsNullOrEmpty(node.Left)
|
||||
? node.Right
|
||||
: node.Left;
|
||||
char expected = value.Value[0];
|
||||
```
|
||||
|
||||
不要从 `value` 读取 `RegexCountPayload`。`value` 携带的是 `RegexLiteralPayload` 或其他被重复元素的 payload。
|
||||
|
||||
字符应从 `value.Value` 读取,因为它是解码后的值。使用 `value.Source.Source[0]` 会把 `\t` 等转义序列错误地识别成 `'\\'`。
|
||||
|
||||
## 量词语义
|
||||
|
||||
| 量词 | `min` | `max` | 尾部处理 |
|
||||
| --- | ---: | ---: | --- |
|
||||
| `a{n}` | `n` | `n` | 无可选尾部 |
|
||||
| `a{m,n}` | `m` | `n` | 最多再消费 `n - m` 次 |
|
||||
| `a*` | `0` | `-1` | 无上限 |
|
||||
| `a+` | `1` | `-1` | 验证一次后无上限 |
|
||||
| `a?` | `0` | `1` | 最多消费一次 |
|
||||
| `a{m,}` | `m` | `-1` | 验证 `m` 次后无上限 |
|
||||
|
||||
有限上限可以生成:
|
||||
|
||||
```csharp
|
||||
__remain = __remain.Slice(min);
|
||||
|
||||
for (int i = 0;
|
||||
i < max - min &&
|
||||
!__remain.IsEmpty &&
|
||||
__remain[0] == expected;
|
||||
i++)
|
||||
{
|
||||
__remain = __remain.Slice(1);
|
||||
}
|
||||
```
|
||||
|
||||
无上限不能计算 `max - min`,否则结果为负数、循环不会执行。应单独生成:
|
||||
|
||||
```csharp
|
||||
__remain = __remain.Slice(min);
|
||||
|
||||
while (!__remain.IsEmpty && __remain[0] == expected)
|
||||
{
|
||||
__remain = __remain.Slice(1);
|
||||
}
|
||||
```
|
||||
|
||||
`!__remain.IsEmpty` 必须位于 `__remain[0]` 之前。输入恰好在必需前缀后耗尽时,直接索引会抛出 `IndexOutOfRangeException`。
|
||||
|
||||
## 贪婪与惰性
|
||||
|
||||
贪婪量词可以先消费尽可能多的可选尾部,再尝试后续 continuation。如果后续失败,需要逐次回退消费数量并重试。
|
||||
|
||||
惰性量词只应先消费 `min`,立即尝试后续 continuation;后续失败时,才逐次多消费一个匹配字符。
|
||||
|
||||
因此 `is_greedy` 不能只改变扫描循环,它最终需要与后续节点的 continuation 和回溯状态一起实现。若 COUNT 位于匹配末尾,贪婪模式可直接扫描至上限,惰性模式保留在 `min` 即可。
|
||||
|
||||
## 失败工作 region
|
||||
|
||||
当前试点使用 `do { } while (false)`,使局部失败可以写成 `break`:
|
||||
|
||||
```csharp
|
||||
bool regionSucceeded = false;
|
||||
ReadOnlySpan<char> checkpoint = __remain;
|
||||
|
||||
do
|
||||
{
|
||||
if (!__remain.StartsWith(requiredPrefix))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
__remain = __remain.Slice(min);
|
||||
// 匹配可选尾部和后续节点。
|
||||
|
||||
regionSucceeded = true;
|
||||
}
|
||||
while (false);
|
||||
|
||||
if (!regionSucceeded)
|
||||
{
|
||||
__remain = checkpoint;
|
||||
// 向父 region 传播失败,或者在顶层返回失败结果。
|
||||
}
|
||||
```
|
||||
|
||||
只有 COUNT 自己被 `do/while(false)` 包裹时,`break` 结束后仍会执行下一个 CONCAT 节点,不能真正表达匹配失败。最终 region 应由以下之一持有:
|
||||
|
||||
- 包含后续 continuation 的 `CONCAT`;
|
||||
- 一条完整的 ALT 候选路径;
|
||||
- 顶层匹配方法。
|
||||
|
||||
父 region 负责保存 cursor 和 capture checkpoint。子节点失败后退出父 region,由父 region 统一恢复状态。ALT 和贪婪/惰性 COUNT 还需要保存多个可重试位置,不能只依赖一次 `break`。
|
||||
|
||||
## 摊平阈值
|
||||
|
||||
不要无条件执行:
|
||||
|
||||
```csharp
|
||||
Span<char> required = stackalloc char[min];
|
||||
```
|
||||
|
||||
量词来自用户输入,`a{1000000}` 会让源生成器尝试进行巨大的栈分配,并生成巨型字符串字面量。建议设置较小阈值,例如 32 或 64:
|
||||
|
||||
- `min <= threshold`:摊平为字面量并调用 `StartsWith`;
|
||||
- `min > threshold`:循环比较固定长度的摊平块,最后比较不足一个块的余数;
|
||||
- 非法或超过生成器限制的数量:报告 Roslyn diagnostic,而不是让生成器崩溃。
|
||||
|
||||
以阈值 64 为例,`a{150}` 被拆成两个 64 字符块和一个 22 字符余数块。运行时使用局部 span 逐块调用 `StartsWith`,全部成功后才一次性推进正式 cursor。这样失败时无需恢复已经提交的 `__remain`:
|
||||
|
||||
```csharp
|
||||
if (__remain.Length < 150)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ReadOnlySpan<char> __requiredRest_0 = __remain;
|
||||
bool __requiredMatched_0 = true;
|
||||
|
||||
for (int __requiredChunkIndex_0 = 0; __requiredChunkIndex_0 < 2; __requiredChunkIndex_0++)
|
||||
{
|
||||
if (!__requiredRest_0.StartsWith("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
|
||||
{
|
||||
__requiredMatched_0 = false;
|
||||
break;
|
||||
}
|
||||
|
||||
__requiredRest_0 = __requiredRest_0.Slice(64);
|
||||
}
|
||||
|
||||
if (__requiredMatched_0 && !__requiredRest_0.StartsWith("aaaaaaaaaaaaaaaaaaaaaa"))
|
||||
{
|
||||
__requiredMatched_0 = false;
|
||||
}
|
||||
|
||||
if (!__requiredMatched_0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
__remain = __remain.Slice(150);
|
||||
```
|
||||
|
||||
## 后续实现顺序
|
||||
|
||||
1. 从 `node` 读取 `RegexCountPayload`,并用 Empty sentinel 安全选择子节点。
|
||||
2. 改用 `value.Value[0]` 获取解码字符。
|
||||
3. 为所有尾部循环加入 `!__remain.IsEmpty`。
|
||||
4. 分开生成有限上限和 `max < 0` 两种循环。
|
||||
5. 增加摊平阈值,避免巨大 `stackalloc` 和生成源膨胀。
|
||||
6. 将工作 region 上移到 CONCAT/候选路径,完成失败传播和 checkpoint 恢复。
|
||||
7. 接入 greedy/lazy continuation 回溯。
|
||||
8. 最后处理 capture 状态的保存与恢复。
|
||||
|
||||
## 最小测试矩阵
|
||||
|
||||
至少覆盖以下模式和输入:
|
||||
|
||||
- `a{0}`:空输入、`a`;
|
||||
- `a{3}`:`aa`、`aaa`、`aaaa`;
|
||||
- `a{2,4}`:长度 1 到 5;
|
||||
- `a*`、`a+`、`a?`、`a{2,}`;
|
||||
- `\t{2}`:确认使用解码字符;
|
||||
- `a{2,4}ab`:验证贪婪回退;
|
||||
- `a{2,4}?ab`:验证惰性扩展;
|
||||
- COUNT 位于 CONCAT、ALT 和捕获 GROUP 内的失败恢复;
|
||||
- 输入恰好在 `min` 处结束,确认不会索引空 span;
|
||||
- 超大 `min`,确认不会导致生成器栈溢出或生成巨型源码。
|
||||
Reference in New Issue
Block a user