diff --git a/README.md b/README.md index 8fce000f..1d687240 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ Neither is a general untrusted-bytecode loader or Feature Parity claim. - [Current implementation status](docs/status.md) - [Profiling and external benchmarks](docs/profiling.md) - [Data structure improvement plan](docs/data-structure-plan.md) +- [Published bytecode and VM execution plan](docs/published-execution-plan.md) +- [Published execution contracts](docs/published-execution-contracts.md) - [Pinned Test262 baseline and metric definitions](docs/test262.md) - [Parity acceptance contract](docs/parity.md) - [Playground build and trust boundary](docs/playground.md) diff --git a/docs/published-execution-contracts.md b/docs/published-execution-contracts.md new file mode 100644 index 00000000..7c1b4b8b --- /dev/null +++ b/docs/published-execution-contracts.md @@ -0,0 +1,57 @@ +# 已发布代码与 VM 执行契约 + +本轮基于 PR #17 的 `b11f2be`,在同一 PR 中按发布所有权、建帧、绑定、常量/捕获和分派拆分提交。[实施计划](published-execution-plan.md)仍是范围与后续工作的入口。 + +## 采用的抽象 + +`VerifiedFunction` 消费并拥有通过对应 verifier 的确切草稿;它没有可伪造的独立 token,也不暴露可变草稿。Script、受限 ordinary BC5、eval、module 使用各自的验证入口。模块只把 `UnlinkedModuleParts` 中函数的所有权状态由草稿转为已验证,其余表仍由原 module 发布流程处理。发布器负责原有的链接、分配、roots 和失败回滚。 + +`PublishedFunctionSnapshot` 移至 code/executable,私有字段将 bytecode root 与只读数据放在一起。Runtime/realm 检查之后才可构造;读取投影只借出数据,不提供生产 DerefMut。测试 fixture 可以修改无 root 的数据,真正发布的 snapshot 在测试中也不可变。没有新增指令表示、常量池、opcode 分类表或逐条指令的 rooted Value。 + +`RuntimeVmHost` 持有整个 snapshot。`CallInput` 只表示本次调用的动态输入;`new_activation` 从 host 本身获得代码、布局、realm 与当前函数。上游 callable 与其 bytecode/capture roots 继续由现有 `bytecode_for_callable` 的生产路径提取;这不是一次全面重写 callable 类型系统。恢复仍经过 `decode_vm_activation`,返回字段私有的 `RootedVmActivation`。普通调用和可挂起调用保留不同驱动返回类型,维持原有本机栈边界。 + +## 静态事实如何进入执行 + +共同链路:来源专用 verifier → 拥有草稿的 `VerifiedFunction` → 原事务发布器 → heap 的不可变 bytecode → 持有 root 的 snapshot → 对应 host 的建帧/恢复 → VM。下表按真实符号定位,避免依赖会漂移的源码行号。 + +| 候选 | 静态依据与消费位置 | 本轮处置与仍然动态的部分 | +| --- | --- | --- | +| local 普通/词法访问 | `verify_unlinked_tree_with_root`、`private_elements::verify_unlinked` 验证 opcode 与定义;`get_local`、`put_local`、`set_local_uninitialized`、`get_local_checked` 消费 | 删除生产路径重复的静态模式查询;保留下标访问错误、TDZ、Direct/Captured/private 动态状态和重新进入作用域 | +| argument | 发布参数布局;同一 snapshot 初始化缺省槽;`get_argument`/`put_argument` | 与 local 复用内联的 `read_frame_binding`/`write_frame_binding`;不把参数直接槽永久化,mapped arguments 与捕获仍有效 | +| VarRef | `verify_unlinked_tree_with_root` 验证 descriptor 与读写 opcode;`get_var_ref`、`put_var_ref`、`get_var_ref_checked` | 去掉普通/checked 读取中的重复描述符模式检查;实际 root、cell、TDZ 和 live binding 不省略 | +| checked 写入、初始化、CloseLocal | 发布器拒绝 checked 写入普通/private/const 定义;验证 InitializeLocal、InitializeVarRef 和 CloseLocal 的模式 | 成功写入不再查询模式与错误名字;初始化/CloseLocal 的静态模式检查仅在合成 fixture 保留。实际 TDZ、cell const、with 值身份、捕获复用和派生构造器初始化协议保留 | +| 常量和静态名字 | verifier 区分 PushConst、FClosure、RegExp、字符串名字;发布已有 property Atom 表 | 共享 `snapshot.constant` 的下标投影,继续使用原 Atom 表。安全 Rust 的 enum match 保留;无第二份种类表,不宣称消除了所有常量分类 | +| 父子捕获 | `verify_unlinked_tree_with_root` 与 `verify_capture_flags` 验证来源、flags、名字及 FunctionName 视图;`instantiate_closure` | ParentLocal/ParentArgument 不再重复静态匹配;保留 canonical local metadata、`capture_frame_binding`、`validate_var_ref_metadata`、实际共享 cell 和失败清理 | +| eval 环境 | `verify_eval_environments`、`verify_eval_scope_topology`、专用 eval verifier;`prepare_direct_eval_environment` 与 `validate_eval_frame_bindings` | 从同一 snapshot 取环境,不再重复扫描静态拓扑与匹配名字/flags;编译前检查 caller strictness、实际槽与 closure cell 元数据。动态捕获仍由原捕获入口验证;没有缓存 eval 查找结果 | +| 静态控制流和栈 | 原栈/目标 verifier;`execute_inner`、unwind 与 decode/resume | PC 递增实验已撤回。`static_branch_target` 仅对已发布 host 的 IfTrue/IfFalse/Goto 复用目标范围证明,通用和合成 host 继续验界。`pop_pair` 合并两次长度判断,`clone_at_depth` 保留一次安全查询;非法栈仍返回原错误。异常、Gosub/Ret、恢复检查保留,验收状态见计划 | +| dispatch | immutable opcode;`execute_inner` | 常用绑定、字面量、简单栈操作与条件/无条件分支在顶层 match 直接执行。复杂语义仍委派原处理器,以限制普通递归的本机帧;PC 发布、异常处理、挂起点不变,不新增分类表 | + +所有移除静态模式检查的 host 方法,对无 root 的合成 fixture 仍保留拒绝检查。该 fixture 无法进入 `execute_published`/`start_published`。这保留了内部错误契约测试,不建立生产兼容分支。 + +## 成本与未采用方案 + +snapshot 复用原 Rc 数组;构造增加固定数量引用,数据空间不随指令数新增一份表。它确实把 code/metadata 保留在 host 中,不能据此声称单个 host 更小。动态参数、locals 与 capture roots 的分配方式不变。空间成本主要是固定数量的 Rc 和 root 持有;没有新增随指令数量增长的派生表。 + +没有采用独立执行指令枚举、预解码类别、帧池、下标机械包装、通用访问 trait 或由多个布尔参数控制的万能绑定访问函数。现有 opcode 已表达访问模式,额外复制这些事实会增加同步与审查成本。 + +建帧时将已拥有的输入 bytecode root 直接交给 active frame,避免重复 clone;snapshot 保留自己的 root。 + +## 测试与后续修改入口 + +真实发布到执行的契约测试位于 `src/engine/vm/published_execution_tests.rs`;snapshot 的 Runtime 身份、root 生命周期与只读保证测试位于 `src/engine/code/executable.rs`。发布边界的拒绝规则与变异测试位于 `scripts/checks/binary_object/`。修改这些契约时同步维护对应正例、反例和模块 README。 + +此前候选处置不等同于全部优化完成;补充实施状态见计划。常量 enum match 是安全取得变体的操作;给 getter 改名或增加种类表不自动消除它,因此沿用单一常量池。本轮不重写栈表示或新增静态 PC 表,静态分支、双操作数范围判断及深度查询已按计划实现;最终集成状态以计划为准。 + +`host_bridge/eval_validation.rs` 只检查实际帧;静态反例由发布器的 `eval_super_capabilities_are_authenticated_at_publication`、`strict_script_global_eval_anchor_does_not_leak_to_strict_functions` 和环境来源/flags 测试维护,不复制第二套测试专用验证器。真实发布到执行的 eval 测试覆盖遮蔽、捕获、嵌套 eval 与 super,另有实际槽缺失和错误 cell 元数据的反例。 + +## 补充实施:eval 环境共享 + +`PublishedEvalEnvironment` 私有构造只接受 snapshot 中存在的索引,持有同一环境数组和字节码 root。`PreparedEvalEnvironment` 与 `MaterializedEvalEnvironment` 传递该视图,编译边界的身份检查比较 Rc 数组和索引;不再深拷贝 scopes/bindings 或重新比较结构。编译失败前不捕获、实际帧槽和 cell 元数据检查不变。没有新增每环境的预计算数组或动态查找缓存。 + +## 补充实施:已捕获局部槽 + +`instantiate_closure` 仅在建立新捕获单元时读取父局部定义并构造 canonical metadata。已有 `FrameBinding::Captured` 直接由 `reuse_frame_capture` 验证实际 cell 与子 descriptor 的合法视图关系并复用 root,避免再次取静态定义。CloseLocal 后的槽会重新走建立路径;FunctionName 的视图差异不能覆盖新 cell 的 canonical metadata。 + +## 补充实施:栈访问 + +`pop_pair` 在一个长度门槛后移动右、左值;检查与移动之间不调用用户代码。失败时保留先消费右值、构造错误、再释放右值的顺序。`clone_at_depth` 由 u8 深度计算的非零偏移通过 wrapping_sub 映射尾部索引;过深索引环绕到栈长以外,仍由安全 get 返回原错误。没有 unsafe、临时 Vec 或新的栈表示。切片占位值、drain 和强制内联实验增加了清理工作或造成耗时退化,因此未保留。 diff --git a/docs/published-execution-plan.md b/docs/published-execution-plan.md new file mode 100644 index 00000000..90a90e43 --- /dev/null +++ b/docs/published-execution-plan.md @@ -0,0 +1,355 @@ +# 已发布字节码与 VM 执行优化计划 + +状态:计划中的代码优化已实现,本地集成验收已完成;独立评审仍待完成,不将实施者自查算作独立评审。基于 PR #17 的 `b11f2be`,见 [PR #18](https://github.com/pocket-stack/quickjs-oxide/pull/18) 和[执行契约说明](published-execution-contracts.md)。 + +读者:共同维护 quickjs-oxide 的人和 agent。读完后应能选择一个依赖已满足的步骤,找到模块拥有者,说明该步骤的验证依据与动态语义,完成实现、评审、测量和交接,无需恢复聊天上下文。 + +## 当前实施计划与交接状态 + +本节是当前进度的唯一清单;第 4 节保留原步骤及其对应关系,不另行宣告完成。补齐工作以 `49cfa30` 为起点,代码已推进至 `c55d59d`,仍提交到原 PR #18 的 `perf/published-vm-execution` 分支,base 保持 `perf/indexed-data-structures`。P1–P4 的代码及本地集成验收已完成;独立评审尚未完成。 + +### 交付范围和顺序 + +按 **eval 环境共享 → 闭包捕获元数据 → 静态分支 → 栈重复操作 → 最终集成验收** 推进。四项均已实现,分步 A/B 与最终组合的本地集成验收已完成,证据入口见下方记录。已提交不等于已验收,阶段性收益不等于全计划完成。 + +| 项目 | 代码入口与具体改动 | 保留的动态语义与抽象边界 | 当前状态与下一步 | +| --- | --- | --- | --- | +| eval 环境共享(E06) | `code/executable.rs` 的 `PublishedEvalEnvironment` 共享已发布环境数组及索引,并持有字节码 root;`prepare_direct_eval_environment`、`materialize_direct_eval_environment` 与 `builtins/eval.rs` 传递该视图,用身份核对替换两次深拷贝及结构比较 | 私有构造检查索引;不缓存动态名字解析或实际槽;编译成功后才捕获,保留 strictness、实际槽和 cell 检查。静态规则只由发布器维护,旧测试专用验证器已在 `ed80e61` 删除 | 已实现并完成本地验收:`4b7bd08` 共享视图,`64785fa` 加强最后一个 root 释放测试;深拷贝与结构比较已移除。指令工作减少,最终耗时未证明稳定收益 | +| 闭包捕获元数据(E06) | `host_bridge.rs::instantiate_closure` 对已有 Captured 槽直接复用实际 cell;仅首次创建或 CloseLocal 后重新捕获才读取父局部定义、构造 canonical metadata | 由 `reuse_frame_capture` 统一已有 cell 的验证与复用;保留 cell/descriptor 合法视图验证、root 所有权及清理。FunctionName 的访问视图不能覆盖新 cell 的 canonical metadata;不复制完整捕获描述符表 | `d0d329e` 实现元数据复用,`64785fa` 消除随后重复的状态分派。十轮复核未复现初测的调用控制组大幅退化,但没有证明独立的耗时收益;最终组合已完成正确性、诊断负载和调用控制验证 | +| 静态分支(E07) | `VmHost::static_branch_target` 的默认实现验界;`RuntimeVmHost` 复用与自身 snapshot 配对的发布证明;`frame_execution.rs` 仅将 IfTrue/IfFalse/Goto 的立即数目标交给该入口 | 发布器检查所有目标,包括不可达指令;无 root 的合成 host 保留验界。异常展开、Gosub/Ret、恢复 PC 和取指检查不使用该入口;不新增可伪造的信任标志或 PC 表 | `7521630` 已实现并完成提交后的 A/B 和指令数确认;`64785fa` 补充通用/合成 host 反例、恢复组合及两个架构变异。最终隔离复测确认循环收益和指令减少;算术控制耗时有代价,未宣称该项全面提速。最终本地正确性验收已完成 | +| 栈重复操作(E07) | `frame_execution.rs::pop_pair` 用一个长度条件证明两次移动;`clone_at_depth` 用 wrapping_sub 与一次安全 get 代替两次验界 | 保持现有 Vec/Value 表示,不引入 unsafe。保留空栈和单元素失败时的消费、释放顺序,不能以 panic 替代内部错误;动态恢复检查保留 | `800242c` / `a3c988c` 已实现,最终 `c55d59d` 保留简单长度门槛和一次深度查询。机器码确认重复判断减少;cold/内联、切片占位值和 drain 实验不采用。最终本地集成验收已完成 | +| 最终集成(E09) | 在最终采用的代码上验证各优化组合及控制组,清除实验残留,更新本节和模块契约 | 所有入口沿用同一发布拥有权与帧配对契约;不复制静态验证器、opcode 表或依赖人工同步的元数据 | 本地集成已完成:最终 workspace/feature、Clippy、701 个架构变异、完整 Test262 行为比对、Node/WASM 及固定控制矩阵已验证。独立评审仍待完成 | + +### 每项验收 + +1. **eval**:环境身份与错误索引、root 生命周期、嵌套 eval、with 遮蔽、super、编译失败前不捕获;使用 `eval-wide`、`eval-capture`,加无 eval 的调用/循环控制组。共享结构的收益不能只靠 getter 或结构相等测试证明。 +2. **闭包**:首次捕获、同槽反复捕获、参数捕获、FunctionName/导入视图、私有绑定、循环 CloseLocal、逃逸与重入;分别测 `closure-create`、`argument-capture`、`lexical-lifetime` 和 `func_closure_call`。十轮控制组复核已经完成,最终组合的耗时与指令数已记录在本地证据中。 +3. **静态分支**:发布拒绝不可达坏目标、通用 host 拒绝越界、合法边界目标、分支与 finally/异常/恢复组合,以及保护执行入口和路由的架构变异;测空/递增/递减循环、整数运算、普通与闭包调用。 +4. **栈**:空栈、单元素、双元素顺序,带 root 的值在成功/失败路径上的释放与异常清理;检查生成代码是否实际合并范围判断,再测整数运算、比较、Swap/Nip 及调用控制。不能只把 `.get()` 改成下标并称为优化。 +5. **集成**:按第 5 节完成最终 workspace/feature 组合、严格 Clippy、全部架构变异、Test262 focused/full、Node/WASM 和 50+8 固定控制矩阵。冻结结果不改写;若只有引擎指纹变化,单独核对后明确说明。以前提交的验证不替代最终代码验收。 + +### 明确保留、不重复实施的部分 + +- **常量投影(E05)**:保留单一常量池和安全 enum match;已有静态属性 Atom 表继续使用。统一访问入口属于结构整理,不能记为常量分类已消除。不为去掉一次 match 引入复制常量池或逐指令类型表;rooting、新闭包和 RegExp 对象创建仍需执行。 +- **PC 递增**:此前 checked_add 简化已有重复测量证明退化,保持回退,不重做同一实验。该结论不适用于静态分支目标的重复验界。 +- **动态检查和表示**:保留 TDZ、实际捕获状态、参数别名、cell const、引用管理、动态返回地址、异常展开与恢复验证。不进行全栈表示重写、独立执行指令枚举或静态 PC 表建设;这不免除上表局部栈操作的检查合并工作。 + +### 提交、测量与完成规则 + +- 所有工作继续进入原 PR #18,一个可独立解释的优化一个小提交。提交前运行受影响的正确性检查;**只有实际优化提交之后才做 benchmark**,准备性抽象和纯文档提交不另做性能测量。 +- 每项至少五轮交错 A/B,保留正确输出准入、耗时及 instructions/cycles;对无法区分的波动和疑似退化追加双方样本并调查。某个方案失败只撤回该方案,不自动关闭尚未探索的方向。 +- 原 PR #17 的 `b11f2be` 用于整体比较;补齐阶段以重新构建的 `49cfa30` 为起点,各步骤再与直接前序比较。构建须核对源码、工具链、参数及 ELF 哈希;旧 receipt 与二进制不符时禁止直接测量或改写 receipt 冒充验证。 +- 本轮原始数据仅保存在忽略的 `target/published-follow-*`,不提交 benchmark 结果文件。本文件记录方案、状态和验证入口,不复制测量表。更换工作区时须重新生成数据,不能假定本地 target 可用。 +- 状态分别写“已实现”“已运行验证”“性能待确认”“已验收”或“保留且未优化”。只有所有待做项都有实现与验收证据、明确保留项没有被算成收益、最终集成完成,才可宣告交付完成。不得用“候选均有处置”替代这一条件。 + +### 可执行任务分解 + +以下任务号用于提交说明与交接;每个任务先读对应拥有者 README。任务状态以上方清单为准;下列步骤保留实现和复核入口,不以修改任务描述代替实际优化。 + +#### P1:完成 eval 共享视图的验收 + +**输入与拥有者**:`4b7bd08`;`code/executable.rs`、`vm/host_bridge.rs`、`vm/host_bridge/eval_validation.rs`、`builtins/eval.rs`。已有类型为 `PublishedEvalEnvironment`,不再新增“环境计划”包装层。 + +实施检查: + +1. 检查构造入口只从 snapshot 取得有效索引;私有字段必须同时保存只读环境数组、索引和对应字节码 root。构造失败不应产生可用视图。 +2. 确认 `PreparedEvalEnvironment → 编译 → MaterializedEvalEnvironment` 移动或共享同一视图;比较只能验证同一数组和索引,不重新逐项比较名称、flags 和拓扑。 +3. 确认编译前没有创建 VarRef;编译失败时视图与临时引用被释放。编译成功后才能捕获当前帧的真实绑定。 +4. 搜索这条路径上的 `EvalEnvironment::clone` 和 descriptor 克隆,区分 Rc/root 引用复制与 scopes/bindings 深拷贝。若仍有深拷贝消费者,逐一说明用途或迁移;不以“已加 Rc 类型”作为验收依据。 +5. 检查静态规则由发布验证层(含 `bytecode_publish` 调用的 `bytecode_validation`)统一拥有;VM 测试不得再保存一份静态验证算法。 + +测试落点:`executable::tests` 验证共享身份、不同索引、不同数组、root 存活及越界拒绝;`published_execution_tests` 验证同一环境多次 eval 仍观察新值、with 遮蔽、嵌套 eval、super;现有编译失败/捕获顺序测试必须通过。新增测试只覆盖现有测试未证明的契约,不复制 getter 实现。 + +采用条件:深拷贝与结构比较已实际移除;没有新增按绑定数复制的缓存;正确性通过;eval 目标负载指令或分配工作减少,且控制组无未解释退化。耗时无法区分时明确标为未证明耗时收益。需要修正时单独提交 `perf(eval): ...`,纯验收不制造空提交。 + +#### P2:核实并修正捕获复用路径 + +**输入与拥有者**:`d0d329e`;`host_bridge.rs::instantiate_closure`、`capture_frame_binding`、`close_frame_binding`;`published_execution_tests.rs`。优先处理初步测量中闭包调用控制组的异常变化。 + +实施检查: + +1. 对 ParentLocal 按实际 FrameBinding 分两种职责:已有 Captured 只验证并复用 cell;Direct、Uninitialized、Private 等首次捕获状态取得父定义,用 canonical flags 建立 cell。 +2. 保持 ParentArgument、ParentClosure、ParentGlobal 各自的来源语义,不能为了共用函数把所有来源伪装成局部槽。 +3. 对 FunctionName 的可变访问视图以及 ModuleImportView,检查实际 cell 验证仍允许发布器认证的差异;首次创建必须使用 canonical metadata,不能直接复制视图 flags。 +4. 检查新增状态分支是否随后又进入相同的完整状态 match,抵消已省的定义访问。若反汇编或诊断负载证实重复分派有成本,可提取小型“验证并复用已有 cell”函数供真实调用者共享;不引入布尔模式参数、通用 capture trait 或完整副本表。 +5. 检查借用在任何可能重入的运行时操作前结束;错误与中途失败不得泄漏已捕获 roots。CloseLocal 后必须重新进入首次捕获路径。 + +测试落点:在发布到执行测试中分别覆盖第一次捕获、同槽两个闭包共享更新、循环每轮独立 cell、命名函数与直接 eval 共用 cell、私有字段/方法。保留模块 live-binding 与错误 cell 元数据的既有边界测试。 + +测量步骤:先对 `4b7bd08 → d0d329e` 的上述四个负载补十轮交错 A/B,双方使用相同机器、CPU 与普通 release 构建;同时比较 instructions/cycles,检查退化是否可复现。若复现,先定位代码布局或新增分派成本,实施一个具体修正后提交并重新五轮 A/B。不能用目标微负载略少指令掩盖调用控制组变慢。若最终撤回,保留不采用该实现的证据,明确首次/重复捕获仍有哪些工作,而不是宣布捕获方向全部完成。 + +#### P3:验证静态分支确实减少运行时工作 + +**输入与拥有者**:`7521630`;`bytecode.rs::validate_target`、`protocol.rs::VmHost`、`host_bridge.rs::static_branch_target`、`frame_execution.rs::execute_inner`。 + +证明链必须逐点核对: + +1. verifier 对所有 IfTrue/IfFalse/Goto 操作数执行范围验证,不能只检查可达块。 +2. `execute_published`、`start_published` 经 `new_activation` 取得同一 host 的代码和布局;恢复路径经 `decode_vm_activation` 保持对应关系。任何能混配代码与 host 的生产入口都必须先封闭,不能靠注释假定不存在。 +3. `VmHost` 默认入口继续调用受检目标转换;Runtime host 仅复用已发布目标范围证明。无 root 的测试 fixture 必须继续拒绝坏目标。 +4. 搜索新入口的全部调用者,只允许三个立即数分支使用;Catch/Gosub/Ret、异常 region、恢复 PC 继续用原检查。u32 到 usize 的必要转换保留,不能通过截断扩大平台假设。 +5. 检查生成代码中 Runtime host 路径的目标范围比较确实消失,且未新增每次指令的信任标志分支或间接调用。 + +测试落点:现有 `static_branch_targets_remain_checked_at_untrusted_boundaries` 要断言明确的目标错误,防止因 max_stack 等无关错误“通过”;补合法首/尾目标、两个条件方向、finally 内分支、挂起后分支及回溯 PC 的缺口。架构规则不仅更新 hash:执行现有 tail/throw 路由变异,确认错误拦截仍被拒绝;补充“让通用/合成目标入口不验界”的反例覆盖。 + +提交后测量:以 `d0d329e` 为直接前序,对 `7521630` 做至少五轮 loop/int/call/closure A/B 和 instructions/cycles。若 P2 后续修正或撤回,在最终前序版本上重新比较,不能仅沿用这组历史结果验收。若发现可复现退化,调整具体实现或撤回它;不使用已失败的 PC 递增实验代替本步骤测量。 + +#### P4:实现一次范围判断的双操作数取出 + +**输入与拥有者**:最终采用的 P3 版本;先检查现有 release 机器码是否仍有重复范围判断,再决定是否实施候选算法。`frame_execution.rs::pop_pair`,消费者为同文件 Nip/Swap、`numeric_execution.rs` 和 `dispatch.rs` 的二元操作。先改共同拥有者,不逐 opcode 复制快路径。 + +当前语义是先 pop 右值,再 pop 左值,返回 `(left, right)`。新实现必须保留下面的可观察状态: + +| 输入栈 | 返回与剩余状态 | +| --- | --- | +| 空栈 | 返回原 underflow 错误,栈仍为空 | +| 单元素 | 消费并释放该元素,返回原 underflow 错误,栈为空 | +| 至少两元素 | 返回原顺序的左右值,前缀不变;取出的 roots 不提前释放 | + +实际实现方案:先统一检查 `len >= 2`,成功后直接使用两次 Vec::pop 移出右、左值,不再分别构造 VM 的 Result;失败分支保留原来的消费、错误构造及释放顺序。这个动态范围检查证明两个 expect 不会失败,非法输入仍返回原内部错误。备选切片匹配加 mem::replace/truncate 不先采用,避免引入占位值写入/释放;所有移动使用安全 Rust,不使用 unchecked/set_len,不新增临时 Vec,不 clone Value。最终以生成代码是否合并范围判断及实测决定采用。 + +实施顺序: + +1. 先为上表三个状态增加直接行为测试,包含对象或其他持有 root 的值,检查失败释放及成功后值仍存活。 +2. 修改单个 `pop_pair`,让所有现有消费者自然使用;不同时改变算术或属性语义。 +3. 检查普通二元运算、Swap/Nip、属性写入和异常路径;保持用户转换回调发生时机不变。 +4. 检查 release 反汇编中的范围判断、Value 移动及 drop 数量。若两次 Vec::pop 的成功条件未被消除,再比较局部切片分解的等价实现,不引入独立栈类型;仍不成立时记录此方案不采用。 +5. `clone_at_depth` 的基线 release 机器码仍有两个范围分支;最终使用 len.wrapping_sub(usize::from(depth) + 1) 后一次 get;depth 为 u8,减数非零且不会溢出 usize,过深时环绕索引大于 len,由同一个 get 拒绝。穷举全部 u8 深度与空栈、255/256/257 长度边界,验证值、错误、栈不变及 root 克隆;release 机器码只保留一个范围条件。 + +验证负载:整数与浮点运算、比较、Swap/Nip 专用循环,外加调用与属性访问控制组。新增诊断脚本固定循环次数和输出,并在第一次 A/B 前冻结哈希;结果不代替完整控制矩阵。已完成上述实现及分步测量。切片占位值方案保留了额外写入和析构循环;drain 保留额外边界与清理;cold/强制内联策略改变调用布局并造成耗时退化,均已撤回。最终采用普通辅助函数和局部错误路径,未添加内联策略。 + +#### P5:最终集成、文档与独立评审 + +依赖 P1–P4 的最终采用版本,不依赖旧的“候选处置完成”结论。 + +1. 清点每项最终减少的操作,注明只属于维护性改善或必要保留的部分。检查没有测试专用静态验证器、双 opcode 表、可伪造凭据或临时兼容入口残留。 +2. 按下面命令入口执行最终正确性验收;所有失败都要定位到本次变更或冻结基线,不能直接更新 oracle/Test262 期望。完整矩阵按最终采用代码重建,构建和测试不得与测量争抢同一机器。 +3. 比较完整 50 microbench + 8 V8 控制组,按固定输出准入;与 `49cfa30` 比较补齐部分,与 `b11f2be` 比较累计效果。不得将固定工作量的比率称为上游 adaptive score。 +4. 独立评审重点为 eval 视图原子生命周期、FunctionName/cell 视图差异、静态分支的代码配对及栈错误路径的释放语义。自查单独记录,不冒充独立评审。 +5. 在本文件更新任务状态、提交和未解决项,在模块 README/契约文档记录最终职责。PR 描述同步实际范围;原始 benchmark 文件不提交。没有满足本任务时保持“集成未完成”。 + +### 最终本地验收记录 + +实施者:Codex;生产代码为 `c55d59d`,与 `04bf79d` 的 src/Cargo 输入完全一致。最终普通 release 二进制重新构建,并与此前保留副本逐字节核对;后续文档提交不改变生产源码。 + +- Rust 1.88.0:workspace/all-targets(引擎 1,969 项)、profiling、doc、test262-host 组合及五组严格 Clippy 通过;额外 65K 参数边界测试通过。14 个架构规则测试、16 个 benchmark 工具测试,以及源码、registry、布局、rust-only 和 runner provenance 门禁通过。 +- 架构:完整 701 个变异全部被拒绝,包括新增的通用/合成静态目标验界反例。没有只更新指纹后跳过变异测试。 +- Test262:完整 102,037 条向量中,79,982 pass、原有 50 fail、3,530 unsupported、18,475 skipped 不变。原 full 命令因源码指纹变化返回 receipt checksum drift;只规范化唯一引擎指纹字段后,完整 TSV/JSONL 哈希与冻结记录相同。从本次 full 中逐列核对的 6,844 个 focused 条目全部一致;没有更新冻结基线。 +- Node/WASM:15 个 playground 示例、direct eval、指标与构建元数据、可捕获的深层 yield-star 溢出通过。 +- 性能:优化提交后执行分步 A/B、最终三版本 50+8 控制矩阵和 8 个诊断负载;最终 990 个样本均通过输出准入。疑似退化项追加双方各十轮;单独复核最终代码中的静态分支和栈改动,并对静态分支的算术/调用控制追加十轮。静态分支隔离对照的算术耗时仍上升,保留该实现的依据是目标循环收益与指令工作减少;最终完整组合的算术耗时仍低于本轮起点,不能用这一点掩盖单项代价。累计优化改善固定工作量耗时,本轮补齐的综合耗时基本持平;闭包写入、数组操作及宽 eval 环境仍有小幅耗时代价,不据此宣称全面提速。常量分类和 PC 递增保留项没有计入已消除的工作。 +- 证据在忽略的 `target/published-follow-*`:`validation.log`、`boundary-full.log`、`test262-full.log`、`test262-comparison.json`、`web.log`,以及 `complete-ab`、`complete-recheck`、`static-control-ab`、`static-control-recheck`、`final-stack-recheck` 与各 `*-counters.json`。文件前缀均为 `published-follow-`;原始报告不进入 PR。迁移工作区须按下方入口重建并复测。 + +实施者已自查构造封闭性、实际 cell 视图、eval root 生命周期、静态目标配对和栈失败释放顺序。独立评审仍待完成;这是剩余评审事项,不是未实施的优化,也不以自查代替它。 + +### 验证与复现入口 + +命令从仓库根目录执行。下列定向命令对应 P1–P4;工作区集成组合以 README 和 `.github/workflows/ci.yml` 为准,不在计划里维护第二份完整 CI 配置。 + +```sh +cargo fmt --all -- --check +cargo +1.88.0 test --locked -p quickjs-oxide --lib published_execution_tests +cargo +1.88.0 test --locked -p quickjs-oxide --lib eval_ +cargo +1.88.0 test --locked -p quickjs-oxide --lib +cargo +1.88.0 clippy --locked -p quickjs-oxide --lib -- -D warnings +./scripts/checks/check-binary-object-boundary.sh --scan-only "$PWD" +PYTHONPATH=scripts/checks python3 -m unittest discover -s scripts/checks/binary_object/tests +./scripts/checks/check-binary-object-boundary.sh +TEST262_WORKERS=2 ./scripts/test262/test-test262.sh --full +./scripts/web/test-web-playground.sh +``` + +源码指纹变化时,冻结的 `--focused` 入口会拒绝运行。此时执行 `--full`,再按 `(path, variant)` 从完整结果逐列核对冻结 focused 的全部条目;完整 TSV/JSONL 只规范化唯一引擎指纹字段后比较冻结哈希,不能改写冻结记录或忽略行为列差异。 + +性能入口使用 `scripts/benchmark/fixed.py`。当前本地 manifest 为 `target/published-follow-manifest.json`,仅适用于哈希校验仍通过的工作区;迁移机器必须从固定负载来源重新生成。先构建已提交版本,分别复制为不可变 before/after 二进制并记录 receipt,再执行,例如 P3: + +```sh +python3 scripts/benchmark/fixed.py \ + --manifest target/published-follow-manifest.json \ + --engine before=/absolute/before/qjs --engine after=/absolute/after/qjs \ + --case empty_loop --case int_arith --case func_call --case func_closure_call \ + --repeat 5 --cpu 2 --output target/published-follow-branch-ab +``` + +CPU 2 是当前机器的绑定设置;更换机器须重新记录实际 CPU。另用 `perf stat -x, -e instructions:u,cycles:u -- taskset -c 2 ...` 对同一冻结负载按相同轮次交错采样,逐样本验证退出码和标准输出,不混入 profiling 特性或后台构建。当前 P1/P2 的本地证据位于 `target/published-follow-eval-*`、`target/published-follow-capture-*`;P3/P4 不得引用这些结果声称自己的优化已经测量。 + +## 1. 目标与基线 + +将发布阶段确认的静态事实可靠地传递给 VM,减少绑定访问、常量访问、闭包创建与控制流中的重复工作。共同建立执行契约,分别验证优化收益;可维护性、可读性与语言兼容是验收条件。 + +计划依据为 [Issue #16](https://github.com/pocket-stack/quickjs-oxide/issues/16)、[PR #17](https://github.com/pocket-stack/quickjs-oxide/pull/17),以及本地 `b11f2be` 的源码与报告。实施时核对 PR 状态和基线提交;这里不假定 PR 已合入。上一轮范围见[数据结构计划](data-structure-plan.md)。 + +- [最终硬件与采样报告](reports/data-structure-hardware-final.md):空循环 `read_frame_binding` 自身成本为 15.1–23.8%,hot/numeric dispatch 与执行循环也占明显比例;优化后的空循环约执行 110.47 亿条指令,QuickJS 约 5.62 亿条。 +- [最终固定工作量报告](reports/data-structure-fixed-final.md):保留全部 50 项 microbench 和 8 项 V8 子套件,作为控制组;不能只选此次优化收益最大的程序。 +- [数值栈与调用参数报告](reports/vm-stack-and-call.md):数值原地更新有收益,参数借用的调用结果基本持平;不能据此直接优先引入帧池。 + +上述数字属于历史测量,不是本计划的基线复测结果。Issue #16 与 PR #17 的固定源文件组织不同,绝对时间不能直接拼接。采样占比不能当作可取得的加速幅度。 + +本轮不包含 JIT、unsafe、新的公开字节码格式、全局 Value 表示重写、帧池或另一套 GC。属性 inline cache 和一般对象语义优化也不混入本轮。 + +## 2. 已发现的问题与检查归属 + +规划时的基线发布验证已覆盖多种指令和元数据关系,但 `VerifiedBytecode` 只返回 `max_stack`;`PublishedFunctionSnapshot` 将代码、常量和定义分别交出;`CallInput` 与 VM 执行入口仍接受指令切片及独立 host。现有生产调用链具有约定,不等于类型已保证代码与帧来自同一发布对象。 + +下表是检查迁移候选,不是删除检查的授权清单。实施前逐项补全所有生产入口、证明范围和反例。尤其不能把相似错误字符串当作两次检查等价的证据。 + +| 领域 | 发布时的静态事实 | 建帧或恢复时的保证 | 执行时必须保留 | +| --- | --- | --- | --- | +| 局部变量 | 下标与定义匹配;普通、词法、private/with 等访问是否合法;写指令权限 | locals 布局来自对应代码;初始状态与定义一致 | TDZ、作用域重新进入、Direct/Captured 变化、初始化次序 | +| 参数 | 指令下标与形参布局匹配 | 形参槽补 Undefined;实际参数数独立保留 | mapped arguments、捕获、实际参数语义;参数路径本来较短,不假设有同等重复检查 | +| 闭包与全局描述符 | 指令访问模式与描述符匹配;静态 const/特殊绑定限制 | 实际 closure slots 数量、来源和必要 cell 元数据与代码匹配 | TDZ、live binding、全局属性行为、实际 cell 状态与引用所有权 | +| 常量与名字 | PushConst/FClosure/RegExp/字符串名字操作数的种类和范围 | 常量与 Atom 受同一发布对象的 roots 保护 | rooting、新对象创建、属性访问与异常语义 | +| 闭包创建 | 父子捕获来源、名称、flags 和特殊视图关系合法 | 当前父帧与捕获计划相符 | 建立/复用实际 VarRef,捕获逃逸、失败回滚与引用管理 | +| 静态控制流 | 分支目标合法;可达路径不越出代码;栈效果、汇合与特殊标记协议 | 初始 activation 符合布局;恢复 PC、栈与 regions 有效 | 动态返回地址、异常展开、挂起恢复、backtrace PC | +| eval 环境 | 描述符拓扑、静态名字与来源、能力来源合法 | 当前调用方身份、环境与实际引用匹配 | direct-eval 身份门槛、动态查找、遮蔽、重入与活引用 | +| dispatch | 指令类别由不可变 opcode 决定 | 执行视图与代码一致 | 每次操作的转换、异常、暂停与调用行为 | + +每项候选建立一条简短记录:静态事实 → 验证函数 → 发布入口 → 不可变拥有者 → 建帧/恢复约束 → 使用者 → 拒绝反例。不能完成此链条的检查继续保留。 + +## 3. 统一抽象与职责 + +沿用[职责边界](responsibility-boundaries.md)和[工作区架构](architecture.md)。以下类型名为设计名称,实施前按真实调用者定稿;不要求机械地新增三个包装类型。 + +```text +编译器 / 受限 BC5 解码 / 模块与 eval 草稿 + → 验证、链接、事务发布 + → PublishedExecutable(不可变代码与执行布局) + → 建帧或恢复边界 + → ExecutionFrame(对应代码与动态状态) + → VM 执行 +``` + +### 3.1 已发布代码 + +由 code 模块拥有不可变执行描述与受限构造入口,将指令、常量布局、绑定布局和必要派生计划作为一个整体提供。优先收拢现有 snapshot 和 bytecode 数据,而不是再复制一套完整数据。 + +- 只允许成功完成相应验证与链接的生产发布路径构造;覆盖 Script、模块、eval、动态函数和受限 BC5,不扩大现有解码信任范围。 +- 验证凭据必须关联实际被验证的数据。禁止公开可伪造的布尔标志、脱离所属函数的通用 token,以及验证后可变更代码的别名。 +- 现有 heap 节点仍拥有强边和 Atom 引用。执行视图保留已有字节码 root,派生表默认不额外拥有 JS roots;如需例外,明确 retain/release/trace 与回滚。 +- Runtime 身份、realm 和实际 heap 对象的检查仍由相应边界负责。静态结构合法不代表某个运行时句柄有效。 + +### 3.2 帧布局与绑定访问 + +由 VM 拥有帧的可变状态,构造时从同一 executable 推导布局,避免调用方手工组合 code、metadata、locals 与 host。借用或拥有的实现选择须满足递归、重入和挂起;不能跨用户代码持有 Runtime 的 RefCell 借用。 + +统一 local/argument/closure 的访问契约和共享值读写机制,但保留语义不同的明确入口,例如普通读取、词法检查读取和初始化。不要用多个布尔参数组成一个万能访问函数,也不把全局属性查找伪装成直接槽读取。 + +静态访问权限与动态存储状态分开:发布时知道普通读取合法,不意味着变量永远是 Direct;捕获和 eval 可以改变其存储。TDZ、初始化、CloseLocal 复用与别名关系不能被静态标签覆盖。 + +### 3.3 常量、捕获与环境计划 + +优先复用现有 opcode 表达的访问模式及已链接的属性 Atom。确有重复解析时,由 code 模块在发布时生成紧凑的种类明确的操作数或计划,VM 只应用计划。 + +- 常量计划不重复 intern、不复制完整常量池,也不为每条指令创建独立的 rooted Value。 +- 捕获计划记录静态来源和必要的 canonical metadata;保留 FunctionName、ModuleImportView 等合法视图差异,不能简单要求所有 flags 完全相同。 +- eval 计划仅描述静态部分,不缓存动态名字解析结果或跨回调保存可能失效的槽位置。 +- 派生数据与源数据只有一个构建拥有者,不能由调用者同步维护。空间、发布时间与销毁成本列入验收。 + +### 3.4 控制流与执行循环 + +静态跳转与动态恢复分开处理。已验证的目标可考虑在发布时转换为内部 PC 表示;保持与原指令 PC 的对应关系,不破坏源码定位、回溯、异常和暂停位置。 + +栈验证不会自动证明任意恢复快照合法;取消栈检查之前必须覆盖 handlers、regions、Gosub/Ret、yield/await、异常路径。安全 Rust 的新类型不会自动消除边界检查,`.get()` 改成 `[]` 也可能只是将错误变为 panic,不算优化依据。 + +dispatch 分类可统一拥有,先评估单一明确分派结构;仅在实测支持时增加预解码类别。不要让类别表与执行 match 靠人工双向同步。普通完成与可挂起执行共享内部契约,但保留各自驱动返回类型,避免扩大每层普通递归的本机栈帧,维持已有两 MiB 栈回归边界。 + +### 3.5 可读性与抽象选择 + +- 使用具体类型、受限可见性和少量明确入口;不新增独立 crate、插件注册层、策略框架或仅为此次改动服务的通用 trait。 +- 保留现有 VmHost 的语义桥接职责,按真实生产与测试调用者评估边界,不为减少参数再造一层 host。 +- 不默认复制整套 Instruction 枚举。比较“收拢现有数据”“紧凑派生计划”“独立执行指令”三种方案,优先第一种,按证据局部采用第二种;第三种必须证明额外代码量和映射成本值得。 +- 不给每个整数机械地加新类型。类型须防止真实误用;LocalSlot 与 ArgumentSlot 可以区分,但裸下标包装本身不能证明所属函数身份。 +- 按验证、发布存储、绑定访问、帧构造、恢复等职责组织模块,避免继续堆入巨型 host bridge 或转移到通用 utils。 +- 注释解释证明依赖、回调边界、所有权和例外原因,不逐行复述操作。重要不变量应由私有字段、构造器和行为测试共同落实。 + +## 4. 实施步骤与依赖 + +实现者:Codex。下表保留原计划范围,并记录实际处置;具体证明链与保留项见[执行契约说明](published-execution-contracts.md)。本 PR 保留代码、测试和维护说明,不提交本轮 benchmark 测量结果。准备工作不等于性能收益,独立评审尚未完成。 + +| 步骤 | 依赖 | 原计划交付与范围 | 完成条件 | 实际处置 | +| --- | --- | --- | --- | --- | +| E01 证明清单与基线 | 无 | 清点全部发布、建帧、执行、恢复及测试入口;填充第 2 节证明链;复测固定负载 | 每个候选有已证明/待证明状态、反例和测量口径;记录基线身份 | 当前代码的证明链已记录;基线身份沿用 PR #17。测量结果不纳入本 PR。 | +| E02 已发布执行描述 | E01 | 收拢不可变代码及布局;封闭构造;迁移生产发布与 snapshot | 未验证草稿或不匹配元数据无法经正常接口进入生产 VM;发布失败回滚与原有边界测试通过 | 已实现:`VerifiedFunction` 消费准确草稿;snapshot 收拢只读拥有权。`7a343e5` / `5b96313`。 | +| E03 代码与帧配对 | E02 | 统一建帧契约;普通调用、模块特殊入口、generator/async 恢复接入;隔离合成测试入口 | 错代码、错 Runtime、错 closure 布局和非法恢复被拒绝;重入、清理、递归上限不退化 | 已实现:`new_activation` 从 host 推导代码/布局;恢复封装字段私有。`5b96313`。上游 callable 来源仍由原提取边界负责。 | +| E04 绑定访问 | E03 | 一起处理 local、argument、VarRef 的共享机制与静态模式检查 | 普通/词法/捕获语义测试、错误反例通过;逐项记录减少的工作与 loop/call/closure A/B | 已实现:普通/checked 读写复用发布保证;初始化和 CloseLocal 移除已证明的模式检查;捕获写入不再 clone cell,错误名字按需读取。TDZ、实际 cell const、特殊初始化协议保留。`a4b7830` / `27f5f30`。 | +| E05 常量与静态名字 | E03 | 种类明确的常量访问;复用链接 Atom;避免重复分类 | 常量种类混用在边界拒绝;root 生命周期与异常一致;执行、发布、内存数据齐全 | 明确保留分类操作:统一常量投影,复用既有 Atom 表。安全 enum match 仍在,不计作已消除的重复分类;不新增复制池/种类表。`51065a5` / `a23ec52`。 | +| E06 捕获与 eval 计划 | E04、E05 | 将静态父子描述符匹配和环境整理移至发布;实际 cell/调用方验证留在边界 | 特殊视图、eval 遮蔽/捕获、逃逸、失败回滚通过;无回调重放或缓存失效问题 | 初始静态验证迁移已实现(`51065a5` / `8d4a2da`);补齐环境共享与捕获元数据分别见 `4b7bd08` / `d0d329e`。`64785fa` 补齐已有 cell 的直接复用;最终本地正确性和性能验证已完成,耗时结论以上方清单及本地证据为准。 | +| E07 静态控制流与栈 | E03、E04 | 评估目标转换、PC 递增与栈检查;只迁移证明完整的部分 | 分支、finally、异常、恢复、回溯及畸形字节码覆盖;无 panic 替代原错误;记录保留项 | 已实现:静态分支 `7521630`、双操作数检查合并与一次深度查询 `c55d59d`,分步测量已完成,最终本地集成验收已完成。PC 递增实验撤回;动态目标与恢复检查保留。 | +| E08 dispatch | E04、E05、E07 | 统一指令分类拥有者,评估减少多层分类;不改变操作语义 | opcode 覆盖完整,原 PC 对应不变;机器码/指令数与综合负载支持选型 | 已实现:常用绑定、字面量、简单栈操作和分支在循环中直接执行;调用、复杂数值转换与其他语义处理器独立保留以控制递归帧。无预解码表或第二指令枚举。`7a7fab7` / `254581d`。 | +| E09 集成与交接 | E06、E08 | 清除临时兼容入口与双实现;更新职责文档;完整正确性与性能复测 | 第 5、6 节验收齐全,保留无收益/退化结果,每项候选有最终处置 | 本地集成已完成:最终代码重新通过第 5 节测试和固定控制矩阵;独立评审仍待进行,不将 E09 标成全部评审完成。原始测量结果只在本地 target。 | + +补齐前已经实施 E04 写入与生命周期、E08 常用指令直接执行、E06 eval 发布保证,并撤回 PC 递增实验。当前后续顺序、必要保留项与验收标准见文首清单;补齐后的实际完成范围以上方状态表为准,独立评审仍未完成。计划不承诺消除 QuickJS 与本解释器的全部机器指令差距。 + +E04/E05/E07 在设计上部分独立,但共享 VM 与代码存储,不默认并行编辑同一核心文件。E01 确认证明缺口后可细分步骤或调整依赖,并在此表记录理由。 + +实验无收益时可以保留能显著简化契约且成本可接受的结构改进,但必须明确称为维护性改进。增加复杂度的快路径若无可信收益,应撤回;证据不足的检查继续保留。每个候选最终只能是已验证实现、实测后不采用,或有明确原因与后续入口的延期,不能从清单消失。 + +## 5. 正确性与性能验收 + +### 5.1 正确性 + +先补会在旧错误实现上失败的契约测试,再迁移对应行为。合成测试需要独立的 checked 入口;测试构造便利性不能成为生产绕过验证的能力。验证器反例与真实发布到执行的端到端测试同时维护,不能只有封装 getter 的镜像测试。 + +必测类别: + +- 普通与 checked local/closure、const、TDZ、缺省和多余实参、mapped/strict arguments、循环词法绑定、CloseLocal 与异常后的复用。 +- closure 在捕获前后读取、逃逸与嵌套 relay;FunctionName、模块导入视图与 live binding;direct eval、with、动态函数与动态环境遮蔽。 +- 错常量种类、错索引、错帧/代码配对、错 Runtime、非法闭包引用、受限 BC5 和模块/eval 能力伪造。 +- generator、async、async generator 的 yield/await/throw/return、重复恢复与异常展开;finally/Gosub/Ret、iterator close、回溯 PC 和源码位置。 +- 用户回调重入、GC、发布中途失败、Return/Throw/internal error 的清理;普通与混合递归的现有栈边界。 + +边界变更要同步修改 architecture checker 的规则、证据及 mutation canaries;必须证明错误变体仍命中实际入口并被拒绝,不能仅更新源码指纹让检查变绿。冻结 oracle/Test262 历史记录不改写;新结果单独生成并比较。 + +每步跑拥有者相关测试与受影响边界检查。集成时按当前 CI 固定工具链跑完整 workspace、profiling、doc、test262-host 组合、严格 lint、源码布局和全部架构 canaries,补 focused 与 full Test262、native release 和 Node/WASM playground 验收。具体命令以 [README 验证入口](../README.md#verify) 与当前 CI 为准,不复制一份长期漂移的组合清单。 + +性能和正确性分别报告;stdout 一致仅是负载准入,不替代上述测试。不能以 quickjs-oxide 已有失败为由接受新增失败,也不能静默刷新冻结数据。 + +### 5.2 测量 + +E01 固定并保存源码提交、引擎 fingerprint、工具链、编译参数、ELF/workload 哈希、机器、CPU 绑定、重复次数及超时。普通 timing binary 关闭 profiling;CPU sampling 使用独立带符号/帧指针 binary。构建、测试与基准不争用同一测量机器。 + +每个性能步骤至少五轮交错 A/B,负载足够长,报告全部样本、中位数、范围及 retired instructions/cycles。重复数和准入规则在实验前固定;噪声大时按事先约定扩展双方样本,不挑最快值。性能退化超过基线噪声范围或分布无法区分时,调查或记录为未证明收益,不预先承诺统一加速阈值。 + +重点负载包括 empty/up/down loop、int/float arithmetic、普通/闭包调用、local/argument/捕获绑定混合访问、反复创建闭包、静态属性名、常量与 RegExp literal、eval、分支/finally,以及 generator/async 恢复。新增诊断负载用来隔离机制,不能替换现有完整 50+8 控制矩阵。保留原 adaptive harness 的成功/超时状态,不用固定工作量冒充其 score。 + +新增派生计划还要覆盖空函数、小函数、大常量池、宽局部变量、深闭包树以及反复发布/销毁。记录代码/计划长度、容量和可测的拥有存储,检查是否按指令重复存数据;不要用窄 profiler 覆盖推断总分配或 RSS。只能取得整进程时间时如实标注,不能用相减构造未经验证的 compile-only 时间。 + +E09 对 E01 基线做完整对比,逐项列出收益、持平、退化与成本转移。优化只改变热点占比而没有更少工作或更好时间,不足以认定成功。 + +## 6. 人与 agent 的维护协议 + +### 6.1 模块入口和扩展方式 + +| 要修改的行为 | 首先阅读的拥有者 | 同步维护 | +| --- | --- | --- | +| 新 opcode 或操作数种类 | code 的指令、验证与发布 | 栈效果、访问模式、执行分派、拒绝反例、PC/序列化对应 | +| 新发布来源 | code 的事务发布;相应编译或解码入口 | 能力来源、构造封闭性、roots/回滚、生产边界 canary | +| 绑定读写与捕获 | VM 的帧绑定;code 的绑定布局 | TDZ/const、别名和复用、实际 VarRef 与静态计划一致性 | +| 常量或环境计划 | code 的已发布执行描述 | 构建拥有者、派生表空间、Atom/GC 生命周期与销毁 | +| 新调用或恢复方式 | VM 的建帧、activation 与 suspension | 代码身份、动态状态验证、异常清理、本机栈预算 | + +实现后在每个真实拥有者的模块 README 写明:拥有/借用什么、允许哪些调用者、哪些函数可能调用用户代码、错误与回滚契约、复杂度/空间成本,以及正向和反例测试入口。以模块名和符号定位,不把历史行号当接口。本文记录跨模块决策,局部契约留在所属模块,避免两处维护完整重复说明。 + +新增一个检查时,先回答它依赖的是草稿结构、已发布布局、实际帧还是当前 JS 值;据此放到相应边界。新增一个缓存或计划时,先回答谁构建、谁拥有、何时失效、是否跨回调,再决定是否需要抽象。 + +### 6.2 实施、评审与交接 + +人和 agent 使用同一标准。每步开始先核对本计划状态和实际提交,读拥有者说明,明确准备迁移的检查及证明链;不要仅凭上一位维护者称“已验证”就删除检查。 + +E02/E03 的执行契约,以及 E06/E07 的捕获、恢复和控制流变化,应安排独立评审,评审者可以是另一位人或 agent。重点检查构造能力是否泄漏、是否混配所属函数、静态证明是否覆盖实际入口以及动态语义是否遗漏。实施者自己的通读不称为独立评审。本计划不要求每个可逆操作额外请求许可。当前实现者的通读只记为自查;PR 的独立评审仍待完成。 + +每步完成后在本计划状态表与对应报告保留以下信息;报告是证据附件,不另建冲突的总任务清单: + +```text +步骤/状态/实施者: +基线与结果提交: +实际拥有者与公开到模块间的接口: +迁移的检查、静态依据、保留的动态条件: +所有权、回调/重入、错误与回滚契约: +采用/未采用方案及原因: +验证命令、环境、退出结果、报告位置: +性能/空间样本、收益、持平与退化: +独立评审状态及未解决意见: +剩余工作、下一入口和复现命令: +``` + +交接必须标明测试是“已编写”“已运行”还是“通过”,以及当前工作树和正在运行的任务。没有实测的结论标为假设;未完成步骤保持未完成。最终收口要求所有入口使用统一契约、临时迁移接口已移除、维护文档可供新读者操作,且第 2 节每项候选都有可追溯处置。 diff --git a/scripts/benchmark/README.md b/scripts/benchmark/README.md index 83e8e9a1..2bb37a03 100644 --- a/scripts/benchmark/README.md +++ b/scripts/benchmark/README.md @@ -176,3 +176,23 @@ benchmarking. Never revise conformance baselines to turn a performance change into an apparent pass. Scaling workloads also cover Array/TypedArray integer reads and writes, repeated interior Array deletion/reinsertion, strict and mapped Arguments construction, and RegExp named groups/indices. Use sizes below 255 for regexp-groups. Mapped arguments use a non-strict Function body explicitly because workload files are modules. + +## Replay the fixed-work matrix + +`fixed.py` replays the workload manifest in the final data-structure report. +It checks every source hash before measuring, rotates engine order, retains raw +outputs and rejects nonempty stderr. `--workload-dir` relocates existing files; +it never regenerates or silently changes third-party workloads. Reconstruct +missing files using the recipe in the fixed-work report, then verify the hashes. + +```sh +python3 scripts/benchmark/fixed.py \ + --manifest docs/reports/data-structure-fixed-final.json \ + --engine before=/absolute/baseline/qjs --engine after=/absolute/changed/qjs \ + --repeat 5 --cpu 2 --output target/published-fixed +``` + +Omitting `--case` covers all 58 manifest entries. Repeated `--case` options are +for step-level experiments only. All times include the whole process; these are +not adaptive harness scores. Preserve build receipts separately and do not run +benchmarks alongside builds, tests or architecture canaries. diff --git a/scripts/benchmark/fixed.py b/scripts/benchmark/fixed.py new file mode 100644 index 00000000..fe14c0ee --- /dev/null +++ b/scripts/benchmark/fixed.py @@ -0,0 +1,104 @@ +"""Replay a recorded fixed-work matrix, verifying bytes before measuring.""" +import argparse +import json +import re +from pathlib import Path +from run import binary_metadata, digest, machine_metadata, run_sample +from scaling import admit, summarize + + +def load_workloads(report, directory=None, cases=None): + workloads = json.loads(Path(report).read_text())["metadata"]["workloads"]["workloads"] + names = set() + for item in workloads: + name = item["case"] + if not re.fullmatch(r"[a-zA-Z0-9_-]+", name) or name in names: + raise ValueError("workload names must be unique safe identifiers") + names.add(name) + if cases and (len(cases) != len(set(cases)) or set(cases) - names): + raise ValueError("requested cases must be unique and present in the manifest") + selected = [] + for item in workloads: + if cases and item["case"] not in cases: + continue + item = dict(item) + path = (Path(directory) / Path(item["path"]).name) if directory else Path(item["path"]) + if digest(path) != item["sha256"]: + raise ValueError(f"workload bytes changed: {item['case']}") + item["path"] = str(path.resolve()) + selected.append(item) + if not selected: + raise ValueError("empty fixed-work matrix") + return selected + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--manifest', type=Path, required=True) + parser.add_argument('--workload-dir', type=Path) + parser.add_argument('--engine', action='append', required=True) + parser.add_argument('--case', action='append') + parser.add_argument('--repeat', type=int, default=5) + parser.add_argument('--timeout', type=float, default=180) + parser.add_argument('--cpu', type=int) + parser.add_argument('--output', type=Path, required=True) + args = parser.parse_args() + if args.repeat < 1 or args.timeout <= 0 or (args.cpu is not None and args.cpu < 0): + parser.error('invalid repeat, timeout or CPU') + engines = {} + for entry in args.engine: + name, separator, path = entry.partition('=') + if not separator or not re.fullmatch(r'[a-zA-Z0-9_-]+', name) or name in engines: + parser.error('engines must be unique name=path entries') + engines[name] = Path(path).resolve() + workloads = load_workloads(args.manifest, args.workload_dir, args.case) + metadata = dict( + machine=machine_metadata(), + runner_sha256=digest(__file__), + manifest_sha256=digest(args.manifest), + workloads=workloads, + engines={name: binary_metadata(path) for name, path in engines.items()}, + repeat=args.repeat, + timeout_seconds=args.timeout, + cpu=args.cpu, + metric='whole-process wall nanoseconds; not adaptive scores', + ) + output = args.output.resolve() + output.mkdir(parents=True, exist_ok=False) + (output / 'raw').mkdir() + (output / 'metadata.json').write_text(json.dumps(metadata, indent=2) + '\n') + samples = [] + with (output / 'samples.jsonl').open('w') as journal: + for workload in workloads: + for repetition in range(args.repeat): + names = list(engines) + offset = repetition % len(names) + for name in names[offset:] + names[:offset]: + if digest(workload['path']) != workload['sha256']: + raise ValueError('workload changed during measurement') + if digest(engines[name]) != metadata['engines'][name]['sha256']: + raise ValueError('binary changed during measurement') + prefix = output / 'raw' / f"{workload['case']}-{name}-{repetition}" + cmd = [str(engines[name]), workload['path']] + if args.cpu is not None: + cmd = ['taskset', '-c', str(args.cpu), *cmd] + sample = run_sample(cmd, output, prefix, args.timeout) + status = admit(sample, workload['expected'].encode()) + if status == 'ok' and Path(sample['stderr']).read_bytes(): + status = 'unexpected-stderr' + sample.update( + case=workload['case'], size=workload['size'], engine=name, + repetition=repetition, status=status, + ) + samples.append(sample) + journal.write(json.dumps(sample) + '\n') + journal.flush() + print(f'{prefix.name}: {status}', flush=True) + summary = summarize(samples) + result = dict(metadata=metadata, summary=summary, samples=samples) + (output / 'results.json').write_text(json.dumps(result, indent=2) + '\n') + return 0 if all(row['eligible'] for row in summary) else 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/scripts/benchmark/test_fixed.py b/scripts/benchmark/test_fixed.py new file mode 100644 index 00000000..f6640ed1 --- /dev/null +++ b/scripts/benchmark/test_fixed.py @@ -0,0 +1,46 @@ +"""Manifest admission must reject stale or ambiguous fixed-work experiments.""" +import json +import tempfile +import unittest +from pathlib import Path +from fixed import load_workloads +from run import digest + + +class ManifestTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.directory = Path(self.temp.name) + self.source = self.directory / 'loop.js' + self.source.write_text('print(42);\n') + self.item = dict(case='loop', path='/missing/loop.js', sha256=digest(self.source), + size=1, expected='42\n') + self.report = self.directory / 'report.json' + + def write(self, items): + self.report.write_text(json.dumps(dict(metadata=dict(workloads=dict(workloads=items))))) + + def test_relocation_preserves_byte_identity(self): + self.write([self.item]) + result = load_workloads(self.report, self.directory, ['loop']) + self.assertEqual(result[0]['path'], str(self.source)) + self.assertEqual(result[0]['expected'], '42\n') + + def test_changed_workload_is_rejected(self): + self.write([self.item]) + self.source.write_text('print(41);\n') + with self.assertRaisesRegex(ValueError, 'bytes changed'): + load_workloads(self.report, self.directory) + + def test_missing_or_repeated_selection_is_rejected(self): + self.write([self.item]) + for cases in [['missing'], ['loop', 'loop']]: + with self.subTest(cases=cases), self.assertRaises(ValueError): + load_workloads(self.report, self.directory, cases) + + def test_ambiguous_or_unsafe_manifest_is_rejected(self): + for items in [[self.item, self.item], [dict(self.item, case='../loop')], []]: + self.write(items) + with self.subTest(items=items), self.assertRaises(ValueError): + load_workloads(self.report, self.directory) diff --git a/scripts/checks/binary_object/canaries/fixture.sh b/scripts/checks/binary_object/canaries/fixture.sh index 9802b2fc..a6796a3d 100644 --- a/scripts/checks/binary_object/canaries/fixture.sh +++ b/scripts/checks/binary_object/canaries/fixture.sh @@ -20,6 +20,10 @@ cp -- "$repository_root/src/engine/value/primitive.rs" "$fixture/src/engine/valu cp -R -- "$repository_root/src/engine/api/context" "$fixture/src/engine/api/context" cp -- "$repository_root/src/engine/code/bytecode_publish.rs" \ "$fixture/src/engine/code/bytecode_publish.rs" +mkdir -p -- "$fixture/src/engine/code/bytecode_publish" +cp -- "$repository_root/src/engine/code/bytecode_publish/verified.rs" \ + "$fixture/src/engine/code/bytecode_publish/verified.rs" +cp -- "$repository_root/src/engine/code/executable.rs" "$fixture/src/engine/code/executable.rs" printf '%s\n' \ 'mod atoms;' \ 'mod code;' \ diff --git a/scripts/checks/binary_object/canaries/ordinary_leaf.sh b/scripts/checks/binary_object/canaries/ordinary_leaf.sh index 07c71ae1..04b44e7f 100644 --- a/scripts/checks/binary_object/canaries/ordinary_leaf.sh +++ b/scripts/checks/binary_object/canaries/ordinary_leaf.sh @@ -8,8 +8,8 @@ expect_rewrite_rejected ordinary-consumer-op-remap ordinary-leaf-consumer-loweri 'OrdinaryLeafBinaryOp::Add => Instruction::Sub,' expect_rewrite_rejected ordinary-consumer-verifier-dead-branch ordinary-leaf-consumer-publication \ src/engine/code/binary_object_publish.rs \ - $' super::bytecode_publish::verify_unlinked_ordinary_leaf(&function)\n .map_err(map_ordinary_leaf_verification_error)?;' \ - $' if false {\n super::bytecode_publish::verify_unlinked_ordinary_leaf(&function)\n .map_err(map_ordinary_leaf_verification_error)?;\n }' + $' let function = super::bytecode_publish::VerifiedFunction::ordinary_leaf(function)\n .map_err(map_ordinary_leaf_verification_error)?;' \ + $' if false {\n let function = super::bytecode_publish::VerifiedFunction::ordinary_leaf(function)\n .map_err(map_ordinary_leaf_verification_error)?;\n }' expect_rewrite_rejected ordinary-consumer-generic-publisher ordinary-leaf-consumer-publication \ src/engine/code/binary_object_publish.rs \ 'self.publish_verified_unlinked_function(realm, function)?' \ @@ -107,3 +107,35 @@ expect_rewrite_rejected scalar-unary-name-widening scalar-unary-operation-shape src/engine/code/binary_object/scalar_script.rs \ ' FunctionUnaryOp::TypeOf => Self::TypeOf,' \ $' FunctionUnaryOp::TypeOf => Self::TypeOf,\n FunctionUnaryOp::Neg => Self::TypeOf,' + +expect_full_rewrite_rejected published-function-skip-verifier published-function-verification \ + src/engine/code/bytecode_publish/verified.rs \ + ' verify_unlinked_ordinary_leaf(&function)?;' \ + ' // skipped by mutation' +expect_full_rewrite_rejected published-function-public-draft published-function-verification \ + src/engine/code/bytecode_publish/verified.rs \ + 'pub(crate) struct VerifiedFunction(UnlinkedFunction);' \ + 'pub(crate) struct VerifiedFunction(pub(crate) UnlinkedFunction);' + +expect_full_rewrite_rejected published-executable-wrong-runtime published-executable-owner \ + src/engine/code/executable.rs \ + ' if !function.belongs_to(self) {' \ + ' if false {' +expect_full_rewrite_rejected published-executable-mutable-layout published-executable-owner \ + src/engine/code/executable.rs \ + ' data: PublishedFunctionData,' \ + ' pub(crate) data: PublishedFunctionData,' + +expect_full_rewrite_rejected published-frame-code-substitution published-frame-owner \ + src/engine/vm/host_bridge.rs \ + ' Ok((self.executable.code.clone(), activation))' \ + ' Ok((Rc::from([]), activation))' + +expect_full_rewrite_rejected published-branch-general-check published-static-target \ + src/engine/vm/protocol.rs \ + ' super::activation::checked_target(target, code_len)' \ + ' Ok(target as usize)' +expect_full_rewrite_rejected published-branch-fixture-check published-static-target \ + src/engine/vm/host_bridge.rs \ + ' return super::activation::checked_target(target, _code_len);' \ + ' return Ok(target as usize);' diff --git a/scripts/checks/binary_object/canaries/stage3i.sh b/scripts/checks/binary_object/canaries/stage3i.sh index c4b20dab..a77893a9 100644 --- a/scripts/checks/binary_object/canaries/stage3i.sh +++ b/scripts/checks/binary_object/canaries/stage3i.sh @@ -298,16 +298,16 @@ expect_full_rewrite_rejected stage3d-raise-bypass \ $' ) -> Result, Error> {\n if matches!(value, Value::Undefined) {\n return Ok(Some(Completion::Throw(value)));\n }\n host.ensure_backtrace(&value)?;\n loop {' expect_full_rewrite_rejected stage3d-execute-inner-post-route-throw-return \ stage3d-throw-critical-route src/engine/vm/mod.rs \ - ' if let Some(completion) = self.execute_hot_instruction(code, instruction, host)? {' \ - $' if matches!(instruction, Instruction::Throw) {\n return Ok(InterpreterExit::Complete(Completion::Return(Value::Undefined)));\n }\n if let Some(completion) = self.execute_hot_instruction(code, instruction, host)? {' + ' let completion = match instruction {' \ + $' if matches!(instruction, Instruction::Throw) {\n return Ok(InterpreterExit::Complete(Completion::Return(Value::Undefined)));\n }\n let completion = match instruction {' expect_full_rewrite_rejected stage3d-execute-hot-entry-throw-return \ stage3d-throw-critical-route src/engine/vm/mod.rs \ $' ) -> Result, Error> {\n match instruction {\n Instruction::Nop => {}' \ $' ) -> Result, Error> {\n if matches!(instruction, Instruction::Throw) {\n return self.pop().map(|value| Some(Completion::Return(value)));\n }\n match instruction {\n Instruction::Nop => {}' expect_full_rewrite_rejected stage3d-execute-published-throw-return \ stage3d-throw-critical-route src/engine/vm/mod.rs \ - $' )\n .execute(code, host)\n }' \ - $' )\n .execute(code, host)\n .map(|completion| match completion {\n Completion::Throw(value) => Completion::Return(value),\n completion => completion,\n })\n }' + $' activation.execute(&code, host)\n }' \ + $' activation.execute(&code, host)\n .map(|completion| match completion {\n Completion::Throw(value) => Completion::Return(value),\n completion => completion,\n })\n }' expect_full_rewrite_rejected stage3d-bytecode-normal-bridge-throw-return \ stage3d-throw-critical-route src/engine/vm/host_bridge.rs \ $' let result = Vm::new().execute_published(input, &mut host);\n active_frame.finish()?;\n result.map_err(RuntimeError::Engine)\n }\n}' \ diff --git a/scripts/checks/binary_object/canaries/translation.sh b/scripts/checks/binary_object/canaries/translation.sh index e4281433..d366c2a8 100644 --- a/scripts/checks/binary_object/canaries/translation.sh +++ b/scripts/checks/binary_object/canaries/translation.sh @@ -155,10 +155,11 @@ expect_full_rewrite_rejected stage3c-call-dispatch-alias-bypass \ stage3c-tail-vm src/engine/vm/mod.rs \ $' ) -> Result, Error> {\n let completion = match instruction {\n Instruction::Import => {' \ $' ) -> Result, Error> {\n use Instruction as I;\n let completion = match instruction {\n I::TailCall(argument_count) if *argument_count == 0 => {\n let _ = self.pop()?;\n return host.call(Value::Undefined, Value::Null, Vec::new()).map(Some);\n }\n Instruction::Import => {' +# Use the PC publication point so new direct-dispatch arms do not stale this probe. expect_full_rewrite_rejected stage3c-execute-inner-tail-intercept \ stage3c-tail-vm src/engine/vm/mod.rs \ - $' if matches!(\n instruction,\n Instruction::Import\n | Instruction::Call(_)' \ - $' use Instruction as I;\n if matches!(instruction, I::TailCall(_) | I::TailCallMethod(_)) {\n return Ok(InterpreterExit::Complete(Completion::Return(Value::Undefined)));\n }\n\n if matches!(\n instruction,\n Instruction::Import\n | Instruction::Call(_)' + ' host.update_active_bytecode_pc(BytecodePc::new(self.pc))?;' \ + $' host.update_active_bytecode_pc(BytecodePc::new(self.pc))?;\n use Instruction as I;\n if matches!(instruction, I::TailCall(_) | I::TailCallMethod(_)) {\n return Ok(InterpreterExit::Complete(Completion::Return(Value::Undefined)));\n }' expect_full_rewrite_rejected stage3c-execute-alias-return-bypass \ stage3c-tail-completion src/engine/vm/mod.rs \ $' ) -> Result {\n loop {\n let raised = match self.execute_inner(code, host) {' \ diff --git a/scripts/checks/binary_object/evidence/coercion.py b/scripts/checks/binary_object/evidence/coercion.py index 004bc273..b67ee43f 100644 --- a/scripts/checks/binary_object/evidence/coercion.py +++ b/scripts/checks/binary_object/evidence/coercion.py @@ -4,8 +4,8 @@ 'src/engine/code/binary_object/function_translate/dto.rs': '1abab8ac751b9695e4d9dabd53da8d0dd2a78bbc7cdb7f06588c22bb6f60405a', 'src/engine/code/binary_object/function_translate/mod.rs': 'ee4d423731977d088c45a0ee2f0fcf642b119f2c2dd4c6f9243d724a761545d7', 'src/engine/code/binary_object/ordinary_leaf.rs': 'b7553332908929126730993a655bbd55acbe34df3ae7acf7415486f0c3fd81a4', - 'src/engine/code/binary_object_publish.rs': '137a5583eaa84f1f28cc3a4d7990f28e992fb2cff931ff05c0b51411733ec5d0', - 'src/engine/heap/runtime/tests.rs': '5dbc650b81f826b551f31a3dd018d1d2318b796d279347c35738d91c8b50d895', + 'src/engine/code/binary_object_publish.rs': 'd7dd67291f5a8e7535ff7e7fb6802aa8348f5186c32235a8411ec79d6c5a0ae7', + 'src/engine/heap/runtime/tests.rs': '36ce3fafb354a8c6049bc098069437ea653eed726cd3748f6e00cf30ccacad96', 'src/engine/code/binary_object/ordinary_leaf/tests.rs': '8a659586b5dfd17d5b3f1863913ddc952fd46fbb080bbf422133939fedb86630', 'src/engine/heap/runtime/tests/accessors.rs': '7e7aadee6f2248192a6c1438029e26e1153f9412e148cf8c672f7e7d97ddc1d4', 'src/engine/heap/runtime/tests/active_frames.rs': '609cbd20f1c0499710c9494e365fabfa94f3e8c19be359d4a721aff39bfde0bd', diff --git a/scripts/checks/binary_object/evidence/runtime_protocols.py b/scripts/checks/binary_object/evidence/runtime_protocols.py index 21168db5..8e699327 100644 --- a/scripts/checks/binary_object/evidence/runtime_protocols.py +++ b/scripts/checks/binary_object/evidence/runtime_protocols.py @@ -15,9 +15,4 @@ 'self.take_call_arguments(*argument_count, 2)?; let function = self.pop()?; let receiver = ' 'self.pop()?; return host.call(function, receiver, arguments).map(Some); }') -CALL_ROUTE = ('if matches!( instruction, Instruction::Import | Instruction::Call(_) | Instruction::TailCall(_) ' - '| Instruction::Eval { .. } | Instruction::CallMethod(_) | Instruction::TailCallMethod(_) | ' - 'Instruction::Construct(_) | Instruction::ConstructSuper(_) | Instruction::InitDerivedConstructor ' - '| Instruction::Apply(_) | Instruction::ApplySuper | Instruction::ApplyEval { .. } ) { if let ' - 'Some(completion) = self.execute_call_instruction(instruction, host)? { return ' - 'Ok(InterpreterExit::Complete(completion)); } continue; }') +CALL_ROUTE = 'Instruction::Import | Instruction::Call(_) | Instruction::TailCall(_) | Instruction::Eval { .. } | Instruction::CallMethod(_) | Instruction::TailCallMethod(_) | Instruction::Construct(_) | Instruction::ConstructSuper(_) | Instruction::InitDerivedConstructor | Instruction::Apply(_) | Instruction::ApplySuper | Instruction::ApplyEval { .. } => { self.execute_call_instruction(instruction, host)? }' diff --git a/scripts/checks/binary_object/rules/publication.py b/scripts/checks/binary_object/rules/publication.py index da4f4101..00dafc7b 100644 --- a/scripts/checks/binary_object/rules/publication.py +++ b/scripts/checks/binary_object/rules/publication.py @@ -8,6 +8,21 @@ def check(ctx): + ctx.require_normalized_code_sha256( + "published-executable-owner", + "Execution snapshots must pair immutable metadata with their owning Runtime root", + ctx.rust_code_only(ctx.read_source("src/engine/code/executable.rs")), + "d1a410c0ca1911d654a48ba91c15a4130e179e8823fe41024b02a5e5078bea07", + ) + # The owning wrapper is the only path from a draft to verified publication. + # Authenticate constructors too: checking a consumer call alone would allow + # the wrapper to stop invoking its role-specific verifier. + ctx.require_normalized_code_sha256( + "published-function-verification", + "VerifiedFunction must own its exact draft and authenticate each publication role", + ctx.rust_code_only(ctx.read_source("src/engine/code/bytecode_publish/verified.rs")), + "c1058806ecf500e426b76862985341a2e1e50d0d9a1127b4724e64fd3946197c", + ) if ctx.consumer_exists: consumer_production_code = ctx.consumer_code.split("#[cfg(test)]", 1)[0] consumer_top_level_item_pattern = re.compile( @@ -217,7 +232,7 @@ def check(ctx): re.compile(r"\bdraft[ \t\n]*\.[ \t\n]*into_parts[ \t\n]*\("), re.compile(r"\bUnlinkedFunction[ \t\n]*::[ \t\n]*new[ \t\n]*\("), re.compile( - r"\bbytecode_publish[ \t\n]*::[ \t\n]*verify_unlinked_ordinary_leaf" + r"\bbytecode_publish[ \t\n]*::[ \t\n]*VerifiedFunction[ \t\n]*::[ \t\n]*ordinary_leaf" r"[ \t\n]*\(" ), re.compile( diff --git a/scripts/checks/binary_object/rules/runtime_protocols.py b/scripts/checks/binary_object/rules/runtime_protocols.py index fbf4afaa..cc462475 100644 --- a/scripts/checks/binary_object/rules/runtime_protocols.py +++ b/scripts/checks/binary_object/rules/runtime_protocols.py @@ -38,6 +38,17 @@ def stage3b_function(relative: str, name: str, diagnostic: str) -> str: return stage3b_items[key] ctx.stage3b_function = stage3b_function + for relative, expected in ( + ("src/engine/vm/protocol.rs", "0a19ef66059453bfa14d9d92a9b617f5d94e8dc1f40044a582dcbfc4db8a8026"), + ("src/engine/vm/host_bridge.rs", "1552130a60bd6192571af5b09a7b9a4b1387acc1dffd2bb9e946546205e11c28"), + ): + ctx.require_normalized_code_sha256( + "published-static-target", + "Static target reuse must retain the general and synthetic host checks", + ctx.stage3b_function(relative, "static_branch_target", "published-static-target"), + expected, + ) + def stage3j_source_function(relative: str, name: str, diagnostic: str) -> str: source = ctx.read_source(relative) code = ctx.rust_code_only(source) @@ -346,7 +357,7 @@ def stage3j_source_function(relative: str, name: str, diagnostic: str) -> str: "stage3c-tail-vm", "the execute_inner prefix through call-family routing must not intercept, alias, or remove tail completion", normalized_execute_inner[:call_route_end], - "e425f4d42ef9a3a7b6552994a6f7da31009e70451f5b48e3312581929a54e54c", + "35d38d833ed12acd0dfd6de284398e2df696ba0abb7f0046ebe47fdfa6c39210", ) capability_relative = "src/engine/code/binary_object/function_translate/capability.rs" @@ -554,7 +565,7 @@ def stage3j_source_function(relative: str, name: str, diagnostic: str) -> str: "read_trusted_ordinary_function_in_realm", "stage3e-read-only-publication", ), - "7bce6b697724b3bf4e6cd3b4747887d7e3a4a3376bf940907650d44ad7261f4e", + "6528a8aefe09efba3d732e33a6de6a6d8daae0de67cb62fe3d5f68b97fe9d745", ) if normalized_stack_effect.count("| Self::Throw => (1, 0),") != 1: @@ -756,14 +767,14 @@ def stage3j_source_function(relative: str, name: str, diagnostic: str) -> str: "stage3d-throw-critical-route", "execute_inner must carry raw48 from fetch through the hot dispatcher without a guarded completion alias", execute_inner_item, - "fa323bad632c685546d3efadbe860a77f540b1066559744ea23c333958036358", + "cc82cb84b962afef73b2141002623e6802c0c047a008eea157c178cfbeb9da3a", ) ctx.require_normalized_code_sha256( "stage3d-throw-critical-route", "execute_hot_instruction must enter its unique match before handling Throw and retain the exact dispatch body", execute_hot_item, - "2fab69bd24de64e6ab0149f4c267b8312e1cebecb6064c4f8c81a44a74156304", + "2b6f0378a1ab5e2a7fec880e88ee7ad700f6cdb9872828d0ed86ab164ec6dde0", ) execute_published_item = ctx.stage3b_function( @@ -774,10 +785,17 @@ def stage3j_source_function(relative: str, name: str, diagnostic: str) -> str: "stage3d-throw-critical-route", "execute_published must return the activation's Completion directly without post-processing Throw", execute_published_item, - "b2743fde8341d22bb2592d3810e10030ecce6f812befe9be150a80ccd982a0a7", + "6704a2e5ab9c5cdd3086ab43544dd900ffa22497bc2d5da9ab2c7842eee6bb01", ) runtime_vm_host_relative = "src/engine/vm/host_bridge.rs" + ctx.require_normalized_code_sha256( + "published-frame-owner", + "Activation code and metadata must come from the same sealed host snapshot", + ctx.stage3b_function(runtime_vm_host_relative, "new_activation", "published-frame-owner"), + "88806b84077ca2ffc8bb4691e5dc30e344f95241ae30a1ead4ed3092df0e7022", + ) + execute_bytecode_callable_item = ctx.stage3b_function( runtime_vm_host_relative, diff --git a/scripts/checks/binary_object/rules/source_ownership.py b/scripts/checks/binary_object/rules/source_ownership.py index b6c6a728..f8d49c25 100644 --- a/scripts/checks/binary_object/rules/source_ownership.py +++ b/scripts/checks/binary_object/rules/source_ownership.py @@ -93,11 +93,11 @@ def check(ctx): + ctx.location(ctx.relative, ctx.source, ctx.match.start()), ) - if ctx.relative not in {ctx.bytecode_publish_relative, ctx.consumer_relative}: + if ctx.relative not in {ctx.bytecode_publish_relative, "src/engine/code/bytecode_publish/verified.rs"}: for ctx.match in re.finditer(r"\bverify_unlinked_ordinary_leaf\b", ctx.code): ctx.fail( "ordinary-leaf-verifier-consumer-set", - "only binary_object_publish.rs may call the dedicated ordinary-leaf verifier; found " + "only the owning VerifiedFunction constructor may call the dedicated ordinary-leaf verifier; found " + ctx.location(ctx.relative, ctx.source, ctx.match.start()), ) diff --git a/src/engine/builtins/eval.rs b/src/engine/builtins/eval.rs index 0c98e2e9..d0326a21 100644 --- a/src/engine/builtins/eval.rs +++ b/src/engine/builtins/eval.rs @@ -15,18 +15,7 @@ impl Runtime { function: UnlinkedFunction, expected: &EvalCompileContext, ) -> Result { - bytecode_publish::verify_unlinked_eval_tree_with_profile_and_arguments( - &function, - expected.kind, - expected.caller_strict, - &expected.bindings, - &expected.caller_profile, - bytecode_publish::EvalPublicationCapabilities { - super_call_allowed: expected.super_call_allowed, - super_allowed: expected.super_allowed, - arguments_forbidden: expected.arguments_forbidden, - }, - )?; + let function = bytecode_publish::VerifiedFunction::eval(function, expected)?; self.publish_verified_unlinked_function(realm, function) } @@ -140,7 +129,7 @@ impl Runtime { })?; let (bindings, caller_profile) = self.direct_eval_root_bindings(realm, &environment)?; let arguments_forbidden = self - .snapshot_function_bytecode(&environment.caller_bytecode)? + .snapshot_function_bytecode(environment.descriptor.owner())? .metadata .arguments_forbidden; let function = match self.compile_eval_in_realm( @@ -164,7 +153,11 @@ impl Runtime { // attaches it to caller VarRefs. Preserve that error/GC ordering by // invoking the host's capture step only after successful compilation. let environment = materialize(environment).map_err(RuntimeError::Engine)?; - if environment.index != environment_index || environment.descriptor != expected_descriptor { + if environment.index != environment_index + || !environment + .descriptor + .same_environment(&expected_descriptor) + { return Err(RuntimeError::Invariant( "materialized eval environment disagrees with its prepared descriptor", )); @@ -215,11 +208,11 @@ impl Runtime { realm: ContextId, environment: &crate::engine::vm::host_bridge::PreparedEvalEnvironment, ) -> Result<(Vec>, EvalCallerProfile), RuntimeError> { - if !environment.caller_bytecode.belongs_to(self) { + if !environment.descriptor.owner().belongs_to(self) { return Err(RuntimeError::WrongRuntime("direct eval caller bytecode")); } let caller_realm = self - .snapshot_function_bytecode(&environment.caller_bytecode)? + .snapshot_function_bytecode(environment.descriptor.owner())? .realm; if caller_realm != realm { return Err(RuntimeError::Invariant( @@ -501,13 +494,14 @@ impl Runtime { // A restricted host must reject newly compiled eval bytecode before // declaration preflight or binding creation can mutate the realm. self.ensure_dynamic_import_bytecode_tree_authorized(function)?; - let PublishedFunctionSnapshot { + let snapshot = self.snapshot_function_bytecode(function)?; + let crate::engine::code::runtime::PublishedFunctionData { closure_variables, metadata, realm: function_realm, .. - } = self.snapshot_function_bytecode(function)?; - if function_realm != realm || metadata.eval_kind != kind { + } = &*snapshot; + if *function_realm != realm || metadata.eval_kind != kind { return Err(RuntimeError::Invariant( "published eval bytecode disagrees with its invocation realm or kind", )); diff --git a/src/engine/builtins/mod.rs b/src/engine/builtins/mod.rs index 0666774f..bca018f6 100644 --- a/src/engine/builtins/mod.rs +++ b/src/engine/builtins/mod.rs @@ -13,7 +13,7 @@ use crate::engine::code::function::metadata::{ ClosureSource, ClosureVariableKind, ClosureVariableName, }; use crate::engine::code::rooted::FunctionBytecodeRef; -use crate::engine::code::runtime::{Compilation, PublishedFunctionSnapshot}; +use crate::engine::code::runtime::Compilation; use crate::engine::compiler::DEFAULT_EVAL_FILENAME; use crate::engine::heap::roots::VarRefRoot; diff --git a/src/engine/code/README.md b/src/engine/code/README.md index 3148bb4f..a493368d 100644 --- a/src/engine/code/README.md +++ b/src/engine/code/README.md @@ -14,6 +14,7 @@ - [bytecode_validation.rs](bytecode_validation.rs):Validate compiler-authored frame, parameter, and eval bytecode layouts before publication.。 - [debug.rs](debug.rs):Typed source locations and bytecode-to-source metadata.。 - [dynamic_import_policy.rs](dynamic_import_policy.rs):动态导入代码授权状态。 +- [executable.rs](executable.rs):发布快照、只读数据投影与常量访问。 - [dynamic_source.rs](dynamic_source.rs):动态函数源码组装。 - [function/](function/README.md):子模块职责与文件说明。 - [function.rs](function.rs):Runtime-independent compilation products.。 @@ -21,3 +22,18 @@ - [module.rs](module.rs):Runtime-independent ECMAScript module drafts.。 - [rooted.rs](rooted.rs):Runtime-rooted immutable function bytecode and compiler drafts.。 - [runtime.rs](runtime.rs):运行时操作或共享所有权入口(见本目录边界)。 + +`executable` 提供不可变的已发布执行快照。快照在 Runtime 身份与 realm +验证后取得字节码 root,共享代码和常量等只读 backing storage,不复制内容。 +读取投影不会调用 JS;只有合成测试可以修改无 root 的 fixture,已发布快照 +在测试中也保持不可变。运行时值的 rooting 和释放仍遵循原 heap 契约。 + +快照构造只增加固定数量的 Rc/root 引用,不遍历或复制指令、常量与绑定数组; +销毁释放这些引用。`constant` 共享下标转换与边界处理,调用者保留明确的 +Value/Function/RegExp 种类 match;属性名字继续使用原有链接 Atom 表。 +错误 Runtime 或 realm 在借出执行视图之前被拒绝。root 的生命周期与不可变性 +反例见 `executable::tests`;发布失败的事务回滚仍由 `runtime` 拥有。 + +`PublishedEvalEnvironment` 通过受检索引共享已发布环境数组,并持有字节码 root。 +准备、编译和物化只复制 Rc/root 引用;同一环境按数组身份和索引核对, +不再深拷贝或比较静态 scopes/bindings。动态名字解析与捕获不在此视图缓存。 diff --git a/src/engine/code/binary_object_publish.rs b/src/engine/code/binary_object_publish.rs index 6c67ac88..b78691b7 100644 --- a/src/engine/code/binary_object_publish.rs +++ b/src/engine/code/binary_object_publish.rs @@ -161,7 +161,7 @@ impl Runtime { Vec::new(), ); - super::bytecode_publish::verify_unlinked_ordinary_leaf(&function) + let function = super::bytecode_publish::VerifiedFunction::ordinary_leaf(function) .map_err(map_ordinary_leaf_verification_error)?; let bytecode = self.publish_verified_unlinked_function(realm, function)?; self.new_bytecode_closure(realm, &bytecode) diff --git a/src/engine/code/bytecode_publish.rs b/src/engine/code/bytecode_publish.rs index 8c1979ee..3387e3a2 100644 --- a/src/engine/code/bytecode_publish.rs +++ b/src/engine/code/bytecode_publish.rs @@ -4,6 +4,8 @@ use crate::engine::code::function::{UnlinkedVariableDefinition, metadata::EvalEn mod module_initializer_flow; mod private_elements; +mod verified; +pub(crate) use verified::VerifiedFunction; use crate::engine::api::error::Error; use crate::engine::api::runtime_error::RuntimeError; diff --git a/src/engine/code/bytecode_publish/README.md b/src/engine/code/bytecode_publish/README.md index ec457621..2a41ae3b 100644 --- a/src/engine/code/bytecode_publish/README.md +++ b/src/engine/code/bytecode_publish/README.md @@ -8,3 +8,11 @@ - [module_initializer_flow.rs](module_initializer_flow.rs):Context-sensitive module-initializer flow validation.。 - [private_elements.rs](private_elements.rs):Publication-time authentication for class-private bytecode.。 + +## 验证结果的所有权 + +`VerifiedFunction` 拥有通过角色验证的原始草稿,只能由 Script、受限 +ordinary leaf、eval 或 module 构造入口产生。发布消费该值;验证和发布之间 +不暴露可变草稿。模块 parts 的类型参数仅表示 function 从草稿变为验证结果, +其余表保持与该函数同一次验证的内容。该结果不拥有运行时 roots;后续发布的 +名称链接、heap 保留和失败回滚仍由 runtime 发布事务负责。 diff --git a/src/engine/code/bytecode_publish/verified.rs b/src/engine/code/bytecode_publish/verified.rs new file mode 100644 index 00000000..2d6c75ee --- /dev/null +++ b/src/engine/code/bytecode_publish/verified.rs @@ -0,0 +1,77 @@ +//! Owned verification result. The draft cannot change between authentication +//! and publication, and callers cannot substitute a different function. + +use super::*; +use crate::engine::code::module::UnlinkedModuleParts; +use crate::engine::compiler::EvalCompileContext; + +pub(crate) struct VerifiedFunction(UnlinkedFunction); + +impl VerifiedFunction { + pub(crate) fn script(function: UnlinkedFunction) -> Result { + verify_unlinked_tree(&function)?; + Ok(Self(function)) + } + + pub(in crate::engine::code) fn ordinary_leaf( + function: UnlinkedFunction, + ) -> Result { + verify_unlinked_ordinary_leaf(&function)?; + Ok(Self(function)) + } + + pub(crate) fn eval( + function: UnlinkedFunction, + expected: &EvalCompileContext, + ) -> Result { + verify_unlinked_eval_tree_with_profile_and_arguments( + &function, + expected.kind, + expected.caller_strict, + &expected.bindings, + &expected.caller_profile, + EvalPublicationCapabilities { + super_call_allowed: expected.super_call_allowed, + super_allowed: expected.super_allowed, + arguments_forbidden: expected.arguments_forbidden, + }, + )?; + Ok(Self(function)) + } + + /// Keep module tables paired with the exact function they authenticated. + /// The generic parts carrier changes only the function's ownership state. + pub(crate) fn module( + module: UnlinkedModule, + ) -> Result, RuntimeError> { + verify_unlinked_module_tree(&module)?; + let UnlinkedModuleParts { + name, + function, + has_top_level_await, + declaration_order, + link_initializers, + import_collisions, + requested_modules, + imports, + exports, + star_exports, + } = module.into_parts(); + Ok(UnlinkedModuleParts { + name, + function: Self(function), + has_top_level_await, + declaration_order, + link_initializers, + import_collisions, + requested_modules, + imports, + exports, + star_exports, + }) + } + + pub(in crate::engine::code) fn into_function(self) -> UnlinkedFunction { + self.0 + } +} diff --git a/src/engine/code/executable.rs b/src/engine/code/executable.rs new file mode 100644 index 00000000..08eb259d --- /dev/null +++ b/src/engine/code/executable.rs @@ -0,0 +1,249 @@ +//! Immutable execution projection. Only a runtime snapshot can pair this +//! layout with its owning bytecode root. Deref exposes shared fields for +//! readers, never mutable metadata or a constructor accepting arbitrary parts. +use crate::engine::api::runtime::Runtime; +use crate::engine::api::runtime_error::RuntimeError; +use crate::engine::atom::Atom; +use crate::engine::code::function::metadata::{ + ClosureVariable, EvalEnvironment, FunctionMetadata, VariableDefinition, +}; +use crate::engine::code::rooted::FunctionBytecodeRef; +use crate::engine::heap::{BytecodeConstant, ContextId}; +use std::rc::Rc; + +/// A rooted, immutable eval descriptor selected from its publisher's array. +/// Cloning this view shares the array; it never copies scopes or bindings. +#[derive(Clone)] +pub(crate) struct PublishedEvalEnvironment { + owner: FunctionBytecodeRef, + environments: Rc<[EvalEnvironment]>, + index: usize, +} + +impl PublishedEvalEnvironment { + pub(crate) fn same_environment(&self, other: &Self) -> bool { + self.index == other.index && Rc::ptr_eq(&self.environments, &other.environments) + } + + pub(crate) fn owner(&self) -> &FunctionBytecodeRef { + &self.owner + } +} + +impl std::ops::Deref for PublishedEvalEnvironment { + type Target = EvalEnvironment; + + fn deref(&self) -> &Self::Target { + &self.environments[self.index] + } +} + +pub(crate) struct PublishedFunctionSnapshot { + root: Option, + data: PublishedFunctionData, +} + +impl std::ops::Deref for PublishedFunctionSnapshot { + type Target = PublishedFunctionData; + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl PublishedFunctionSnapshot { + /// One checked projection for all constant consumers. The opcode still + /// chooses the kind-specific operation; this view owns no extra roots. + #[inline] + pub(crate) fn constant(&self, index: u32) -> Option<&BytecodeConstant> { + usize::try_from(index) + .ok() + .and_then(|index| self.constants.get(index)) + } + + pub(crate) fn eval_environment(&self, index: u16) -> Option { + let index = usize::from(index); + self.eval_environments.get(index)?; + Some(PublishedEvalEnvironment { + owner: self.root.as_ref()?.clone(), + environments: self.eval_environments.clone(), + index, + }) + } + + pub(crate) fn root(&self) -> Option<&FunctionBytecodeRef> { + self.root.as_ref() + } + + #[cfg(test)] + pub(crate) fn empty_for_test(realm: ContextId) -> Self { + Self { + root: None, + data: PublishedFunctionData { + code: Rc::from([]), + constants: Rc::from([]), + property_key_atoms: None, + argument_definitions: Rc::from([]), + local_definitions: Rc::from([]), + closure_variables: Rc::from([]), + eval_environments: Rc::from([]), + arg_eval_variable_object_local: None, + metadata: FunctionMetadata::default(), + realm, + }, + } + } +} + +// Synthetic host fixtures exercise rejected internal operations. They never +// provide a production constructor or a mutable view in non-test builds. +#[cfg(test)] +impl std::ops::DerefMut for PublishedFunctionSnapshot { + fn deref_mut(&mut self) -> &mut Self::Target { + assert!( + self.root.is_none(), + "published snapshots remain immutable in tests" + ); + &mut self.data + } +} + +pub(crate) struct PublishedFunctionData { + pub(crate) code: Rc<[crate::engine::code::bytecode::Instruction]>, + pub(crate) constants: Rc<[BytecodeConstant]>, + pub(crate) property_key_atoms: Option>, + pub(crate) argument_definitions: Rc<[VariableDefinition]>, + pub(crate) local_definitions: Rc<[VariableDefinition]>, + pub(crate) closure_variables: Rc<[ClosureVariable]>, + pub(crate) eval_environments: Rc<[EvalEnvironment]>, + /// Parameter-scope variable-object slot, carried separately from the + /// body `` slot in `FunctionMetadata`. + pub(crate) arg_eval_variable_object_local: Option, + pub(crate) metadata: FunctionMetadata, + pub(crate) realm: ContextId, +} + +impl Runtime { + pub(crate) fn snapshot_function_bytecode( + &self, + function: &FunctionBytecodeRef, + ) -> Result { + let _operation = self.operation(); + if !function.belongs_to(self) { + return Err(RuntimeError::WrongRuntime("function bytecode")); + } + let root = function.clone(); + let state = self.0.state.borrow(); + let bytecode = state.heap.function_bytecode(function.bytecode_id())?; + // The realm is a strong edge of the bytecode node. Validating it here + // makes a corrupt realm edge fail before entering a VM frame. + state.heap.context(bytecode.realm)?; + Ok(PublishedFunctionSnapshot { + root: Some(root), + data: PublishedFunctionData { + code: bytecode.code.clone(), + constants: bytecode.constants.clone(), + property_key_atoms: bytecode.property_key_atoms.clone(), + argument_definitions: bytecode.argument_definitions.clone(), + local_definitions: bytecode.local_definitions.clone(), + closure_variables: bytecode.closure_variables.clone(), + eval_environments: bytecode.eval_environments.clone(), + arg_eval_variable_object_local: bytecode + .parameter_environment + .as_ref() + .and_then(|layout| layout.arg_eval_variable_object_local), + metadata: bytecode.metadata, + realm: bytecode.realm, + }, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::engine::code::bytecode::Instruction; + use crate::engine::code::function::UnlinkedFunction; + + fn publish(runtime: &Runtime, realm: ContextId) -> FunctionBytecodeRef { + runtime + .publish_unlinked_function( + realm, + UnlinkedFunction::fixture( + vec![Instruction::PushI32(42), Instruction::Return], + vec![], + FunctionMetadata { + max_stack: 1, + ..FunctionMetadata::default() + }, + ), + ) + .unwrap() + } + + #[test] + fn snapshot_retains_its_owner_and_rejects_another_runtime() { + let runtime = Runtime::new(); + let context = runtime.new_context(); + let function = publish(&runtime, context.realm); + let other = Runtime::new(); + assert!(matches!( + other.snapshot_function_bytecode(&function), + Err(RuntimeError::WrongRuntime("function bytecode")) + )); + let snapshot = runtime.snapshot_function_bytecode(&function).unwrap(); + let id = function.bytecode_id(); + drop(function); + assert_eq!(snapshot.root().unwrap().bytecode_id(), id); + assert!(matches!( + snapshot.code.as_ref(), + [Instruction::PushI32(42), Instruction::Return] + )); + assert!(runtime.0.state.borrow().heap.function_bytecode(id).is_ok()); + } + + #[test] + fn eval_view_shares_storage_but_authenticates_the_selected_environment() { + use crate::engine::code::function::metadata::EvalVariableEnvironment; + let runtime = Runtime::new(); + let context = runtime.new_context(); + let owner = publish(&runtime, context.realm); + let id = owner.bytecode_id(); + let environment = EvalEnvironment { + scopes: Box::new([]), + variable_environment: EvalVariableEnvironment::Global, + caller_strict: false, + super_call_allowed: false, + super_allowed: false, + }; + let view = PublishedEvalEnvironment { + owner, + environments: Rc::from([environment.clone(), environment.clone()]), + index: 0, + }; + let shared = view.clone(); + assert!(view.same_environment(&shared)); + let mut another_index = view.clone(); + another_index.index = 1; + assert!(!view.same_environment(&another_index)); + let mut another_owner = view.clone(); + another_owner.environments = Rc::from([environment]); + assert!(!view.same_environment(&another_owner)); + drop(view); + drop(another_index); + drop(another_owner); + assert!(runtime.0.state.borrow().heap.function_bytecode(id).is_ok()); + assert!(!shared.caller_strict); + drop(shared); + assert!(runtime.0.state.borrow().heap.function_bytecode(id).is_err()); + } + + #[test] + #[should_panic(expected = "published snapshots remain immutable in tests")] + fn synthetic_fixture_mutation_cannot_change_published_code() { + let runtime = Runtime::new(); + let context = runtime.new_context(); + let function = publish(&runtime, context.realm); + let mut snapshot = runtime.snapshot_function_bytecode(&function).unwrap(); + snapshot.code = Rc::from([]); + } +} diff --git a/src/engine/code/mod.rs b/src/engine/code/mod.rs index 5b195477..bb230892 100644 --- a/src/engine/code/mod.rs +++ b/src/engine/code/mod.rs @@ -17,3 +17,5 @@ pub(crate) mod dynamic_import_policy; pub(crate) mod runtime; pub(crate) mod dynamic_source; + +mod executable; diff --git a/src/engine/code/module.rs b/src/engine/code/module.rs index 1c1e5804..7a77196d 100644 --- a/src/engine/code/module.rs +++ b/src/engine/code/module.rs @@ -188,9 +188,9 @@ pub struct UnlinkedModuleTables { } /// Owned pieces crossing the one-way module publication boundary. -pub struct UnlinkedModuleParts { +pub struct UnlinkedModuleParts { pub name: JsString, - pub function: UnlinkedFunction, + pub function: F, pub has_top_level_await: bool, pub declaration_order: Box<[u16]>, pub link_initializers: Box<[ModuleLinkInitializer]>, diff --git a/src/engine/code/runtime.rs b/src/engine/code/runtime.rs index d4d18d7c..8320410f 100644 --- a/src/engine/code/runtime.rs +++ b/src/engine/code/runtime.rs @@ -1,7 +1,9 @@ +pub(crate) use super::executable::{ + PublishedEvalEnvironment, PublishedFunctionData, PublishedFunctionSnapshot, +}; use crate::engine::api::error::{Error, ErrorKind, NativeErrorKind}; use crate::engine::api::runtime::Runtime; use crate::engine::api::runtime_error::RuntimeError; -use crate::engine::atom::Atom; use crate::engine::code::bytecode_publish; use crate::engine::code::debug::DebugInfoMode; use crate::source::QuickJsSourceLocator; @@ -35,16 +37,16 @@ impl Runtime { realm: ContextId, function: UnlinkedFunction, ) -> Result { - bytecode_publish::verify_unlinked_tree(&function)?; + let function = bytecode_publish::VerifiedFunction::script(function)?; self.publish_verified_unlinked_function(realm, function) } pub(crate) fn publish_verified_unlinked_function( &self, realm: ContextId, - function: UnlinkedFunction, + function: bytecode_publish::VerifiedFunction, ) -> Result { - let flat_functions = bytecode_publish::flatten_unlinked_tree(function)?; + let flat_functions = bytecode_publish::flatten_unlinked_tree(function.into_function())?; #[cfg(feature = "test262-host")] if !self.0.dynamic_import_bytecode_allowed.get() && flat_functions.iter().any(|function| { @@ -352,38 +354,6 @@ impl Runtime { )) } - pub(crate) fn snapshot_function_bytecode( - &self, - function: &FunctionBytecodeRef, - ) -> Result { - let _operation = self.operation(); - if !function.belongs_to(self) { - return Err(RuntimeError::WrongRuntime("function bytecode")); - } - let root = function.clone(); - let state = self.0.state.borrow(); - let bytecode = state.heap.function_bytecode(function.bytecode_id())?; - // The realm is a strong edge of the bytecode node. Validating it here - // makes a corrupt realm edge fail before entering a VM frame. - state.heap.context(bytecode.realm)?; - Ok(PublishedFunctionSnapshot { - root, - code: bytecode.code.clone(), - constants: bytecode.constants.clone(), - property_key_atoms: bytecode.property_key_atoms.clone(), - argument_definitions: bytecode.argument_definitions.clone(), - local_definitions: bytecode.local_definitions.clone(), - closure_variables: bytecode.closure_variables.clone(), - eval_environments: bytecode.eval_environments.clone(), - arg_eval_variable_object_local: bytecode - .parameter_environment - .as_ref() - .and_then(|layout| layout.arg_eval_variable_object_local), - metadata: bytecode.metadata, - realm: bytecode.realm, - }) - } - #[cfg(test)] pub fn test_function_debug_location( &self, @@ -522,27 +492,6 @@ pub(crate) enum Compilation { Throw(Value), } -/// Immutable VM inputs detached from the runtime `RefCell` borrow. -/// -/// `constants` contains raw heap identities, so the owning bytecode root is -/// part of the snapshot. The raw constant pool therefore cannot outlive the GC -/// node whose edges keep those identities valid. -pub(crate) struct PublishedFunctionSnapshot { - pub(crate) root: FunctionBytecodeRef, - pub(crate) code: Rc<[crate::engine::code::bytecode::Instruction]>, - pub(crate) constants: Rc<[BytecodeConstant]>, - pub(crate) property_key_atoms: Option>, - pub(crate) argument_definitions: Rc<[VariableDefinition]>, - pub(crate) local_definitions: Rc<[VariableDefinition]>, - pub(crate) closure_variables: Rc<[ClosureVariable]>, - pub(crate) eval_environments: Rc<[EvalEnvironment]>, - /// Parameter-scope variable-object slot, carried separately from the - /// body `` slot in `FunctionMetadata`. - pub(crate) arg_eval_variable_object_local: Option, - pub(crate) metadata: FunctionMetadata, - pub(crate) realm: ContextId, -} - pub(crate) enum FlatConstant { Value(RawValue), AtomString(JsString), diff --git a/src/engine/heap/runtime/static_property_key_tests.rs b/src/engine/heap/runtime/static_property_key_tests.rs index 51055637..2dd31032 100644 --- a/src/engine/heap/runtime/static_property_key_tests.rs +++ b/src/engine/heap/runtime/static_property_key_tests.rs @@ -125,11 +125,13 @@ fn static_property_keys_are_runtime_local_and_failed_publication_rolls_back() { .snapshot_function_bytecode(&a) .unwrap() .property_key_atoms + .as_ref() .unwrap()[0]; let b_atom = second .snapshot_function_bytecode(&b) .unwrap() .property_key_atoms + .as_ref() .unwrap()[0]; assert_ne!(a_atom, b_atom); assert!(first_context.execute(&b).is_err()); @@ -171,6 +173,7 @@ fn static_property_keys_preserve_numeric_spellings_and_exact_utf16() { .snapshot_function_bytecode(&bytecode) .unwrap() .property_key_atoms + .as_ref() .unwrap()[0]; assert_eq!( runtime.0.state.borrow().atoms.to_js_string(atom).unwrap(), @@ -184,6 +187,7 @@ fn static_property_keys_preserve_numeric_spellings_and_exact_utf16() { .snapshot_function_bytecode(&plain) .unwrap() .property_key_atoms + .as_ref() .is_none() ); } diff --git a/src/engine/heap/runtime/tests.rs b/src/engine/heap/runtime/tests.rs index 9c6b483e..ff000086 100644 --- a/src/engine/heap/runtime/tests.rs +++ b/src/engine/heap/runtime/tests.rs @@ -696,13 +696,13 @@ fn push_named_eval_active_frame( compile_context, ) .unwrap(); - crate::engine::code::bytecode_publish::verify_unlinked_eval_tree( - &function, - kind, - false, - &[], - false, - false, + let function = crate::engine::code::bytecode_publish::VerifiedFunction::eval( + function, + &match kind { + EvalKind::Direct => EvalCompileContext::direct(false, Vec::new()), + EvalKind::Indirect => EvalCompileContext::indirect(), + EvalKind::None => unreachable!(), + }, ) .unwrap(); let bytecode = runtime @@ -723,8 +723,9 @@ fn push_named_module_active_frame( ) -> super::ActiveFrameGuard { let module = compile_unlinked_module_with_filename("", filename, runtime.debug_info_mode()).unwrap(); - crate::engine::code::bytecode_publish::verify_unlinked_module_tree(&module).unwrap(); - let function = module.into_parts().function; + let function = crate::engine::code::bytecode_publish::VerifiedFunction::module(module) + .unwrap() + .function; let bytecode = runtime .publish_verified_unlinked_function(context.realm, function) .unwrap(); diff --git a/src/engine/modules/mod.rs b/src/engine/modules/mod.rs index 21a682db..27f6901a 100644 --- a/src/engine/modules/mod.rs +++ b/src/engine/modules/mod.rs @@ -1896,7 +1896,7 @@ impl Runtime { module: UnlinkedModule, import_meta: Option<&ObjectRef>, ) -> Result { - bytecode_publish::verify_unlinked_module_tree(&module)?; + let parts = bytecode_publish::VerifiedFunction::module(module)?; let parsing_record = self.module_record(parsing_module)?; if parsing_module.cache != realm @@ -1907,7 +1907,6 @@ impl Runtime { )); } - let parts = module.into_parts(); let function = self.publish_verified_unlinked_function(realm, parts.function)?; let exports = parts .exports diff --git a/src/engine/vm/README.md b/src/engine/vm/README.md index d7518d23..fe7e017e 100644 --- a/src/engine/vm/README.md +++ b/src/engine/vm/README.md @@ -26,6 +26,40 @@ - [numeric.rs](numeric.rs):共享原始值/数值转换入口、数值类型及比较辅助函数。 - [numeric_coercion_tests.rs](numeric_coercion_tests.rs):原始值直通及对象转换顺序回归测试。 - [numeric_execution.rs](numeric_execution.rs):数值指令执行。 +- [published_execution_tests.rs](published_execution_tests.rs):真实发布入口的绑定、eval、挂起与非法访问模式回归。 - [protocol.rs](protocol.rs):执行请求、宿主协议和 VM 入口。 - [tests.rs](tests.rs):模块回归测试。 - [unwind.rs](unwind.rs):异常和迭代器展开。 + +## 已发布执行入口 + +RuntimeVmHost 持有 code 模块的完整只读执行快照,而非独立常量、绑定定义 +和代码片段。`CallInput` 只携带本次调用的动态输入;`new_activation` 从 host +自身的快照取得代码与布局。普通与可挂起驱动共享构造契约,保持不同返回类型, +避免扩大普通递归栈帧。恢复状态由 `decode_vm_activation` 验证后封装为字段 +私有的 `RootedVmActivation`,只通过其 run 消费;动态恢复检查不能由发布验证 +替代。合成 host fixture 不能进入生产 published 执行入口。 + +普通 local/VarRef 的访问模式由发布验证保证;生产读取直接定位动态槽, +checked 读取仍在每次访问时检查 TDZ,只在构造异常时查询名字等静态元数据。 +参数与局部变量共享 `read_frame_binding`/`write_frame_binding`,Captured 仍走 +实际 VarRef 的读写。无 root 的合成测试保留原内部错误检查,不作为生产入口。 +新增绑定指令时必须同步发布验证、动态状态测试与外部字节码反例。 + +新帧仍按参数/局部变量数量分配动态槽;没有引入帧池或每指令派生表。 +`execute_inner` 直接执行常用绑定、字面量、简单栈操作和分支; +复杂语义处理器继续负责 JS 转换和用户回调,以限制递归帧大小。 +不能将跨回调的可变 Runtime 借用移入执行快照。active-frame guard 和挂起 +编码继续拥有异常/返回时的清理责任。新增执行类别时同步原处理器、分派与 +架构变异测试,不能只改源码指纹。checked 写入与初始化复用发布的模式保证,仍检查实际 TDZ、const cell +和生命周期。eval 复用发布拓扑,在编译前验证实际槽与 closure cell; +异常栈、恢复和特殊初始化协议仍由原边界检查。 + +捕获复用由 `reuse_frame_capture` 验证实际 cell 与 descriptor 的视图关系并保留 root; +已 Captured 的局部槽不再进入完整建单元分派,首次捕获仍由父定义提供 canonical metadata。 +`static_branch_target` 只消费 IfTrue/IfFalse/Goto 的已验证立即数;生产 code/host +必须来自同一 snapshot。通用 host 和无 root fixture 继续验界,异常/恢复地址不走此入口。 + +`pop_pair` 用一次长度门槛证明两个直接 Vec::pop;没有用户代码或回调能在 +检查和移动间改变栈。单元素失败仍先消费右值、构造错误,再释放右值。 +`clone_at_depth` 用尾部索引的 wrapping_sub 加一次 get 验界;过大深度会得到大于栈长的索引,仍返回原越界错误。正常取出保留 root 克隆。 diff --git a/src/engine/vm/async_function.rs b/src/engine/vm/async_function.rs index ca22d46d..aeb3d6d5 100644 --- a/src/engine/vm/async_function.rs +++ b/src/engine/vm/async_function.rs @@ -130,7 +130,7 @@ impl Runtime { &self, caller_realm: ContextId, mut host: RuntimeVmHost, - input: CallInput<'_>, + input: CallInput, active_frame: ActiveFrameGuard, ) -> Result { let capability = self.new_default_promise_capability(caller_realm)?; diff --git a/src/engine/vm/async_generator.rs b/src/engine/vm/async_generator.rs index a8afdf1a..cb96f0f9 100644 --- a/src/engine/vm/async_generator.rs +++ b/src/engine/vm/async_generator.rs @@ -180,7 +180,7 @@ impl Runtime { caller_realm: ContextId, callable: &CallableRef, mut host: RuntimeVmHost, - input: CallInput<'_>, + input: CallInput, active_frame: ActiveFrameGuard, ) -> Result { let result = Vm::new().start_published(input, &mut host); diff --git a/src/engine/vm/dispatch.rs b/src/engine/vm/dispatch.rs index 2bc3642c..8f14d37d 100644 --- a/src/engine/vm/dispatch.rs +++ b/src/engine/vm/dispatch.rs @@ -823,6 +823,35 @@ impl VmActivation { ) -> Result, Error> { match instruction { Instruction::Nop => {} + Instruction::PushI32(_) + | Instruction::Undefined + | Instruction::Null + | Instruction::PushFalse + | Instruction::PushTrue + | Instruction::GetLocal(_) + | Instruction::PutLocal(_) + | Instruction::SetLocal(_) + | Instruction::GetLocalCheck(_) + | Instruction::PutLocalCheck(_) + | Instruction::SetLocalCheck(_) + | Instruction::GetArg(_) + | Instruction::PutArg(_) + | Instruction::SetArg(_) + | Instruction::GetVarRef(_) + | Instruction::PutVarRef(_) + | Instruction::SetVarRef(_) + | Instruction::GetVarRefCheck(_) + | Instruction::PutVarRefCheck(_) + | Instruction::Drop + | Instruction::Dup + | Instruction::Nip + | Instruction::Swap + | Instruction::IfFalse(_) + | Instruction::IfTrue(_) + | Instruction::Goto(_) => { + unreachable!("frame-local dispatch was bypassed") + } + Instruction::InitialYield | Instruction::Yield | Instruction::YieldStar @@ -839,7 +868,7 @@ impl VmActivation { | Instruction::IteratorGetValueDone => { unreachable!("yield-star iterator dispatch was bypassed") } - Instruction::PushI32(value) => self.stack.push(Value::Int(*value)), + Instruction::PushAtomValueIndex(value) => self.stack.push(Value::String( crate::engine::value::JsString::from_fresh_decimal_u32(*value), )), @@ -990,10 +1019,7 @@ impl VmActivation { "unsupported reference to 'super'", )); } - Instruction::Undefined => self.stack.push(Value::Undefined), - Instruction::Null => self.stack.push(Value::Null), - Instruction::PushFalse => self.stack.push(Value::Bool(false)), - Instruction::PushTrue => self.stack.push(Value::Bool(true)), + Instruction::PushThis => { let value = self.normalized_this(host)?; self.stack.push(value); @@ -1008,27 +1034,11 @@ impl VmActivation { self.stack.push(host.home_object()?); } Instruction::PushNewTarget => self.stack.push(self.new_target.clone()), - Instruction::GetLocal(index) => { - self.stack.push(host.get_local(*index)?); - } - Instruction::PutLocal(index) => { - let value = self.pop()?; - host.put_local(*index, value)?; - } - Instruction::SetLocal(index) => { - let value = self - .stack - .last() - .cloned() - .ok_or_else(|| Error::internal("set local on an empty stack"))?; - host.put_local(*index, value)?; - } + Instruction::SetLocalUninitialized(index) => { host.set_local_uninitialized(*index)?; } - Instruction::GetLocalCheck(index) => { - self.stack.push(host.get_local_checked(*index)?); - } + Instruction::InitializeLocal(index) => { let value = self.pop()?; host.initialize_local(*index, value)?; @@ -1037,55 +1047,7 @@ impl VmActivation { let value = self.pop()?; host.initialize_derived_local(*index, value)?; } - Instruction::PutLocalCheck(index) => { - let value = self.pop()?; - host.put_local_checked(*index, value)?; - } - Instruction::SetLocalCheck(index) => { - let value = self - .stack - .last() - .cloned() - .ok_or_else(|| Error::internal("set lexical local on an empty stack"))?; - host.put_local_checked(*index, value)?; - } - Instruction::GetArg(index) => { - self.stack.push(host.get_argument(*index)?); - } - Instruction::PutArg(index) => { - let value = self.pop()?; - host.put_argument(*index, value)?; - } - Instruction::SetArg(index) => { - let value = self - .stack - .last() - .cloned() - .ok_or_else(|| Error::internal("set argument on an empty stack"))?; - host.put_argument(*index, value)?; - } - Instruction::GetVarRef(index) => { - self.stack.push(host.get_var_ref(*index)?); - } - Instruction::PutVarRef(index) => { - let value = self.pop()?; - host.put_var_ref(*index, value)?; - } - Instruction::SetVarRef(index) => { - let value = self - .stack - .last() - .cloned() - .ok_or_else(|| Error::internal("set VarRef on an empty stack"))?; - host.put_var_ref(*index, value)?; - } - Instruction::GetVarRefCheck(index) => { - self.stack.push(host.get_var_ref_checked(*index)?); - } - Instruction::PutVarRefCheck(index) => { - let value = self.pop()?; - host.put_var_ref_checked(*index, value)?; - } + Instruction::InitializeVarRef(index) => { let value = self.pop()?; host.initialize_var_ref(*index, value)?; @@ -1432,26 +1394,7 @@ impl VmActivation { Completion::Throw(value) => return Ok(Some(Completion::Throw(value))), } } - Instruction::Drop => { - self.pop()?; - } - Instruction::Nip => { - let (_, value) = self.pop_pair()?; - self.stack.push(value); - } - Instruction::Swap => { - let (left, right) = self.pop_pair()?; - self.stack.push(right); - self.stack.push(left); - } - Instruction::Dup => { - let value = self - .stack - .last() - .cloned() - .ok_or_else(|| Error::internal("dup on an empty stack"))?; - self.stack.push(value); - } + Instruction::Dup1 => { let index = self .stack @@ -1518,21 +1461,7 @@ impl VmActivation { Completion::Throw(value) => return Ok(Some(Completion::Throw(value))), } } - Instruction::IfFalse(target) => { - let value = self.pop()?; - if !host.to_boolean(&value)? { - self.pc = checked_target(*target, code.len())?; - } - } - Instruction::IfTrue(target) => { - let value = self.pop()?; - if host.to_boolean(&value)? { - self.pc = checked_target(*target, code.len())?; - } - } - Instruction::Goto(target) => { - self.pc = checked_target(*target, code.len())?; - } + Instruction::Catch(target) => { self.regions.push(VmUnwindRegion::Catch { target: checked_target(*target, code.len())?, diff --git a/src/engine/vm/frame_execution.rs b/src/engine/vm/frame_execution.rs index a1d34190..2c00ab83 100644 --- a/src/engine/vm/frame_execution.rs +++ b/src/engine/vm/frame_execution.rs @@ -145,128 +145,250 @@ impl VmActivation { .checked_add(1) .ok_or_else(|| Error::internal("program counter overflow"))?; - let suspension = match instruction { - Instruction::InitialYield => Some(VmSuspendKind::Initial), - Instruction::Yield => Some(VmSuspendKind::Yield), - Instruction::YieldStar => Some(VmSuspendKind::YieldStar), - Instruction::AsyncYieldStar => Some(VmSuspendKind::AsyncYieldStar), - Instruction::Await => Some(VmSuspendKind::Await), - _ => None, - }; - if let Some(kind) = suspension { - return Ok(InterpreterExit::Suspend(kind)); - } - - if matches!( - instruction, + // Frame-local operations finish here without a second opcode match. + // Larger semantic handlers remain separate to bound recursive + // native frames. Every route publishes PC before executing. + let completion = match instruction { + Instruction::PushI32(value) => { + self.stack.push(Value::Int(*value)); + continue; + } + Instruction::Undefined => { + self.stack.push(Value::Undefined); + continue; + } + Instruction::Null => { + self.stack.push(Value::Null); + continue; + } + Instruction::PushFalse => { + self.stack.push(Value::Bool(false)); + continue; + } + Instruction::PushTrue => { + self.stack.push(Value::Bool(true)); + continue; + } + Instruction::GetLocal(index) => { + self.stack.push(host.get_local(*index)?); + continue; + } + Instruction::PutLocal(index) => { + let value = self.pop()?; + host.put_local(*index, value)?; + continue; + } + Instruction::SetLocal(index) => { + let value = self + .stack + .last() + .cloned() + .ok_or_else(|| Error::internal("set local on an empty stack"))?; + host.put_local(*index, value)?; + continue; + } + Instruction::GetLocalCheck(index) => { + self.stack.push(host.get_local_checked(*index)?); + continue; + } + Instruction::PutLocalCheck(index) => { + let value = self.pop()?; + host.put_local_checked(*index, value)?; + continue; + } + Instruction::SetLocalCheck(index) => { + let value = + self.stack.last().cloned().ok_or_else(|| { + Error::internal("set lexical local on an empty stack") + })?; + host.put_local_checked(*index, value)?; + continue; + } + Instruction::GetArg(index) => { + self.stack.push(host.get_argument(*index)?); + continue; + } + Instruction::PutArg(index) => { + let value = self.pop()?; + host.put_argument(*index, value)?; + continue; + } + Instruction::SetArg(index) => { + let value = self + .stack + .last() + .cloned() + .ok_or_else(|| Error::internal("set argument on an empty stack"))?; + host.put_argument(*index, value)?; + continue; + } + Instruction::GetVarRef(index) => { + self.stack.push(host.get_var_ref(*index)?); + continue; + } + Instruction::PutVarRef(index) => { + let value = self.pop()?; + host.put_var_ref(*index, value)?; + continue; + } + Instruction::SetVarRef(index) => { + let value = self + .stack + .last() + .cloned() + .ok_or_else(|| Error::internal("set VarRef on an empty stack"))?; + host.put_var_ref(*index, value)?; + continue; + } + Instruction::GetVarRefCheck(index) => { + self.stack.push(host.get_var_ref_checked(*index)?); + continue; + } + Instruction::PutVarRefCheck(index) => { + let value = self.pop()?; + host.put_var_ref_checked(*index, value)?; + continue; + } + Instruction::Drop => { + self.pop()?; + continue; + } + Instruction::Dup => { + let value = self + .stack + .last() + .cloned() + .ok_or_else(|| Error::internal("dup on an empty stack"))?; + self.stack.push(value); + continue; + } + Instruction::Nip => { + let (_, value) = self.pop_pair()?; + self.stack.push(value); + continue; + } + Instruction::Swap => { + let (left, right) = self.pop_pair()?; + self.stack.push(right); + self.stack.push(left); + continue; + } + Instruction::IfFalse(target) => { + let value = self.pop()?; + if !host.to_boolean(&value)? { + self.pc = host.static_branch_target(*target, code.len())?; + } + continue; + } + Instruction::IfTrue(target) => { + let value = self.pop()?; + if host.to_boolean(&value)? { + self.pc = host.static_branch_target(*target, code.len())?; + } + continue; + } + Instruction::Goto(target) => { + self.pc = host.static_branch_target(*target, code.len())?; + continue; + } + Instruction::InitialYield => { + return Ok(InterpreterExit::Suspend(VmSuspendKind::Initial)); + } + Instruction::Yield => return Ok(InterpreterExit::Suspend(VmSuspendKind::Yield)), + Instruction::YieldStar => { + return Ok(InterpreterExit::Suspend(VmSuspendKind::YieldStar)); + } + Instruction::AsyncYieldStar => { + return Ok(InterpreterExit::Suspend(VmSuspendKind::AsyncYieldStar)); + } + Instruction::Await => return Ok(InterpreterExit::Suspend(VmSuspendKind::Await)), Instruction::Arguments(_) - | Instruction::Rest(_) - | Instruction::VariableEnvironment - | Instruction::HasEvalVariable { .. } - | Instruction::GetEvalVariable { .. } - | Instruction::PutEvalVariable { .. } - | Instruction::DeleteEvalVariable { .. } - | Instruction::DefineEvalVariable { .. } - | Instruction::ToObject - | Instruction::HasDynamicBinding { .. } - | Instruction::GetDynamicBinding { .. } - | Instruction::PutDynamicBinding { .. } - | Instruction::DeleteDynamicBinding { .. } - | Instruction::DynamicEnvironmentObject(_) - | Instruction::GlobalReference(_) - | Instruction::GetRefValue(_) - | Instruction::GetRefValueUndef(_) - | Instruction::PutRefValue(_) - | Instruction::Object - | Instruction::RegExp(_) - | Instruction::SetNameComputed - | Instruction::DefineMethod { .. } - | Instruction::DefineMethodComputed { .. } - | Instruction::DefineClass { .. } - | Instruction::SetProto - | Instruction::CopyDataProperties - | Instruction::CopyDataPropertiesExcluded { .. } - | Instruction::IteratorStart - | Instruction::AsyncIteratorStart - | Instruction::IteratorNext - | Instruction::IteratorCall(_) - | Instruction::ForAwaitOfStart - | Instruction::ForAwaitOfNext - | Instruction::IteratorGetValueDone - | Instruction::ForInStart - | Instruction::ForInNext - ) { - if let Some(completion) = self.execute_cold_instruction(instruction, host)? { - return Ok(InterpreterExit::Complete(completion)); - } - continue; - } - - if matches!( - instruction, + | Instruction::Rest(_) + | Instruction::VariableEnvironment + | Instruction::HasEvalVariable { .. } + | Instruction::GetEvalVariable { .. } + | Instruction::PutEvalVariable { .. } + | Instruction::DeleteEvalVariable { .. } + | Instruction::DefineEvalVariable { .. } + | Instruction::ToObject + | Instruction::HasDynamicBinding { .. } + | Instruction::GetDynamicBinding { .. } + | Instruction::PutDynamicBinding { .. } + | Instruction::DeleteDynamicBinding { .. } + | Instruction::DynamicEnvironmentObject(_) + | Instruction::GlobalReference(_) + | Instruction::GetRefValue(_) + | Instruction::GetRefValueUndef(_) + | Instruction::PutRefValue(_) + | Instruction::Object + | Instruction::RegExp(_) + | Instruction::SetNameComputed + | Instruction::DefineMethod { .. } + | Instruction::DefineMethodComputed { .. } + | Instruction::DefineClass { .. } + | Instruction::SetProto + | Instruction::CopyDataProperties + | Instruction::CopyDataPropertiesExcluded { .. } + | Instruction::IteratorStart + | Instruction::AsyncIteratorStart + | Instruction::IteratorNext + | Instruction::IteratorCall(_) + | Instruction::ForAwaitOfStart + | Instruction::ForAwaitOfNext + | Instruction::IteratorGetValueDone + | Instruction::ForInStart + | Instruction::ForInNext => self.execute_cold_instruction(instruction, host)?, Instruction::Import - | Instruction::Call(_) - | Instruction::TailCall(_) - | Instruction::Eval { .. } - | Instruction::CallMethod(_) - | Instruction::TailCallMethod(_) - | Instruction::Construct(_) - | Instruction::ConstructSuper(_) - | Instruction::InitDerivedConstructor - | Instruction::Apply(_) - | Instruction::ApplySuper - | Instruction::ApplyEval { .. } - ) { - if let Some(completion) = self.execute_call_instruction(instruction, host)? { - return Ok(InterpreterExit::Complete(completion)); - } - continue; - } - - if matches!( - instruction, + | Instruction::Call(_) + | Instruction::TailCall(_) + | Instruction::Eval { .. } + | Instruction::CallMethod(_) + | Instruction::TailCallMethod(_) + | Instruction::Construct(_) + | Instruction::ConstructSuper(_) + | Instruction::InitDerivedConstructor + | Instruction::Apply(_) + | Instruction::ApplySuper + | Instruction::ApplyEval { .. } => { + self.execute_call_instruction(instruction, host)? + } Instruction::Neg - | Instruction::Plus - | Instruction::Inc - | Instruction::Dec - | Instruction::PostInc - | Instruction::PostDec - | Instruction::BitNot - | Instruction::Not - | Instruction::TypeOf - | Instruction::IsUndefinedOrNull - | Instruction::IsUndefined - | Instruction::IsNull - | Instruction::TypeOfIsUndefined - | Instruction::TypeOfIsFunction - | Instruction::Add - | Instruction::Sub - | Instruction::Mul - | Instruction::Div - | Instruction::Mod - | Instruction::Pow - | Instruction::Shl - | Instruction::Sar - | Instruction::Shr - | Instruction::BitAnd - | Instruction::BitXor - | Instruction::BitOr - | Instruction::Eq - | Instruction::StrictEq - | Instruction::Neq - | Instruction::StrictNeq - | Instruction::Lt - | Instruction::Lte - | Instruction::Gt - | Instruction::Gte - ) { - if let Some(completion) = self.execute_numeric_instruction(instruction, host)? { - return Ok(InterpreterExit::Complete(completion)); - } - continue; - } - - if let Some(completion) = self.execute_hot_instruction(code, instruction, host)? { + | Instruction::Plus + | Instruction::Inc + | Instruction::Dec + | Instruction::PostInc + | Instruction::PostDec + | Instruction::BitNot + | Instruction::Not + | Instruction::TypeOf + | Instruction::IsUndefinedOrNull + | Instruction::IsUndefined + | Instruction::IsNull + | Instruction::TypeOfIsUndefined + | Instruction::TypeOfIsFunction + | Instruction::Add + | Instruction::Sub + | Instruction::Mul + | Instruction::Div + | Instruction::Mod + | Instruction::Pow + | Instruction::Shl + | Instruction::Sar + | Instruction::Shr + | Instruction::BitAnd + | Instruction::BitXor + | Instruction::BitOr + | Instruction::Eq + | Instruction::StrictEq + | Instruction::Neq + | Instruction::StrictNeq + | Instruction::Lt + | Instruction::Lte + | Instruction::Gt + | Instruction::Gte => self.execute_numeric_instruction(instruction, host)?, + _ => self.execute_hot_instruction(code, instruction, host)?, + }; + if let Some(completion) = completion { return Ok(InterpreterExit::Complete(completion)); } } @@ -279,11 +401,9 @@ impl VmActivation { } pub(in crate::engine::vm) fn clone_at_depth(&self, depth: u8) -> Result { - let index = self - .stack - .len() - .checked_sub(usize::from(depth) + 1) - .ok_or_else(|| Error::internal("bytecode stack depth operand is out of bounds"))?; + // A too-large depth wraps above len, so the single checked lookup + // rejects it. Valid depths select the original tail-relative slot. + let index = self.stack.len().wrapping_sub(usize::from(depth) + 1); self.stack .get(index) .cloned() @@ -362,8 +482,18 @@ impl VmActivation { } pub(in crate::engine::vm) fn pop_pair(&mut self) -> Result<(Value, Value), Error> { - let right = self.pop()?; - let left = self.pop()?; + if self.stack.len() < 2 { + // Match sequential-pop failure: consume a lone right operand, + // construct the error, then release the operand. + let right = self.pop()?; + let error = Error::internal("bytecode stack underflow"); + drop(right); + return Err(error); + } + // The shared bound proves both pops. Neither move can invoke user + // code or change the stack except by removing its own operand. + let right = self.stack.pop().expect("two operands were checked"); + let left = self.stack.pop().expect("one checked operand remains"); Ok((left, right)) } } diff --git a/src/engine/vm/host_bridge.rs b/src/engine/vm/host_bridge.rs index 3a0c0d85..c34c7d7a 100644 --- a/src/engine/vm/host_bridge.rs +++ b/src/engine/vm/host_bridge.rs @@ -12,15 +12,16 @@ use crate::engine::atom::{Atom, AtomKind, PropertyKeyKind}; use crate::engine::builtins::native::{ArrayIteratorKind, NativeFunctionId, PrimitiveKind}; use crate::engine::code::bytecode::{ ApplyKind, ArgumentsKind, DefineMethodKind, DynamicEnvironmentSource, EvalVariableSource, - PrivateNameSource, + Instruction, PrivateNameSource, }; use crate::engine::code::function::metadata::{ ClosureSource, ClosureVariable, ClosureVariableKind, ClosureVariableName, EvalBinding, - EvalBindingSource, EvalEnvironment, EvalVariableEnvironment, FunctionKind, FunctionMetadata, - VariableDefinition, + EvalBindingSource, EvalEnvironment, FunctionKind, FunctionMetadata, VariableDefinition, }; use crate::engine::code::rooted::FunctionBytecodeRef; -use crate::engine::code::runtime::PublishedFunctionSnapshot; +use crate::engine::code::runtime::{ + PublishedEvalEnvironment, PublishedFunctionData, PublishedFunctionSnapshot, +}; use crate::engine::heap::roots::VarRefRoot; use crate::engine::code::module::{ModuleImportAttribute, ModuleImportAttributes}; @@ -43,12 +44,13 @@ use crate::engine::vm::frames::ActiveFrameGuard; use crate::engine::vm::{ AppendStartOutcome, ArgumentListOutcome, BytecodePc, CallInput, Completion, DefineClassOutcome, DirectEvalInvocation, ForInNextOutcome, ForInStartOutcome, ForOfNextOutcome, ForOfStartOutcome, - IteratorCloseOutcome, ToPrimitiveHint, Vm, VmActivationParts, VmExit, VmHost, VmResume, - VmSuspendKind, VmSuspension, + IteratorCloseOutcome, ToPrimitiveHint, Vm, VmActivation, VmActivationParts, VmExit, VmHost, + VmResume, VmSuspendKind, VmSuspension, }; use std::rc::Rc; mod dynamic_environment; +mod eval_validation; mod private_elements; mod super_property; @@ -72,8 +74,7 @@ pub(crate) const TYPEOF_STATIC_ATOMS: [&str; 8] = [ /// QuickJS's ordering: parse/publish errors occur before closure capture. pub(crate) struct PreparedEvalEnvironment { pub(crate) index: u16, - pub(crate) caller_bytecode: FunctionBytecodeRef, - pub(crate) descriptor: EvalEnvironment, + pub(crate) descriptor: PublishedEvalEnvironment, } /// Live cells paired with one immutable caller-environment descriptor. @@ -84,9 +85,7 @@ pub(crate) struct PreparedEvalEnvironment { /// actual cells live for the instantiation/execution interval. pub(crate) struct MaterializedEvalEnvironment { pub(crate) index: u16, - /// Retain the owner of descriptor atoms through final instantiation. - pub(crate) _caller_bytecode: FunctionBytecodeRef, - pub(crate) descriptor: EvalEnvironment, + pub(crate) descriptor: PublishedEvalEnvironment, pub(crate) roots: Box<[VarRefRoot]>, } @@ -132,6 +131,7 @@ pub(crate) fn closure_view_matches_cell( && descriptor.kind == ClosureVariableKind::Normal) } +#[inline] fn read_frame_binding(runtime: &Runtime, binding: &FrameBinding) -> Result { match binding { FrameBinding::Direct(value) => Ok(value.clone()), @@ -147,6 +147,7 @@ fn read_frame_binding(runtime: &Runtime, binding: &FrameBinding) -> Result { - runtime - .validate_var_ref_metadata(root, descriptor) - .map_err(|error| Error::internal(error.to_string()))?; - Ok(root.clone()) - } + FrameBinding::Captured(root) => reuse_frame_capture(runtime, root, descriptor), } } +/// Reuse a live cell through a publication-authenticated descriptor view. +/// This checks actual cell metadata without redispatching its frame storage. +fn reuse_frame_capture( + runtime: &Runtime, + root: &VarRefRoot, + descriptor: ClosureVariable, +) -> Result { + runtime + .validate_var_ref_metadata(root, descriptor) + .map_err(|error| Error::internal(error.to_string()))?; + Ok(root.clone()) +} + fn close_frame_binding( runtime: &Runtime, binding: &mut FrameBinding, @@ -419,27 +428,13 @@ pub(crate) struct RuntimeVmHost { /// constructor return-protocol errors are allocated here, unlike ordinary /// bytecode errors which belong to `current_realm`. caller_realm: ContextId, - current_bytecode: Option, - /// Compile-time QuickJS `strip_var_debug` decision for this exact frame. - strip_variable_debug: bool, + executable: PublishedFunctionSnapshot, /// Current callee retained for sloppy mapped `arguments.callee`. /// Detached host-only tests do not execute the arguments opcode. current_function: Option, /// Authored call arity before the argument frame was padded to formal /// width. `arguments.length` and its dense prefix use this exact count. actual_argument_count: usize, - constants: Rc<[BytecodeConstant]>, - property_key_atoms: Option>, - argument_definitions: Rc<[VariableDefinition]>, - local_definitions: Rc<[VariableDefinition]>, - closure_variables: Rc<[ClosureVariable]>, - eval_environments: Rc<[EvalEnvironment]>, - /// Exact local slot authenticated by bytecode metadata as this frame's - /// hidden sloppy-eval variable object. - eval_variable_object_local: Option, - /// Exact local slot authenticated by the parameter-environment layout as - /// the independent hidden sloppy-eval argument-scope variable object. - arg_eval_variable_object_local: Option, closure_slots: Vec, arguments: Vec, locals: Vec, @@ -507,12 +502,12 @@ fn generator_raw_value_atom(value: &RawValue) -> Option { /// are detached. `host.active_frame_token` remains a sentinel until the /// short-lived bytecode active frame is pushed for the actual resume. pub(crate) struct RootedVmActivation { - pub(crate) suspend_kind: VmSuspendKind, - pub(crate) suspension: VmSuspension, - pub(crate) host: RuntimeVmHost, - pub(crate) bytecode: FunctionBytecodeRef, - pub(crate) code: Rc<[crate::engine::code::bytecode::Instruction]>, - pub(crate) metadata: FunctionMetadata, + suspend_kind: VmSuspendKind, + suspension: VmSuspension, + host: RuntimeVmHost, + bytecode: FunctionBytecodeRef, + code: Rc<[crate::engine::code::bytecode::Instruction]>, + metadata: FunctionMetadata, saved_pc: usize, } @@ -619,6 +614,41 @@ enum VmPropertyKeyConversion { } impl RuntimeVmHost { + /// Code and layout are taken from this host's sealed snapshot, never from + /// caller-supplied slices. Keep ordinary and suspendable drivers separate. + #[inline] + pub(super) fn new_activation( + &self, + input: CallInput, + ) -> Result<(Rc<[Instruction]>, VmActivation), Error> { + if self.executable.root().is_none() { + return Err(Error::internal( + "unpublished host cannot execute published code", + )); + } + let metadata = self.executable.metadata; + if self.closure_count() != usize::from(metadata.closure_count) { + return Err(Error::internal( + "function object closure slot count does not match bytecode metadata", + )); + } + let function = self + .current_function + .as_ref() + .ok_or_else(|| Error::internal("published frame has no current function"))? + .clone(); + let activation = VmActivation::new_in_realm( + metadata, + self.caller_realm, + self.current_realm, + function, + input.this_value, + input.new_target, + input.callee_global, + ); + Ok((self.executable.code.clone(), activation)) + } + /// QuickJS `OP_typeof` converts one of its predefined type atoms back to /// the atom's canonical String cell. Runtime construction pins the full /// result set, so every realm reuses the same representation while sibling @@ -642,18 +672,9 @@ impl RuntimeVmHost { active_frame_token: ActiveFrameToken(0), current_realm, caller_realm: current_realm, - current_bytecode: None, - strip_variable_debug: false, + executable: PublishedFunctionSnapshot::empty_for_test(current_realm), current_function: None, actual_argument_count: 0, - constants: Rc::from([]), - property_key_atoms: None, - argument_definitions: Rc::from([]), - local_definitions: Rc::from([]), - closure_variables: Rc::from([]), - eval_environments: Rc::from([]), - eval_variable_object_local: None, - arg_eval_variable_object_local: None, closure_slots: Vec::new(), arguments: Vec::new(), locals: Vec::new(), @@ -670,19 +691,15 @@ impl RuntimeVmHost { arguments: Vec, locals: Vec, ) -> Result { - let PublishedFunctionSnapshot { - root, - code: _, - constants, - property_key_atoms, + let executable = runtime.snapshot_function_bytecode(bytecode)?; + let PublishedFunctionData { argument_definitions, local_definitions, closure_variables, - eval_environments, - arg_eval_variable_object_local, - metadata, realm, - } = runtime.snapshot_function_bytecode(bytecode)?; + .. + } = &*executable; + let realm = *realm; if realm != current_realm { return Err(RuntimeError::Invariant( "test eval frame realm disagrees with its bytecode", @@ -702,18 +719,9 @@ impl RuntimeVmHost { active_frame_token: ActiveFrameToken(0), current_realm, caller_realm: current_realm, - current_bytecode: Some(root), - strip_variable_debug: metadata.strip_variable_debug, + executable, current_function: None, actual_argument_count: arguments.len(), - constants, - property_key_atoms, - argument_definitions, - local_definitions, - closure_variables, - eval_environments, - eval_variable_object_local: metadata.eval_variable_object_local, - arg_eval_variable_object_local, closure_slots, arguments: arguments.into_iter().map(FrameBinding::Direct).collect(), locals: locals.into_iter().map(FrameBinding::Direct).collect(), @@ -743,12 +751,9 @@ impl RuntimeVmHost { suspension: VmSuspension, ) -> Result { let (kind, parts) = suspension.into_parts().map_err(RuntimeError::Engine)?; - let bytecode = self - .current_bytecode - .as_ref() - .ok_or(RuntimeError::Invariant( - "resumable host has no current bytecode root", - ))?; + let bytecode = self.executable.root().ok_or(RuntimeError::Invariant( + "resumable host has no current bytecode root", + ))?; let caller_realm = parts.caller_realm.ok_or(RuntimeError::Invariant( "resumable VM activation has no caller realm", ))?; @@ -767,8 +772,8 @@ impl RuntimeVmHost { if caller_realm != self.caller_realm || callee_realm != self.current_realm || self.current_function.as_ref() != Some(current_function) - || self.arguments.len() < self.argument_definitions.len() - || self.locals.len() != self.local_definitions.len() + || self.arguments.len() < self.executable.argument_definitions.len() + || self.locals.len() != self.executable.local_definitions.len() || self.reusable_captured_locals.len() != self.locals.len() || self.actual_argument_count > self.arguments.len() { @@ -831,19 +836,22 @@ impl RuntimeVmHost { runtime.0.state.borrow().heap.context(resume_caller_realm)?; let bytecode_probe = FunctionBytecodeRef::from_borrowed_handle(runtime.clone(), data.bytecode)?; - let PublishedFunctionSnapshot { - root, + let executable = runtime.snapshot_function_bytecode(&bytecode_probe)?; + let PublishedFunctionData { code, - constants, - property_key_atoms, argument_definitions, local_definitions, closure_variables, - eval_environments, - arg_eval_variable_object_local, metadata, realm, - } = runtime.snapshot_function_bytecode(&bytecode_probe)?; + .. + } = &*executable; + let metadata = *metadata; + let realm = *realm; + let root = executable + .root() + .expect("runtime snapshot owns bytecode") + .clone(); drop(bytecode_probe); if metadata.function_kind != expected_function_kind || realm != data.vm.callee_realm @@ -925,23 +933,15 @@ impl RuntimeVmHost { callee_global: Some(callee_global), }; let suspension = VmSuspension::from_parts(kind, parts).map_err(RuntimeError::Engine)?; + let code = code.clone(); let host = RuntimeVmHost { runtime, active_frame_token: ActiveFrameToken(0), current_realm: data.vm.callee_realm, caller_realm: resume_caller_realm, - current_bytecode: Some(root.clone()), - strip_variable_debug: metadata.strip_variable_debug, + executable, current_function: Some(current_function), actual_argument_count: data.actual_argument_count, - constants, - property_key_atoms, - argument_definitions, - local_definitions, - closure_variables, - eval_environments, - eval_variable_object_local: metadata.eval_variable_object_local, - arg_eval_variable_object_local, closure_slots, arguments, locals, @@ -988,19 +988,23 @@ impl RuntimeVmHost { } fn local_definition(&self, index: u16) -> Result { - self.local_definitions + self.executable + .local_definitions .get(usize::from(index)) .copied() .ok_or_else(|| Error::internal("local definition index is out of bounds")) } + #[cfg(test)] fn argument_definition(&self, index: u16) -> Result { - self.argument_definitions + self.executable + .argument_definitions .get(usize::from(index)) .copied() .ok_or_else(|| Error::internal("argument definition index is out of bounds")) } + #[cfg(test)] fn validate_capture_definition( &self, definition: VariableDefinition, @@ -1052,360 +1056,36 @@ impl RuntimeVmHost { } } - fn validate_eval_definition( - definition: VariableDefinition, - binding: &EvalBinding, - ) -> Result<(), Error> { - if definition.name != Some(binding.name) - || definition.is_lexical != binding.is_lexical - || definition.is_const != binding.is_const - || definition.kind != binding.kind - { - return Err(Error::internal( - "eval binding disagrees with its caller variable definition", - )); - } - Ok(()) - } - - fn validate_eval_closure( - descriptor: ClosureVariable, - binding: &EvalBinding, - ) -> Result<(), Error> { - if matches!( - descriptor.source, - ClosureSource::GlobalDeclaration - | ClosureSource::Global - | ClosureSource::ParentGlobal(_) - ) { - return Err(Error::internal( - "eval environment retained a global closure binding", - )); - } - let name_matches = - matches!(descriptor.name, ClosureVariableName::Atom(name) if name == binding.name); - if !name_matches - || descriptor.is_lexical != binding.is_lexical - || descriptor.is_const != binding.is_const - || descriptor.kind != binding.kind - { - return Err(Error::internal( - "eval binding disagrees with its caller closure descriptor", - )); - } - Ok(()) - } - fn eval_variable_object_local_kind(&self, index: u16) -> Option { - if self.eval_variable_object_local == Some(index) { + if self.executable.metadata.eval_variable_object_local == Some(index) { return Some(ClosureVariableKind::EvalVariableObject); } - if self.arg_eval_variable_object_local == Some(index) { + if self.executable.arg_eval_variable_object_local == Some(index) { return Some(ClosureVariableKind::ArgEvalVariableObject); } None } - fn validate_eval_environment( - &self, - environment: &EvalEnvironment, - caller_strict: bool, - caller_metadata: FunctionMetadata, - ) -> Result<(), Error> { - if environment.caller_strict != caller_strict { - return Err(Error::internal( - "eval environment caller strictness disagrees with its bytecode frame", - )); - } - if caller_metadata.super_call_allowed && !caller_metadata.super_allowed { - return Err(Error::internal( - "caller bytecode permits super() without SuperProperty", - )); - } - if environment.super_call_allowed && !environment.super_allowed { - return Err(Error::internal( - "eval environment permits super() without SuperProperty", - )); - } - if (environment.super_call_allowed, environment.super_allowed) - != ( - caller_metadata.super_call_allowed, - caller_metadata.super_allowed, - ) - { - return Err(Error::internal( - "eval environment super capability disagrees with caller bytecode", - )); - } - let first_function_anchor = environment - .scopes - .iter() - .position(|scope| { - matches!( - scope.kind, - crate::engine::code::function::metadata::EvalScopeKind::FunctionRoot - | crate::engine::code::function::metadata::EvalScopeKind::Parameter - ) - }) - .and_then(|scope| u16::try_from(scope).ok()) - .ok_or_else(|| { - Error::internal( - "eval environment contains no representable current function anchor", - ) - })?; - match environment.variable_environment { - EvalVariableEnvironment::Global => { - let current_body_is_program = first_function_anchor - .checked_sub(1) - .and_then(|scope| environment.scopes.get(usize::from(scope))) - .is_some_and(|scope| { - scope.kind - == crate::engine::code::function::metadata::EvalScopeKind::ProgramBody - }); - if caller_metadata.is_module - || !current_body_is_program - || (caller_strict - && caller_metadata.eval_kind - != crate::engine::code::function::metadata::EvalKind::None) - { - return Err(Error::internal( - "global eval variable environment escaped an authored Script root", - )); - } - } - EvalVariableEnvironment::StrictLocal(scope) => { - if !caller_strict { - return Err(Error::internal( - "sloppy eval environment selected a strict-local destination", - )); - } - if scope != first_function_anchor { - return Err(Error::internal( - "strict eval variable environment selected the wrong current function segment", - )); - } - let current_body_is_program = first_function_anchor - .checked_sub(1) - .and_then(|scope| environment.scopes.get(usize::from(scope))) - .is_some_and(|scope| { - scope.kind - == crate::engine::code::function::metadata::EvalScopeKind::ProgramBody - }); - if current_body_is_program - && caller_metadata.eval_kind - == crate::engine::code::function::metadata::EvalKind::None - && !caller_metadata.is_module - { - return Err(Error::internal( - "authored Script eval environment used a non-canonical strict-local target", - )); - } - let Some(scope) = environment.scopes.get(usize::from(scope)) else { - return Err(Error::internal( - "eval variable-environment scope is out of bounds", - )); - }; - if !matches!( - scope.kind, - crate::engine::code::function::metadata::EvalScopeKind::FunctionRoot - | crate::engine::code::function::metadata::EvalScopeKind::Parameter - ) { - return Err(Error::internal( - "strict eval variable environment did not select a function anchor", - )); - } - } - EvalVariableEnvironment::VariableObject { scope, source } => { - if caller_strict || matches!(source, EvalBindingSource::Argument(_)) { - return Err(Error::internal( - "eval variable-object destination is not authentic", - )); - } - let target_matches_function_segment = if caller_metadata.eval_kind - == crate::engine::code::function::metadata::EvalKind::None - { - scope == first_function_anchor && matches!(source, EvalBindingSource::Local(_)) - } else { - caller_metadata.eval_kind - == crate::engine::code::function::metadata::EvalKind::Direct - && scope > first_function_anchor - && matches!(source, EvalBindingSource::Closure(_)) - }; - if !target_matches_function_segment { - return Err(Error::internal( - "eval variable object selected the wrong current function segment", - )); - } - let target_scope = environment.scopes.get(usize::from(scope)).ok_or_else(|| { - Error::internal("eval variable-object scope is out of bounds") - })?; - let expected_kind = match target_scope.kind { - crate::engine::code::function::metadata::EvalScopeKind::FunctionRoot => { - ClosureVariableKind::EvalVariableObject - } - crate::engine::code::function::metadata::EvalScopeKind::Parameter => { - ClosureVariableKind::ArgEvalVariableObject - } - _ => { - return Err(Error::internal( - "eval variable object selected a non-function scope", - )); - } - }; - if target_scope - .bindings - .iter() - .filter(|binding| { - binding.source == source - && binding.kind == expected_kind - && !binding.is_lexical - && !binding.is_const - && !binding.is_catch_parameter - }) - .count() - != 1 - { - return Err(Error::internal("eval variable-object target is not exact")); - } - match source { - EvalBindingSource::Local(index) => { - if self.eval_variable_object_local_kind(index) != Some(expected_kind) { - return Err(Error::internal( - "eval variable-object local role is not authentic", - )); - } - let definition = self.local_definition(index)?; - if definition.kind != expected_kind - || definition.is_lexical - || definition.is_const - { - return Err(Error::internal( - "eval variable-object local definition is malformed", - )); - } - self.locals.get(usize::from(index)).ok_or_else(|| { - Error::internal("eval variable-object local is out of bounds") - })?; - } - EvalBindingSource::Closure(index) => { - let descriptor = *self - .closure_variables - .get(usize::from(index)) - .ok_or_else(|| { - Error::internal("eval variable-object closure is out of bounds") - })?; - if descriptor.kind != expected_kind - || descriptor.is_lexical - || descriptor.is_const - { - return Err(Error::internal( - "eval variable-object closure descriptor is malformed", - )); - } - let root = self.closure_slots.get(usize::from(index)).ok_or_else(|| { - Error::internal("eval variable-object closure slot is out of bounds") - })?; - self.runtime - .validate_var_ref_metadata(root, descriptor) - .map_err(|error| Error::internal(error.to_string()))?; - } - EvalBindingSource::Argument(_) => unreachable!( - "argument variable-object source was rejected before validation" - ), - } - } - } - for scope in &environment.scopes { - for binding in &scope.bindings { - if binding.kind.is_eval_variable_object() - && match scope.kind { - crate::engine::code::function::metadata::EvalScopeKind::FunctionRoot => { - false - } - crate::engine::code::function::metadata::EvalScopeKind::Parameter => { - binding.kind != ClosureVariableKind::ArgEvalVariableObject - } - _ => true, - } - { - return Err(Error::internal( - "eval variable-object binding escaped its authenticated function anchor", - )); - } - match binding.source { - EvalBindingSource::Local(index) => { - let definition = self.local_definition(index)?; - Self::validate_eval_definition(definition, binding)?; - self.locals.get(usize::from(index)).ok_or_else(|| { - Error::internal("eval local binding index is out of bounds") - })?; - } - EvalBindingSource::Argument(index) => { - let definition = self.argument_definition(index)?; - Self::validate_eval_definition(definition, binding)?; - self.arguments.get(usize::from(index)).ok_or_else(|| { - Error::internal("eval argument binding index is out of bounds") - })?; - } - EvalBindingSource::Closure(index) => { - let descriptor = *self - .closure_variables - .get(usize::from(index)) - .ok_or_else(|| { - Error::internal("eval closure binding index is out of bounds") - })?; - Self::validate_eval_closure(descriptor, binding)?; - let root = self.closure_slots.get(usize::from(index)).ok_or_else(|| { - Error::internal("eval closure slot index is out of bounds") - })?; - self.runtime - .validate_var_ref_metadata(root, descriptor) - .map_err(|error| Error::internal(error.to_string()))?; - } - } - } - } - Ok(()) - } - fn prepare_direct_eval_environment( &self, index: u16, caller_strict: bool, ) -> Result { let descriptor = self - .eval_environments - .get(usize::from(index)) - .cloned() + .executable + .eval_environment(index) .ok_or_else(|| Error::internal("eval environment index is out of bounds"))?; - let caller_bytecode = self.current_bytecode.clone().ok_or_else(|| { - Error::internal("direct eval frame did not retain its caller bytecode") - })?; - let caller_metadata = self - .runtime - .snapshot_function_bytecode(&caller_bytecode) - .map_err(runtime_error_to_vm_error)? - .metadata; - // Authenticate every immutable source before compilation. Corrupt - // published bytecode must fail without compiling attacker-selected - // names or converting any frame binding to a VarRef. - self.validate_eval_environment(&descriptor, caller_strict, caller_metadata)?; - Ok(PreparedEvalEnvironment { - index, - caller_bytecode, - descriptor, - }) + // Publication authenticates the immutable topology and source modes. + // Check this frame's actual slots before compiling or capturing anything. + self.validate_eval_frame_bindings(&descriptor, caller_strict)?; + Ok(PreparedEvalEnvironment { index, descriptor }) } fn materialize_direct_eval_environment( &mut self, prepared: PreparedEvalEnvironment, ) -> Result { - let PreparedEvalEnvironment { - index, - caller_bytecode, - descriptor, - } = prepared; + let PreparedEvalEnvironment { index, descriptor } = prepared; let binding_count = descriptor .scopes .iter() @@ -1446,7 +1126,6 @@ impl RuntimeVmHost { } Ok(MaterializedEvalEnvironment { index, - _caller_bytecode: caller_bytecode, descriptor, roots: roots.into_boxed_slice(), }) @@ -1499,7 +1178,10 @@ impl RuntimeVmHost { /// of those atoms as publication/authentication metadata, so diagnostics /// must apply the same independent rule without deleting semantic names. fn local_lexical_uninitialized_error(&self, name: Option) -> Result { - self.lexical_uninitialized_error_with_visibility(name, !self.strip_variable_debug) + self.lexical_uninitialized_error_with_visibility( + name, + !self.executable.metadata.strip_variable_debug, + ) } fn closure_lexical_uninitialized_error( @@ -1519,7 +1201,7 @@ impl RuntimeVmHost { ); self.lexical_uninitialized_error_with_visibility( name, - semantic_name || !self.strip_variable_debug, + semantic_name || !self.executable.metadata.strip_variable_debug, ) } @@ -1536,6 +1218,7 @@ impl RuntimeVmHost { fn closure_name(&self, index: u16) -> Result, Error> { let descriptor = self + .executable .closure_variables .get(usize::from(index)) .ok_or_else(|| Error::internal("closure variable index is out of bounds"))?; @@ -1554,11 +1237,8 @@ impl RuntimeVmHost { // Synthetic host-only unit tests have no published bytecode owner. // Production and published-code tests always require the linked table. #[cfg(test)] - if self.current_bytecode.is_none() { - let name = match usize::try_from(index) - .ok() - .and_then(|index| self.constants.get(index)) - { + if self.executable.root().is_none() { + let name = match self.executable.constant(index) { Some(BytecodeConstant::Value(RawValue::String(name))) => name.clone(), Some( BytecodeConstant::Value(_) @@ -1579,7 +1259,7 @@ impl RuntimeVmHost { } let atom = usize::try_from(index) .ok() - .and_then(|index| self.property_key_atoms.as_ref()?.get(index)) + .and_then(|index| self.executable.property_key_atoms.as_ref()?.get(index)) .copied() .filter(|atom| !atom.is_null()) .ok_or_else(|| Error::internal("static name opcode has no linked property key"))?; @@ -1624,6 +1304,7 @@ impl RuntimeVmHost { } EvalVariableSource::Closure(index) => { let descriptor = self + .executable .closure_variables .get(usize::from(index)) .copied() @@ -2121,7 +1802,7 @@ impl Runtime { caller_realm: ContextId, callable: &CallableRef, mut host: RuntimeVmHost, - input: CallInput<'_>, + input: CallInput, active_frame: ActiveFrameGuard, ) -> Result { let result = Vm::new().start_published(input, &mut host); @@ -2154,23 +1835,19 @@ impl Runtime { if self.bytecode_call_would_overflow() { return self.bytecode_stack_overflow_completion(caller_realm, &bytecode); } - let PublishedFunctionSnapshot { - root, - code, - constants, - property_key_atoms, - argument_definitions, + let executable = self.snapshot_function_bytecode(&bytecode)?; + let PublishedFunctionData { local_definitions, - closure_variables, - eval_environments, - arg_eval_variable_object_local, metadata, realm, - } = self.snapshot_function_bytecode(&bytecode)?; + .. + } = &*executable; + let metadata = *metadata; + let realm = *realm; let callee_global = self.global_object_for_realm(realm)?; let active_frame = self.push_bytecode_active_frame( callable.as_object().clone(), - root.clone(), + bytecode, realm, metadata.strict, )?; @@ -2203,18 +1880,9 @@ impl Runtime { active_frame_token: active_frame.token(), current_realm: realm, caller_realm, - current_bytecode: Some(root), - strip_variable_debug: metadata.strip_variable_debug, + executable, current_function: Some(callable.as_object().clone()), actual_argument_count: arguments.len(), - constants, - property_key_atoms, - argument_definitions, - local_definitions, - closure_variables, - eval_environments, - eval_variable_object_local: metadata.eval_variable_object_local, - arg_eval_variable_object_local, closure_slots, arguments: frame_arguments, locals: frame_locals, @@ -2222,11 +1890,6 @@ impl Runtime { }; let is_module_link_entry = metadata.is_module && this_value == Value::Bool(true); let input = CallInput { - code: &code, - metadata, - caller_realm, - callee_realm: realm, - current_function: callable.as_object().clone(), this_value, new_target, callee_global, @@ -2273,6 +1936,18 @@ impl Runtime { } impl VmHost for RuntimeVmHost { + #[inline] + fn static_branch_target(&self, target: u32, _code_len: usize) -> Result { + // Production execution takes code from this host's immutable snapshot. + // The verifier checks every immediate target, including unreachable + // instructions. Keep synthetic hosts on the checked path. + #[cfg(test)] + if self.executable.root().is_none() { + return super::activation::checked_target(target, _code_len); + } + usize::try_from(target).map_err(|_| Error::internal("jump target overflow")) + } + fn update_active_bytecode_pc(&mut self, pc: BytecodePc) -> Result<(), Error> { self.runtime .update_active_bytecode_pc(self.active_frame_token, pc) @@ -2622,9 +2297,9 @@ impl VmHost for RuntimeVmHost { } fn load_constant(&mut self, index: u32) -> Result { - let constant = usize::try_from(index) - .ok() - .and_then(|index| self.constants.get(index)) + let constant = self + .executable + .constant(index) .ok_or_else(|| Error::internal("constant index is out of bounds"))?; match constant { BytecodeConstant::Value(value) => self @@ -2792,9 +2467,9 @@ impl VmHost for RuntimeVmHost { } fn instantiate_closure(&mut self, index: u32) -> Result { - let constant = usize::try_from(index) - .ok() - .and_then(|index| self.constants.get(index)) + let constant = self + .executable + .constant(index) .ok_or_else(|| Error::internal("constant index is out of bounds"))?; let BytecodeConstant::Function(bytecode) = constant else { return Err(Error::internal( @@ -2816,26 +2491,48 @@ impl VmHost for RuntimeVmHost { for descriptor in closure_variables.iter().copied() { let root = match descriptor.source { ClosureSource::ParentLocal(index) => { - let definition = self.local_definition(index)?; - self.validate_capture_definition(definition, descriptor)?; + #[cfg(test)] + if self.executable.root().is_none() { + self.validate_capture_definition( + self.local_definition(index)?, + descriptor, + )?; + } let binding = self .locals .get_mut(usize::from(index)) .ok_or_else(|| Error::internal("captured local index is out of bounds"))?; - capture_frame_binding( - &self.runtime, - binding, - ClosureVariable { + // Existing cells already own canonical metadata. Validate + // the child's authenticated view against that actual cell; + // only a new cell needs the parent's definition. In + // particular, do not recreate FunctionName view metadata. + if let FrameBinding::Captured(root) = binding { + reuse_frame_capture(&self.runtime, root, descriptor)? + } else { + let definition = self + .executable + .local_definitions + .get(usize::from(index)) + .ok_or_else(|| { + Error::internal("local definition index is out of bounds") + })?; + let capture = ClosureVariable { is_lexical: definition.is_lexical, is_const: definition.is_const, kind: definition.kind, ..descriptor - }, - )? + }; + capture_frame_binding(&self.runtime, binding, capture)? + } } ClosureSource::ParentArgument(index) => { - let definition = self.argument_definition(index)?; - self.validate_capture_definition(definition, descriptor)?; + #[cfg(test)] + if self.executable.root().is_none() { + self.validate_capture_definition( + self.argument_definition(index)?, + descriptor, + )?; + } let binding = self.arguments.get_mut(usize::from(index)).ok_or_else(|| { Error::internal("captured argument index is out of bounds") })?; @@ -2886,9 +2583,9 @@ impl VmHost for RuntimeVmHost { } fn set_function_name(&mut self, value: &Value, name_index: u32) -> Result<(), Error> { - let constant = usize::try_from(name_index) - .ok() - .and_then(|index| self.constants.get(index)) + let constant = self + .executable + .constant(name_index) .ok_or_else(|| Error::internal("function-name constant index is out of bounds"))?; let BytecodeConstant::Value(RawValue::String(name)) = constant else { return Err(Error::internal( @@ -2941,7 +2638,7 @@ impl VmHost for RuntimeVmHost { .ok_or_else(|| Error::internal("arguments creation has no current function"))?; let mapped_argument_count = self .actual_argument_count - .min(self.argument_definitions.len()); + .min(self.executable.argument_definitions.len()); let mut roots = Vec::with_capacity(self.actual_argument_count); for (index, binding) in self .arguments @@ -3038,8 +2735,12 @@ impl VmHost for RuntimeVmHost { } fn create_variable_environment(&mut self) -> Result { - if self.eval_variable_object_local.is_none() - && self.arg_eval_variable_object_local.is_none() + if self + .executable + .metadata + .eval_variable_object_local + .is_none() + && self.executable.arg_eval_variable_object_local.is_none() { return Err(Error::internal( "variable-environment creation has no authenticated local", @@ -3162,6 +2863,7 @@ impl VmHost for RuntimeVmHost { fn global_reference(&mut self, index: u16) -> Result { if self + .executable .closure_variables .get(usize::from(index)) .is_some_and(|descriptor| descriptor.kind.is_private()) @@ -3193,10 +2895,7 @@ impl VmHost for RuntimeVmHost { } fn create_regexp(&mut self, index: u32) -> Result { - let (pattern, program) = match usize::try_from(index) - .ok() - .and_then(|index| self.constants.get(index)) - { + let (pattern, program) = match self.executable.constant(index) { Some(BytecodeConstant::RegExp { pattern, program }) => { (pattern.clone(), program.clone()) } @@ -3312,10 +3011,7 @@ impl VmHost for RuntimeVmHost { name: u32, has_heritage: bool, ) -> Result { - let name = match usize::try_from(name) - .ok() - .and_then(|index| self.constants.get(index)) - { + let name = match self.executable.constant(name) { Some(BytecodeConstant::Value(RawValue::String(name))) => name.clone(), Some( BytecodeConstant::Value(_) @@ -3495,6 +3191,7 @@ impl VmHost for RuntimeVmHost { fn get_global_var(&mut self, index: u16, throw_if_missing: bool) -> Result { let descriptor = *self + .executable .closure_variables .get(usize::from(index)) .ok_or_else(|| Error::internal("global closure index is out of bounds"))?; @@ -3567,6 +3264,7 @@ impl VmHost for RuntimeVmHost { fn delete_global_var(&mut self, index: u16) -> Result { let descriptor = *self + .executable .closure_variables .get(usize::from(index)) .ok_or_else(|| Error::internal("global closure index is out of bounds"))?; @@ -3633,6 +3331,7 @@ impl VmHost for RuntimeVmHost { strict: bool, ) -> Result { let descriptor = *self + .executable .closure_variables .get(usize::from(index)) .ok_or_else(|| Error::internal("global closure index is out of bounds"))?; @@ -3939,7 +3638,7 @@ impl VmHost for RuntimeVmHost { // Final host-policy boundary: reject before filename observation, // Promise allocation, conversion side effects, or loader callbacks. self.runtime - .ensure_dynamic_import_bytecode_authorized(self.current_bytecode.as_ref()) + .ensure_dynamic_import_bytecode_authorized(self.executable.root()) .map_err(runtime_error_to_vm_error)?; // QuickJS snapshots the active Script/Module name before allocating // the caller-facing capability. A missing name is intentionally not @@ -4283,21 +3982,26 @@ impl VmHost for RuntimeVmHost { } fn get_local(&mut self, index: u16) -> Result { - let definition = self.local_definition(index)?; - if definition.kind == ClosureVariableKind::WithObject { - return Err(Error::internal( - "ordinary local read referenced a private with object", - )); - } - if definition.kind.is_private() { - return Err(Error::internal( - "ordinary local read referenced a private-name binding", - )); - } - if definition.is_lexical { - return Err(Error::internal( - "unchecked local read referenced a lexical definition", - )); + // Published instructions already authenticate this access mode. + // Synthetic host tests keep their checked internal-operation contract. + #[cfg(test)] + if self.executable.root().is_none() { + let definition = self.local_definition(index)?; + if definition.kind == ClosureVariableKind::WithObject { + return Err(Error::internal( + "ordinary local read referenced a private with object", + )); + } + if definition.kind.is_private() { + return Err(Error::internal( + "ordinary local read referenced a private-name binding", + )); + } + if definition.is_lexical { + return Err(Error::internal( + "unchecked local read referenced a lexical definition", + )); + } } let binding = self .locals @@ -4307,21 +4011,26 @@ impl VmHost for RuntimeVmHost { } fn put_local(&mut self, index: u16, value: Value) -> Result<(), Error> { - let definition = self.local_definition(index)?; - if definition.kind == ClosureVariableKind::WithObject { - return Err(Error::internal( - "ordinary local write referenced a private with object", - )); - } - if definition.kind.is_private() { - return Err(Error::internal( - "ordinary local write referenced a private-name binding", - )); - } - if definition.is_lexical { - return Err(Error::internal( - "unchecked local write referenced a lexical definition", - )); + // Published instructions already authenticate this access mode. + // Synthetic host tests keep their checked internal-operation contract. + #[cfg(test)] + if self.executable.root().is_none() { + let definition = self.local_definition(index)?; + if definition.kind == ClosureVariableKind::WithObject { + return Err(Error::internal( + "ordinary local write referenced a private with object", + )); + } + if definition.kind.is_private() { + return Err(Error::internal( + "ordinary local write referenced a private-name binding", + )); + } + if definition.is_lexical { + return Err(Error::internal( + "unchecked local write referenced a lexical definition", + )); + } } let binding = self .locals @@ -4331,11 +4040,16 @@ impl VmHost for RuntimeVmHost { } fn set_local_uninitialized(&mut self, index: u16) -> Result<(), Error> { - let definition = self.local_definition(index)?; - if !definition.is_lexical { - return Err(Error::internal( - "lexical scope entry referenced an ordinary local definition", - )); + // Published instructions already authenticate this access mode. + // Synthetic host tests keep their checked internal-operation contract. + #[cfg(test)] + if self.executable.root().is_none() { + let definition = self.local_definition(index)?; + if !definition.is_lexical { + return Err(Error::internal( + "lexical scope entry referenced an ordinary local definition", + )); + } } let reusable = self .reusable_captured_locals @@ -4379,16 +4093,21 @@ impl VmHost for RuntimeVmHost { } fn get_local_checked(&mut self, index: u16) -> Result { - let definition = self.local_definition(index)?; - if definition.kind.is_private() { - return Err(Error::internal( - "checked local read referenced a private-name binding", - )); - } - if !definition.is_lexical { - return Err(Error::internal( - "checked local read referenced an ordinary definition", - )); + // Published instructions already authenticate this access mode. + // Synthetic host tests keep their checked internal-operation contract. + #[cfg(test)] + if self.executable.root().is_none() { + let definition = self.local_definition(index)?; + if definition.kind.is_private() { + return Err(Error::internal( + "checked local read referenced a private-name binding", + )); + } + if !definition.is_lexical { + return Err(Error::internal( + "checked local read referenced an ordinary definition", + )); + } } let binding = self .locals @@ -4400,7 +4119,7 @@ impl VmHost for RuntimeVmHost { "checked local read reached a private-element frame cell", )), FrameBinding::Uninitialized => { - Err(self.local_lexical_uninitialized_error(definition.name)?) + Err(self.local_lexical_uninitialized_error(self.local_definition(index)?.name)?) } FrameBinding::Captured(root) => { let raw = self @@ -4408,7 +4127,7 @@ impl VmHost for RuntimeVmHost { .raw_var_ref_value(root) .map_err(runtime_error_to_vm_error)?; if matches!(raw, RawValue::Uninitialized) { - Err(self.local_lexical_uninitialized_error(definition.name)?) + Err(self.local_lexical_uninitialized_error(self.local_definition(index)?.name)?) } else { self.runtime .root_raw_value(&raw) @@ -4420,15 +4139,19 @@ impl VmHost for RuntimeVmHost { fn initialize_local(&mut self, index: u16, value: Value) -> Result<(), Error> { let definition = self.local_definition(index)?; - if definition.kind.is_private() { - return Err(Error::internal( - "ordinary lexical initialization referenced a private-name binding", - )); - } - if !definition.is_lexical && definition.kind != ClosureVariableKind::WithObject { - return Err(Error::internal( - "local initialization referenced an ordinary local definition", - )); + // Publication proves the operand mode; actual binding state stays dynamic. + #[cfg(test)] + if self.executable.root().is_none() { + if definition.kind.is_private() { + return Err(Error::internal( + "ordinary lexical initialization referenced a private-name binding", + )); + } + if !definition.is_lexical && definition.kind != ClosureVariableKind::WithObject { + return Err(Error::internal( + "local initialization referenced an ordinary local definition", + )); + } } if definition.kind == ClosureVariableKind::WithObject { let Value::Object(object) = &value else { @@ -4522,16 +4245,20 @@ impl VmHost for RuntimeVmHost { } fn put_local_checked(&mut self, index: u16, value: Value) -> Result<(), Error> { - let definition = self.local_definition(index)?; - if definition.kind.is_private() { - return Err(Error::internal( - "checked local write referenced a private-name binding", - )); - } - if !definition.is_lexical { - return Err(Error::internal( - "checked local write referenced an ordinary definition", - )); + // Publication proves the operand mode; actual binding state stays dynamic. + #[cfg(test)] + if self.executable.root().is_none() { + let definition = self.local_definition(index)?; + if definition.kind.is_private() { + return Err(Error::internal( + "checked local write referenced a private-name binding", + )); + } + if !definition.is_lexical { + return Err(Error::internal( + "checked local write referenced an ordinary definition", + )); + } } let binding = self .locals @@ -4539,8 +4266,12 @@ impl VmHost for RuntimeVmHost { .ok_or_else(|| Error::internal("local index is out of bounds"))?; match binding { FrameBinding::Direct(slot) => { - if definition.is_const { - return Err(self.lexical_read_only_error(definition.name)?); + #[cfg(test)] + if self.executable.root().is_none() { + let definition = self.executable.local_definitions[usize::from(index)]; + if definition.is_const { + return Err(self.lexical_read_only_error(definition.name)?); + } } *slot = value; Ok(()) @@ -4549,23 +4280,24 @@ impl VmHost for RuntimeVmHost { "checked local write reached a private-element frame cell", )), FrameBinding::Uninitialized => { - Err(self.local_lexical_uninitialized_error(definition.name)?) + Err(self.local_lexical_uninitialized_error(self.local_definition(index)?.name)?) } FrameBinding::Captured(root) => { - let cell = self - .runtime - .0 - .state - .borrow() - .heap - .var_ref(root.id()) - .map_err(|error| Error::internal(error.to_string()))? - .clone(); - if matches!(cell.value, RawValue::Uninitialized) { - return Err(self.local_lexical_uninitialized_error(definition.name)?); + let (uninitialized, is_const) = { + let state = self.runtime.0.state.borrow(); + let cell = state + .heap + .var_ref(root.id()) + .map_err(|error| Error::internal(error.to_string()))?; + (matches!(cell.value, RawValue::Uninitialized), cell.is_const) + }; + if uninitialized { + return Err( + self.local_lexical_uninitialized_error(self.local_definition(index)?.name)? + ); } - if cell.is_const { - return Err(self.lexical_read_only_error(definition.name)?); + if is_const { + return Err(self.lexical_read_only_error(self.local_definition(index)?.name)?); } self.runtime .write_var_ref(root, value) @@ -4576,10 +4308,14 @@ impl VmHost for RuntimeVmHost { fn close_local(&mut self, index: u16) -> Result<(), Error> { let definition = self.local_definition(index)?; - if !definition.is_lexical && definition.kind != ClosureVariableKind::WithObject { - return Err(Error::internal( - "CloseLocal referenced an ordinary local definition", - )); + // Publication proves the operand mode; actual binding state stays dynamic. + #[cfg(test)] + if self.executable.root().is_none() { + if !definition.is_lexical && definition.kind != ClosureVariableKind::WithObject { + return Err(Error::internal( + "CloseLocal referenced an ordinary local definition", + )); + } } let reusable = self .reusable_captured_locals @@ -4610,19 +4346,25 @@ impl VmHost for RuntimeVmHost { } fn get_var_ref(&mut self, index: u16) -> Result { - let descriptor = self - .closure_variables - .get(usize::from(index)) - .ok_or_else(|| Error::internal("closure variable index is out of bounds"))?; - if descriptor.kind.is_private() { - return Err(Error::internal( - "ordinary closure read referenced a private-name binding", - )); - } - if descriptor.is_lexical { - return Err(Error::internal( - "unchecked closure read referenced a lexical binding", - )); + // Published instructions already authenticate this access mode. + // Synthetic host tests keep their checked internal-operation contract. + #[cfg(test)] + if self.executable.root().is_none() { + let descriptor = self + .executable + .closure_variables + .get(usize::from(index)) + .ok_or_else(|| Error::internal("closure variable index is out of bounds"))?; + if descriptor.kind.is_private() { + return Err(Error::internal( + "ordinary closure read referenced a private-name binding", + )); + } + if descriptor.is_lexical { + return Err(Error::internal( + "unchecked closure read referenced a lexical binding", + )); + } } let root = self .closure_slots @@ -4634,19 +4376,25 @@ impl VmHost for RuntimeVmHost { } fn put_var_ref(&mut self, index: u16, value: Value) -> Result<(), Error> { - let descriptor = self - .closure_variables - .get(usize::from(index)) - .ok_or_else(|| Error::internal("closure variable index is out of bounds"))?; - if descriptor.kind.is_private() { - return Err(Error::internal( - "ordinary closure write referenced a private-name binding", - )); - } - if descriptor.is_lexical { - return Err(Error::internal( - "unchecked closure write referenced a lexical binding", - )); + // Published instructions already authenticate this access mode. + // Synthetic host tests keep their checked internal-operation contract. + #[cfg(test)] + if self.executable.root().is_none() { + let descriptor = self + .executable + .closure_variables + .get(usize::from(index)) + .ok_or_else(|| Error::internal("closure variable index is out of bounds"))?; + if descriptor.kind.is_private() { + return Err(Error::internal( + "ordinary closure write referenced a private-name binding", + )); + } + if descriptor.is_lexical { + return Err(Error::internal( + "unchecked closure write referenced a lexical binding", + )); + } } let root = self .closure_slots @@ -4658,19 +4406,25 @@ impl VmHost for RuntimeVmHost { } fn get_var_ref_checked(&mut self, index: u16) -> Result { - let descriptor = self - .closure_variables - .get(usize::from(index)) - .ok_or_else(|| Error::internal("closure variable index is out of bounds"))?; - if descriptor.kind.is_private() { - return Err(Error::internal( - "checked closure read referenced a private-name binding", - )); - } - if !descriptor.is_lexical { - return Err(Error::internal( - "checked closure read referenced an ordinary binding", - )); + // Published instructions already authenticate this access mode. + // Synthetic host tests keep their checked internal-operation contract. + #[cfg(test)] + if self.executable.root().is_none() { + let descriptor = self + .executable + .closure_variables + .get(usize::from(index)) + .ok_or_else(|| Error::internal("closure variable index is out of bounds"))?; + if descriptor.kind.is_private() { + return Err(Error::internal( + "checked closure read referenced a private-name binding", + )); + } + if !descriptor.is_lexical { + return Err(Error::internal( + "checked closure read referenced an ordinary binding", + )); + } } let root = self .closure_slots @@ -4681,6 +4435,11 @@ impl VmHost for RuntimeVmHost { .raw_var_ref_value(root) .map_err(runtime_error_to_vm_error)?; if matches!(raw, RawValue::Uninitialized) { + let descriptor = self + .executable + .closure_variables + .get(usize::from(index)) + .ok_or_else(|| Error::internal("closure variable index is out of bounds"))?; return Err(self.closure_lexical_uninitialized_error( descriptor.source, self.closure_name(index)?, @@ -4692,39 +4451,46 @@ impl VmHost for RuntimeVmHost { } fn put_var_ref_checked(&mut self, index: u16, value: Value) -> Result<(), Error> { - let descriptor = self - .closure_variables - .get(usize::from(index)) - .ok_or_else(|| Error::internal("closure variable index is out of bounds"))?; - if descriptor.kind.is_private() { - return Err(Error::internal( - "checked closure write referenced a private-name binding", - )); - } - if !descriptor.is_lexical { - return Err(Error::internal( - "checked closure write referenced an ordinary binding", - )); + // Publication proves the operand mode; actual binding state stays dynamic. + #[cfg(test)] + if self.executable.root().is_none() { + let descriptor = self + .executable + .closure_variables + .get(usize::from(index)) + .ok_or_else(|| Error::internal("closure variable index is out of bounds"))?; + if descriptor.kind.is_private() { + return Err(Error::internal( + "checked closure write referenced a private-name binding", + )); + } + if !descriptor.is_lexical { + return Err(Error::internal( + "checked closure write referenced an ordinary binding", + )); + } } let root = self .closure_slots .get(usize::from(index)) .ok_or_else(|| Error::internal("closure variable index is out of bounds"))?; - let cell = self - .runtime - .0 - .state - .borrow() - .heap - .var_ref(root.id()) - .map_err(|error| Error::internal(error.to_string()))? - .clone(); - let name = self.closure_name(index)?; - if matches!(cell.value, RawValue::Uninitialized) { - return Err(self.closure_lexical_uninitialized_error(descriptor.source, name)?); + let (uninitialized, is_const) = { + let state = self.runtime.0.state.borrow(); + let cell = state + .heap + .var_ref(root.id()) + .map_err(|error| Error::internal(error.to_string()))?; + (matches!(cell.value, RawValue::Uninitialized), cell.is_const) + }; + if uninitialized { + let descriptor = self.executable.closure_variables[usize::from(index)]; + return Err(self.closure_lexical_uninitialized_error( + descriptor.source, + self.closure_name(index)?, + )?); } - if cell.is_const { - return Err(self.lexical_read_only_error(name)?); + if is_const { + return Err(self.lexical_read_only_error(self.closure_name(index)?)?); } self.runtime .write_var_ref(root, value) @@ -4732,18 +4498,23 @@ impl VmHost for RuntimeVmHost { } fn initialize_var_ref(&mut self, index: u16, value: Value) -> Result<(), Error> { - let descriptor = self - .closure_variables - .get(usize::from(index)) - .copied() - .ok_or_else(|| Error::internal("closure variable index is out of bounds"))?; - if descriptor.source != ClosureSource::ModuleDeclaration - || !descriptor.is_lexical - || descriptor.kind != ClosureVariableKind::Normal - { - return Err(Error::internal( - "module lexical initialization referenced a non-declaration binding", - )); + // Publication proves the operand mode; actual binding state stays dynamic. + #[cfg(test)] + if self.executable.root().is_none() { + let descriptor = self + .executable + .closure_variables + .get(usize::from(index)) + .copied() + .ok_or_else(|| Error::internal("closure variable index is out of bounds"))?; + if descriptor.source != ClosureSource::ModuleDeclaration + || !descriptor.is_lexical + || descriptor.kind != ClosureVariableKind::Normal + { + return Err(Error::internal( + "module lexical initialization referenced a non-declaration binding", + )); + } } let root = self .closure_slots @@ -4761,6 +4532,7 @@ impl VmHost for RuntimeVmHost { value: Value, ) -> Result<(), Error> { let descriptor = self + .executable .closure_variables .get(usize::from(index)) .copied() @@ -4788,6 +4560,7 @@ impl VmHost for RuntimeVmHost { fn initialize_derived_var_ref(&mut self, index: u16, value: Value) -> Result<(), Error> { let descriptor = self + .executable .closure_variables .get(usize::from(index)) .copied() @@ -4904,6 +4677,27 @@ impl VmHost for RuntimeVmHost { #[cfg(test)] mod tests { + #[test] + fn unpublished_host_cannot_enter_published_execution() { + let runtime = Runtime::new(); + let context = runtime.new_context(); + let mut host = RuntimeVmHost::empty_for_test(runtime.clone(), context.realm); + let error = Vm::new() + .execute_published( + CallInput { + this_value: Value::Undefined, + new_target: Value::Undefined, + callee_global: runtime.global_object_for_realm(context.realm).unwrap(), + }, + &mut host, + ) + .unwrap_err(); + assert_eq!( + error.message(), + "unpublished host cannot execute published code" + ); + } + use super::*; use crate::engine::api::Context; use crate::engine::code::bytecode::EvalVariableSource; @@ -4932,17 +4726,17 @@ mod tests { ) -> (RuntimeVmHost, ObjectRef) { let object = runtime.new_object(None).unwrap(); let mut host = RuntimeVmHost::empty_for_test(runtime, realm); - host.constants = Rc::from([BytecodeConstant::Value(RawValue::String( + host.executable.constants = Rc::from([BytecodeConstant::Value(RawValue::String( JsString::from_static("added"), ))]); - host.local_definitions = Rc::from([VariableDefinition { + host.executable.local_definitions = Rc::from([VariableDefinition { name: None, is_lexical: false, is_const: false, is_parameter_initializer: false, kind, }]); - host.eval_variable_object_local = authenticated_local; + host.executable.metadata.eval_variable_object_local = authenticated_local; host.locals = vec![FrameBinding::Direct(Value::Object(object.clone()))]; host.reusable_captured_locals = vec![false]; (host, object) @@ -5030,7 +4824,7 @@ mod tests { let second = runtime.new_object(None).unwrap(); let mut local_host = RuntimeVmHost::empty_for_test(runtime.clone(), context.realm); - local_host.local_definitions = Rc::from([derived_this_definition()]); + local_host.executable.local_definitions = Rc::from([derived_this_definition()]); local_host.locals = vec![FrameBinding::Uninitialized]; local_host.reusable_captured_locals = vec![false]; local_host @@ -5051,7 +4845,7 @@ mod tests { .new_uninitialized_captured_var_ref(true, false, ClosureVariableKind::Normal) .unwrap(); let mut closure_host = RuntimeVmHost::empty_for_test(runtime.clone(), context.realm); - closure_host.closure_variables = Rc::from([ClosureVariable { + closure_host.executable.closure_variables = Rc::from([ClosureVariable { source: ClosureSource::ParentLocal(0), name: ClosureVariableName::Atom(this_key.atom()), is_lexical: true, @@ -5083,7 +4877,7 @@ mod tests { let mut host = RuntimeVmHost::empty_for_test(runtime.clone(), defining.realm); host.caller_realm = caller.realm; - host.local_definitions = Rc::from([derived_this_definition()]); + host.executable.local_definitions = Rc::from([derived_this_definition()]); host.locals = vec![FrameBinding::Uninitialized]; host.reusable_captured_locals = vec![false]; @@ -5270,10 +5064,10 @@ mod tests { ) .unwrap(); let mut closure = RuntimeVmHost::empty_for_test(runtime, context.realm); - closure.constants = Rc::from([BytecodeConstant::Value(RawValue::String( + closure.executable.constants = Rc::from([BytecodeConstant::Value(RawValue::String( JsString::from_static("added"), ))]); - closure.closure_variables = Rc::from([ClosureVariable { + closure.executable.closure_variables = Rc::from([ClosureVariable { source: ClosureSource::ParentClosure(0), name: ClosureVariableName::None, is_lexical: false, @@ -5299,7 +5093,7 @@ mod tests { ClosureVariableKind::EvalVariableObject, ) .unwrap(); - closure.closure_variables = Rc::from([ClosureVariable { + closure.executable.closure_variables = Rc::from([ClosureVariable { source: ClosureSource::ParentClosure(0), name: ClosureVariableName::None, is_lexical: false, @@ -5335,7 +5129,7 @@ mod tests { ) .unwrap(); let mut host = RuntimeVmHost::empty_for_test(runtime, context.realm); - host.local_definitions = Rc::from([VariableDefinition { + host.executable.local_definitions = Rc::from([VariableDefinition { name: Some(Atom::from_raw(71)), is_lexical: false, is_const: false, @@ -5368,7 +5162,7 @@ mod tests { "with-object initialization did not receive an Object" ); - host.local_definitions = Rc::from([VariableDefinition { + host.executable.local_definitions = Rc::from([VariableDefinition { name: None, is_lexical: false, is_const: false, @@ -5561,150 +5355,4 @@ mod tests { "object-rest copy target was not an Object" ); } - - #[test] - fn direct_eval_environment_authenticates_super_capability_before_materialization() { - let runtime = Runtime::new(); - let context = runtime.new_context(); - let host = RuntimeVmHost::empty_for_test(runtime, context.realm); - let mut environment = EvalEnvironment:: { - scopes: Box::new([]), - variable_environment: EvalVariableEnvironment::Global, - caller_strict: false, - super_call_allowed: false, - super_allowed: false, - }; - - let error = host - .validate_eval_environment( - &environment, - false, - FunctionMetadata { - super_allowed: true, - ..FunctionMetadata::default() - }, - ) - .unwrap_err(); - assert_eq!( - error.message(), - "eval environment super capability disagrees with caller bytecode" - ); - - environment.super_call_allowed = true; - let error = host - .validate_eval_environment(&environment, false, FunctionMetadata::default()) - .unwrap_err(); - assert_eq!( - error.message(), - "eval environment permits super() without SuperProperty" - ); - } - - #[test] - fn strict_script_global_eval_anchor_is_not_valid_for_functions_or_eval_roots() { - let runtime = Runtime::new(); - let context = runtime.new_context(); - let host = RuntimeVmHost::empty_for_test(runtime, context.realm); - let strict_script = EvalEnvironment:: { - scopes: vec![ - crate::engine::code::function::metadata::EvalScope { - kind: crate::engine::code::function::metadata::EvalScopeKind::ProgramBody, - bindings: Box::new([]), - }, - crate::engine::code::function::metadata::EvalScope { - kind: crate::engine::code::function::metadata::EvalScopeKind::FunctionRoot, - bindings: Box::new([]), - }, - ] - .into_boxed_slice(), - variable_environment: EvalVariableEnvironment::Global, - caller_strict: true, - super_call_allowed: false, - super_allowed: false, - }; - host.validate_eval_environment( - &strict_script, - true, - FunctionMetadata { - strict: true, - ..FunctionMetadata::default() - }, - ) - .unwrap(); - - let mut strict_eval_local = strict_script.clone(); - strict_eval_local.variable_environment = EvalVariableEnvironment::StrictLocal(1); - assert_eq!( - host.validate_eval_environment( - &strict_eval_local, - true, - FunctionMetadata { - strict: true, - ..FunctionMetadata::default() - }, - ) - .unwrap_err() - .message(), - "authored Script eval environment used a non-canonical strict-local target" - ); - host.validate_eval_environment( - &strict_eval_local, - true, - FunctionMetadata { - strict: true, - eval_kind: crate::engine::code::function::metadata::EvalKind::Direct, - ..FunctionMetadata::default() - }, - ) - .unwrap(); - - let mut strict_function = strict_script.clone(); - strict_function.scopes = vec![ - crate::engine::code::function::metadata::EvalScope { - kind: crate::engine::code::function::metadata::EvalScopeKind::FunctionBody, - bindings: Box::new([]), - }, - crate::engine::code::function::metadata::EvalScope { - kind: crate::engine::code::function::metadata::EvalScopeKind::FunctionRoot, - bindings: Box::new([]), - }, - crate::engine::code::function::metadata::EvalScope { - kind: crate::engine::code::function::metadata::EvalScopeKind::ProgramBody, - bindings: Box::new([]), - }, - crate::engine::code::function::metadata::EvalScope { - kind: crate::engine::code::function::metadata::EvalScopeKind::FunctionRoot, - bindings: Box::new([]), - }, - ] - .into_boxed_slice(); - assert_eq!( - host.validate_eval_environment( - &strict_function, - true, - FunctionMetadata { - strict: true, - ..FunctionMetadata::default() - }, - ) - .unwrap_err() - .message(), - "global eval variable environment escaped an authored Script root" - ); - - assert_eq!( - host.validate_eval_environment( - &strict_script, - true, - FunctionMetadata { - strict: true, - eval_kind: crate::engine::code::function::metadata::EvalKind::Direct, - ..FunctionMetadata::default() - }, - ) - .unwrap_err() - .message(), - "global eval variable environment escaped an authored Script root" - ); - } } diff --git a/src/engine/vm/host_bridge/README.md b/src/engine/vm/host_bridge/README.md index 3c1cea71..cbb0c018 100644 --- a/src/engine/vm/host_bridge/README.md +++ b/src/engine/vm/host_bridge/README.md @@ -7,5 +7,11 @@ ## 文件与子目录 - [dynamic_environment.rs](dynamic_environment.rs):Authenticated object-environment operations used by `with` and sloppy eval.。 +- [eval_validation.rs](eval_validation.rs):eval 的实际帧槽检查;发布拓扑不在执行时重复验证。 - [private_elements.rs](private_elements.rs):VM adapter for authenticated class-private element instructions.。 - [super_property.rs](super_property.rs):QuickJS HomeObject and `super` property bridges for one VM frame.。 + +`prepare_direct_eval_environment` 只接受已发布 snapshot 的环境。发布器验证 +拓扑、名字、flags、变量目标和 super 能力;执行前仍检查 caller strictness、 +实际槽与 closure cell 元数据。校验不执行用户代码,不跨编译或捕获保留 +Runtime 借用;本地/参数槽的实际捕获由 `capture_frame_binding` 完成并验证。 diff --git a/src/engine/vm/host_bridge/dynamic_environment.rs b/src/engine/vm/host_bridge/dynamic_environment.rs index 421b902a..152004bd 100644 --- a/src/engine/vm/host_bridge/dynamic_environment.rs +++ b/src/engine/vm/host_bridge/dynamic_environment.rs @@ -58,6 +58,7 @@ impl RuntimeVmHost { } WithObjectSource::Closure(index) => { let descriptor = self + .executable .closure_variables .get(usize::from(index)) .copied() @@ -227,6 +228,7 @@ impl RuntimeVmHost { /// bytecode was published, and that live lexical VarRef must win. pub(crate) fn global_reference_impl(&mut self, index: u16) -> Result { let descriptor = self + .executable .closure_variables .get(usize::from(index)) .copied() @@ -434,12 +436,12 @@ mod tests { names: &[&'static str], ) -> RuntimeVmHost { let mut host = RuntimeVmHost::empty_for_test(runtime, realm); - host.constants = names + host.executable.constants = names .iter() .map(|name| BytecodeConstant::Value(RawValue::String(JsString::from_static(name)))) .collect::>() .into(); - host.local_definitions = Rc::from([VariableDefinition { + host.executable.local_definitions = Rc::from([VariableDefinition { name: Some(Atom::from_raw(71)), is_lexical: false, is_const: false, @@ -459,10 +461,10 @@ mod tests { let key = runtime.intern_property_key(name).unwrap(); let root = runtime.resolve_global_var(realm, key.atom()).unwrap(); let mut host = RuntimeVmHost::empty_for_test(runtime.clone(), realm); - host.constants = Rc::from([BytecodeConstant::Value(RawValue::String( + host.executable.constants = Rc::from([BytecodeConstant::Value(RawValue::String( JsString::from_static(name), ))]); - host.closure_variables = Rc::from([ClosureVariable { + host.executable.closure_variables = Rc::from([ClosureVariable { source: ClosureSource::Global, name: ClosureVariableName::Atom(key.atom()), is_lexical: false, @@ -555,7 +557,7 @@ mod tests { ClosureVariableKind::EvalVariableObject, &names, ); - eval_host.eval_variable_object_local = Some(0); + eval_host.executable.metadata.eval_variable_object_local = Some(0); let source = DynamicEnvironmentSource::Eval(EvalVariableSource::Local(0)); assert_eq!( eval_host.has_dynamic_binding_impl(source, 3).unwrap(), diff --git a/src/engine/vm/host_bridge/eval_validation.rs b/src/engine/vm/host_bridge/eval_validation.rs new file mode 100644 index 00000000..1dd76e4d --- /dev/null +++ b/src/engine/vm/host_bridge/eval_validation.rs @@ -0,0 +1,121 @@ +//! Eval publication owns topology, names and access modes. Execution validates +//! the caller and actual frame slots before compilation can create captures. +use super::*; + +impl RuntimeVmHost { + pub(super) fn validate_eval_frame_bindings( + &self, + environment: &EvalEnvironment, + caller_strict: bool, + ) -> Result<(), Error> { + if environment.caller_strict != caller_strict { + return Err(Error::internal( + "eval environment caller strictness disagrees with its bytecode frame", + )); + } + for scope in &environment.scopes { + for binding in &scope.bindings { + match binding.source { + EvalBindingSource::Local(index) => { + self.locals.get(usize::from(index)).ok_or_else(|| { + Error::internal("eval local binding index is out of bounds") + })?; + } + EvalBindingSource::Argument(index) => { + self.arguments.get(usize::from(index)).ok_or_else(|| { + Error::internal("eval argument binding index is out of bounds") + })?; + } + EvalBindingSource::Closure(index) => { + let descriptor = *self + .executable + .closure_variables + .get(usize::from(index)) + .ok_or_else(|| { + Error::internal("eval closure binding index is out of bounds") + })?; + let root = self.closure_slots.get(usize::from(index)).ok_or_else(|| { + Error::internal("eval closure slot index is out of bounds") + })?; + self.runtime + .validate_var_ref_metadata(root, descriptor) + .map_err(|error| Error::internal(error.to_string()))?; + } + } + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::engine::code::function::metadata::{ + EvalScope, EvalScopeKind, EvalVariableEnvironment, + }; + + #[test] + fn eval_frame_validation_rejects_missing_slots_and_wrong_cell_metadata() { + let runtime = Runtime::new(); + let context = runtime.new_context(); + let key = runtime.intern_property_key("binding").unwrap(); + let mut host = RuntimeVmHost::empty_for_test(runtime.clone(), context.realm); + let descriptor = ClosureVariable { + source: ClosureSource::ParentLocal(0), + name: ClosureVariableName::None, + is_lexical: true, + is_const: false, + kind: ClosureVariableKind::Normal, + }; + host.executable.closure_variables = Rc::from([descriptor]); + let mut environment = EvalEnvironment { + scopes: vec![EvalScope { + kind: EvalScopeKind::FunctionRoot, + bindings: vec![EvalBinding { + name: key.atom(), + source: EvalBindingSource::Closure(0), + is_lexical: true, + is_const: false, + kind: ClosureVariableKind::Normal, + is_catch_parameter: false, + }] + .into_boxed_slice(), + }] + .into_boxed_slice(), + variable_environment: EvalVariableEnvironment::StrictLocal(0), + caller_strict: true, + super_call_allowed: false, + super_allowed: false, + }; + assert!( + host.validate_eval_frame_bindings(&environment, false) + .is_err() + ); + assert!( + host.validate_eval_frame_bindings(&environment, true) + .is_err() + ); + host.closure_slots.push( + runtime + .new_var_ref(Value::Int(1), false, false, ClosureVariableKind::Normal) + .unwrap(), + ); + assert!( + host.validate_eval_frame_bindings(&environment, true) + .is_err() + ); + host.closure_slots[0] = runtime + .new_var_ref(Value::Int(1), true, false, ClosureVariableKind::Normal) + .unwrap(); + host.validate_eval_frame_bindings(&environment, true) + .unwrap(); + for source in [EvalBindingSource::Local(0), EvalBindingSource::Argument(0)] { + environment.scopes[0].bindings[0].source = source; + assert!( + host.validate_eval_frame_bindings(&environment, true) + .is_err() + ); + } + } +} diff --git a/src/engine/vm/host_bridge/private_elements.rs b/src/engine/vm/host_bridge/private_elements.rs index 80931b9c..7069c986 100644 --- a/src/engine/vm/host_bridge/private_elements.rs +++ b/src/engine/vm/host_bridge/private_elements.rs @@ -238,6 +238,7 @@ impl RuntimeVmHost { } PrivateNameSource::Closure(index) => { let descriptor = self + .executable .closure_variables .get(usize::from(index)) .copied() @@ -279,6 +280,7 @@ impl RuntimeVmHost { } PrivateNameSource::Closure(index) => { let descriptor = self + .executable .closure_variables .get(usize::from(index)) .copied() @@ -335,6 +337,7 @@ impl RuntimeVmHost { } PrivateNameSource::Closure(index) => { let descriptor = self + .executable .closure_variables .get(usize::from(index)) .copied() @@ -367,12 +370,13 @@ impl RuntimeVmHost { Ok(match source { PrivateNameSource::Local(index) => self.local_definition(index)?.name, PrivateNameSource::Closure(index) => { - let descriptor = - self.closure_variables - .get(usize::from(index)) - .ok_or_else(|| { - Error::internal("private-element closure index is out of bounds") - })?; + let descriptor = self + .executable + .closure_variables + .get(usize::from(index)) + .ok_or_else(|| { + Error::internal("private-element closure index is out of bounds") + })?; match descriptor.name { ClosureVariableName::Atom(name) => Some(name), ClosureVariableName::None | ClosureVariableName::Constant(_) => None, diff --git a/src/engine/vm/mod.rs b/src/engine/vm/mod.rs index dac7d8fb..74ee75ff 100644 --- a/src/engine/vm/mod.rs +++ b/src/engine/vm/mod.rs @@ -63,3 +63,6 @@ mod dispatch; mod frame_execution; mod numeric_execution; mod unwind; + +#[cfg(test)] +mod published_execution_tests; diff --git a/src/engine/vm/protocol.rs b/src/engine/vm/protocol.rs index 9ef0e0fd..68f95684 100644 --- a/src/engine/vm/protocol.rs +++ b/src/engine/vm/protocol.rs @@ -28,18 +28,21 @@ pub(crate) struct DirectEvalInvocation { pub caller_strict: bool, } -pub(crate) struct CallInput<'a> { - pub code: &'a [Instruction], - pub metadata: FunctionMetadata, - pub caller_realm: ContextId, - pub callee_realm: ContextId, - pub current_function: ObjectRef, +pub(crate) struct CallInput { pub this_value: Value, pub new_target: Value, pub callee_global: ObjectRef, } pub(crate) trait VmHost { + /// Resolve an immediate IfTrue/IfFalse/Goto operand. General hosts + /// validate targets here; a host paired with published code may reuse its + /// verifier's bound. Dynamic return/unwind/resume PCs never use this hook. + #[inline] + fn static_branch_target(&self, target: u32, code_len: usize) -> Result { + super::activation::checked_target(target, code_len) + } + fn update_active_bytecode_pc(&mut self, pc: BytecodePc) -> Result<(), Error>; /// Attach a QuickJS-style backtrace before the active frame can unwind. /// Detached execution has no realm heap and therefore implements this as @@ -617,68 +620,22 @@ impl Vm { /// frame, regressing the proven two-MiB recursion boundary. pub(crate) fn execute_published( &mut self, - input: CallInput<'_>, - host: &mut impl VmHost, + input: CallInput, + host: &mut host_bridge::RuntimeVmHost, ) -> Result { - let CallInput { - code, - metadata, - caller_realm, - callee_realm, - current_function, - this_value, - new_target, - callee_global, - } = input; - if host.closure_count() != usize::from(metadata.closure_count) { - return Err(Error::internal( - "function object closure slot count does not match bytecode metadata", - )); - } - VmActivation::new_in_realm( - metadata, - caller_realm, - callee_realm, - current_function, - this_value, - new_target, - callee_global, - ) - .execute(code, host) + let (code, activation) = host.new_activation(input)?; + activation.execute(&code, host) } /// Start an immutable published bytecode activation and allow it to /// transfer ownership at a generator or async-function suspension point. pub(crate) fn start_published( &mut self, - input: CallInput<'_>, - host: &mut impl VmHost, + input: CallInput, + host: &mut host_bridge::RuntimeVmHost, ) -> Result { - let CallInput { - code, - metadata, - caller_realm, - callee_realm, - current_function, - this_value, - new_target, - callee_global, - } = input; - if host.closure_count() != usize::from(metadata.closure_count) { - return Err(Error::internal( - "function object closure slot count does not match bytecode metadata", - )); - } - VmActivation::new_in_realm( - metadata, - caller_realm, - callee_realm, - current_function, - this_value, - new_target, - callee_global, - ) - .run(code, host) + let (code, activation) = host.new_activation(input)?; + activation.run(&code, host) } /// Continue past a generator's hidden initial-yield barrier. No resume diff --git a/src/engine/vm/published_execution_tests.rs b/src/engine/vm/published_execution_tests.rs new file mode 100644 index 00000000..35819a12 --- /dev/null +++ b/src/engine/vm/published_execution_tests.rs @@ -0,0 +1,366 @@ +//! Published execution must retain dynamic binding transitions after static +//! access-mode checks move to publication. These tests use the real compiler, +//! publisher and runtime host, not synthetic instruction fixtures. +use crate::engine::api::runtime::Runtime; +use crate::engine::value::Value; + +#[test] +fn invalid_binding_modes_are_rejected_before_creating_a_runtime_frame() { + use crate::engine::code::bytecode::Instruction; + use crate::engine::code::function::metadata::FunctionMetadata; + use crate::engine::code::function::{UnlinkedFunction, UnlinkedVariableDefinition}; + use crate::engine::value::JsString; + + let cases = [ + ( + Instruction::GetLocal(0), + UnlinkedVariableDefinition::lexical(Some(JsString::from_static("n")), false), + "unchecked local opcode referenced a lexical definition", + ), + ( + Instruction::GetLocalCheck(0), + UnlinkedVariableDefinition::ordinary(None), + "checked lexical-local opcode referenced an ordinary definition", + ), + ]; + for (instruction, definition, expected) in cases { + let runtime = Runtime::new(); + let context = runtime.new_context(); + let before = runtime.heap_counts().function_bytecode_nodes; + let function = UnlinkedFunction::fixture( + vec![instruction, Instruction::Return], + vec![], + FunctionMetadata { + local_count: 1, + max_stack: 1, + ..FunctionMetadata::default() + }, + ) + .with_fixture_definitions(vec![], vec![definition]); + let error = runtime + .publish_unlinked_function(context.realm, function) + .unwrap_err(); + assert!(error.to_string().contains(expected), "{error}"); + assert_eq!(runtime.heap_counts().function_bytecode_nodes, before); + } +} + +#[test] +fn published_bindings_keep_capture_eval_and_argument_aliases_live() { + for (source, expected) in [ + ( + "(function(){var n=2; n+=3; var f=()=>n; n+=7; return f();})()", + 12, + ), + ( + "(function(){var n=2; eval('var f=()=>n'); n=7; return f();})()", + 7, + ), + ( + "(function(a){arguments[0]=7; return a+arguments.length;})(1,2,3)", + 10, + ), + ( + "(function(a){'use strict'; arguments[0]=7; return a+arguments.length;})(1,2,3)", + 4, + ), + ] { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + assert!( + matches!(context.eval(source).unwrap(), Value::Int(n) if n == expected), + "{source}" + ); + } +} + +#[test] +fn published_lexical_reads_preserve_tdz_then_observe_initialization() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let value = context + .eval( + "(function(){ + var f=()=>n; var caught=0; + try { f(); } catch(e) { if (!(e instanceof ReferenceError)) throw e; caught=1; } + let n=41; return f()+caught; + })()", + ) + .unwrap(); + assert!(matches!(value, Value::Int(42))); +} + +#[test] +fn published_resume_keeps_captured_cells_live_through_finally() { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let value = context + .eval( + "(function(){ + function* g(){let n=2; let f=()=>n; yield f; try {yield ++n;} finally {n=9;}} + var it=g(); var f=it.next().value; + if(f()!==2 || it.next().value!==3) throw Error('resume'); + it.return(0); return f()+1; + })()", + ) + .unwrap(); + assert!(matches!(value, Value::Int(10))); +} + +#[test] +fn published_lexical_writes_preserve_tdz_const_and_iteration_lifetimes() { + for source in [ + "(function(){let x=1; x=4; return x===4;})()", + "(function(){let x=1; const set=v=>x=v; set(4); return x===4;})()", + "(function(){const set=v=>x=v; let caught=false; try{set(4)}catch(e){caught=e instanceof ReferenceError} let x=1; set(5); return caught&&x===5;})()", + "(function(){let caught=false; try{x=4}catch(e){caught=e instanceof ReferenceError} let x=1; return caught&&x===1;})()", + "(function(){const x=1; try{x=4}catch(e){return e instanceof TypeError&&x===1} return false;})()", + "(function(){const x=1; const set=v=>x=v; try{set(4)}catch(e){return e instanceof TypeError&&x===1} return false;})()", + "(function(){let fs=[]; for(let i=0;i<3;i++){let x=i; fs.push(()=>++x)} return fs[0]()===1&&fs[1]()===2&&fs[2]()===3&&fs[0]()===2;})()", + "(function(){let x=1; eval('x=4'); return x===4;})()", + ] { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + assert!( + matches!(context.eval(source).unwrap(), Value::Bool(true)), + "{source}" + ); + } +} + +#[test] +fn published_eval_reuses_topology_but_observes_live_scope_and_super() { + for source in [ + "(function(a){let x=2; const f=()=>eval('x+=a'); f(); return x===5;})(3)", + "(function(){let x=1; let o={x:4}; with(o){eval('x+=2')} return x===1&&o.x===6;})()", + "(function(){let fs=[]; for(let i=0;i<3;i++){let x=i; fs.push(eval('()=>++x'))} return fs[0]()===1&&fs[1]()===2&&fs[2]()===3;})()", + "(function(){class A{m(){return 2}} class B extends A{m(){return eval('super.m()')+1}} return new B().m()===3;})()", + "(function(){class A{constructor(){this.n=3}} class B extends A{constructor(){eval('super()')}} return new B().n===3;})()", + "(function(){let x=1; eval(\"eval('x=4')\"); return x===4;})()", + ] { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + assert!( + matches!(context.eval(source).unwrap(), Value::Bool(true)), + "{source}" + ); + } +} + +#[test] +fn instruction_fetch_rejects_invalid_pc_before_advancing_it() { + use super::{DetachedHost, VmActivation}; + use crate::engine::code::bytecode::{DetachedBytecode, Instruction}; + let function = DetachedBytecode:: { + code: vec![Instruction::Nop], + constants: vec![], + local_count: 0, + max_stack: 0, + }; + for pc in [0, 1, usize::MAX] { + let mut host = DetachedHost::new(&function); + let mut activation = VmActivation::new(0); + activation.pc = pc; + let error = activation + .execute_inner(&function.code, &mut host) + .err() + .expect("invalid PC must fail at instruction fetch"); + assert_eq!(error.message(), "bytecode ended without return"); + assert_eq!(activation.pc, if pc == 0 { 1 } else { pc }); + } +} + +#[test] +fn repeated_closure_creation_reuses_cells_without_erasing_their_metadata() { + for source in [ + "(function(){let x=1; let a=()=>x; let b=()=>++x; return b()===2&&a()===2;})()", + "(function(){let fs=[]; for(let i=0;i<3;i++){let x=i; fs.push(()=>x,()=>++x)} return fs[1]()===1&&fs[0]()===1&&fs[3]()===2&&fs[2]()===2;})()", + "(function named(){let a=()=>named; let b=eval('()=>named'); named=1; return a()===b()&&typeof a()==='function';})()", + "(function(){class A{#x=3; read(){return [()=>this.#x,()=>this.#x]}} let fs=new A().read(); return fs[0]()===3&&fs[1]()===3;})()", + ] { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + assert!( + matches!(context.eval(source).unwrap(), Value::Bool(true)), + "{source}" + ); + } +} + +#[test] +fn static_branch_targets_remain_checked_at_untrusted_boundaries() { + use super::{DetachedHost, VmActivation}; + use crate::engine::code::bytecode::{DetachedBytecode, Instruction}; + use crate::engine::code::function::UnlinkedFunction; + use crate::engine::code::function::metadata::FunctionMetadata; + + // Even an unreachable malformed operand is rejected by publication. + let runtime = Runtime::new(); + let context = runtime.new_context(); + let draft = UnlinkedFunction::fixture( + vec![ + Instruction::Undefined, + Instruction::Return, + Instruction::Goto(u32::MAX), + ], + vec![], + FunctionMetadata { + max_stack: 1, + ..FunctionMetadata::default() + }, + ); + let error = runtime + .publish_unlinked_function(context.realm, draft) + .unwrap_err(); + assert!( + error.to_string().contains("jump target is out of bounds"), + "{error}" + ); + + for branch in [ + Instruction::Goto(2), + Instruction::IfTrue(2), + Instruction::IfFalse(2), + ] { + let function = DetachedBytecode:: { + code: vec![branch.clone()], + constants: vec![], + local_count: 0, + max_stack: 1, + }; + let mut host = DetachedHost::new(&function); + let mut activation = VmActivation::new(1); + activation + .stack + .push(Value::Bool(!matches!(branch, Instruction::IfFalse(_)))); + let error = activation + .execute_inner(&function.code, &mut host) + .err() + .unwrap(); + assert_eq!(error.message(), "jump target is out of bounds"); + } +} + +#[test] +fn synthetic_runtime_host_checks_static_targets() { + use super::VmHost; + use super::host_bridge::RuntimeVmHost; + let runtime = Runtime::new(); + let context = runtime.new_context(); + let host = RuntimeVmHost::empty_for_test(runtime.clone(), context.realm); + assert_eq!(host.static_branch_target(0, 1).unwrap(), 0); + for (target, length) in [(0, 0), (1, 1), (u32::MAX, 1)] { + assert_eq!( + host.static_branch_target(target, length) + .unwrap_err() + .message(), + "jump target is out of bounds" + ); + } +} + +#[test] +fn published_static_branches_preserve_resume_finally_and_loop_targets() { + for source in [ + "(function(){let n=0; do {++n} while(n<3); while(n>1){--n} return n===1;})()", + "(function(){let n=0; for(let i=0;i<5;i++){try{if(i%2)continue; n+=i;}finally{++n;}}return n===11;})()", + "(function(){function* g(){let n=0;try{while(n<3){yield n++;}}finally{n=9;}return n;}let it=g();return it.next().value===0&&it.next().value===1&&it.return(7).value===7;})()", + ] { + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + assert!( + matches!(context.eval(source).unwrap(), Value::Bool(true)), + "{source}" + ); + } +} + +#[test] +fn paired_stack_reads_preserve_order_and_root_cleanup_on_every_length() { + use super::VmActivation; + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let mut activation = VmActivation::new(3); + assert_eq!( + activation.pop_pair().unwrap_err().message(), + "bytecode stack underflow" + ); + let single = context.new_object().unwrap(); + let single_id = single.object_id(); + activation.stack.push(Value::Object(single)); + assert_eq!( + activation.pop_pair().unwrap_err().message(), + "bytecode stack underflow" + ); + assert!(activation.stack.is_empty()); + assert!(runtime.0.state.borrow().heap.object(single_id).is_err()); + + let left = context.new_object().unwrap(); + let right = context.new_object().unwrap(); + let left_id = left.object_id(); + let right_id = right.object_id(); + activation + .stack + .extend([Value::Int(7), Value::Object(left), Value::Object(right)]); + let (left, right) = activation.pop_pair().unwrap(); + assert_eq!(activation.stack, [Value::Int(7)]); + assert!(matches!(&left, Value::Object(root) if root.object_id() == left_id)); + assert!(matches!(&right, Value::Object(root) if root.object_id() == right_id)); + assert!(runtime.0.state.borrow().heap.object(left_id).is_ok()); + assert!(runtime.0.state.borrow().heap.object(right_id).is_ok()); + drop((left, right)); + assert!(runtime.0.state.borrow().heap.object(left_id).is_err()); + assert!(runtime.0.state.borrow().heap.object(right_id).is_err()); +} + +#[test] +fn depth_stack_reads_select_from_the_tail_and_retain_roots() { + use super::VmActivation; + let runtime = Runtime::new(); + let mut context = runtime.new_context(); + let object = context.new_object().unwrap(); + let id = object.object_id(); + let mut activation = VmActivation::new(2); + activation + .stack + .extend([Value::Object(object), Value::Int(9)]); + assert_eq!(activation.clone_at_depth(0).unwrap(), Value::Int(9)); + let saved = activation.clone_at_depth(1).unwrap(); + for depth in [2, u8::MAX] { + assert_eq!( + activation.clone_at_depth(depth).unwrap_err().message(), + "bytecode stack depth operand is out of bounds" + ); + assert_eq!(activation.stack.len(), 2); + } + drop(activation); + assert!(runtime.0.state.borrow().heap.object(id).is_ok()); + drop(saved); + assert!(runtime.0.state.borrow().heap.object(id).is_err()); +} + +#[test] +fn depth_stack_reads_cover_the_entire_bytecode_operand_range() { + use super::VmActivation; + for length in [0, 1, 255, 256, 257] { + let mut activation = VmActivation::new(length); + activation + .stack + .extend((0..length).map(|index| Value::Int(index as i32))); + for depth in 0..=u8::MAX { + let result = activation.clone_at_depth(depth); + if usize::from(depth) < length { + assert_eq!( + result.unwrap(), + Value::Int((length - usize::from(depth) - 1) as i32) + ); + } else { + assert_eq!( + result.unwrap_err().message(), + "bytecode stack depth operand is out of bounds" + ); + } + assert_eq!(activation.stack.len(), length); + } + } +}