# 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]*(?[A-Za-z_][A-Za-z0-9_]*)/, id); Number := Regex(/[ \t]*(?[0-9]+)/, num); NewLine := Regex/(?\r\n|\n|\r/, nl); IfExpression := (?: ) (?: ) ; ``` --- ## 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 := | | | ; IfExpression := (?: ) (?: ) ; ``` --- ## 6. 单条 NGRAP 表达式与整体 `.ngra` 的关系 一条 NGRAP 表达式的右值可以视为一种增强型正则表达式。 它支持: - 顺序拼接; - 选择; - 分组; - 捕获; - 计数; - 条件; - 局部规则调用; - 类型化序列元素匹配。 但整个 `.ngra` 文件不是一条正则表达式,而是一个规则集合。 可以概括为: > 单条规则是增强型序列正则; > 整个 `.ngra` 文件是分层序列压缩规则系统。 或者: > Rule-level regex, system-level grammar. --- ## 7. 调用算符 `<...>` ### 7.1 普通规则调用 ```ngra ``` 表示调用已有规则或匹配已有目标元素类型。 例如: ```ngra Expression := | | ; ``` ### 7.2 带值匹配简写 ```ngra ``` 表示匹配某个类型并要求其 `value` 属性满足给定字面量。 这是一种常用简写,等价于: ```ngra {value="if"} {value="("} {value="{"} ``` 例如: ```ngra IfExpression := ; ``` ### 7.3 带属性约束匹配 ```ngra {kind=Identifier} {value="if"} {value < 500} {type="Damage" && amount > 50} ``` 表示先调用某个目标元素或规则,再对该元素属性施加约束。 约束写在 `{...}` 中,约定如下: - 元素属性使用裸属性名,例如 `value`、`kind`、`type`; - NGRA 匹配上下文仍保留 `@count`、`@size` 这类 `@...` 名称; - `` 继续保留,作为 `{value="..."}` 的简写。 --- ## 8. 匿名局部正则调用 `` 为了避免过度声明语法胶水,可以使用: ```ngra ``` `` 表示: > 使用局部正则进行拼接匹配,但该片段默认 discard,不作为后续语义分析中的显式目标元素。 在当前输入类型支持正则的前提下,`` 内允许书写任何可用的正则表达式,而不只限字面量。 由于其匹配结果默认 discard,因此不提供类似 `Regex(expr, group)` 的局部取组语义。 示例: ```ngra IfExpression := (?: ) (?: ) ; ``` `` 适合表示: - 关键字和字面量胶水; - 括号; - 逗号; - 分号; - 冒号; - 局部正则片段; - 其他纯语法胶水。 它应当参与 source span 与 diagnostics,但默认不成为 AST 子节点。 --- ## 9. 捕获组 建议使用接近 PCRE / .NET Regex 的命名捕获形式: ```ngra (?: pattern) ``` 示例: ```ngra IfExpression := (?: ) (?: ) ( (?: ))? ; ``` 捕获引用使用 `$name`: ```ngra LengthPrefixed := (?: ) {=$len.value} ; ``` 命名空间建议: | 类型 | 示例 | | --- | --- | | 捕获定义 | `(?: 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]*(?[A-Za-z_][A-Za-z0-9_]*)/, id); ``` 语义: 1. 整个 `expr` 负责匹配和消费输入; 2. 只有 `group` 指定的捕获组用于生成左值元素; 3. group 外的内容不进入主序列; 4. group 外内容可以视实现保留为 trivia/span 信息; 5. 匹配必须从当前位置开始。 这使得 `.ngra` 可以局部决定哪些空白有用、哪些空白只是分隔符。 例如: ```ngra Identifier := Regex(/[ \t]*(?[A-Za-z_][A-Za-z0-9_]*)/, id); NewLine := Regex/(?\r\n|\n|\r/, nl); IndentRaw := Regex/(?m)^(?[ \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]*/, /(?[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]*(?[A-Za-z_][A-Za-z0-9_]*)/, id); Number := Regex(/[ \t]*(?[0-9]+)/, num); ``` 这表示: - 水平空白被消费; - 只有 `id` / `num` 捕获组成为 token; - 换行可以单独声明为有意义 token。 如果后续确实需要全局忽略机制,可以另行考虑: ```ngra #ignore Whitespace #trivia Comment ``` 但这不应作为首要机制。 --- ## 12. 条件表达式与属性谓词 `<...>` 调用算符后的 `{...}` 应支持简单条件表达式。 示例: ```ngra Plain := {value < 500}+; ``` 表示匹配连续的高度小于 500 的 `Height` 元素。 可以支持: ```ngra {kind=Identifier} {value="if"} {value < 500} {type="Damage" && amount > 50} ``` 建议支持的小型表达式能力: - 字面量; - 属性访问; - 比较; - 布尔运算; - 简单算术; - 捕获引用; - 有限纯函数。 不建议支持任意内嵌 C#。 复杂逻辑应通过用户注册命名谓词或后续 semantic pass 实现。 --- ## 13. 基本属性 NGRA 应内建一组 match context 基本属性。 ### 13.1 `@count` 当前匹配片段中的序列元素数量。 ```ngra Plain := {value < 500}+ where @count >= 8; ``` ### 13.2 `@size` 当前匹配片段覆盖的字节长度。 ```ngra Packet :=
where @size <= 4096; ``` 如果当前输入不提供 byte span,则该属性不可用或需要由用户序列适配器提供。 ### 13.3 `@line` 当前匹配起始位置所在行。 ```ngra TopLevelOnly := where @line == 1; ``` 如果设置为换行符无关序列,则 `@line` 可恒定为 `1`。 ### 13.4 `@column` 当前匹配起始位置相对于行首的偏移量。 ```ngra Indented := where @column > 1; ``` 对于无行列概念的输入,可以不可用,或由输入适配器定义。 建议: - `@...` 命名空间保留给 NGRA match context; - 用户元素属性使用裸属性名,并写在 `<...>{...}` 内; - 捕获变量使用 `$name`。 --- ## 14. 岛屿语法与分层压缩 NGRA 的核心机制之一是: > 规则匹配成功后,可以将匹配到的序列片段压缩成左值类型的岛屿元素,供更高层规则继续分析。 例如: ```ngra IfExpression := (?: ) (?: ) ; ``` 匹配成功后,可以生成: ```text ``` 上层规则不必再关心其内部 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 := | | ; IfExpression := (?: ) (?: ) ; ``` 但为了避免卡死在递归识别中,NGRA 在编译 `.ngra` 文件时应当进行规则图分析。 需要检测: - 递归调用; - 左递归; - 空匹配递归; - 强连通规则组; - 是否存在明确边界; - 是否需要预压缩; - 是否可以并行分析。 核心策略: > 对被调用序列预先进行边界识别和压缩,使上层规则不必纠结内部细节。 例如: ```ngra Block := ... ; ``` 可以先识别 `{ ... }` 的边界并生成 `Block` island,再进入内部解析 statements。 --- ## 16. Tree Builder 与用户代码 NGRA 可以提供默认树节点类型,例如: ```csharp NgraNode NgraToken NgraMatch NgraSpan ``` 但实际使用中,用户通常需要根据领域定义自己的节点。 例如 MagicSpinner 可能需要: ```csharp MagicSpinnerToken MagicSpinnerSyntaxNode SpellDeclarationNode IfExpressionNode VisionTagNode ``` 因此 `.ngra` 文件只描述匹配和捕获,不应强行规定所有 AST 构造细节。 可选设计: ```ngra IfExpression := (?: ) (?: ) ; ``` 生成器产生匹配结果后,调用用户 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]*(?[A-Za-z_][A-Za-z0-9_]*)/, id); Number := Regex(/[ \t]*(?[0-9]+)/, num); Expression := | ; ``` 适合一次性输入: ```text source.ngra input -> complete output ``` --- ### 18.2 stepping 模式 ```ngra #input event #element event #run stepping DangerPattern := {amount > 50} {value < 20} + ; ``` 适合持续输入: ```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)` 支持消费输入但只产出指定组; - `<...>` 调用目标元素或规则,`` 是 `{value="..."}` 的简写; - `<...>{...}` 对元素属性施加约束; - `` 可直接书写完整可用正则,且默认 discard; - `(?: 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. `` 与 `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 表达式描述 如何从输入序列中提取、匹配、捕获、约束并压缩目标元素, 最终生成可用于源码、二进制流、事件流或任意可分类线性序列的分析器。