里程碑明细 · 0.2.x 时期(R 线 + M46–M56)

⚠️ 这是归档材料,不作为设计参考。 原文是 README.md 的「当前进度」整节 (2026-10-08 搬出)—— README 只该回答「现在能做什么」,历次交付的明细堆在那里 是流水账,而且会让「现在的状态」被历史淹没。 现在能做什么 → README.md 往哪走 / 下一步 → 「内部路线图(未公开)」 为什么这么定 → docs/设计决策.md 0.1.x 时期的逐里程碑明细 → 同目录 里程碑.md 顺序是反向时间(最近的一轮在最上面)。以下是原文,未做删改;里面的 内部链接可能已经失效(文件被搬过)。


M0–M56 全部完成,M49(自主分发)也已落地。当前处于 0.2.x 架构变革阶段: 前端正从 Python 迁到 Rust(目标:单二进制解释器、语言实现从 4 种收敛到 2 种)。 迁移按 R0 → R8 推进,每一步都以一致性夹具为验收闸门(tools/conformance.py + 93 个语料的前端四阶段冻结)—— 先把「不许悄悄变」这条规矩立起来,再动手换实现。

现在:源码 → 词法 → 语法 → 字节码,整条前端链都能在 Rust 里原生跑完。

步内容状态
R0一致性夹具(前端四阶段冻结,后端未知)✅ 2026-09-30
R1前端契约固化(产物格式 + 能定位的尺子)✅ 2026-10-01
R2分词器换 Rust(pyo3 扩展,JISHI_FRONTEND=rust 开关)✅ 2026-10-01
R3语法 → AST 换 Rust(含全部解析期脱糖)✅ 2026-10-01
R4前端报错(契约 v2 + 错误码体检)✅ 2026-10-01
R5编译器 → 字节码换 Rust(前端整条链打通)✅ 2026-10-02
R6Rust VM + 标准库(R6.0 侦察 / R6.1 遍历 O(n²) / R6.2+R6.3 报错对齐 / R6.4 Node 进判据 / R6.5 负索引 / R6.6 报错位置与 local_hints / R6.7 实例方法报错位置 / 主体·字典 O(n) / 主体·集合 O(1) / 主体·整数循环 / 主体·函数调用 / 主体·树遍历参考执行器 / 主体·内建宿主接口 / 主体·树遍历①控制流与字面量 / ③函数与闭包 / ④类与继承 / ⑤异常与导入 / ⑥报错渲染对齐 / 收尾·两块跨里程碑账(错误码表 + 捕获体 finally + 调用链)/ 主体验收·C VM 去向(实测后保留)(树遍历执行器语料全覆盖 + 报错渲染逐字一致)✅ 2026-10-04)✅ 已完成
R7–R8工具链 / 单二进制 + wasm未开始

R2–R5 实测:逐 token 一致 93/93 语料、AST 逐项一致 93/93、 报错逐字一致 93/93、字节码逐字节一致 93/93(连 checksum 一起 —— 它是 payload 的 SHA-256)。另 2 万 / 5 万 / 3 万组随机源码对拍 0 分歧。 运行期另有一道闸门:93 个语料在三个执行器上跑出来逐字节相同 (tools/check_engines.py,对拍 91 / 豁免 2 / 不一致 0)。 分词纯核心 7.05 倍、端到端 3.72 倍;语法纯核心 5.64 倍、端到端 3.04 倍; 编译「源码 → 字节码 JSON」5.37 倍 ✅ —— 前两个阶段的天花板(4.23 / 4.73 倍) 来自「把对象交回 Python」,而字节码的产物是文本,没有这个地板。 详见 tools/bench_frontend.py 的三段输出。 ⚠️ 默认仍是 Python 前端:Rust 侧要显式开 JISHI_FRONTEND=rust, 且扩展要按本机 Python 现编(python core/build.py)。

逐里程碑的详细记录移到 docs/里程碑.md (本 README 只讲「现在能做什么」,要查「哪一版加了什么」看那份)。

测试现状:3310 项(python -m pytest tests/),其中 240 余项是三执行器 对拍(树遍历 / Python VM / C VM 输出逐字节一致),另有跨语言宿主对拍 (Rust 101 + 58 + 37 项、JS 62 + 43 项,都与 Python VM 逐字节一致,含三个真实示例 项目的端到端对比)。全量约 3.5 分钟:pytest tests/ -q -n 12 -m "not 计时" 再加一条串行的 -m 计时(6 条墙上时间护栏,见 AGENTS.md —— 12 路并行会把墙钟抬高)。 ⚠️ 本机(Windows)上有 3 个 Node 网络用例是「环境性失败」(spawnSync … EBUSY——Node 宿主自己 spawn 自己时的占用问题,Linux / CI 不复现;把工作区回退到 M51 状态复跑 同样失败)—— 环境性的,重试即过,别把它当成「新问题去查」。

本轮还起了个头(R6.0 · 侦察与度量):

(JISHI_FRONTEND=rust 编字节码 JSON → 二进制跑,全程不经过 Python 对象)。

—— 不扣就会把「VM 快慢」量成「进程启动快慢」。

迭代器的实现是「列表 + 每次 remove(0)」(Vec::remove(0) 要整体左移), 而 Python VM / Node 用的都是真迭代器。改成「快照反转 + 尾部 pop()」 (顺序不变、每步 O(1)):同一个 n=20000,净耗时 2490 ms → 4.82 ms(约 500 倍), 规模翻倍从 ×4.2 回到 ×2 线性;行为逐字节不变。

② 捕获 运行期错误 接不住 键错误/索引错误; ③ 报错时 stdout 一个字都不冲出来;④ 报错渲染缺源码行/^/调用链。

(300 万次整数循环,进程启动成本已摊薄):C VM 净 489.0 ms vs Rust VM 净 649.0 ms —— C VM 快约 1.33 倍,且短任务上 Rust VM 还要多付约 100 ms 启动成本。 17 个场景里 Rust VM 除 递归斐波那契、当循环倒计数 外全面落后;但短场景那一列 不可信(净耗时小于启动成本时误差会被放大,tools/benchmark.py 自己会打 ⚠)。 ⚠️ 这不是「Rust 不行」:现有 Rust VM 是 M27/M33 时期的兼容宿主, 与 C VM 的定位(进程内的快执行器)是互补关系,不是替代关系。

R7 已交付(2026-10-05 开工 → 2026-10-07 收口):工具链迁移(LSP/DAP/格式化/ 检查/CLI/包/MCP → Rust)的侦察台账与拍板见内部路线图;R7.1–R7.6 六件全部完成 —— 编辑器(LSP/DAP)、agent(MCP)、脚本(CLI)与包管理四条路都不再需要 Python。

core/,而 core 是编成 Python 扩展的 crate —— 于是「能解析源码的东西」 只有 Python 拿得到。抽出来之后,独立宿主二进制才可能「不依赖 Python」跑 (编辑器不再依赖 Python 是 R7 的验收原文)。

(32 个码逐个对拍 + 总览 + 「没有这个码」+ 只写数字的别名写法),两张表 (码+标题 / 说明)由测试钉住不许漂。

jishi 源码静态检查 逐字一致(四条 lint:未定义名 / 遮蔽内建 / 未使用变量 / 可疑相等 + 语法错 + 退出码),真语料四个目录 × 两种输出形态全对拍。 ⚠️ 这条命令在 R7.1 时叫 检查,R7.6 起让回给包管理(Python 的 检查 是 包依赖冲突)—— 免得 Rust 顶上 jishi 之后脚本走错命令。

「往错误流写」这个宿主能力。

(含目录模式)与 jishi 格式化 全语料逐字节一致 + 幂等(格式化两次 = 一次); 注释、引号风格、全角、转义一律原样保留。顺带让宿主支持多字符下划线的错误渲染 (~^)。

jishi --lang-spec / --ai-card 逐字节一致(关键字 / 运算符 / 内建 / 对象方法 / 标准库 25 模块 229 个函数(含说明)/ 异常 / 错误码 / 静态检查码 / 等价 Python 写法)。Rust 侧那张表由生成器一次性搬运,之后由漂移检测钉住。

(33 条消息覆盖全部 18 个方法:诊断 / 补全 / 悬停 / 跳转 / 大纲 / 格式化 / 重命名 / 快速修复 / 签名提示 / 工作区符号 / 语义高亮),编辑器自带的 verify-lsp.js 两边各跑一遍都是 25/25 全过 —— 编辑器这条路不再需要 Python。 每次编辑的耗时:5003 行从 289ms 降到 50ms,20003 行从 1224ms 降到 207ms。

--json-errors / --sandbox / --json-result / --timeout,以及 jishi-rs mcp (7 个工具)。与 jishi 侧逐字节一致(新闸门 tools/check_cli.py: 89 个语料 × 3 种模式 × 两个引擎 → 不一致 0;MCP 40 条报文逐字节一致)。 顺带修掉 6 处「只有逐字节对拍才照得出来」的差异:宿主把 值错误("x") 当立刻抛 而不是构造异常(令 e = 值错误("x") 会直接崩)、调用链缺列号、 文件.路径拼接 与 路径.连接 都没做路径归一(. / 空段没被吃掉, 还会渗进「找不到文件「X」」这类用户可见的报错)、空源码行的报错多印两行、 --ast 少印 _super_ok。

(普通单步 / 就地求值 / 出错现场三种场景)—— 编辑器里设断点、单步、看变量与 调用栈这条路不再需要 Python。给树遍历参考执行器加了只读的调试钩子 (「带调试器跑」与「不带」输出逐字节相同),顺带把 打印 的写出粒度对齐 CPython 的 print、补齐了调用链的列号。

与 jishi 调试(默认树遍历)逐字节一致 —— 断点 / 单步(进·过·出)/ 看变量(含「变量 全部」)/ 看调用栈 / 就地求值 / 看源码 / 帮助, 外加 c·s·n·p·bt 这些 gdb 风格别名。顺带修掉一个宿主真 bug: 内建与异常类型的显示以前是 <内建函数 打印> / 裸的 值错误, 现在与 Python/Node 一致(<内建 打印> / <异常类型 值错误>)。

的同名子命令逐字节一致(索引 JSON 的格式与排版、错误文案、退出码)—— 从本地目录 / zip / 远端(或 file:// 本地镜像)安装、依赖冲突检测(钻石依赖)、 打包发布与批量重建索引全齐。⚠️ 一处如实说明的差异:宿主打的 zip 是不压缩 (stored,与标准库 压缩.打包 同一条),所以包的体积与 sha256 与 Python 打出来的不同 —— 内容完全一样,安装不受影响。

R8 已开工(2026-10-07):单二进制 + wasm + 退役 Python / JS,收尾后进 0.3.x。 验收是体积 ≤ 5 MB、启动 ≤ 100 ms(开工实测 4.60 MB / 92 ms,都擦着线过; 现有 Python 发行包是 20 MB)。R8.1 已交付:

整条链(前端 + 编译器 + 字节码 VM + 原生标准库)编成 wasm,模块 1.47 MB。 用的是裸 C ABI + 手写 JavaScript 胶水,没有引入 wasm-bindgen (「零第三方依赖」是这条语言从 R0 起的核心资产)。

逐字节一致;外加「死循环会被停下来」「默认预算有限」这些独立判据。

Pyodide(浏览器里的 Python) 换成上面这份 wasm。页面从 432 KB 缩到 7 KB, 且不再从 CDN 拉那约 10 MB 的 Pyodide,只取本站那份 1.47 MB 的 .wasm (浏览器会缓存它)。在线试玩与命令行、编辑器插件从此是同一份引擎。 判据里有一条端到端:把生成出来的那个页面放进真实 DOM/Node 环境跑一遍, 点「运行」比输出文本;另用无头 Edge 实机加载过(引擎就绪、示例跑出正确结果)。

最近一轮(2026-10-05):把「遍历中改容器」这一族收干净,并清了 Val 这块账:

Class 120,不是当初以为的整数宽度)。把 Class / Func / Module 载荷 装箱 → 128 → 32 字节(语义零变化,只多一层间接)。交错 A/B(同机同时段、 5 轮交替、取最优):建列表 + 求和 1409.8 → 1149.0 ms(1.23x)、只求和 (不建列表)301.1 → 181.2 ms(1.66x);递归斐波那契 0.95x(噪声内 —— 函数调用密集的瓶颈在帧分配与参数绑定,不在值宽度)。 📌 「只求和」也快 1.66x,因为值栈与局部槽本身就是 Val**。新增 jishi-rs --dump-val-size(自报家底)+ tests/test_m82_val_size.py(结构钉: 钉 32 字节,并防大载荷被顺手内联回来)。

裸 RuntimeError 漏给用户(英文、无码、无行列),C VM 还把这件事整个吞掉 —— 现在六个实现(树遍历 / Python VM / C VM / Rust VM / Rust 树遍历 / Node)报 同一条带码(E2000)、带行列、带修法提示的错误。 列表那最后一格靠「变更即失效」收口:C VM 为省跨语言回调会预取超前一截, 改动后缓冲里还没交付的元素就作废 —— 于是 C 侧记「已交付计数」,取批一律从那个 绝对下标重新读(iter_next_batch 加 from,ABI 升 v2 + 装载时版本核对); 宿主按对象身份(不是句柄号 —— 那会漏判)判「被遍历的容器被改了」。 ⚠️ 「干脆不批量」实测要慢 1.81x,所以没走那条路。

下标赋值 + 「改别的列表」对照)→ tools/check_engines.py 不一致 0(103 语料); 另有结构钉 tests/test_m84_cvm_live_iter.py,其中「批量还在」这条按 取批次数判(不用墙上时间,免得并行跑时飘)。

再往前一轮(R6 收尾 + 主体验收 · 2026-10-04)做了什么:

① 宿主补全错误码表(M56-5 收尾):抛出 一段文本 以前没有错误码, 现在两个宿主都报 E2009;捕获 后面写了非异常类型 → 宿主以前(Rust)悄悄不匹配、现在报 E2002;模块属性不存在 → E0302 + 模块名 + 「怎么改」的提示。三处都在码 / 消息 / 位置 / 提示四个维度上 与 Python 侧逐字对齐(五执行器对拍)。 📌 顺序不能反:先把语义错误改成具体类型名,再给笼统的「异常」配码 —— 否则「找不到名字」这类错误会被误标成 E2009。

② 编译器修复:捕获体里跳出时,「最终」块以前不跑(R6 参考执行器抓到的那个 bug, 见下一节)—— 根因是 c_try 只把 尝试 体套进 finally。修法是给每个捕获体再包 一层 SETUP_TRY:返回 走 finally_ip、中断/继续 走 catch_ip、 捕获体自己抛新错误则照既有口径直接重抛不跑最终。只动编译器, 四个字节码宿主零改动;重冻夹具只 2 个语料字节码变。

③ 方法调用的调用链对齐:Rust VM / Node 以前把「方法帧 / 构造帧」也压进调用链, 与 Python 规范(方法走 UserFunction.__call__、不动调用栈)不一致 —— 现在只有 直接函数调用进链。

④ 全量回归并行化:28 分钟 → 约 3.5 分钟(pytest-xdist -n 12)。代价是要守住 三条:临时文件一律带 pid 后缀、别在 cwd 里读写真实文件、断言里有墙上时间上限的用例 打 计时 标记(并行时排除、再串行跑一遍)。这三条已写进 AGENTS.md。

(上一轮)R6 主体 · 树遍历参考执行器:⑤ 异常与导入 做了什么:

尝试 / 捕获 / 最终 / 抛出 / 导入 全通,顺带补上切片(甲[1:4])。 进度仪表第一次清零「还没实现」那一列:语料 75 · 一致 75 · 还没实现 0 · 不一致 0(python tools/check_walk.py)—— 语料里出现过的节点类型,现在全部实现了。

三条边界都照既有规范(各有注释与测试):控制信号(返回/中断/继续)不被 捕获 接住、但「最终」一定要跑;「最终」自己跳出去会覆盖外层信号; 捕获体在当前作用域跑(R5.1 定的)。

顺手修掉一个真 bug:宿主把一个不存在的异常类型名 数值错误 当类型用了三处 —— 后果不是文案难看,而是 捕获 值错误 在 Rust 宿主上接不住切片错误 (Python / C VM / Node 都接得住)。改成 值错误(Python 侧就是 RunValueError,E2005)。 📌 「类型名」是接口不是文案:它同时被 捕获 X 的匹配、e.类型、错误码表三处消费。

✅ 参考执行器抓到的那个真 bug —— 已修(2026-10-04):函数里 尝试/捕获/最终, 「捕获体里 返回 / 中断 / 继续」时四个字节码宿主都不跑「最终」块,而规范跑 —— 根因在编译器侧(c_try 只把 尝试 体套进了 finally,捕获体没有)。现已按上面 ② 修掉, 语料里也可以进这个形状了(新增 3 条进 test_m80),原来那条「会过期报红」的登记测试 按期删掉。 📌 这正是这个参考执行器存在的理由:它跟字节码路径独立,所以能看见「字节码 路径自己看不出来的错」——而且它抓到的每一条,最后都真的修掉了。

(再上一轮)R6 主体 · 树遍历参考执行器:④ 类与继承 做了什么:

类 / 继承 / 新建 / 自身 / 属性读写 / 下标赋值全通了,顺带把内建类型的方法 (列.追加(…) 那些)也接上。

核心决定:不另造一套「类」 —— 树遍历产出的就是宿主那个 Class (方法表里放它自己的函数值),于是 是实例 / 超() / get_attr / set_attr / display / type_label / identity_key 一行都不用改就能用。 📌 加 10 行 pub(crate)、省下几百行重复语义(省下的正是最容易悄悄漂的那种)。

顺带把内建方法表(248 行、73 个分支)从字节码 VM 里搬成了自由函数, 两个执行器共用一份 —— 先量出它只依赖一个宿主能力(4 处)才敢搬, 搬的过程 self. → host.、逻辑一行没动。

进度仪表从 一致 39 涨到 53(还没实现 34 → 21,不一致 0)。

(再上一轮)R6 主体 · 树遍历参考执行器:③ 函数与闭包 做了什么:

补上 函数 / 返回 / 匿名函数,以及词法作用域链 —— 树遍历手里只有 AST、 没有编译器算好的槽位,所以「读沿链找、写只写当前作用域」,与字节码那套 「槽位 + 单元表」是两套做法、同一套语义。能共用的只有「判断」: 形参绑定(含全部报错文案)抽成了一份两边共用。

🔴 最容易写错的是「赋值即局部」的判断顺序:一个名字只要在本函数里赋过值, 读它就不许穿透到外层。顺序放反的症状是静默拿到外层的值 —— 不报错、 答案是错的。另外两处:返回 要能穿出循环;默认值在定义处求值。

🔴 新语料第一次拿 类型() 去问一个函数,C VM 当场露了内裤: 它打的是内部类名 CvmFunction(另外四个都是「函数」)—— 根因是一份手写清单 漏了一个类名。这个 bug 一直躺着,因为此前没有任何语料问过这个问题。 📌 又是「判据没覆盖到 = 没实现」。

进度仪表从 一致 25 涨到 39(还没实现 47 → 34,不一致 0)。

(再上一轮)R6 主体 · 树遍历参考执行器:① 控制流 + 字面量 做了什么:

把「还没实现」清单里的前几名一次收掉:如果 / 当 / 遍历 / 循环 / 中断 / 继续、比较(含链式)、与/或、非 / -、列表 / 字典字面量、取下标、 解包赋值(令 a, *余 = …)、复合赋值 += 那一族。

三个藏在别处的字面量(照「节点类型」的直觉写一定翻车):真 / 假 在前端是 Num 且 value 是布尔;空 是 Name(编译期才换成 none 常量); break 的关键字是 中断 不是「跳出」。

两处容易想当然的语义:链式比较要短路(1 > 2 > d["键"] 的第二节点求值 根本不该发生);与 / 或 的结果一定是真/假(1 或 2 给「真」而不是 1)。

🔴 顺手修掉一个不报错的错答案:unary_neg 非数字一律给 空 —— -「x」 在三个 Python 执行器上都报「不能取负号」,而 Rust 打印出「空」。 📌 共用也是共债:树遍历要调这个函数,才发现「共用的那一份」自己就是分叉源。

进度仪表从 一致 11 涨到 25(还没实现 60 → 47,不一致 0)。

(再上一轮)R6 主体 · 内建宿主接口:内建只有一份实现 做了什么:

内建的签名原来是 fn(&[Val], &mut VM) —— 内建只能在字节码 VM 里跑, 树遍历参考执行器就用不上,只能另写一份;两份一定会漂,而「两个独立实现互拍」 这条验证资产的全部价值来自「语义独立」。

改成 &mut dyn BuiltinHost 之后,VM 与 Walker 各自实现这个接口, 内建只有一份实现。接口是先把用法列出来再定的(数了一遍,内建只用到 5 个能力)。 ⚠️ 规模比预想大:lib.rs 只有 4 处,标准库有 217 处(23 个文件), 批量替换前先用断言把处数数清楚。顺手把「容器递归 + 标量显示」抽成两个执行器 共用的自由函数(只有「自定义对象怎么算文本」是宿主特有的)。

🔴 接上之后立刻撞出一处真问题:参考执行器把出错前打印的内容丢了 (02_计算器 那种「先打印、再读输入读到 EOF」的程序)—— 字节码 VM 在 R6.2 就统一过这条,于是互拍工具把它判成不一致。 📌 判据扩大覆盖面的那一刻,就是旧实现露馅的那一刻。

进度仪表从 一致 7 涨到 11(还没实现 63 → 60,不一致 0)。

(再上一轮)R6 主体 · 树遍历参考执行器:第一个切片 做了什么:

R6 主体三件事的最后一件。它是干什么的:五个执行器之所以能互相抓 bug,靠的是 语义独立;而字节码 VM 有个天生盲区 —— 字节码生成错了,它照着错的跑, 一切「正常」。树遍历直接看 AST、不经过字节码,两边对不上就说明是编译器的问题。

关键设计决策:吃 AST JSON(而不是让宿主自己解析)—— AST 已经是契约产物 (两个前端都产它),吃 JSON 就不必把 core 的 pyo3 依赖解耦进宿主, 也与 R5「产物优先给文本」同一条思路。

第一个切片只实现 8 种节点,未实现的明确报错、不静默给错值。 新工具 tools/check_walk.py 当进度仪表(一致 7 → 75 一路走到「还没实现 0」): 语料 70 · 一致 7 · 还没实现 63 · 不一致 0 —— 那一列同时就是下一步的清单。

(再上一轮)R6 主体 · 函数调用:帧创建的三处开销 做了什么:

把「调用」按参数个数拆开量,发现 Rust 一参调用比零参多 350ns、三参多 595ns, 而 C VM 几乎与参数个数无关 —— 差的是每次调用的固定开销。三处:

Rc<Module>,借引用而不是克隆。单这一项就让 斐波(24) 从 135ms 掉到 95.7ms。

交错 A/B:斐波(24) 1.48×、一参调用 1.47×、三参 1.44×;对 C VM 的差距从 2.0~2.3 倍收窄到 1.2~1.5 倍。

⚠️ 顺带更正上一轮的一句话:那儿说「instrs.clone() 只占 ~2%」是错的, 它其实是最主要的一处 —— 当时的估算拿最小的场景算、还没把「一次堆分配」算进去。

(再上一轮)R6 主体 · 整数循环:全局变量表 + 整数比较 做了什么:

基准里差距最大的一项(整数循环,Rust 比 C 慢 4.9 倍)。先量再改:把「循环」 拆成一段段量,再把同一逻辑换个写法量一遍 —— 模块级变量 vs 函数内局部:

                C VM        Rust VM
模块级变量      26.2ms      198.3ms     7.58x
函数内局部      27.8ms       90.7ms     3.27x

C VM 两者一样,Rust 差一倍多 —— 矛头直接指向「全局变量」。根因:Rust 的 全局变量表是个哈希表,而且每次查之前还要新建一个字符串;而 C VM 一直是 「按名字下标索引的数组」。改成数组之后:当循环 190.7 → 55.8ms(3.42x)、 纯读写 2.32x,而且模块级与函数内终于持平。

🔴 顺手撞出一个既有 bug:2 62 > 2 62 - 1 在 Rust 宿主上给「假」 (它把整数转成小数再比),另外四个执行器都给「真」。语料里从来没有大整数 比较,所以它一直是绿的 —— 现在整数对整数直接精确比较。

(再上一轮)R6 主体 · 集合 O(n) → O(1) 做了什么:

D64 把字典的 O(n²) 收掉之后,集合按同一个思路走:原来是一个线性表, 「建 n 个元素」是 O(n²)。现在「保序表 + 哈希索引」,添加 去重 / 判断在不在 / 删除 都 O(1),而集合照旧是有序的(打印与遍历稳定可复现)。

⚠️ 删除这里有个坑:保序集合删中间元素如果前移整段,删 n 个还是 O(n²)。 第一版就是这么写的 —— n=16000 的批量删除要 1845ms;改成打墓碑 + 过半压缩 之后 37ms。

🔴 顺带撞出三个既有分叉(都不是这一轮引入的):

bool 是 int 的子类)。跟着一起歪的还有 {真, 1} 算两个元素、d[真] 与 d[1] 是两个键、1 在 [真] 是假。

📌 最值钱的一条:这些分叉能藏这么久,不是因为藏得深,而是 tests/cases/ 里一个集合语料都没有 —— 集合从来没进过四阶段夹具与全语料体检。 判据没覆盖到的地方,错了也没人知道。 所以这一轮先补语料(18_集合.jsh),再修代码。

(再上一轮)R6.7 · 实例方法的报错位置:三个执行器终于说同一句话 做了什么:

D63 登记的形状 —— 类 里定义的方法、调用时少给参数,报错落在哪一行:

类 甲:
    函数 方法(自身, x):
        返回 x
令 对 = 甲()
对.方法()

→ 树遍历 5:5     两个宿主 5:5     Python VM 2:1 ✗     C VM 2:1 ✗

「我这一行调用写错了」当然该指在这一行上。根因不是一个,是三个, 而且分散在 Python 与 C 两种语言里:

修法一律「照树遍历的机制来」:让「Python 侧回流」的入口用调用点。 少给 / 多给 / 关键字名写错三种都治了,语料也进了 (tests/error_cases/36_实例方法参数个数.jsh)。

🔴 顺带撞出两笔:Rust 的用户方法调用把关键字参数静默丢了 (对.方法(不存在的名=1) 报成「缺少参数:x」,像另一件事); 树遍历的类构造参数错没有行列。

🔴 还有一个工具链级的、更阴的:python cvm/build.py 产出的 cvm/bin/jsvm.dll 与 wheel 里预编译的 jishi/_native/jsvm.dll 是两份, 而加载顺序是后者优先 —— 改了 cvm/src/jsvm.c 并重建之后,Python 加载的 仍是那份九天前的 dll,而且一句提示都没有。排查时只觉得 「C 侧的修法像是完全没作用」。现在改成源码树优先。 📌 又一次「静默用错产物」——这类 bug 的症状只有「怎么改都不动」。

(再上一轮)R6.6 · 报错的位置,以及两个宿主手里的最后一点信息差 做了什么:

R6.3/R6.4 把两个宿主的报错从「一行标题」做到了「与 Python 逐字相同」,之后还剩 5 条语料差一点点 —— 这一轮全收掉:

而不是后面那个 ()。受影响的是一整类:参数个数不对、a.移除(9)、 "abc".转整数()、d.弹出("b")… 以前宿主指到 ( 上,你还得自己数哪一列。 ⚠️ 数.开方()(标准库)没有变 —— 那是模块属性,与普通调用一样用调用点位置。

「外层也有一个叫「全局」的变量…」—— 以前只有三个 Python 执行器说得出这句。

(报版本不兼容),重新编译即可。版本号在四处同步,有测试钉着。

原因都写着「宿主没有 AST → 只能报指令的列 → 要动字节码格式」。真去修时把现象 重量了一遍才发现 —— 两边手里都有那两个位置(GET_ATTR @2:2 / CALL @2:5), 差的只是「报错时挑了哪一个」。一个字节都不用动。 ⚠️ 登记里写的「原因」是当时的猜测,是待验的假设,不是事实。

(再上一轮)R6.2/R6.3 · 四个执行器的「报错」对齐 做了什么:

19 个常见运行期错误形状里 18 个在四个执行器之间不一致。两类根因:

'乙'(键的 repr)、'int' object is not subscriptable、「([],)」(元组 repr)。 现在一律中文。这与 errors.py 里早就写下的规矩一致(「宁可不翻,也不要翻错」), 只是 IndexError / KeyError 这两支没被覆盖到。

一律报成「异常」「运行期错误」—— 不只是文案差,e.类型、捕获 X、 错误码三处一起错。 现在:0/19。

而语言规格明写「基类能抓子类」)。

屏幕上一个字都没有。

+ 提示语。⚠️ 做法是出错那一刻按 filename 现读一次源文件, 不把源码塞进字节码;读不到就退回「没有源码行」,不崩。 宿主在 R7/R8 之后是唯一形态,缺这块就是体验退步。

这个缺口根本比不出来;改成「stdout + 用户看到的报错渲染」之后, 可见问题从 0 个变成 26 个。 ✅ 剩下那几条已在 R6.4/R6.5/R6.6 全部收掉 —— 现在宿主的已知缺口登记只剩 3 条「有意边界」(宿主是单二进制、里面没有 Python,所以不支持 从 python / 本地包 导入;那是设计,不是缺口)。

(上一轮)R5.1 · 把 R5 移植时挖出的四个既有 bug 清掉 做了什么:

逐行移植为了搬对,每一行都得读懂;读懂之后,代码里漏掉的那一支就藏不住了。 这四个缺口全都与 R5 无关,而且两个前端上一直是一致的 (所以一致性夹具全程是绿的)—— 它们只在三个执行器之间露出来。

#在哪症状
①编译器名字收集漏了 Try 分支「尝试/捕获/最终」里的赋值漏成全局;里面读的闭包变量捕获不到;里面定义函数/类会编译期直接崩(发射期 KeyError)
②Python VM 的 BUILD_CLASS 用局部名 base撞掉帧的求值栈基址 → RETURN 执行 del stack[None:],把整个栈清空 → 「在函数里定义类」必炸(报「「7」不能调用」这种莫名其妙的话)
③树遍历给「捕获 … 为 名字」偷偷开了子作用域捕获体里的赋值出了捕获体就没了(全语言只有这一处这样)
④调用链的挂载时机三处不一「显式 抛出 的错误」在两个引擎下没有调用链(「除零」那种有)—— 同一种错误两种长相

(否则全局与局部恰好等价);②模块级定义类侥幸没事(模块帧以 HALT 结束、不收 RETURN),而示例与语料里的类恰好都在模块级;③④只在报错文本里。

(含 tests/error_cases/ —— 报错文本也是可观察行为)在三个执行器上跑, 逐字节比「树遍历 vs Python VM vs C VM」。93 个语料 / 对拍 91 / 豁免 2 (超 i64 整数、打印墙上时间的跑分示例)/ 不一致 0。 它管的是「产物一样 ≠ 跑起来一样」那一半 —— 上面四个里有三个是在 一致性夹具全程绿的情况下存在的。

语料 92 → 93,基线重冻(9 个语料受影响:它们都有 尝试 块)。

(上一轮)R5 · 编译器 → 字节码换 Rust 做了什么:

(作用域收集 / 定种 / 全部语句与表达式发射 / 脱糖 / C VM 的方法调用融合), 外加 jishilib.rs(基石库层展开)、serialize.rs、opcodes.rs、sha256.rs。 前端(源码 → 字节码)整条链现在都能在 Rust 里原生跑完。

字节码逐字节相同 —— 连 checksum 都相同(它是 payload 的 SHA-256, 差一个字节就不一样,比「字段逐个比」还硬);另 3 万组随机源码对拍 0 分歧。

R2/R3 的端到端倍数一直撞在「把对象交回 Python」这堵墙上(11.7 万 Token ≈ 39 ms、 7 万 AST 节点 ≈ 28 ms 的地板)—— 文本没有这个地板。 实测编译端到端 2035 → 379 ms(5.37 倍),把从 R2 挪过来的 「端到端 ≥5 倍」验收线兑现了。

——local_hints 不在字节码 JSON 里,而报错文案要用它,只给 JSON 会让它静默消失。

下一轮(R5.1)已全部修掉:编译器的名字收集漏 Try 分支、 Python VM 的 BUILD_CLASS 撞掉帧栈基址、树遍历给「捕获 为」开子作用域、 调用链挂载时机三处不一。

(上一轮)R4 · 前端报错 做了什么:

修正(机器可读的替换建议)、渲染(用户真正看到的那段多行文本)。 ⚠️ 渲染 看着冗余,其实是兜底:哪天给错误加了个事实字段却忘了列进契约, 两个实现的渲染立刻不一样 —— 而「字段没进契约」本身是看不出来的。

「这个码真的会出现」,这件事以前从来没人查过**。第一次跑抓到两笔:

也查得到,但全仓库没有任何触发点(括号没闭合其实报 E0201)。 一份永远兑现不了的承诺。→ ✅ 已拍板接上:期望右括号而已到文件末尾 ⇒ E0205,且报错位置指向那个没闭合的开括号(EOF 的位置是「末行第 1 列」, 指着它连源码行都显示不出来)。

「不可达」必须登记且经得起复核,登记过期就报红。 前端 13 个码:11 个有语料钉住、0 死码,2 个基类登记为结构性不可达。

(「这里需要一个值,但看到了「None」」)—— 和 R3 那个「插值字符串把分段表 泄出来」是一回事:凡是内部表示,都不许落到用户眼前。

字段清单,词法那份漏了「修正」—— 而当时恰好没有一个词法错误带 fix, 所以怎么测都是绿的。现在收成一个出口,并用字段表往返测试**钉死。

tokens / ast / diagnostics 三阶段逐字一致。

(上一轮)R3 · 语法 → AST 换 Rust 做了什么:

含全部解析期脱糖(条件表达式 / 超() / 匹配 / 枚举 / 推导式 / f-string), 脱糖发生在与 Python 同一位置。parse_source() 在 JISHI_FRONTEND=rust 时走它。

端到端跑出来也一样。判据是 tools/conformance.py compare --stage ast (连语法阶段的报错也逐字比过),不是「看起来对」。

⚠️ 端到端天花板 4.73 倍 —— 同 R2 的道理:把 AST 节点对象交回 Python 本身要 125 ms(其中纯构造地板 28 ms)。收益的兑现要等 R5/R8。

原做法是从 source_lines 反拼,而那是个有损载体(增量分词的行列表会 去掉末尾空行)→ 报错行号整体少一行(实测 9 例错 4 例)。于是新增 parse_source(源码) 作生产入口(顺带省掉一次重复分词),旧入口留给 LSP 增量分词并加精确守卫:丢了内容当场报错,不猜、不静默。

([('text', …)])泄给用户;类外 超() 那条错补上了源码行与 ^ 下划线。

JISHI_FRONTEND=rust 会明确报错,不静默回落)。

(历史)M49 · 自主分发:自建源 + 官网 + HTTPS 做了什么:

前面挂 EdgeOne CDN;页脚按法定要求放了 ICP 备案号并链到工信部。

① 自建源(基石.cn)② GitHub 备用。自建源那份索引里的「下载地址」 也指向同一个域名,所以整条安装链(索引 + 包)都在境内,实测 0.11s。

走 HTTPS 回源(443)—— HTTPS 必然失败。给源站配好证书、开 443 之后 两条路都通。诊断依据是源站访问日志里有没有那条请求**(HTTP 有、HTTPS 没有)。

--index / --local-zip / --local-dir 三条具体做法;jishi 医生 新增索引可达性自检(--no-net 可跳过)。

tools/deploy_push.sh(本地构建 + 推送)、tools/nginx-jishi*.conf。 🔒 密钥路径与服务器地址只从环境变量读,绝不写进文件。

M56 第四批 · 宿主的 hint 做了什么:

没有 hint 字段,报错只说「找不到名字」,不告诉人可能该写什么。 报错现在长这样:

`` 错误(异常):找不到名字「长渡」 ← 第 1 行第 4 列 提示:你是不是想写「长度」? ``

而两个宿主都没有 —— 各逐字复刻了一份 Ratcliff-Obershelp。 不一致比不给候选更糟(用户在两个引擎上看到不同候选,会以为另一侧是错的)。 复刻里抓到两个抄错就分叉的点:最长公共块要用 difflib 的 DP 版 (k > bestsize 严格大于,并列取先遇到的);get_close_matches 收集的是 (分数, 名字) → 同分时按名字降序(不是保持原序)。

3000 组候选全一致),确认算法钉死后再翻译成 JS / Rust。

免得用户去改一个本来没错的名字。

M56 第三批(文件语义 + 宿主报错):

tell() 的字节 cookie、Rust 给字节偏移——同一段程序三侧给三个值); 写文件一律用 \n(Windows 上 Python 原来写 \r\n、两个宿主写 \n —— 同一台机器上五个引擎产出的字节都不同)。

报「文件内容不是 UTF-8 文本」—— 而那个文件明明是合法的 UTF-8。

渲染成 错误 E2001:不能除以零 + ← 第 3 行第 6 列,与 Python 侧同形。 📌 报错里的码可以直接贴给 jishi 错误 <码> 查「什么意思 / 常见成因 / 怎么改」。

M56 第二批(宿主报错对齐 + 错误码索引):

文本() / 类型() / 退出上下文() 在 Node 上直接返回值,而 Python 与 Rust 都报错(「不报错的错答案」最难查)。根因是签名写成 (v) => …, 0 个实参时形参是 undefined、函数体照跑。顺带收口 Rust 4 处自己抄的文案。 → 现在「参数个数错」在五个引擎上逐字一致。

直接报「没有属性」,打印(e) 只给「类型错误」)—— 现在带消息,与 Node 一致。

32 个错误码(按词法/语法/语义/运行期分段)。--ai-card 里也加了这一节。

M56 第一批(内建体检):

五个引擎上真跑(正常用法 + 参数个数错 + Rust panic 扫描)—— 不再只比名字。一建起来就抓到一条 M55 漏掉的账。

把 超 当「编译器形式、宿主不需要有它」而排除在比对之外 —— 判断错了, 它是 LOAD_GLOBAL 超 + CALL 的真运行期内建,于是两个宿主一直报 「找不到名字「超」」而三个执行器全对。已修:Node 补 JishiSuper; Rust 给 Class 补 base 字段(那边建类时展开合并基类方法表后 把基类指针丢了)。修完五引擎连 打印(超()) 的 <超 乙> 都逐字一致。

参数个数错三执行器一致、Rust 全部不再 panic;⚠️ Node 还有 16 个内建 缺参数检查,其中 3 个是「静默成功」(文本() / 类型() / 退出上下文() 在 Node 上直接返回值,Python 与 Rust 都报错)。

M55(定下「一个字符是什么」):

字符(20013) 给「中」,互为反函数。口径是一个字符 = 一个 Unicode 码点。

UTF-16 码元 —— 长度("😀") 给 2(另两个给 1)、"😀"[0] 只拿到 半个代理项(打印出来是 �)、"a😀b".查找("b") 给 3、反转 出乱码。

对象方法)—— 而它建起来当场就抓到:断言 / 是实例 在 Node 与 Rust 上根本没有(一写就报「找不到名字」),断言错误 这个捕获类型也缺。

Python 侧参数个数不对会漏英文原生错误(_b_len() missing 1 required…)。

M54(结清「五引擎的账」):

struct(Python / Node 是对象),于是「先创建闭包、后给它赋值」永远失效 —— 最典型的写法嵌套的递归函数直接崩 「空」不能调用。顶层递归没事, 单测怎么写都碰不到;三个执行器都是对的,只有真跑 Rust 才抓得住。 M53 记的三笔 Rust 缺口(容器 / 迭代 / 参数)根因都是它,一并解决。

跟「没赋过值」必须分得开)。

回调 C 库 → access violation。崩在 GC 里,位置跟出错代码毫无关系。

而 列表/字典/文本/集合/文件/小数 的方法表同样是五份实现。 代价是两笔漏网 —— Rust 缺 文本.查找、Node 缺 文件.位置/定位/刷新, 都是偶然撞见的。现在两个宿主都有 --dump-methods(与分派同源), 测试接在 tests/test_m51_consistency.py,并补了一条「表里有名字 ≠ 真的能用」 的实测。上面那几个缺口已全部补齐。

M53(基石库层批量迁移):

加上 统计,共 4 模块 / 34 函数,都是「用基石写一份、五个引擎共用同一份 字节码」,且与宿主层实现逐字节一致(tests/test_m52_jishilib.py)。

循环」会把栈搞脏,最终报 TypeError: 'RunTypeError' object is not an iterator。 根因在编译器(尝试/捕获 的字节码多了一句 DUP_TOP)。三个执行器 + 两个宿主里只有树遍历是对的 —— 五引擎对拍才抓得住。已修 + 加回归测试。

M52(基石库层):

(Python / JS / Rust),改一处得改三处 —— M51 的漂移检测就是被这个逼出来的。 现在 jishi/stdlib-jishi/统计.jsh(就是基石源码)在编译期展开成外壳函数, 导入 统计 被改写成 统计 = $造模块("统计", __基石库_统计__()), 于是五个引擎跑的完全是同一份字节码,三个执行器与两个跨语言宿主一个字都没改。

与宿主层一致 + 错误路径同类 + 机制 + 回落)。

C VM 27.7x)—— 库代码内部每调一次内建,在 C VM 上都是一次宿主回调,而宿主层版 把这一切做在一次调用里。所以热路径函数必须留在宿主层,这是下一阶段 「性能分层」的存在理由。完整数据、走过的弯路与边界见 docs/设计决策.md D45。

「内部错误(宿主回调):TypeError: …」,现在是清楚的中文类型错误。

M51(更早一轮,一致性止血):

宿主上悄悄用默认的 sha256(Python 给 900150…、两宿主给 ba7816bf…), 一声不响——这是本项目最危险的一类问题:错了不报警。现在两宿主按 「模块.函数」查由标准库源码自动生成的签名表,把命名参数映射回位置参数; 映射不了一律明确报错(类型「类型错误」,与 Python 侧一致)。

不走正则解析源码),tests/test_m51_consistency.py 断言「Python 侧有的模块与 函数,宿主一个都不能少、也不能多」,两宿主彼此也要对得上。 验收实测:故意从 Node 删掉 统计 模块 → 3 条测试报红并点名缺哪个; 把参数映射退回「静默丢掉」→ 7 条报红(含 md5 vs sha256 那条)。

数学(正弦 / 余弦 / 正切 / 对数 / 常用对数 / 角度转弧度)、 文本(填充 / 截断)、迭代(累积 / 组合 / 排列 / 笛卡尔积)。 补后两宿主与 Python 侧同宽,三个执行器 + 两个宿主逐字节一致。

带方法的对象并支持上下文协议,而两宿主的上下文查找只认「Jishi 实例」与 「内建容器」,标准库直接返回的宿主对象走不通。缺口显式写在测试的 KNOWN_GAPS 里(不掩盖、也不让它静默),归属下一阶段:改成用基石写的 库之后,它天然是 Jishi 实例,这类缺口会结构性消失。

M50(更早一轮,分两批):

配置 / html / 对比 / 标识,并给 迭代(+累积/组合/排列/笛卡尔积)、 文本(+截断/填充)、时间(+计时,上下文管理器式)补缺。

(39 项)做逐字节对拍。Rust 侧的 base64、行级 diff、UUID 都是自写的 (它零第三方依赖)。⚠️ 但当时说的「五个引擎同宽」只针对这批新模块—— 更早的 13 个函数缺口没人发现(M51 的漂移检测就是这么来的)。

转义()(4 次 替换,而且漏了引号)改用 html.转义; 排序跑分/排序.jsh 的手写计时改用 时间.计时。

(只看了基石类实例与内建方法表),于是标准库返回的计时器不能当上下文管理器用。

M48 阶段三(再往前):

两次宿主回调(实测每次约 4µs,其中必要工作只 2–3µs)。现在编译成 一条 METHOD_CALL,一次回调做完。同机交错 A/B 实测: 方法调用 1.83× 快、端到端真实项目 生成.jsh 521.9ms → 334.8ms(1.56× 快)。

可折叠(0.16%);窥孔候选 <1.7% 且收益 ~0.2%;文件 I/O 的包装零开销 (按行读 201µs vs 裸调 212µs)。不是做不动,是量完之后发现不值得—— 原计划「纯计算 +5–15%」的前提不成立。

M47(更早):

AST 缓存(361 行项目编译 14.13ms → 1.46ms)。

一份引用**(遍历 1000 个文本一次漏 33KB)。不报错、对拍全绿,只在长跑进程里 涨内存——靠本轮新增的 tools/leak_probe.py 量出来。

M46 阶段一(更早):

② 宿主回调路径提速约 10%。

读侧仍慢(字典查找 0.96x)——要治它得让 C 侧真正持有容器内容。 基准已按类型拆细,见 tools/benchmark.py。

下一步:见 PROGRESS.md 的「当前进行中」与 docs/里程碑.md(更细的规划与评估属内部文档,不公开)。