进化你自己的 Hack
← ISA 指南 · 汇编器内部原理 → · English
这个工作区里的模型不是只能观赏的标准答案,而是一个起点。标准 Hack 程序通过后,可以继续改变机器周围的语言、连接机器的平台,甚至 ISA 本身。最重要的习惯是:先说明自己改的是哪一层,再把这个决定一致地落实到模型、工具、测试和文档中。
先看清伪指令展开保留了什么
假设 Hack+ 源码是:
默认的 summary 产物会显示这条伪指令生成的每一条正式指令:
[i/n] 是展开来源:当前机器字是一条源伪指令产生的 n 个结果中的第 i 个。它不是 Hack 机器码的一部分。普通 A/C 指令没有展开标记。
显式选择 full 后,还会保留完整原始源码行:
生成的 driver 必须重新加载 .hack 后,才能把同一份来源信息附在原始 ROM 加载行上:
Driver 不解码该机器字;随后由 hack_step() 从模型自有的 ROM 取指,并走 Sail 的解码/执行路径。
可以直接对照三种产物:
修改前先说清楚层次
这可以避免一个常见误区:只让 parser 接受一个新助记符,就把它称为“新指令”,但实际上既没有新编码,也没有 Sail 执行语义。
原地修改,还是创建派生 profile?
模块已经包含两个长期身份:canonical nand2tetris 基线 hack16 与 Verylogic 扩展 hack32。应选择能保持身份清晰的最小边界:
- 个人分支或课程练习:可以局部修改一个 profile,并把另一个 profile 的回归作为兼容性参照;
- 同时保持两种编码的工具改进:在现有模块中实现,并显式传递 profile。新伪指令、trace 与诊断应保持默认
hack16行为; - 平台实验:优先使用 driver/platform 配置,不要改变任何 profile 的 ISA 编码;
- 长期 ISA 变化:只有当它能清晰 include
model/core.sail,并保持小型 profile 配置/mapping clauses、独立 project、manifest identity 与tests/sail/<profile>/门禁时,才增加命名 profile;若语义不再适合这个边界,应建立另一个具名 ISA family。
换句话说:canonical 基线不能被静默重定义,长期变体必须有身份。 Artifact 使用 isa=hack,profile 为 hack16 或 hack32,从不使用 standard,因此不同 profile 不能互相冒充。
一条循序渐进的项目路线
1. 增加一条安全的伪指令
适合作为第一次修改的候选:
先写出准确的标准 Hack 展开和寄存器副作用,再修改 parser、结构化展开来源、assembler 测试、一个端到端程序和 Hack+ 表。因为 ISA 没有变化,Sail 模型文件应保持不动。
如果选择 PUSH、POP、CALL 或 RET,要先定义 stack 和 calling convention。只有方便的语法、没有约定,还不能算完整设计。
2. 改进观察与调试
可以尝试:
- 用 driver trace 记录
PC、解码指令和发生变化的状态; - 在 driver 中实现 breakpoint 或 watchpoint;
- 编写
.hack反汇编器,输出规范 A/C 语法; - 生成逐步 HTML trace,把源码、ROM 机器字和状态转换连起来;
- 让断言失败同时显示 actual 与 expected。
这些项目能学习 executor 和工具设计,同时保持标准 Hack 兼容。
3. 补全更多 Hack 平台能力
当前模型保留 SCREEN 和 KBD 标准地址,但不模拟设备副作用。可以增加:
- screen memory 的 framebuffer 渲染;
- keyboard 输入快照;
- timer 或串口输出等 memory-mapped device;
- 可重复测试的脚本化设备输入。
这些应称为平台扩展,而不是指令扩展。需要定义地址范围、读写行为、复位值和确定性测试输入。
4. 增加真正的 ISA 指令
值得探索的候选:
- 逻辑或算术移位;
- 用真实机器字表示
HALT,替代当前 Hack+ 自循环约定; - 新的条件或 ALU 运算;
- 一个有明确教学目的的小型扩展寄存器。
不要看到一段“似乎没用”的 C 指令位型就直接占用。先枚举规范编码和电路同义编码,再定义:
- 准确位布局和汇编语法;
- 解码结果与架构状态转换;
- 非法或保留编码;
- assembler 编码以及与伪指令的关系;
- known-word、decode、语义和端到端测试;
- 旧的标准 Hack 程序是否保持二进制兼容。
真正的新指令必须在正确的共享/profile 模型边界、assembler、受影响的两个一致性测试、示例、artifact identity 和 ISA 参考中同步实现。
5. 设计更激进的新机器
当兼容性不再是目标,可以继续问:
- Hack 如果拥有更宽的 PC 或更多可寻址内存,会发生什么?
- 能否在不牺牲简单解码的前提下加入 immediate arithmetic?
- 独立 load/store 指令会不会让数据移动更清楚?
- flags register 会怎样改变分支?
- 让函数调用显著变简单的最小修改是什么?
- 扩展后还能否容易地从逻辑门搭建出来?
走到这一步时,最好给 dialect 或 ISA 起一个新名字,而不是悄悄重新定义 nand2tetris Hack。
修改范围地图
给实验写一份契约
动手前先写一份小而持久的规格:
除非实验主题本身就是不兼容设计,否则应保持标准回归测试继续通过。新增行为至少应有一个直接语义测试,以及一个带 .assert 契约的可运行程序。
创意挑战
- 向 screen memory 绘制图案,并制作最小 viewer。
- 先把
CALL/RET设计成 Hack+,再与真正 ISA 支持比较。 - 增加移位,用它实现更快的乘法或位提取。
- 编写 VM-to-Hack translator,并把 VM 源码位置保留到
.hack注释。 - 增加确定性串口设备,让程序输出文本。
- 随机生成合法 A/C 指令,检查 encode/decode 或执行性质。
- 用两个独立实现对同一条指令的上千组状态做差分测试。
- 扩展已有的
hack32profile,或设计hack64,并准确说明它获得和失去了哪些简单性。
目标不是堆积最多功能。一项层次清楚、契约准确、产物易读、测试可信的小扩展,比一个语义模糊的大扩展更有教学价值。
下一步:继续阅读教学汇编器如何表示并降级源码。