Add token-based PEG frontend and CLI source parsing
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 7m30s
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 7m30s
This commit is contained in:
Generated
+35
@@ -131,6 +131,7 @@ name = "cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"core",
|
||||
"frontend",
|
||||
"host",
|
||||
]
|
||||
|
||||
@@ -299,6 +300,13 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frontend"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"peg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.34"
|
||||
@@ -814,6 +822,33 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "peg"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0aad070be5b63aa72103f2fcdd70a83adbd5e90112ce5b574171ff1c65501773"
|
||||
dependencies = [
|
||||
"peg-macros",
|
||||
"peg-runtime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "peg-macros"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd8ef6825cae95355031ae26a99b616a2a21f22ba2de0197c43dfb05acbe7ee"
|
||||
dependencies = [
|
||||
"peg-runtime",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "peg-runtime"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7011d97b484a5ebdc4b1fdb3b12d5e4bbbea56e9d22b688f2e79e04b65a7d8a6"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
members = ["crates/core", "crates/storage", "crates/cli", "crates/server", "crates/host"]
|
||||
members = ["crates/core", "crates/frontend", "crates/storage", "crates/cli", "crates/server", "crates/host"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
@@ -12,6 +12,8 @@ license = "MIT"
|
||||
core = { path = "crates/core" }
|
||||
storage = { path = "crates/storage" }
|
||||
host = { path = "crates/host" }
|
||||
frontend = { path = "crates/frontend" }
|
||||
peg = "=0.8.6"
|
||||
axum = { version = "0.8", default-features = false, features = ["http1", "json", "tokio"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Rust 编写的 Endor 虚拟机实现。当前具备 EndorObject、稳定对象分配、代际句柄、引用计数、显式循环回收,以及可执行语言函数的小型 wordcode VM。数字统一为无损表示优先的 Number。
|
||||
|
||||
VM 从空堆启动;尚无源码 lexer/parser、Python type/元类体系、数值算术或完整异常传播。数据库连接仅为需要数据库的特殊类保留;普通宿主不连接数据库;基础信息改用 host 包的 UTF-8 JSON 文件,不保存 VM 堆。当前边界见 [系统架构](docs/architecture/README.md)。
|
||||
独立 frontend 已提供手写 lexer、peg parser、基础函数/表达式 AST 与 cli parse 文件入口。VM 从空堆启动;尚无 AST 到字节码编译器、Python type/元类体系、数值算术或完整异常传播。数据库连接仅为需要数据库的特殊类保留;普通宿主不连接数据库;基础信息改用 host 包的 UTF-8 JSON 文件,不保存 VM 堆。当前边界见 [系统架构](docs/architecture/README.md)。
|
||||
|
||||
## 从哪里读
|
||||
|
||||
@@ -10,6 +10,7 @@ VM 从空堆启动;尚无源码 lexer/parser、Python type/元类体系、
|
||||
| --- | --- |
|
||||
| 查一个功能、设计或代码位置 | [文档总索引](docs/README.md) |
|
||||
| 看源码目录与 crate 边界 | [crates](crates/README.md) |
|
||||
| 看源码词法、语法、AST 和解析命令 | [源码前端](docs/runtime/frontend.md) |
|
||||
| 看指令、代码对象、执行帧和执行循环 | [字节码 VM](docs/runtime/bytecode.md) |
|
||||
| 看 VM/帧/对象如何存活和销毁 | [生命周期](docs/runtime/lifecycle.md) |
|
||||
| 看基础信息文件和旧库迁移 | [宿主信息](docs/operations/host-information.md) |
|
||||
@@ -22,8 +23,8 @@ VM 从空堆启动;尚无源码 lexer/parser、Python type/元类体系、
|
||||
|
||||
| 文件或目录 | 职责 |
|
||||
| --- | --- |
|
||||
| Cargo.toml / Cargo.lock | 工作区、包依赖、Rust 版本要求及依赖锁定;包名与目录一致:core、storage、host、cli、server |
|
||||
| crates/ | core、storage、host、cli、server 五个包,每个目录的 README 解释本目录 |
|
||||
| Cargo.toml / Cargo.lock | 工作区、包依赖、Rust 版本要求及依赖锁定;包名与目录一致:core、frontend、storage、host、cli、server |
|
||||
| crates/ | core、frontend、storage、host、cli、server 六个包,每个目录的 README 解释本目录 |
|
||||
| docs/ | 跨目录的架构、运行时契约、配置、运行、部署与维护说明 |
|
||||
| scripts/ | 指定远端环境中的检查、冒烟、CI 与部署脚本 |
|
||||
| .gitea/ | Gitea Actions 工作流 |
|
||||
|
||||
+3
-2
@@ -5,11 +5,12 @@
|
||||
| 目录 | 包名/产物 | 职责 |
|
||||
| --- | --- | --- |
|
||||
| [core](core/README.md) | core,库 endor_core | 零外部依赖的对象运行时、堆与字节码 VM |
|
||||
| [frontend](frontend/README.md) | frontend | 手写 lexer、token PEG parser、基础函数/表达式 AST,不执行源码 |
|
||||
| [host](host/README.md) | host | UTF-8 JSON 文件基础信息,与数据库分离 |
|
||||
| [storage](storage/README.md) | storage | 为数据库特殊类保留的显式连接适配,不提供基础信息存储 |
|
||||
| [cli](cli/README.md) | cli,二进制 cli | 引导演示与文件信息命令,不使用数据库 |
|
||||
| [cli](cli/README.md) | cli,二进制 cli | 引导演示、源码解析与文件信息命令,不使用数据库 |
|
||||
| [server](server/README.md) | server,二进制 server | HTTP 宿主与文件版本信息,不使用数据库 |
|
||||
|
||||
CLI 和 Server 依赖 Core 与 Host,不依赖 Storage;Core 与 Storage 也互不依赖。未来特殊类的宿主实现按需依赖 Storage,见 [数据库对象](../docs/runtime/database-objects.md)。
|
||||
CLI 依赖 Core、Frontend 与 Host,Server 依赖 Core 与 Host,二者不依赖 Storage;Frontend 依赖 peg,不依赖 Core;Core 与 Storage 也互不依赖。未来特殊类的宿主实现按需依赖 Storage,见 [数据库对象](../docs/runtime/database-objects.md)。
|
||||
|
||||
core 的 Rust 库名为 endor_core,避免与标准库 core 重名,见 [Cargo 目标说明](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-name-field)。网络、数据库、进程配置和信号不进入 Core。全局说明见 [架构](../docs/architecture/README.md),返回 [项目入口](../README.md)。
|
||||
|
||||
@@ -11,6 +11,7 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
core.workspace = true
|
||||
frontend.workspace = true
|
||||
host.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# cli
|
||||
|
||||
同步命令行宿主,提供 bootstrap、help 和文件 metadata 命令。bootstrap 演示两个运行时对象并释放,不读取数据库配置、不连接或写入数据库。
|
||||
同步命令行宿主,提供 bootstrap、parse、help 和文件 metadata 命令。bootstrap 演示两个运行时对象并释放,不读取数据库配置、不连接或写入数据库。
|
||||
|
||||
| 内容 | 职责 |
|
||||
| --- | --- |
|
||||
| Cargo.toml | cli 二进制与 core/host 依赖;无 storage 或 Tokio 依赖 |
|
||||
| [src](src/README.md) | 参数路由、引导演示和退出码 |
|
||||
| Cargo.toml | cli 二进制与 core/frontend/host 依赖;无 storage 或 Tokio 依赖 |
|
||||
| [src](src/README.md) | 参数路由、引导演示、源码解析和退出码 |
|
||||
| [tests](tests/README.md) | parse 命令的 AST 输出、错误位置和严格 UTF-8 检查 |
|
||||
|
||||
运行方式见 [运行指南](../../docs/operations/README.md),输入边界见 [配置](../../docs/operations/configuration.md)。当前不是 Endor 源码解释命令,metadata put/get 和只导入缺失项的 import 均使用文件,见 [宿主信息](../../docs/operations/host-information.md)。返回 [工作区](../README.md)。
|
||||
运行方式见 [运行指南](../../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)。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# CLI 入口
|
||||
|
||||
[main.rs](main.rs) 是唯一入口:同步 main 将 run 的结果转成进程退出码;run 处理 bootstrap、help、文件 metadata put/get/import,错误打印到 stderr 并返回失败。
|
||||
[main.rs](main.rs) 是唯一入口:同步 main 将 run 的结果转成进程退出码;run 处理 bootstrap、parse 文件、help、文件 metadata put/get/import,错误打印到 stderr 并返回失败。
|
||||
|
||||
bootstrap 创建并显式释放两个对象,不读取数据库环境、不打开数据库。未知命令报错;metadata 通过 host::MetadataStore 读写文件,import 从标准输入接收旧记录且不覆盖已有文件。参数目前由 std::env::args 读取,非 Unicode 参数可能 panic,边界见 [配置](../../../docs/operations/configuration.md)。
|
||||
bootstrap 创建并显式释放两个对象,不读取数据库环境、不打开数据库。parse 使用 frontend 输出 AST,词法/语法错误显示文件、行、列;不创建 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)。
|
||||
|
||||
+13
-1
@@ -4,7 +4,7 @@ use std::process::ExitCode;
|
||||
use endor_core::{Value, Vm};
|
||||
use host::MetadataStore;
|
||||
|
||||
const USAGE: &str = "Usage: cli [bootstrap | metadata put <key> <value> | metadata get <key> | metadata import | help]";
|
||||
const USAGE: &str = "Usage: cli [bootstrap | parse <file> | metadata put <key> <value> | metadata get <key> | metadata import | help]";
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
@@ -45,6 +45,18 @@ fn run() -> Result<(), Box<dyn Error>> {
|
||||
println!("bootstrap uses no database");
|
||||
}
|
||||
["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
|
||||
)
|
||||
})?;
|
||||
println!("{program:#?}");
|
||||
}
|
||||
["metadata", "put", key, value] => {
|
||||
MetadataStore::from_env()?.put(key, value)?;
|
||||
println!("stored (file)");
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# CLI 命令检查
|
||||
|
||||
[parse.rs](parse.rs) 使用 Cargo 提供的 cli 测试二进制检查 parse 命令:合法 UTF-8 源码输出 AST 且不执行名称调用;非法语法输出文件/行/列并返回失败;非法 UTF-8 不替换字符。
|
||||
|
||||
测试仅在 ces-control 的工作区检查中运行,本地不执行。测试临时文件使用独立名称并在结束时清理。返回 [cli](../README.md),验证约定见 [验证](../../../docs/operations/validation.md)。
|
||||
@@ -0,0 +1,67 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
static NEXT_FILE: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
struct SourceFile(PathBuf);
|
||||
|
||||
impl SourceFile {
|
||||
fn new(content: &[u8]) -> Self {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"aster-parse-{}-{}.endor",
|
||||
std::process::id(),
|
||||
NEXT_FILE.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
std::fs::write(&path, content).unwrap();
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SourceFile {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_file(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_command_prints_ast_without_executing_source() {
|
||||
let source = SourceFile::new("function 表示(): String { return 未定義() }".as_bytes());
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_cli"))
|
||||
.arg("parse")
|
||||
.arg(&source.0)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||
assert!(stdout.contains("表示"));
|
||||
assert!(stdout.contains("未定義"));
|
||||
assert!(stdout.contains("Call"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_command_reports_source_locations_and_rejects_invalid_utf8() {
|
||||
let source = SourceFile::new(b"function f(): Number {\n return )\n}");
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_cli"))
|
||||
.arg("parse")
|
||||
.arg(&source.0)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!output.status.success());
|
||||
assert!(output.stdout.is_empty());
|
||||
let stderr = String::from_utf8(output.stderr).unwrap();
|
||||
assert!(stderr.contains(":2:10:"), "{stderr}");
|
||||
assert!(stderr.contains("expected"));
|
||||
let source = SourceFile::new(&[0xff]);
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_cli"))
|
||||
.arg("parse")
|
||||
.arg(&source.0)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!output.status.success());
|
||||
assert!(output.stdout.is_empty());
|
||||
}
|
||||
@@ -8,6 +8,6 @@ Endor VM 内核,保持零外部依赖。负责 EndorObject、堆、代际句
|
||||
| [src](src/README.md) | 文件职责、公开接口和常见修改位置 |
|
||||
| [tests](tests/README.md) | 对象、基本载荷、执行语义与生命周期检查 |
|
||||
|
||||
内核通过 Vm::new 从空堆启动;代码对象和全局字典由宿主显式创建,再 execute/call。没有环境变量探测、数据库、HTTP、Python type 系统或源码前端。
|
||||
内核通过 Vm::new 从空堆启动;代码对象和全局字典由宿主显式创建,再 execute/call。内核没有环境变量探测、数据库、HTTP、Python type 系统或源码解析;源码 lexer/parser 在独立 [frontend](../frontend/README.md) 包中,尚未接入字节码编译。
|
||||
|
||||
对象结果所有权见 [生命周期](../../docs/runtime/lifecycle.md),对象/字节码设计见 [运行时](../../docs/runtime/README.md),限额与 UTF-8 边界见 [配置](../../docs/operations/configuration.md)。本地不构建或运行测试,验证入口见 [验证](../../docs/operations/validation.md)。返回 [工作区](../README.md)。
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "frontend"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
peg.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,12 @@
|
||||
# frontend
|
||||
|
||||
独立源码前端:手写 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 规则 |
|
||||
| [tests](tests/README.md) | Unicode、优先级、函数、完整输入与错误位置验证 |
|
||||
| [examples](examples/README.md) | 可交给 cli parse 的源码样例 |
|
||||
|
||||
公开入口为 lex、parse、parse_expression;CLI 文件读取在宿主层。Core 仍为零外部依赖;未来编译器将 AST 转成已有字节码,不改为 AST 直接解释。返回 [工作区](../README.md)。
|
||||
@@ -0,0 +1,5 @@
|
||||
# Frontend 源码样例
|
||||
|
||||
[basic.endor](basic.endor) 包含字符串函数、位置参数、let 绑定、return 与算术表达式。它展示当前能解析的源码,不表示已能编译和执行运算。
|
||||
|
||||
在指定远端环境使用 `cli parse crates/frontend/examples/basic.endor` 输出 AST;工作区运行方法见 [运行指南](../../../docs/operations/README.md)。syntax.rs 包含此样例的解析检查。返回 [frontend](../README.md)。
|
||||
@@ -0,0 +1,9 @@
|
||||
// Parsing produces an AST; it does not execute these functions.
|
||||
function echo(text: String): String {
|
||||
let result = text
|
||||
return result
|
||||
}
|
||||
|
||||
function discountedPrice(price: Number, discount: Number): Number {
|
||||
return price - discount
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# Frontend 源码
|
||||
|
||||
| 文件 | 职责 |
|
||||
| --- | --- |
|
||||
| [lib.rs](lib.rs) | 公开 AST 模块、lex/parse/parse_expression 与 token、诊断导出 |
|
||||
| [token.rs](token.rs) | 可复制的 token 种类和原始源码范围,文本由源码切片取得 |
|
||||
| [diagnostic.rs](diagnostic.rs) | UTF-8 字节范围、词法/语法诊断与一基行列转换 |
|
||||
| [lexer.rs](lexer.rs) | 名称、数字、字符串、标点、空白与注释扫描;字符串校验及解码 |
|
||||
| [ast.rs](ast.rs) | 拥有文本的函数、参数、绑定、return、表达式与运算符结构 |
|
||||
| [parser.rs](parser.rs) | token 输入的 peg::parser! 规则、优先级、AST 构造及错误映射 |
|
||||
|
||||
改词法时同步 token 和错误检查;改语法时同步 AST、PEG 规则、[测试](../tests/README.md)、样例与 [前端契约](../../../docs/runtime/frontend.md)。不在语法动作中做 VM 分配、I/O 或修改外部状态,避免回溯造成副作用。返回 [frontend](../README.md)。
|
||||
@@ -0,0 +1,90 @@
|
||||
use crate::Span;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Name {
|
||||
pub text: String,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Program {
|
||||
pub functions: Vec<Function>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Function {
|
||||
pub name: Name,
|
||||
pub parameters: Vec<Parameter>,
|
||||
pub return_type: Name,
|
||||
pub body: Block,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Parameter {
|
||||
pub name: Name,
|
||||
pub type_name: Name,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
/// Basic draft function body: zero or more bindings and one final return.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Block {
|
||||
pub bindings: Vec<Binding>,
|
||||
pub result: Expr,
|
||||
pub return_span: Span,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Binding {
|
||||
pub name: Name,
|
||||
pub value: Expr,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Expr {
|
||||
pub kind: ExprKind,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ExprKind {
|
||||
Name(String),
|
||||
/// The decimal spelling is retained, without f64 conversion.
|
||||
Number(String),
|
||||
String(String),
|
||||
Symbol(Name),
|
||||
Unary {
|
||||
operation: UnaryOp,
|
||||
operand: Box<Expr>,
|
||||
},
|
||||
Binary {
|
||||
operation: BinaryOp,
|
||||
left: Box<Expr>,
|
||||
right: Box<Expr>,
|
||||
},
|
||||
Member {
|
||||
object: Box<Expr>,
|
||||
name: Name,
|
||||
},
|
||||
Call {
|
||||
callee: Box<Expr>,
|
||||
arguments: Vec<Expr>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UnaryOp {
|
||||
Negate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BinaryOp {
|
||||
Add,
|
||||
Subtract,
|
||||
Multiply,
|
||||
Divide,
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
/// Half-open UTF-8 byte range in the original source, including trivia between
|
||||
/// the first and last syntax token. EOF has an empty range at source.len().
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct Span {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
pub const fn new(start: usize, end: usize) -> Self {
|
||||
Self { start, end }
|
||||
}
|
||||
|
||||
pub(crate) fn through(self, last: Self) -> Self {
|
||||
Self::new(self.start, last.end)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DiagnosticKind {
|
||||
Lexical,
|
||||
Syntax,
|
||||
}
|
||||
|
||||
/// Lines and Unicode scalar columns are one-based. CRLF counts as one newline.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SourceLocation {
|
||||
pub line: usize,
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Diagnostic {
|
||||
pub kind: DiagnosticKind,
|
||||
pub span: Span,
|
||||
pub message: String,
|
||||
pub expected: Vec<String>,
|
||||
}
|
||||
|
||||
impl Diagnostic {
|
||||
pub(crate) fn lexical(span: Span, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
kind: DiagnosticKind::Lexical,
|
||||
span,
|
||||
message: message.into(),
|
||||
expected: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn location(&self, source: &str) -> SourceLocation {
|
||||
let mut location = SourceLocation { line: 1, column: 1 };
|
||||
let mut previous_cr = false;
|
||||
for (offset, character) in source.char_indices() {
|
||||
if offset >= self.span.start {
|
||||
break;
|
||||
}
|
||||
match character {
|
||||
'\r' => {
|
||||
location.line += 1;
|
||||
location.column = 1;
|
||||
}
|
||||
'\n' if previous_cr => {}
|
||||
'\n' => {
|
||||
location.line += 1;
|
||||
location.column = 1;
|
||||
}
|
||||
_ => location.column += 1,
|
||||
}
|
||||
previous_cr = character == '\r';
|
||||
}
|
||||
location
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Diagnostic {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
formatter,
|
||||
"{} at bytes {}..{}",
|
||||
self.message, self.span.start, self.span.end
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for Diagnostic {}
|
||||
@@ -0,0 +1,183 @@
|
||||
use crate::{Diagnostic, Span, Token, TokenKind};
|
||||
|
||||
/// 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<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),
|
||||
});
|
||||
}
|
||||
Ok(tokens)
|
||||
}
|
||||
|
||||
struct Lexer<'source> {
|
||||
source: &'source str,
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
impl<'source> Lexer<'source> {
|
||||
fn remaining(&self) -> &'source str {
|
||||
&self.source[self.offset..]
|
||||
}
|
||||
|
||||
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 {
|
||||
let mut result = String::new();
|
||||
let mut characters = raw.chars();
|
||||
while let Some(character) = characters.next() {
|
||||
if character == '\\' {
|
||||
result.push(match characters.next().expect("lexer validated escape") {
|
||||
'n' => '\n',
|
||||
't' => '\t',
|
||||
'"' => '"',
|
||||
'\\' => '\\',
|
||||
_ => unreachable!("lexer validated escape"),
|
||||
});
|
||||
} else {
|
||||
result.push(character);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//! UTF-8 source frontend: a 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.
|
||||
|
||||
pub mod ast;
|
||||
mod diagnostic;
|
||||
mod lexer;
|
||||
mod parser;
|
||||
mod token;
|
||||
|
||||
pub use diagnostic::{Diagnostic, DiagnosticKind, SourceLocation, Span};
|
||||
pub use lexer::lex;
|
||||
pub use parser::{parse, parse_expression};
|
||||
pub use token::{Token, TokenKind};
|
||||
@@ -0,0 +1,182 @@
|
||||
use crate::ast::{
|
||||
BinaryOp, Binding, Block, Expr, ExprKind, Function, Name, Parameter, Program, UnaryOp,
|
||||
};
|
||||
use crate::lexer::decode_string;
|
||||
use crate::{Diagnostic, DiagnosticKind, Span, Token, TokenKind, lex};
|
||||
|
||||
/// 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<Program, Diagnostic> {
|
||||
let tokens = lex(source)?;
|
||||
syntax::program(&tokens, source).map_err(|error| diagnostic(error, &tokens, source.len()))
|
||||
}
|
||||
|
||||
/// Parse exactly one complete expression, useful independently of declarations.
|
||||
pub fn parse_expression(source: &str) -> Result<Expr, Diagnostic> {
|
||||
let tokens = lex(source)?;
|
||||
syntax::expression(&tokens, source).map_err(|error| diagnostic(error, &tokens, source.len()))
|
||||
}
|
||||
|
||||
fn diagnostic(error: peg::error::ParseError<usize>, tokens: &[Token], end: usize) -> Diagnostic {
|
||||
let span = tokens
|
||||
.get(error.location)
|
||||
.map_or(Span::new(end, end), |token| token.span);
|
||||
let expected: Vec<String> = error.expected.tokens().map(str::to_owned).collect();
|
||||
Diagnostic {
|
||||
kind: DiagnosticKind::Syntax,
|
||||
span,
|
||||
message: format!("expected {}", expected.join(" or ")),
|
||||
expected,
|
||||
}
|
||||
}
|
||||
|
||||
fn binary(left: Expr, operation: BinaryOp, right: Expr) -> Expr {
|
||||
Expr {
|
||||
span: left.span.through(right.span),
|
||||
kind: ExprKind::Binary {
|
||||
operation,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
enum Suffix {
|
||||
Member(Name),
|
||||
Call(Vec<Expr>, Span),
|
||||
}
|
||||
|
||||
fn postfix(expression: Expr, suffix: Suffix) -> Expr {
|
||||
match suffix {
|
||||
Suffix::Member(name) => Expr {
|
||||
span: expression.span.through(name.span),
|
||||
kind: ExprKind::Member {
|
||||
object: Box::new(expression),
|
||||
name,
|
||||
},
|
||||
},
|
||||
Suffix::Call(arguments, end) => Expr {
|
||||
span: expression.span.through(end),
|
||||
kind: ExprKind::Call {
|
||||
callee: Box::new(expression),
|
||||
arguments,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
peg::parser! {
|
||||
grammar syntax(source: &str) for [Token] {
|
||||
rule keyword(word: &str) -> Span
|
||||
= [Token { kind: TokenKind::Name, span } if &source[span.start..span.end] == word] { span }
|
||||
|
||||
rule function_keyword() -> Span
|
||||
= quiet!{keyword("function")} / expected!("'function'")
|
||||
rule let_keyword() -> Span
|
||||
= quiet!{keyword("let")} / expected!("'let'")
|
||||
rule return_keyword() -> Span
|
||||
= quiet!{keyword("return")} / expected!("'return'")
|
||||
|
||||
rule name() -> Name
|
||||
= span:quiet!{[Token { kind: TokenKind::Name, span }] { span }} {
|
||||
Name { text: source[span.start..span.end].into(), span }
|
||||
}
|
||||
/ expected!("identifier")
|
||||
|
||||
rule lparen() -> Span
|
||||
= quiet!{[Token { kind: TokenKind::LeftParen, span }] { span }} / expected!("'('")
|
||||
rule rparen() -> Span
|
||||
= quiet!{[Token { kind: TokenKind::RightParen, span }] { span }} / expected!("')'")
|
||||
rule lbrace() -> Span
|
||||
= quiet!{[Token { kind: TokenKind::LeftBrace, span }] { span }} / expected!("'{'")
|
||||
rule rbrace() -> Span
|
||||
= quiet!{[Token { kind: TokenKind::RightBrace, span }] { span }} / expected!("'}'")
|
||||
rule colon()
|
||||
= quiet!{[Token { kind: TokenKind::Colon, .. }]} / expected!("':'")
|
||||
rule comma()
|
||||
= quiet!{[Token { kind: TokenKind::Comma, .. }]} / expected!("','")
|
||||
rule equal()
|
||||
= quiet!{[Token { kind: TokenKind::Equal, .. }]} / expected!("'='")
|
||||
rule dot() -> Span
|
||||
= quiet!{[Token { kind: TokenKind::Dot, span }] { span }} / expected!("'.'")
|
||||
rule plus()
|
||||
= [Token { kind: TokenKind::Plus, .. }]
|
||||
rule minus() -> Span
|
||||
= [Token { kind: TokenKind::Minus, span }] { span }
|
||||
rule star()
|
||||
= [Token { kind: TokenKind::Star, .. }]
|
||||
rule slash()
|
||||
= [Token { kind: TokenKind::Slash, .. }]
|
||||
|
||||
pub rule program() -> Program
|
||||
= functions:function()+ {
|
||||
let span = functions[0].span.through(functions.last().unwrap().span);
|
||||
Program { functions, span }
|
||||
}
|
||||
|
||||
rule function() -> Function
|
||||
= start:function_keyword() name:name() lparen()
|
||||
parameters:(parameter() ** comma()) rparen() colon() return_type:name() body:block() {
|
||||
Function { span: start.through(body.span), name, parameters, return_type, body }
|
||||
}
|
||||
|
||||
rule parameter() -> Parameter
|
||||
= name:name() colon() type_name:name() {
|
||||
Parameter { span: name.span.through(type_name.span), name, type_name }
|
||||
}
|
||||
|
||||
rule block() -> Block
|
||||
= start:lbrace() bindings:binding()* returned:return_statement() end:rbrace() {
|
||||
Block { bindings, result: returned.0, return_span: returned.1, span: start.through(end) }
|
||||
}
|
||||
|
||||
rule binding() -> Binding
|
||||
= start:let_keyword() name:name() equal() value:expression() {
|
||||
Binding { span: start.through(value.span), name, value }
|
||||
}
|
||||
|
||||
rule return_statement() -> (Expr, Span)
|
||||
= start:return_keyword() value:expression() {
|
||||
let span = start.through(value.span);
|
||||
(value, span)
|
||||
}
|
||||
|
||||
pub rule expression() -> Expr = precedence! {
|
||||
left:(@) plus() right:@ { binary(left, BinaryOp::Add, right) }
|
||||
left:(@) minus() right:@ { binary(left, BinaryOp::Subtract, right) }
|
||||
--
|
||||
left:(@) star() right:@ { binary(left, BinaryOp::Multiply, right) }
|
||||
left:(@) slash() right:@ { binary(left, BinaryOp::Divide, right) }
|
||||
--
|
||||
start:minus() operand:@ {
|
||||
Expr { span: start.through(operand.span), kind: ExprKind::Unary { operation: UnaryOp::Negate, operand: Box::new(operand) } }
|
||||
}
|
||||
--
|
||||
expression:postfix_expression() { expression }
|
||||
}
|
||||
|
||||
rule postfix_expression() -> Expr
|
||||
= expression:primary() suffixes:suffix()* {
|
||||
suffixes.into_iter().fold(expression, postfix)
|
||||
}
|
||||
|
||||
rule suffix() -> Suffix
|
||||
= dot() name:name() { Suffix::Member(name) }
|
||||
/ lparen() arguments:(expression() ** comma()) end:rparen() { Suffix::Call(arguments, end) }
|
||||
|
||||
rule primary() -> Expr
|
||||
= start:lparen() expression:expression() end:rparen() {
|
||||
Expr { span: start.through(end), ..expression }
|
||||
}
|
||||
/ start:dot() name:name() {
|
||||
Expr { span: start.through(name.span), kind: ExprKind::Symbol(name) }
|
||||
}
|
||||
/ [Token { kind: TokenKind::Number, span }] {
|
||||
Expr { span, kind: ExprKind::Number(source[span.start..span.end].into()) }
|
||||
}
|
||||
/ [Token { kind: TokenKind::String, span }] {
|
||||
Expr { span, kind: ExprKind::String(decode_string(&source[span.start + 1..span.end - 1])) }
|
||||
}
|
||||
/ name:name() { Expr { span: name.span, kind: ExprKind::Name(name.text) } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use crate::Span;
|
||||
|
||||
/// Text is located by span in the original source. Names stay contextual: the
|
||||
/// lexer does not turn syntax words into globally reserved keywords.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TokenKind {
|
||||
Name,
|
||||
Number,
|
||||
/// Span includes quotes. Escapes have been checked, not decoded.
|
||||
String,
|
||||
LeftParen,
|
||||
RightParen,
|
||||
LeftBrace,
|
||||
RightBrace,
|
||||
Colon,
|
||||
Comma,
|
||||
Dot,
|
||||
Equal,
|
||||
Plus,
|
||||
Minus,
|
||||
Star,
|
||||
Slash,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Token {
|
||||
pub kind: TokenKind,
|
||||
pub span: Span,
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Frontend 行为检查
|
||||
|
||||
[syntax.rs](syntax.rs) 覆盖 UTF-8 token 范围、中文/日文名称、数字原文、负号、字符串转义与错误、算术优先级和结合性、成员/调用链、函数与绑定、上下文名称、完整输入、非法/未支持语法以及 CRLF 和 EOF 诊断位置。
|
||||
|
||||
样例也通过此文件解析。只验证语法和 AST,不宣称类型检查或运行求值通过。随工作区检查在 ces-control 执行,本地不运行;入口见 [验证](../../../docs/operations/validation.md)。返回 [frontend](../README.md)。
|
||||
@@ -0,0 +1,278 @@
|
||||
use frontend::ast::{BinaryOp, Expr, ExprKind, UnaryOp};
|
||||
use frontend::{DiagnosticKind, Span, TokenKind, lex, parse, parse_expression};
|
||||
|
||||
fn binary(expression: &Expr, expected: BinaryOp) -> (&Expr, &Expr) {
|
||||
let ExprKind::Binary {
|
||||
operation,
|
||||
left,
|
||||
right,
|
||||
} = &expression.kind
|
||||
else {
|
||||
panic!("expected a binary expression: {expression:?}");
|
||||
};
|
||||
assert_eq!(*operation, expected);
|
||||
(left, right)
|
||||
}
|
||||
|
||||
fn number(expression: &Expr, expected: &str) {
|
||||
assert_eq!(expression.kind, ExprKind::Number(expected.into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_tokens_refer_to_exact_original_byte_ranges() {
|
||||
let source = "// 日本語\r\nlet 商品_2 = \"こんにちは🌸\"";
|
||||
let tokens = lex(source).unwrap();
|
||||
assert_eq!(tokens.len(), 4);
|
||||
assert_eq!(tokens[0].kind, TokenKind::Name);
|
||||
assert_eq!(&source[tokens[0].span.start..tokens[0].span.end], "let");
|
||||
assert_eq!(&source[tokens[1].span.start..tokens[1].span.end], "商品_2");
|
||||
assert_eq!(tokens[2].kind, TokenKind::Equal);
|
||||
assert_eq!(tokens[3].kind, TokenKind::String);
|
||||
for token in tokens {
|
||||
assert!(source.is_char_boundary(token.span.start));
|
||||
assert!(source.is_char_boundary(token.span.end));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decimal_spelling_and_minus_tokens_are_preserved_without_float_conversion() {
|
||||
let source = "-000123456789012345678901234567890.00100 / 7 // comment";
|
||||
let tokens = lex(source).unwrap();
|
||||
assert_eq!(
|
||||
tokens.iter().map(|token| token.kind).collect::<Vec<_>>(),
|
||||
vec![
|
||||
TokenKind::Minus,
|
||||
TokenKind::Number,
|
||||
TokenKind::Slash,
|
||||
TokenKind::Number,
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
&source[tokens[1].span.start..tokens[1].span.end],
|
||||
"000123456789012345678901234567890.00100"
|
||||
);
|
||||
let expression = parse_expression(source).unwrap();
|
||||
let (negative, divisor) = binary(&expression, BinaryOp::Divide);
|
||||
let ExprKind::Unary {
|
||||
operation: UnaryOp::Negate,
|
||||
operand,
|
||||
} = &negative.kind
|
||||
else {
|
||||
panic!("expected unary minus");
|
||||
};
|
||||
number(operand, "000123456789012345678901234567890.00100");
|
||||
number(divisor, "7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strings_decode_only_the_supported_escapes_and_keep_comments_inside_strings() {
|
||||
let expression = parse_expression(r#""商品\n\t\"\\// text""#).unwrap();
|
||||
assert_eq!(
|
||||
expression.kind,
|
||||
ExprKind::String("商品\n\t\"\\// text".into())
|
||||
);
|
||||
assert_eq!(
|
||||
expression.span,
|
||||
Span::new(0, r#""商品\n\t\"\\// text""#.len())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_strings_numbers_and_unknown_characters_report_lexical_errors() {
|
||||
for source in [
|
||||
"\"unfinished",
|
||||
"\"bad\\q\"",
|
||||
"\"bad\nline\"",
|
||||
"\"bad\rline\"",
|
||||
"\"bad\0\"",
|
||||
"\"trailing\\",
|
||||
"1e3",
|
||||
"12name",
|
||||
"1_000",
|
||||
"'text'",
|
||||
";",
|
||||
"🌸",
|
||||
"\0",
|
||||
] {
|
||||
let error = lex(source).unwrap_err();
|
||||
assert_eq!(error.kind, DiagnosticKind::Lexical, "{source:?}");
|
||||
assert!(error.span.start <= error.span.end);
|
||||
assert!(error.span.end <= source.len());
|
||||
assert!(source.is_char_boundary(error.span.start));
|
||||
assert!(source.is_char_boundary(error.span.end));
|
||||
}
|
||||
let source = "\"日\\q\"";
|
||||
let error = lex(source).unwrap_err();
|
||||
assert_eq!(&source[error.span.start..error.span.end], "\\q");
|
||||
assert_eq!(error.location(source).column, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithmetic_precedence_and_left_associativity_form_the_expected_tree() {
|
||||
let expression = parse_expression("1 - 2 - 3 * 4 / 5 + 6").unwrap();
|
||||
let (subtract, last) = binary(&expression, BinaryOp::Add);
|
||||
number(last, "6");
|
||||
let (first_subtract, divide) = binary(subtract, BinaryOp::Subtract);
|
||||
let (first, second) = binary(first_subtract, BinaryOp::Subtract);
|
||||
number(first, "1");
|
||||
number(second, "2");
|
||||
let (multiply, fifth) = binary(divide, BinaryOp::Divide);
|
||||
number(fifth, "5");
|
||||
let (third, fourth) = binary(multiply, BinaryOp::Multiply);
|
||||
number(third, "3");
|
||||
number(fourth, "4");
|
||||
let grouped = parse_expression("(1 + 2) * 3").unwrap();
|
||||
let (sum, _) = binary(&grouped, BinaryOp::Multiply);
|
||||
binary(sum, BinaryOp::Add);
|
||||
assert_eq!(sum.span, Span::new(0, 7));
|
||||
let nested = parse_expression("--1").unwrap();
|
||||
let ExprKind::Unary { operand, .. } = nested.kind else {
|
||||
panic!("expected outer negate");
|
||||
};
|
||||
assert!(matches!(
|
||||
operand.kind,
|
||||
ExprKind::Unary {
|
||||
operation: UnaryOp::Negate,
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn member_and_call_chains_bind_more_tightly_than_unary_minus() {
|
||||
let expression = parse_expression("-factory(1).build(.title)(\"日\", 2)").unwrap();
|
||||
let ExprKind::Unary {
|
||||
operation: UnaryOp::Negate,
|
||||
operand,
|
||||
} = expression.kind
|
||||
else {
|
||||
panic!("expected negate");
|
||||
};
|
||||
let ExprKind::Call { callee, arguments } = operand.kind else {
|
||||
panic!("expected last call");
|
||||
};
|
||||
assert_eq!(arguments.len(), 2);
|
||||
assert_eq!(arguments[0].kind, ExprKind::String("日".into()));
|
||||
let ExprKind::Call { callee, arguments } = callee.kind else {
|
||||
panic!("expected build call");
|
||||
};
|
||||
let ExprKind::Symbol(name) = &arguments[0].kind else {
|
||||
panic!("expected a symbol");
|
||||
};
|
||||
assert_eq!(name.text, "title");
|
||||
let ExprKind::Member { object, name } = callee.kind else {
|
||||
panic!("expected member");
|
||||
};
|
||||
assert_eq!(name.text, "build");
|
||||
let ExprKind::Call { callee, arguments } = object.kind else {
|
||||
panic!("expected factory call");
|
||||
};
|
||||
assert_eq!(callee.kind, ExprKind::Name("factory".into()));
|
||||
number(&arguments[0], "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_typed_functions_bindings_and_multiple_top_level_definitions() {
|
||||
let example = parse(include_str!("../examples/basic.endor")).unwrap();
|
||||
assert_eq!(example.functions.len(), 2);
|
||||
let source = r#"
|
||||
// Basic draft syntax, including a Unicode name.
|
||||
function discountedPrice(price: Number, discount: Number): Number {
|
||||
let amount = price - discount
|
||||
return amount
|
||||
}
|
||||
function 表示(): String { return "こんにちは" }
|
||||
"#;
|
||||
let program = parse(source).unwrap();
|
||||
assert_eq!(program.functions.len(), 2);
|
||||
let function = &program.functions[0];
|
||||
assert_eq!(function.name.text, "discountedPrice");
|
||||
assert_eq!(function.parameters.len(), 2);
|
||||
assert_eq!(function.parameters[0].type_name.text, "Number");
|
||||
assert_eq!(function.return_type.text, "Number");
|
||||
assert_eq!(function.body.bindings[0].name.text, "amount");
|
||||
binary(&function.body.bindings[0].value, BinaryOp::Subtract);
|
||||
assert_eq!(function.body.result.kind, ExprKind::Name("amount".into()));
|
||||
assert_eq!(
|
||||
&source[function.body.return_span.start..function.body.return_span.end],
|
||||
"return amount"
|
||||
);
|
||||
assert_eq!(
|
||||
program.functions[1].body.result.kind,
|
||||
ExprKind::String("こんにちは".into())
|
||||
);
|
||||
assert_eq!(
|
||||
&source[program.functions[1].span.start..program.functions[1].span.end],
|
||||
"function 表示(): String { return \"こんにちは\" }"
|
||||
);
|
||||
// The output owns its text and can outlive the input buffer.
|
||||
let owned = {
|
||||
let source = String::from("function f(): String { return \"ok\" }");
|
||||
parse(&source).unwrap()
|
||||
};
|
||||
assert_eq!(owned.functions[0].name.text, "f");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_words_are_contextual_and_block_layout_does_not_depend_on_newlines() {
|
||||
let program =
|
||||
parse("function class(function: Type): Type { let agent = function return agent }")
|
||||
.unwrap();
|
||||
assert_eq!(program.functions[0].name.text, "class");
|
||||
assert_eq!(program.functions[0].parameters[0].name.text, "function");
|
||||
assert_eq!(program.functions[0].body.bindings[0].name.text, "agent");
|
||||
let multiline =
|
||||
parse("function f\r\n(\r\nx: Number\r\n): Number\r\n{\r\nreturn x\r\n}").unwrap();
|
||||
assert_eq!(
|
||||
multiline.functions[0].body.result.kind,
|
||||
ExprKind::Name("x".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_requires_complete_input_and_rejects_unimplemented_or_malformed_syntax() {
|
||||
for source in [
|
||||
"",
|
||||
"// empty",
|
||||
"let x = 1",
|
||||
"use atelier:shop.data",
|
||||
"class Product {}",
|
||||
"agent Worker {}",
|
||||
"function f() { return 1 }",
|
||||
"function f(x): Number { return x }",
|
||||
"function f(x: Number,): Number { return x }",
|
||||
"function f(): Number {}",
|
||||
"function f(): Number { return 1 let x = 2 }",
|
||||
"function f(): Number { let x = 1 }",
|
||||
"function f(): Number { return 1 } extra",
|
||||
"function f(): Number { function g(): Number { return 1 } return 2 }",
|
||||
] {
|
||||
assert!(parse(source).is_err(), "{source}");
|
||||
}
|
||||
for source in [
|
||||
"", "x y", "1 +", "+1", ".5", "1.", "f(1,)", "f(,1)", "f(1 2)", "(1", "obj.", "[1]",
|
||||
"a == b", "1.2.3",
|
||||
] {
|
||||
assert!(parse_expression(source).is_err(), "{source}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors_map_token_indexes_to_utf8_source_and_eof_locations() {
|
||||
let source = "function f(): String {\r\n let 商品 = \"日\"\r\n return )\r\n}";
|
||||
let error = parse(source).unwrap_err();
|
||||
assert_eq!(error.kind, DiagnosticKind::Syntax);
|
||||
assert_eq!(&source[error.span.start..error.span.end], ")");
|
||||
assert_eq!(error.location(source).line, 3);
|
||||
assert_eq!(error.location(source).column, 10);
|
||||
assert!(!error.expected.is_empty());
|
||||
let source = "function f(): Number { return 1\r\n ";
|
||||
let error = parse(source).unwrap_err();
|
||||
assert_eq!(error.span, Span::new(source.len(), source.len()));
|
||||
assert_eq!(error.location(source).line, 2);
|
||||
assert_eq!(error.location(source).column, 3);
|
||||
let source = "\r\n\r\n🌸";
|
||||
let error = lex(source).unwrap_err();
|
||||
assert_eq!(error.location(source).line, 3);
|
||||
assert_eq!(error.location(source).column, 1);
|
||||
}
|
||||
+2
-1
@@ -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) |
|
||||
| 源码如何分词、PEG 规则、AST 和诊断 | [源码前端](runtime/frontend.md) | [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) |
|
||||
| UTF-8 在日本地区是否适用,其他编码怎样进入 VM | [文本编码](runtime/text-encoding.md) | [core](../crates/core/README.md)、[host](../crates/host/README.md) |
|
||||
@@ -25,7 +26,7 @@
|
||||
| 目录 | 负责的内容 |
|
||||
| --- | --- |
|
||||
| [architecture](architecture/README.md) | 系统边界、依赖方向和跨组件约束 |
|
||||
| [runtime](runtime/README.md) | 对象、基本载荷、字节码和生命周期 |
|
||||
| [runtime](runtime/README.md) | 源码前端、对象、基本载荷、字节码和生命周期 |
|
||||
| [operations](operations/README.md) | 运行、配置、部署和验证 |
|
||||
| [contributing](contributing/README.md) | 开发定位方法与随代码更新文档的规则 |
|
||||
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
# 系统架构
|
||||
|
||||
Endor 的目标是云原生语言;本仓库当前实现运行时和宿主演示,尚无源码编译器、云调度器或多租户执行沙箱。当前侧重核心语义独立于进程环境,以及数据库能力按对象需求显式提供。
|
||||
Endor 的目标是云原生语言;本仓库当前实现运行时、基础语法前端和宿主演示,尚无源码编译器、云调度器或多租户执行沙箱。当前侧重核心语义独立于进程环境,以及数据库能力按对象需求显式提供。
|
||||
|
||||
## 组件与依赖方向
|
||||
|
||||
```text
|
||||
CLI ────→ Core + Host
|
||||
CLI ────→ Core + Frontend + Host
|
||||
Frontend → peg
|
||||
Server ─→ Core + Host
|
||||
未来数据库特殊类的宿主实现 ─→ Storage
|
||||
```
|
||||
|
||||
CLI、Server 依赖 Host 保存文件信息,不依赖 Storage;Core 与 Storage 也互不依赖。HTTP、Tokio、SQLx、数据库、网络和浏览器能力不进入零依赖的 core。包与文件定位见 [crates](../../crates/README.md)。
|
||||
CLI、Server 依赖 Host 保存文件信息,不依赖 Storage;Core 与 Storage 也互不依赖。CLI 的 parse 命令使用 Frontend;Frontend 只依赖 peg,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) |
|
||||
| Host | 基础信息的逐键 UTF-8 JSON 文件与旧库导入 | [宿主信息](../operations/host-information.md) |
|
||||
| Storage | 按显式配置建立 SQLite/PostgreSQL 连接池,不定义应用表 | [数据库特殊类](../runtime/database-objects.md) |
|
||||
| CLI | 对象引导演示与文件信息命令 | [CLI](../../crates/cli/README.md) |
|
||||
| CLI | 对象引导演示、源码解析与文件信息命令 | [CLI](../../crates/cli/README.md) |
|
||||
| Server | HTTP 存活检查、只读 VM 信息及文件版本记录 | [Server](../../crates/server/README.md) |
|
||||
| Gitea/Docker | 双数据库适配验证、镜像构建、宿主与 PostgreSQL 资源部署 | [部署](../operations/deployment.md) |
|
||||
|
||||
@@ -40,4 +42,4 @@ HTTP 宿主通过 Arc 共享只读 Vm。GET /bootstrap 只读对象模型和数
|
||||
|
||||
## 后续边界
|
||||
|
||||
特殊类接口与异步数据库调用、源码前端、更多指令、异常传播、自动 GC、内存配额、取消和能力隔离等均需按实际需求设计。当前帧/指令限额不等于完整沙箱。返回 [文档索引](../README.md)。
|
||||
特殊类接口与异步数据库调用、AST 到字节码的编译与名称/类型分析、更多指令、异常传播、自动 GC、内存配额、取消和能力隔离等均需按实际需求设计。当前帧/指令限额不等于完整沙箱。返回 [文档索引](../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 文件表 |
|
||||
| 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 |
|
||||
| 地区与文本编码边界 | [text-encoding.md](../runtime/text-encoding.md) | 配置主题与实际 I/O 入口 |
|
||||
@@ -41,9 +42,9 @@
|
||||
|
||||
Core 保持零外部依赖、不引入 Python type/元类、源码前端或宿主库。句柄构造保持私有;非法句柄检查放堆内部单元测试,不能为测试开放非法构造器。容器更新须经过 VM 的引用事务;call/execute 对象结果持有根,借用读取仍须由使用方按需 retain。
|
||||
|
||||
Rust 实现依赖模块可见性、trait 和 rustfmt,不照搬 CPython C 符号命名规范。公开导出集中在 core/src/lib.rs,内部协作用 pub(crate) 或私有方法区分。当前没有 Endor 源码风格规范或通用配置框架。
|
||||
Rust 实现依赖模块可见性、trait 和 rustfmt,不照搬 CPython C 符号命名规范。Core 公开导出集中在 core/src/lib.rs,Frontend 导出集中在 frontend/src/lib.rs,内部协作用 pub(crate) 或私有方法区分。当前基础源码解析契约在 frontend.md,尚无完整 Endor 源码风格规范或通用配置框架。
|
||||
|
||||
后续优先按实际需求扩充函数定义/迭代器等指令、宿主回调、错误传播和资源边界;新增前端或云服务机制需要相应设计,不写成既有功能。
|
||||
独立 Frontend 已提供 lexer/parser 和 AST;后续按实际需求接入名称/闭包分析和字节码编译、扩充迭代器等指令、宿主回调、错误传播和资源边界。语法可继续调整,AST 到字节码和云服务机制尚未实现,不写成既有功能。
|
||||
|
||||
## 导出与迁移
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
|
||||
## 当前项目与命名
|
||||
|
||||
仓库为 endorphin-aster-vm,归属 ProjectEndorphin。包与目录为 core、storage、host、cli、server;core 的 Rust 库名为 endor_core,命令为 cli/server。旧部署服务、数据库和路径保留既有标识以保护数据。
|
||||
仓库为 endorphin-aster-vm,归属 ProjectEndorphin。包与目录为 core、frontend、storage、host、cli、server;core 的 Rust 库名为 endor_core,命令为 cli/server。旧部署服务、数据库和路径保留既有标识以保护数据。
|
||||
|
||||
## 已实现
|
||||
|
||||
- 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)。
|
||||
- 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 作为未来特殊类的预置资源保留。
|
||||
@@ -21,7 +22,7 @@
|
||||
|
||||
## 尚未实现
|
||||
|
||||
源码 lexer/parser 与编译器、迭代器等扩展指令、数值算术/相等/哈希、完整异常传播、可变 VM/异步数据库回调、生成器、自动 GC、堆快照、内存配额、取消及不可信代码沙箱。宿主回调 panic 后的执行根恢复也未完成。不能把未来设计写成已有能力。
|
||||
AST 到字节码编译器、名称/闭包与类型分析、源码闭包声明、导入/类/Agent 语法、迭代器等扩展指令、数值算术/相等/哈希、完整异常传播、可变 VM/异步数据库回调、生成器、自动 GC、堆快照、内存配额、取消及不可信代码沙箱。宿主回调 panic 后的执行根恢复也未完成。不能把未来设计写成已有能力。
|
||||
|
||||
## 工作约定
|
||||
|
||||
@@ -33,4 +34,4 @@
|
||||
|
||||
## 下一步
|
||||
|
||||
函数创建指令完成后可以开始设计源码前端:先明确 Endor 的最小语法、token 与 AST,再依次实现 lexer、parser、名称/闭包分析及字节码生成。目标路径为源码 → token → AST → 现有字节码 → VM,不改成 AST 直接解释。最初可覆盖字符串/布尔/Nil、变量、位置参数函数、调用、return 和闭包;数字运算与异常传播按后续需求补齐,不作为开始前端的前置要求。当前尚未启动前端实现,也未把 Python 示例视作 Endor 语法规范;前端包的边界与语法仍待确定。
|
||||
前端已完成到 AST 的首版实现;下一步接 AST → 名称/局部槽与闭包分析 → 字节码 → 现有 VM,不改成 AST 直接解释。可以先连接字符串、名称、let、位置参数函数和调用,再补算术指令与数值语义。参数与返回类型目前只保存为语法节点,不引入 Python type/元类或借此实现整个语言类型系统。语法不必等外部设计稿全部定稿,按用户要求先实现可用子集、以后再调整;当前尚无源码执行命令。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 运行宿主
|
||||
|
||||
CLI 和 Server 运行 VM 引导、状态与文件基础信息,不连接数据库。Storage 为未来数据库特殊类保留显式连接能力,见 [数据库对象](../runtime/database-objects.md)。以下构建和运行命令仅供用户指定的远端环境,本地不执行。
|
||||
CLI 提供 VM 引导、源码解析及文件基础信息,Server 提供状态与文件基础信息,二者不连接数据库。Storage 为未来数据库特殊类保留显式连接能力,见 [数据库对象](../runtime/database-objects.md)。以下构建和运行命令仅供用户指定的远端环境,本地不执行。
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -10,10 +10,13 @@ CLI 和 Server 运行 VM 引导、状态与文件基础信息,不连接数据
|
||||
cargo build --workspace --locked
|
||||
cargo run -p cli -- bootstrap
|
||||
cargo run -p cli -- help
|
||||
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)。
|
||||
|
||||
CLI 不读取数据库环境,不创建基础信息数据库表。metadata put/get 通过 host 包写 UTF-8 JSON 文件,metadata import 仅导入缺失记录;示例、配置和协议见 [宿主信息](host-information.md)。输入与退出码见 [cli/src](../../crates/cli/src/README.md)。
|
||||
|
||||
## HTTP
|
||||
|
||||
@@ -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。外部库、终端、字体或下游工具自身的行为不在这项保证之内。
|
||||
- 尚无源码加载器、标识符语法、通用字节串对象或文件系统 API,因此不能宣称已完成源码编码、路径编码和全部 I/O 策略。
|
||||
- CLI parse 严格读取 UTF-8 文件,Frontend 提供基础标识符与语法解析,见 [源码前端](../runtime/frontend.md)。尚无通用字节串对象、语言文件系统 API 或非 UTF-8 源码自动转码入口。
|
||||
|
||||
日文与外部 Shift_JIS/CP932 的处理见 [文本编码](../runtime/text-encoding.md);内部 UTF-8 不因地区切换。
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ ci-ces-control.sh 负责准备全新专用临时 PostgreSQL 和测试 URL;成
|
||||
| 对象运行时 | 地址稳定、引用/借用、属性、长链、循环、原生操作、VM 销毁 | [core/tests](../../crates/core/tests/README.md) 的 runtime.rs |
|
||||
| 基本对象 | Number 表示、字符串、Dict/Cell 引用与更新、函数环境、异常数据 | 同目录 builtins.rs |
|
||||
| 字节码 | 编解码、非法指令/控制流、跳转、调用、闭包、字节码创建函数/默认值/环境捕获、函数创建及后续失败清理、结果存活与限额 | 同目录 execution.rs |
|
||||
| 源码前端 | Unicode 范围、数字原文、字符串与错误、优先级/结合性、函数/绑定、完整输入与错误位置、源码样例 | [frontend/tests](../../crates/frontend/tests/README.md) |
|
||||
| CLI 解析 | AST 输出、未执行源码、文件/行/列错误与非法 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) |
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
| --- | --- |
|
||||
| [对象模型](objects.md) | EndorObject/ObjectHeader、代际句柄、操作协议、托管引用与循环回收 |
|
||||
| [基本对象与 Number](builtins.md) | 函数、原生函数、字典、Cell、异常数据、字符串与无损数字 |
|
||||
| [源码前端](frontend.md) | 手写 lexer、token PEG parser、AST、基础函数/表达式语法与诊断 |
|
||||
| [字节码 VM](bytecode.md) | 指令编码、代码校验、局部槽、执行帧、函数创建、调用/返回与边界 |
|
||||
| [文本编码](text-encoding.md) | UTF-8、日文、外部 Shift_JIS/CP932 与宿主解码边界 |
|
||||
| [数据库特殊类](database-objects.md) | 显式连接、调用方数据模型、未来类接入与资源生命周期 |
|
||||
|
||||
@@ -119,7 +119,7 @@ fn main() -> Result<(), endor_core::VmError> {
|
||||
}
|
||||
```
|
||||
|
||||
当前已能通过字节码读取、写入和删除共享 Cell,并创建捕获参数的 Cell。LoadClosure 与 MakeFunction 可以在外围函数运行时创建共享这些 Cell 的嵌套函数,外围帧返回后 Cell 仍由闭包保活。上面的 Python 代码说明闭包语义;Endor 尚无源码解析器,代码对象仍由宿主准备。
|
||||
当前已能通过字节码读取、写入和删除共享 Cell,并创建捕获参数的 Cell。LoadClosure 与 MakeFunction 可以在外围函数运行时创建共享这些 Cell 的嵌套函数,外围帧返回后 Cell 仍由闭包保活。上面的 Python 代码说明闭包语义;独立 frontend 已能解析基础函数,但尚无源码闭包语法或字节码编译器,代码对象仍由宿主准备。
|
||||
|
||||
## 字符串与异常的准确含义
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 小型字节码 VM
|
||||
|
||||
当前提供指令与代码对象、内部执行帧、取指执行循环,语言函数已接入执行器。参考 CPython 3.14 的职责划分和栈式执行设计;不实现 lexer/parser,不读取 Python `.pyc`,不承诺 CPython 操作码编号、ABI 或完整语言兼容性。
|
||||
当前提供指令与代码对象、内部执行帧、取指执行循环,语言函数已接入执行器。参考 CPython 3.14 的职责划分和栈式执行设计;源码 lexer/parser 由独立 [frontend](frontend.md) 提供,尚无 AST 到字节码编译器;执行器不读取 Python `.pyc`,不承诺 CPython 操作码编号、ABI 或完整语言兼容性。
|
||||
|
||||
## 与 CPython 的对应
|
||||
|
||||
@@ -148,7 +148,7 @@ fn main() -> Result<(), endor_core::VmError> {
|
||||
}
|
||||
```
|
||||
|
||||
该示例直接提供指令,不表示已经支持 Endor 或 Python 源码解析。宿主仍负责准备代码对象和执行环境;函数既可由宿主 new_function 工厂创建,也可由上述 MakeFunction 在字节码运行期间创建。
|
||||
该示例直接提供指令,不经过源码前端;当前 Endor 前端可生成 AST,但尚不能从源码生成字节码,也不解析 Python 源码。宿主仍负责准备代码对象和执行环境;函数既可由宿主 new_function 工厂创建,也可由上述 MakeFunction 在字节码运行期间创建。
|
||||
|
||||
## 限额、诊断与边界
|
||||
|
||||
@@ -156,7 +156,7 @@ fn main() -> Result<(), endor_core::VmError> {
|
||||
|
||||
运行错误仍返回 `VmError`。`last_traceback()` 另提供从外到内的函数名、文件、代码首行和出错指令偏移;它保存独立元数据,不保活已经清理的帧。首行是代码声明的 first_line,不是由行号表推导出的实际错误源码行。成功的新一次调用会清空旧记录。尚无语言异常对象传播、raise/try/except 或完整源码行号表。
|
||||
|
||||
暂未实现:lexer/parser、源码编译器、关键字/可变参数、生成器/协程、迭代器专用指令、数值算术、自动方法绑定、指令特化和 JIT。
|
||||
暂未实现:源码编译器、关键字/可变参数、生成器/协程、迭代器专用指令、数值算术、自动方法绑定、指令特化和 JIT。
|
||||
|
||||
## 验证
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# 源码词法与 PEG 语法前端
|
||||
|
||||
frontend 包提供手写 Rust lexer 和 rust-peg 生成的 token parser。当前入口为 lex、parse、parse_expression,CLI 提供 parse 文件命令;结果是 AST,还没有 AST 到字节码的编译器、类型检查、名称解析或源码执行。Core 不依赖 frontend 或 peg,仍保持零外部依赖。
|
||||
|
||||
首版以 Endorphin-Language 设计稿的基础词法、表达式与函数章节为起点,语法可以继续修改;设计稿中的未完成部分不作为阻塞前端实现的前置条件,也不被整体视作已经实现的语言能力。这里维护当前解析契约;源码位置见 [frontend](../../crates/frontend/README.md)。
|
||||
|
||||
## 入口和所有权
|
||||
|
||||
| API | 输入与结果 |
|
||||
| --- | --- |
|
||||
| lex(&str) | 产生 Vec<Token>;每个 token 只有种类和原始源码的字节范围,不复制文本 |
|
||||
| parse(&str) | 解析一个或多个完整函数定义,返回 Program;空输入和未消费的尾部内容报错 |
|
||||
| parse_expression(&str) | 解析一个完整表达式,不能忽略尾部 token |
|
||||
|
||||
AST 的名称、数字原文和解码后的字符串均由 AST 自己持有,可以在释放输入后继续使用。Token/AST 的 Span 均为原始 UTF-8 源码的半开字节区间 start..end;节点跨度包含首尾 token 之间的空白及注释,不包含外侧 trivia。分组表达式的范围包括括号,Program 范围从第一个函数到最后一个函数。
|
||||
|
||||
语法动作只构造 AST,不做 I/O、VM 分配或其他外部写入。PEG 回溯不会执行语言函数或产生宿主业务副作用。
|
||||
|
||||
## 词法规则
|
||||
|
||||
- 输入是有效 UTF-8 的 &str。CLI 使用严格的 read_to_string 读文件;其他编码由宿主先显式解码,不修改全局 locale。
|
||||
- 名称首字符为下划线或 Rust char::is_alphabetic,后续为下划线或 char::is_alphanumeric。这是当前明确采用的分类,不承诺 XID/NFKC 规则;没有自动规范化。中文、日文可用于名称,emoji 可以用于字符串。
|
||||
- 名称统一产生 Name token。function、let、return 只在相应语法位置按拼写识别,不预先固定全局保留字表;class、agent 等也仍可作为名称。
|
||||
- 数字为 ASCII 十进制数字,可带两边都有数字的小数部分;原文保留,不转换为 f64。负号单独分词;科学计数法、数字后缀、下划线分隔、前导正号、.5 和 1. 形式不在当前表达式语法中。
|
||||
- 双引号字符串允许 Unicode 文本,支持 `\n`、`\t`、`\"`、`\\`。token 范围包含引号,构造 AST 时解码转义。未闭合字符串、未知转义、裸换行和 NUL 报错;没有单引号、多行或插值字符串。
|
||||
- 空格、Tab、LF、CR 和 CRLF 分隔 token;缩进和换行不形成语句或代码块。`//` 到行末为注释,字符串内的 `//` 保持文本。没有块注释。
|
||||
- 支持 `(`、`)`、`{`、`}`、`:`、`,`、`.`、`=`、`+`、`-`、`*`、`/`;不接受分号和其他未支持字符。
|
||||
|
||||
## 当前语法
|
||||
|
||||
```text
|
||||
program = function+
|
||||
function = "function" name "(" parameters? ")" ":" name block
|
||||
parameter = name ":" name
|
||||
block = "{" binding* "return" expression "}"
|
||||
binding = "let" name "=" expression
|
||||
```
|
||||
|
||||
parameters 和调用参数列表使用逗号分隔,不接受尾随逗号。函数体要求一个末尾 return;不支持独立表达式语句、重赋值、嵌套函数声明或默认参数源码写法。参数和返回类型目前只是名称节点;并不据此创建语言 type 对象或检查类型。
|
||||
|
||||
表达式包括名称、数字、字符串、`.name` 符号、括号分组、成员访问与位置参数调用、一元负号以及四则运算。优先级从高到低为:
|
||||
|
||||
1. 成员访问与调用,可任意连续组合,例如 factory(1).build(.title)("日文")。
|
||||
2. 一元负号,可嵌套。
|
||||
3. 乘除,从左到右结合。
|
||||
4. 加减,从左到右结合。
|
||||
|
||||
符号字面量、名称与成员分别保留为独立 AST 结构。true、false、Nil 等词目前只是普通名称,没有额外的源码字面量或内建值绑定。
|
||||
|
||||
use、namespace、class、agent、完整类型语法、条件/循环、比较/布尔运算和闭包声明等尚未加入首版。当前语法可修改;扩展时同步 AST、PEG 规则、样例和测试,不将旧草案视作不可修改的 ABI。
|
||||
|
||||
## 诊断和命令
|
||||
|
||||
词法和语法错误分别标记为 Lexical/Syntax。语法错误将 PEG 的 token 下标转换成原始源码 Span,保留期望项目列表;EOF 错误使用 source.len() 处的空区间。Diagnostic::location 返回一基行列,列按 Unicode 标量计数,CRLF 只算一次换行,不按字节列或字形簇计数。
|
||||
|
||||
`cli parse <file>` 成功时输出 Rust Debug 格式的 AST,失败时在 stderr 输出文件/行/列和诊断并返回非零退出码。AST Debug 文本不是稳定的序列化协议。命令不执行源码、不初始化 VM、不连接数据库;具体运行与本地禁测约定见 [运行](../operations/README.md) 和 [验证](../operations/validation.md)。
|
||||
|
||||
后续编译器应将 AST 经名称/闭包分析转换为现有字节码,不另设 AST 求值器;解析成功不表示表达式已有运行语义。VM 虽已支持 MakeFunction,但从源码创建函数仍需要编译器接入。解析器也不是不可信代码沙箱,当前没有通用输入大小或解析递归深度限额。
|
||||
@@ -20,10 +20,10 @@ 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)。
|
||||
|
||||
当前尚无通用文件/网络文本加载器、Shift_JIS 解码器或源码 parser;以上非 UTF-8 转换是未来宿主入口应遵守的边界,不是已经提供的内置转码能力。无需修改全局 locale 来处理单个文件。
|
||||
CLI parse 已有严格 UTF-8 文件读取,frontend 接收 &str 进行词法和语法解析。当前仍无通用文件/网络文本加载器或 Shift_JIS 解码器;以上非 UTF-8 转换由宿主显式提供,不是已有内置转码能力。无需修改全局 locale 来处理单个文件。
|
||||
|
||||
## 其他日文文本差异
|
||||
|
||||
当前按 Unicode 标量值访问字符,不按人眼字形簇。预组合的“ガ”和“カ”加组合浊点可显示相似,但标量数量和序列不同;当前没有自动规范化或全角/半角转换。字符串和字典按当前内容语义处理,不为了所在地悄悄改写数据。源码标识符规则尚未定义。
|
||||
当前按 Unicode 标量值访问字符,不按人眼字形簇。预组合的“ガ”和“カ”加组合浊点可显示相似,但标量数量和序列不同;当前没有自动规范化或全角/半角转换。字符串和字典按当前内容语义处理,不为了所在地悄悄改写数据。前端当前采用的名称字符分类与源码位置规则见 [源码前端](frontend.md),名称同样不自动规范化。
|
||||
|
||||
宿主基础信息以 UTF-8 JSON 写入;控制键的 ASCII 命名限制属于文件信息协议,值可保存日文,详见 [宿主信息](../operations/host-information.md)。配置与 locale 见 [配置](../operations/configuration.md),返回 [运行时索引](README.md)。
|
||||
|
||||
Reference in New Issue
Block a user