refactor: separate language engine from CLI and server hosts
Verify Aster engine / verify (push) Failing after 0s
Verify Aster engine / verify (push) Failing after 0s
This commit is contained in:
@@ -8,3 +8,8 @@ ci-output
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
**/.git
|
||||
**/target
|
||||
**/AGENTS.md
|
||||
**/work
|
||||
**/ci-output
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+1
-6
@@ -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
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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
@@ -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)。
|
||||
|
||||
@@ -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
|
||||
@@ -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)。
|
||||
@@ -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)。
|
||||
@@ -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)?,
|
||||
})
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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)。
|
||||
@@ -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:"));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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)。
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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
|
||||
@@ -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)。
|
||||
@@ -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)。
|
||||
@@ -1,4 +0,0 @@
|
||||
//! Host information stored in UTF-8 JSON files, independently of the VM and SQL.
|
||||
mod metadata;
|
||||
|
||||
pub use metadata::MetadataStore;
|
||||
@@ -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")))
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# Host 文件信息检查
|
||||
|
||||
[metadata.rs](metadata.rs) 检查缺失读取不建目录、日文/中文/emoji 值和日文路径、重开与同键替换、非法键不越界、坏 JSON/版本/键不匹配不改原文件、并发替换可见完整记录,以及旧记录幂等导入和保留更新值。
|
||||
|
||||
检查使用临时目录,仅在指定远端环境运行。本地不执行文件行为测试;当前是否通过应以对应提交的 CI 为准。命令见 [验证](../../../docs/operations/validation.md),返回 [host](../README.md)。
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)。
|
||||
@@ -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)。
|
||||
@@ -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;
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)。
|
||||
@@ -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)。
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)。
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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:
|
||||
@@ -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
@@ -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
@@ -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)。
|
||||
|
||||
@@ -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 使用者的程序构建步骤。
|
||||
|
||||
## 名称、资源与修订
|
||||
|
||||
|
||||
@@ -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 结果就写未核实,不能继承上次通过的结论。
|
||||
|
||||
不是每次提交都要给所有文档改日期或复制版本号;只有相关内容发生变化时更新相应文档。提交及代码审查描述说明目的和验证,文档保留长期可查询的当前行为。
|
||||
|
||||
@@ -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)。下一步按具体语言需求完善引擎,宿主调度和渲染在各宿主仓库推进。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
@@ -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)。
|
||||
@@ -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
@@ -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
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
@@ -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)。源码闭包尚未实现。解析器也不是不可信代码沙箱,当前没有通用输入大小或解析递归深度限额。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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 自动完成。
|
||||
|
||||
|
||||
@@ -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
@@ -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)。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user