refactor: define lexical rules in a separate PEG grammar
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 6m57s

This commit is contained in:
2026-10-19 13:56:05 +08:00
parent f77b8370ea
commit be4f1add58
18 changed files with 206 additions and 167 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ Rust 编写的 Endor 虚拟机实现。核心对象与对象存储位于独立 o
Endor 采用平台解释执行模型;AST 到字节码的生成属于解释器内部准备步骤,使用者无需先构建独立程序。资源修订与依赖锁用于选择用户代码,解释器由平台统一升级;相关设计方向见 [解释执行与平台资源版本](docs/architecture/execution-and-versions.md),资源版本管理尚未实现。
独立 frontend 已提供 Endor 阅读器、手写 lexer、peg parser、基础函数/表达式 AST 与 cli parse 文件/stdin 入口。VM 从空堆启动;AST 到字节码生成、Python type/元类体系、数值算术和完整异常传播均未实现。数据库连接仅为需要数据库的特殊类保留;普通宿主不连接数据库;基础信息改用 host 包的 UTF-8 JSON 文件,不保存 VM 堆。当前边界见 [系统架构](docs/architecture/README.md)。
独立 frontend 已提供 Endor 阅读器、字符 PEG lexer、peg parser、基础函数/表达式 AST 与 cli parse 文件/stdin 入口。VM 从空堆启动;AST 到字节码生成、Python type/元类体系、数值算术和完整异常传播均未实现。数据库连接仅为需要数据库的特殊类保留;普通宿主不连接数据库;基础信息改用 host 包的 UTF-8 JSON 文件,不保存 VM 堆。当前边界见 [系统架构](docs/architecture/README.md)。
## 从哪里读
+1 -1
View File
@@ -6,7 +6,7 @@
| --- | --- | --- |
| [core](core/README.md) | core,库 endor_core | VM、执行帧与字节码求值循环,仅依赖本地 objects |
| [objects](objects/README.md) | objects | 零依赖的核心对象、对象存储、代码对象与指令数据/静态验证 |
| [frontend](frontend/README.md) | frontend | Endor 阅读器、手写 lexer、token PEG parser、基础函数/表达式 AST,不执行源码 |
| [frontend](frontend/README.md) | frontend | Endor 阅读器、字符 PEG lexer、token PEG parser、基础函数/表达式 AST,不执行源码 |
| [host](host/README.md) | host | UTF-8 JSON 文件基础信息,与数据库分离 |
| [storage](storage/README.md) | storage | 为数据库特殊类保留的显式连接适配,不提供基础信息存储 |
| [cli](cli/README.md) | cli,二进制 cli | 引导演示、源码解析与文件信息命令,不使用数据库 |
+2 -2
View File
@@ -1,11 +1,11 @@
# frontend
独立源码前端:EndorReader → SourceUnit → 手写 Rust lexer → token → rust-peg parser → 拥有文本的 AST。当前解析基础函数与表达式,不生成字节码、不进行类型/名称解析、不执行源码。语法是可继续调整的首版实现,当前契约见 [源码前端](../../docs/runtime/frontend.md)。
独立源码前端:EndorReader → SourceUnit → 字符 PEG lexer → token → token PEG parser → 拥有文本的 AST。当前解析基础函数与表达式,不生成字节码、不进行类型/名称解析、不执行源码。语法是可继续调整的首版实现,当前契约见 [源码前端](../../docs/runtime/frontend.md)。
| 内容 | 职责 |
| --- | --- |
| Cargo.toml | frontend 包与 peg 0.8.6 依赖,不依赖 core、Host 或 Storage |
| [src](src/README.md) | 阅读器、SourceUnit、token、源码范围、诊断、AST、词法;PEG 规则独立在 [grammar.peg](src/grammar.peg) |
| [src](src/README.md) | 阅读器、SourceUnit、token、源码范围、诊断、AST、词法规则独立在 [lexer.peg](src/lexer.peg),语法规则在 [grammar.peg](src/grammar.peg) |
| [tests](tests/README.md) | Unicode、优先级、函数、完整输入与错误位置验证 |
| [examples](examples/README.md) | 可交给 cli parse 的源码样例 |
+3 -2
View File
@@ -7,9 +7,10 @@
| [reader.rs](reader.rs) | 字符串、流、文件读取、严格 UTF-8 解码与宿主 EndorSourceProvider 适配 |
| [token.rs](token.rs) | 可复制的 token 种类和原始源码范围,文本由源码切片取得 |
| [diagnostic.rs](diagnostic.rs) | UTF-8 字节范围、词法/语法诊断与一基行列转换 |
| [lexer.rs](lexer.rs) | 名称、数字、字符串、标点、空白与注释扫描;字符串校验及解码 |
| [lexer.rs](lexer.rs) | lex/lex_source 接口、源码游标、逐 Token 调用、位置/诊断换算与字符串解码 |
| [lexer.peg](lexer.peg) | 字符输入的 PEG 词法规则:名称、数字、字符串/转义、标点、空白/注释和错误识别 |
| [ast.rs](ast.rs) | 拥有文本的函数、参数、绑定、return、表达式与运算符结构 |
| [parser.rs](parser.rs) | 解析 API、错误映射、AST 辅助函数,以及独立语法文件的编译期引入 |
| [grammar.peg](grammar.peg) | token 输入的 PEG 规则、运算符优先级与 AST 构造动作;保留 peg::parser! 外层声明 |
改输入适配时同步 [阅读器契约](../../../docs/runtime/reader.md) 与读取错误检查;改词法时同步 token 和错误检查;改语法从 grammar.peg 进入,并同步 AST、[测试](../tests/README.md)、样例与 [前端契约](../../../docs/runtime/frontend.md)。不在语法动作中做 VM 分配、I/O 或修改外部状态,避免回溯造成副作用。返回 [frontend](../README.md)。
改输入适配时同步 [阅读器契约](../../../docs/runtime/reader.md) 与读取错误检查;改词法从 lexer.peg 进入,并同步 token、诊断和错误检查;改语法从 grammar.peg 进入,并同步 AST、[测试](../tests/README.md)、样例与 [前端契约](../../../docs/runtime/frontend.md)。词法与语法两份 PEG 动作均不做 VM 分配、I/O 或修改外部状态,避免回溯造成副作用。返回 [frontend](../README.md)。
+90
View File
@@ -0,0 +1,90 @@
peg::parser! {
grammar lexical() for str {
rule trivia()
= ([' ' | '\t' | '\r' | '\n'] / "//" [^'\r' | '\n']*)*
rule name_start()
= [character if character == '_' || character.is_alphabetic()]
rule name_continue()
= [character if character == '_' || character.is_alphanumeric()]
rule name() -> Result<TokenKind, Diagnostic>
= name_start() name_continue()* { Ok(TokenKind::Name) }
rule number() -> Result<TokenKind, Diagnostic>
= start:position!() ['0'..='9']+ ("." ['0'..='9']+)?
suffix:$(name_continue()*) end:position!() {
if suffix.is_empty() {
Ok(TokenKind::Number)
} else {
Err(Diagnostic::lexical(
Span::new(start, end),
"unsupported numeric suffix or missing token separator",
))
}
}
rule escape()
= "\\" ['n' | 't' | '"' | '\\']
rule string_character()
= [^'"' | '\\' | '\r' | '\n' | '\0']
rule string_end(start: usize) -> Result<(), Diagnostic>
= "\"" { Ok(()) }
/ position:position!() "\\" [_] end:position!() {
Err(Diagnostic::lexical(Span::new(position, end), "unsupported string escape"))
}
/ "\\" end:position!() ![_] {
Err(Diagnostic::lexical(Span::new(start, end), "unterminated string"))
}
/ position:position!() ['\r' | '\n' | '\0'] end:position!() {
Err(Diagnostic::lexical(Span::new(position, end), "unescaped newline or NUL in string"))
}
/ end:position!() ![_] {
Err(Diagnostic::lexical(Span::new(start, end), "unterminated string"))
}
rule string() -> Result<TokenKind, Diagnostic>
= start:position!() "\"" (escape() / string_character())* end:string_end(start) {
end.map(|()| TokenKind::String)
}
rule punctuation() -> TokenKind
= "(" { TokenKind::LeftParen }
/ ")" { TokenKind::RightParen }
/ "{" { TokenKind::LeftBrace }
/ "}" { TokenKind::RightBrace }
/ ":" { TokenKind::Colon }
/ "," { TokenKind::Comma }
/ "." { TokenKind::Dot }
/ "=" { TokenKind::Equal }
/ "+" { TokenKind::Plus }
/ "-" { TokenKind::Minus }
/ "*" { TokenKind::Star }
/ "/" { TokenKind::Slash }
rule kind() -> Result<TokenKind, Diagnostic>
= name()
/ number()
/ string()
/ kind:punctuation() { Ok(kind) }
/ start:position!() [_] end:position!() {
Err(Diagnostic::lexical(Span::new(start, end), "unexpected source character"))
}
rule token() -> Result<Token, Diagnostic>
= start:position!() kind:kind() end:position!() {
kind.map(|kind| Token { kind, span: Span::new(start, end) })
}
// Scan only one prefix. Errors are returned as values, so a matched
// malformed number/string cannot backtrack into another token kind.
#[no_eof]
pub rule next() -> (usize, Result<Option<Token>, Diagnostic>)
= trivia() result:(token:token() { token.map(Some) } / ![_] { Ok(None) }) end:position!() {
(end, result)
}
}
}
+30 -146
View File
@@ -10,59 +10,8 @@ pub fn lex_source(source: &crate::SourceUnit) -> Result<Vec<Token>, crate::Sourc
pub fn lex(source: &str) -> Result<Vec<Token>, Diagnostic> {
let mut lexer = Lexer { source, offset: 0 };
let mut tokens = Vec::new();
while let Some(character) = lexer.peek() {
if matches!(character, ' ' | '\t' | '\r' | '\n') {
let _ = lexer.advance();
continue;
}
if lexer.remaining().starts_with("//") {
while lexer
.peek()
.is_some_and(|character| !matches!(character, '\r' | '\n'))
{
let _ = lexer.advance();
}
continue;
}
let start = lexer.offset;
let kind = match character {
character if name_start(character) => {
let _ = lexer.advance();
while lexer.peek().is_some_and(name_continue) {
let _ = lexer.advance();
}
TokenKind::Name
}
'0'..='9' => lexer.number()?,
'"' => lexer.string()?,
_ => {
let _ = lexer.advance();
match character {
'(' => TokenKind::LeftParen,
')' => TokenKind::RightParen,
'{' => TokenKind::LeftBrace,
'}' => TokenKind::RightBrace,
':' => TokenKind::Colon,
',' => TokenKind::Comma,
'.' => TokenKind::Dot,
'=' => TokenKind::Equal,
'+' => TokenKind::Plus,
'-' => TokenKind::Minus,
'*' => TokenKind::Star,
'/' => TokenKind::Slash,
_ => {
return Err(Diagnostic::lexical(
Span::new(start, lexer.offset),
"unexpected source character",
));
}
}
}
};
tokens.push(Token {
kind,
span: Span::new(start, lexer.offset),
});
while let Some(token) = lexer.next()? {
tokens.push(token);
}
Ok(tokens)
}
@@ -72,100 +21,33 @@ struct Lexer<'source> {
offset: usize,
}
impl<'source> Lexer<'source> {
fn remaining(&self) -> &'source str {
&self.source[self.offset..]
impl Lexer<'_> {
fn next(&mut self) -> Result<Option<Token>, Diagnostic> {
let base = self.offset;
let (consumed, token) = lexical::next(&self.source[base..]).map_err(|error| {
let start = base + error.location.offset;
let end = start
+ self.source[start..]
.chars()
.next()
.map_or(0, char::len_utf8);
Diagnostic::lexical(Span::new(start, end), "unexpected source character")
})?;
self.offset += consumed;
token
.map(|token| {
token.map(|mut token| {
token.span.start += base;
token.span.end += base;
token
})
})
.map_err(|mut error| {
error.span.start += base;
error.span.end += base;
error
})
}
fn peek(&self) -> Option<char> {
self.remaining().chars().next()
}
fn advance(&mut self) -> Option<char> {
let character = self.peek()?;
self.offset += character.len_utf8();
Some(character)
}
fn digits(&mut self) {
while self
.peek()
.is_some_and(|character| character.is_ascii_digit())
{
let _ = self.advance();
}
}
fn number(&mut self) -> Result<TokenKind, Diagnostic> {
let start = self.offset;
self.digits();
let mut following = self.remaining().chars();
if following.next() == Some('.')
&& following
.next()
.is_some_and(|character| character.is_ascii_digit())
{
let _ = self.advance();
self.digits();
}
if self.peek().is_some_and(name_continue) {
while self.peek().is_some_and(name_continue) {
let _ = self.advance();
}
return Err(Diagnostic::lexical(
Span::new(start, self.offset),
"unsupported numeric suffix or missing token separator",
));
}
Ok(TokenKind::Number)
}
fn string(&mut self) -> Result<TokenKind, Diagnostic> {
let start = self.offset;
let _ = self.advance();
loop {
let position = self.offset;
match self.advance() {
Some('"') => return Ok(TokenKind::String),
Some('\\') => match self.advance() {
Some('n' | 't' | '"' | '\\') => {}
Some(_) => {
return Err(Diagnostic::lexical(
Span::new(position, self.offset),
"unsupported string escape",
));
}
None => {
return Err(Diagnostic::lexical(
Span::new(start, self.offset),
"unterminated string",
));
}
},
Some('\r' | '\n' | '\0') => {
return Err(Diagnostic::lexical(
Span::new(position, self.offset),
"unescaped newline or NUL in string",
));
}
None => {
return Err(Diagnostic::lexical(
Span::new(start, self.offset),
"unterminated string",
));
}
_ => {}
}
}
}
}
fn name_start(character: char) -> bool {
character == '_' || character.is_alphabetic()
}
fn name_continue(character: char) -> bool {
character == '_' || character.is_alphanumeric()
}
pub(crate) fn decode_string(raw: &str) -> String {
@@ -186,3 +68,5 @@ pub(crate) fn decode_string(raw: &str) -> String {
}
result
}
include!("lexer.peg");
+1 -1
View File
@@ -1,4 +1,4 @@
//! UTF-8 source reader, handwritten lexer and token-based rust-peg parser.
//! UTF-8 source reader and rust-peg rules for lexing and token-based parsing.
//! Produces syntax only; it does not resolve types, generate code or execute VM
//! operations. AST data owns its text and does not borrow the source buffer.
+2
View File
@@ -2,4 +2,6 @@
[syntax.rs](syntax.rs) 覆盖 UTF-8 token 范围、中文/日文名称、数字原文、负号、字符串转义与错误、算术优先级和结合性、成员/调用链、函数与绑定、上下文名称、完整输入、非法/未支持语法以及 CRLF 和 EOF 诊断位置。
[lexical.rs](lexical.rs) 专门覆盖字符 PEG 迁移后的原文位置换算、首个错误及详细诊断、Unicode 非法转义、未闭合字符串、注释/除法/成员/数字/字符串边界和空输入/尾随注释。
[reader.rs](reader.rs) 覆盖字符串/分块流/文件、严格 UTF-8、读取失败、URI provider、修订标签、旧缓冲区保留及带来源的诊断。样例也通过 syntax.rs 解析。只验证语法和 AST,不宣称类型检查或运行求值通过。随工作区检查在 ces-control 执行,本地不运行;入口见 [验证](../../../docs/operations/validation.md)。返回 [frontend](../README.md)。
+53
View File
@@ -0,0 +1,53 @@
use frontend::{DiagnosticKind, Span, TokenKind, lex};
#[test]
fn lexical_errors_keep_original_byte_ranges_after_multiple_tokens() {
let prefix = "let 商品 = ";
for (tail, offending, message) in [
(
"12.50名前 ;",
"12.50名前",
"unsupported numeric suffix or missing token separator",
),
("\"日\\q\" ;", "\\q", "unsupported string escape"),
("\"日\\🌸\" ;", "\\🌸", "unsupported string escape"),
("\"日\\\n\" ;", "\\\n", "unsupported string escape"),
("\"日\nrest\" ;", "\n", "unescaped newline or NUL in string"),
("\"日\rrest\" ;", "\r", "unescaped newline or NUL in string"),
("\"日\0rest\" ;", "\0", "unescaped newline or NUL in string"),
("\"未完", "\"未完", "unterminated string"),
("\"未完\\", "\"未完\\", "unterminated string"),
("🌸 ;", "🌸", "unexpected source character"),
] {
let source = format!("{prefix}{tail}");
let error = lex(&source).unwrap_err();
let start = source.find(offending).unwrap();
assert_eq!(error.kind, DiagnosticKind::Lexical, "{source:?}");
assert_eq!(error.span, Span::new(start, start + offending.len()));
assert_eq!(error.message, message, "{source:?}");
assert!(error.expected.is_empty());
}
}
#[test]
fn token_boundaries_distinguish_members_division_comments_and_string_contents() {
let source = " \t// 前置\r\n商品.価格 / 12.50// 行末\0\n\"//実文\" \r\n// 終端";
let tokens = lex(source).unwrap();
let spellings: Vec<_> = tokens
.iter()
.map(|token| (token.kind, &source[token.span.start..token.span.end]))
.collect();
assert_eq!(
spellings,
vec![
(TokenKind::Name, "商品"),
(TokenKind::Dot, "."),
(TokenKind::Name, "価格"),
(TokenKind::Slash, "/"),
(TokenKind::Number, "12.50"),
(TokenKind::String, "\"//実文\""),
]
);
assert!(lex("").unwrap().is_empty());
assert!(lex(" \t\r\n// 注釈\0").unwrap().is_empty());
}
+1 -1
View File
@@ -11,7 +11,7 @@
| EndorObject、对象头、句柄、默认操作 | [对象模型](runtime/objects.md) | [objects/src](../crates/objects/src/README.md) |
| Number、String、Dict、Cell、Function、异常数据 | [基本对象](runtime/builtins.md) | [objects/src](../crates/objects/src/README.md) |
| 字符串、文件、流与 endor URI 如何进入 lexer | [Endor 阅读器](runtime/reader.md) | [reader.rs](../crates/frontend/src/reader.rs)、[source.rs](../crates/frontend/src/source.rs) |
| 源码如何分词、PEG 规则、AST 和诊断 | [源码前端](runtime/frontend.md) | [grammar.peg](../crates/frontend/src/grammar.peg)、[frontend](../crates/frontend/README.md)、[CLI](../crates/cli/README.md) |
| 源码如何分词、PEG 规则、AST 和诊断 | [源码前端](runtime/frontend.md) | [lexer.peg](../crates/frontend/src/lexer.peg)、[grammar.peg](../crates/frontend/src/grammar.peg)、[frontend](../crates/frontend/README.md)、[CLI](../crates/cli/README.md) |
| 操作码、代码验证、局部槽、函数定义与调用协议 | [字节码 VM](runtime/bytecode.md) | instruction.rs/code.rs 在 [objects/src](../crates/objects/src/README.md),frame.rs/eval.rs 在 [core/src](../crates/core/src/README.md) |
| 基础信息保存在哪里、旧库如何迁移 | [宿主文件信息](operations/host-information.md) | [host](../crates/host/README.md) |
| UTF-8 在日本地区是否适用,其他编码怎样进入 VM | [文本编码](runtime/text-encoding.md) | [core](../crates/core/README.md)、[host](../crates/host/README.md) |
+1 -1
View File
@@ -26,7 +26,7 @@ CLI、Server 依赖 Host 保存文件信息,不依赖 Storage;Core 只依赖
| --- | --- | --- |
| Core | Vm、执行帧、调用协议与取指求值循环;对象方法委托到 Objects | [字节码 VM](../runtime/bytecode.md) |
| Objects | 核心对象、私有堆、ObjectStore、Number、代码对象、指令数据和静态验证;无求值器 | [对象模型](../runtime/objects.md) |
| Frontend | Endor 阅读器、手写 UTF-8 lexer、token PEG parser、基础函数/表达式 AST,不执行源码 | [源码前端](../runtime/frontend.md) |
| Frontend | Endor 阅读器、UTF-8 字符 PEG lexer、token PEG parser、基础函数/表达式 AST,不执行源码 | [源码前端](../runtime/frontend.md) |
| Host | 基础信息的逐键 UTF-8 JSON 文件与旧库导入 | [宿主信息](../operations/host-information.md) |
| Storage | 按显式配置建立 SQLite/PostgreSQL 连接池,不定义应用表 | [数据库特殊类](../runtime/database-objects.md) |
| CLI | 对象引导演示、源码解析与文件信息命令 | [CLI](../../crates/cli/README.md) |
+1 -1
View File
@@ -16,7 +16,7 @@
| 对象头、句柄、引用协议与 GC 算法 | [objects.md](../runtime/objects.md) | objects/src 文件表 |
| Number、String、Dict、Cell、函数环境和异常数据 | [builtins.md](../runtime/builtins.md) | objects/src、objects/tests 与 core/tests 文件表 |
| lexer 前的输入适配、SourceUnit 与来源诊断 | [reader.md](../runtime/reader.md) | frontend 的 reader/source、CLI 与读取检查 |
| token、PEG 规则、AST 与源码诊断 | [frontend.md](../runtime/frontend.md) | frontend 源码/测试/样例和 CLI |
| Token、lexer.peg 词法/grammar.peg 语法规则、AST 与源码诊断 | [frontend.md](../runtime/frontend.md) | frontend 源码/测试/样例和 CLI |
| 指令编码、栈效果、调用协议与执行限制 | [bytecode.md](../runtime/bytecode.md) | instruction/code/frame/eval 文件职责 |
| 宿主基础信息、JSON 文件和旧库导入 | [host-information.md](../operations/host-information.md) | host、cli、server 与脚本 README |
| 地区与文本编码边界 | [text-encoding.md](../runtime/text-encoding.md) | 配置主题与实际 I/O 入口 |
+1 -1
View File
@@ -16,7 +16,7 @@ Objects 零依赖,统一拥有核心对象、Number、代码对象/指令数
- Function、NativeFunction、Dict、Cell、异常数据、UTF-8 String 和无损表示优先的 Number。
- 小型 wordcode、共享代码对象、操作数/控制流/栈校验、显式执行帧及 execute/call;MakeFunction 可在执行中创建带默认参数与共享闭包 Cell 的函数。对象返回值持有根,用后 release_value;借用读取不自动 retain。
- EndorReader 在 lexer 前提供字符串/流/文件/宿主 URI provider 输入,产出不可变 SourceUnit,保存来源与不透明修订标签;带来源的词法/语法错误保留行列,CLI parse 支持文件和 stdin。没有平台 URI 后端或版本锁实现,见 [阅读器](../runtime/reader.md)。
- Frontend 使用手写 lexer 与 peg 0.8.6,提供完整输入解析、基础函数/表达式 AST、Unicode 字节范围与行列诊断;CLI parse 可读 UTF-8 文件/stdin 输出 AST,不执行源码。当前语法以未完成设计稿为起点,可以继续调整,见 [源码前端](../runtime/frontend.md)。
- Frontend 使用 peg 0.8.6,词法规则在 lexer.peg(字符 → Token),语法规则在 grammar.peg(Token → AST);lexer.rs 保留逐 Token 游标与诊断位置换算,提供完整输入解析、基础函数/表达式 AST、Unicode 字节范围与行列诊断;CLI parse 可读 UTF-8 文件/stdin 输出 AST,不执行源码。当前语法以未完成设计稿为起点,可以继续调整,见 [源码前端](../runtime/frontend.md)。
- Host 文件信息独立于数据库:host.version、deployment.release 与 metadata put/get 使用 UTF-8 JSON。旧 PostgreSQL 基础信息只读导入缺失文件,不删除原库,见 [文件信息](../operations/host-information.md)。
- Storage 只提供显式 DatabaseConfig/DatabaseConnection;没有默认数据库、自动应用建表或基础信息 KV。特殊类的名称/接口尚未定义,没有将连接接入 VM。
- CLI 引导与文件信息,Server HTTP 存活/只读 VM 信息;生产 PostgreSQL 作为未来特殊类的预置资源保留。
+2 -2
View File
@@ -16,7 +16,7 @@ cargo test -p storage --test postgres --locked -- --ignored
python3 scripts/smoke.py
```
check.sh 执行 cargo fmt --all --check、独立语法文件的 rustfmt --edition 2024 --check crates/frontend/src/grammar.peg、cargo test --workspace --locked 和 cargo clippy --workspace --all-targets --locked -- -D warnings。workspace 测试也包含 rustdoc 示例;显式忽略的 PostgreSQL 测试需要单独执行。
check.sh 执行 cargo fmt --all --check、独立词法/语法文件的 rustfmt --edition 2024 --check crates/frontend/src/lexer.peg crates/frontend/src/grammar.peg、cargo test --workspace --locked 和 cargo clippy --workspace --all-targets --locked -- -D warnings。workspace 测试也包含 rustdoc 示例;显式忽略的 PostgreSQL 测试需要单独执行。
ci-ces-control.sh 负责准备全新专用临时 PostgreSQL 和测试 URL;成功路径使用真实数据库,不用模拟 socket 代替。测试库与生产数据不共享挂载。没有 ASTER_TEST_POSTGRES_URL 时,显式执行 PostgreSQL 测试会失败,不标记为通过。
@@ -30,7 +30,7 @@ ci-ces-control.sh 负责准备全新专用临时 PostgreSQL 和测试 URL;成
| 基本对象 | Number 表示、字符串、Dict/Cell 引用与更新、函数环境、异常数据 | 同目录 builtins.rs |
| 字节码 | 编解码、非法指令/控制流、跳转、调用、闭包、字节码创建函数/默认值/环境捕获、函数创建及后续失败清理、结果存活与限额 | 同目录 execution.rs |
| Endor 阅读器 | 字符串、分块 UTF-8 流、文件、读取失败、URI provider、修订标签与来源诊断 | [frontend/tests](../../crates/frontend/tests/README.md) 的 reader.rs |
| 源码前端 | Unicode 范围、数字原文、字符串与错误、优先级/结合性、函数/绑定、完整输入与错误位置、源码样例 | [frontend/tests](../../crates/frontend/tests/README.md) |
| 源码前端 | 字符 PEG 词法边界、逐 Token 原文位置与首个错误诊断、Unicode 范围、数字原文、字符串与错误、优先级/结合性、函数/绑定、完整输入与错误位置、源码样例 | [frontend/tests](../../crates/frontend/tests/README.md) |
| CLI 解析 | AST 输出、未执行源码、文件/stdin 来源和行列错误、EOF 读取与非法 UTF-8 退出 | [cli/tests](../../crates/cli/tests/README.md) |
| 公开示例 | 对象操作、手写指令执行与释放 | objects 与 core 的 src/lib.rs rustdoc,见 [objects/src](../../crates/objects/src/README.md)、[core/src](../../crates/core/src/README.md) |
| 宿主文件信息 | UTF-8 值/路径、持久读取、同键替换、并发、坏记录、幂等迁移与保留新值 | [host/tests](../../crates/host/tests/README.md) |
+1 -1
View File
@@ -7,7 +7,7 @@
| [对象模型](objects.md) | EndorObject/ObjectHeader、代际句柄、操作协议、托管引用与循环回收 |
| [基本对象与 Number](builtins.md) | 函数、原生函数、字典、Cell、异常数据、字符串与无损数字 |
| [Endor 阅读器](reader.md) | SourceUnit、来源/修订标签、字符串/流/文件和宿主 URI provider |
| [源码前端](frontend.md) | 手写 lexer、token PEG parser、AST、基础函数/表达式语法与诊断 |
| [源码前端](frontend.md) | 字符 PEG lexer、token PEG parser、AST、基础函数/表达式语法与诊断 |
| [字节码 VM](bytecode.md) | 指令编码、代码校验、局部槽、执行帧、函数创建、调用/返回与边界 |
| [文本编码](text-encoding.md) | UTF-8、日文、外部 Shift_JIS/CP932 与宿主解码边界 |
| [数据库特殊类](database-objects.md) | 显式连接、调用方数据模型、未来类接入与资源生命周期 |
+14 -5
View File
@@ -1,14 +1,23 @@
# 源码词法与 PEG 语法前端
frontend 包提供 [Endor 阅读器](reader.md)、手写 Rust lexer 和 rust-peg 生成的 token parser。宿主通过 SourceUnit 调用带来源的词法/语法入口,原有 lex、parse、parse_expression 保留;CLI 提供 parse 文件/stdin 命令;结果是 AST,尚未实现 AST 到字节码生成、类型检查、名称解析或源码执行。Core 仅依赖本地 Objects,不依赖 frontend 或 peg;Objects 保持零依赖。
frontend 包提供 [Endor 阅读器](reader.md)、rust-peg 生成的字符 lexer 与 token parser。宿主通过 SourceUnit 调用带来源的词法/语法入口,原有 lex、parse、parse_expression 保留;CLI 提供 parse 文件/stdin 命令;结果是 AST,尚未实现 AST 到字节码生成、类型检查、名称解析或源码执行。Core 仅依赖本地 Objects,不依赖 frontend 或 peg;Objects 保持零依赖。
首版以 Endorphin-Language 设计稿的基础词法、表达式与函数章节为起点,语法可以继续修改;设计稿中的未完成部分不作为阻塞前端实现的前置条件,也不被整体视作已经实现的语言能力。这里维护当前解析契约;源码位置见 [frontend](../../crates/frontend/README.md)。
## 语法文件
## 词法与语法文件
实际 PEG 规则集中在 [grammar.peg](../../crates/frontend/src/grammar.peg),包含 token 匹配、函数/绑定/return、表达式优先级与 AST 构造动作。[parser.rs](../../crates/frontend/src/parser.rs) 保留解析入口、诊断转换和 AST 辅助函数,通过 include!("grammar.peg") 在编译期引入规则。
两份规则复用现有 peg 0.8.6 依赖,分别处理字符和 Token:
grammar.peg 保留 rust-peg 0.8 的 peg::parser! 外层声明,因此是直接参与编译的语法源文件;不另存一份说明性文法,也不新增生成脚本或 build-dependencies。修改规则后随 frontend 重新编译。远端检查脚本单独用 rustfmt 检查该扩展名文件的格式。
| 文件 | 输入与职责 | Rust 接口 |
| --- | --- | --- |
| [lexer.peg](../../crates/frontend/src/lexer.peg) | str 字符输入,识别名称、数字、字符串/转义、标点、空白/注释及词法错误,生成 Token | [lexer.rs](../../crates/frontend/src/lexer.rs) 保留 lex/lex_source、游标、位置换算和字符串解码 |
| [grammar.peg](../../crates/frontend/src/grammar.peg) | [Token] 输入,匹配函数/绑定/return、表达式优先级并构造 AST | [parser.rs](../../crates/frontend/src/parser.rs) 保留解析入口、诊断转换和 AST 辅助函数 |
lexer.rs 与 parser.rs 分别通过 include! 在编译期引入对应规则;两份 .peg 文件保留 peg::parser! 外层声明,直接参与编译,不新增 build.rs、生成脚本或依赖。远端检查脚本单独对两份规则执行 rustfmt。
字符 PEG 的内部 next 规则使用 #[no_eof] 每次识别一个前缀,跳过空白/注释并返回一个 Token 或输入结束。Lexer 保存 source/offset,循环调用该规则,直到全输入结束或首个错误;公开 lex 仍扫描整份输入,不是增量/可恢复输入接口。规则中的 position! 相对本次输入切片,lexer.rs 将 Token 和诊断位置统一换算为原始源码 UTF-8 字节范围。
已识别的非法数字后缀、字符串转义、裸换行/NUL、未闭合字符串和未知字符,以规则返回的 Diagnostic 值传递,不作为允许 PEG 改试其他 Token 种类的匹配失败。识别到词法错误即停止;词法规则不修改外部游标或 Token 容器,避免回溯副作用。前缀规则及动作约定见 [peg 官方文档](https://docs.rs/peg/0.8.6/peg/#end-of-file-handling)。
## 入口和所有权
@@ -23,7 +32,7 @@ grammar.peg 保留 rust-peg 0.8 的 peg::parser! 外层声明,因此是直接
AST 的名称、数字原文和解码后的字符串均由 AST 自己持有,可以在释放输入后继续使用。Token/AST 的 Span 均为原始 UTF-8 源码的半开字节区间 start..end;节点跨度包含首尾 token 之间的空白及注释,不包含外侧 trivia。分组表达式的范围包括括号,Program 范围从第一个函数到最后一个函数。
语法动作只构造 AST,不做 I/O、VM 分配或其他外部写入。PEG 回溯不会执行语言函数或产生宿主业务副作用。
词法动作只返回 Token/诊断,语法动作只构造 AST;两者均不做 I/O、VM 分配或其他外部写入。PEG 回溯不会执行语言函数或产生宿主业务副作用。
## 词法规则
+1 -1
View File
@@ -4,7 +4,7 @@
| 文件 | 入口与副作用 |
| --- | --- |
| [check.sh](check.sh) | Cargo fmt 与独立 grammar.peg 的 rustfmt 检查、workspace 测试、严格 Clippy;会使用 Cargo 依赖和构建产物 |
| [check.sh](check.sh) | Cargo fmt 与独立 lexer.peg/grammar.peg 的 rustfmt 检查、workspace 测试、严格 Clippy;会使用 Cargo 依赖和构建产物 |
| [smoke.py](smoke.py) | 标准库 Python 脚本;构建后用 Cargo metadata 定位 cli/server 的 debug 二进制,支持 CARGO_TARGET_DIR;文件 metadata 跨进程与日文值;失效数据库变量下独立启动、不创建 SQLite 文件、HTTP 与 SIGTERM,不改生产配置 |
| [ci-ces-control.sh](ci-ces-control.sh) | 需要 ASTER_RELEASE_SHA;构建 ci 镜像、创建临时网络/PostgreSQL/检查容器,运行全部检查;退出清理临时容器和网络,保留专属远端 Cargo 缓存卷 |
| [deploy-ces-control.sh](deploy-ces-control.sh) | ces-control root 执行;校验已构建镜像、锁定发布、保留密码和数据、验证 HTTP 与无数据库 CLI 引导后切换 current,失败尝试恢复旧版;保留镜像;应用服务 aster 内调用 cli bootstrap,基础信息改写文件;一次性只读导入旧基础信息,不覆盖新文件或删除原库 |
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/sh
set -eu
cargo fmt --all --check
rustfmt --edition 2024 --check crates/frontend/src/grammar.peg
rustfmt --edition 2024 --check crates/frontend/src/lexer.peg crates/frontend/src/grammar.peg
cargo test --workspace --locked
cargo clippy --workspace --all-targets --locked -- -D warnings