From be4f1add58172f73436e030ba57a86a255420cfe Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Mon, 19 Oct 2026 13:56:05 +0800 Subject: [PATCH] refactor: define lexical rules in a separate PEG grammar --- README.md | 2 +- crates/README.md | 2 +- crates/frontend/README.md | 4 +- crates/frontend/src/README.md | 5 +- crates/frontend/src/lexer.peg | 90 ++++++++++++++++ crates/frontend/src/lexer.rs | 176 ++++++------------------------- crates/frontend/src/lib.rs | 2 +- crates/frontend/tests/README.md | 2 + crates/frontend/tests/lexical.rs | 53 ++++++++++ docs/README.md | 2 +- docs/architecture/README.md | 2 +- docs/contributing/README.md | 2 +- docs/contributing/handoff.md | 2 +- docs/operations/validation.md | 4 +- docs/runtime/README.md | 2 +- docs/runtime/frontend.md | 19 +++- scripts/README.md | 2 +- scripts/check.sh | 2 +- 18 files changed, 206 insertions(+), 167 deletions(-) create mode 100644 crates/frontend/src/lexer.peg create mode 100644 crates/frontend/tests/lexical.rs diff --git a/README.md b/README.md index 26bfca0..eaac90e 100644 --- a/README.md +++ b/README.md @@ -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)。 ## 从哪里读 diff --git a/crates/README.md b/crates/README.md index 2654673..61ebad3 100644 --- a/crates/README.md +++ b/crates/README.md @@ -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 | 引导演示、源码解析与文件信息命令,不使用数据库 | diff --git a/crates/frontend/README.md b/crates/frontend/README.md index f14ed33..4fdd51f 100644 --- a/crates/frontend/README.md +++ b/crates/frontend/README.md @@ -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 的源码样例 | diff --git a/crates/frontend/src/README.md b/crates/frontend/src/README.md index 2334538..4bb4571 100644 --- a/crates/frontend/src/README.md +++ b/crates/frontend/src/README.md @@ -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)。 diff --git a/crates/frontend/src/lexer.peg b/crates/frontend/src/lexer.peg new file mode 100644 index 0000000..cb32503 --- /dev/null +++ b/crates/frontend/src/lexer.peg @@ -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 + = name_start() name_continue()* { Ok(TokenKind::Name) } + + rule number() -> Result + = 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 + = 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 + = name() + / number() + / string() + / kind:punctuation() { Ok(kind) } + / start:position!() [_] end:position!() { + Err(Diagnostic::lexical(Span::new(start, end), "unexpected source character")) + } + + rule token() -> Result + = 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, Diagnostic>) + = trivia() result:(token:token() { token.map(Some) } / ![_] { Ok(None) }) end:position!() { + (end, result) + } + } +} diff --git a/crates/frontend/src/lexer.rs b/crates/frontend/src/lexer.rs index 408d176..41c2d5c 100644 --- a/crates/frontend/src/lexer.rs +++ b/crates/frontend/src/lexer.rs @@ -10,59 +10,8 @@ pub fn lex_source(source: &crate::SourceUnit) -> Result, crate::Sourc pub fn lex(source: &str) -> Result, 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, 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 { - self.remaining().chars().next() - } - - fn advance(&mut self) -> Option { - 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 { - 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 { - 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"); diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index c2ad5a6..4ee0522 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -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. diff --git a/crates/frontend/tests/README.md b/crates/frontend/tests/README.md index 33d2045..4a6ce6e 100644 --- a/crates/frontend/tests/README.md +++ b/crates/frontend/tests/README.md @@ -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)。 diff --git a/crates/frontend/tests/lexical.rs b/crates/frontend/tests/lexical.rs new file mode 100644 index 0000000..0d5fd7d --- /dev/null +++ b/crates/frontend/tests/lexical.rs @@ -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()); +} diff --git a/docs/README.md b/docs/README.md index caf0fdb..fd92f9b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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) | diff --git a/docs/architecture/README.md b/docs/architecture/README.md index d5ee66b..4b94f1c 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -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) | diff --git a/docs/contributing/README.md b/docs/contributing/README.md index 6a0b0df..3590c33 100644 --- a/docs/contributing/README.md +++ b/docs/contributing/README.md @@ -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 入口 | diff --git a/docs/contributing/handoff.md b/docs/contributing/handoff.md index fa568d6..a14eaa7 100644 --- a/docs/contributing/handoff.md +++ b/docs/contributing/handoff.md @@ -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 作为未来特殊类的预置资源保留。 diff --git a/docs/operations/validation.md b/docs/operations/validation.md index 546e3c0..6d225c2 100644 --- a/docs/operations/validation.md +++ b/docs/operations/validation.md @@ -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) | diff --git a/docs/runtime/README.md b/docs/runtime/README.md index 1b70fe5..493ce6d 100644 --- a/docs/runtime/README.md +++ b/docs/runtime/README.md @@ -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) | 显式连接、调用方数据模型、未来类接入与资源生命周期 | diff --git a/docs/runtime/frontend.md b/docs/runtime/frontend.md index 497bd74..6e0b9a7 100644 --- a/docs/runtime/frontend.md +++ b/docs/runtime/frontend.md @@ -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 回溯不会执行语言函数或产生宿主业务副作用。 ## 词法规则 diff --git a/scripts/README.md b/scripts/README.md index 4eb0aea..99adfd2 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -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,基础信息改写文件;一次性只读导入旧基础信息,不覆盖新文件或删除原库 | diff --git a/scripts/check.sh b/scripts/check.sh index 40f25cc..e2f3b48 100644 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -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