From 483d9d17172b084b9ff2631be9bc09dee90a4c19 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Mon, 19 Oct 2026 03:23:38 +0800 Subject: [PATCH] Add Endor source reader before lexing with provenance and URI provider --- README.md | 3 +- crates/README.md | 2 +- crates/cli/README.md | 4 +- crates/cli/src/README.md | 4 +- crates/cli/src/main.rs | 18 +- crates/cli/tests/README.md | 2 +- crates/cli/tests/parse.rs | 42 ++++- crates/frontend/README.md | 6 +- crates/frontend/src/README.md | 6 +- crates/frontend/src/lexer.rs | 5 + crates/frontend/src/lib.rs | 12 +- crates/frontend/src/parser.rs | 11 ++ crates/frontend/src/reader.rs | 147 ++++++++++++++++ crates/frontend/src/source.rs | 79 +++++++++ crates/frontend/tests/README.md | 2 +- crates/frontend/tests/reader.rs | 283 +++++++++++++++++++++++++++++++ docs/README.md | 1 + docs/architecture/README.md | 4 +- docs/contributing/README.md | 1 + docs/contributing/handoff.md | 3 +- docs/operations/README.md | 3 +- docs/operations/configuration.md | 2 +- docs/operations/validation.md | 3 +- docs/runtime/README.md | 1 + docs/runtime/frontend.md | 9 +- docs/runtime/reader.md | 49 ++++++ docs/runtime/text-encoding.md | 2 +- 27 files changed, 667 insertions(+), 37 deletions(-) create mode 100644 crates/frontend/src/reader.rs create mode 100644 crates/frontend/src/source.rs create mode 100644 crates/frontend/tests/reader.rs create mode 100644 docs/runtime/reader.md diff --git a/README.md b/README.md index 9d23c65..f5e1b47 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Rust 编写的 Endor 虚拟机实现。当前具备 EndorObject、稳定对象分配、代际句柄、引用计数、显式循环回收,以及可执行语言函数的小型 wordcode VM。数字统一为无损表示优先的 Number。 -独立 frontend 已提供手写 lexer、peg parser、基础函数/表达式 AST 与 cli parse 文件入口。VM 从空堆启动;尚无 AST 到字节码编译器、Python type/元类体系、数值算术或完整异常传播。数据库连接仅为需要数据库的特殊类保留;普通宿主不连接数据库;基础信息改用 host 包的 UTF-8 JSON 文件,不保存 VM 堆。当前边界见 [系统架构](docs/architecture/README.md)。 +独立 frontend 已提供 Endor 阅读器、手写 lexer、peg parser、基础函数/表达式 AST 与 cli parse 文件/stdin 入口。VM 从空堆启动;尚无 AST 到字节码编译器、Python type/元类体系、数值算术或完整异常传播。数据库连接仅为需要数据库的特殊类保留;普通宿主不连接数据库;基础信息改用 host 包的 UTF-8 JSON 文件,不保存 VM 堆。当前边界见 [系统架构](docs/architecture/README.md)。 ## 从哪里读 @@ -10,6 +10,7 @@ Rust 编写的 Endor 虚拟机实现。当前具备 EndorObject、稳定对象 | --- | --- | | 查一个功能、设计或代码位置 | [文档总索引](docs/README.md) | | 看源码目录与 crate 边界 | [crates](crates/README.md) | +| 看字符串、流、文件与平台 URI 如何进入 lexer | [Endor 阅读器](docs/runtime/reader.md) | | 看源码词法、语法、AST 和解析命令 | [源码前端](docs/runtime/frontend.md) | | 看指令、代码对象、执行帧和执行循环 | [字节码 VM](docs/runtime/bytecode.md) | | 看 VM/帧/对象如何存活和销毁 | [生命周期](docs/runtime/lifecycle.md) | diff --git a/crates/README.md b/crates/README.md index b32d043..6296dc6 100644 --- a/crates/README.md +++ b/crates/README.md @@ -5,7 +5,7 @@ | 目录 | 包名/产物 | 职责 | | --- | --- | --- | | [core](core/README.md) | core,库 endor_core | 零外部依赖的对象运行时、堆与字节码 VM | -| [frontend](frontend/README.md) | frontend | 手写 lexer、token PEG parser、基础函数/表达式 AST,不执行源码 | +| [frontend](frontend/README.md) | frontend | Endor 阅读器、手写 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/cli/README.md b/crates/cli/README.md index 227a168..b6ca2bd 100644 --- a/crates/cli/README.md +++ b/crates/cli/README.md @@ -5,7 +5,7 @@ | 内容 | 职责 | | --- | --- | | Cargo.toml | cli 二进制与 core/frontend/host 依赖;无 storage 或 Tokio 依赖 | -| [src](src/README.md) | 参数路由、引导演示、源码解析和退出码 | +| [src](src/README.md) | 参数路由、引导演示、文件/stdin 阅读、源码解析和退出码 | | [tests](tests/README.md) | parse 命令的 AST 输出、错误位置和严格 UTF-8 检查 | -运行方式见 [运行指南](../../docs/operations/README.md),输入边界见 [配置](../../docs/operations/configuration.md)。parse 严格读取 UTF-8 文件并输出 AST,不执行源码,见 [前端](../../docs/runtime/frontend.md);metadata put/get 和只导入缺失项的 import 均使用文件,见 [宿主信息](../../docs/operations/host-information.md)。返回 [工作区](../README.md)。 +运行方式见 [运行指南](../../docs/operations/README.md),输入边界见 [配置](../../docs/operations/configuration.md)。parse 通过 EndorReader 严格读取 UTF-8 文件或 `-` 指定的 stdin 并输出 AST,不执行源码,见 [前端](../../docs/runtime/frontend.md);metadata put/get 和只导入缺失项的 import 均使用文件,见 [宿主信息](../../docs/operations/host-information.md)。返回 [工作区](../README.md)。 diff --git a/crates/cli/src/README.md b/crates/cli/src/README.md index cdb8b08..fd328a0 100644 --- a/crates/cli/src/README.md +++ b/crates/cli/src/README.md @@ -1,7 +1,7 @@ # CLI 入口 -[main.rs](main.rs) 是唯一入口:同步 main 将 run 的结果转成进程退出码;run 处理 bootstrap、parse 文件、help、文件 metadata put/get/import,错误打印到 stderr 并返回失败。 +[main.rs](main.rs) 是唯一入口:同步 main 将 run 的结果转成进程退出码;run 处理 bootstrap、parse 文件/stdin、help、文件 metadata put/get/import,错误打印到 stderr 并返回失败。 -bootstrap 创建并显式释放两个对象,不读取数据库环境、不打开数据库。parse 使用 frontend 输出 AST,词法/语法错误显示文件、行、列;不创建 VM 或执行源码。未知命令报错;metadata 通过 host::MetadataStore 读写文件,import 从标准输入接收旧记录且不覆盖已有文件。参数目前由 std::env::args 读取,非 Unicode 参数可能 panic,边界见 [配置](../../../docs/operations/configuration.md)。 +bootstrap 创建并显式释放两个对象,不读取数据库环境、不打开数据库。parse 使用 EndorReader 和 parse_source 输出 AST;`-` 从 stdin 读到 EOF,来源为 ``。词法/语法错误显示来源、行、列;不创建 VM 或执行源码。未知命令报错;metadata 通过 host::MetadataStore 读写文件,import 从标准输入接收旧记录且不覆盖已有文件。参数目前由 std::env::args 读取,非 Unicode 参数可能 panic,边界见 [配置](../../../docs/operations/configuration.md)。 新增命令时更新用法、[运行指南](../../../docs/operations/README.md) 和 [smoke.py 的导航](../../../scripts/README.md)。返回 [cli](../README.md)。 diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 79d6fc4..85bdeb7 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -2,9 +2,10 @@ use std::error::Error; use std::process::ExitCode; use endor_core::{Value, Vm}; +use frontend::{EndorReader, parse_source}; use host::MetadataStore; -const USAGE: &str = "Usage: cli [bootstrap | parse | metadata put | metadata get | metadata import | help]"; +const USAGE: &str = "Usage: cli [bootstrap | parse | metadata put | metadata get | metadata import | help]"; fn main() -> ExitCode { match run() { @@ -46,15 +47,12 @@ fn run() -> Result<(), Box> { } ["help"] | ["--help"] | ["-h"] => println!("{USAGE}"), ["parse", path] => { - let source = - std::fs::read_to_string(path).map_err(|error| format!("{path}: {error}"))?; - let program = frontend::parse(&source).map_err(|error| { - let location = error.location(&source); - format!( - "{path}:{}:{}: {}", - location.line, location.column, error.message - ) - })?; + let source = if *path == "-" { + EndorReader::from_stream("", std::io::stdin().lock())? + } else { + EndorReader::from_file(path)? + }; + let program = parse_source(&source)?; println!("{program:#?}"); } ["metadata", "put", key, value] => { diff --git a/crates/cli/tests/README.md b/crates/cli/tests/README.md index 54a7571..426c263 100644 --- a/crates/cli/tests/README.md +++ b/crates/cli/tests/README.md @@ -1,5 +1,5 @@ # CLI 命令检查 -[parse.rs](parse.rs) 使用 Cargo 提供的 cli 测试二进制检查 parse 命令:合法 UTF-8 源码输出 AST 且不执行名称调用;非法语法输出文件/行/列并返回失败;非法 UTF-8 不替换字符。 +[parse.rs](parse.rs) 使用 Cargo 提供的 cli 测试二进制检查 parse 命令:合法 UTF-8 源码输出 AST 且不执行名称调用;非法语法输出文件/行/列并返回失败;非法 UTF-8 不替换字符;`parse -` 检查 stdin 输入、EOF 和带流名的错误。 测试仅在 ces-control 的工作区检查中运行,本地不执行。测试临时文件使用独立名称并在结束时清理。返回 [cli](../README.md),验证约定见 [验证](../../../docs/operations/validation.md)。 diff --git a/crates/cli/tests/parse.rs b/crates/cli/tests/parse.rs index 18e12b7..83b0334 100644 --- a/crates/cli/tests/parse.rs +++ b/crates/cli/tests/parse.rs @@ -1,5 +1,6 @@ +use std::io::Write; use std::path::PathBuf; -use std::process::Command; +use std::process::{Command, Stdio}; use std::sync::atomic::{AtomicUsize, Ordering}; static NEXT_FILE: AtomicUsize = AtomicUsize::new(0); @@ -65,3 +66,42 @@ fn parse_command_reports_source_locations_and_rejects_invalid_utf8() { assert!(!output.status.success()); assert!(output.stdout.is_empty()); } + +fn parse_stdin(source: &[u8]) -> std::process::Output { + let mut child = Command::new(env!("CARGO_BIN_EXE_cli")) + .args(["parse", "-"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let mut stdin = child.stdin.take().unwrap(); + stdin.write_all(source).unwrap(); + drop(stdin); + child.wait_with_output().unwrap() +} + +#[test] +fn parse_command_reads_stdin_to_eof_and_reports_its_origin() { + let output = parse_stdin("function 表示(): String { return \"日\" }".as_bytes()); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(String::from_utf8(output.stdout).unwrap().contains("表示")); + let output = parse_stdin(b"function f(): Number {\n return )\n}"); + assert!(!output.status.success()); + assert!( + String::from_utf8(output.stderr) + .unwrap() + .contains(":2:10:") + ); + let output = parse_stdin(&[0xff]); + assert!(!output.status.success()); + assert!( + String::from_utf8(output.stderr) + .unwrap() + .contains(": invalid UTF-8 at byte 0") + ); +} diff --git a/crates/frontend/README.md b/crates/frontend/README.md index d2c1e64..d417508 100644 --- a/crates/frontend/README.md +++ b/crates/frontend/README.md @@ -1,12 +1,12 @@ # frontend -独立源码前端:手写 Rust lexer → token → rust-peg parser → 拥有文本的 AST。当前解析基础函数与表达式,不生成字节码、不进行类型/名称解析、不执行源码。语法是可继续调整的首版实现,当前契约见 [源码前端](../../docs/runtime/frontend.md)。 +独立源码前端:EndorReader → SourceUnit → 手写 Rust lexer → token → rust-peg parser → 拥有文本的 AST。当前解析基础函数与表达式,不生成字节码、不进行类型/名称解析、不执行源码。语法是可继续调整的首版实现,当前契约见 [源码前端](../../docs/runtime/frontend.md)。 | 内容 | 职责 | | --- | --- | | Cargo.toml | frontend 包与 peg 0.8.6 依赖,不依赖 core、Host 或 Storage | -| [src](src/README.md) | token、源码范围、诊断、AST、词法;PEG 规则独立在 [grammar.peg](src/grammar.peg) | +| [src](src/README.md) | 阅读器、SourceUnit、token、源码范围、诊断、AST、词法;PEG 规则独立在 [grammar.peg](src/grammar.peg) | | [tests](tests/README.md) | Unicode、优先级、函数、完整输入与错误位置验证 | | [examples](examples/README.md) | 可交给 cli parse 的源码样例 | -公开入口为 lex、parse、parse_expression;CLI 文件读取在宿主层。Core 仍为零外部依赖;未来编译器将 AST 转成已有字节码,不改为 AST 直接解释。返回 [工作区](../README.md)。 +宿主通过 EndorReader 读取字符串、流、文件或注入的 URI provider,取得 SourceUnit 后调用 lex_source、parse_source、parse_source_expression;低层 lex、parse、parse_expression 仍保留。读取契约见 [Endor 阅读器](../../docs/runtime/reader.md)。Core 仍为零外部依赖;未来编译器将 AST 转成已有字节码,不改为 AST 直接解释。返回 [工作区](../README.md)。 diff --git a/crates/frontend/src/README.md b/crates/frontend/src/README.md index fd52a43..2334538 100644 --- a/crates/frontend/src/README.md +++ b/crates/frontend/src/README.md @@ -2,7 +2,9 @@ | 文件 | 职责 | | --- | --- | -| [lib.rs](lib.rs) | 公开 AST 模块、lex/parse/parse_expression 与 token、诊断导出 | +| [lib.rs](lib.rs) | 公开 AST 模块、阅读器/来源单元、低层及带来源的词法/语法入口与诊断导出 | +| [source.rs](source.rs) | 不可变 SourceUnit、来源类别和携带来源/行列的 SourceDiagnostic | +| [reader.rs](reader.rs) | 字符串、流、文件读取、严格 UTF-8 解码与宿主 EndorSourceProvider 适配 | | [token.rs](token.rs) | 可复制的 token 种类和原始源码范围,文本由源码切片取得 | | [diagnostic.rs](diagnostic.rs) | UTF-8 字节范围、词法/语法诊断与一基行列转换 | | [lexer.rs](lexer.rs) | 名称、数字、字符串、标点、空白与注释扫描;字符串校验及解码 | @@ -10,4 +12,4 @@ | [parser.rs](parser.rs) | 解析 API、错误映射、AST 辅助函数,以及独立语法文件的编译期引入 | | [grammar.peg](grammar.peg) | token 输入的 PEG 规则、运算符优先级与 AST 构造动作;保留 peg::parser! 外层声明 | -改词法时同步 token 和错误检查;改语法从 grammar.peg 进入,并同步 AST、[测试](../tests/README.md)、样例与 [前端契约](../../../docs/runtime/frontend.md)。不在语法动作中做 VM 分配、I/O 或修改外部状态,避免回溯造成副作用。返回 [frontend](../README.md)。 +改输入适配时同步 [阅读器契约](../../../docs/runtime/reader.md) 与读取错误检查;改词法时同步 token 和错误检查;改语法从 grammar.peg 进入,并同步 AST、[测试](../tests/README.md)、样例与 [前端契约](../../../docs/runtime/frontend.md)。不在语法动作中做 VM 分配、I/O 或修改外部状态,避免回溯造成副作用。返回 [frontend](../README.md)。 diff --git a/crates/frontend/src/lexer.rs b/crates/frontend/src/lexer.rs index 406b8b1..408d176 100644 --- a/crates/frontend/src/lexer.rs +++ b/crates/frontend/src/lexer.rs @@ -1,5 +1,10 @@ use crate::{Diagnostic, Span, Token, TokenKind}; +/// Lex a reader snapshot and preserve provenance in any diagnostic. +pub fn lex_source(source: &crate::SourceUnit) -> Result, crate::SourceDiagnostic> { + lex(source.text()).map_err(|error| source.annotate(error)) +} + /// Lex the basic Endorphin draft syntax without reading files or changing /// locale. Positions always refer to bytes in this exact UTF-8 source. pub fn lex(source: &str) -> Result, Diagnostic> { diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index 242d523..c2ad5a6 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -1,4 +1,4 @@ -//! UTF-8 source frontend: a handwritten lexer and token-based rust-peg parser. +//! UTF-8 source reader, handwritten lexer and token-based rust-peg parser. //! 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. @@ -6,9 +6,15 @@ pub mod ast; mod diagnostic; mod lexer; mod parser; +mod reader; +mod source; mod token; pub use diagnostic::{Diagnostic, DiagnosticKind, SourceLocation, Span}; -pub use lexer::lex; -pub use parser::{parse, parse_expression}; +pub use lexer::{lex, lex_source}; +pub use parser::{parse, parse_expression, parse_source, parse_source_expression}; +pub use reader::{ + EndorReader, EndorSourceProvider, ResolvedEndorSource, SourceReadError, SourceReadErrorKind, +}; +pub use source::{SourceDiagnostic, SourceOrigin, SourceUnit}; pub use token::{Token, TokenKind}; diff --git a/crates/frontend/src/parser.rs b/crates/frontend/src/parser.rs index 1563c93..77ed73e 100644 --- a/crates/frontend/src/parser.rs +++ b/crates/frontend/src/parser.rs @@ -4,6 +4,17 @@ use crate::ast::{ use crate::lexer::decode_string; use crate::{Diagnostic, DiagnosticKind, Span, Token, TokenKind, lex}; +/// Parse a reader snapshot, retaining its origin and resolved revision on error. +pub fn parse_source(source: &crate::SourceUnit) -> Result { + parse(source.text()).map_err(|error| source.annotate(error)) +} + +pub fn parse_source_expression( + source: &crate::SourceUnit, +) -> Result { + parse_expression(source.text()).map_err(|error| source.annotate(error)) +} + /// Parse one or more basic typed function declarations. This is a syntax /// operation: type names and name references are retained without resolution. pub fn parse(source: &str) -> Result { diff --git a/crates/frontend/src/reader.rs b/crates/frontend/src/reader.rs new file mode 100644 index 0000000..60d2c54 --- /dev/null +++ b/crates/frontend/src/reader.rs @@ -0,0 +1,147 @@ +use std::error::Error; +use std::fmt; +use std::fs::File; +use std::io::{self, Read}; +use std::path::Path; +use std::string::FromUtf8Error; + +use crate::{SourceOrigin, SourceUnit}; + +/// The host resolves names, authorizes access and selects a revision before +/// returning its source bytes. No URI path layout or hash algorithm is assumed. +pub trait EndorSourceProvider { + fn load(&self, uri: &str) -> io::Result; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedEndorSource { + pub revision: String, + pub bytes: Vec, +} + +/// Stateless source adapters, before lexing. Streams are read to EOF; session +/// prompting and incomplete REPL input detection belong to the interactive host. +#[derive(Debug, Clone, Copy, Default)] +pub struct EndorReader; + +impl EndorReader { + pub fn from_text(name: impl Into, text: impl Into) -> SourceUnit { + SourceUnit::new(SourceOrigin::Text { name: name.into() }, text.into()) + } + + pub fn from_stream( + name: impl Into, + reader: impl Read, + ) -> Result { + Self::read(SourceOrigin::Stream { name: name.into() }, reader) + } + + pub fn from_file(path: impl AsRef) -> Result { + let path = path.as_ref(); + let origin = SourceOrigin::File { + path: path.to_owned(), + }; + let file = File::open(path).map_err(|error| SourceReadError { + origin: origin.to_string(), + kind: SourceReadErrorKind::Io(error), + })?; + Self::read(origin, file) + } + + pub fn from_endor( + uri: &str, + provider: &P, + ) -> Result { + // Only recognize the scheme and reject empty/control-filled locators. + // Namespace syntax, revision selectors and percent decoding are host policy. + let valid = uri.split_once(':').is_some_and(|(scheme, address)| { + scheme.eq_ignore_ascii_case("endor") && !address.is_empty() + }) && !uri + .chars() + .any(|character| character.is_whitespace() || character.is_control()); + if !valid { + return Err(SourceReadError { + origin: uri.into(), + kind: SourceReadErrorKind::InvalidEndorUri, + }); + } + let resolved = provider.load(uri).map_err(|error| SourceReadError { + origin: uri.into(), + kind: SourceReadErrorKind::Io(error), + })?; + if resolved.revision.trim().is_empty() { + return Err(SourceReadError { + origin: uri.into(), + kind: SourceReadErrorKind::MissingRevision, + }); + } + let origin = SourceOrigin::Endor { + uri: uri.into(), + revision: resolved.revision, + }; + Self::decode(origin, resolved.bytes) + } + + fn read(origin: SourceOrigin, mut reader: impl Read) -> Result { + let mut bytes = Vec::new(); + reader + .read_to_end(&mut bytes) + .map_err(|error| SourceReadError { + origin: origin.to_string(), + kind: SourceReadErrorKind::Io(error), + })?; + Self::decode(origin, bytes) + } + + fn decode(origin: SourceOrigin, bytes: Vec) -> Result { + let text = String::from_utf8(bytes).map_err(|error| SourceReadError { + origin: origin.to_string(), + kind: SourceReadErrorKind::InvalidUtf8(error), + })?; + Ok(SourceUnit::new(origin, text)) + } +} + +#[derive(Debug)] +pub enum SourceReadErrorKind { + Io(io::Error), + InvalidUtf8(FromUtf8Error), + InvalidEndorUri, + MissingRevision, +} + +#[derive(Debug)] +pub struct SourceReadError { + pub origin: String, + pub kind: SourceReadErrorKind, +} + +impl fmt::Display for SourceReadError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{}: ", self.origin)?; + match &self.kind { + SourceReadErrorKind::Io(error) => write!(formatter, "{error}"), + SourceReadErrorKind::InvalidUtf8(error) => write!( + formatter, + "invalid UTF-8 at byte {}", + error.utf8_error().valid_up_to() + ), + SourceReadErrorKind::InvalidEndorUri => formatter.write_str( + "expected a nonempty endor URI without whitespace or control characters", + ), + SourceReadErrorKind::MissingRevision => { + formatter.write_str("source provider returned no resolved revision") + } + } + } +} + +impl Error for SourceReadError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match &self.kind { + SourceReadErrorKind::Io(error) => Some(error), + SourceReadErrorKind::InvalidUtf8(error) => Some(error), + _ => None, + } + } +} diff --git a/crates/frontend/src/source.rs b/crates/frontend/src/source.rs new file mode 100644 index 0000000..29408b5 --- /dev/null +++ b/crates/frontend/src/source.rs @@ -0,0 +1,79 @@ +use std::error::Error; +use std::fmt; +use std::path::PathBuf; + +use crate::{Diagnostic, SourceLocation}; + +/// Source provenance, independent of filesystem layout or VM heap handles. +/// Endor revisions are opaque labels supplied by the host, not verified hashes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SourceOrigin { + Text { name: String }, + Stream { name: String }, + File { path: PathBuf }, + Endor { uri: String, revision: String }, +} + +impl fmt::Display for SourceOrigin { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Text { name } | Self::Stream { name } => formatter.write_str(name), + Self::File { path } => write!(formatter, "{}", path.display()), + Self::Endor { uri, revision } => write!(formatter, "{uri} [revision {revision}]"), + } + } +} + +/// An owned, immutable UTF-8 snapshot. Reader adapters preserve the text exactly +/// so token byte offsets still refer to the original decoded source. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceUnit { + origin: SourceOrigin, + text: String, +} + +impl SourceUnit { + pub(crate) fn new(origin: SourceOrigin, text: String) -> Self { + Self { origin, text } + } + + pub fn origin(&self) -> &SourceOrigin { + &self.origin + } + + pub fn text(&self) -> &str { + &self.text + } + + /// Bind a diagnostic to this snapshot before discarding its source buffer. + pub fn annotate(&self, diagnostic: Diagnostic) -> SourceDiagnostic { + SourceDiagnostic { + location: diagnostic.location(&self.text), + origin: self.origin.clone(), + diagnostic: Box::new(diagnostic), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceDiagnostic { + pub origin: SourceOrigin, + pub location: SourceLocation, + pub diagnostic: Box, +} + +impl fmt::Display for SourceDiagnostic { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "{}:{}:{}: {}", + self.origin, self.location.line, self.location.column, self.diagnostic.message + ) + } +} + +impl Error for SourceDiagnostic { + fn source(&self) -> Option<&(dyn Error + 'static)> { + Some(self.diagnostic.as_ref()) + } +} diff --git a/crates/frontend/tests/README.md b/crates/frontend/tests/README.md index 95dbe13..33d2045 100644 --- a/crates/frontend/tests/README.md +++ b/crates/frontend/tests/README.md @@ -2,4 +2,4 @@ [syntax.rs](syntax.rs) 覆盖 UTF-8 token 范围、中文/日文名称、数字原文、负号、字符串转义与错误、算术优先级和结合性、成员/调用链、函数与绑定、上下文名称、完整输入、非法/未支持语法以及 CRLF 和 EOF 诊断位置。 -样例也通过此文件解析。只验证语法和 AST,不宣称类型检查或运行求值通过。随工作区检查在 ces-control 执行,本地不运行;入口见 [验证](../../../docs/operations/validation.md)。返回 [frontend](../README.md)。 +[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/reader.rs b/crates/frontend/tests/reader.rs new file mode 100644 index 0000000..b9729ee --- /dev/null +++ b/crates/frontend/tests/reader.rs @@ -0,0 +1,283 @@ +use std::cell::Cell; +use std::error::Error; +use std::io::{self, Cursor, Read}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use frontend::{ + DiagnosticKind, EndorReader, EndorSourceProvider, ResolvedEndorSource, SourceOrigin, + SourceReadErrorKind, lex_source, parse_source, parse_source_expression, +}; + +struct OneByte(R); + +impl Read for OneByte { + fn read(&mut self, buffer: &mut [u8]) -> io::Result { + let count = buffer.len().min(1); + self.0.read(&mut buffer[..count]) + } +} + +#[test] +fn strings_and_split_utf8_streams_produce_the_same_ast_and_exact_text() { + let text = "// 日本語\r\nfunction 表示(): String { return \"こんにちは🌸\" }\r\n"; + let memory = EndorReader::from_text("", text); + let stream = EndorReader::from_stream("", OneByte(Cursor::new(text.as_bytes()))).unwrap(); + assert_eq!(memory.text(), text); + assert_eq!(stream.text(), text); + assert_eq!( + parse_source(&memory).unwrap(), + parse_source(&stream).unwrap() + ); + assert_eq!( + memory.origin(), + &SourceOrigin::Text { + name: "".into() + } + ); + assert_eq!( + stream.origin(), + &SourceOrigin::Stream { + name: "".into() + } + ); + assert_eq!(lex_source(&memory).unwrap(), lex_source(&stream).unwrap()); + let empty = EndorReader::from_stream("", Cursor::new(b"")).unwrap(); + assert!(empty.text().is_empty()); + assert_eq!( + parse_source(&empty).unwrap_err().diagnostic.kind, + DiagnosticKind::Syntax + ); + let bom = EndorReader::from_stream("", Cursor::new("\u{feff}1".as_bytes())).unwrap(); + assert_eq!(bom.text(), "\u{feff}1"); + let error = lex_source(&bom).unwrap_err(); + assert_eq!(error.diagnostic.kind, DiagnosticKind::Lexical); + assert_eq!(error.diagnostic.span.end, 3); +} + +#[test] +fn source_diagnostics_keep_session_names_unicode_locations_and_original_offsets() { + let source = EndorReader::from_text("", "\"日\" + )"); + let error = parse_source_expression(&source).unwrap_err(); + assert_eq!(error.location.line, 1); + assert_eq!(error.location.column, 7); + assert_eq!( + &source.text()[error.diagnostic.span.start..error.diagnostic.span.end], + ")" + ); + assert_eq!(error.origin, source.origin().clone()); + drop(source); + assert!(error.to_string().starts_with(":1:7:")); + assert!(error.source().is_some()); + + let source = EndorReader::from_text("", "\r\n\"日\\q\""); + let error = lex_source(&source).unwrap_err(); + assert_eq!(error.diagnostic.kind, DiagnosticKind::Lexical); + assert_eq!(error.location.line, 2); + assert_eq!(error.location.column, 3); + assert!(error.to_string().contains("unsupported string escape")); +} + +#[test] +fn incomplete_or_invalid_utf8_is_rejected_without_replacement_characters() { + for bytes in [vec![0xff], vec![0xe6, 0x97], vec![b'a', 0xff]] { + let error = EndorReader::from_stream("", OneByte(Cursor::new(bytes))).unwrap_err(); + assert!(matches!(error.kind, SourceReadErrorKind::InvalidUtf8(_))); + assert!(error.to_string().contains(": invalid UTF-8 at byte")); + assert!(error.source().is_some()); + } +} + +struct FailingStream(bool); + +impl Read for FailingStream { + fn read(&mut self, buffer: &mut [u8]) -> io::Result { + if buffer.is_empty() { + return Ok(0); + } + if !self.0 { + self.0 = true; + buffer[0] = b'x'; + return Ok(1); + } + Err(io::Error::other("stream failure")) + } +} + +#[test] +fn a_failed_stream_never_returns_partially_read_source() { + let error = EndorReader::from_stream("", FailingStream(false)).unwrap_err(); + assert!(matches!(error.kind, SourceReadErrorKind::Io(_))); + assert!(error.to_string().contains("stream failure")); +} + +static NEXT_FILE: AtomicUsize = AtomicUsize::new(0); + +struct SourceFile(PathBuf); + +impl SourceFile { + fn new(bytes: &[u8]) -> Self { + let path = std::env::temp_dir().join(format!( + "aster-reader-{}-{}.endor", + std::process::id(), + NEXT_FILE.fetch_add(1, Ordering::Relaxed) + )); + std::fs::write(&path, bytes).unwrap(); + Self(path) + } +} + +impl Drop for SourceFile { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.0); + } +} + +#[test] +fn file_sources_preserve_the_path_and_read_errors_are_not_lexical_errors() { + let file = SourceFile::new(b"function f(): Number { return 1 }\r\n"); + let source = EndorReader::from_file(&file.0).unwrap(); + assert_eq!( + source.origin(), + &SourceOrigin::File { + path: file.0.clone() + } + ); + assert!(source.text().ends_with("\r\n")); + assert!(parse_source(&source).is_ok()); + let invalid = SourceFile::new(&[0xff]); + let error = EndorReader::from_file(&invalid.0).unwrap_err(); + assert!(matches!(error.kind, SourceReadErrorKind::InvalidUtf8(_))); + assert!(error.to_string().contains(&invalid.0.display().to_string())); + let missing = file.0.with_extension("missing"); + let error = EndorReader::from_file(&missing).unwrap_err(); + let SourceReadErrorKind::Io(error) = error.kind else { + panic!("expected I/O error"); + }; + assert_eq!(error.kind(), io::ErrorKind::NotFound); +} + +struct Provider { + calls: Cell, +} + +impl EndorSourceProvider for Provider { + fn load(&self, uri: &str) -> io::Result { + assert_eq!(uri, "endor:workspace/namespace/function"); + let version = self.calls.get() + 1; + self.calls.set(version); + Ok(ResolvedEndorSource { + revision: format!("revision-{version}"), + bytes: format!("function f(): Number {{ return {version} }}").into_bytes(), + }) + } +} + +#[test] +fn each_uri_read_resolves_once_and_old_snapshots_keep_their_original_revision() { + let provider = Provider { + calls: Cell::new(0), + }; + let uri = "endor:workspace/namespace/function"; + let first = EndorReader::from_endor(uri, &provider).unwrap(); + let second = EndorReader::from_endor(uri, &provider).unwrap(); + assert_eq!(provider.calls.get(), 2); + assert!(first.text().contains("return 1")); + assert!(second.text().contains("return 2")); + assert_eq!( + first.origin(), + &SourceOrigin::Endor { + uri: uri.into(), + revision: "revision-1".into() + } + ); + assert_eq!( + second.origin(), + &SourceOrigin::Endor { + uri: uri.into(), + revision: "revision-2".into() + } + ); + assert!(parse_source(&first).is_ok()); + assert!(parse_source(&second).is_ok()); + assert_eq!(provider.calls.get(), 2); // Parsing never resolves the URI again. +} + +struct StaticProvider { + revision: &'static str, + bytes: &'static [u8], +} + +impl EndorSourceProvider for StaticProvider { + fn load(&self, _uri: &str) -> io::Result { + Ok(ResolvedEndorSource { + revision: self.revision.into(), + bytes: self.bytes.into(), + }) + } +} + +#[test] +fn uri_snapshots_require_a_revision_and_preserve_it_in_source_diagnostics() { + let uri = "endor:workspace/namespace/function"; + let provider = StaticProvider { + revision: "rev-one", + bytes: b"function f(): Number { return ) }", + }; + let source = EndorReader::from_endor(uri, &provider).unwrap(); + let error = parse_source(&source).unwrap_err(); + assert_eq!(error.origin, source.origin().clone()); + assert!(error.to_string().contains("[revision rev-one]")); + let missing = StaticProvider { + revision: " \t", + bytes: b"1", + }; + assert!(matches!( + EndorReader::from_endor(uri, &missing).unwrap_err().kind, + SourceReadErrorKind::MissingRevision + )); + let invalid = StaticProvider { + revision: "rev-two", + bytes: &[0xff], + }; + let error = EndorReader::from_endor(uri, &invalid).unwrap_err(); + assert!(matches!(error.kind, SourceReadErrorKind::InvalidUtf8(_))); + assert!(error.to_string().contains("[revision rev-two]")); +} + +struct DeniedProvider; + +impl EndorSourceProvider for DeniedProvider { + fn load(&self, _uri: &str) -> io::Result { + Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "host denied source access", + )) + } +} + +#[test] +fn invalid_uris_do_not_call_the_provider_and_denials_do_not_fall_back_to_files() { + let provider = Provider { + calls: Cell::new(0), + }; + for uri in [ + "", + "endor:", + "file:source", + "https://example.com", + "endor:space here", + "endor:bad\nname", + ] { + assert!(matches!( + EndorReader::from_endor(uri, &provider).unwrap_err().kind, + SourceReadErrorKind::InvalidEndorUri + )); + } + assert_eq!(provider.calls.get(), 0); + let error = EndorReader::from_endor("endor:anywhere", &DeniedProvider).unwrap_err(); + let SourceReadErrorKind::Io(error) = error.kind else { + panic!("expected provider error"); + }; + assert_eq!(error.kind(), io::ErrorKind::PermissionDenied); +} diff --git a/docs/README.md b/docs/README.md index d48a2c7..b93d5ad 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,6 +9,7 @@ | 各个 crate 如何分层,哪些能力已实现 | [架构](architecture/README.md) | [crates](../crates/README.md) | | EndorObject、对象头、句柄、默认操作 | [对象模型](runtime/objects.md) | [core/src](../crates/core/src/README.md) | | Number、String、Dict、Cell、Function、异常数据 | [基本对象](runtime/builtins.md) | [core/src](../crates/core/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) | | 操作码、代码验证、局部槽、函数定义与调用协议 | [字节码 VM](runtime/bytecode.md) | instruction.rs / code.rs / frame.rs / eval.rs;见 [文件表](../crates/core/src/README.md) | | 基础信息保存在哪里、旧库如何迁移 | [宿主文件信息](operations/host-information.md) | [host](../crates/host/README.md) | diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 66566de..d5e132a 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -11,12 +11,12 @@ Server ─→ Core + Host 未来数据库特殊类的宿主实现 ─→ Storage ``` -CLI、Server 依赖 Host 保存文件信息,不依赖 Storage;Core 与 Storage 也互不依赖。CLI 的 parse 命令使用 Frontend;Frontend 只依赖 peg,Core 不依赖 Frontend。HTTP、Tokio、SQLx、数据库、网络和浏览器能力不进入零依赖的 core。包与文件定位见 [crates](../../crates/README.md)。 +CLI、Server 依赖 Host 保存文件信息,不依赖 Storage;Core 与 Storage 也互不依赖。CLI 的 parse 命令通过 Frontend 的 EndorReader 读取文件/stdin,再解析 AST;Frontend 的外部依赖只有 peg,读取适配使用标准库。平台 URI 的存储与授权由宿主 provider 提供,Core 不依赖 Frontend。HTTP、Tokio、SQLx、数据库、网络和浏览器能力不进入零依赖的 core。包与文件定位见 [crates](../../crates/README.md)。 | 组件 | 当前职责 | 全局契约 | | --- | --- | --- | | Core | EndorObject、对象堆、Number、wordcode、执行帧、求值器 | [运行时](../runtime/README.md) | -| Frontend | 手写 UTF-8 lexer、token PEG parser、基础函数/表达式 AST,不执行源码 | [源码前端](../runtime/frontend.md) | +| Frontend | Endor 阅读器、手写 UTF-8 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 b495149..6f9d3c5 100644 --- a/docs/contributing/README.md +++ b/docs/contributing/README.md @@ -14,6 +14,7 @@ | 各 crate 的依赖方向和系统边界 | [architecture/README](../architecture/README.md) | 根 README、docs 索引摘要 | | 对象头、句柄、引用协议与 GC 算法 | [objects.md](../runtime/objects.md) | core/src 文件表 | | Number、String、Dict、Cell、函数环境和异常数据 | [builtins.md](../runtime/builtins.md) | core/src 与 tests 文件表 | +| lexer 前的输入适配、SourceUnit 与来源诊断 | [reader.md](../runtime/reader.md) | frontend 的 reader/source、CLI 与读取检查 | | token、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 | diff --git a/docs/contributing/handoff.md b/docs/contributing/handoff.md index e38f34a..90ba33a 100644 --- a/docs/contributing/handoff.md +++ b/docs/contributing/handoff.md @@ -11,7 +11,8 @@ - EndorObject、稳定 Box、代际句柄、显式强引用及手动循环回收;VM 从空堆启动,没有 Python type/元类或语言根类。 - Function、NativeFunction、Dict、Cell、异常数据、UTF-8 String 和无损表示优先的 Number。 - 小型 wordcode、共享代码对象、操作数/控制流/栈校验、显式执行帧及 execute/call;MakeFunction 可在执行中创建带默认参数与共享闭包 Cell 的函数。对象返回值持有根,用后 release_value;借用读取不自动 retain。 -- Frontend 使用手写 lexer 与 peg 0.8.6,提供完整输入解析、基础函数/表达式 AST、Unicode 字节范围与行列诊断;CLI parse 可读 UTF-8 文件输出 AST,不执行源码。当前语法以未完成设计稿为起点,可以继续调整,见 [源码前端](../runtime/frontend.md)。 +- 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)。 - 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/README.md b/docs/operations/README.md index 4e9b2fe..fa686a9 100644 --- a/docs/operations/README.md +++ b/docs/operations/README.md @@ -11,11 +11,12 @@ cargo build --workspace --locked cargo run -p cli -- bootstrap cargo run -p cli -- help cargo run -p cli -- parse crates/frontend/examples/basic.endor +cargo run -p cli -- parse - < crates/frontend/examples/basic.endor ``` bootstrap 创建两个 Record,输出 runtime=EndorObject、type_system=disabled、objects_distinct=true 和 heap_objects=2,显式释放后输出 released heap_objects=0。默认无参数执行同一示例。 -parse 严格读取 UTF-8 源码文件,成功时输出 Rust Debug 格式的 AST;失败时输出文件、行、列并返回非零退出码。该命令只解析语法,不执行源码;语法与当前边界见 [源码前端](../runtime/frontend.md)。 +parse 通过 EndorReader 严格读取 UTF-8 源码文件;`parse -` 从 stdin 读到 EOF,输入来源为 ``,没有 REPL 提示符或会话求值。成功时输出 Rust Debug 格式的 AST;失败时输出来源、行、列并返回非零退出码;阅读错误在进入 lexer 前返回。输入适配与 URI provider 见 [阅读器](../runtime/reader.md)。该命令只解析语法,不执行源码;语法与当前边界见 [源码前端](../runtime/frontend.md)。 CLI 不读取数据库环境,不创建基础信息数据库表。metadata put/get 通过 host 包写 UTF-8 JSON 文件,metadata import 仅导入缺失记录;示例、配置和协议见 [宿主信息](host-information.md)。输入与退出码见 [cli/src](../../crates/cli/src/README.md)。 diff --git a/docs/operations/configuration.md b/docs/operations/configuration.md index 67111b5..c51805d 100644 --- a/docs/operations/configuration.md +++ b/docs/operations/configuration.md @@ -27,7 +27,7 @@ LC_CTYPE 处理字符分类、大小写和多字节字符相关约定;小数 - Value::Text 与 EndorStringObject 使用 Rust String,内容始终是有效 UTF-8。字符访问按 Unicode 标量值,不按字节或字形簇;不支持孤立代理码点。[Rust String](https://doc.rust-lang.org/std/string/struct.String.html#utf-8)。 - Number 的整数与十进制构造器明确检查 ASCII 数字和语法;十进制点为句点,不根据 LANG/LC_NUMERIC 切换为逗号。当前数值比较、哈希和算术尚未实现。 - 内核没有 setlocale 调用;不会为了启动 VM 修改 LANG、LC_ALL 或 LC_CTYPE。外部库、终端、字体或下游工具自身的行为不在这项保证之内。 -- CLI parse 严格读取 UTF-8 文件,Frontend 提供基础标识符与语法解析,见 [源码前端](../runtime/frontend.md)。尚无通用字节串对象、语言文件系统 API 或非 UTF-8 源码自动转码入口。 +- CLI parse 通过 EndorReader 严格读取 UTF-8 文件/stdin,阅读器也接受字符串和宿主 URI provider;来源与解码失败契约见 [阅读器](../runtime/reader.md)。Frontend 提供基础标识符与语法解析,见 [源码前端](../runtime/frontend.md)。尚无通用字节串对象、语言文件系统 API 或非 UTF-8 源码自动转码入口。 日文与外部 Shift_JIS/CP932 的处理见 [文本编码](../runtime/text-encoding.md);内部 UTF-8 不因地区切换。 diff --git a/docs/operations/validation.md b/docs/operations/validation.md index c39969a..e6557ab 100644 --- a/docs/operations/validation.md +++ b/docs/operations/validation.md @@ -28,8 +28,9 @@ ci-ces-control.sh 负责准备全新专用临时 PostgreSQL 和测试 URL;成 | 对象运行时 | 地址稳定、引用/借用、属性、长链、循环、原生操作、VM 销毁 | [core/tests](../../crates/core/tests/README.md) 的 runtime.rs | | 基本对象 | 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) | -| CLI 解析 | AST 输出、未执行源码、文件/行/列错误与非法 UTF-8 退出 | [cli/tests](../../crates/cli/tests/README.md) | +| CLI 解析 | AST 输出、未执行源码、文件/stdin 来源和行列错误、EOF 读取与非法 UTF-8 退出 | [cli/tests](../../crates/cli/tests/README.md) | | 公开示例 | 对象操作、手写指令执行与释放 | [core/src](../../crates/core/src/README.md) 的 lib.rs rustdoc | | 宿主文件信息 | UTF-8 值/路径、持久读取、同键替换、并发、坏记录、幂等迁移与保留新值 | [host/tests](../../crates/host/tests/README.md) | | 数据库连接 | 不自动建表、显式配置、隔离/共享、调用方数据重连、失败不降级、真实 PostgreSQL | [storage/tests](../../crates/storage/tests/README.md) | diff --git a/docs/runtime/README.md b/docs/runtime/README.md index 5621cbc..3a8d946 100644 --- a/docs/runtime/README.md +++ b/docs/runtime/README.md @@ -6,6 +6,7 @@ | --- | --- | | [对象模型](objects.md) | EndorObject/ObjectHeader、代际句柄、操作协议、托管引用与循环回收 | | [基本对象与 Number](builtins.md) | 函数、原生函数、字典、Cell、异常数据、字符串与无损数字 | +| [Endor 阅读器](reader.md) | SourceUnit、来源/修订标签、字符串/流/文件和宿主 URI provider | | [源码前端](frontend.md) | 手写 lexer、token PEG parser、AST、基础函数/表达式语法与诊断 | | [字节码 VM](bytecode.md) | 指令编码、代码校验、局部槽、执行帧、函数创建、调用/返回与边界 | | [文本编码](text-encoding.md) | UTF-8、日文、外部 Shift_JIS/CP932 与宿主解码边界 | diff --git a/docs/runtime/frontend.md b/docs/runtime/frontend.md index b494ef5..7313c0a 100644 --- a/docs/runtime/frontend.md +++ b/docs/runtime/frontend.md @@ -1,6 +1,6 @@ # 源码词法与 PEG 语法前端 -frontend 包提供手写 Rust lexer 和 rust-peg 生成的 token parser。当前入口为 lex、parse、parse_expression,CLI 提供 parse 文件命令;结果是 AST,还没有 AST 到字节码的编译器、类型检查、名称解析或源码执行。Core 不依赖 frontend 或 peg,仍保持零外部依赖。 +frontend 包提供 [Endor 阅读器](reader.md)、手写 Rust lexer 和 rust-peg 生成的 token parser。宿主通过 SourceUnit 调用带来源的词法/语法入口,原有 lex、parse、parse_expression 保留;CLI 提供 parse 文件/stdin 命令;结果是 AST,还没有 AST 到字节码的编译器、类型检查、名称解析或源码执行。Core 不依赖 frontend 或 peg,仍保持零外部依赖。 首版以 Endorphin-Language 设计稿的基础词法、表达式与函数章节为起点,语法可以继续修改;设计稿中的未完成部分不作为阻塞前端实现的前置条件,也不被整体视作已经实现的语言能力。这里维护当前解析契约;源码位置见 [frontend](../../crates/frontend/README.md)。 @@ -14,6 +14,9 @@ grammar.peg 保留 rust-peg 0.8 的 peg::parser! 外层声明,因此是直接 | API | 输入与结果 | | --- | --- | +| lex_source(&SourceUnit) | 对阅读器输出分词,错误携带来源、修订及行列 | +| parse_source(&SourceUnit) | 对阅读器输出解析基础函数 Program,错误携带来源信息 | +| parse_source_expression(&SourceUnit) | 对阅读器输出解析一个完整表达式,错误携带来源信息 | | lex(&str) | 产生 Vec;每个 token 只有种类和原始源码的字节范围,不复制文本 | | parse(&str) | 解析一个或多个完整函数定义,返回 Program;空输入和未消费的尾部内容报错 | | parse_expression(&str) | 解析一个完整表达式,不能忽略尾部 token | @@ -24,7 +27,7 @@ AST 的名称、数字原文和解码后的字符串均由 AST 自己持有, ## 词法规则 -- 输入是有效 UTF-8 的 &str。CLI 使用严格的 read_to_string 读文件;其他编码由宿主先显式解码,不修改全局 locale。 +- 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. 形式不在当前表达式语法中。 @@ -59,6 +62,6 @@ use、namespace、class、agent、完整类型语法、条件/循环、比较 词法和语法错误分别标记为 Lexical/Syntax。语法错误将 PEG 的 token 下标转换成原始源码 Span,保留期望项目列表;EOF 错误使用 source.len() 处的空区间。Diagnostic::location 返回一基行列,列按 Unicode 标量计数,CRLF 只算一次换行,不按字节列或字形簇计数。 -`cli parse ` 成功时输出 Rust Debug 格式的 AST,失败时在 stderr 输出文件/行/列和诊断并返回非零退出码。AST Debug 文本不是稳定的序列化协议。命令不执行源码、不初始化 VM、不连接数据库;具体运行与本地禁测约定见 [运行](../operations/README.md) 和 [验证](../operations/validation.md)。 +`cli parse ` 与 `cli parse -` 成功时输出 Rust Debug 格式的 AST,失败时在 stderr 输出来源/行/列和诊断并返回非零退出码;`-` 表示从 stdin 读到 EOF,不是 REPL。阅读与来源包装规则见 [阅读器](reader.md)。AST Debug 文本不是稳定的序列化协议。命令不执行源码、不初始化 VM、不连接数据库;具体运行与本地禁测约定见 [运行](../operations/README.md) 和 [验证](../operations/validation.md)。 后续编译器应将 AST 经名称/闭包分析转换为现有字节码,不另设 AST 求值器;解析成功不表示表达式已有运行语义。VM 虽已支持 MakeFunction,但从源码创建函数仍需要编译器接入。解析器也不是不可信代码沙箱,当前没有通用输入大小或解析递归深度限额。 diff --git a/docs/runtime/reader.md b/docs/runtime/reader.md new file mode 100644 index 0000000..675cf2d --- /dev/null +++ b/docs/runtime/reader.md @@ -0,0 +1,49 @@ +# Endor 阅读器与源码来源 + +EndorReader 位于 lexer 之前,把宿主输入转换为 SourceUnit,再交给 lex_source、parse_source 或 parse_source_expression。它负责读取、严格 UTF-8 解码和来源记录,不解析 token、不解析 namespace 依赖、不执行代码。源码在 [reader.rs](../../crates/frontend/src/reader.rs),来源与诊断结构在 [source.rs](../../crates/frontend/src/source.rs),词法和语法见 [前端](frontend.md)。 + +```text +字符串/字节流/文件/宿主 URI provider +→ EndorReader → SourceUnit → lexer → PEG parser → AST +``` + +## 输入适配 + +| 入口 | 行为 | +| --- | --- | +| EndorReader::from_text(name, text) | 接收已经解码的 Unicode 字符串,复制或接管其 String 所有权,记录文本来源名称 | +| EndorReader::from_stream(name, reader) | 接收实现 std::io::Read 的流,读到 EOF 后统一严格解码,记录流名称 | +| EndorReader::from_file(path) | 按原生 Path 打开文件,读到 EOF 并严格解码,保留 PathBuf 来源 | +| EndorReader::from_endor(uri, provider) | 调用宿主 EndorSourceProvider 取得一次解析结果,严格解码并保存请求 URI 与已选修订标签 | + +Reader 不修改 locale,不猜测 Shift_JIS/CP932 或其他编码,不以替换字符修复非法 UTF-8。其他编码由宿主先明确解码后通过字符串或 UTF-8 输入交付。流可以在任意字节处分块,包括拆开一个 Unicode 字符;所有字节收齐后才进行 UTF-8 校验。 + +文本原样保留:不转换 CRLF/CR,不移除 BOM,也不规范化 Unicode。Span 继续指向这份原始 UTF-8 文本;Reader 不提前处理注释、转义或缩进。BOM 等字符是否构成合法语法由 lexer 决定,当前 lexer 不接受开头 BOM。 + +SourceUnit 拥有源码和 SourceOrigin,只公开只读访问,不依赖仍打开的文件、流或平台连接。一次读取完成后,重新读取同一 URI 或修改源文件不会改写已有单元的内容;这保证输入缓冲区固定,不表示对可变文件读取过程实现了文件系统事务快照。 + +## 来源与诊断 + +SourceOrigin 区分 Text、Stream、File 和 Endor。文本/流名称由宿主指定,例如 ``、`` 或会话输入编号;文件保留操作系统路径;Endor 来源额外记录 provider 给出的修订标签。 + +lex_source、parse_source 和 parse_source_expression 将原有 Diagnostic 包装为 SourceDiagnostic,保存来源、行列和完整诊断(包括 Span 与 expected 列表)。行列根据读取到的原文计算,使用一基 Unicode 标量列,CRLF 算一次换行。包装后的错误可以在源码缓冲区释放后继续报告;错误不会保存整份源码文本。 + +Read 阶段的 SourceReadError 与词法/语法 Diagnostic 分开:Io 保留原始 I/O/provider 错误,InvalidUtf8 报告第一个无效字节位置,InvalidEndorUri 表示入口不属于可接受的 Endor URI,MissingRevision 表示宿主没有提供已选修订。读取中途失败不返回部分源码,不继续解析部分内容;URI 失败不会改从本地文件读取。I/O 和 UTF-8 的底层错误可通过 Error::source 取得。 + +原有 lex(&str)、parse(&str)、parse_expression(&str) 保留为不带来源的低层便捷接口。需要文件名、URI、修订或会话标签时,宿主应使用 SourceUnit 入口;这些标签不是 VM 的 ObjectRef,也不充当持久资源身份。 + +## 平台 URI 边界 + +EndorSourceProvider::load(&str) 返回 io::Result。ResolvedEndorSource 包含 bytes 和非空 revision;宿主负责名称解析、权限检查、版本选择与实际存储访问,失败直接返回相应错误。例如权限不足可使用 PermissionDenied,不存在可使用 NotFound。 + +Reader 只校验 scheme 为不区分大小写的 endor、冒号后非空,且 URI 不含原始空白/控制字符。它不固定 workspace/namespace 的 URI 路径形式,不解码百分号、不实现相对 URI,也不规定修订选择器或 hash 算法。scheme 以外的协议校验由宿主执行,原 URI 不重写。每次读取只调用一次 provider,之后词法和语法处理不再解析 URI。 + +revision 是宿主返回的原样标签;Reader 只拒绝全空白标签,不验证标签是否为内容 hash、不验证内容与 hash 的对应关系,也不实现资源历史、依赖锁、发布快照或签名。当前没有内置平台 provider、网络客户端、数据库存储或 CLI URI 执行入口。具体版本模型仍需另行设计。 + +## CLI 与交互输入 + +CLI parse 文件通过 from_file → parse_source 输出 AST;`cli parse -` 通过 from_stream 读取 stdin 到 EOF,来源为 ``。错误保留文件或流名及行列;文件读取与非法 UTF-8 错误在进入 lexer 前返回。命令仍不执行源码。 + +stdin 入口不是 REPL,不打印提示符、不保持求值环境,也不区分“需要继续输入”和“真正语法错误”。未来 REPL 宿主先收集一次提交的输入单元,再使用 from_text 并指定会话来源;reader 不承担交互调度。包与 namespace 的成员查找和依赖解析也不由 reader 自动完成。 + +运行方式见 [运行指南](../operations/README.md),编码边界见 [文本编码](text-encoding.md),检查范围见 [验证](../operations/validation.md)。本地不编译、不执行测试;流、文件与 provider 检查由指定远端环境执行。 diff --git a/docs/runtime/text-encoding.md b/docs/runtime/text-encoding.md index f05d139..f98e099 100644 --- a/docs/runtime/text-encoding.md +++ b/docs/runtime/text-encoding.md @@ -20,7 +20,7 @@ Value::Text 与 EndorStringObject 使用 Rust String,始终是有效 UTF-8。 String::from_utf8 验证字节;它不会自动识别 Shift_JIS,也不会自动转换编码。某些错误编码字节可能恰好仍是合法 UTF-8,因此“能通过 UTF-8 校验”也不能代替输入协议。[from_utf8 文档](https://doc.rust-lang.org/std/string/struct.String.html#method.from_utf8)。 -CLI parse 已有严格 UTF-8 文件读取,frontend 接收 &str 进行词法和语法解析。当前仍无通用文件/网络文本加载器或 Shift_JIS 解码器;以上非 UTF-8 转换由宿主显式提供,不是已有内置转码能力。无需修改全局 locale 来处理单个文件。 +EndorReader 提供严格 UTF-8 文件/流读取和宿主 URI provider 适配,输出带来源的 SourceUnit;frontend 将其文本交给 lexer/parser。CLI parse 文件和 stdin 都经过阅读器,见 [阅读器](reader.md)。当前仍无通用文件/网络文本加载器或 Shift_JIS 解码器;以上非 UTF-8 转换由宿主显式提供,不是已有内置转码能力。无需修改全局 locale 来处理单个文件。 ## 其他日文文本差异