Files
endorphin-aster-vm/docs/runtime/frontend.md
T
2026-10-20 01:08:51 +08:00

8.9 KiB
Raw Blame History

源码词法与 PEG 语法前端

frontend 包提供 Endor 阅读器、rust-peg 生成的字符 lexer 与 token parser。宿主通过 SourceUnit 调用带来源的词法/语法入口,原有 lex、parse、parse_expression 保留;CLI 提供 parse 文件/stdin 命令;结果是 AST;独立 Compiler 已接入名称/作用域分析、最小字节码生成和 CLI run/eval 执行,frontend 自身不执行代码或检查类型。Core 仅依赖本地 Objects,不依赖 frontend 或 peg;Objects 保持零依赖。

首版以 Endorphin-Language 设计稿的基础词法、表达式与函数章节为起点,语法可以继续修改;设计稿中的未完成部分不作为阻塞前端实现的前置条件,也不被整体视作已经实现的语言能力。这里维护当前解析契约;源码位置见 frontend。

词法与语法文件

两份规则复用现有 peg 0.8.6 依赖,分别处理字符和 Token:

文件 输入与职责 Rust 接口
lexer.peg str 字符输入,识别名称、数字、字符串/转义、标点、空白/注释及词法错误,生成 Token lexer.rs 保留 lex/lex_source、游标、位置换算和字符串解码
grammar.peg [Token] 输入,匹配函数/绑定/return、表达式优先级并构造 AST parser.rs 保留解析入口、诊断转换和 AST 辅助函数

lexer.rs 与 parser.rs 分别通过 include! 在编译期引入对应规则;两份 .peg 文件保留 peg::parser! 外层声明,直接参与编译,自身不需要 build.rs 或生成脚本。AST 类型另由 Endor.asdl 离线生成,交换接口使用 serde/serde_json,见 AST 模型。远端检查脚本单独对两份规则执行 rustfmt。

字符 PEG 的内部 next 规则使用 #[no_eof] 每次识别一个前缀,跳过空白/注释并返回一个 Token 或输入结束。Lexer 保存 source/offset,循环调用该规则,直到全输入结束或首个错误;公开 lex 仍扫描整份输入,不是增量/可恢复输入接口。规则中的 position! 相对本次输入切片,lexer.rs 将 Token 和诊断位置统一换算为原始源码 UTF-8 字节范围。

已识别的非法数字后缀、字符串转义、裸换行/NUL、未闭合字符串和未知字符,以规则返回的 Diagnostic 值传递,不作为允许 PEG 改试其他 Token 种类的匹配失败。识别到词法错误即停止;词法规则不修改外部游标或 Token 容器,避免回溯副作用。前缀规则及动作约定见 peg 官方文档。

入口和所有权

API 输入与结果
lex_source(&SourceUnit) 对阅读器输出分词,错误携带来源、修订及行列
parse_source(&SourceUnit) 对阅读器输出解析基础函数 Program,错误携带来源信息
parse_source_expression(&SourceUnit) 对阅读器输出解析一个完整表达式,错误携带来源信息
lex(&str) 产生 Vec;每个 token 只有种类和原始源码的字节范围,不复制文本
parse(&str) 解析一个或多个完整函数定义,返回 Program;空输入和未消费的尾部内容报错
parse_expression(&str) 解析一个完整表达式,不能忽略尾部 token
parse_ast(&str, ParseMode) 返回 Module/Expression/Interactive 三类 AstRoot,模式独立于输入来源
parse_source_ast(&SourceUnit, ParseMode) 同一统一入口,失败保留来源、修订和行列

AST 结构及 Visitor、Spanned 和跨工具 JSON 契约见 AST 模型。Program 使用 declarations,Block 使用 statements;ExprKind 保留既有表达式形状。AST 的名称、数字原文和解码后的字符串均由 AST 自己持有,可以在释放输入后继续使用。Token/AST 的 Span 均为原始 UTF-8 源码的半开字节区间 start..end;节点跨度包含首尾 token 之间的空白及注释,不包含外侧 trivia。分组表达式的范围包括括号,Program 范围从第一个函数到最后一个函数。

词法动作只返回 Token/诊断,语法动作只构造 AST;两者均不做 I/O、VM 分配或其他外部写入。PEG 回溯不会执行语言函数或产生宿主业务副作用。

词法规则

  • lexer 输入是有效 UTF-8 的 &str。阅读器将字符串、流、文件或宿主 URI provider 的结果转换为不可变 SourceUnit;CLI 通过阅读器严格解码字节。其他编码由宿主先显式解码,不修改全局 locale。
  • 名称首字符为下划线或 Rust char::is_alphabetic,后续为下划线或 char::is_alphanumeric。这是当前明确采用的分类,不承诺 XID/NFKC 规则;没有自动规范化。中文、日文可用于名称,emoji 可以用于字符串。
  • 名称统一产生 Name token。function、let、return 只在相应语法位置按拼写识别,不预先固定全局保留字表;class、agent 等也仍可作为名称。
  • 数字为 ASCII 十进制数字,可带两边都有数字的小数部分;原文保留,不转换为 f64。负号单独分词;科学计数法、数字后缀、下划线分隔、前导正号、.5 和 1. 形式不在当前表达式语法中。
  • 双引号字符串允许 Unicode 文本,支持 \n、\t、\"、\\。token 范围包含引号,构造 AST 时解码转义。未闭合字符串、未知转义、裸换行和 NUL 报错;没有单引号、多行或插值字符串。
  • 空格、Tab、LF、CR 和 CRLF 分隔 token;缩进和换行不形成语句或代码块。// 到行末为注释,字符串内的 // 保持文本。没有块注释。
  • 支持 (、)、{、}、:、,、.、=、+、-、*、/;不接受分号和其他未支持字符。

当前语法

program   = function+
function  = "function" name "(" parameters? ")" ":" name block
parameter = name ":" name
block     = "{" binding* "return" expression "}"
binding   = "let" name "=" expression

parameters 和调用参数列表使用逗号分隔,不接受尾随逗号。函数体要求一个末尾 return;函数体尚不支持独立表达式语句、重赋值、嵌套函数声明或默认参数源码写法。新增 Interactive 解析模式接受一次函数声明、let 绑定或表达式,完整消费输入,但不创建 REPL 或执行代码;AST 可表示的语句组合比当前源码子集更广。参数和返回类型目前只是名称节点;并不据此创建语言 type 对象或检查类型。

表达式包括名称、数字、字符串、.name 符号、括号分组、成员访问与位置参数调用、一元负号以及四则运算。优先级从高到低为:

  1. 成员访问与调用,可任意连续组合,例如 factory(1).build(.title)("日文")。
  2. 一元负号,可嵌套。
  3. 乘除,从左到右结合。
  4. 加减,从左到右结合。

符号字面量、名称与成员分别保留为独立 AST 结构。true、false、Nil 等词目前只是普通名称,没有额外的源码字面量或内建值绑定。

use、namespace、class、agent、完整类型语法、条件/循环、比较/布尔运算和闭包声明等尚未加入首版。当前语法可修改;扩展时同步 AST、PEG 规则、样例和测试,不将旧草案视作不可修改的 ABI。

诊断和命令

词法和语法错误分别标记为 Lexical/Syntax。语法错误将 PEG 的 token 下标转换成原始源码 Span,保留期望项目列表;EOF 错误使用 source.len() 处的空区间。Diagnostic::location 返回一基行列,列按 Unicode 标量计数,CRLF 只算一次换行,不按字节列或字形簇计数。

lyra parse <file> 与 lyra parse - 成功时输出 Rust Debug 格式的 AST,失败时在 stderr 输出来源/行/列和诊断并返回非零退出码;- 表示从 stdin 读到 EOF,不是 REPL。阅读与来源包装规则见 阅读器。可用 --mode 选择 module/expression/interactive,用 --json 输出带 schema 标识的 AstDocument;普通输出为 AstRoot Debug 文本,稳定交换格式见 AST 模型。命令不执行源码、不初始化 VM、不连接数据库;具体运行与本地禁测约定见 运行 和 验证。

当前 Compiler 将 AST 经模块/函数名称分析转换为现有字节码,再交给 VM 解释执行。这是解释器内部的编译/执行准备步骤,不要求使用者先构建程序,见 执行与版本模型。MakeFunction 已接入源码函数编译,但解析接受的二元算术和符号值仍没有执行语义;编译器会给出明确诊断,见 编译链路。源码闭包尚未实现。解析器也不是不可信代码沙箱,当前没有通用输入大小或解析递归深度限额。