• 简体中文
  • 进化你自己的 Hack

    ← ISA 指南 · 汇编器内部原理 → · English

    这个工作区里的模型不是只能观赏的标准答案,而是一个起点。标准 Hack 程序通过后,可以继续改变机器周围的语言、连接机器的平台,甚至 ISA 本身。最重要的习惯是:先说明自己改的是哪一层,再把这个决定一致地落实到模型、工具、测试和文档中。

    先看清伪指令展开保留了什么

    假设 Hack+ 源码是:

    SET R0, 6 // 被乘数

    默认的 summary 产物会显示这条伪指令生成的每一条正式指令:

    0000000000000110 // ROM[0000] L4 [1/4] SET R0, 6 => @6
    1110110000010000 // ROM[0001] L4 [2/4] SET R0, 6 => D=A
    0000000000000000 // ROM[0002] L4 [3/4] SET R0, 6 => @R0
    1110001100001000 // ROM[0003] L4 [4/4] SET R0, 6 => M=D

    [i/n] 是展开来源:当前机器字是一条源伪指令产生的 n 个结果中的第 i 个。它不是 Hack 机器码的一部分。普通 A/C 指令没有展开标记。

    显式选择 full 后,还会保留完整原始源码行:

    0000000000000110 // ROM[0000] L4 [1/4] SET R0, 6 => @6 // 被乘数

    生成的 driver 必须重新加载 .hack 后,才能把同一份来源信息附在原始 ROM 加载行上:

    ROM[0] = 0b0000000000000110; // ROM[0000] L4 [1/4] SET R0, 6 => @6 // 被乘数

    Driver 不解码该机器字;随后由 hack_step() 从模型自有的 ROM 取指,并走 Sail 的解码/执行路径。

    可以直接对照三种产物:

    pixi run just hack assemble multiply
    pixi run just hack assemble multiply full
    pixi run just hack run multiply full

    修改前先说清楚层次

    层次例子标准 Hack 机器码是否变化
    汇编表层与工具新 Hack+ 伪指令、诊断、反汇编器、调试器、trace不变化;最终仍降级为标准 A/C 机器字
    Runtime 或平台Screen/Keyboard 行为、timer、串口、启动约定通常不变化;变化的是设备或运行环境
    ISA移位指令、真正的 HALT 编码、新寄存器或寻址方式变化;指令编码或架构状态发生改变
    实现或验证流水线实验、门级实现、性质测试、差分测试不一定变化;同一 ISA 可以有不同实现

    这可以避免一个常见误区:只让 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. 增加一条安全的伪指令

    适合作为第一次修改的候选:

    CLR target
    COPY source, target
    PUSH source
    POP target

    先写出准确的标准 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 指令位型就直接占用。先枚举规范编码和电路同义编码,再定义:

    1. 准确位布局和汇编语法;
    2. 解码结果与架构状态转换;
    3. 非法或保留编码;
    4. assembler 编码以及与伪指令的关系;
    5. known-word、decode、语义和端到端测试;
    6. 旧的标准 Hack 程序是否保持二进制兼容。

    真正的新指令必须在正确的共享/profile 模型边界、assembler、受影响的两个一致性测试、示例、artifact identity 和 ISA 参考中同步实现。

    5. 设计更激进的新机器

    当兼容性不再是目标,可以继续问:

    • Hack 如果拥有更宽的 PC 或更多可寻址内存,会发生什么?
    • 能否在不牺牲简单解码的前提下加入 immediate arithmetic?
    • 独立 load/store 指令会不会让数据移动更清楚?
    • flags register 会怎样改变分支?
    • 让函数调用显著变简单的最小修改是什么?
    • 扩展后还能否容易地从逻辑门搭建出来?

    走到这一步时,最好给 dialect 或 ISA 起一个新名字,而不是悄悄重新定义 nand2tetris Hack。

    修改范围地图

    修改最小实现范围
    Hack+ 伪指令assembler.py parser/lowering、展开测试、示例、包参考
    产物解释writer/loader 契约、driver 注释传播、comment-level 测试、文档
    执行观察能力executor/driver、确定性测试、执行文档
    Memory-mapped device平台状态与读写语义、driver 输入输出、Sail 与端到端测试
    新编码指令共享/profile Sail 边界、profile mapping/project、assembler 编码、独立 known words、一致性与集成测试、ISA 指南
    VM-to-Hack 等新前端独立 translator、规范 .asm 边界、源码来源、translator 与端到端测试

    给实验写一份契约

    动手前先写一份小而持久的规格:

    目标:
    修改层次:
    源码语法或外部接口:
    编码(如果有):
    读取和写入的状态:
    错误与保留情况:
    兼容性承诺:
    独立测试向量:
    端到端示例:
    需要更新的文档:

    除非实验主题本身就是不兼容设计,否则应保持标准回归测试继续通过。新增行为至少应有一个直接语义测试,以及一个带 .assert 契约的可运行程序。

    创意挑战

    • 向 screen memory 绘制图案,并制作最小 viewer。
    • 先把 CALL/RET 设计成 Hack+,再与真正 ISA 支持比较。
    • 增加移位,用它实现更快的乘法或位提取。
    • 编写 VM-to-Hack translator,并把 VM 源码位置保留到 .hack 注释。
    • 增加确定性串口设备,让程序输出文本。
    • 随机生成合法 A/C 指令,检查 encode/decode 或执行性质。
    • 用两个独立实现对同一条指令的上千组状态做差分测试。
    • 扩展已有的 hack32 profile,或设计 hack64,并准确说明它获得和失去了哪些简单性。

    目标不是堆积最多功能。一项层次清楚、契约准确、产物易读、测试可信的小扩展,比一个语义模糊的大扩展更有教学价值。


    下一步:继续阅读教学汇编器如何表示并降级源码。