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