PPysolate SpineREAD THE CODE · UNDERSTAND THE SYSTEM
章节目录
LEARNING NOTES / 01

学习顺序与完整路径

从两行 Python 到 Host 边界,逐步读懂实现。

当前版本:普通执行 + whole-program PLM + prefix,附可选 prepared full-copy。 本文描述当前代码,不是旧 Pysolate 的缩写。第 1–9 节先讲普通路径,第 10 节接入实际 PLM。Prefix 已接入同一套 Future;本版用 full-copy 交代基线/隔离,不实现 Linux COW,不宣称共享页收益。后续每个实现阶段同时更新本手册;不要求你先读旧仓库,也不做性能测量。

0. 学习顺序

你已经了解 Python 和研究目标,这里先补实际实现所需的背景,不从 C 指针开始。

  1. 跑一个结果,解释两行 Python:读 §1。能区分输入、单价、总价,以及它们分别在哪里计算。
  2. 建立执行模型:读 §2。能区分 CPython、Wasm、wazero、WASI,以及 Host 和 Guest。
  3. 跟完一次工具调用:读 §3–4,对照 guest/bootstrap.pybridge.gocmd/demo/main.go。能解释错误怎样回到 Python。
  4. 理解每次 Run 的状态:读 §5,对照 runner.go。能解释为什么不串状态、谁负责释放。
  5. 再读 C 和构建:读 §6–7。能解释指针、长度、引用计数,以及改哪种文件需要重建 Wasm。
  6. 用测试检查理解:读 §8–9。先自己判断改动位置,再和 AI 实现。
  7. 学习 PLM:读 §10 链接的实际实现章节,比较 prepare 启动和 resolve 交付。
  8. 学习 prefix:再读 流式前缀章节,理解源码接收、最终执行与缓存领取的边界。
  9. 可选背景prepared-copy 只说明干净基线与私有复制,可先跳过。

后续按 pysolate-explained 的方式用 Astro 发布;当前 Markdown 是内容源,尚未搭站或发布。你可以稍后集中学习,不需要每阶段停下来验课。

演示入口和备用例子见 面试操作顺序五个边界例子

不用一口气读完。每一层的验收标准是能用自己的话解释,不是记住 API 拼写。源码链接定位到文件,正文用函数名定位,避免行号变化后讲义失效。

1. 先跑:输入到输出

在这个独立仓库根目录运行:

go run ./cmd/demo
# {"item": "book", "total": 42}
go run ./cmd/demo -inputs '{"item":"pen","quantity":5}'
# {"item": "pen", "total": 15}

这两条命令已实际运行过。Go CLI 读取 examples/lookup.py

price = tool("lookup", key=inputs["item"])
result = {"item": inputs["item"], "total": price * inputs["quantity"]}

默认输入是 {"item":"book","quantity":2}inputs 是 bootstrap 放进执行环境的 Python 对象,不是 Python 自动提供的变量。

第一行读取 inputs["item"] 得到 book,然后调用 tool。真正的价格表在 Go Host 中:book: 21pen: 3。Host 返回 21,Python 将它赋给 price。第二行的 21 * 2 在 Guest 的 CPython 中计算。

result 是这个项目规定的输出变量。bootstrap 执行完模块后读取它并编码为 JSON;没设置时返回 None,即 JSON nullprint() 是 stdout,不是 result;Go 的 Output 分别保存这两者。

改自己的 Python 程序时用 -source path/to/file.py。这是更换输入源码,不是修改解释器,不需要重建 Wasm

**先回答:**如果把第二行改为 result = price,总价逻辑消失在哪一侧?哪些 Go/C 文件完全不用改?

2. 背景:到底是谁在执行 Python?

2.1 CPython、Wasm、wazero 是三件事

  • CPython 是 Python 的参考解释器,主要用 C 实现。本项目使用 CPython 3.14。
  • WebAssembly(Wasm) 是一种可验证的二进制指令格式。这里把 CPython 和 C bridge 编译、链接成 Wasm。
  • wazero 是 Go 写的 Wasm runtime。Go Host 用它加载模块、编译 Wasm、实例化并调用导出函数。

因此,这里不是把用户 Python 源码翻译成 Wasm。用户源码由已经在 Wasm 内运行的 CPython compile/exec 执行。也没有调用本机 Python 子进程来伪装 Guest。

不要混淆两次编译:wazero 的 CompileModule 处理 Wasm 指令;Python 的 compile 处理这次收到的 Python 源码,生成 CPython 可执行的 code object。

2.2 Host / Guest

Host 是外面的 Go 程序,持有工具注册表、价格数据和 runtime。未来真实工具使用的凭据也应留在 Host。

Guest 是 Wasm 实例中的 CPython、标准库和用户程序。每次 Run 创建新实例;可共享编译代码,但不会因此共享 Python 的堆、模块或 global。

Host 可以主动读写 Guest 的线性内存;Guest 不能把一个数字直接当成 Go 地址来访问 Host 内存。Guest 要请求外部能力,必须调用 runtime 接入的函数。

2.3 WASI 和嵌入式文件系统

WASI 给 Wasm 提供一组与系统相关的接口,例如文件读写和退出。安装 WASI 不代表把整个宿主操作系统都授权给 Guest。

本项目没有 preopen(预先授权挂载)宿主目录,没有传入宿主环境变量或配置外部 socket;不能据此读取 Host 的 /etc/passwd 或随意启动宿主进程。部分基础 WASI 接口仍存在,例如 stdout 和 Guest 退出,不应说成“Guest 完全没有系统接口”。

CPython 又需要 json 等标准库文件。构建时用 wasi-vfs 把这些文件打包进 Wasm,Guest 内路径是 /usr/lib/python3.14。这不等于挂载 Host 的同名目录。

Python 的名字隐藏不是安全边界。用户即使自行 import _spine 调底层入口,仍只能经过 Host 注册表获得工具权限。

2.4 Reactor 和初始化

这个 Wasm 是 reactor:由 Host 显式调用导出函数,而不是启动一个等待 stdin 的 Python CLI。

  • _initialize:工具链提供的初始化入口,完成 C/WASI/VFS 所需初始化。
  • init:我们在 C 中写的入口,启动 CPython、导入 bootstrap。
  • execute:执行这一轮输入。

这三个名字不能互换。wazero 实例化完成也不意味着 CPython 已经初始化。

3. 一次调用怎样往返?

Go Runner.Run
  → C execute
    → Python bootstrap.execute
      → 用户 Python
        → Python tool
          → C python_call
            → Go hostCall
              → Go lookup
            ← JSON 结果或错误
          ← Python bytes
        ← Python 值,或在这里 raise RuntimeError
      → 继续 Python 控制流
    → 把 result 编码成 bytes
  → 返回结果 buffer 的位置和长度
← Go Output.Value / error

3.1 Python bootstrap:执行约定

guest/bootstrap.py,先看 execute,再看 tool

execute(request) 接收 JSON 文本,解析得到 sourceinputs。它建立 scope

  • __name__ = "__main__",允许常见的模块入口判断;
  • inputs 是这次输入;
  • tool 是项目提供的调用函数。

exec(compile(...), scope) 执行普通 Python 模块。代码包含 if、循环、函数、try/except 都由 CPython 处理;普通模式不运行 AST 优化器。scope 也不是权限沙箱;只负责把执行所需的变量放到一起。

执行完成后,scope.get("result") 决定输出。外层 except BaseException 把未捕获异常的类型和消息编码成错误;它也覆盖 SystemExit。当前没有完整 traceback。finally 刷新 stdout,避免缓冲的 print 随实例关闭消失。如果 bootstrap 自己失败,C bridge 会向 stderr 报错,Go 将其作为 bridge 失败返回。

tool(name, **args) 把调用变成 {"tool":"lookup","args":{"key":"book"}},调用 C 扩展 _spine.call,再通过 decode 解码响应。存在 error 字段就 raise RuntimeError,否则返回 value

这里使用“字段是否存在”,而不是“错误消息是否非空”:空字符串也可以是合法错误消息;value: null 则是成功返回 None

3.2 Host bridge:权限与参数

bridge.goTool 是函数类型:

type Tool func(ctx context.Context, args json.RawMessage) (any, error)

含义是:工具接收取消上下文和 JSON 参数,返回可编码的值或错误。不是大接口,也没有 Broker 子类。

hostCall 通过 readRequestinvokeencodeResponse 完成以下步骤。这几个普通函数也被 PLM 路径复用,不是通用 Broker:

  1. 根据指针、长度读取 Guest 线性内存,检查范围和消息上限。
  2. json.Unmarshal 解码请求,得到工具名和参数。
  3. r.tools[request.Tool]。没有注册就报 unknown tool,不做自动猜测或回退。
  4. 同步执行 Go 函数,得到值或错误。
  5. 编码响应,交给 writeResponse 写回 Guest。

json.RawMessage 是保留原始 JSON 内容的字节切片。它的解码方法会复制参数,所以工具不持有 Guest 的可变内存视图。工具应把收到的 RawMessage 当作只读数据,因为 Run 还可能保留它用于 Future 匹配;需要修改时先解码成自己的结构,不原地改传入字节。外层请求只解析一次;具体 lookup 再解析自己的参数类型,这是两层不同的协议结构,不是反复证明同一对象。

响应的 Error*stringnil 表示无错误,指向 "" 表示有一个空消息错误。Value 不用 omitempty,否则成功的 nil 会被省略。这个区别已经由真实测试覆盖。

工具返回的 Go 值不能编码为 JSON 时,bridge 返回明确工具错误,不假造一个结果。

3.3 CLI:业务不放进解释器

cmd/demo/main.go

main 读取 -wasm-source-inputs,以及可选的 -plm-prefix-show-transformed-prepared-copy,调用 run。失败打印到 stderr 并非零退出。

run 读取 Wasm 和 Python 文件,解析输入 JSON;demoTools 建立固定价格表及工具注册表。lookup 是一个捕获 catalog 的 Go 闭包;它负责解析 key、查价格、决定 missing key 的错误,不是 bootstrap 或 C bridge 的职责。

然后它建立演示用的 30 秒 context,调用 NewRun(开启优化时为 RunPLM),打印 stdout 和结果,最后关闭 Runner。30 秒是 CLI 的选择,不是 CPython 的限制;测试中的 2 秒只是取消测试条件,不是性能指标。

**练习:**新增 discount 工具时,你会先改 cmd/demo/main.go 还是 guest/runtime.c?先写下理由,不急着实现。

4. 读 Go 所需的最少背景

不需要先学完整 Go。当前代码遇到这些语法时,按下列含义读:

  • struct:一组具名字段,例如 Runner 持有什么、Output 返回什么。
  • func (r *Runner) Run(...):属于 Runner 的方法,r 是当前对象;*Runner 是指针,不是复制整个 Runner。
  • []byte:字节切片,包含底层存储的位置和长度等信息;不能假设拿到切片就获得独立副本。
  • map[string]Tool:字符串到函数的注册表,类似 Python 的 dict of callables。
  • any:允许不同 Go 值的接口类型。能放入 any 不代表能转成 JSON,例如 channel 不能直接作为结果。
  • (value, error):Go 用返回值表达正常错误;err != nil 时由调用者决定处理方式。这和 Python exception 不同,所以 bridge 必须显式转换。
  • defer:当前函数退出时执行,后注册的先执行。不只是成功时执行。
  • context.Context:传递 deadline/取消信号,不是 Python globals,也不自动强杀任意 Go 函数。
  • 闭包:函数保留外层变量。例如 lookup 访问 catalogfail 访问本轮 stdout/stderr。

uint32/uint64 在这里主要用于 Wasm ABI。^uint32(0) 是所有位为 1 的 32 位值,跨到 C 的 int32_t 时被解释为 -1,表示底层传输失败。

5. Runner:谁拥有状态,怎样结束?

runner.go。推荐顺序:Runner / OutputNewRun → 最后几个 helper。

5.1 Runner 和 Output

Runner 拥有:

  • runtime:wazero runtime;
  • code:已编译 Wasm,可供多次实例化;
  • tools:固定工具注册表;
  • earlyReads:Host 明确允许提前执行的工具名集合,语义见第 10 节。
  • image:可选的干净初始化内存基线;nil 为 fresh 初始化,有值时每个实例全量复制一份。

Output 返回 ValueStdout,以及 PLM 模式下供讲解的 TransformedValuejson.RawMessage 保留 JSON,而不是强迫库层猜测业务结果的 Go 类型。

Runner 不持有可复用的 Python 实例。 每次 Run 的实例、I/O buffer 和输入输出分配都在方法局部。不要把“重用代码”和“重用 Python 状态”混为一谈。

5.2 New:准备不变部分

New 建立 wazero runtime,并启用 WithCloseOnContextDone(true),让执行能响应 context 取消。复制工具注册表意味着调用者以后替换原 map 的条目不会改变 Runner;但复制的是函数值,不是深拷贝闭包捕获的数据

它依次安装 WASI、注册名为 spine 的 Host 模块及其 call/prepare/resolve 函数、编译传入的 Wasm,并确认 _initialize/init/alloc/release/execute 这些入口存在。失败时关闭已创建的 runtime。

这里检查导出名是为了给错误 artifact 一个直接错误;它不是给任意未知 Wasm 建完整兼容性证明。项目运行自己构建的 Guest。

5.3 Run:按代码顺序跟一次

RunRunPLMRunPrefix 共用一个 run 生命周期;前两者传完整源码,prefix 在同一实例里先接收片段。它先建立本轮 runState 和子 context;普通模式不启动 Future,PLM/prefix 的任务也由这里收尾。

  1. 编码输入。sourceinputs 变成 JSON。不能编码或超过请求上限时,尚未创建 Guest,直接返回。
  2. 创建私有实例。 为本轮创建 stdout/stderr buffer,交给 newGuest(prepared.go)使用编译 code 实例化。空模块名避免名字争用,WithStartFunctions() 禁用默认启动调用。
  3. 初始化。 newGuest_initialize;fresh 模式再调 C init,prepared 模式则全量复制干净 image。初始化失败由 helper 关闭实例并报错。
  4. 登记关闭。 newGuest 成功返回后,run 登记 defer m.Close(context.Background()),覆盖后面的成功和失败出口。
  5. 写请求。 共用 callWithBytes(在 prefix.go)调用 Guest alloc,再 Memory().Write 写入 JSON;调用返回时释放请求。
  6. 执行。 调 Guest execute(ptr, len),它最终进入 Python bootstrap。
  7. 读响应。 将返回的 64 位数拆成结果位置、长度,检查可读范围和结果上限,解码 JSON,登记结果 buffer 的 release
  8. 交付。 返回 Output;存在错误则返回 Go error。json.RawMessage 已复制结果,不会在 Guest 关闭后悬空。

请求已由 callWithBytes 在调用返回时释放;执行结果是 C 单独分配的副本。之后退出时的顺序是结果释放 → 实例关闭 → runState 取消/等待任务。普通模式没有提前任务,PLM worker 也不持有 Guest 内存。如果 trap/取消使 Guest 已不能再调用 release,仍会执行实例关闭,回收这份 Guest 的资源;不是承诺取消后还能执行任意 Guest 清理代码。

fail 只是局部 helper,把已有 stdout 和 stderr 带回调用者,没有状态机或自动重试。

5.4 Close、boundedText、writeResponse

Runner.Close 关闭 runtime 及编译代码等资源;调用者必须等自己的 Run 调用结束后再 Close。这是当前所有权约定,不是额外实现了并发关闭协调器。

boundedText.Write 限制单个 stdout/stderr buffer 的累计大小。嵌入 strings.Builder 复用字符串拼接,覆盖 Write 加入上限检查。写入超限返回错误,不静默截断成看似完整的输出。

writeResponse 是 Host 写回工具响应的地方。检查消息大小、Guest 提供的 buffer 容量和地址范围;失败返回全 1 的状态码,C 将它转为 Python 错误。

这些是跨 Wasm 内存边界的检查,不同于对可信内部对象反复编码、hash、批准。

5.5 隔离与取消的准确范围

CPython 堆、模块、C static 数据和线性内存属于实例。测试先修改 builtins,下一轮看不到,是因为换了整个 Guest,不是靠删几个 global reset。

Host 工具不是每次 Run 自动隔离的。如果闭包捕获可变 map,多个调用能看到同一份 Host 状态;工具实现者负责语义和并发。当前演示价格表只读。

Wasm 无限循环能被 deadline 中止。Go 工具若调用网络,应把 ctx 传进去;若工具卡在不响应取消的 Go 函数,当前设计不能强杀它。现在普通 Run 不启动异步任务。RunPLM 已增加 Run-owned Future,但仍没有工具失败后的自动重试。

**练习:**如果把 Python 实例存进 Runner,每次只换 inputsbuiltins.secret 测试为什么可能失败?清空 scope 是否足够?

6. C 与 ABI:为什么需要指针和长度?

guest/runtime.c。先理解 ABI,再读引用计数。

6.1 线性内存与 ABI

ABI(Application Binary Interface)约定两边怎样用底层参数传值。这里不直接把 Python dict 或 Go map 当 Wasm 参数,而是传 JSON bytes 的位置和长度。

wasm32 的指针是 Guest 线性内存中的 32 位偏移,不是 Host 的机器地址。ptr=1000, len=20 表示 Guest 内存从偏移 1000 开始的 20 个字节;wazero Memory.Read/Write 将这个约定落实为有界访问。

  • alloc(size) 返回 Guest 内的空间。
  • release(ptr) 释放 Guest 内的分配。
  • execute(ptr, size) 返回 (length << 32) | pointer

最后一项把两个 32 位数装入一个 64 位整数:低 32 位是地址,高 32 位是长度。它不是 hash,也不是 certificate;只是这个短 ABI 的返回约定。

6.2 宏、methods、module、init_spine

EXPORT(name) 给函数标记 Wasm 导出名,让 Go 找到它。import_module("spine") / import_name("call") 声明外部导入;实际实现就是 Go 注册的 spine.call

MAX_MESSAGE 是 C 侧工具消息容量,与 Go maxMessage 同为 1 MiB。以后改变消息约定要一起修改,不为这两个常量另造生成器。

methods 列表把 Python 方法名 call 对应到 C 函数 python_call;阶段 2 又增加 prepare/resolve,详见 PLM 章节。module 描述名为 _spine 的内置扩展模块;init_spine 创建它。这不是一个另行加载的宿主 .so

execute_fn 缓存 bootstrap 的 execute Python 对象。它是 Guest C static 状态,每个新实例各有一份,不是 Go 的全局变量。

6.3 init:启动 CPython

PyImport_AppendInittab 在启动前注册 _spinePyConfig_InitIsolatedConfig 建立隔离的解释器配置;当前关闭 site 导入、字节码文件写入,并明确指定嵌入标准库路径。

Py_InitializeFromConfig 启动解释器,随后清理配置对象。再 import spine_bootstrap,取得 execute 函数保存到 execute_fn。失败打印 CPython 错误并返回非零状态。

fresh Guest 调用一次 init;prepared 模式只有参考 Guest 调 init,后续实例复制其基线。两者都不实现同实例反复初始化协议。

6.4 python_call:CPython 到 Host

PyArg_ParseTuple(..., "s#", ...) 取得请求字符串数据及长度。PY_SSIZE_T_CLEANPy_ssize_t 配合 CPython 的带长度 API 使用。

接着分配临时响应 buffer,调用导入的 Host 函数。Host 在这个 buffer 写入 JSON,并返回写入长度。C 把响应复制成 Python bytes 后释放临时 buffer。

当 ABI 状态不合法时,PyErr_SetString 设置 Python 异常,返回 NULL 告知 CPython 调用失败。这不同于把字符串 "error" 当成功返回值。

6.5 execute:CPython 到 Go

C execute 调用缓存的 Python execute_fn。bootstrap 返回 bytes;C 获得其内容后,复制进独立 malloc buffer,再减少 Python 对象引用。Go 随后读取并释放这个 buffer。

为什么不直接返回 Python bytes 内部地址?因为 Python 对象被释放后,借出的指针可能失效。明确复制和所有权比跨语言保持一条隐式活引用简单。

Py_DECREF 是 CPython 引用计数管理,free 是 C 分配管理,两者不能混用。bootstrap 模块对象取得函数后减少引用;execute_fn 的长期引用留到实例结束。当前不调用 Py_Finalize 后复用实例,而是关闭整个 Guest;不承诺关闭时运行所有 Python atexit 逻辑。

函数返回 0 表示桥接失败;正常返回的 buffer 归 Go 调用者负责。工具临时 buffer 则只属于一次 python_call

**练习:**如果删掉 memcpy,直接返回 PyBytes 的内容地址,同时保留 Py_DECREF(result),生命周期错在哪里?先说清楚,不要实际引入悬空指针。

7. 构建:源码怎样变成真正运行的 artifact?

7.1 哪些改动需要重建?

  • examples/lookup.py 或换 -source:这是运行输入,不重建 Wasm。
  • 改 Go Host:重新 go rungo test 即可。
  • guest/bootstrap.pyguest/runtime.c 或 Guest 链接配置:必须重建、打包并替换 dist/spine.wasm,再跑真实测试。

你编辑的 guest/bootstrap.py 不会被运行中的 Guest 从宿主目录自动读取。它位于 artifact 内部。只改源码却运行旧 Wasm,是这里最容易造成假验证的错误。

阶段 2 已实际重新链接 C bridge,并把新 bootstrap 和 plm.py 打包进入新的 Wasm;不是运行阶段 1 的旧 artifact。CPython 静态库仍复用已有构建输入,不从头编译解释器。

7.2 build-guest.sh 逐段读

build-guest.sh。它只做一个有界构建,不管理云机器,不调用旧 runtime。

  1. set -euo pipefail:失败、未定义变量和管道失败不能静默忽略。读取脚本根目录,以及 SPINE_BUILD_INPUTS 指定的已有工具链/CPython 构建输入;检查 Linux x86_64。
  2. 在本仓库 build/ 创建临时目录,退出时清理。只操作该临时目录,不清空共享 CPython 缓存或旧论文 dist。
  3. 用 wasi-sdk 的 clang 编译本项目 C bridge,并链接 CPython 静态库、其所需支持库、wasi-vfs。静态库是已编译对象的集合,这一步不重新编译 CPython 源码。
  4. -mexec-model=reactor 选择 reactor;导出内存、设置初始/最大内存与 C stack 大小,生成 raw Wasm。
  5. Python 小段复制 CPython 标准库,排除测试/cache 文件,把本项目 bootstrap 复制为 spine_bootstrap.py,并复制 plm.py
  6. wasi-vfs 将标准库目录嵌入 raw Wasm。wasm-tools validate 检查 Wasm 格式;成功后才替换 dist/spine.wasm

validate 不代表工具语义已正确,仍需执行 Go 的真实 Guest 测试。

7.3 构建依赖的范围

这个仓库是独立 Go module,源码和运行不依赖旧仓库;构建 Guest 仍依赖 CPython/WASI 工具链和预编译库,不把这些大型输入塞进 Git。

在具有这些输入的 Linux x86_64 环境,运行:

SPINE_BUILD_INPUTS=/path/to/extracted-build-inputs bash build-guest.sh

这里的 /path/to/... 是需要替换的路径,不是已经执行成功的具体命令。构建输入布局、当前机器上的可用路径见 构建与来源说明。当前脚本不负责从零下载/编译 CPython;空环境不能只靠 go mod tidy 生成 Guest。

本机已有 artifact,所以日常学习只需要 Go 与 dist/spine.wasm。换到全新 clone 时,Git 不包含这个生成文件,需要取回已构建 artifact 或在合适的 Linux 环境构建。

7.4 限制常量来自哪里?

  • 请求、执行结果、单次工具消息,以及 stdout/stderr 各有 1 MiB 上限,定义于 Go/C。
  • Guest 初始线性内存 128 MiB,最大 512 MiB;C stack 16 MiB,来自链接参数。线性内存中的 stack 不是额外再加一份 Host 内存。

这些是当前演示的工程选择,不是 Wasm 标准强制值,也不是实测内存占用。上限检查不等于从输入到输出全链路的 Host 内存配额:例如 JSON 序列化先产生数据,再检查大小。可信 Go 工具本身也不被 Guest 内存上限限制。

没有承诺完整 CPython 包兼容、任意 native 扩展、任意精度 Python 数值的无损 Go JSON 往返,或完整终端交互。这些若成为新需求,先讨论需要保留的语义。

8. 测试:每个检查在回答什么?

本节先读 runner_test.go 的普通路径测试;PLM 的 13 个真实路径子测试与 6 个 AST 单测见第 10 节。TestRealGuest 先读取真实 artifact,缺失直接失败,不 skip、不换成本机 Python。它建一个 Runner,顺序运行多个新 Guest。

表驱动部分列出输入源码、预期 JSON 或预期错误;比较 JSON 时忽略空白格式,不把打印风格当业务语义。case 名称与含义如下:

  1. python:在 Guest 内计算,并报告 CPython 3.14。
  2. host:真实工具结果与输入组合得到 42。
  3. null tool result:Host 成功的 nil 交付为 Python None。
  4. caught tool error:Python 在工具调用位置捕获 RuntimeError。
  5. uncaught tool error:未捕获工具错误返回 Go。
  6. empty tool error:空错误消息不能被省略为成功。
  7. malformed tool request:自行调用底层 _spine.call 的坏 JSON 不绕过解析。
  8. unknown tool:注册表不包含的工具拒绝。
  9. python error:除零不会变成成功结果。
  10. syntax error:源码编译失败能报告。
  11. non JSON result:Python set 不能静默当作 JSON 成功。
  12. private state write:一轮修改 builtins。
  13. private state read:下一轮看不到上轮改动。
  14. no host filesystem:尝试读 /etc/passwd 失败。这个具体测试不是完整安全审计。
  15. stdout:print 与 result 分开交付。
  16. deadline:中止 Guest 无限循环,随后再次运行仍成功;没有声称可以强杀任意 Go 工具。

工具调用计数按每个用例检查预期次数;单独用 -run 筛选子测试也能正确验证,不再依赖整组全部运行。隔离检查现在按顺序执行,不要随手加 t.Parallel:测试共享 Host 计数器,而写/读用例本身也有先后关系。

执行:

go test ./... -count=1 -v
go vet ./...

go test 的用时是测试框架输出,不是性能实验。本项目只检查结果、失败和隔离语义。

常见失败怎么定位

  • open dist/spine.wasm: no such file:缺少构建输出,不是 Python 语法错。
  • missing Guest export:artifact 与这套 ABI 不匹配,先查实际文件。
  • CPython initialization failed:查 Guest stderr、标准库打包与初始化。
  • unknown tool:检查注册表及调用名称,不先改 C。
  • RuntimeError: missing key:业务工具自己的错误,是否捕获由 Python 决定。
  • Guest execution bridge failed:bootstrap/C 调用未正常产生响应,查 stderr。
  • result exceeds 1 MiB:输出协议上限,不直接等同于 Wasm 内存耗尽。

9. 练习方式:你决定行为,AI 协助实现

每次练习只选一个小变化:

  1. 你描述新需求,并给一个输入输出例子。
  2. 你先猜要改哪个函数、哪些地方不该改。
  3. AI 检查调用链,指出语义问题并建议小补丁。
  4. 确认成功和失败行为后再实现;你解释 diff 和测试结果。

可选练习:

  • 业务层:新增另一个只读工具。观察为什么无需改变解释器。
  • 错误呈现:返回更有用的 Python traceback。先决定暴露哪些内容、是否改变 Output,再改 bootstrap。
  • 执行约定:忘记赋值 result 时应返回 None,还是明确报错?这不是“增强安全”的默认选择,而是你要决定的 API 行为。

这些题没有提前实现。涉及核心语义时先讨论,不由 AI 自动选择“更通用”的框架。

10. PLM 与后续阶段

Whole-program PLM(已实现)

完整讲解见 阶段 2:Whole-program PLM,包括实际转换、AST 支持范围、逐函数导航、channel/WaitGroup 背景、错误/取消和 13 个真实路径子测试。它直接接在本手册的普通执行路径上,没有另一套解释器或 Broker。

现在 Host 可以提前开始明确 opt-in 的稳定快照读取;结果/错误仍在原调用处 resolve。不支持的小语言之外的程序原样运行。

Prefix(已实现)

完整讲解见 阶段 3:PrefixRunPrefix 在同一个 Guest 中接收 append-only 片段,只提前准备连续的顶层简单读取;关闭 stream 后编译执行拼接的最终源码。最终优化或不优化时都复用已准备 handle,没有第二套 Future。

Prepared full-copy(已实现,替代本版 COW 实现任务)

按当前准备重点,只用 短的 full-copy 实现 说明初始化基线和私有写入。NewPrepared 捕获干净内存,所有 Run 模式仍共用原有路径。没有 memfd/MAP_PRIVATE,没有真实页共享或性能结论;Linux COW 不再是本版待完成项。

实现重点仍是 PLM 与协作 coding。

11. 仓库文件索引与补充背景

  • README:运行入口和学习手册入口,不重复整篇讲解。
  • go.mod / go.sum:独立模块及依赖版本/下载校验;直接依赖 wazero,没有旧 runtime 包依赖。依赖图仍可能包含 wazero 自己需要的库。
  • 构建与来源说明:artifact 和构建输入的来源、平台边界。
  • .gitignore:排除 dist/build/cache 和本机临时状态,避免将大型工具链及 artifact 混入源码历史。
  • LICENSE:沿用抽取源码的 MIT 许可;CPython 等第三方依赖仍遵守各自许可,不能把 MIT 当成整个生成 Wasm 的统一许可。

按需查原始背景文档,不要求通读:

这些是补充阅读链接,当前实现行为以本仓库源码和真实测试为准。