# 扫描周期 vm 模块:寄存器虚拟机。CMake 目标:`vm`(`STATIC`),只依赖 [`指令与映像.md`](../isa/指令与映像.md)。 运行时是 **一台 VM、一个入口、一份映像**。不链接 `compiler`。扫描周期与确定性见 [`初步计划.md`](../初步计划.md) 第 2、8 节。 ## 扫描周期 ```text 1. 按 sidecar 的 io 绑定,采样 → I / 输入全局 2. 从 PROGRAM MAIN 的 pc=0 执行到 RET 3. 写回 Q / 输出全局 4. 用本周期 Δt 推进内置 FB(不用 wall clock) ``` ## 边界 - 只认 `isa` 映像;类型与溢出按 [`指令与映像.md`](../isa/指令与映像.md)(`INT` 饱和) - 局部 / `VAR` = 固定寄存器;`nregs` 在函数头 - `CALL` 目标是立即数 `fn_id`,有界调用栈 - 无 GC、无堆、无线程;每周期受 `cycle_limit` 限制 - I/O 从哪来、Δt 谁给、回放怎么喂,属于 `BytecodeExecutor`(读 `.stb` + sidecar) --- ## 可观察性接口(v1 预留) TUI 是执行器(`BytecodeExecutor`)的界面,用途是方便观察 VM 运行状态。**v1 不做 TUI**,只预留接口,避免后续改 VM 的观察面。原则: - **数据与展示分离**:`vm` 只提供只读观察 API(数据);TUI 渲染与文本输出属于 `executor`(展示)。vm 不画任何界面。 - **只读轮询**:TUI 通过快照轮询,绝不写 VM 状态;"继续 / 单步"是 executor 侧命令,不改变确定性。 - **快照 POD 化**:快照保持简单数据(将来跨进程 TUI 可序列化)。 建议的 API 形态(12.9 实现时冻结签名,本节只定方向): ```cpp struct Snapshot { uint32_t pc; // 当前指令下标(相对函数字节码段) uint32_t fn_id; // 当前函数 uint32_t cycle_count; // 本周期已执行指令数 uint32_t call_depth; // 调用栈深度 // 寄存器 / I / Q / M / 全局 / FB 字段 的只读视图 }; bool step(); // 精确执行一条指令,停在指令边界 const Snapshot& snapshot() const; ``` - 寄存器 ≤256、映像很小,快照拷贝成本可忽略。 - 单步、回放、dump 都建立在同一套观察 API 上。 --- ## 12.9 执行计划 指令执行语义(帧/调用栈、指令表、定时器、故障、line1 推演)见 [`指令执行.md`](指令执行.md)。 ### 步骤 0:数据段改 8 字节定宽槽(方案 a,前置) VM 需要无歧义地访问数据区,当前变宽布局(BOOL 1B / INT 2B / TIME 8B 对齐)无法从槽号推断宽度。定案:**每槽 8 字节定宽**,指令 slot 语义 = **槽号**(数据偏移 = slot × 8)。 - 文档修订:`Doc/isa/指令与映像.md`(数据段规则 + slot 语义)、`Doc/compiler/寄存器码.md`(存储布局,`global_offset` 映射取消)、`Doc/isa/stb文件格式.md`(line1 数据段 16B → 56B) - `Codegen.cpp` 简化:`width_of`/`align_up` 删除、`layout_data`/`layout_fb_instances` 按槽号布局、指令 slot = 符号表槽号 - `codegen_test` 断言更新(slot 从字节偏移变槽号、数据区字节数变化) ### 步骤 1:`Machine.h`(新建 `vm/include/vm/Machine.h`) ```cpp namespace vm { enum class Fault { None, CycleLimit, StackOverflow, BadOp, BadSlot, BadConst }; class Machine { public: static bool create(const std::vector& image, Machine* out, std::string* err); Fault run_cycle(); // MAIN pc=0 → RET(MAIN 帧跨周期保留) // 可观察性(v1 预留,见上节): bool step(); // 执行一条指令,停在指令边界 uint32_t pc() / fn_id() / cycle_count() / call_depth(); uint8_t* data(); // 数据区(I 采样 / Q 读回由外部做) int64_t reg(uint8_t i); // 当前帧寄存器 }; } ``` - `Fault` 枚举 + 帧结构 `{ fn_id, regs[nregs], ret_pc, ret_fn_id }`;调用栈深度上限 64 - 只依赖 `isa`(`ImageView` + 类型/饱和函数) ### 步骤 2:`Machine.cpp` 译码 switch - 取指:函数表 → `code_offset`(字节)→ 读 u32 → 拆 `op/rd/a/b` - 标量:`MOVE` / `LOADK`(常量表 tag 定值表示)/ `NOT` / `AND` / `OR`(布尔)/ `ADD/SUB/MUL/DIV`(`sat_*`)/ `CMP_xx`(全宽比较) - 跳转:`JMP` / `JT` / `JF`(`pc += off`,相对下一条语义) - 数据区:`LOAD_I/LOAD_M/LOAD_GLOBAL`、`STORE_Q/STORE_M/STORE_GLOBAL`(同一实现,slot × 8) ### 步骤 3:CALL / RET 帧栈 + 故障 - `CALL fn_id`:压新帧(复制 r0..r7)→ pc=0;`RET`:复制 r0..r7 回调用方 → 弹栈(MAIN 的 RET = 周期结束) - `cycle_limit` 超限(用例 6)、栈深 > 64、`BadOp` / `BadSlot` / `BadConst` → 周期中止返回故障 ### 步骤 4:定时器与边沿(8 个 CAL_*) - 执行时用映像 `dt_ms` 推进(每周期一次调用 = 推进一次),不读系统时钟 - TON:in 真 → et += dt(到 pt 停)、q = et ≥ pt;in 假 → et = 0 - TOF:in 真 → q=1、et=0;掉电 → et += dt、et ≥ pt → q=0 - CTU:cu 上升沿(**上次 cu 存 VM 侧 vector**,按实例基槽索引)→ cv+1;r → 复位;q = cv ≥ pv ### 步骤 5:可观察性基础 - `step()` / `pc` / `fn_id` / `cycle_count` / `call_depth` / `reg` 落基础实现;`snapshot()` 留 12.11 补全 ### 步骤 6:`vm_test`(`tests/src/vm_test.cpp` + CMake,链 `vm` 库) - 手工拼小映像(`isa` 编码函数):赋值、跳转、CALL/RET 帧复制、cycle_limit - 编译器产物全链路:用例 01/02/04/05/07/09/13/15/17/18/20 跑周期断言寄存器与数据区 - 用例 06:`cycle_limit` 打满 → `CycleLimit` 故障 - TON:dt=10、pt=30 → 第 3 周期 q=1;CTU:上升沿计数 - **确定性**:同一 I 序列跑两遍,数据区完全一致 ### 步骤 7:验证 + 提交 - `cmake --build` + `ctest`(新增 `vm_cycles` 后 10/10 全绿);20 用例扫描:14 正例执行通过