Files
stgsharp/StgSharp.RegularAnalysis/NGRA_pattern_file.md
T
2026-04-30 20:38:44 +08:00

1111 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NGRA `.ngra` 描述文件设计草案
## 1. 定位
**NGRA**,即 **Nitload Generalized Regulation Analysis**,是一个广义序列分析器框架。
其实现位于StgSharp.RegularAnalysis项目/命名空间下
NGRA 的目标不是只处理字符串正则,而是处理任意“可分类线性序列”,例如:
- 字符串;
- 字节流;
- 比特流;
- token 序列;
- AST 子节点序列;
- 游戏事件流;
- 玩家行为流;
- 二进制协议流。
`.ngra` 文件是 NGRA 接收的描述文件,用于描述如何从输入流中提取目标序列元素,以及如何通过规则递归地识别、分组、压缩出更高层的岛屿结构。
NGRA 读取 `.ngra` 文件后,会进行解析器/分析器的源生成。使用者需要根据场景补充必要的序列元素定义、分类逻辑以及树构造方法。
---
## 2. 基本设计原则
`.ngra` 文件应当保持声明式,主要负责描述:
1. 输入是什么;
2. 分析的目标元素是什么;
3. 规则如何从输入中提取目标元素;
4. 规则之间如何调用、组合、分层;
5. 分析器运行模式是什么。
`.ngra` 文件不应当承担过多语义任务,例如:
- 类型推导;
- 作用域解析;
- 复杂数据流分析;
- IL 生成;
- 任意 C# 代码嵌入;
- 复杂 tree builder 逻辑。
这些应当交给 NGRA 生成器外部的使用者代码完成。
---
## 3. `.ngra` 文件结构
一个 `.ngra` 文件由两部分组成:
1. 文件头部指令;
2. NGRAP 表达式集合。
示例:
```ngra
#input string
#element token
#import std_text.ngra
#run scanning
Identifier := Regex(/[ \t]*(?<id>[A-Za-z_][A-Za-z0-9_]*)/, id);
Number := Regex(/[ \t]*(?<num>[0-9]+)/, num);
NewLine := Regex/(?<nl>\r\n|\n|\r/, nl);
IfExpression :=
<r"if">
<r"(">
(?<condition>: <Expression>)
<r")">
(?<then>: <Block>)
;
```
---
## 4. 文件头部指令
### 4.1 `#input`
声明输入流类型。
```ngra
#input string
#input byte
#input bit
#input token
#input event
#input custom
```
含义:
| 指令 | 含义 |
| --- | --- |
| `#input string` | 输入为字符串/文本流 |
| `#input byte` | 输入为字节流 |
| `#input bit` | 输入为比特流 |
| `#input token` | 输入为已有 token 序列 |
| `#input event` | 输入为事件流 |
| `#input custom` | 输入为用户自定义序列 |
`#input` 会影响哪些内联函数可用。
例如:
- `#input string` 下可用 `Regex(...)`、`Char(...)`;
- `#input byte` 下可用 `Byte(...)`;
- `#input bit` 下可用 `Bit(...)`;
- `#input token` / `#input event` 通常不使用原始提取函数,而直接使用 `<...>` 规则匹配。
---
### 4.2 `#element`
声明规则左值对应的目标元素类型。
```ngra
#element token
#element syntax
#element event
#element custom
```
示例:
```ngra
#input string
#element token
```
表示:
- 输入是字符串;
- `.ngra` 规则主要生成和处理 token 类型的目标元素。
又如:
```ngra
#input event
#element event
```
表示:
- 输入是事件流;
- 规则处理的基本单元就是事件元素。
`#element` 可对应具体宿主语言类型,例如 C# 中的:
```csharp
SyntaxNode
PlayerEvent
TerrainUnit
```
具体映射方式由 NGRA 源生成器配置或用户代码提供。
---
### 4.3 `#import`
导入其他 `.ngra` 文件作为当前上下文。
```ngra
#import std_text.ngra
#import std_binary.ngra
#import magicspinner_common.ngra
```
被导入文件可以提供:
- 通用 token 定义;
- 通用规则;
- 公共正则片段;
- 标准库式规则;
- 共享的序列描述。
建议规则:
1. 重复 import 忽略;
2. import 循环需要检测;
3. 同名符号默认报错;
4. 后续可以考虑 namespace 或显式覆盖机制。
---
### 4.4 `#run`
声明分析器运行模式。
```ngra
#run scanning
#run stepping
```
#### `scanning`
一次性扫描模式。
```ngra
#run scanning
```
含义:
> 分析器读取完整输入,尽可能完成全部分析,产出最终结果,然后结束。
适合:
- 编译器前端;
- 源码文件解析;
- 二进制文件解析;
- 静态日志分析;
- 一次性数据处理。
#### `stepping`
增量步进模式。
```ngra
#run stepping
```
含义:
> 分析器保持内部状态,每次接收一个或一批输入元素,执行一步或若干步分析,不自动结束。
适合:
- 游戏事件流;
- 玩家行为流;
- 网络流;
- 实时日志;
- 交互式 CLI;
- MagicSpinner 运行时施法流;
- 增量编译。
`stepping` 模式下,分析器通常需要提供类似接口:
```csharp
Feed(...)
Step()
Flush()
Reset()
Complete()
```
并支持:
- pending match;
- partial result;
- 状态保留;
- 用户显式结束输入。
---
## 5. NGRAP 表达式
`.ngra` 文件主体由一组 NGRAP 表达式组成。
每条 NGRAP 表达式的根操作是赋值:
```ngra
Symbol := Pattern;
```
其中:
- 左值 `Symbol` 必须是单个符号;
- 左值表示一个目标元素或目标元素序列;
- 右值 `Pattern` 是得到该目标元素/序列的规则描述;
- 左值符号可以在其他右值中被调用;
- 这种调用可以递归发生;
- NGRA 通过分层调用和岛屿压缩生成分析结构。
示例:
```ngra
Identifier := Regex(/[A-Za-z_][A-Za-z0-9_]*/);
Expression :=
<Identifier>
| <Number>
| <String>
| <IfExpression>
;
IfExpression :=
<r"if">
<r"(">
(?<condition>: <Expression>)
<r")">
(?<then>: <Block>)
;
```
---
## 6. 单条 NGRAP 表达式与整体 `.ngra` 的关系
一条 NGRAP 表达式的右值可以视为一种增强型正则表达式。
它支持:
- 顺序拼接;
- 选择;
- 分组;
- 捕获;
- 计数;
- 条件;
- 局部规则调用;
- 类型化序列元素匹配。
但整个 `.ngra` 文件不是一条正则表达式,而是一个规则集合。
可以概括为:
> 单条规则是增强型序列正则;
> 整个 `.ngra` 文件是分层序列压缩规则系统。
或者:
> Rule-level regex, system-level grammar.
---
## 7. 调用算符 `<...>`
### 7.1 普通规则调用
```ngra
<Identifier>
<Expression>
<IfExpression>
```
表示调用已有规则或匹配已有目标元素类型。
例如:
```ngra
Expression :=
<Identifier>
| <Number>
| <IfExpression>
;
```
### 7.2 带值匹配简写
```ngra
<Identifier "if">
<Token "(">
<Symbol "{">
```
表示匹配某个类型并要求其 `value` 属性满足给定字面量。
这是一种常用简写,等价于:
```ngra
<Identifier>{value="if"}
<Token>{value="("}
<Symbol>{value="{"}
```
例如:
```ngra
IfExpression :=
<Identifier "if">
<Symbol "(">
<Expression>
<Symbol ")">
<Block>
;
```
### 7.3 带属性约束匹配
```ngra
<Token>{kind=Identifier}
<Token>{value="if"}
<Height>{value < 500}
<Event>{type="Damage" && amount > 50}
```
表示先调用某个目标元素或规则,再对该元素属性施加约束。
约束写在 `{...}` 中,约定如下:
- 元素属性使用裸属性名,例如 `value`、`kind`、`type`;
- NGRA 匹配上下文仍保留 `@count`、`@size` 这类 `@...` 名称;
- `<T "...">` 继续保留,作为 `<T>{value="..."}` 的简写。
---
## 8. 匿名局部正则调用 `<r"...">`
为了避免过度声明语法胶水,可以使用:
```ngra
<r"if">
<r"(">
<r")">
<r"{">
<r"}">
<r",">
<r"(if|when|unless)">
<r"[A-Za-z_][A-Za-z0-9_]*">
```
`<r"...">` 表示:
> 使用局部正则进行拼接匹配,但该片段默认 discard,不作为后续语义分析中的显式目标元素。
在当前输入类型支持正则的前提下,`<r"...">` 内允许书写任何可用的正则表达式,而不只限字面量。
由于其匹配结果默认 discard,因此不提供类似 `Regex(expr, group)` 的局部取组语义。
示例:
```ngra
IfExpression :=
<r"if">
<r"(">
(?<condition>: <Expression>)
<r")">
(?<then>: <Block>)
;
```
`<r"...">` 适合表示:
- 关键字和字面量胶水;
- 括号;
- 逗号;
- 分号;
- 冒号;
- 局部正则片段;
- 其他纯语法胶水。
它应当参与 source span 与 diagnostics,但默认不成为 AST 子节点。
---
## 9. 捕获组
建议使用接近 PCRE / .NET Regex 的命名捕获形式:
```ngra
(?<name>: pattern)
```
示例:
```ngra
IfExpression :=
<r"if">
<r"(">
(?<condition>: <Expression>)
<r")">
(?<then>: <Block>)
(<r"else"> (?<else>: <Block>))?
;
```
捕获引用使用 `$name`:
```ngra
LengthPrefixed :=
(?<len>: <Byte>)
<Byte>{=$len.value}
;
```
命名空间建议:
| 类型 | 示例 |
| --- | --- |
| 捕获定义 | `(?<name>: pattern)` |
| 捕获引用 | `$name` |
| NGRA 元属性 | `@count`, `@size`, `@line`, `@column` |
| 元素属性 | `value`, `kind`, `type` |
不建议使用 `@name:` 作为捕获语法,以免和 `@count` 等元属性混淆。
---
## 10. 内联函数
`.ngra` 内联函数应保持克制,主要分为两类:
1. 输入提取函数;
2. 规则定义辅助函数。
目前建议首发保留少量输入提取函数和少量正则组合函数。
---
### 10.1 `Regex(expr)`
从字符串输入中提取匹配正则的目标元素。
```ngra
Identifier := Regex(/[A-Za-z_][A-Za-z0-9_]*/);
Number := Regex(/[0-9]+/);
```
要求:
- 输入必须是字符串或兼容文本流;
- 匹配应当锚定在当前位置;
- 匹配失败不消费输入。
---
### 10.2 `Regex(expr, group)`
从字符串输入中匹配完整正则,但只把指定捕获组赋值给左值。
```ngra
Identifier := Regex(/[ \t]*(?<id>[A-Za-z_][A-Za-z0-9_]*)/, id);
```
语义:
1. 整个 `expr` 负责匹配和消费输入;
2. 只有 `group` 指定的捕获组用于生成左值元素;
3. group 外的内容不进入主序列;
4. group 外内容可以视实现保留为 trivia/span 信息;
5. 匹配必须从当前位置开始。
这使得 `.ngra` 可以局部决定哪些空白有用、哪些空白只是分隔符。
例如:
```ngra
Identifier := Regex(/[ \t]*(?<id>[A-Za-z_][A-Za-z0-9_]*)/, id);
NewLine := Regex/(?<nl>\r\n|\n|\r/, nl);
IndentRaw := Regex/(?m)^(?<indent>[ \t]+)/, indent);
```
这样普通水平空白可以被 identifier 消费掉,但换行和行首缩进仍然可以作为有意义元素保留。
这对于 Python、YAML、Makefile、Markdown 等空白敏感语言很重要。
---
### 10.3 `Byte(int)`
从字节流中读取指定数量的字节。
```ngra
Magic := Byte(4);
Height := Byte(2);
```
要求:
```ngra
#input byte
```
或兼容字节流输入。
---
### 10.4 `Bit(int)`
从比特流中读取指定数量的比特。
```ngra
Flag := Bit(1);
Mode := Bit(3);
```
要求:
```ngra
#input bit
```
或兼容比特流输入。
---
### 10.5 `Char(int)`
根据字符集规则读取指定数量的字符。
```ngra
OneChar := Char(1);
FourChars := Char(4);
```
要求:
```ngra
#input string
```
或兼容文本输入。
注意:
- 需要明确字符单位;
- 在 C# 中 `char` 是 UTF-16 code unit;
- 后续可以考虑是否需要 `Rune(int)` 表示 Unicode scalar value。
---
### 10.6 `Append(expr1, expr2, ...)`
按顺序拼接多个正则片段,等价于正则级串接。
```ngra
Identifier := Regex(
Append(/[ \t]*/, /(?<id>[A-Za-z_][A-Za-z0-9_]*)/),
id
);
```
适合把较长的正则拆成多个可读片段,而不是把所有内容压在一个表达式里。
---
### 10.7 `Cases(expr1, expr2, ...)`
在多个正则片段之间做分支选择,等价于正则级 `|`。
```ngra
NewLine := Regex(Cases(/\r\n/, /\n/, /\r/));
```
适合把若干候选形式显式列出,避免在单个正则中写过长的分支链。
---
### 10.8 `Count(expr, count)` / `Count(expr, min, max)`
对正则片段施加重复次数约束,等价于正则级 `{n}` 或 `{min,max}`。
```ngra
HexByte := Regex(Count(/[0-9A-Fa-f]/, 2));
Indent := Regex(Count(/[ \t]/, 1, 4));
```
这些函数用于构造传给 `Regex(...)` 的正则表达式本身,不引入宿主语言动作语义。
---
## 11. 空白处理策略
不建议默认引入类似:
```ngra
Whitespace := Regex(/\s+/) -> skip;
```
因为:
1. `-> skip` 类似 lexer action,会污染纯声明式设计;
2. 全局丢弃空白过于粗暴;
3. 无法很好支持缩进敏感语言;
4. 容易导致后续扩展成任意动作系统。
推荐使用:
```ngra
Regex(expr, group)
```
通过 group 外部内容控制哪些输入被消费但不产出。
例如:
```ngra
Identifier := Regex(/[ \t]*(?<id>[A-Za-z_][A-Za-z0-9_]*)/, id);
Number := Regex(/[ \t]*(?<num>[0-9]+)/, num);
```
这表示:
- 水平空白被消费;
- 只有 `id` / `num` 捕获组成为 token;
- 换行可以单独声明为有意义 token。
如果后续确实需要全局忽略机制,可以另行考虑:
```ngra
#ignore Whitespace
#trivia Comment
```
但这不应作为首要机制。
---
## 12. 条件表达式与属性谓词
`<...>` 调用算符后的 `{...}` 应支持简单条件表达式。
示例:
```ngra
Plain := <Height>{value < 500}+;
```
表示匹配连续的高度小于 500 的 `Height` 元素。
可以支持:
```ngra
<Token>{kind=Identifier}
<Token>{value="if"}
<Height>{value < 500}
<Event>{type="Damage" && amount > 50}
```
建议支持的小型表达式能力:
- 字面量;
- 属性访问;
- 比较;
- 布尔运算;
- 简单算术;
- 捕获引用;
- 有限纯函数。
不建议支持任意内嵌 C#。
复杂逻辑应通过用户注册命名谓词或后续 semantic pass 实现。
---
## 13. 基本属性
NGRA 应内建一组 match context 基本属性。
### 13.1 `@count`
当前匹配片段中的序列元素数量。
```ngra
Plain :=
<Height>{value < 500}+
where @count >= 8;
```
### 13.2 `@size`
当前匹配片段覆盖的字节长度。
```ngra
Packet :=
<Header>
<Payload>
where @size <= 4096;
```
如果当前输入不提供 byte span,则该属性不可用或需要由用户序列适配器提供。
### 13.3 `@line`
当前匹配起始位置所在行。
```ngra
TopLevelOnly :=
<Statement>
where @line == 1;
```
如果设置为换行符无关序列,则 `@line` 可恒定为 `1`。
### 13.4 `@column`
当前匹配起始位置相对于行首的偏移量。
```ngra
Indented :=
<Statement>
where @column > 1;
```
对于无行列概念的输入,可以不可用,或由输入适配器定义。
建议:
- `@...` 命名空间保留给 NGRA match context;
- 用户元素属性使用裸属性名,并写在 `<...>{...}` 内;
- 捕获变量使用 `$name`。
---
## 14. 岛屿语法与分层压缩
NGRA 的核心机制之一是:
> 规则匹配成功后,可以将匹配到的序列片段压缩成左值类型的岛屿元素,供更高层规则继续分析。
例如:
```ngra
IfExpression :=
<r"if">
<r"(">
(?<condition>: <Expression>)
<r")">
(?<then>: <Block>)
;
```
匹配成功后,可以生成:
```text
<IfExpression>
```
上层规则不必再关心其内部 token 细节。
这使得 NGRA 可以形成分层分析:
```text
raw string
-> token sequence
-> expression islands
-> statement islands
-> block/function/module islands
-> AST / domain nodes
```
岛屿语法的优势:
1. 局部错误不影响全局;
2. 适合增量编译;
3. 同层岛屿可并行分析;
4. 支持先壳后体;
5. 有助于解决声明顺序问题;
6. 可以用于代码、二进制流、事件流等多种序列。
---
## 15. 递归调用与预压缩
`.ngra` 规则允许递归调用。
示例:
```ngra
Expression :=
<Literal>
| <CallExpression>
| <IfExpression>
;
IfExpression :=
<r"if">
<r"(">
(?<condition>: <Expression>)
<r")">
(?<then>: <Block>)
;
```
但为了避免卡死在递归识别中,NGRA 在编译 `.ngra` 文件时应当进行规则图分析。
需要检测:
- 递归调用;
- 左递归;
- 空匹配递归;
- 强连通规则组;
- 是否存在明确边界;
- 是否需要预压缩;
- 是否可以并行分析。
核心策略:
> 对被调用序列预先进行边界识别和压缩,使上层规则不必纠结内部细节。
例如:
```ngra
Block := <r"{"> ... <r"}">;
```
可以先识别 `{ ... }` 的边界并生成 `Block` island,再进入内部解析 statements。
---
## 16. Tree Builder 与用户代码
NGRA 可以提供默认树节点类型,例如:
```csharp
NgraNode
NgraToken
NgraMatch
NgraSpan
```
但实际使用中,用户通常需要根据领域定义自己的节点。
例如 MagicSpinner 可能需要:
```csharp
MagicSpinnerToken
MagicSpinnerSyntaxNode
SpellDeclarationNode
IfExpressionNode
VisionTagNode
```
因此 `.ngra` 文件只描述匹配和捕获,不应强行规定所有 AST 构造细节。
可选设计:
```ngra
IfExpression :=
<r"if">
<r"(">
(?<condition>: <Expression>)
<r")">
(?<then>: <Block>)
;
```
生成器产生匹配结果后,调用用户 tree builder:
```csharp
BuildIfExpression(NgraMatch match)
```
如果用户不提供 builder,则使用默认 generic node:
```text
GenericNode {
Type = "IfExpression",
Captures = {
condition,
then
},
Span = ...
}
```
---
## 17. 与 ANTLR 的关系
ANTLR 的强项是:
- 单个 `.g4` 文件即可定义 lexer/parser;
- 工具链成熟;
- 生成 visitor/listener;
- 非常适合传统编译器前端。
NGRA 不应当在 ANTLR 最强的传统 parser generator 领域硬碰硬。
NGRA 的核心优势应当是:
- 输入序列泛化;
- 不局限于字符/token;
- 支持任意可分类线性序列;
- 支持岛屿压缩;
- 支持分层并行分析;
- 支持事件流、二进制流、AST 节点流;
- 规则右值使用增强型正则表达式风格;
- `scanning` / `stepping` 双运行模式。
可以概括为:
> ANTLR 是传统语言前端生成器;
> NGRA 是广义序列分析器源生成框架。
此外,`.ngra` 文件的解释器/解析器本身应当使用 NGRA 构建。
也就是说,NGRA 的实现路线默认采用自举:先落地最小可运行核心,再用 NGRA 描述 `.ngra` 自身语法并逐步替换过渡实现。
这可以保证语法设计、规则执行模型和解释器实现始终保持同一种表达体系。
---
## 18. 运行模式示例
### 18.1 scanning 模式
```ngra
#input string
#element token
#run scanning
Identifier := Regex(/[ \t]*(?<id>[A-Za-z_][A-Za-z0-9_]*)/, id);
Number := Regex(/[ \t]*(?<num>[0-9]+)/, num);
Expression :=
<Identifier>
| <Number>
;
```
适合一次性输入:
```text
source.ngra input -> complete output
```
---
### 18.2 stepping 模式
```ngra
#input event
#element event
#run stepping
DangerPattern :=
<DamageTaken>{amount > 50}
<HealthBelow>{value < 20}
<EnemyNearby>+
;
```
适合持续输入:
```text
event1 -> analyzer state
event2 -> analyzer state
event3 -> partial/full match
...
```
分析器不会自动关闭,直到用户显式结束。
---
## 19. 当前设计小结
当前 `.ngra` 设计可以概括为:
```text
.ngra file
= header directives
+ NGRAP assignment expressions
```
头部指令:
```ngra
#input string|byte|bit|token|event|custom_name
#element token|syntax|event|custom_name
#import other.ngra
#run scanning|stepping
```
规则形式:
```ngra
Symbol := Pattern;
```
核心机制:
- 左值是目标元素或目标元素序列;
- 右值是增强型序列正则;
- `Regex(expr, group)` 支持消费输入但只产出指定组;
- `<...>` 调用目标元素或规则,`<T "...">` 是 `<T>{value="..."}` 的简写;
- `<...>{...}` 对元素属性施加约束;
- `<r"...">` 可直接书写完整可用正则,且默认 discard;
- `(?<name>: pattern)` 定义捕获组;
- `$name` 引用捕获;
- `Append`、`Cases`、`Count` 用于组合正则表达式;
- `@count`、`@size`、`@line`、`@column` 提供匹配上下文;
- 规则调用可递归;
- NGRA 通过预压缩和岛屿分层避免递归卡死;
- `scanning` 用于一次性分析;
- `stepping` 用于持续事件流分析;
- `.ngra` 解释器本身采用 NGRA 自举实现;
- 树构造由默认节点或用户 tree builder 完成。
---
## 20. 后续待定问题
以下内容可以后续继续设计:
1. 是否需要 `#start` 指令;
2. 是否需要 `#namespace`;
3. 是否需要 `#output`;
4. 是否需要 `#builder`;
5. 是否保留 `#ignore` / `#trivia`;
6. `<r"...">` 与 `Regex(...)` 共享哪一套正则方言与 flags 语义;
7. `Regex(expr, group)` 中 group 是否支持数字组;
8. `@offset`、`@endLine`、`@endColumn` 是否内建;
9. 用户自定义内联函数的注册方式;
10. 用户自定义谓词的纯度声明;
11. stepping 模式的窗口和缓存策略;
12. import 的 namespace 与符号冲突策略;
13. 默认 GenericNode 的具体结构;
14. tree builder 的源生成接口形式;
15. 自举阶段的 core grammar 与完整 grammar 应如何分层演化。
---
## 22. 一句话定义
NGRA 是一个广义序列分析器源生成框架;`.ngra` 文件通过头部指令声明分析环境,
通过 `Symbol := Pattern;` 形式的 NGRAP 表达式描述
如何从输入序列中提取、匹配、捕获、约束并压缩目标元素,
最终生成可用于源码、二进制流、事件流或任意可分类线性序列的分析器。