refactor: separate language engine from CLI and server hosts
Verify Aster engine / verify (push) Failing after 0s

This commit is contained in:
2026-10-20 01:08:51 +08:00
parent 51cd64e688
commit 55475a9d4b
72 changed files with 214 additions and 4466 deletions
+5
View File
@@ -8,3 +8,8 @@ ci-output
*.db
*.db-shm
*.db-wal
**/.git
**/target
**/AGENTS.md
**/work
**/ci-output
-8
View File
@@ -1,8 +0,0 @@
# Docker Compose reads .env; programs read exported host settings.
ASTER_PORT=8080
ASTER_LISTEN=127.0.0.1:8080
# JSON host information, separate from databases:
ASTER_STATE_DIR=data/host
# PostgreSQL provisioning only: credentials are not consumed by the VM host.
# Database-backed objects will receive an explicit DatabaseConfig.
POSTGRES_PASSWORD=replace-with-a-random-hex-password
+20 -3
View File
@@ -1,5 +1,22 @@
# Gitea 配置
# Aster engine automation
本目录保存仓库自己的自动化定义。目前仅有 [workflows](workflows/README.md),不保存服务器凭据或共享 Runner 配置。
[workflows](workflows/README.md) contains repository workflows.
main 推送沿用 ces-control 验证和部署;机制见 [部署](../docs/operations/deployment.md),结果边界见 [验证](../docs/operations/validation.md)。返回 [项目入口](../README.md)。
## CI checkout prerequisites
All workflows use `runs-on: ces-control`; no shared Runner configuration is changed.
Configure repository or organization secrets `CODEX_BOT_SSH_KEY` (the dedicated
codex-bot private SSH key) and `GITEA_SSH_KNOWN_HOSTS` (the independently verified
known_hosts entry for `[gitea.entroia.com]:22222`). The bot needs read access to the
repository and its Aster submodule. Workflows use an isolated SSH command with
`-F /dev/null`, `IdentitiesOnly=yes`, an explicit key and port, strict host-key
checking, and temporary credential files removed after checkout. Keys are never
written into tracked files. Missing secrets fail checkout explicitly.
The SSH submodule URL's repository owner is separate from the authenticated bot.
Local Git operations must use the same dedicated bot identity; configure the
checkout's isolated command in `GIT_SSH_COMMAND` for submodule initialization.
Workflow definitions do not prove CI passed. CI/deployment state is not queried
automatically. Local runtime checks are not performed for Aster, Lyra or Hestia;
Tessera permits local native builds/window runs, with automated tests in CI.
+3 -9
View File
@@ -1,11 +1,5 @@
# CI 工作流
# Aster engine workflows
[ci-deploy.yml](ci-deploy.yml) 是当前唯一工作流:main push 或 workflow_dispatch 触发,运行于 ces-control,生产并发组串行且不取消正在运行的发布,任务超时 60 分钟。
[ci.yml](ci.yml) verifies this project on main pushes, pull requests and manual dispatch. It runs on ces-control, with a 60-minute timeout. Checkout and secret requirements are in [the parent README](../README.md).
顺序为检出 → ci-ces-control.sh 验证及执行 Endor 示例 → 展示报告并上传 artifact → 构建 SHA 生产镜像 → main 分支执行 deploy-ces-control.sh。报告展示和上传使用 always(),验证失败也保留诊断。Artifact 名为 endor-execution-output-<sha>,包含 endor-examples.md 和 endor-examples.json,保留 30 天;从对应 Actions run 页面下载。使用 actions/upload-artifact@v3 连接当前 Gitea 的 artifact 服务,接口参考 [action 定义](https://github.com/actions/upload-artifact/blob/v3/action.yml)。
设置 DOCKER_BUILDKIT=0 使用当前 builder 路径,检出不保留认证信息;工作流不更改共享 Runner。PostgreSQL 保留为预置资源,基础信息由文件保存;发布只为旧基础信息做一次只读导入,不再写入数据库。
脚本职责见 [scripts](../../scripts/README.md),完整发布、保留数据和回退规则见 [部署](../../docs/operations/deployment.md)。main 推送会进入现有发布链,不能把推送成功写成 CI 或部署成功;按用户约定不主动跟踪状态。
修改步骤后同步本页及对应全局主题。返回 [Gitea 配置](../README.md)。
There is no production service deployment in this workflow.
-48
View File
@@ -1,48 +0,0 @@
name: Verify databases and deploy Aster VM
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: endorphin-aster-vm-production
cancel-in-progress: false
jobs:
verify-and-deploy:
runs-on: ces-control
timeout-minutes: 60
env:
DOCKER_BUILDKIT: "0"
ASTER_RELEASE_SHA: ${{ gitea.sha }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Verify SQLite, PostgreSQL and host behavior
run: sh scripts/ci-ces-control.sh
- name: Show Endor example execution output
if: always()
run: cat ci-output/endor-examples.md
- name: Upload Endor example execution output
if: always()
uses: actions/upload-artifact@v3
with:
name: endor-execution-output-${{ gitea.sha }}
path: |
ci-output/endor-examples.md
ci-output/endor-examples.json
if-no-files-found: error
retention-days: 30
- name: Build production image
run: docker build --target runtime -t "endorphin-aster-vm:$ASTER_RELEASE_SHA" .
- name: Deploy VM host and provision PostgreSQL
if: gitea.ref == 'refs/heads/main'
run: sh scripts/deploy-ces-control.sh
+49
View File
@@ -0,0 +1,49 @@
name: Verify Aster engine
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
verify:
runs-on: ces-control
timeout-minutes: 60
env:
DOCKER_BUILDKIT: "0"
steps:
- name: Checkout with codex-bot
env:
CODEX_BOT_SSH_KEY: ${{ secrets.CODEX_BOT_SSH_KEY }}
GITEA_SSH_KNOWN_HOSTS: ${{ secrets.GITEA_SSH_KNOWN_HOSTS }}
SOURCE_SHA: ${{ gitea.sha }}
SOURCE_REPOSITORY: ${{ gitea.repository }}
run: |
set -eu
test -n "$CODEX_BOT_SSH_KEY"
test -n "$GITEA_SSH_KNOWN_HOSTS"
case "$SOURCE_SHA" in *[!0-9a-f]*|'') exit 1;; esac
test "${#SOURCE_SHA}" -eq 40
umask 077
checkout_keys=$(mktemp -d)
trap 'rm -rf "$checkout_keys"' EXIT
printf '%s\n' "$CODEX_BOT_SSH_KEY" > "$checkout_keys/key"
printf '%s\n' "$GITEA_SSH_KNOWN_HOSTS" > "$checkout_keys/known_hosts"
unset CODEX_BOT_SSH_KEY GITEA_SSH_KNOWN_HOSTS
export GIT_SSH_COMMAND="ssh -F /dev/null -o IdentitiesOnly=yes -i $checkout_keys/key -p 22222 -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$checkout_keys/known_hosts"
git init .
source_url="ssh://git@gitea.entroia.com:22222/$SOURCE_REPOSITORY.git"
if git remote get-url origin >/dev/null 2>&1; then
git remote set-url origin "$source_url"
else
git remote add origin "$source_url"
fi
git fetch --no-tags --depth=1 origin "$SOURCE_SHA"
git checkout --detach --force FETCH_HEAD
git submodule update --init --recursive
- name: Verify engine only
run: |
image="endorphin-aster-vm-ci:${{ gitea.sha }}"
docker build -f Dockerfile.ci -t "$image" .
docker run --rm --init "$image"
Generated
-1780
View File
File diff suppressed because it is too large Load Diff
+1 -6
View File
@@ -1,5 +1,5 @@
[workspace]
members = ["crates/core", "crates/objects", "crates/frontend", "crates/compiler", "crates/storage", "crates/cli", "crates/server", "crates/host"]
members = ["crates/core", "crates/objects", "crates/frontend", "crates/compiler"]
resolver = "2"
[workspace.package]
@@ -11,16 +11,11 @@ license = "MIT"
[workspace.dependencies]
core = { path = "crates/core" }
objects = { path = "crates/objects" }
storage = { path = "crates/storage" }
host = { path = "crates/host" }
frontend = { path = "crates/frontend" }
compiler = { path = "crates/compiler" }
peg = "=0.8.6"
axum = { version = "0.8", default-features = false, features = ["http1", "json", "tokio"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.8.6", default-features = false, features = ["runtime-tokio", "tls-rustls-ring", "sqlite", "postgres"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal", "fs"] }
[workspace.lints.rust]
unsafe_code = "forbid"
-31
View File
@@ -1,31 +0,0 @@
FROM rust:1.91.1-bookworm AS source
WORKDIR /build
ENV CARGO_BUILD_JOBS=1
COPY Cargo.toml Cargo.lock ./
COPY crates ./crates
COPY scripts ./scripts
FROM source AS ci
RUN apt-get update && apt-get install -y --no-install-recommends python3 \
&& rm -rf /var/lib/apt/lists/* \
&& rustup component add rustfmt clippy
ENV CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0
CMD ["sh", "scripts/check.sh"]
FROM source AS build
RUN cargo build --locked --release -p server -p cli
FROM debian:bookworm-slim AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --system --uid 10001 --create-home aster \
&& mkdir -p /data/host && chown -R aster:aster /data
COPY --from=build /build/target/release/server /usr/local/bin/server
COPY --from=build /build/target/release/cli /usr/local/bin/cli
USER 10001
WORKDIR /data
ENV ASTER_LISTEN=0.0.0.0:8080 ASTER_STATE_DIR=/data/host
EXPOSE 8080
HEALTHCHECK --interval=10s --timeout=6s --start-period=10s --retries=3 \
CMD curl --fail --silent http://127.0.0.1:8080/health || exit 1
CMD ["server"]
+10
View File
@@ -0,0 +1,10 @@
FROM rust:1.91.1-bookworm
WORKDIR /build
RUN apt-get update && apt-get install -y --no-install-recommends python3 \
&& rm -rf /var/lib/apt/lists/* \
&& rustup component add rustfmt clippy
ENV CARGO_BUILD_JOBS=1 CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0
COPY Cargo.toml Cargo.lock ./
COPY crates ./crates
COPY scripts ./scripts
CMD ["sh", "scripts/check.sh"]
+13 -41
View File
@@ -1,47 +1,19 @@
# Endor / Aster VM
# Aster VM
Rust 编写的 Endor 虚拟机实现。核心对象与对象存储位于独立 objects 包,core 专注 VM、执行帧与求值循环。当前具备 EndorObject、稳定对象分配、代际句柄、引用计数、显式循环回收,以及可执行语言函数的小型 wordcode VM。数字统一为无损表示优先的 Number。
Aster 是可嵌入的 Endor 语言引擎,提供源码阅读、解析、AST、字节码编译、对象系统和 VM。宿主通过 Rust 库接口创建 VM;引擎不部署 HTTP 服务,也不提供应用级数据库或文件信息存储。
Endor 采用平台解释执行模型;AST 到字节码的生成属于解释器内部准备步骤,使用者无需先构建独立程序。资源修订与依赖锁用于选择用户代码,解释器由平台统一升级;相关设计方向见 [解释执行与平台资源版本](docs/architecture/execution-and-versions.md),资源版本管理尚未实现。
独立 frontend 提供阅读器、字符 PEG lexer、token PEG parser、ASDL 生成的 AST 与编辑器接口。compiler 已连接名称/作用域分析、字节码生成和代码对象装载,CLI run/eval 可执行基础函数与表达式;可执行示例见 [compiler/examples](crates/compiler/examples/README.md)。VM 从空堆启动;Python type/元类体系、数值算术、源码闭包和完整异常传播尚未实现。数据库连接仅为需要数据库的特殊类保留;普通宿主不连接数据库;基础信息改用 host 包的 UTF-8 JSON 文件,不保存 VM 堆。当前边界见 [系统架构](docs/architecture/README.md)。
## 从哪里读
| 要做的事 | 入口 |
| 目录 | 职责 |
| --- | --- |
| 查一个功能、设计或代码位置 | [文档总索引](docs/README.md) |
| 看解释执行、资源修订、依赖锁和平台升级的关系 | [执行与版本模型](docs/architecture/execution-and-versions.md) |
| 看核心对象与对象存储 | [objects](crates/objects/README.md) |
| 看源码目录与 crate 边界 | [crates](crates/README.md) |
| 看字符串、流、文件与平台 URI 如何进入 lexer | [Endor 阅读器](docs/runtime/reader.md) |
| 看 AST 根节点、ASDL、遍历与 JSON/编辑器接口 | [AST 模型](docs/runtime/ast.md) |
| 看源码词法、语法、AST 和解析命令 | [源码前端](docs/runtime/frontend.md) |
| 看名称/作用域分析、源码编译与执行示例 | [编译链路](docs/runtime/compiler.md) |
| 看指令、代码对象、执行帧和执行循环 | [字节码 VM](docs/runtime/bytecode.md) |
| 看 VM/帧/对象如何存活和销毁 | [生命周期](docs/runtime/lifecycle.md) |
| 看基础信息文件和旧库迁移 | [宿主信息](docs/operations/host-information.md) |
| 看配置、UTF-8 与 locale 边界 | [配置](docs/operations/configuration.md) |
| 运行 CLI/HTTP 宿主 | [运行指南](docs/operations/README.md) |
| 看 ces-control CI 与 PostgreSQL 部署 | [部署](docs/operations/deployment.md) |
| 修改代码并同步文档 | [开发与文档维护](docs/contributing/README.md) |
| [crates](crates/README.md) | objects、core、frontend、compiler 四个库 |
| [docs](docs/README.md) | 架构、语言运行时、嵌入契约与维护说明 |
| [scripts](scripts/README.md) | AST 生成与远端引擎验证 |
| [.gitea](.gitea/README.md) | ces-control 上的引擎 CI,无应用部署 |
| Dockerfile.ci | 远端验证镜像,不是运行时部署镜像 |
| Cargo.toml / Cargo.lock | 库工作区及锁定依赖;Rust 1.91+ |
| LICENSE / .gitignore / .dockerignore | 许可和产物排除规则 |
## 根目录内容
宿主独立维护:[endorphin-lyra-cli](https://gitea.entroia.com/Zayden-Jung/endorphin-lyra-cli) 提供命令行执行与五个 Endor 示例的实际输出报告;[Hestia Server](https://gitea.entroia.com/Zayden-Jung/endorphin-hestia-server) 提供 HTTP、管理命令、宿主 JSON、数据库适配及既有 Docker 部署;Tessera GUI 提供桌面窗口和绘制。宿主以 Git 子模块固定引擎提交,再通过 Cargo 路径依赖链接所需库。
| 文件或目录 | 职责 |
| --- | --- |
| Cargo.toml / Cargo.lock | 工作区、包依赖、Rust 版本要求及依赖锁定;包名与目录一致:core、objects、frontend、compiler、storage、host、cli、server |
| crates/ | core、objects、frontend、compiler、storage、host、cli、server 八个包,每个目录的 README 解释本目录 |
| docs/ | 跨目录的架构、运行时契约、配置、运行、部署与维护说明 |
| scripts/ | 指定远端环境中的检查、冒烟、CI 与部署脚本 |
| .gitea/ | Gitea Actions 工作流 |
| Dockerfile | source、ci、build、runtime 多阶段镜像 |
| docker-compose.yml | VM 宿主运行方案,保留原有数据卷 |
| docker-compose.postgres.yml | VM 宿主与预置 PostgreSQL |
| docker-compose.ces-control.yml | ces-control 宿主与 PostgreSQL 资源部署 |
| .env.example / .gitignore / .dockerignore | 配置样例、Git 与 Docker 上下文排除规则 |
| LICENSE | MIT 许可 |
当前已接通 reader → lexer/parser → ASDL AST → 名称分析 → wordcode → VM。EndorObject、无损 Number 表示、函数与显式引用管理已实现;数值算术、完整异常传播、可靠取消、内存配额和不可信代码沙箱仍未实现。实际入口及边界见 [文档索引](docs/README.md)。
本地不编译、不测试、不下载依赖;可做文档和静态文件检查。运行验证交给 ces-control;按用户约定不主动跟踪 CI/部署结果。命令和使用条件见 [验证](docs/operations/validation.md)。
文档随代码版本更新:就地解释放各目录 README,跨目录契约放 docs,对外阅读副本以仓库文档为准。忽略的 work、target、data、ci-output 和 Git 内部目录不属于需要维护 README 的源码目录。Endor 示例实际输出由 CI 保存为 artifact,查看方式见 [验证报告](docs/operations/validation.md)。
本地不构建或测试 Aster,不下载依赖;运行验证在 ces-control。仅保留 CI,移除旧宿主部署定义不会停止或删除现有服务、数据库、卷及生产目录。生产运维已归 Hestia;按约定不主动查询 CI/部署结果。
+8 -18
View File
@@ -1,20 +1,10 @@
# Cargo 工作区
# 引擎工作区
包名与目录一致,路径依赖定义在根 Cargo.toml。
| 目录 | 包及职责 |
| --- | --- |
| [objects](objects/README.md) | 零外部依赖;核心对象、私有堆、Number、代码和指令数据 |
| [core](core/README.md) | 库名 endor_core;仅依赖 objects,提供 VM、执行帧和求值循环 |
| [frontend](frontend/README.md) | Reader、PEG lexer/parser、ASDL AST 和编辑器接口 |
| [compiler](compiler/README.md) | Frontend AST 到 wordcode;依赖 frontend 和 core |
| 目录 | 包名/产物 | 职责 |
| --- | --- | --- |
| [core](core/README.md) | core,库 endor_core | VM、执行帧与字节码求值循环,仅依赖本地 objects |
| [objects](objects/README.md) | objects | 零依赖的核心对象、对象存储、代码对象与指令数据/静态验证 |
| [frontend](frontend/README.md) | frontend | Endor 阅读器、字符 PEG lexer、token PEG parser、ASDL 生成的 AST/遍历/JSON 与编辑器接口,不执行源码 |
| [compiler](compiler/README.md) | compiler | AST 名称/作用域分析、wordcode 模板生成和 VM 代码对象装载 |
| [host](host/README.md) | host | UTF-8 JSON 文件基础信息,与数据库分离 |
| [storage](storage/README.md) | storage | 为数据库特殊类保留的显式连接适配,不提供基础信息存储 |
| [cli](cli/README.md) | cli,二进制 cli | 引导演示、源码解析/执行与文件信息命令,不使用数据库 |
| [server](server/README.md) | server,二进制 server | HTTP 宿主与文件版本信息,不使用数据库 |
CLI 依赖 Compiler、Core、Frontend 与 Host,Server 依赖 Core 与 Host,二者不依赖 Storage;Compiler 依赖 Frontend 与 Core;Frontend 依赖 peg、serde/serde_json,不依赖 Core;Core 只依赖本地 Objects;Objects 不依赖 Core 或第三方包,二者均不依赖 Storage。未来特殊类的宿主实现按需依赖 Storage,见 [数据库对象](../docs/runtime/database-objects.md)。
Endor 的解释执行流程与平台资源版本方向见 [执行与版本模型](../docs/architecture/execution-and-versions.md)。Compiler 已连接 Frontend 与 Core 的最小源码执行链路,见 [编译契约](../docs/runtime/compiler.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)。
依赖方向为 compiler → frontend + core、core → objects;frontend 依赖 peg、serde 和 serde_json。网络、数据库、进程配置和 GUI 能力由独立宿主提供。CLI、Server、Host、Storage 已移到独立宿主仓库。返回 [项目入口](../README.md)。
-19
View File
@@ -1,19 +0,0 @@
[package]
name = "cli"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[[bin]]
name = "cli"
path = "src/main.rs"
[dependencies]
compiler.workspace = true
core.workspace = true
frontend.workspace = true
host.workspace = true
[lints]
workspace = true
-11
View File
@@ -1,11 +0,0 @@
# cli
同步命令行宿主,提供 bootstrap、parse、run、eval、help 和文件 metadata 命令。bootstrap 演示两个运行时对象并释放,不读取数据库配置、不连接或写入数据库。
| 内容 | 职责 |
| --- | --- |
| Cargo.toml | cli 二进制与 compiler/core/frontend/host 依赖;无 storage 或 Tokio 依赖 |
| [src](src/README.md) | 参数路由、引导演示、文件/stdin 阅读、源码解析/执行和退出码 |
| [tests](tests/README.md) | parse 的 AST/JSON 与 run/eval 的文件/stdin 执行、错误和退出码 |
运行方式见 [运行指南](../../docs/operations/README.md),输入边界见 [配置](../../docs/operations/configuration.md)。parse 通过 EndorReader 严格读取 UTF-8 文件或 `-` 指定的 stdin 并输出 AstRoot;--mode 选择 module/expression/interactive,--json 输出带 schema 的 AST JSON,不执行源码,见 [前端](../../docs/runtime/frontend.md);run 调用模块的无参数入口(默认 main),eval 执行独立表达式,见 [编译链路](../../docs/runtime/compiler.md);metadata put/get 和只导入缺失项的 import 均使用文件,见 [宿主信息](../../docs/operations/host-information.md)。返回 [工作区](../README.md)。
-9
View File
@@ -1,9 +0,0 @@
# CLI 入口
[main.rs](main.rs) 是二进制入口:同步 main 将 run 的结果转成进程退出码;run 路由 bootstrap、parse、源码 run/eval、help、文件 metadata put/get/import,错误打印到 stderr 并返回失败。
bootstrap 创建并显式释放两个对象,不读取数据库环境、不打开数据库。parse_command 使用 EndorReader 和 parse_source_ast 输出 AstRoot;--mode 选择 module/expression/interactive(默认 module),--json 通过 AstDocument 输出 JSON,格式见 [AST 模型](../../../docs/runtime/ast.md);`-` 从 stdin 读到 EOF,来源为 `<stdin>`。词法/语法错误显示来源、行、列;不创建 VM 或执行源码。未知命令报错;metadata 通过 host::MetadataStore 读写文件,import 从标准输入接收旧记录且不覆盖已有文件。参数目前由 std::env::args 读取,非 Unicode 参数可能 panic,边界见 [配置](../../../docs/operations/configuration.md)。
[execute.rs](execute.rs) 负责 run/eval 的文件/stdin 阅读、调用 compiler、创建 VM/globals、模块初始化与入口调用、结果格式化、traceback 和成功/失败后的根释放;命名空间由 CLI 自己创建并清空,见 [编译契约](../../../docs/runtime/compiler.md)。
新增命令时更新用法、[运行指南](../../../docs/operations/README.md) 和 [smoke.py 的导航](../../../scripts/README.md)。返回 [cli](../README.md)。
-105
View File
@@ -1,105 +0,0 @@
use std::error::Error;
use compiler::{CompileOptions, compile_source};
use endor_core::{Value, Vm};
use frontend::{EndorReader, ParseMode, SourceUnit};
pub(super) fn run_command(arguments: &[&str]) -> Result<(), Box<dyn Error>> {
let mut entry = "main";
let mut path = None;
let mut arguments = arguments.iter().copied();
while let Some(argument) = arguments.next() {
match argument {
"--entry" => {
entry = arguments
.next()
.filter(|name| !name.is_empty() && !name.starts_with("--"))
.ok_or(super::USAGE)?;
}
value if !value.starts_with("--") && path.is_none() => path = Some(value),
_ => return Err(super::USAGE.into()),
}
}
let source = read(path.ok_or(super::USAGE)?)?;
let output = execute(&source, ParseMode::Module, Some(entry))?;
println!("{output}");
Ok(())
}
pub(super) fn eval_command(path: &str) -> Result<(), Box<dyn Error>> {
let source = read(path)?;
let output = execute(&source, ParseMode::Expression, None)?;
println!("{output}");
Ok(())
}
fn read(path: &str) -> Result<SourceUnit, Box<dyn Error>> {
Ok(if path == "-" {
EndorReader::from_stream("<stdin>", std::io::stdin().lock())?
} else {
EndorReader::from_file(path)?
})
}
fn execute(
source: &SourceUnit,
mode: ParseMode,
entry: Option<&str>,
) -> Result<String, Box<dyn Error>> {
let compiled = compile_source(source, mode, &CompileOptions::default())?;
let mut vm = Vm::new()?;
let code = compiled.load(&mut vm)?;
let globals = match vm.new_dict() {
Ok(globals) => globals,
Err(error) => {
vm.release(code)?;
return Err(error.into());
}
};
let result = (|| -> Result<String, Box<dyn Error>> {
let mut value = vm.execute(code, globals)?;
if let Some(entry) = entry {
vm.release_value(value)?;
let Some(Value::Object(function)) = vm.dict_get(globals, &Value::Text(entry.into()))?
else {
return Err(format!("entry function not found: {entry}").into());
};
value = vm.call(function, &[])?;
}
let output = format_value(&vm, &value);
vm.release_value(value)?;
output
})();
// Copy traceback metadata before cleanup. This is the entry location of
// each code object plus bytecode offset, not a per-instruction source map.
let result = result.map_err(|error| {
let mut message = format!("{}: {error}", source.origin());
for location in vm.last_traceback() {
message.push_str(&format!(
"\n at {} ({}:{}, code unit {})",
location.function,
location.filename,
location.first_line,
location.instruction_offset
));
}
Box::<dyn Error>::from(message)
});
vm.release(code)?;
// CLI owns this fresh namespace. Clearing it breaks function↔globals
// cycles on both success and failure; embedders choose their own lifetime.
vm.dict_clear(globals)?;
vm.release(globals)?;
vm.collect_cycles();
result
}
fn format_value(vm: &Vm, value: &Value) -> Result<String, Box<dyn Error>> {
Ok(match value {
Value::Nil => "nil".into(),
Value::Bool(value) => value.to_string(),
Value::Text(value) => value.clone(),
Value::Number(value) => value.to_string(),
Value::Object(reference) => vm.str(*reference)?,
})
}
-110
View File
@@ -1,110 +0,0 @@
use std::error::Error;
use std::process::ExitCode;
use endor_core::{Value, Vm};
use frontend::ast::AstDocument;
use frontend::{EndorReader, ParseMode, parse_source_ast};
use host::MetadataStore;
mod execute;
const USAGE: &str = "Usage: cli [bootstrap | run [--entry name] <file|-> | eval <file|-> | parse [--mode module|expression|interactive] [--json] <file|-> | metadata put <key> <value> | metadata get <key> | metadata import | help]";
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("cli: {error}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = std::env::args().skip(1).collect();
match args
.iter()
.map(String::as_str)
.collect::<Vec<_>>()
.as_slice()
{
[] | ["bootstrap"] => {
let mut vm = Vm::new()?;
let first = vm.new_object()?;
let second = vm.new_object()?;
vm.set_attr(
first,
"message",
Value::Text("Hello from EndorObject".into()),
)?;
println!("runtime=EndorObject type_system=disabled");
println!(
"objects_distinct={} heap_objects={}",
first != second,
vm.object_count()
);
vm.release(first)?;
vm.release(second)?;
println!("released heap_objects={}", vm.object_count());
println!("bootstrap uses no database");
}
["help"] | ["--help"] | ["-h"] => println!("{USAGE}"),
["parse", arguments @ ..] => parse_command(arguments)?,
["run", arguments @ ..] => execute::run_command(arguments)?,
["eval", path] => execute::eval_command(path)?,
["metadata", "put", key, value] => {
MetadataStore::from_env()?.put(key, value)?;
println!("stored (file)");
}
["metadata", "get", key] => {
println!(
"{}",
MetadataStore::from_env()?
.get(key)?
.as_deref()
.unwrap_or("(missing)")
);
}
["metadata", "import"] => {
let count = MetadataStore::from_env()?.import_missing(std::io::stdin().lock())?;
println!("imported {count} missing entries (file)");
}
_ => return Err(USAGE.into()),
}
Ok(())
}
fn parse_command(arguments: &[&str]) -> Result<(), Box<dyn Error>> {
let mut mode = ParseMode::Module;
let mut json = false;
let mut path = None;
let mut arguments = arguments.iter().copied();
while let Some(argument) = arguments.next() {
match argument {
"--json" => json = true,
"--mode" => {
mode = match arguments.next() {
Some("module") => ParseMode::Module,
Some("expression") => ParseMode::Expression,
Some("interactive") => ParseMode::Interactive,
_ => return Err(USAGE.into()),
}
}
value if !value.starts_with("--") && path.is_none() => path = Some(value),
_ => return Err(USAGE.into()),
}
}
let path = path.ok_or(USAGE)?;
let source = if path == "-" {
EndorReader::from_stream("<stdin>", std::io::stdin().lock())?
} else {
EndorReader::from_file(path)?
};
let root = parse_source_ast(&source, mode)?;
if json {
println!("{}", AstDocument::new(root)?.to_json()?);
} else {
println!("{root:#?}");
}
Ok(())
}
-7
View File
@@ -1,7 +0,0 @@
# CLI 命令检查
[parse.rs](parse.rs) 使用 Cargo 提供的 cli 测试二进制检查 parse 命令:合法 UTF-8 源码输出 AST 且不执行名称调用;非法语法输出文件/行/列并返回失败;非法 UTF-8 不替换字符;`parse -` 检查 stdin 输入、EOF 和带流名的错误;--json/--mode 检查表达式与交互根、schema 标识以及非法模式退出。
[execute.rs](execute.rs) 检查 compiler/examples 中的真实 Endor 文件、run/eval、显式入口、stdin、运行时 traceback、名称/编译/语法/UTF-8 错误和无成功输出的失败退出。
测试仅在 ces-control 的工作区检查中运行,本地不执行。测试临时文件使用独立名称并在结束时清理。返回 [cli](../README.md),验证约定见 [验证](../../../docs/operations/validation.md)。
-136
View File
@@ -1,136 +0,0 @@
use std::io::Write;
use std::process::{Command, Output, Stdio};
fn stdin_command(arguments: &[&str], source: &[u8]) -> Output {
let mut child = Command::new(env!("CARGO_BIN_EXE_cli"))
.args(arguments)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(source).unwrap();
child.wait_with_output().unwrap()
}
#[test]
fn run_executes_real_endor_files_and_prints_entry_results() {
for (file, expected) in [
("hello.endor", "你好,Endor 🌱\n"),
("calls.endor", "函数调用已接通\n"),
("shadowing.endor", "局部名称优先\n"),
("number.endor", "-123456789012345678901234567890.00100\n"),
] {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../compiler/examples")
.join(file);
let output = Command::new(env!("CARGO_BIN_EXE_cli"))
.arg("run")
.arg(path)
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(String::from_utf8(output.stdout).unwrap(), expected);
assert!(output.stderr.is_empty());
}
}
#[test]
fn eval_and_run_read_stdin_and_support_explicit_entry_names() {
let output = stdin_command(&["eval", "-"], "\"表达式🌱\"".as_bytes());
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(String::from_utf8(output.stdout).unwrap(), "表达式🌱\n");
let output = stdin_command(
&["run", "--entry", "greet", "-"],
b"function greet(): String { return \"ok\" }",
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(String::from_utf8(output.stdout).unwrap(), "ok\n");
let path =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../compiler/examples/literal.endor");
let output = Command::new(env!("CARGO_BIN_EXE_cli"))
.arg("eval")
.arg(path)
.output()
.unwrap();
assert!(output.status.success());
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
"表达式 → 字节码 → VM\n"
);
}
#[test]
fn execution_errors_fail_without_success_output_and_report_their_stage() {
for (arguments, source, expected) in [
(
vec!["run", "-"],
"function main(): T {\n return missing\n}",
"<stdin>:2:10: undefined name: missing",
),
(
vec!["run", "-"],
"function main(): T { return f() } function f(x: T): T { return x }",
"positional arguments",
),
(
vec!["run", "-"],
"function other(): T { return 1 }",
"entry function not found: main",
),
(
vec!["eval", "-"],
"1 + 2",
"arithmetic bytecode is not implemented",
),
(vec!["eval", "-"], "1()", "does not support call"),
(
vec!["run", "-"],
"function main(): T { return ) }",
"expected",
),
] {
let output = stdin_command(&arguments, source.as_bytes());
assert!(!output.status.success());
assert!(output.stdout.is_empty());
let error = String::from_utf8(output.stderr).unwrap();
assert!(error.contains(expected), "{error}");
if expected == "positional arguments" {
assert!(error.contains("at main (<stdin>:1, code unit"));
}
}
let output = stdin_command(&["eval", "-"], &[0xff]);
assert!(!output.status.success());
assert!(
String::from_utf8(output.stderr)
.unwrap()
.contains("invalid UTF-8")
);
for arguments in [
vec!["run"],
vec!["run", "--entry"],
vec!["run", "--unknown", "-"],
vec!["eval"],
vec!["eval", "-", "extra"],
] {
let output = Command::new(env!("CARGO_BIN_EXE_cli"))
.args(arguments)
.output()
.unwrap();
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(String::from_utf8(output.stderr).unwrap().contains("Usage:"));
}
}
-139
View File
@@ -1,139 +0,0 @@
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
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());
}
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("<stdin>:2:10:")
);
let output = parse_stdin(&[0xff]);
assert!(!output.status.success());
assert!(
String::from_utf8(output.stderr)
.unwrap()
.contains("<stdin>: invalid UTF-8 at byte 0")
);
}
#[test]
fn parse_command_exports_versioned_json_for_expression_and_interactive_inputs() {
for (mode, source) in [("expression", "f(\"日\")"), ("interactive", "let x = 1")] {
let source = SourceFile::new(source.as_bytes());
let output = Command::new(env!("CARGO_BIN_EXE_cli"))
.args(["parse", "--json", "--mode", mode])
.arg(&source.0)
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let document =
frontend::ast::AstDocument::from_json(std::str::from_utf8(&output.stdout).unwrap())
.unwrap();
assert_eq!(document.schema, frontend::ast::AST_SCHEMA_ID);
match (mode, document.root) {
("expression", frontend::ast::AstRoot::Expression(_))
| ("interactive", frontend::ast::AstRoot::Interactive(_)) => {}
_ => panic!("incorrect parse mode"),
}
}
let output = Command::new(env!("CARGO_BIN_EXE_cli"))
.args(["parse", "--mode", "unknown", "-"])
.output()
.unwrap();
assert!(!output.status.success());
assert!(output.stdout.is_empty());
}
+1 -1
View File
@@ -6,7 +6,7 @@
| --- | --- |
| [Cargo.toml](Cargo.toml) | compiler 包、本地 frontend/core 依赖与工作区 lint |
| [src](src/README.md) | 分析、字节码生成与显式代码对象装载 |
| [examples](examples/README.md) | 可供 cli run/eval 的 Endor 源码文件,也是远端集成测试输入 |
| [examples](examples/README.md) | 可供 lyra run/eval 的 Endor 源码文件,也是远端集成测试输入 |
| [tests](tests/README.md) | 源码到 VM、错误路径、名称与对象生命周期检查 |
公开入口为 analyze、compile_ast、compile_source、CompileOptions 和 CompiledCode::load。完整规则与所有权见 [编译链路](../../docs/runtime/compiler.md);CLI 使用见 [运行指南](../../docs/operations/README.md)。修改语法仍进入 [frontend](../frontend/README.md),修改指令/验证进入 [objects](../objects/README.md),修改执行进入 [core](../core/README.md)。返回 [工作区](../README.md)。
+6 -6
View File
@@ -12,13 +12,13 @@
| [literal.endor](literal.endor) | eval;返回“表达式 → 字节码 → VM” |
```sh
cli run crates/compiler/examples/hello.endor
cli run crates/compiler/examples/calls.endor
cli run crates/compiler/examples/shadowing.endor
cli run crates/compiler/examples/number.endor
cli eval crates/compiler/examples/literal.endor
lyra run crates/compiler/examples/hello.endor
lyra run crates/compiler/examples/calls.endor
lyra run crates/compiler/examples/shadowing.endor
lyra run crates/compiler/examples/number.endor
lyra eval crates/compiler/examples/literal.endor
```
run 默认调用无参数 main,可用 --entry name 选择另一个无参数入口;eval 解析一个完整表达式,二者都支持 - 读取 stdin 到 EOF。类型名称暂只保留语法,Callable 不要求预先注册语言类。例子不依赖 print 或其他默认标准库。编译器尚无算术运行语义;前端 basic.endor 含算术,只是解析示例,不能混作可执行示例。规则见 [编译契约](../../../docs/runtime/compiler.md),返回 [compiler](../README.md)。
CI 使用 [endor_examples.py](../../../scripts/endor_examples.py) 再次执行真实 CLI 并保存实际结果,不只显示 Rust 测试名称。对应 Actions run 的 endor-execution-output-<sha> artifact 包含源码、命令、预期/实际 stdout、stderr、退出码和状态;位置与失败规则见 [验证报告](../../../docs/operations/validation.md)。新增 .endor 示例时同步 cases.json,未覆盖文件会让报告步骤失败。
本目录与 cases.json 继续作为引擎测试资料。独立 Lyra CLI 的 CI 使用固定 Aster 子模块中的同一份样例,实际执行 lyra run/eval 并保存 stdout/stderr、退出码及报告 artifact;报告脚本和进程入口检查位于 Lyra 仓库。Aster CI 只验证引擎,不构建 CLI。
+1 -1
View File
@@ -2,4 +2,4 @@
[pipeline.rs](pipeline.rs) 将真实 Endor 示例交给 reader/parser/名称分析/代码生成/VM,检查函数值、前向引用、局部遮蔽、精确数字常量、语义诊断、宿主全局与属性回调、对象结果保活、跨 VM 模板装载、反复调用、错误帧清理、递归限额、共享交互命名空间及超过 255 的操作数。
同时检查重复名称、局部提前读取、未声明名称、不支持的执行特性和不合法 AST 原文范围。CLI 的文件/stdin 与退出码检查见 [cli/tests](../../cli/tests/README.md)。源码说明验证意图;仅由 ces-control 的 cargo test --workspace --locked 执行,本地不运行,结果边界见 [验证](../../../docs/operations/validation.md)。返回 [compiler](../README.md)。
同时检查重复名称、局部提前读取、未声明名称、不支持的执行特性和不合法 AST 原文范围。CLI 的文件/stdin 与退出码检查见 [cli/tests](https://gitea.entroia.com/Zayden-Jung/endorphin-lyra-cli/src/branch/main/crates/cli/tests/README.md)。源码说明验证意图;仅由 ces-control 的 cargo test --workspace --locked 执行,本地不运行,结果边界见 [验证](../../../docs/operations/validation.md)。返回 [compiler](../README.md)。
+2 -2
View File
@@ -9,6 +9,6 @@
| [editor](editor/README.md) | 同源生成的 TypeScript 类型/遍历器和 JSON Schema |
| [src](src/README.md) | 阅读器、SourceUnit、token、源码范围、诊断、AST、词法规则独立在 [lexer.peg](src/lexer.peg),语法规则在 [grammar.peg](src/grammar.peg) |
| [tests](tests/README.md) | Unicode、优先级、函数、完整输入与错误位置验证 |
| [examples](examples/README.md) | 可交给 cli parse 的源码样例 |
| [examples](examples/README.md) | 可交给 lyra parse 的源码样例 |
宿主通过 EndorReader 读取字符串、流、文件或注入的 URI provider,取得 SourceUnit 后调用 lex_source、parse_source、parse_source_expression;统一入口 parse_ast/parse_source_ast 通过 ParseMode 返回 AstRoot;低层 lex、parse、parse_expression 仍保留。AST 定义、生成命令、Visitor 与 JSON 交换见 [AST 模型](../../docs/runtime/ast.md)。读取契约见 [Endor 阅读器](../../docs/runtime/reader.md)。Objects 保持零依赖,Core 仅依赖本地 Objects;后续把 AST 转成已有字节码并交给 VM 解释执行;这是平台内部准备步骤,尚未接通。资源修订、依赖锁和解释器统一升级的设计方向见 [执行与版本模型](../../docs/architecture/execution-and-versions.md)。返回 [工作区](../README.md)。
宿主通过 EndorReader 读取字符串、流、文件或注入的 URI provider,取得 SourceUnit 后调用 lex_source、parse_source、parse_source_expression;统一入口 parse_ast/parse_source_ast 通过 ParseMode 返回 AstRoot;低层 lex、parse、parse_expression 仍保留。AST 定义、生成命令、Visitor 与 JSON 交换见 [AST 模型](../../docs/runtime/ast.md)。读取契约见 [Endor 阅读器](../../docs/runtime/reader.md)。Objects 保持零依赖,Core 仅依赖本地 Objects;独立 Compiler 已将 AST 转成 wordcode 并交给 VM 执行;这是平台内部准备步骤。资源修订、依赖锁和解释器统一升级的设计方向见 [执行与版本模型](../../docs/architecture/execution-and-versions.md)。返回 [工作区](../README.md)。
+1 -1
View File
@@ -2,4 +2,4 @@
[basic.endor](basic.endor) 包含字符串函数、位置参数、let 绑定、return 与算术表达式。它展示当前能解析的源码,不表示已能编译和执行运算。
在指定远端环境使用 `cli parse crates/frontend/examples/basic.endor` 输出 AST;工作区运行方法见 [运行指南](../../../docs/operations/README.md)。syntax.rs 包含此样例的解析检查。返回 [frontend](../README.md)。
在指定远端环境使用 `lyra parse crates/frontend/examples/basic.endor` 输出 AST;工作区运行方法见 [运行指南](../../../docs/operations/README.md)。syntax.rs 包含此样例的解析检查。返回 [frontend](../README.md)。
-16
View File
@@ -1,16 +0,0 @@
[package]
name = "host"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
serde.workspace = true
serde_json.workspace = true
[dev-dependencies]
tempfile = "3"
[lints]
workspace = true
-11
View File
@@ -1,11 +0,0 @@
# host
宿主基础信息的文件层,与 Core 和数据库 Storage 分开。CLI/Server 使用它保存 host.version、deployment.release 和其他命名信息;不打开数据库,不保存 VM 堆。
| 内容 | 职责 |
| --- | --- |
| Cargo.toml | host 包、Serde/JSON 依赖及远端检查依赖 |
| [src](src/README.md) | MetadataStore、UTF-8 JSON 文件与迁移导入 |
| [tests](tests/README.md) | 持久读取、替换、并发、坏记录、路径边界和幂等导入 |
ASTER_STATE_DIR 指定信息目录;默认 data/host,容器使用 /data/host。协议、生命周期及旧数据库一次性迁移见 [宿主信息](../../docs/operations/host-information.md)。返回 [工作区](../README.md)。
-8
View File
@@ -1,8 +0,0 @@
# Host 源码导航
| 文件 | 职责 |
| --- | --- |
| [lib.rs](lib.rs) | 导出 MetadataStore,明确文件层不依赖 VM 或 SQL |
| [metadata.rs](metadata.rs) | 目录配置、键校验、逐键 JSON 读取/同步写入、原子发布、仅导入缺失项 |
文件数据以 UTF-8 JSON 保存,操作系统路径用 PathBuf;坏记录不自动覆盖。每个键独立发布,不提供多键事务。旧库导入不覆盖新文件,规则见 [宿主信息](../../../docs/operations/host-information.md)。返回 [host](../README.md)。
-4
View File
@@ -1,4 +0,0 @@
//! Host information stored in UTF-8 JSON files, independently of the VM and SQL.
mod metadata;
pub use metadata::MetadataStore;
-172
View File
@@ -1,172 +0,0 @@
use std::fs::{self, OpenOptions};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use serde::{Deserialize, Serialize};
static NEXT_TEMP: AtomicU64 = AtomicU64::new(0);
const FORMAT_VERSION: u32 = 1;
#[derive(Deserialize)]
struct LegacyRecord {
key: String,
value: String,
}
#[derive(Serialize, Deserialize)]
struct Record {
format_version: u32,
key: String,
value: String,
}
/// One independently replaceable JSON file per ASCII metadata key. Clones
/// share a directory, not a cached map; reads always observe the file system.
#[derive(Debug, Clone)]
pub struct MetadataStore {
directory: PathBuf,
}
impl MetadataStore {
/// Does not create the directory until the first write.
pub fn new(directory: impl Into<PathBuf>) -> io::Result<Self> {
let directory = directory.into();
if directory.as_os_str().is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"metadata directory is empty",
));
}
Ok(Self { directory })
}
/// Preserve OS paths without requiring the environment value to be UTF-8.
pub fn from_env() -> io::Result<Self> {
Self::new(
std::env::var_os("ASTER_STATE_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("data/host")),
)
}
pub fn directory(&self) -> &Path {
&self.directory
}
pub fn get(&self, key: &str) -> io::Result<Option<String>> {
let path = self.path(key)?;
let bytes = match fs::read(path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error),
};
let record: Record = serde_json::from_slice(&bytes)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
if record.format_version != FORMAT_VERSION || record.key != key {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"metadata key or format version mismatch",
));
}
Ok(Some(record.value))
}
/// Writes a new sibling file, syncs it, then publishes it with rename.
/// Different keys do not share a read/modify/write map. For the same key,
/// the last successful rename wins. This is not a multi-key transaction.
pub fn put(&self, key: &str, value: &str) -> io::Result<()> {
self.store(key, value, true).map(|_| ())
}
/// One-time migration input: a JSON array of key/value string records.
/// Validate all keys first, and never overwrite an existing file record.
pub fn import_missing(&self, reader: impl Read) -> io::Result<usize> {
let records: Vec<LegacyRecord> = serde_json::from_reader(reader)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
for record in &records {
self.path(&record.key)?;
}
let mut imported = 0;
for record in records {
if self.store(&record.key, &record.value, false)? {
imported += 1;
} else if self.get(&record.key)?.is_none() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"existing metadata disappeared during import",
));
}
}
Ok(imported)
}
fn store(&self, key: &str, value: &str, replace: bool) -> io::Result<bool> {
let destination = self.path(key)?;
let mut contents = serde_json::to_vec_pretty(&Record {
format_version: FORMAT_VERSION,
key: key.into(),
value: value.into(),
})
.map_err(io::Error::other)?;
contents.push(b'\n');
fs::create_dir_all(&self.directory)?;
for _ in 0..32 {
let sequence = NEXT_TEMP.fetch_add(1, Ordering::Relaxed);
let temporary = self
.directory
.join(format!(".{key}.{}.{sequence}.tmp", std::process::id()));
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = match options.open(&temporary) {
Ok(file) => file,
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
Err(error) => return Err(error),
};
let result = (|| {
file.write_all(&contents)?;
file.sync_all()?;
drop(file);
if replace {
fs::rename(&temporary, &destination)?;
Ok(true)
} else {
// Atomic create-if-absent: a concurrent writer's record
// cannot be replaced by the legacy import.
match fs::hard_link(&temporary, &destination) {
Ok(()) => Ok(true),
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => Ok(false),
Err(error) => Err(error),
}
}
})();
let _ = fs::remove_file(&temporary);
return result;
}
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"cannot reserve a metadata temporary file",
))
}
fn path(&self, key: &str) -> io::Result<PathBuf> {
if key.is_empty()
|| key.len() > 128
|| !key.as_bytes()[0].is_ascii_alphanumeric()
|| !key
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"metadata keys must start with an ASCII letter/digit and contain at most 128 ASCII letters, digits, dots, underscores or hyphens",
));
}
Ok(self.directory.join(format!("{key}.json")))
}
}
-5
View File
@@ -1,5 +0,0 @@
# Host 文件信息检查
[metadata.rs](metadata.rs) 检查缺失读取不建目录、日文/中文/emoji 值和日文路径、重开与同键替换、非法键不越界、坏 JSON/版本/键不匹配不改原文件、并发替换可见完整记录,以及旧记录幂等导入和保留更新值。
检查使用临时目录,仅在指定远端环境运行。本地不执行文件行为测试;当前是否通过应以对应提交的 CI 为准。命令见 [验证](../../../docs/operations/validation.md),返回 [host](../README.md)。
-131
View File
@@ -1,131 +0,0 @@
use host::MetadataStore;
#[test]
fn missing_reads_do_not_create_state() {
let temporary = tempfile::tempdir().unwrap();
let directory = temporary.path().join("state");
let store = MetadataStore::new(&directory).unwrap();
assert_eq!(store.get("missing").unwrap(), None);
assert!(!directory.exists());
}
#[test]
fn utf8_values_survive_reopening_and_same_key_replacement() {
let temporary = tempfile::tempdir().unwrap();
let directory = temporary.path().join("日本語");
let store = MetadataStore::new(&directory).unwrap();
store.put("host.version", "0.1.0").unwrap();
store.put("project.note", "日本語・中文・😀").unwrap();
let reopened = MetadataStore::new(&directory).unwrap();
assert_eq!(
reopened.get("project.note").unwrap().as_deref(),
Some("日本語・中文・😀")
);
reopened.put("host.version", "0.2.0").unwrap();
assert_eq!(store.get("host.version").unwrap().as_deref(), Some("0.2.0"));
assert_eq!(
store.get("project.note").unwrap().as_deref(),
Some("日本語・中文・😀")
);
let record: serde_json::Value =
serde_json::from_slice(&std::fs::read(directory.join("project.note.json")).unwrap())
.unwrap();
assert_eq!(record["format_version"], 1);
assert_eq!(record["key"], "project.note");
}
#[test]
fn keys_cannot_escape_the_state_directory() {
let temporary = tempfile::tempdir().unwrap();
let directory = temporary.path().join("state");
let store = MetadataStore::new(&directory).unwrap();
for key in [
"",
"../outside",
"/outside",
"nested/key",
"nested\\key",
".hidden",
"日本語",
] {
assert!(store.put(key, "invalid").is_err(), "{key}");
assert!(store.get(key).is_err(), "{key}");
}
assert!(store.put(&"x".repeat(129), "invalid").is_err());
assert!(!directory.exists());
}
#[test]
fn corrupt_or_mismatched_records_fail_without_being_rewritten() {
let temporary = tempfile::tempdir().unwrap();
let store = MetadataStore::new(temporary.path()).unwrap();
let path = temporary.path().join("host.version.json");
for bytes in [
b"not-json".as_slice(),
b"\xff".as_slice(),
br#"{"format_version":2,"key":"host.version","value":"old"}"#.as_slice(),
br#"{"format_version":1,"key":"wrong","value":"old"}"#.as_slice(),
] {
std::fs::write(&path, bytes).unwrap();
assert!(store.get("host.version").is_err());
assert_eq!(std::fs::read(&path).unwrap(), bytes);
}
}
#[test]
fn concurrent_replacements_expose_complete_records_and_preserve_other_keys() {
let temporary = tempfile::tempdir().unwrap();
let store = MetadataStore::new(temporary.path()).unwrap();
store.put("independent", "keep").unwrap();
let a = "a".repeat(8192);
let b = "b".repeat(8192);
store.put("shared", &a).unwrap();
std::thread::scope(|scope| {
for value in [&a, &b] {
let store = &store;
scope.spawn(move || {
for _ in 0..8 {
store.put("shared", value).unwrap();
}
});
}
for _ in 0..64 {
let value = store.get("shared").unwrap().unwrap();
assert!(value == a || value == b);
}
});
assert_eq!(store.get("independent").unwrap().as_deref(), Some("keep"));
assert!(std::fs::read_dir(temporary.path()).unwrap().all(|entry| {
!entry
.unwrap()
.file_name()
.to_string_lossy()
.ends_with(".tmp")
}));
}
#[test]
fn legacy_import_is_repeatable_and_preserves_newer_file_values() {
let temporary = tempfile::tempdir().unwrap();
let store = MetadataStore::new(temporary.path()).unwrap();
store.put("host.version", "new").unwrap();
let input =
br#"[{"key":"host.version","value":"old"},{"key":"project.note","value":"legacy"}]"#;
assert_eq!(store.import_missing(input.as_slice()).unwrap(), 1);
assert_eq!(store.import_missing(input.as_slice()).unwrap(), 0);
assert_eq!(store.get("host.version").unwrap().as_deref(), Some("new"));
assert_eq!(
store.get("project.note").unwrap().as_deref(),
Some("legacy")
);
}
#[test]
fn invalid_import_keys_are_rejected_before_any_record_is_written() {
let temporary = tempfile::tempdir().unwrap();
let directory = temporary.path().join("state");
let store = MetadataStore::new(&directory).unwrap();
let input = br#"[{"key":"valid","value":"keep"},{"key":"../outside","value":"invalid"}]"#;
assert!(store.import_missing(input.as_slice()).is_err());
assert!(!directory.exists());
}
-16
View File
@@ -1,16 +0,0 @@
[package]
name = "server"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
core.workspace = true
host.workspace = true
axum.workspace = true
serde.workspace = true
tokio.workspace = true
[lints]
workspace = true
-10
View File
@@ -1,10 +0,0 @@
# server
最小常驻 HTTP 宿主,提供存活检查和只读 VM 信息。通过 Arc 共享 Vm,启动不读取数据库配置、不连接或写入数据库;通过 host 包把 host.version 写入文件。特殊类的数据库能力尚未接入 HTTP 宿主。
| 内容 | 职责 |
| --- | --- |
| Cargo.toml | core/host、Axum、Tokio、Serde 依赖;无 storage 依赖 |
| [src](src/README.md) | AppState、启动、路由、信号与退出 |
HTTP 返回内容见 [运行指南](../../docs/operations/README.md),监听变量见 [配置](../../docs/operations/configuration.md),预置 PostgreSQL 资源见 [部署](../../docs/operations/deployment.md)。没有语言执行接口或可变 VM 请求上下文。返回 [工作区](../README.md)。
-13
View File
@@ -1,13 +0,0 @@
# Server 入口
[main.rs](main.rs) 包含所有宿主启动代码:
| 部分 | 职责 |
| --- | --- |
| AppState | 共享只读 Arc<Vm> |
| main/run | 创建空 VM、绑定地址、设置退出信号、写文件 host.version、组装路由;不创建数据库连接 |
| health | 返回 200 和 status=ok,仅检查 HTTP 宿主可响应 |
| bootstrap | 返回对象模型、type_system=false 和堆对象数量,不分配对象 |
| shutdown_signal | Ctrl-C;Unix 上同时接受 SIGTERM,触发 Axum 优雅退出 |
/health 不表示 PostgreSQL、特殊类查询或执行脚本正常;数据库检查应属于实际消费连接的对象。宿主独立于数据库的行为由远端 smoke.py 验证,见 [验证](../../../docs/operations/validation.md)。改变端点同步 [运行指南](../../../docs/operations/README.md) 和 [scripts](../../../scripts/README.md)。返回 [server](../README.md)。
-77
View File
@@ -1,77 +0,0 @@
use std::error::Error;
use std::future::Future;
use std::process::ExitCode;
use std::sync::Arc;
use axum::{Json, Router, extract::State, routing::get};
use endor_core::Vm;
use host::MetadataStore;
use serde::Serialize;
#[derive(Clone)]
struct AppState {
vm: Arc<Vm>,
}
#[derive(Serialize)]
struct Health {
status: &'static str,
}
#[derive(Serialize)]
struct BootstrapInfo {
object_model: &'static str,
type_system: bool,
heap_objects: usize,
}
#[tokio::main]
async fn main() -> ExitCode {
match run().await {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("server: {error}");
ExitCode::FAILURE
}
}
}
async fn run() -> Result<(), Box<dyn Error>> {
let vm = Arc::new(Vm::new()?);
let address = std::env::var("ASTER_LISTEN").unwrap_or_else(|_| "127.0.0.1:8080".into());
let listener = tokio::net::TcpListener::bind(&address).await?;
let shutdown = shutdown_signal()?;
MetadataStore::from_env()?.put("host.version", env!("CARGO_PKG_VERSION"))?;
let app = Router::new()
.route("/health", get(health))
.route("/bootstrap", get(bootstrap))
.with_state(AppState { vm });
println!("Endor host listening on {}", listener.local_addr()?);
axum::serve(listener, app)
.with_graceful_shutdown(shutdown)
.await?;
Ok(())
}
async fn health() -> Json<Health> {
Json(Health { status: "ok" })
}
async fn bootstrap(State(state): State<AppState>) -> Json<BootstrapInfo> {
Json(BootstrapInfo {
object_model: "EndorObject",
type_system: false,
heap_objects: state.vm.object_count(),
})
}
fn shutdown_signal() -> std::io::Result<impl Future<Output = ()>> {
#[cfg(unix)]
let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
Ok(async move {
#[cfg(unix)]
tokio::select! { _ = tokio::signal::ctrl_c() => {}, _ = terminate.recv() => {} }
#[cfg(not(unix))]
let _ = tokio::signal::ctrl_c().await;
})
}
-16
View File
@@ -1,16 +0,0 @@
[package]
name = "storage"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
sqlx.workspace = true
tokio.workspace = true
[dev-dependencies]
tempfile = "3"
[lints]
workspace = true
-11
View File
@@ -1,11 +0,0 @@
# storage
按需数据库连接适配层,供将来需要数据库的特殊类或宿主适配器使用。依赖 SQLx/Tokio,不依赖 VM 内核;普通对象、CLI 和 Server 不连接这层。
| 内容 | 入口 |
| --- | --- |
| Cargo.toml | SQLx、Tokio 与远端检查依赖 |
| [src](src/README.md) | 显式 DatabaseConfig、连接池与关闭 |
| [tests](tests/README.md) | 连接不建表、调用方 SQL、隔离/持久化、真实 PostgreSQL |
没有默认数据库、自动环境变量读取、基础信息表或通用元数据 put/get。特殊类的名称、布局与查询接口尚未定义;当前只保留连接能力。设计边界见 [数据库对象](../../docs/runtime/database-objects.md),参数见 [配置](../../docs/operations/configuration.md)。返回 [工作区](../README.md)。
-7
View File
@@ -1,7 +0,0 @@
# 数据库连接源码导航
[lib.rs](lib.rs) 是唯一实现文件:DatabaseConfig 显式选择 SQLite 文件/内存或 PostgreSQL;DatabaseConnection 提供 connect、sqlite_pool/postgres_pool、backend_name、ping 和 close;DatabaseError 对公开错误脱敏。
connect 只建立连接池,不执行应用 DDL,不保存基础信息。没有 schema.sql、初始化表、put/get 或 from_env。业务表和查询由未来的数据库特殊类决定;本层不替它定义数据模型。
克隆共享连接池,close 会关闭所有克隆;具体生命周期及限制见 [数据库对象](../../../docs/runtime/database-objects.md)。配置变动同步 [配置](../../../docs/operations/configuration.md) 和 [tests](../tests/README.md)。返回 [storage](../README.md)。
-181
View File
@@ -1,181 +0,0 @@
//! Explicit database connections for database-backed objects.
//! Connecting never creates application tables or stores VM/host metadata.
use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions};
use sqlx::{PgPool, SqlitePool};
/// An explicit PostgreSQL configuration never falls back to SQLite.
#[derive(Clone)]
pub enum DatabaseConfig {
SqliteFile(PathBuf),
SqliteMemory,
Postgres(String),
}
impl fmt::Debug for DatabaseConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SqliteFile(path) => f.debug_tuple("SqliteFile").field(path).finish(),
Self::SqliteMemory => f.write_str("SqliteMemory"),
Self::Postgres(_) => f.write_str("Postgres([REDACTED])"),
}
}
}
#[derive(Debug)]
pub enum DatabaseError {
Config(&'static str),
Directory(std::io::Error),
Database(sqlx::Error),
}
impl fmt::Display for DatabaseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Config(message) => f.write_str(message),
Self::Directory(_) => f.write_str("cannot create SQLite parent directory"),
// CLI/HTTP logs must never print a connection URL or credentials.
Self::Database(_) => {
f.write_str("database operation failed; no fallback was attempted")
}
}
}
}
impl std::error::Error for DatabaseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Config(_) => None,
Self::Directory(error) => Some(error),
Self::Database(error) => Some(error),
}
}
}
impl From<sqlx::Error> for DatabaseError {
fn from(error: sqlx::Error) -> Self {
Self::Database(error)
}
}
#[derive(Clone)]
enum Backend {
Sqlite(SqlitePool),
Postgres(PgPool),
}
/// Created only on explicit request by an object or host adapter. Clones share
/// a pool; close() closes it for all users. There is no automatic schema or data.
/// In-memory SQLite uses one connection to preserve its database lifetime.
#[derive(Clone)]
pub struct DatabaseConnection {
backend: Backend,
}
impl DatabaseConnection {
/// Connects eagerly to the explicit backend without executing application DDL.
pub async fn connect(config: &DatabaseConfig) -> Result<Self, DatabaseError> {
let backend = match config {
DatabaseConfig::Postgres(url) => {
if !(url.starts_with("postgres://") || url.starts_with("postgresql://")) {
return Err(DatabaseError::Config(
"expected a PostgreSQL connection URL",
));
}
let options = PgConnectOptions::from_str(url)
.map_err(|_| DatabaseError::Config("invalid PostgreSQL connection URL"))?;
Backend::Postgres(
PgPoolOptions::new()
.max_connections(5)
.acquire_timeout(Duration::from_secs(5))
.connect_with(options)
.await?,
)
}
DatabaseConfig::SqliteFile(path) => {
if path.as_os_str().is_empty() || path == std::path::Path::new(":memory:") {
return Err(DatabaseError::Config(
"expected a nonempty file path; use SqliteMemory for :memory:",
));
}
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
tokio::fs::create_dir_all(parent)
.await
.map_err(DatabaseError::Directory)?;
}
let options = SqliteConnectOptions::new()
.filename(path)
.create_if_missing(true)
.journal_mode(SqliteJournalMode::Wal)
.busy_timeout(Duration::from_secs(5));
Backend::Sqlite(
SqlitePoolOptions::new()
.max_connections(5)
.acquire_timeout(Duration::from_secs(5))
.connect_with(options)
.await?,
)
}
DatabaseConfig::SqliteMemory => {
let options = SqliteConnectOptions::new().in_memory(true);
Backend::Sqlite(
SqlitePoolOptions::new()
.max_connections(1)
.min_connections(1)
.idle_timeout(None)
.max_lifetime(None)
.connect_with(options)
.await?,
)
}
};
Ok(Self { backend })
}
/// Backend-specific access for the database-backed object implementation.
pub fn sqlite_pool(&self) -> Option<&SqlitePool> {
match &self.backend {
Backend::Sqlite(pool) => Some(pool),
Backend::Postgres(_) => None,
}
}
pub fn postgres_pool(&self) -> Option<&PgPool> {
match &self.backend {
Backend::Postgres(pool) => Some(pool),
Backend::Sqlite(_) => None,
}
}
pub fn backend_name(&self) -> &'static str {
match &self.backend {
Backend::Sqlite(_) => "sqlite",
Backend::Postgres(_) => "postgresql",
}
}
pub async fn ping(&self) -> Result<(), DatabaseError> {
match &self.backend {
Backend::Sqlite(pool) => {
sqlx::query("SELECT 1").execute(pool).await?;
}
Backend::Postgres(pool) => {
sqlx::query("SELECT 1").execute(pool).await?;
}
}
Ok(())
}
/// Explicitly closes the shared pool, including all DatabaseConnection clones.
pub async fn close(&self) {
match &self.backend {
Backend::Sqlite(pool) => pool.close().await,
Backend::Postgres(pool) => pool.close().await,
}
}
}
-8
View File
@@ -1,8 +0,0 @@
# 数据库连接检查
| 文件 | 覆盖与前提 |
| --- | --- |
| [storage.rs](storage.rs) | 显式错误配置、凭据脱敏、SQLite 连接后无应用表、克隆共享与独立内存隔离、调用方自建数据跨文件重连、目录错误、PostgreSQL 失败不降级 |
| [postgres.rs](postgres.rs) | 真实连接后 public 无表;测试在事务中创建临时 class_rows、参数绑定和读取,提交自动删除临时表,再连接;默认 ignore,需全新专用 ASTER_TEST_POSTGRES_URL |
所有业务表都由测试自己创建,连接层不得自动初始化基础信息表。临时 SQLite 文件和 PostgreSQL 测试库由远端 CI 隔离,不使用生产数据。检查命令见 [验证](../../../docs/operations/validation.md),原生连接能力边界见 [数据库对象](../../../docs/runtime/database-objects.md)。本地不运行测试或下载依赖。返回 [storage](../README.md)。
-53
View File
@@ -1,53 +0,0 @@
use storage::{DatabaseConfig, DatabaseConnection};
/// Run against a fresh disposable database, never a production or shared schema.
#[tokio::test]
#[ignore = "requires ASTER_TEST_POSTGRES_URL pointing to a fresh disposable PostgreSQL database"]
async fn postgres_connects_without_schema_and_executes_caller_owned_sql() {
let url = std::env::var("ASTER_TEST_POSTGRES_URL")
.expect("set ASTER_TEST_POSTGRES_URL to a fresh disposable PostgreSQL database");
let config = DatabaseConfig::Postgres(url);
let database = DatabaseConnection::connect(&config).await.unwrap();
assert_eq!(database.backend_name(), "postgresql");
assert!(database.sqlite_pool().is_none());
database.ping().await.unwrap();
let pool = database.postgres_pool().unwrap();
let tables: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public'",
)
.fetch_one(pool)
.await
.unwrap();
assert_eq!(tables, 0, "connecting must not create application tables");
let mut transaction = pool.begin().await.unwrap();
sqlx::query(
"CREATE TEMP TABLE class_rows (id INTEGER PRIMARY KEY, value TEXT NOT NULL) ON COMMIT DROP",
)
.execute(&mut *transaction)
.await
.unwrap();
sqlx::query("INSERT INTO class_rows (id, value) VALUES ($1, $2)")
.bind(1_i64)
.bind("你好 ' ; $1")
.execute(&mut *transaction)
.await
.unwrap();
let value: String = sqlx::query_scalar("SELECT value FROM class_rows WHERE id = $1")
.bind(1_i64)
.fetch_one(&mut *transaction)
.await
.unwrap();
assert_eq!(value, "你好 ' ; $1");
transaction.commit().await.unwrap();
database.close().await;
let reopened = DatabaseConnection::connect(&config).await.unwrap();
reopened.ping().await.unwrap();
let tables: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public'",
)
.fetch_one(reopened.postgres_pool().unwrap())
.await
.unwrap();
assert_eq!(tables, 0);
reopened.close().await;
}
-130
View File
@@ -1,130 +0,0 @@
use storage::{DatabaseConfig, DatabaseConnection};
#[test]
fn postgres_configuration_debug_redacts_credentials() {
let config = DatabaseConfig::Postgres("postgres://user:secret@localhost/db".into());
assert!(!format!("{config:?}").contains("secret"));
}
#[tokio::test]
async fn explicit_invalid_backends_fail_without_fallback() {
for config in [
DatabaseConfig::Postgres(String::new()),
DatabaseConfig::Postgres("mysql://localhost/db".into()),
DatabaseConfig::SqliteFile("".into()),
DatabaseConfig::SqliteFile(":memory:".into()),
] {
assert!(DatabaseConnection::connect(&config).await.is_err());
}
}
#[tokio::test]
async fn memory_connections_start_without_tables_and_clones_share_the_pool() {
let database = DatabaseConnection::connect(&DatabaseConfig::SqliteMemory)
.await
.unwrap();
let other = DatabaseConnection::connect(&DatabaseConfig::SqliteMemory)
.await
.unwrap();
assert!(database.postgres_pool().is_none());
let pool = database.sqlite_pool().unwrap();
let tables: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table'")
.fetch_one(pool)
.await
.unwrap();
assert_eq!(tables, 0, "connecting must not create application tables");
// This schema belongs to the test's simulated database-backed object.
sqlx::query("CREATE TABLE class_rows (id INTEGER PRIMARY KEY, value TEXT NOT NULL)")
.execute(pool)
.await
.unwrap();
sqlx::query("INSERT INTO class_rows (id, value) VALUES (?, ?)")
.bind(1_i64)
.bind("你好 ' ? $1 ;")
.execute(pool)
.await
.unwrap();
let cloned = database.clone();
let value: String = sqlx::query_scalar("SELECT value FROM class_rows WHERE id = ?")
.bind(1_i64)
.fetch_one(cloned.sqlite_pool().unwrap())
.await
.unwrap();
assert_eq!(value, "你好 ' ? $1 ;");
let other_tables: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table'")
.fetch_one(other.sqlite_pool().unwrap())
.await
.unwrap();
assert_eq!(other_tables, 0);
database.close().await;
assert!(cloned.ping().await.is_err());
other.close().await;
}
#[tokio::test]
async fn explicit_sqlite_file_preserves_only_caller_created_data_across_reconnects() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nested/data/class.db");
let config = DatabaseConfig::SqliteFile(path.clone());
let database = DatabaseConnection::connect(&config).await.unwrap();
let pool = database.sqlite_pool().unwrap();
let tables: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table'")
.fetch_one(pool)
.await
.unwrap();
assert_eq!(tables, 0);
sqlx::query("CREATE TABLE class_rows (id INTEGER PRIMARY KEY, value TEXT NOT NULL)")
.execute(pool)
.await
.unwrap();
sqlx::query("INSERT INTO class_rows (id, value) VALUES (?, ?)")
.bind(1_i64)
.bind("caller-owned data")
.execute(pool)
.await
.unwrap();
database.close().await;
assert!(path.is_file());
let reopened = DatabaseConnection::connect(&config).await.unwrap();
let value: String = sqlx::query_scalar("SELECT value FROM class_rows WHERE id = ?")
.bind(1_i64)
.fetch_one(reopened.sqlite_pool().unwrap())
.await
.unwrap();
assert_eq!(value, "caller-owned data");
let tables: Vec<String> =
sqlx::query_scalar("SELECT name FROM sqlite_master WHERE type = 'table'")
.fetch_all(reopened.sqlite_pool().unwrap())
.await
.unwrap();
assert_eq!(tables, vec!["class_rows"]);
reopened.close().await;
}
#[tokio::test]
async fn invalid_sqlite_directory_reports_error() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("file");
std::fs::write(&file, "occupied").unwrap();
assert!(
DatabaseConnection::connect(&DatabaseConfig::SqliteFile(file.join("class.db")))
.await
.is_err()
);
}
#[tokio::test]
async fn postgres_connection_failure_is_an_error() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let url = format!(
"postgres://aster:secret@{}/aster?sslmode=disable",
listener.local_addr().unwrap()
);
let error = match DatabaseConnection::connect(&DatabaseConfig::Postgres(url)).await {
Ok(_) => panic!("unreachable PostgreSQL must fail"),
Err(error) => error,
};
assert!(!error.to_string().contains("secret"));
assert!(error.to_string().contains("no fallback"));
}
-41
View File
@@ -1,41 +0,0 @@
name: endorphin-aster-vm
services:
aster:
image: ${ASTER_IMAGE:?set ASTER_IMAGE to the verified release image}
init: true
ports:
- "127.0.0.1:${ASTER_PORT:-18081}:8080"
environment:
ASTER_LISTEN: 0.0.0.0:8080
ASTER_STATE_DIR: /data/host
volumes:
- /var/lib/endorphin-aster-vm/host:/data/host
restart: unless-stopped
stop_grace_period: 20s
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
postgres:
image: postgres:17-bookworm
environment:
POSTGRES_DB: aster
POSTGRES_USER: aster
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
volumes:
- /var/lib/endorphin-aster-vm/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U aster -d aster"]
interval: 5s
timeout: 3s
retries: 20
restart: unless-stopped
stop_grace_period: 30s
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
-31
View File
@@ -1,31 +0,0 @@
# Standalone alternative: docker compose -f docker-compose.postgres.yml up --build -d
services:
aster:
build: .
ports:
- "127.0.0.1:${ASTER_PORT:-8080}:8080"
environment:
ASTER_LISTEN: 0.0.0.0:8080
ASTER_STATE_DIR: /data/host
volumes:
- aster-host:/data/host
restart: unless-stopped
postgres:
image: postgres:17-bookworm
environment:
POSTGRES_DB: aster
POSTGRES_USER: aster
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
volumes:
- aster-postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U aster -d aster"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
volumes:
aster-host:
aster-postgres:
-14
View File
@@ -1,14 +0,0 @@
services:
aster:
build: .
ports:
- "127.0.0.1:${ASTER_PORT:-8080}:8080"
environment:
ASTER_LISTEN: 0.0.0.0:8080
ASTER_STATE_DIR: /data/host
volumes:
- aster-sqlite:/data
restart: unless-stopped
volumes:
aster-sqlite:
+18 -45
View File
@@ -1,49 +1,22 @@
# 文档索引
这里保存与源码一起版本管理的全局说明。当前页面负责找入口;局部文件职责由对应代码目录 README 维护。阅读次序:系统架构 → 运行时主题 → 对应源码目录。运行部署则从 operations 进入。
## 按问题查找
| 想知道什么 | 权威说明 | 代码入口 |
| 问题 | 权威说明 | 代码 |
| --- | --- | --- |
| 各个 crate 如何分层,哪些能力已实现 | [架构](architecture/README.md) | [crates](../crates/README.md) |
| 解释型为何生成字节码,资源修订与平台升级如何区分 | [执行与版本模型(设计方向)](architecture/execution-and-versions.md) | [frontend](../crates/frontend/README.md)、[core](../crates/core/README.md);最小源码执行已接入,平台资源版本管理未实现 |
| EndorObject、对象头、句柄、默认操作 | [对象模型](runtime/objects.md) | [objects/src](../crates/objects/src/README.md) |
| Number、String、Dict、Cell、Function、异常数据 | [基本对象](runtime/builtins.md) | [objects/src](../crates/objects/src/README.md) |
| 字符串、文件、流与 endor URI 如何进入 lexer | [Endor 阅读器](runtime/reader.md) | [reader.rs](../crates/frontend/src/reader.rs)、[source.rs](../crates/frontend/src/source.rs) |
| 源码如何分词、PEG 规则、AST 和诊断 | [源码前端](runtime/frontend.md) | [lexer.peg](../crates/frontend/src/lexer.peg)、[grammar.peg](../crates/frontend/src/grammar.peg)、[frontend](../crates/frontend/README.md)、[CLI](../crates/cli/README.md) |
| AST 根/声明/语句、ASDL 生成、遍历、位置与 JSON/编辑器接口 | [AST 模型](runtime/ast.md) | [Endor.asdl](../crates/frontend/Endor.asdl)、[AST 实现](../crates/frontend/src/ast/README.md)、[editor](../crates/frontend/editor/README.md) |
| 名称/作用域分析、源码到字节码和 VM、可执行示例 | [编译链路](runtime/compiler.md) | [compiler](../crates/compiler/README.md)、[CLI](../crates/cli/README.md) |
| 操作码、代码验证、局部槽、函数定义与调用协议 | [字节码 VM](runtime/bytecode.md) | instruction.rs/code.rs 在 [objects/src](../crates/objects/src/README.md),frame.rs/eval.rs 在 [core/src](../crates/core/src/README.md) |
| 基础信息保存在哪里、旧库如何迁移 | [宿主文件信息](operations/host-information.md) | [host](../crates/host/README.md) |
| UTF-8 在日本地区是否适用,其他编码怎样进入 VM | [文本编码](runtime/text-encoding.md) | [core](../crates/core/README.md)、[host](../crates/host/README.md) |
| 数据库连接给谁用,是否自动建表或保存基础信息 | [数据库特殊类](runtime/database-objects.md) | [storage](../crates/storage/README.md) |
| VM、对象与执行帧何时创建/释放,循环如何回收 | [生命周期](runtime/lifecycle.md) | heap.rs 在 [objects/src](../crates/objects/src/README.md),eval.rs 在 [core/src](../crates/core/src/README.md) |
| LC_CTYPE、UTF-8、显式数据库配置和执行限额 | [配置](operations/configuration.md) | [core](../crates/core/README.md)、[storage](../crates/storage/README.md)、[server](../crates/server/README.md) |
| CLI/HTTP 怎么运行,数据库在哪里使用 | [运行](operations/README.md) | [cli](../crates/cli/README.md)、[server](../crates/server/README.md) |
| Docker、PostgreSQL、ces-control、回退与数据目录 | [部署](operations/deployment.md) | [scripts](../scripts/README.md)、[工作流](../.gitea/workflows/README.md) |
| 检查覆盖什么,CI 中哪里查看 Endor 实际输出与 artifact | [验证](operations/validation.md) | [示例清单](../crates/compiler/examples/cases.json)、[报告脚本](../scripts/endor_examples.py)、[core/tests](../crates/core/tests/README.md)、[storage/tests](../crates/storage/tests/README.md) |
| 改代码后文档写在哪里、怎样避免失效 | [维护规则](contributing/README.md) | 各目录 README 与本索引 |
| 引擎与宿主边界 | [架构](architecture/README.md) | [crates](../crates/README.md) |
| 解释执行、资源修订与引擎升级 | [执行与版本](architecture/execution-and-versions.md) | frontend/compiler/core |
| EndorObject、句柄、默认操作 | [对象模型](runtime/objects.md) | objects/src |
| Number、String、Dict、Cell、Function | [基本对象](runtime/builtins.md) | objects/src |
| 源码读取与来源 | [Reader](runtime/reader.md) | frontend/src/reader.rs |
| 词法、语法和诊断 | [Frontend](runtime/frontend.md) | frontend/src/lexer.peg/grammar.peg |
| ASDL、AST 与编辑器接口 | [AST](runtime/ast.md) | frontend/Endor.asdl |
| 名称分析、编译和装载 | [Compiler](runtime/compiler.md) | compiler/src |
| 字节码、函数与限额 | [字节码 VM](runtime/bytecode.md) | objects/src、core/src |
| 引用与 VM 生命周期 | [生命周期](runtime/lifecycle.md) | objects/src、core/src |
| UTF-8 与地区 | [文本编码](runtime/text-encoding.md) | frontend、objects |
| 嵌入及宿主入口 | [使用引擎](operations/README.md) | 独立宿主仓库 |
| 引擎配置边界 | [配置](operations/configuration.md) | core/src/frame.rs |
| CI 和验证边界 | [验证](operations/validation.md) | scripts、.gitea |
| 文档维护 | [维护规则](contributing/README.md) | 各目录 README |
| 后续开发交接 | [交接](contributing/handoff.md) | 实时源码和 Git 状态 |
## 多层目录
| 目录 | 负责的内容 |
| --- | --- |
| [architecture](architecture/README.md) | 系统边界、依赖方向、解释执行与平台资源版本方向 |
| [runtime](runtime/README.md) | 源码前端、对象、基本载荷、字节码和生命周期 |
| [operations](operations/README.md) | 运行、配置、部署和验证 |
| [contributing](contributing/README.md) | 开发定位方法与随代码更新文档的规则 |
旧 outputs 的设计正文已融合到 runtime,重复入口和 outputs 目录已移除;CPython 研究与早期报告保存在本地忽略的 work/research,不纳入 Git。原 docs 平铺文档已迁入这些主题。过时启动模型、旧测试通过数量和机器资源快照已移除;历史内容需要时查 Git 历史,不混入当前行为说明。
## 仓库内查询
在仓库根目录使用静态文本检索:
```sh
rg -n 'LC_CTYPE|UTF-8|DATABASE_URL' docs crates --glob '*.md'
rg -n 'EndorFrame|ReturnValue|collect_cycles' crates/core/src crates/objects/src
rg --files -g README.md -g '!work/**' -g '!outputs/**' -g '!target/**'
```
没有额外的文档服务器或搜索依赖。新增主题时更新此表;新增受版本管理的目录时增加 README,并从父目录 README 链接过去。
目录入口:[architecture](architecture/README.md)、[runtime](runtime/README.md)、[operations](operations/README.md)、[contributing](contributing/README.md)。宿主部署、文件信息和数据库正文已移至 Hestia,命令行与实际输出报告已移至 Lyra;本仓库不保留重复正文。研究资料仍在忽略的 work/research,不提交。
+10 -49
View File
@@ -1,63 +1,24 @@
# 系统架构
Endor 的目标是云原生语言;本仓库当前实现运行时、基础语法前端、最小源码编译链路和宿主演示,尚无云调度器或多租户执行沙箱。当前侧重核心语义独立于进程环境,以及数据库能力按对象需求显式提供。
## 本目录
Aster 是可嵌入的 Endor 语言引擎,类似 V8 的职责边界;源码前端和编译器属于引擎。Lyra CLI、Hestia Server、Tessera GUI 是并列宿主。引擎自身没有常驻服务或生产部署。
| 文件 | 职责 |
| --- | --- |
| README.md | 当前组件边界、依赖方向和跨组件约束 |
| [execution-and-versions.md](execution-and-versions.md) | 解释执行流程、资源修订/依赖锁与平台升级的设计方向,区分已实现与计划 |
## 组件与依赖方向
| README.md | 组件边界和依赖方向 |
| [execution-and-versions.md](execution-and-versions.md) | 平台解释执行、源码修订和引擎版本 |
```text
CLI ────→ Compiler + Core + Frontend + Host
Lyra / Hestia / Tessera → 按需链接 Aster 库
Compiler → Frontend + Core
Core ───→ Objects
Objects → 标准库
Core → Objects
Objects → Rust 标准库
Frontend → peg + serde + serde_json
Server ─→ Core + Host
未来数据库特殊类的宿主实现 ─→ Storage
```
CLI、Server 依赖 Host 保存文件信息,不依赖 Storage;Core 只依赖本地 Objects,Objects 无依赖且不反向依赖 Core;二者均不依赖 Storage。Compiler 负责名称/作用域分析、wordcode 生成与代码对象装载,CLI run/eval 连接源码执行;CLI 的 parse 命令通过 Frontend 的 EndorReader 读取文件/stdin,再解析 AST;Frontend 使用 peg 及工作区已有 serde/serde_json,读取适配使用标准库;Endor.asdl 在开发时生成 AST、遍历器与编辑器接口,普通构建不运行生成器。平台 URI 的存储与授权由宿主 provider 提供,Core 不依赖 Frontend。HTTP、Tokio、SQLx、数据库、网络和浏览器能力不进入 core 或 objects;两包均无第三方依赖。包与文件定位见 [crates](../../crates/README.md)。
Objects 拥有对象、稳定 Box、代际句柄、引用事务、私有堆和字节码数据;Core 只负责 VM、内部 EndorFrame、帧切换与求值循环。Core 不反向依赖 Frontend,也不依赖 HTTP、数据库或 GUI。Frontend 的 AST 生成产物随源码提交,普通构建不运行生成器。
| 组件 | 当前职责 | 全局契约 |
| --- | --- | --- |
| Core | Vm、执行帧、调用协议与取指求值循环;对象方法委托到 Objects | [字节码 VM](../runtime/bytecode.md) |
| Objects | 核心对象、私有堆、ObjectStore、Number、代码对象、指令数据和静态验证;无求值器 | [对象模型](../runtime/objects.md) |
| Frontend | Endor 阅读器、UTF-8 字符 PEG lexer、token PEG parser、ASDL 生成的根/声明/语句/表达式 AST 与跨工具接口,不执行源码 | [源码前端](../runtime/frontend.md) |
| Compiler | AST 名称/作用域分析、wordcode 模板、代码对象装载;不引入默认语言类 | [编译链路](../runtime/compiler.md) |
| Host | 基础信息的逐键 UTF-8 JSON 文件与旧库导入 | [宿主信息](../operations/host-information.md) |
| Storage | 按显式配置建立 SQLite/PostgreSQL 连接池,不定义应用表 | [数据库特殊类](../runtime/database-objects.md) |
| CLI | 对象引导演示、源码解析/执行与文件信息命令 | [CLI](../../crates/cli/README.md) |
| Server | HTTP 存活检查、只读 VM 信息及文件版本记录 | [Server](../../crates/server/README.md) |
| Gitea/Docker | 双数据库适配验证、镜像构建、宿主与 PostgreSQL 资源部署 | [部署](../operations/deployment.md) |
宿主以子模块固定 Aster 提交,并通过 Cargo 路径依赖编译到宿主进程中。每个 Vm 拥有独立 ObjectStore。ObjectRef 不跨 VM 或进程使用;EndorFrame 是语言调用帧,不是 GUI 页面。宿主决定线程、进程、输入、显示与执行任务之间的边界。
core 保留既有对象类型的重导出与 Vm 方法,避免宿主导入方式随拆包改变。核心对象的定义和引用事务只在 objects 实现;ObjectRef 字段、Heap 和载荷替换不对包外开放。代码/指令属于可共享的执行数据,放在 objects;如何执行这些数据由 core 决定。定位见 [objects](../../crates/objects/README.md)。
引擎从空堆启动,不预设 Python type/元类或语言根类。代码共享不可变对象,函数用显式帧向量执行;对象返回值拥有根引用,借用读取不自动 retain。Number 保留无损表示,数学运算语义尚未实现。现有帧数/指令数限制不构成内存配额或沙箱。
## 当前契约
- Vm 从空堆启动。EndorObject 是实现层公共结构,不是语言根类;没有 Python type、继承或元类。
- 对象地址稳定,公开句柄校验 VM 身份和槽位代数;句柄不是跨进程 ID。
- 引用计数负责通常的释放,显式全堆追踪负责不可达循环;执行帧中的对象值也作为根。
- 函数共享不可变代码对象,既可由宿主工厂创建,也可由 MakeFunction 字节码绑定默认值与闭包后创建;语言调用使用显式帧向量;字节码参考 CPython 的职责划分,不兼容其操作码 ABI。
- Number 优先无损表示;数值算术、相等和哈希尚未定义。异常对象保存数据,尚无 raise/try/except 传播。
- 内核不读取环境或修改 locale。宿主处理监听和信号;数据库配置由真正消费它的对象适配层显式传入,见 [配置](../operations/configuration.md)。
## 数据与服务边界
数据库用于未来某种特殊类,不保存基础信息、版本、部署记录或普通对象。Storage 连接不建应用表,不提供统一 KV;实际数据布局和查询由特殊类定义。当前只有连接适配,还没有实现该类或把它接入 Vm。
HTTP 宿主通过 Arc 共享只读 Vm。GET /bootstrap 只读对象模型和数量,GET /health 仅报告 HTTP 可响应。没有语言执行、对象写入或认证协议,不能称为远程执行平台。重启仍创建新空 VM,没有堆快照。
部署保留既有 PostgreSQL 服务与数据目录,应用启动不依赖数据库连接。基础信息从数据库改为 Host 文件层;发布脚本一次性只读导入旧 PostgreSQL 记录,不删除原表或卷。脚本结果未主动跟踪,不能据此认定远端已经完成迁移。
## 执行与版本方向
Endor 的目标流程是源码经阅读器、lexer、parser 和内部字节码生成后,由 VM 解释执行。源码资源与依赖版本由平台管理,不要求真实代码文件或项目级 Git 作为语言执行单位;使用者无需单独构建程序。解释器由平台统一升级,升级实现不改写用户源码修订。资源身份、修订与依赖锁尚未实现,持久化字节码缓存暂不作为首版要求;完整说明见 [解释执行与平台资源版本](execution-and-versions.md)。
## 后续边界
特殊类接口与异步数据库调用、AST 到字节码的编译与名称/类型分析、更多指令、异常传播、自动 GC、内存配额、取消和能力隔离等均需按实际需求设计。当前帧/指令限额不等于完整沙箱。返回 [文档索引](../README.md)。
宿主文件信息和数据库适配现归 [Hestia](https://gitea.entroia.com/Zayden-Jung/endorphin-hestia-server),不保存 VM 堆。原生产服务、ASTER_* 配置、数据库、卷和路径由 Hestia 保持兼容;拆仓本身不迁移或删除这些资源。Aster CI 只验证引擎,数据库/HTTP/CLI 检查分别由宿主 CI 承担。返回 [文档索引](../README.md)。
+1 -1
View File
@@ -13,7 +13,7 @@ Endor 由平台解释执行,目标流程为:
生成字节码是解释器内部的执行准备,可以称为内部编译。使用者提交或调用源码资源,由平台完成这些步骤,无需先构建独立程序。现有 VM 解释 wordcode,源码编译沿用该执行器。
Frontend 产出 AST,由独立 Compiler 分析名称并生成代码模板,装载后交给 Core 执行。`cli run` 执行基础函数模块,`cli eval` 执行表达式,`cli parse` 继续只输出 AST;支持范围见 [编译链路](../runtime/compiler.md)。源码闭包与数值算术尚未接入,没有 CLI REPL。仓库中的 Cargo/Docker 构建用于构建 Rust 实现和宿主,不是 Endor 使用者的程序构建步骤。
Frontend 产出 AST,由独立 Compiler 分析名称并生成代码模板,装载后交给 Core 执行。`lyra run` 执行基础函数模块,`lyra eval` 执行表达式,`lyra parse` 继续只输出 AST;支持范围见 [编译链路](../runtime/compiler.md)。源码闭包与数值算术尚未接入,没有 CLI REPL。仓库中的 Cargo/Docker 构建用于构建 Rust 实现和宿主,不是 Endor 使用者的程序构建步骤。
## 名称、资源与修订
+1 -5
View File
@@ -20,13 +20,9 @@
| AST 节点/根模式、ASDL 生成、Visitor、位置与 JSON/编辑器接口 | [ast.md](../runtime/ast.md) | Endor.asdl、生成器、生成产物与 AST/CLI 检查 |
| 名称/作用域、源码编译、代码装载与执行示例 | [compiler.md](../runtime/compiler.md) | compiler 源码/检查/示例、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 入口 |
| 数据库特殊类、显式连接和数据模型归属 | [database-objects.md](../runtime/database-objects.md) | storage 的源码与检查 README |
| VM/帧/对象的创建、错误退出与销毁 | [lifecycle.md](../runtime/lifecycle.md) | 对象与字节码主题链接 |
| 环境变量、默认值、优先级、UTF-8、locale 和配置边界 | [configuration.md](../operations/configuration.md) | 相关 crate README 链接 |
| 运行命令、端点和数据库连接边界 | [operations/README](../operations/README.md) | cli/server/storage README |
| 镜像、CI、部署、回退和数据目录 | [deployment.md](../operations/deployment.md) | scripts/工作流 README |
| 检查入口、覆盖范围、结果边界 | [validation.md](../operations/validation.md) | 各 tests README |
| 不可变研究表、聊天导出、临时脚本或构建产物 | 工作区外输出或忽略目录 | 不作为当前项目契约、不提交研究 HTML |
@@ -37,7 +33,7 @@
1. 新增、删除、重命名文件或目录:更新本目录和父目录 README 的文件表;新增受版本管理目录必须有 README。该规则不覆盖 .git、target、data、work、outputs 或其他忽略产物。
2. 修改 API、默认值、所有权、操作码或错误处理:同时修改上表对应主题、示例与相关检查;区分已实现行为和未来设计。
3. 改主题路径:修复仓库所有内部链接,更新 docs 总索引;失效旧页直接删除,不长久保留一份旧契约。
4. 改宿主配置/部署:同步配置表、运行命令和脚本 README;不把更换应用容器写成恢复 VM 堆或数据库回退。
4. 改引擎验证配置:同步验证主题和脚本 README;宿主配置与部署由各宿主仓库维护。
5. 检查链接、目录覆盖、旧路径和差异空白;仅在指定远端运行行为测试。没有查询 CI 结果就写未核实,不能继承上次通过的结论。
不是每次提交都要给所有文档改日期或复制版本号;只有相关内容发生变化时更新相应文档。提交及代码审查描述说明目的和验证,文档保留长期可查询的当前行为。
+10 -29
View File
@@ -1,49 +1,30 @@
# Aster VM 后续开发交接
从 [文档索引](../README.md) 查询当前实现,再按目录 README 找文件。不要把旧聊天、早期报告或未查询的 CI 状态当成当前契约。
先读 [文档索引](../README.md)、本地 AGENTS.md 和实时 Git 状态。所有本项目对话标题含 Aster VM。
## 当前项目与命名
## 项目边界
仓库为 endorphin-aster-vm,归属 ProjectEndorphin。包与目录为 core、objects、frontend、compiler、storage、host、cli、server;core 的 Rust 库名为 endor_core,命令为 cli/server。旧部署服务、数据库和路径保留既有标识以保护数据。
Aster 是可嵌入引擎,只保留 objects、core、frontend、compiler。Objects 零外部依赖,Core 仅依赖 Objects;不开放私有句柄构造,不把对象引用事务搬回 Core。Reader、PEG 与 ASDL 源码链路属于引擎;网络、文件基础信息、数据库和 GUI 属于宿主。
## 当前包边界
Objects 零依赖,统一拥有核心对象、Number、代码对象/指令数据与静态验证、私有堆、ObjectStore 和共享 VmError;不依赖 Core。Core 仅依赖本地 Objects,保留 Vm、执行帧、取指求值循环;原有对象方法委托给 ObjectStore,并重导出既有对象类型。不要把对象定义或引用事务搬回 Core,也不开放私有句柄构造。源码导航见 [objects](../../crates/objects/README.md) 与 [core](../../crates/core/README.md)。
[Lyra](https://gitea.entroia.com/Zayden-Jung/endorphin-lyra-cli) 接收 CLI 和示例实际输出报告;[Hestia](https://gitea.entroia.com/Zayden-Jung/endorphin-hestia-server) 接收 HTTP、兼容管理命令、Host、Storage 与既有 Docker 部署。宿主使用固定提交的 Aster 子模块。Aster 保留 ces-control 引擎 CI,移除 CD;不删除远端服务、数据库、卷或旧部署目录。Tessera 可本地编译运行但不本地测试,其 CI 同样使用 ces-control。Aster/Lyra/Hestia 不在本地构建测试,不主动查询 CI/部署。
## 已实现
- EndorObject、稳定 Box、代际句柄、显式强引用及手动循环回收;VM 从空堆启动,没有 Python type/元类或语言根类。
- Function、NativeFunction、Dict、Cell、异常数据、UTF-8 String 和无损表示优先的 Number。
- 小型 wordcode、共享代码对象、操作数/控制流/栈校验、显式执行帧及 execute/call;MakeFunction 可在执行中创建带默认参数与共享闭包 Cell 的函数。对象返回值持有根,用后 release_value;借用读取不自动 retain。
- EndorReader 在 lexer 前提供字符串/流/文件/宿主 URI provider 输入,产出不可变 SourceUnit,保存来源与不透明修订标签;带来源的词法/语法错误保留行列,CLI parse 支持文件和 stdin。没有平台 URI 后端或版本锁实现,见 [阅读器](../runtime/reader.md)。
- EndorReader 在 lexer 前提供字符串/流/文件/宿主 URI provider 输入,产出不可变 SourceUnit,保存来源与不透明修订标签;带来源的词法/语法错误保留行列,Lyra parse 支持文件和 stdin。没有平台 URI 后端或版本锁实现,见 [阅读器](../runtime/reader.md)。
- Endor.asdl 是 AST 唯一定义源,生成 Rust 节点/Spanned/Visitor、TypeScript 类型/遍历器和 JSON Schema。AstRoot 支持 Module/Expression/Interactive,Program/Block 使用声明/语句列表;AstDocument 提供带 schema 的 JSON 及范围校验,CLI 支持 --mode/--json,见 [AST 模型](../runtime/ast.md)。生成产物需通过 scripts/generate_ast.py 更新,不能手改。
- Frontend 使用 peg 0.8.6,词法规则在 lexer.peg(字符 → Token),语法规则在 grammar.peg(Token → AST);lexer.rs 保留逐 Token 游标与诊断位置换算,提供完整输入解析、基础函数/表达式 AST、Unicode 字节范围与行列诊断;CLI parse 可读 UTF-8 文件/stdin 输出 AST,不执行源码。当前语法以未完成设计稿为起点,可以继续调整,见 [源码前端](../runtime/frontend.md)。
- Compiler 已连接 AST → 模块/函数名称与初始化检查 → wordcode 模板 → VM 代码对象;CLI run 调用模块无参数入口,eval 执行表达式。Compiler 不依赖语言类注册;无损常量、函数/参数/let/调用和宿主成员可执行,详见 [编译链路](../runtime/compiler.md) 与 [示例](../../crates/compiler/examples/README.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 作为未来特殊类的预置资源保留。
- CI 使用 examples/cases.json 执行全部 Endor 示例,保存实际 stdout/stderr、退出码及状态;对应 run 页面展示 Markdown,并上传 endor-execution-output-<sha> artifact(Markdown/JSON,保留 30 天),失败也保留报告。查看方式见 [验证](../operations/validation.md),本地不运行报告脚本。
- Compiler 已连接 AST → 模块/函数名称与初始化检查 → wordcode 模板 → VM 代码对象;Lyra run 调用模块无参数入口,eval 执行表达式。Compiler 不依赖语言类注册;无损常量、函数/参数/let/调用和宿主成员可执行,详见 [编译链路](../runtime/compiler.md) 与 [示例](../../crates/compiler/examples/README.md)。
## 资料位置
正式说明在 docs,多层 README 负责导航;源码目录 README 描述本目录。outputs 已移除,CPython 3.14 的 284 项对象研究表、生成脚本及早期历史报告保留在本地忽略的 work/research,不上传 Git。
## 当前缺口
## 尚未实现
平台资源存储/修订与依赖锁、闭包捕获与类型分析、源码闭包声明、导入/类/Agent 语法、迭代器等扩展指令、数值算术/相等/哈希、完整异常传播、可变 VM/异步数据库回调、生成器、自动 GC、堆快照、内存配额、取消及不可信代码沙箱。宿主回调 panic 后的执行根恢复也未完成。不能把未来设计写成已有能力。
平台资源版本/依赖锁、源码闭包、数值算术、完整异常传播、内存配额、可靠取消、不可信代码沙箱、自动 GC 和宿主 panic 后执行根恢复尚未完成。类型注解只保存语法,不引入 Python type/元类。UI 接入适配由 Tessera 实现,不为 Tessera 修改引擎接口。
## 工作约定
本地不构建、不运行测试/冒烟,不下载依赖;允许静态阅读、格式化、ASDL 源码生成/同步检查、语法/链接检查。运行验证交给 ces-control 的 SQLite、真实 PostgreSQL 与宿主流程。按用户要求不主动跟踪 CI/部署,结果未查询就明确写未核实。
中文沟通,源码与文档同版本维护。所有受版本管理目录维护 README,新增/删除/移动同步当地与父目录入口,跨目录契约在 docs 维护唯一正文。AGENTS.md 与 work/research 保持本地忽略。Gitea 仅用 codex-bot,隔离 SSH、仓库级作者身份及提交后中文报告按本地约定执行。
数据库、卷和已有资料保留;不得改其他项目或共享 Runner。研究材料与 AGENTS.md 不提交。Git 身份按本地工作约定使用专用 codex-bot,不能借用仓库所有者凭据。
所有本项目对话标题包含 Aster VM。文档与代码同版本维护,不重新建立 outputs 或重复正文目录。开始下一项任务前先看 git status/最新提交,读取本地 AGENTS.md;本页不代替实时状态。
## 已确认的设计方向
Endor 由平台解释执行;AST 到字节码是解释器内部准备步骤,使用者无需先构建程序。代码未来以 workspace/namespace 下的资源提供,通过 Endor URI 查找,不要求真实源码文件或项目级 Git。资源修订与依赖锁选择用户代码,平台统一升级解释器;升级实现不自动改写源码修订。版本信息主要属于资源与依赖元数据,URI 具体格式、hash 方案和发布快照仍待设计。首版源码执行暂不要求持久化字节码缓存,VM 内代码对象共享继续保留。完整方向与尚未实现的边界见 [执行与版本模型](../architecture/execution-and-versions.md)。
## 下一步
最小源码执行链路已连接;下一步可补数值算术指令与明确的数值语义、逐指令源码映射,再按需求开放块语句和嵌套函数/闭包。现有 Interactive 编译接口允许宿主复用命名空间,但没有 CLI REPL。参数与返回类型仍只保存为语法节点,不引入 Python type/元类或借此实现整个语言类型系统。语法不必等外部设计稿全部定稿,先扩展可用子集。新增测试已交给既有远端 workspace 流程,本地不运行,也不主动查询 CI。
Endor 由平台解释执行,AST 到字节码是内部步骤;源码资源修订与解释器升级分开管理,具体 URI、资源锁与持久化协议仍待设计,见 [执行与版本](../architecture/execution-and-versions.md)。下一步按具体语言需求完善引擎,宿主调度和渲染在各宿主仓库推进。
+9 -57
View File
@@ -1,63 +1,15 @@
# 运行宿主
# 使用引擎
CLI 提供 VM 引导、源码解析/执行及文件基础信息,Server 提供状态与文件基础信息,二者不连接数据库。Storage 为未来数据库特殊类保留显式连接能力,见 [数据库对象](../runtime/database-objects.md)。以下构建和运行命令仅供用户指定的远端环境,本地不执行。
Aster 工作区只产出 Rust 库,不包含 CLI 或 HTTP 可执行文件。宿主在自己的进程中创建 Vm,按需使用 Reader、Frontend 和 Compiler;宿主构建会自动编译所需引擎库。
## CLI
下列 Cargo 构建针对 Rust 实现与宿主;Endor 的设计是由平台自动准备字节码并解释执行,无需使用者自行构建程序。当前已有最小 run/eval 源码执行入口,见 [执行与版本模型](../architecture/execution-and-versions.md)。
工作区要求 Rust 1.91 或兼容更高版本,镜像固定 rust:1.91.1-bookworm,依赖由 Cargo.lock 锁定。
```sh
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
cargo run -p cli -- parse --json crates/frontend/examples/basic.endor
cargo run -p cli -- run crates/compiler/examples/hello.endor
cargo run -p cli -- run --entry main crates/compiler/examples/calls.endor
cargo run -p cli -- eval - < crates/compiler/examples/literal.endor
```
bootstrap 创建两个 Record,输出 runtime=EndorObject、type_system=disabled、objects_distinct=true 和 heap_objects=2,显式释放后输出 released heap_objects=0。默认无参数执行同一示例。
parse 通过 EndorReader 严格读取 UTF-8 源码文件;`parse -` 从 stdin 读到 EOF,输入来源为 `<stdin>`,没有 REPL 提示符或会话求值。默认解析 module 模式,成功时输出 Rust Debug 格式的 AstRoot;--mode 支持 expression/interactive,--json 输出带 schema 的 JSON,见 [AST 模型](../runtime/ast.md);失败时输出来源、行、列并返回非零退出码;阅读错误在进入 lexer 前返回。输入适配与 URI provider 见 [阅读器](../runtime/reader.md)。该命令只解析语法,不执行源码;语法与当前边界见 [源码前端](../runtime/frontend.md)。
run 编译模块、初始化函数并调用无参数入口(默认 main),--entry 可指定名称;eval 执行一个完整表达式。二者支持文件或 - 读取 stdin 到 EOF,输出返回值,失败不输出成功结果。支持范围、命名空间释放及 traceback 精度见 [编译链路](../runtime/compiler.md),真实 Endor 输入见 [可执行示例](../../crates/compiler/examples/README.md);frontend/basic.endor 含算术,仍仅作为解析样例。
CLI 不读取数据库环境,不创建基础信息数据库表。metadata put/get 通过 host 包写 UTF-8 JSON 文件,metadata import 仅导入缺失记录;示例、配置和协议见 [宿主信息](host-information.md)。输入与退出码见 [cli/src](../../crates/cli/src/README.md)。
## HTTP
```sh
cargo run -p server
curl --fail http://127.0.0.1:8080/health
curl --fail http://127.0.0.1:8080/bootstrap
```
Server 创建空 VM 并绑定监听地址,默认 127.0.0.1:8080;通过 host 包写入文件 host.version,不读取数据库 URL、不建表。
| 端点 | 行为 |
| 入口 | 位置与职责 |
| --- | --- |
| GET /health | 返回 200 和 status=ok;仅表明 HTTP 宿主存活,不包含数据库后端字段 |
| GET /bootstrap | 返回 object_model=EndorObject、type_system=false、heap_objects=0;只读、不分配对象 |
| [Lyra CLI](https://gitea.entroia.com/Zayden-Jung/endorphin-lyra-cli) | lyra run/eval/parse/bootstrap;命令行文件与 stdin 行为及实际输出报告 |
| [Hestia Server](https://gitea.entroia.com/Zayden-Jung/endorphin-hestia-server) | HTTP、管理命令、文件信息、数据库适配、Docker 运维 |
| Tessera GUI | 原生窗口、布局、绘制和输入;本地可编译运行,但测试交给 CI |
没有语言执行、对象写入或认证 API。Unix 上 Ctrl-C/SIGTERM 触发 Axum 优雅退出,最后的 Arc 释放时销毁 Vm;没有数据库池需要在此处关闭,也没有执行中脚本的取消协议。
宿主固定子模块提交,再在 Cargo 中引用 crates/core(库名 endor_core)、crates/frontend、crates/compiler。源码编译与返回值根引用契约见 [Compiler](../runtime/compiler.md)、[生命周期](../runtime/lifecycle.md)。宿主不能把另一个 VM 的句柄传入当前 VM。
## 数据库能力的使用位置
引擎没有环境变量、locale 初始化、数据库连接或后台服务。宿主负责源码来源、globals、限额、调度和释放。Aster 本地不构建或测试;验证命令只在 ces-control 执行,见 [验证](validation.md)。旧部署运行资源继续由 Hestia 管理,移除本仓库 CD 不会删除资源。
当前应用不把数据库作为启动依赖;即使进程带有失效 DATABASE_URL 或 SQLITE_PATH,也不会因此读取它们。特殊类将来需要连接时,宿主适配实现显式传入 DatabaseConfig,详见 [配置](configuration.md)。
Storage 支持 SQLite 文件/内存和 PostgreSQL,不自带应用 schema 或 key/value 数据模型。旧数据库数据保留原位,发布时只读导入到文件信息;新的普通宿主不会继续写入数据库;数据持久化也不自动保存 VM 堆、句柄或帧。
## 定位问题
| 现象 | 定位 |
| --- | --- |
| Server 无法绑定地址 | ASTER_LISTEN 与端口占用,见 [配置](configuration.md) |
| /health 正常但数据库不可用 | health 不检测数据库;当前没有使用数据库的语言类 |
| 重启后 VM 对象消失 | 当前未实现 VM 快照,见 [生命周期](../runtime/lifecycle.md) |
| 需要连接或查询数据库 | [storage](../../crates/storage/README.md) 和 [数据库对象](../runtime/database-objects.md);先定义实际特殊类接口 |
容器与预置 PostgreSQL 见 [部署](deployment.md),检查范围见 [验证](validation.md)。返回 [文档索引](../README.md)。
本目录:[README.md](README.md) 描述嵌入入口,[configuration.md](configuration.md) 描述配置边界,[validation.md](validation.md) 描述检查。返回 [文档索引](../README.md)。
+5 -69
View File
@@ -1,73 +1,9 @@
# 配置、UTF-8 与进程边界
# 引擎配置边界
## 当前选择
Aster 的 objects、core、frontend、compiler 不读取进程环境或修改 locale。输入文本为 UTF-8,其他编码由宿主在创建 SourceUnit 前转换,见 [文本编码](../runtime/text-encoding.md)。
当前先明确少量稳定边界,不引入 PyPreConfig/PyConfig 式两阶段初始化,也不新增 VM 配置文件、环境变量前缀或配置框架。Vm::new 从空堆启动;监听与网络由宿主处理,数据库连接仅由真正消费它的对象显式请求。
Vm::new 创建独立空堆。宿主显式提供 globals 和必要对象;CompileOptions.globals 只声明可见名称,不注入实际值。execute/call 的对象结果拥有根,使用后 release_value。
| 配置层 | 当前入口 | 责任 |
| --- | --- | --- |
| 内核 | Vm::new、显式创建对象/函数;call_with_limits/execute_with_limits | 可预测的运行时语义,不读取环境变量、修改 locale、注册信号或加载 .env |
| 宿主 | Server 启动代码与 host::MetadataStore | 读取监听/文件信息目录;基础信息存 JSON 文件,不读取数据库配置 |
| 数据库对象适配 | DatabaseConnection::connect(&DatabaseConfig) | 显式选择后端后连接,不预设应用表;特殊类尚未接入 |
| 部署 | Compose 与 ces-control 脚本 | 端口映射、数据库密码、持久卷和镜像版本 |
| 构建/CI | Cargo.toml、Cargo.lock、Dockerfile、工作流 | 工具链、依赖锁定与验证环境;不作为语言运行语义配置 |
ExecutionLimits 默认 max_frames=1000、max_instructions=usize::MAX。嵌入方可通过 execute_with_limits/call_with_limits 显式设置有限值。没有内存配额、可靠取消或完整沙箱;原生回调耗时不由指令数限制覆盖。具体契约见 [字节码 VM](../runtime/bytecode.md)。
内核不直接依赖 HTTP、SQLx 或操作系统 locale 接口。这个隔离来自现有代码与分层,不是已经实现的独立安全沙箱。
## 为什么不照搬 CPython 的 locale 初始化
CPython 的 PyPreConfig 支持选择分配器、UTF-8 模式以及是否配置 LC_CTYPE,以处理它自己的启动和兼容性需求。Python 官方也提供忽略环境、保留 LC_CTYPE、不注册信号处理的隔离初始化。当前 Endor 通过 Rust 宿主接收已构造的值,没有需要复刻这套预初始化的依赖环。[CPython 初始化文档](https://docs.python.org/3.14/c-api/init_config.html)。
LC_CTYPE 处理字符分类、大小写和多字节字符相关约定;小数点和数字分组属于 LC_NUMERIC。只调整 LC_CTYPE 不能直接等同于修改数字小数点,这两类配置应区分。[locale 类别说明](https://docs.python.org/3.14/library/locale.html#locale.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 通过 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 不因地区切换。
## 已有进程与部署变量
| 名称 | 使用位置与默认值 | 错误和优先级 |
| --- | --- | --- |
| ASTER_STATE_DIR | Host 文件信息目录;默认 data/host,容器 /data/host | 使用 var_os 保留操作系统路径;显式空值报错,不影响数据库选择 |
| ASTER_LISTEN | Server;默认 127.0.0.1:8080 | 当前 var(...).unwrap_or_else:缺失或非 Unicode 都用默认值;有效 Unicode 的错误地址在绑定时失败 |
| ASTER_PORT | Compose 宿主端口映射 | 独立方案默认 8080,ces-control 默认 18081;不是 Vm 配置 |
| POSTGRES_PASSWORD | PostgreSQL Compose/生产环境文件 | Compose 必填;示例使用随机十六进制以避免 URL 转义歧义;不写入仓库 |
| ASTER_IMAGE | ces-control Compose | 必填已构建的版本镜像,通常由 release.env 提供 |
| ASTER_RELEASE_SHA | CI 与部署脚本 | 必须为 40 位小写十六进制提交 SHA;用于镜像和版本目录 |
| ASTER_TEST_POSTGRES_URL | 指定环境显式运行 PostgreSQL 集成测试 | 专用测试库 URL;不是生产数据库配置 |
文件信息命令及 Server 通过 host 包读取 ASTER_STATE_DIR,Server 还读取 ASTER_LISTEN;不会把 DATABASE_URL/SQLITE_PATH 作为基础信息存储配置。程序不自动加载 .env,Cargo 运行时须显式导出监听变量。Compose 会按其规则读取 .env;显式 --env-file 的生产文件由部署脚本管理。模板见 [.env.example](../../.env.example),执行链见 [部署](deployment.md)。
## 显式数据库配置
DatabaseConfig 没有默认值和 from_env,仅允许显式构造:Postgres(url)、SqliteFile(path)、SqliteMemory。没有默认 data/aster.db,也没有应用启动时的全局连接。connect 不执行应用 DDL,不保存基础信息;基础信息另由 [host 文件层](host-information.md) 保存;未来的特殊类负责消费连接和定义数据,见 [数据库对象](../runtime/database-objects.md)。
SQLite 文件显式连接时创建父目录与文件、启用 WAL,最多 5 个连接、busy timeout 5 秒;内存库单连接、不因池轮换销毁。PostgreSQL 最多 5 个连接、获取超时 5 秒,错误不降级。数据库 URL 的 Debug 和公开错误脱敏;底层错误 source 保留,不应不加处理地公开输出。
## 已有执行限额
ExecutionLimits 是每次调用显式传入的普通 Rust 值,不是进程全局开关;字段定义见 [frame.rs](../../crates/core/src/frame.rs)。
| 字段 | 默认值 | 作用 |
| --- | --- | --- |
| max_frames | 1000 | 本次执行允许的活动语言帧数量 |
| max_instructions | usize::MAX | 本次执行共享的逻辑指令预算;默认不设实用的有限预算 |
call_with_limits/execute_with_limits 使用自定义值;嵌套语言调用共用计数。预算不能抢占正在执行的 Native 回调,也没有超时、内存上限或异步取消能力;不能据此视为安全执行不可信代码的完整方案。
## 保留的入口缺口与以后再做的内容
CLI 当前使用 std::env::args,遇到非 Unicode 命令行参数可能 panic,并没有统一转换成友好的配置错误。[Rust args 文档](https://doc.rust-lang.org/std/env/fn.args.html#panics)。Server 的非 Unicode ASTER_LISTEN 也尚未严格报错;宿主输入错误接口尚未统一。
以后增加宿主输入时,应明确使用 OsString/PathBuf 保留系统路径,或显式拒绝不符合文本协议的输入;不要通过修改整个进程 locale 让隐式解码暂时成功。未来源码文本默认 UTF-8、协议数字固定格式是设计方向,只有加载器实现后才能写成已实现行为。
现在不新增通用 VmConfig。出现构造期策略时再集中为实例级不可变配置;进程参数由宿主解析后传入,不让 Vm::new 自动读取一组隐含开关。时区、模块路径、凭据注入、内存配额和网络能力也按实际实现分别设计,不预设已存在。
返回 [运行指南](README.md) 或 [文档索引](../README.md)。
ASTER_LISTEN、ASTER_STATE_DIR、数据库 URL 和部署配置属于独立宿主,不属于引擎设置。旧兼容标识由 Hestia 管理。返回 [使用引擎](README.md)。
-74
View File
@@ -1,74 +0,0 @@
# Docker 与 ces-control 部署
这里描述仓库当前工作流和脚本行为,不代表正在运行的服务器状态。生产保留预置 PostgreSQL 与既有数据。当前 VM 宿主不消费数据库,数据库连接仅为未来特殊类保留。配置字段见 [配置](configuration.md),CI 检查细节见 [验证](validation.md)。
## 镜像与独立运行
Dockerfile 的 source 阶段复制 Cargo 工作区及脚本;ci 阶段安装远端检查工具,build 阶段构建 server 和 cli,runtime 阶段使用 Debian slim、UID 10001、/data 工作目录及 HTTP 健康检查。
指定环境的独立宿主方案(沿用原 SQLite 卷,基础信息写入其中的 host 目录):
```sh
cp .env.example .env
docker compose up --build -d
curl --fail http://127.0.0.1:8080/health
docker compose exec aster cli bootstrap
```
默认 docker-compose.yml 提供监听地址和 ASTER_STATE_DIR,保留 aster-sqlite 卷挂载;只写基础信息 JSON,不自动打开卷中的数据库。down 保留数据,down -v 会删除卷。
指定环境中同时预置 PostgreSQL 的独立方案:先在 .env 中将 POSTGRES_PASSWORD 改为随机十六进制字符串,例如 openssl rand -hex 24,再运行:
```sh
docker compose -f docker-compose.postgres.yml up --build -d
docker compose -f docker-compose.postgres.yml exec aster cli bootstrap
```
仅使用该 Compose 文件,不与默认文件叠加。此方案使用 aster-postgres 命名卷、PostgreSQL 17,以及独立的 aster-host 文件信息卷;数据库不发布宿主端口。两个独立方案不要同时占用相同应用端口。未来特殊类跨不可信网络连接数据库时,需要明确配置 PostgreSQL TLS 验证。
## CI 到正式发布
[工作流](../../.gitea/workflows/README.md) 响应 main push 和 workflow_dispatch,在 ces-control 上串行运行:
1. 检出代码,调用 ci-ces-control.sh;在 Docker 中检查 workspace(含 SQLite 适配)、Endor 示例实际输出、独立 HTTP 冒烟及专用真实 PostgreSQL。清理检查容器前复制报告到宿主 ci-output,工作流无论验证成功或失败均展示并上传 endor-execution-output-<sha> artifact;字段与位置见 [验证报告](validation.md)。
2. 检查成功后构建 endorphin-aster-vm:<sha> 生产镜像。
3. main 上调用 deploy-ces-control.sh 更新 VM 宿主并保留预置 PostgreSQL。
临时检查容器与网络退出时清理,Cargo 下载与构建缓存保留在专属远端卷。生产镜像、已有发布目录与数据库数据保留;没有自动 image prune 或数据库卷清理。工作流不改动其他项目或共享 Runner 配置。
## 生产目录与数据
Cargo 包名为 core/objects/frontend/compiler/storage/host/cli/server,core 的 Rust 库名为 endor_core,可执行文件为 cli/server。Compose 应用服务名 aster、数据库名、卷和生产路径是已有部署资源标识,运行命令为该服务中的 cli。应用不接收 DATABASE_URL、不依赖数据库健康才能启动;Compose 的整体 --wait 仍检查所预置 PostgreSQL 的就绪状态。
| 路径 | 用途 |
| --- | --- |
| /opt/endorphin-aster-vm/releases/<sha>/ | 固定版本的 compose.yml 与 release.env |
| /opt/endorphin-aster-vm/current | 最近通过部署检查的版本符号链接 |
| /etc/endorphin-aster-vm/production.env | 首次生成的数据库密码和默认端口,权限 0600 |
| /var/lib/endorphin-aster-vm/host | 文件基础信息,挂载到 /data/host,目录由部署准备并交给 UID 10001 |
| /var/lib/endorphin-aster-vm/postgres | 独立的 PostgreSQL 持久数据目录 |
| /var/lock/endorphin-aster-vm-deploy.lock | 避免并行切换生产版本的文件锁 |
生产 Compose 固定项目名 endorphin-aster-vm,默认应用入口仅绑定 127.0.0.1:18081;数据库不发布端口,两个服务使用 unless-stopped 自动重启和日志轮换。端口绑定回环地址不等于已实现应用认证。
部署脚本只接受已构建镜像。容器就绪、HTTP status=ok 和 CLI 对象引导/释放检查都成功后才切换 current;发布记录改写到 deployment.release.json;首次发布一次性只读导入旧基础信息,再保留完成标记,不删除数据库原记录。详情见 [宿主信息](host-information.md)。镜像 SHA、release.env 与 current 链接负责标识版本。失败时保留数据并尝试恢复上一个健康版本;首次部署没有旧版本可恢复。脚本内容导航见 [scripts](../../scripts/README.md)。
这里的镜像 SHA、host.version 与 deployment.release 标识 Rust 实现/宿主发布,不是 Endor 用户资源的源码修订。平台方向是统一升级解释器,保留用户源码修订及明确的依赖选择;当前没有资源存储、依赖锁、持久化字节码缓存或执行帧热切换机制。两类版本的边界见 [执行与版本模型](../architecture/execution-and-versions.md)。
## 查看、停止与恢复
以下命令在服务器上使用已有生产配置,不重新生成密码:
```sh
docker compose -p endorphin-aster-vm --env-file /etc/endorphin-aster-vm/production.env --env-file /opt/endorphin-aster-vm/current/release.env -f /opt/endorphin-aster-vm/current/compose.yml ps
curl --fail http://127.0.0.1:18081/health
curl --fail http://127.0.0.1:18081/bootstrap
```
同样的 Compose 参数接 stop 停服保留数据,接 up -d --wait 恢复。不要删除数据目录或使用删除卷的操作代替停服。
更改环境文件密码不会自动修改已有 PostgreSQL 数据库用户密码;需另行同步数据库凭据。数据库 major 升级、schema 变更需要迁移设计,恢复旧应用镜像不等于回退数据库。
Compose 配置依据 [Docker 环境变量说明](https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/),触发方式依据 [Gitea Actions](https://docs.gitea.com/usage/actions/)。按用户约定不主动追踪运行状态;需要排障时针对具体提交核实,不能用旧成功记录推断新版本结果。
返回 [运行指南](README.md) 或 [文档索引](../README.md)。
-47
View File
@@ -1,47 +0,0 @@
# 宿主基础信息:文件保存
基础信息不使用数据库。host 包保存逐键 UTF-8 JSON,CLI 提供读写,Server 启动写 host.version,部署检查通过后写 deployment.release。Storage 的数据库连接仅给未来特殊类使用。
## 文件与配置
ASTER_STATE_DIR 使用操作系统路径,默认 data/host;显式空值报错。读取一个不存在的键返回 None,不创建目录;写入时创建目录。容器为 /data/host;生产挂载到 /var/lib/endorphin-aster-vm/host,独立 PostgreSQL Compose 使用 aster-host 卷。
例如 host.version 对应 host.version.json:
```json
{
"format_version": 1,
"key": "host.version",
"value": "0.1.0"
}
```
控制键以 ASCII 字母或数字开头,最长 128 字节,其余只能是字母、数字、点、下划线或连字符。它是信息命名协议,不是文本编码限制;值支持日文、中文、emoji 等 Unicode 文本。用户文本不应被当作文件路径或控制键。
文件必须是有效 UTF-8 JSON,版本和 key 必须匹配请求。损坏记录返回错误,不清空、不用默认值偷偷替换。文件不属于 VM 堆快照,也不存数据库凭据。
## 读写与生命周期
CLI 的 metadata put/get 通过 host::MetadataStore 访问文件,不连接 SQL:
```sh
cli metadata put project.note '日本語・中文・😀'
cli metadata get project.note
cli metadata get host.version
```
MetadataStore::clone 只共享目录路径,没有缓存;每次读取重新打开文件。写入先建立同目录唯一临时文件,写完并 sync_all,再通过 rename 发布;Unix 新文件权限 0600。不同键分别写文件,不会因整张 map 的读改写丢失其他键;同键并发是最后一次成功发布生效。
没有跨键事务、分布式锁或共享网络文件系统一致性保证;文件同步与原子发布也不等于数据库的完整断电持久性协议。异常进程终止可能留下隐藏临时文件,正常失败会清理自己的临时文件。
## 旧基础信息的一次性迁移
ces-control 发布时,在 host/.legacy-imported 标记缺失的情况下,只读检查 PostgreSQL 原 public.aster_metadata 表。存在时,使用 UTF-8 客户端导出 key/value JSON,再经 cli metadata import 的标准输入导入。该命令只创建缺失文件,不覆盖 Server 刚写入的版本或其他已有文件;临时导出文件以私有权限创建并清理,不打印数据内容。
只导入缺失项通过同目录临时文件的原子 hard_link 实现,因此运行目录应使用支持该操作的本地文件系统。导入可重复执行;只有导入成功或源表不存在才写完成标记。损坏记录、不支持的控制键或 I/O 错误会停止发布,保留原库数据,不能当作迁移成功。
本迁移针对已配置的 PostgreSQL 生产路径,不自动扫描或删除其他 SQLite 文件。原数据库表与数据不会被修改/删除。此处说明发布脚本行为,不代表远端已执行成功;按约定不主动跟踪 CI 或服务器。
部署记录仍以版本镜像、release.env 与 current 链接为发布来源;deployment.release.json 是文件信息记录,不是数据库迁移表。脚本见 [scripts](../../scripts/README.md)。
返回 [运行指南](README.md) 或 [文档索引](../README.md)。
+7 -56
View File
@@ -1,62 +1,13 @@
# 验证范围与结果边界
# 引擎验证
本页维护当前检查入口和覆盖范围,不累计已失效的测试数量、机器资源快照或旧对象模型的通过记录。历史行为查 Git 历史;某个版本是否通过,只能依据该提交对应的实际结果。
本地不运行 Cargo 构建、测试、Clippy、冒烟、Docker 或依赖下载。允许静态阅读、格式化、ASDL 生成/同步检查及文档链接检查;运行验证交给 ces-control。不主动查询 CI/部署结果。
## 执行约定
[ci.yml](../../.gitea/workflows/ci.yml) 在 main push、pull request 和手动触发时运行引擎 CI,标签为 ces-control。Dockerfile.ci 只提供远端 Rust 1.91.1 验证环境,不生成生产服务镜像或执行部署。
本地不运行 Cargo 构建、测试、Clippy、冒烟、Docker 或依赖下载;允许阅读、格式化、ASDL 源码生成/产物同步检查和静态文件/文档检查。指定运行验证环境为 ces-control。按用户要求不主动追踪 CI/部署结果,有问题由用户反馈后再查。
远端的 scripts/check.sh 验证 ASDL 产物、生成器回归、Cargo 格式、独立 PEG 文件格式、cargo test --workspace --locked 和 Clippy;包括 objects/core 的对象与执行测试、frontend 的读取/AST/语法测试、compiler 的名称分析/源码执行测试及 rustdoc。Compiler 的五个源码样例继续位于 crates/compiler/examples。
## 检查入口
五个样例的真实命令行输出报告改由 [Lyra CI](https://gitea.entroia.com/Zayden-Jung/endorphin-lyra-cli) 调用真实 lyra 可执行文件生成,保存源码、预期/实际 stdout、stderr 和退出码;失败也保留 artifact。Aster 的编译器测试与 Lyra 的可执行程序测试是不同层次,消费者的 cargo test 不会自动运行依赖库的全部自身测试。
以下命令只在指定的验证环境执行,不表示已经运行:
HTTP、宿主 JSON、SQLite 与真实 PostgreSQL 测试归 [Hestia CI](https://gitea.entroia.com/Zayden-Jung/endorphin-hestia-server)。Aster 无 CD;现有服务和数据由 Hestia 的独立部署定义保留。CI 的 bot SSH 凭据要求见 [.gitea](../../.gitea/README.md)。源码和工作流定义不证明运行通过;未查询时结果记为未核实。
```sh
sh scripts/check.sh
cargo test -p storage --test postgres --locked -- --ignored
python3 scripts/smoke.py
```
check.sh 先运行 generate_ast.py --check 验证 ASDL 产物同步,以及仅远端执行的 test_generate_ast.py 生成器回归检查、test_endor_examples.py 报告失败路径检查,再执行 cargo fmt --all --check、独立词法/语法文件的 rustfmt --edition 2024 --check crates/frontend/src/lexer.peg crates/frontend/src/grammar.peg、cargo test --workspace --locked 和 cargo clippy --workspace --all-targets --locked -- -D warnings。workspace 测试也包含 rustdoc 示例;显式忽略的 PostgreSQL 测试需要单独执行。
ci-ces-control.sh 负责准备全新专用临时 PostgreSQL 和测试 URL;成功路径使用真实数据库,不用模拟 socket 代替。测试库与生产数据不共享挂载。没有 ASTER_TEST_POSTGRES_URL 时,显式执行 PostgreSQL 测试会失败,不标记为通过。
## Endor 示例实际输出
在对应 Gitea 仓库的 Actions → 本次提交的 run 页面,打开 Show Endor example execution output 步骤可看实际运行报告;下载 endor-execution-output-<sha> artifact 可保存两种格式:
- endor-examples.md:逐例展示源码、执行命令、预期值、实际 stdout/stderr、退出码和 PASS/FAIL/NOT_RUN。
- endor-examples.json:保留精确输出(包括换行)、提交 SHA、UTC 时间、构建/定位命令结果和逐例状态,供工具读取。
[endor_examples.py](../../scripts/endor_examples.py) 构建真实 CLI,通过 Cargo metadata 定位二进制并执行 [cases.json](../../crates/compiler/examples/cases.json) 中的全部示例;不以预期值冒充实际值。stdout 必须精确匹配、stderr 为空且退出码为 0 才通过。单例超时 30 秒;构建失败不运行示例并标记 NOT_RUN;报告本身的异常标记 FAIL。一个案例失败仍收集后续案例。示例报告不代表 workspace、数据库、宿主和部署全部通过,整体结果仍看工作流。
ci-ces-control.sh 在检查容器退出后、清理前将两份文件复制到宿主 ci-output;验证步骤提前失败时已有 NOT_RUN 占位说明。报告运行不掩盖前面检查的失败;后续数据库或宿主检查失败也不丢报告。工作流用 always() 展示并上传,artifact 保留 30 天,不写进 Git 或生产数据目录。普通 Actions 日志仍保留完整测试/构建信息;报告上传失败会使工作流失败。
## 覆盖范围与文件位置
| 范围 | 当前检查内容 | 入口 |
| --- | --- | --- |
| 堆内部边界 | 非法索引、槽位代数、代数耗尽、计数溢出原子性 | [objects/src](../../crates/objects/src/README.md) 的 heap.rs 私有单元测试 |
| 独立对象存储 | 不依赖 VM 的稳定地址、跨存储拒绝、更新失败原子性、循环回收、函数校验与共享代码所有权 | [objects/tests](../../crates/objects/tests/README.md) 的 store.rs |
| 对象运行时 | 地址稳定、引用/借用、属性、长链、循环、原生操作、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 |
| AST 与接口生成 | 三类根、声明/语句层次、Visitor 及失败退出、Unicode 位置、JSON 往返、schema/范围错误和跨语言产物标识 | frontend/tests/ast.rs 与 scripts/test_generate_ast.py,见 [AST 模型](../runtime/ast.md) |
| 源码前端 | 字符 PEG 词法边界、逐 Token 原文位置与首个错误诊断、Unicode 范围、数字原文、字符串与错误、优先级/结合性、函数/绑定、完整输入与错误位置、源码样例 | [frontend/tests](../../crates/frontend/tests/README.md) |
| 源码编译与执行 | 真实 Endor 文件到 VM、名称/初始化检查、参数与函数值、精确常量、宿主成员回调、跨 VM 模板、错误清理、递归限额、交互命名空间及扩展操作数 | [compiler/tests](../../crates/compiler/tests/README.md) |
| CLI 执行 | run/eval 文件与 stdin、入口、返回值、语义/编译/运行错误、traceback 和退出码 | [cli/tests](../../crates/cli/tests/README.md) |
| CLI 解析 | AST 输出、未执行源码、文件/stdin 来源和行列错误、EOF 读取与非法 UTF-8 退出 | [cli/tests](../../crates/cli/tests/README.md) |
| 公开示例 | 对象操作、手写指令执行与释放 | objects 与 core 的 src/lib.rs rustdoc,见 [objects/src](../../crates/objects/src/README.md)、[core/src](../../crates/core/src/README.md) |
| 宿主文件信息 | UTF-8 值/路径、持久读取、同键替换、并发、坏记录、幂等迁移与保留新值 | [host/tests](../../crates/host/tests/README.md) |
| 数据库连接 | 不自动建表、显式配置、隔离/共享、调用方数据重连、失败不降级、真实 PostgreSQL | [storage/tests](../../crates/storage/tests/README.md) |
| 端到端宿主 | 文件 metadata 跨进程读写、日文值、Server 文件版本;忽略失效数据库变量且不创建 SQLite 文件、HTTP 与 SIGTERM | [scripts](../../scripts/README.md) 的 smoke.py |
测试源码说明验证意图,不能证明已通过;现有测试也不代表完整 Python 兼容、panic 恢复、云沙箱或 VM 快照已经实现。
## 文档修改的静态检查
修改后核对所有受版本管理目录存在 README、父目录有入口、相对链接指向现存文件;运行 git diff --check 检查差异空白。静态文本检索可以查出旧路径和过时 API;维护规则见 [开发与文档维护](../contributing/README.md)。这些检查不执行 VM 或数据库。
仅修改文档时也不据此宣称运行检查通过。推送触发现有 CI 时,仍由原流程检查和部署;报告应分别说明静态检查、运行测试与推送结果。
返回 [运行指南](README.md) 或 [文档索引](../README.md)。
返回 [使用引擎](README.md)。
+12 -15
View File
@@ -1,18 +1,15 @@
# 运行时设计
# 运行时契约
本目录解释源码输入、前端及跨越 core/objects 的运行时契约。对象与存储定位见 [objects/src](../../crates/objects/src/README.md),VM 与执行帧见 [core/src](../../crates/core/src/README.md);检查分别见 [objects/tests](../../crates/objects/tests/README.md) 和 [core/tests](../../crates/core/tests/README.md)。
| 主题 | 内容 |
| 文档 | 内容 |
| --- | --- |
| [对象模型](objects.md) | EndorObject/ObjectHeader、代际句柄、操作协议、托管引用与循环回收 |
| [基本对象与 Number](builtins.md) | 函数、原生函数、字典、Cell、异常数据、字符串与无损数字 |
| [Endor 阅读器](reader.md) | SourceUnit、来源/修订标签、字符串/流/文件和宿主 URI provider |
| [源码前端](frontend.md) | 字符 PEG lexer、token PEG parser、AST、基础函数/表达式语法与诊断 |
| [AST 模型与 ASDL](ast.md) | 统一根、声明/语句/表达式、生成器、Visitor、位置和 JSON/编辑器接口 |
| [源码编译](compiler.md) | 名称/作用域、局部槽、字节码模板、代码装载、CLI 执行与示例 |
| [字节码 VM](bytecode.md) | 指令编码、代码校验、局部槽、执行帧、函数创建、调用/返回与边界 |
| [文本编码](text-encoding.md) | UTF-8、日文、外部 Shift_JIS/CP932 与宿主解码边界 |
| [数据库特殊类](database-objects.md) | 显式连接、调用方数据模型、未来类接入与资源生命周期 |
| [生命周期](lifecycle.md) | 宿主持有 Vm、帧执行根、结果所有权、错误退出和整体销毁 |
| [objects.md](objects.md) | 对象、句柄与默认操作 |
| [builtins.md](builtins.md) | Number、String、Dict、Cell、Function、异常数据 |
| [reader.md](reader.md) | 输入、来源和修订标签 |
| [frontend.md](frontend.md) | 词法、语法和诊断 |
| [ast.md](ast.md) | ASDL、AST、生成与编辑器接口 |
| [compiler.md](compiler.md) | 名称分析、编译、装载与源码样例 |
| [bytecode.md](bytecode.md) | 指令、验证和调用协议 |
| [lifecycle.md](lifecycle.md) | 根引用、失败、回收与销毁 |
| [text-encoding.md](text-encoding.md) | UTF-8 和宿主转码边界 |
文本/环境约束和执行限额集中在 [配置](../operations/configuration.md);组件之间的关系见 [系统架构](../architecture/README.md),解释执行与源码/平台版本的设计方向见 [执行与版本模型](../architecture/execution-and-versions.md)。返回 [文档索引](../README.md)。
文件信息与数据库适配属于独立 Hestia 宿主,不属于引擎;CLI 属于独立 Lyra 宿主。返回 [文档索引](../README.md)。
+4 -4
View File
@@ -64,11 +64,11 @@ AST_SCHEMA_ID 从 Endor.asdl 的 UTF-8 文本计算 SHA-256,包含注释与空
CLI 可提供具体的交换输出:
```sh
cli parse --json source.endor
cli parse --mode expression --json -
cli parse --mode interactive --json -
lyra parse --json source.endor
lyra parse --mode expression --json -
lyra parse --mode interactive --json -
```
文件/stdin 阅读保持原有 UTF-8 和错误来源规则,stdin 到 EOF 后才解析。普通 cli parse 输出 AstRoot 的 Debug 文本,稳定交换使用带 schema 的 JSON;parse 的各模式均不执行源码;run/eval 执行入口见 [编译链路](compiler.md)。命令在指定远端运行。
文件/stdin 阅读保持原有 UTF-8 和错误来源规则,stdin 到 EOF 后才解析。普通 lyra parse 输出 AstRoot 的 Debug 文本,稳定交换使用带 schema 的 JSON;parse 的各模式均不执行源码;run/eval 执行入口见 [编译链路](compiler.md)。命令在指定远端运行。
检查源码见 [frontend/tests](../../crates/frontend/tests/README.md) 和 [生成器检查](../../scripts/test_generate_ast.py),边界见 [验证](../operations/validation.md)。返回 [运行时索引](README.md)。
+5 -5
View File
@@ -34,7 +34,7 @@ CompileOptions.globals 显式声明宿主会提供的 globals/builtins 名称
| 成员读取 | 求值对象 → LoadAttr;沿用 VM 属性协议,无自动 self 绑定 |
| return | 求值结果 → ReturnValue;无值 return 或 AST 函数末尾无 return 时生成 nil |
源码 parser 仍要求函数体为 let* 加一个带值 return;更通用的表达式语句和空 return 可以由 AST 调用方构造,但不能因此宣称源码语法已开放。模块初始化只发布函数并返回 nil,不自动调用 main。CLI 选择入口属于宿主行为。
源码 parser 仍要求函数体为 let* 加一个带值 return;更通用的表达式语句和空 return 可以由 AST 调用方构造,但不能因此宣称源码语法已开放。模块初始化只发布函数并返回 nil,不自动调用 main。Lyra 宿主选择入口属于宿主行为。
二元算术、非字面量的一元负号和符号值 .name 目前产生明确 Compilation 诊断,不伪造运行结果。导入、类、Agent、条件/循环、关键字参数等仍未实现。函数参数个数和对象是否可调用由 VM 在运行时检查。
@@ -44,12 +44,12 @@ CompiledCode 是纯 Rust 模板,嵌套代码常量在模板内部使用占位
宿主为模块提供 globals 字典,调用 vm.execute(code, globals) 发布函数,然后从字典借用函数句柄并调用 vm.call。函数共享装载后的代码,反复调用不重新编译;execute/call 的对象结果持有根,用后 release_value。函数指向 globals,globals 保存函数,因此宿主需保留命名空间期间的根,并在销毁时清空字典或释放根后显式 collect_cycles。编译器不清空宿主字典。
Expression 模式返回表达式值;Interactive 模式严格编译一次声明、let 或表达式。声明/绑定返回 nil,表达式返回值;宿主可以跨提交复用 globals,并在下一次 CompileOptions 中声明可见名称。CLI 当前没有 REPL、续行或自动会话管理。
Expression 模式返回表达式值;Interactive 模式严格编译一次声明、let 或表达式。声明/绑定返回 nil,表达式返回值;宿主可以跨提交复用 globals,并在下一次 CompileOptions 中声明可见名称。Lyra 当前没有 REPL、续行或自动会话管理。
代码对象保存来源字符串、函数名和定义起始行,现有 VM traceback 保存该位置及指令偏移;没有每条指令对应的精确源码行/列映射。现有 ExecutionLimits 帧数/指令数限制可由嵌入方继续使用;本链路不构成不可信代码沙箱。
## CLI 与检查
## Lyra 宿主与检查
cli run [--entry name] <file|-> 编译模块、执行初始化、调用无参数入口(默认 main),输出返回值一行。cli eval <file|-> 编译执行一个表达式。字符串输出原始内容,数字输出无损表示,对象使用 VM str;失败仅输出 stderr 并返回非零退出码。文件和 stdin 严格 UTF-8,stdin 到 EOF 后才处理。CLI 在成功或失败后释放代码/结果并清空自己创建的命名空间。
lyra run [--entry name] <file|-> 编译模块、执行初始化、调用无参数入口(默认 main),输出返回值一行。lyra eval <file|-> 编译执行一个表达式。字符串输出原始内容,数字输出无损表示,对象使用 VM str;失败仅输出 stderr 并返回非零退出码。文件和 stdin 严格 UTF-8,stdin 到 EOF 后才处理。Lyra 在成功或失败后释放代码/结果并清空自己创建的命名空间。
可执行文件见 [示例目录](../../crates/compiler/examples/README.md),API/所有权与错误路径见 [pipeline.rs](../../crates/compiler/tests/pipeline.rs),进程入口见 [CLI 检查](../../crates/cli/tests/README.md)。这些检查由既有远端 workspace 测试覆盖;本地只做静态检查,不据此宣称实际执行通过,见 [验证约定](../operations/validation.md)。返回 [运行时索引](README.md)。
可执行文件见 [示例目录](../../crates/compiler/examples/README.md),API/所有权与错误路径见 [pipeline.rs](../../crates/compiler/tests/pipeline.rs),进程入口见 [CLI 检查](https://gitea.entroia.com/Zayden-Jung/endorphin-lyra-cli/src/branch/main/crates/cli/tests/README.md)。引擎检查由 Aster CI 覆盖,进程入口检查由独立 Lyra CI 覆盖;本地只做静态检查,不据此宣称实际执行通过,见 [验证约定](../operations/validation.md)。返回 [运行时索引](README.md)。
-36
View File
@@ -1,36 +0,0 @@
# 数据库特殊类与连接边界
数据库连接用于某类明确需要数据库的特殊对象,不用于 VM 版本、部署版本、普通对象信息或统一 key/value 元数据。当前尚未定义该特殊类的名字、载荷布局或语言接口,本页说明已保留的连接能力及接入边界。
## 当前分层
- Objects 负责核心对象与私有堆,Core 负责 VM 和执行;两者不依赖 SQLx 或数据库。
- Storage 仅暴露 DatabaseConfig、DatabaseConnection 和 DatabaseError。
- CLI/Server 不依赖 Storage,启动、bootstrap 和 health 不访问数据库;基础信息由独立 host 文件层处理。
- 特殊类以后由宿主实现按需取得数据库连接;尚未自动挂入 Vm 或对象工厂。
## 显式建立连接
调用方先构造 DatabaseConfig::Postgres(url)、SqliteFile(path) 或 SqliteMemory,再 await DatabaseConnection::connect。没有 from_env、默认 SQLite 路径或应用启动时的全局数据库。
connect 会建立真实连接,并可在显式请求 SQLite 文件时创建父目录和文件,但不创建任何应用表。SQLite WAL、连接协商等驱动自身操作不等于应用 schema 初始化。不会创建基础信息表,也不会写入运行或发布信息。
sqlite_pool/postgres_pool 返回匹配后端的 SQLx 池借用,另一后端返回 None。特殊类以后可在自己的适配实现里执行参数化查询、事务及由该类定义的数据结构;本层不预设通用 KV、ORM、迁移或序列化协议。现有 SQLx API 见 [Pool 文档](https://docs.rs/sqlx/0.8.6/sqlx/struct.Pool.html)。
## 生命周期
DatabaseConnection::clone 共享连接池;close().await 显式关闭所有克隆使用的池,并等待已借出连接归还。取出的 SQLx 池还能被调用方克隆,因此单个包装值销毁不保证所有使用者立即结束。
未来将连接放进原生载荷时,必须另行定义异步关闭、共享所有权和错误传播;当前 VM 的同步 Drop/Native 回调不等于已经完成异步数据库类。连接跨进程不能复用,也不代表 VM 快照。
## 既有数据和部署
原先的基础信息存储已从代码中移除。已有数据库表、卷、密码和服务器数据未执行删除或迁移;它们是否存在、有哪些记录不由本次代码修改推断。
生产继续保留预置 PostgreSQL 服务与既有数据目录,等待实际类的消费接口。应用容器不注入数据库连接变量,当前不消费该服务。部署版本通过镜像标签、release.env 和 current 链接识别,基础信息写入文件;发布时仅为旧数据做一次只读导入,不继续写数据库。详见 [宿主信息](../operations/host-information.md)。
## 验证
SQLite 检查先确认新连接没有应用表,再由测试自己创建 class_rows 验证查询和重连。真实 PostgreSQL 使用全新临时测试库,检查连接不建表,并在事务中创建提交后自动删除的临时表。这些是适配层测试,不代表特殊类已实现。
文件定位见 [storage](../../crates/storage/README.md),运行与生产资源见 [部署](../operations/deployment.md)。返回 [运行时索引](README.md)。
+1 -1
View File
@@ -73,6 +73,6 @@ use、namespace、class、agent、完整类型语法、条件/循环、比较
词法和语法错误分别标记为 Lexical/Syntax。语法错误将 PEG 的 token 下标转换成原始源码 Span,保留期望项目列表;EOF 错误使用 source.len() 处的空区间。Diagnostic::location 返回一基行列,列按 Unicode 标量计数,CRLF 只算一次换行,不按字节列或字形簇计数。
`cli parse <file>` 与 `cli parse -` 成功时输出 Rust Debug 格式的 AST,失败时在 stderr 输出来源/行/列和诊断并返回非零退出码;`-` 表示从 stdin 读到 EOF,不是 REPL。阅读与来源包装规则见 [阅读器](reader.md)。可用 --mode 选择 module/expression/interactive,用 --json 输出带 schema 标识的 AstDocument;普通输出为 AstRoot Debug 文本,稳定交换格式见 [AST 模型](ast.md)。命令不执行源码、不初始化 VM、不连接数据库;具体运行与本地禁测约定见 [运行](../operations/README.md) 和 [验证](../operations/validation.md)。
`lyra parse <file>` 与 `lyra parse -` 成功时输出 Rust Debug 格式的 AST,失败时在 stderr 输出来源/行/列和诊断并返回非零退出码;`-` 表示从 stdin 读到 EOF,不是 REPL。阅读与来源包装规则见 [阅读器](reader.md)。可用 --mode 选择 module/expression/interactive,用 --json 输出带 schema 标识的 AstDocument;普通输出为 AstRoot Debug 文本,稳定交换格式见 [AST 模型](ast.md)。命令不执行源码、不初始化 VM、不连接数据库;具体运行与本地禁测约定见 [运行](../operations/README.md) 和 [验证](../operations/validation.md)。
当前 Compiler 将 AST 经模块/函数名称分析转换为现有字节码,再交给 VM 解释执行。这是解释器内部的编译/执行准备步骤,不要求使用者先构建程序,见 [执行与版本模型](../architecture/execution-and-versions.md)。MakeFunction 已接入源码函数编译,但解析接受的二元算术和符号值仍没有执行语义;编译器会给出明确诊断,见 [编译链路](compiler.md)。源码闭包尚未实现。解析器也不是不可信代码沙箱,当前没有通用输入大小或解析递归深度限额。
+1 -1
View File
@@ -33,6 +33,6 @@ Rust panic 尚无专门的捕获恢复或执行根 RAII 清理机制。宿主捕
Vm 没有独立后台线程、暂停状态或显式 shutdown 状态机。Rust 所有权销毁它所拥有的堆和剩余载荷,包括没有先做循环回收的对象;原生共享资源继续遵循各自的 Rust 所有权。外部 ObjectRef 不保活 Vm。
Server 的 Tokio 运行时、监听器、信号处理和 Host 文件信息由宿主管理;优雅退出见 [运行](../operations/README.md)。当前宿主不持有数据库连接。数据库能力只为未来的特殊类按需提供,异步连接关闭与原生载荷生命周期仍需单独接入,见 [数据库特殊类](database-objects.md)。数据库不自动保存或恢复 VM 对象。
Server 的 Tokio 运行时、监听器、信号处理和 Host 文件信息由宿主管理;优雅退出见 [运行](../operations/README.md)。当前宿主不持有数据库连接。数据库能力只为未来的特殊类按需提供,异步连接关闭与原生载荷生命周期仍需单独接入,见 [数据库特殊类](https://gitea.entroia.com/Zayden-Jung/endorphin-hestia-server/src/branch/main/docs/runtime/database-objects.md)。数据库不自动保存或恢复 VM 对象。
源码定位:[objects/src](../../crates/objects/src/README.md) 中的 store.rs/heap.rs 负责对象生命周期,[core/src](../../crates/core/src/README.md) 中的 vm.rs/frame.rs/eval.rs 负责执行状态和帧根。返回 [运行时索引](README.md)。
+1 -1
View File
@@ -102,6 +102,6 @@ compare 的反向规则是 Lt 对应 Gt、Le 对应 Ge,Eq/Ne 保持不变。
已实现:统一对象结构、稳定地址、代际句柄、身份、显式引用计数、托管属性、原生操作覆盖、比较与哈希、原生调用、资源清理和显式循环回收。
尚未实现:Python type 系统、语言根类、继承和描述符、可变 VM 调用上下文、自动/分代/增量 GC、语言终结器/复活、完整资源限额和 VM 快照;独立 frontend 已提供阅读器、lexer/parser 与 AST,基础源码执行由独立 [Compiler](compiler.md) 接入,见 [源码前端](frontend.md);执行器已有帧数和指令数限额。数据库仅为未来特殊类提供按需连接,普通对象或宿主基础信息不自动持久化,详见 [数据库特殊类](database-objects.md)。
尚未实现:Python type 系统、语言根类、继承和描述符、可变 VM 调用上下文、自动/分代/增量 GC、语言终结器/复活、完整资源限额和 VM 快照;独立 frontend 已提供阅读器、lexer/parser 与 AST,基础源码执行由独立 [Compiler](compiler.md) 接入,见 [源码前端](frontend.md);执行器已有帧数和指令数限额。数据库仅为未来特殊类提供按需连接,普通对象或宿主基础信息不自动持久化,详见 [数据库特殊类](https://gitea.entroia.com/Zayden-Jung/endorphin-hestia-server/src/branch/main/docs/runtime/database-objects.md)。
验证范围与执行约定见 [验证](../operations/validation.md)。
+1 -1
View File
@@ -42,7 +42,7 @@ revision 是宿主返回的原样标签;Reader 只拒绝全空白标签,不
## CLI 与交互输入
CLI parse 文件通过 from_file → parse_source 输出 AST;`cli parse -` 通过 from_stream 读取 stdin 到 EOF,来源为 `<stdin>`。错误保留文件或流名及行列;文件读取与非法 UTF-8 错误在进入 lexer 前返回。parse 不执行源码;run/eval 使用相同阅读器后进入 [Compiler](compiler.md),支持范围见该契约。
CLI parse 文件通过 from_file → parse_source 输出 AST;`lyra parse -` 通过 from_stream 读取 stdin 到 EOF,来源为 `<stdin>`。错误保留文件或流名及行列;文件读取与非法 UTF-8 错误在进入 lexer 前返回。parse 不执行源码;run/eval 使用相同阅读器后进入 [Compiler](compiler.md),支持范围见该契约。
stdin 入口不是 REPL,不打印提示符、不保持求值环境,也不区分“需要继续输入”和“真正语法错误”。未来 REPL 宿主先收集一次提交的输入单元,再使用 from_text 并指定会话来源;reader 不承担交互调度。包与 namespace 的成员查找和依赖解析也不由 reader 自动完成。
+1 -1
View File
@@ -26,4 +26,4 @@ EndorReader 提供严格 UTF-8 文件/流读取和宿主 URI provider 适配
当前按 Unicode 标量值访问字符,不按人眼字形簇。预组合的“ガ”和“カ”加组合浊点可显示相似,但标量数量和序列不同;当前没有自动规范化或全角/半角转换。字符串和字典按当前内容语义处理,不为了所在地悄悄改写数据。前端当前采用的名称字符分类与源码位置规则见 [源码前端](frontend.md),名称同样不自动规范化。
宿主基础信息以 UTF-8 JSON 写入;控制键的 ASCII 命名限制属于文件信息协议,值可保存日文,详见 [宿主信息](../operations/host-information.md)。配置与 locale 见 [配置](../operations/configuration.md),返回 [运行时索引](README.md)。
宿主基础信息以 UTF-8 JSON 写入;控制键的 ASCII 命名限制属于文件信息协议,值可保存日文,详见 [宿主信息](https://gitea.entroia.com/Zayden-Jung/endorphin-hestia-server/src/branch/main/docs/operations/host-information.md)。配置与 locale 见 [配置](../operations/configuration.md),返回 [运行时索引](README.md)。
+7 -15
View File
@@ -1,18 +1,10 @@
# 检查与发布脚本
# 引擎维护脚本
所有构建、测试、冒烟和部署脚本只在用户指定的远端环境运行。generate_ast.py 是源码生成/静态同步工具,可本地使用;test_generate_ast.py 仅远端执行。本目录 README 负责定位与副作用边界;全局流程见 [验证](../docs/operations/validation.md) 和 [部署](../docs/operations/deployment.md)。
| 文件 | 入口与副作用 |
| 文件 | 职责 |
| --- | --- |
| [generate_ast.py](generate_ast.py) | 从 Endor.asdl 生成并格式化 Rust/TypeScript/JSON Schema;--write 更新产物,--check 只比较,无依赖下载或语言执行 |
| [test_generate_ast.py](test_generate_ast.py) | 远端 unittest:schema 扩展同步、递归装箱/遍历、非法定义拒绝 |
| [endor_examples.py](endor_examples.py) | 仅远端:构建 CLI,按 examples/cases.json 执行每个真实 Endor 文件,严格比较 stdout/stderr/退出码;保存源码、命令、实际输出、PASS/FAIL/NOT_RUN 的 Markdown 和 JSON,失败也写报告 |
| [test_endor_examples.py](test_endor_examples.py) | 仅远端:报告清单覆盖、精确输出、不匹配、构建失败、超时部分输出与异常时仍保存双格式报告;模拟子进程,不启动 VM |
| [check.sh](check.sh) | 先检查 ASDL 产物同步、生成器与输出报告行为,再运行 Cargo fmt 与独立 lexer.peg/grammar.peg 的 rustfmt 检查、workspace 测试、严格 Clippy;会使用 Cargo 依赖和构建产物 |
| [smoke.py](smoke.py) | 标准库 Python 脚本;构建后用 Cargo metadata 定位 cli/server 的 debug 二进制,支持 CARGO_TARGET_DIR;文件 metadata 跨进程与日文值;失效数据库变量下独立启动、不创建 SQLite 文件、HTTP 与 SIGTERM,不改生产配置 |
| [ci-ces-control.sh](ci-ces-control.sh) | 需要 ASTER_RELEASE_SHA;构建 ci 镜像、创建临时网络/PostgreSQL/检查容器;检查后独立执行 Endor 示例,失败不掩盖原检查状态;清理容器前复制两份报告到宿主 ci-output,提前失败也保留 NOT_RUN 说明;退出清理临时容器和网络,保留专属远端 Cargo 缓存卷 |
| [deploy-ces-control.sh](deploy-ces-control.sh) | ces-control root 执行;校验已构建镜像、锁定发布、保留密码和数据、验证 HTTP 与无数据库 CLI 引导后切换 current,失败尝试恢复旧版;保留镜像;应用服务 aster 内调用 cli bootstrap,基础信息改写文件;一次性只读导入旧基础信息,不覆盖新文件或删除原库 |
| README.md | 本目录入口 |
| generate_ast.py | 从 Endor.asdl 生成 Rust/TypeScript/JSON Schema;--check 静态核对 |
| test_generate_ast.py | 生成器回归,仅远端运行 |
| check.sh | 远端格式、生成器、引擎测试和 Clippy |
CI/部署使用的镜像与路径仅属于本项目,禁止改动其他项目和共享 Runner。smoke.py 临时选择端口后再启动宿主,存在小的端口竞争窗口,指定环境遇到冲突可重试。
调整变量同步 [配置](../docs/operations/configuration.md),调整端点/命令同步 [运行指南](../docs/operations/README.md),调整脚本名同步 [工作流](../.gitea/workflows/README.md)。返回 [项目入口](../README.md)。
CLI 示例实际输出脚本已归 Lyra;数据库、HTTP 冒烟与部署脚本已归 Hestia。Aster 无生产部署。返回 [项目入口](../README.md)。
-1
View File
@@ -2,7 +2,6 @@
set -eu
python3 scripts/generate_ast.py --check
python3 scripts/test_generate_ast.py
python3 scripts/test_endor_examples.py
cargo fmt --all --check
rustfmt --edition 2024 --check crates/frontend/src/lexer.peg crates/frontend/src/grammar.peg
cargo test --workspace --locked
-75
View File
@@ -1,75 +0,0 @@
#!/bin/sh
# Run only on the designated remote CI host, never on the developer's machine.
set -eu
cd "$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
release_sha=${ASTER_RELEASE_SHA:?ASTER_RELEASE_SHA is required}
case "$release_sha" in *[!0-9a-f]*|'') echo "Invalid release SHA" >&2; exit 1;; esac
test "${#release_sha}" -eq 40
ci_id="${GITHUB_RUN_ID:-manual}-$$"
network="endorphin-aster-vm-ci-$ci_id"
postgres="endorphin-aster-vm-ci-pg-$ci_id"
checks="endorphin-aster-vm-ci-checks-$ci_id"
image="endorphin-aster-vm-ci:$release_sha"
report_dir="$PWD/ci-output"
mkdir -p "$report_dir"
# Reset both outputs before any build, so even an early failure has an honest
# NOT_RUN artifact rather than a previous attempt's example results.
printf '# Endor 示例实际运行结果\n\n提交:%s\n\n状态:NOT_RUN\n\nCI 尚未完成示例执行;请查看验证步骤日志。\n' "$release_sha" > "$report_dir/endor-examples.md"
printf '{"format_version":1,"commit":"%s","status":"NOT_RUN","error":"CI did not finish example execution; inspect the verification log.","cases":[]}\n' "$release_sha" > "$report_dir/endor-examples.json"
cleanup() {
docker rm -f "$checks" "$postgres" >/dev/null 2>&1 || true
docker network rm "$network" >/dev/null 2>&1 || true
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
docker build --target ci -t "$image" .
docker network create "$network" >/dev/null
# Disposable test database: no production mounts and no published ports.
docker run -d --name "$postgres" --network "$network" --network-alias postgres \
--env POSTGRES_USER=aster --env POSTGRES_DB=aster \
--env POSTGRES_PASSWORD=aster-ci-only \
--tmpfs /var/lib/postgresql/data postgres:17-bookworm >/dev/null
ready=false
for attempt in $(seq 1 60); do
if docker exec "$postgres" pg_isready -h 127.0.0.1 -U aster -d aster >/dev/null 2>&1; then
ready=true
break
fi
sleep 1
done
if [ "$ready" != true ]; then
echo "CI PostgreSQL did not become ready" >&2
exit 1
fi
check_status=0
docker run --name "$checks" --init --network "$network" \
--mount type=volume,src=endorphin-aster-vm-ci-cargo,dst=/usr/local/cargo/registry \
--mount type=volume,src=endorphin-aster-vm-ci-target,dst=/build/target \
--env ASTER_TEST_POSTGRES_URL='postgres://aster:aster-ci-only@postgres:5432/aster?sslmode=disable' \
--env ASTER_RELEASE_SHA="$release_sha" \
"$image" sh -eu -c '
checks=0
sh scripts/check.sh || checks=$?
examples=0
python3 scripts/endor_examples.py --output-dir /build/ci-output || examples=$?
test "$checks" -eq 0
test "$examples" -eq 0
cargo test -p storage --test postgres --locked -- --ignored
python3 scripts/smoke.py
' || check_status=$?
# Copy reports before the EXIT trap removes the stopped checks container.
# Keep the original validation failure status if report extraction also fails.
copy_status=0
docker cp "$checks:/build/ci-output/endor-examples.md" "$report_dir/endor-examples.md" || copy_status=$?
docker cp "$checks:/build/ci-output/endor-examples.json" "$report_dir/endor-examples.json" || copy_status=$?
if [ "$check_status" -ne 0 ]; then
exit "$check_status"
fi
exit "$copy_status"
-119
View File
@@ -1,119 +0,0 @@
#!/bin/sh
# Deploy only the image produced after the remote database checks succeed.
set -eu
cd "$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
test "$(id -u)" -eq 0 || { echo "Deployment requires root on ces-control" >&2; exit 1; }
release_sha=${ASTER_RELEASE_SHA:?ASTER_RELEASE_SHA is required}
case "$release_sha" in *[!0-9a-f]*|'') echo "Invalid release SHA" >&2; exit 1;; esac
test "${#release_sha}" -eq 40
image="endorphin-aster-vm:$release_sha"
docker image inspect "$image" >/dev/null
umask 077
root=/opt/endorphin-aster-vm
config=/etc/endorphin-aster-vm/production.env
release="$root/releases/$release_sha"
mkdir -p "$release" /etc/endorphin-aster-vm /var/lib/endorphin-aster-vm/postgres /var/lib/endorphin-aster-vm/host
chown 10001:10001 /var/lib/endorphin-aster-vm/host
chmod 700 /var/lib/endorphin-aster-vm/host
exec 9>/var/lock/endorphin-aster-vm-deploy.lock
flock 9
if [ ! -f "$config" ]; then
password=$(openssl rand -hex 24)
printf 'POSTGRES_PASSWORD=%s\nASTER_PORT=18081\n' "$password" > "$config"
unset password
fi
chmod 600 "$config"
cp docker-compose.ces-control.yml "$release/compose.yml"
printf 'ASTER_IMAGE=%s\n' "$image" > "$release/release.env"
previous=$(readlink -f "$root/current" 2>/dev/null || true)
legacy_export=
cleanup_export() {
if [ -n "$legacy_export" ]; then rm -f "$legacy_export"; fi
}
trap cleanup_export EXIT
compose_at() {
location=$1
shift
docker compose -p endorphin-aster-vm --env-file "$config" \
--env-file "$location/release.env" -f "$location/compose.yml" "$@"
}
rollback() {
echo "Deployment failed; application data has been retained" >&2
if [ -n "$previous" ] && [ "$previous" != "$release" ] && [ -f "$previous/compose.yml" ]; then
echo "Restoring the previously healthy release" >&2
compose_at "$previous" up -d --no-build --wait --wait-timeout 180 || true
fi
}
if ! compose_at "$release" up -d --no-build --wait --wait-timeout 180; then
rollback
exit 1
fi
if ! health=$(compose_at "$release" exec -T aster curl --fail --silent http://127.0.0.1:8080/health); then
rollback
exit 1
fi
printf '%s' "$health" | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d == {"status":"ok"}' || {
rollback
exit 1
}
if ! bootstrap=$(compose_at "$release" exec -T aster cli bootstrap); then
rollback
exit 1
fi
printf '%s' "$bootstrap" | python3 -c 'import sys; s=sys.stdin.read(); assert "runtime=EndorObject type_system=disabled" in s; assert "released heap_objects=0" in s' || {
rollback
exit 1
}
# Import legacy basic information once, without changing database records.
# Files are authoritative; cli import only creates records that are missing.
if [ ! -f /var/lib/endorphin-aster-vm/host/.legacy-imported ]; then
if ! has_legacy=$(compose_at "$release" exec -T postgres env PGCLIENTENCODING=UTF8 psql -X -U aster -d aster -A -t -v ON_ERROR_STOP=1 \
-c "SELECT to_regclass('public.aster_metadata') IS NOT NULL"); then
rollback
exit 1
fi
if [ "$has_legacy" = t ]; then
legacy_export=$(mktemp)
if ! compose_at "$release" exec -T postgres env PGCLIENTENCODING=UTF8 psql -X -U aster -d aster -A -t -v ON_ERROR_STOP=1 \
-c "SELECT COALESCE(json_agg(json_build_object('key', key, 'value', value)), '[]'::json) FROM public.aster_metadata" > "$legacy_export"; then
rollback
exit 1
fi
if ! compose_at "$release" exec -T aster cli metadata import < "$legacy_export"; then
rollback
exit 1
fi
rm -f "$legacy_export"
legacy_export=
elif [ "$has_legacy" != f ]; then
echo "Cannot determine legacy information schema" >&2
rollback
exit 1
fi
if ! touch /var/lib/endorphin-aster-vm/host/.legacy-imported; then
rollback
exit 1
fi
fi
# File-backed host information; no SQL table or database write.
if ! compose_at "$release" exec -T aster cli metadata put deployment.release "$release_sha"; then
rollback
exit 1
fi
if ! actual=$(compose_at "$release" exec -T aster cli metadata get deployment.release); then
rollback
exit 1
fi
if [ "$actual" != "$release_sha" ]; then
rollback
exit 1
fi
ln -sfn "$release" "$root/current.next"
mv -Tf "$root/current.next" "$root/current"
compose_at "$release" ps
echo "Deployed release=$release_sha postgres=provisioned endpoint=127.0.0.1:18081"
echo "Persistent PostgreSQL data: /var/lib/endorphin-aster-vm/postgres"
-145
View File
@@ -1,145 +0,0 @@
#!/usr/bin/env python3
"""Run Endor example files and retain actual output on the designated CI host.
Never run locally: this builds the CLI and executes language code.
"""
import argparse
from datetime import datetime, timezone
import json
import os
from pathlib import Path
import re
import shlex
import subprocess
ROOT = Path(__file__).resolve().parents[1]
EXAMPLES = ROOT / "crates/compiler/examples"
def decoded(value):
return value.decode("utf-8", "replace") if isinstance(value, bytes) else value or ""
def invoke(command, timeout):
"""Keep stdout, stderr and failure state even for launch errors/timeouts."""
record = {"command": command, "exit_code": None, "stdout": "", "stderr": "", "error": None}
try:
result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=timeout)
record.update(exit_code=result.returncode, stdout=result.stdout, stderr=result.stderr)
except subprocess.TimeoutExpired as error:
record.update(stdout=decoded(error.stdout), stderr=decoded(error.stderr),
error=f"timed out after {timeout} seconds")
except OSError as error:
record["error"] = str(error)
return record
def load_cases():
manifest = json.loads((EXAMPLES / "cases.json").read_text(encoding="utf-8"))
if manifest.get("format_version") != 1:
raise ValueError("unsupported example manifest version")
cases = manifest["cases"]
names, sources = set(), set()
for case in cases:
if not re.fullmatch(r"[a-z][a-z0-9_-]*", case["name"]) or case["name"] in names:
raise ValueError("invalid or duplicate example name")
source = EXAMPLES / case["source"]
if source.resolve().parent != EXAMPLES.resolve() or source.suffix != ".endor":
raise ValueError("example source must be a direct .endor file")
if case["mode"] not in ("run", "eval") or not isinstance(case["expected_stdout"], str):
raise ValueError("invalid example mode or expected output")
if case["source"] in sources:
raise ValueError("duplicate example source")
case["source_text"] = source.read_text(encoding="utf-8")
names.add(case["name"])
sources.add(case["source"])
if not cases or sources != {path.name for path in EXAMPLES.glob("*.endor")}:
raise ValueError("manifest must cover every .endor example exactly once")
return cases
def fence(text, language="text"):
# Keep source/output literal even if future examples print Markdown fences.
longest = max((len(match[0]) for match in re.finditer(r"`+", text)), default=0)
delimiter = "`" * max(3, longest + 1)
return f"{delimiter}{language}\n{text}" + ("" if text.endswith("\n") else "\n") + delimiter
def markdown(report):
lines = ["# Endor 示例实际运行结果", "", f"提交:`{report['commit']}`",
f"状态:**{report['status']}**", f"生成时间(UTC):{report['generated_at']}",
"", "本报告仅覆盖 Endor 示例;整个 CI/CD 的状态以工作流结果为准。"]
if report.get("error"):
lines += ["", "报告阶段错误:", fence(report["error"])]
for stage in report["preparation"]:
lines += ["", "## 准备:" + shlex.join(stage["command"]),
f"退出码:{stage['exit_code']};错误:{stage['error'] or '无'}"]
if stage["exit_code"] != 0 or stage["error"]:
lines += ["", "stdout:", fence(stage["stdout"]), "", "stderr:", fence(stage["stderr"])]
for case in report["cases"]:
lines += ["", f"## {case['name']} — {case['status']}", "",
"命令:", fence(shlex.join(case["command"]), "sh"), "", "源码:",
fence(case["source_text"], "endor"), "", "预期 stdout:",
fence(case["expected_stdout"]), "", "实际 stdout:", fence(case["stdout"]),
"", "实际 stderr:", fence(case["stderr"]), "",
f"预期退出码:0;实际退出码:{case['exit_code']}",
f"错误:{case['error'] or '无'}"]
return "\n".join(lines) + "\n"
def collect(report):
for case in load_cases():
report["cases"].append({**case, "status": "NOT_RUN", "exit_code": None,
"stdout": "", "stderr": "", "error": None,
"expected_stderr": "", "expected_exit_code": 0,
"command": ["cli", case["mode"], "crates/compiler/examples/" + case["source"]]})
build = invoke(["cargo", "build", "-p", "cli", "--locked"], timeout=600)
report["preparation"].append(build)
if build["exit_code"] != 0 or build["error"]:
report.update(status="NOT_RUN", error="CLI build failed; examples were not executed.")
return
metadata = invoke(["cargo", "metadata", "--locked", "--no-deps", "--format-version", "1"], timeout=60)
report["preparation"].append(metadata)
if metadata["exit_code"] != 0 or metadata["error"]:
report.update(status="NOT_RUN", error="Cannot locate the compiled CLI.")
return
binary = Path(json.loads(metadata["stdout"])["target_directory"]) / "debug" / "cli"
for case in report["cases"]:
command = [str(binary), *case["command"][1:]]
actual = invoke(command, timeout=30)
case.update({key: actual[key] for key in ["exit_code", "stdout", "stderr", "error"]})
case["binary"] = str(binary)
case["status"] = "PASS" if (
case["exit_code"] == case["expected_exit_code"] and not case["error"]
and case["stdout"] == case["expected_stdout"]
and case["stderr"] == case["expected_stderr"]
) else "FAIL"
report["status"] = "PASS" if all(case["status"] == "PASS" for case in report["cases"]) else "FAIL"
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output-dir", type=Path, required=True)
args = parser.parse_args()
args.output_dir.mkdir(parents=True, exist_ok=True)
report = {"format_version": 1, "commit": os.environ.get("ASTER_RELEASE_SHA", "unknown"),
"generated_at": datetime.now(timezone.utc).isoformat(), "status": "NOT_RUN",
"error": None, "preparation": [], "cases": []}
try:
collect(report)
except Exception as error:
report.update(status="FAIL", error=str(error))
# Always persist diagnostics before returning a failing CI exit code.
args.output_dir.joinpath("endor-examples.json").write_text(
json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
rendered = markdown(report)
args.output_dir.joinpath("endor-examples.md").write_text(rendered, encoding="utf-8")
print(rendered)
return 0 if report["status"] == "PASS" else 1
if __name__ == "__main__":
raise SystemExit(main())
-88
View File
@@ -1,88 +0,0 @@
#!/usr/bin/env python3
"""Remote CI end-to-end smoke test, no external database required."""
import json
import os
from pathlib import Path
import signal
import socket
import subprocess
import tempfile
import time
import urllib.request
ROOT = Path(__file__).resolve().parents[1]
def run(*args, env):
return subprocess.run(args, cwd=ROOT, env=env, check=True, text=True, capture_output=True).stdout.strip()
def main():
env = os.environ.copy()
run("cargo", "build", "--workspace", "--locked", env=env)
# Cargo may use a configured CARGO_TARGET_DIR; discover the actual binaries.
metadata = json.loads(run("cargo", "metadata", "--no-deps", "--format-version", "1", env=env))
binary_dir = Path(metadata["target_directory"]) / "debug"
cli = str(binary_dir / "cli")
server = str(binary_dir / "server")
with tempfile.TemporaryDirectory(prefix="aster-smoke-") as directory:
env["ASTER_STATE_DIR"] = str(Path(directory) / "host-info")
env["SQLITE_PATH"] = str(Path(directory) / "nested" / "aster.db")
# Neither executable may use database settings for basic VM information.
env["DATABASE_URL"] = "invalid-database-url"
bootstrap = run(cli, "bootstrap", env=env)
assert "runtime=EndorObject type_system=disabled" in bootstrap, bootstrap
assert "objects_distinct=true heap_objects=2" in bootstrap, bootstrap
assert "released heap_objects=0" in bootstrap, bootstrap
assert "stored (file)" in run(cli, "metadata", "put", "project.note", "日本語・中文・😀", env=env)
assert run(cli, "metadata", "get", "project.note", env=env) == "日本語・中文・😀"
legacy = json.dumps([{"key": "project.note", "value": "old"},
{"key": "legacy.note", "value": "preserved"}])
subprocess.run([cli, "metadata", "import"], input=legacy, cwd=ROOT, env=env,
check=True, text=True, capture_output=True)
assert run(cli, "metadata", "get", "project.note", env=env) == "日本語・中文・😀"
assert run(cli, "metadata", "get", "legacy.note", env=env) == "preserved"
assert not Path(env["SQLITE_PATH"]).exists(), "CLI must not open a database"
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
env["ASTER_LISTEN"] = f"127.0.0.1:{port}"
with tempfile.TemporaryFile(mode="w+") as log:
process = subprocess.Popen([server], cwd=ROOT, env=env, stdout=log, stderr=log)
try:
deadline = time.monotonic() + 15
while True:
try:
with urllib.request.urlopen(f"http://127.0.0.1:{port}/health", timeout=1) as response:
health = json.load(response)
break
except (OSError, ValueError):
if process.poll() is not None or time.monotonic() >= deadline:
log.seek(0)
raise AssertionError(f"server did not become healthy: {log.read()}")
time.sleep(0.1)
assert health == {"status": "ok"}, health
for _ in range(2):
with urllib.request.urlopen(f"http://127.0.0.1:{port}/bootstrap", timeout=2) as response:
info = json.load(response)
assert info == {"object_model": "EndorObject", "type_system": False, "heap_objects": 0}, info
finally:
process.send_signal(signal.SIGTERM)
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
raise AssertionError("server did not shut down gracefully")
assert process.returncode == 0, process.returncode
assert not Path(env["SQLITE_PATH"]).exists(), "Server must not open a database"
assert run(cli, "metadata", "get", "host.version", env=env) == "0.1.0"
record = json.loads((Path(env["ASTER_STATE_DIR"]) / "host.version.json").read_text(encoding="utf-8"))
assert record == {"format_version": 1, "key": "host.version", "value": "0.1.0"}
assert run(cli, "metadata", "get", "project.note", env=env) == "日本語・中文・😀"
print("PASS: no implicit database access; file metadata persistence; HTTP health/bootstrap; SIGTERM shutdown")
if __name__ == "__main__":
main()
-80
View File
@@ -1,80 +0,0 @@
"""Remote-only regression checks for example output capture and failure reports."""
import contextlib
import io
import json
from pathlib import Path
import subprocess
import tempfile
import unittest
from unittest.mock import patch
import endor_examples as examples
def result(stdout="", code=0):
return {"command": ["mock"], "exit_code": code, "stdout": stdout, "stderr": "", "error": None}
def report():
return {"format_version": 1, "commit": "test", "generated_at": "test", "status": "NOT_RUN",
"preparation": [], "cases": [], "error": None}
class ExampleReportTests(unittest.TestCase):
def test_manifest_runs_every_source_and_captures_exact_output(self):
cases = examples.load_cases()
responses = [result(), result(json.dumps({"target_directory": "/tmp/ci-target"}))]
responses += [result(case["expected_stdout"]) for case in cases]
collected = report()
with patch.object(examples, "invoke", side_effect=responses) as invoke:
examples.collect(collected)
self.assertEqual(collected["status"], "PASS")
self.assertEqual(invoke.call_count, len(cases) + 2)
for actual, expected in zip(collected["cases"], cases):
self.assertEqual(actual["stdout"], expected["expected_stdout"])
self.assertEqual(actual["status"], "PASS")
self.assertEqual(actual["binary"], "/tmp/ci-target/debug/cli")
def test_output_mismatch_fails_and_build_failure_never_claims_examples_ran(self):
cases = examples.load_cases()
responses = [result(), result(json.dumps({"target_directory": "/tmp/target"}))]
responses += [result("wrong\n")] + [result(case["expected_stdout"]) for case in cases[1:]]
collected = report()
with patch.object(examples, "invoke", side_effect=responses):
examples.collect(collected)
self.assertEqual(collected["status"], "FAIL")
self.assertEqual(collected["cases"][0]["stdout"], "wrong\n")
self.assertEqual(collected["cases"][0]["status"], "FAIL")
self.assertIn("wrong", examples.markdown(collected))
collected = report()
with patch.object(examples, "invoke", return_value=result(code=1)) as invoke:
examples.collect(collected)
self.assertEqual(invoke.call_count, 1)
self.assertEqual(collected["status"], "NOT_RUN")
self.assertTrue(all(case["status"] == "NOT_RUN" for case in collected["cases"]))
def test_timeout_keeps_partial_utf8_stdout_and_stderr(self):
failure = subprocess.TimeoutExpired(["cli"], 1, output="日".encode(), stderr=b"partial error")
with patch.object(examples.subprocess, "run", side_effect=failure):
captured = examples.invoke(["cli"], 1)
self.assertIsNone(captured["exit_code"])
self.assertEqual(captured["stdout"], "日")
self.assertEqual(captured["stderr"], "partial error")
self.assertIn("timed out", captured["error"])
def test_unexpected_failure_still_writes_both_artifacts(self):
with tempfile.TemporaryDirectory() as directory:
with patch("sys.argv", ["endor_examples.py", "--output-dir", directory]), \
patch.object(examples, "collect", side_effect=ValueError("broken manifest")), \
contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(examples.main(), 1)
payload = json.loads(Path(directory, "endor-examples.json").read_text(encoding="utf-8"))
self.assertEqual(payload["status"], "FAIL")
self.assertEqual(payload["error"], "broken manifest")
self.assertIn("broken manifest", Path(directory, "endor-examples.md").read_text(encoding="utf-8"))
self.assertTrue(examples.fence("```\n日").startswith("````text\n"))
if __name__ == "__main__":
unittest.main()