switch root namespace name to NITLOAD

This commit is contained in:
Youmu Liu
2026-09-02 18:15:03 +08:00
parent 4218baad48
commit 7c7101ee48
502 changed files with 710 additions and 721 deletions
@@ -0,0 +1,204 @@
# 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`,确认不会导致生成器栈溢出或生成巨型源码。