手搓JSVM第 6 篇:把 IR 编成字节码:emit 与 label fixup

源码地址:

github.com/MoMeak9/scr...

1. 本文目标

第 5 篇我们已经能把简化 AST 降级成 IR:

plaintext 复制代码
load_const r0, 1
load_const r1, 2
binary r2, r0, r1, '+'
init_slot slot0, r2
load_slot r3, slot0
return r3

本篇继续向前走一步:

把人类可读的 IR 编码成 VM 可以顺序读取的数字 bytecode。

最终我们要得到:

javascript 复制代码
{
  bytecode: [4, 0, 0, 4, 1, 1, 13, 2, 0, 1, 1, 7, 0, 0, 2, 6, 3, 0, 0, 19, 3],
  constantPool: [1, 2]
}

这里的数字只是一种示意,正式源码中的 opcode 来自 src/runtime/opcodes.ts

2. 为什么需要 emit 阶段

IR 像施工步骤清单,人能直接读懂:

plaintext 复制代码
load_const r0, 1
binary r2, r0, r1, '+'

但 VM 更喜欢数字协议:

plaintext 复制代码
4, 0, 0, 13, 2, 0, 1, 1

形象化比喻:把菜谱翻译成机器按钮

厨师看得懂:

plaintext 复制代码
把鸡蛋打散,然后下锅翻炒。

自动炒菜机只认按钮编号:

plaintext 复制代码
08, 12, 03, 21

emit 阶段就是把"菜谱语言"翻译成"机器按钮编号"。

3. 前置知识

概念 说明
IR 编译器内部的可读指令
Bytecode Runtime 消费的数字数组
Constant Pool 保存常量值,bytecode 引用索引
Opcode 指令编号
Operand opcode 后面的参数
Label 控制流目标的名字
Fixup 先占位,等 label 地址确定后回填

4. 源码中的对应位置

概念 源码位置 说明
emitBytecode() src/compiler/emit.ts IR 到 bytecode 的主函数
ConstantPool src/compiler/emit.ts 常量去重与索引分配
OPCODES / BINARY_OPS src/runtime/opcodes.ts 指令和操作符数字编码
ProgramArtifact src/compiler/types.ts 保存 bytecode、constantPool、functions

正式源码的 emitBytecode() 遍历每个函数的 IR 指令,根据 instruction.opbytecode 数组 push 数字;遇到 label 会记录地址,遇到 jump 会先写 -1,最后再 fixup。

5. 核心数据结构

5.1 Bytecode

它解决什么问题

Bytecode 是 runtime 真正读取的程序格式。

它的数据结构

typescript 复制代码
type Bytecode = number[];

教学版简化实现

javascript 复制代码
const bytecode = [];
bytecode.push(OPCODES.LOAD_CONST, 0, 0);

5.2 Constant Pool

它解决什么问题

把常量集中放进池子,bytecode 里只保留索引,整个指令流保持纯数字。

教学版简化实现

javascript 复制代码
class ConstantPool {
  constructor() {
    this.values = [];
    this.map = new Map();
  }

  add(value) {
    const key = JSON.stringify(value);
    if (this.map.has(key)) return this.map.get(key);
    const index = this.values.length;
    this.values.push(value);
    this.map.set(key, index);
    return index;
  }
}

5.3 ProgramArtifact

它解决什么问题

把 runtime 需要的所有数据放进统一产物。

教学版简化实现

javascript 复制代码
const artifact = {
  bytecode,
  constantPool: pool.values,
  registerCount,
  slotNames,
};

5.4 Label 与 Fixup

它解决什么问题

跳转指令需要知道目标地址,但编译到跳转时,目标 label 可能还没出现。

形象化比喻:先贴便签,最后填门牌号

你装修房子时先在墙上贴一张便签:

plaintext 复制代码
这里通向"厨房"

等厨房门牌号确定后,再回来把"厨房"换成真正的地址。fixup 做的就是这件事:先占位,最后回填。

教学版简化实现

javascript 复制代码
fixups.push({ index: bytecode.length - 1, target: 'end' });

6. Mermaid 图解

flowchart LR A["IRInstruction[]"] --> B["Emitter"] B --> C["Bytecode number[]"] B --> D["Constant Pool"] B --> E["Function Metadata"] C --> F["Runtime"] D --> F E --> F
flowchart TD A["遇到 jump end"] --> B["写入 JUMP -1"] B --> C["记录 fixup: index -> end"] C --> D["继续 emit"] D --> E["遇到 label end"] E --> F["记录 label end = 当前 bytecode 地址"] F --> G["回填 fixup 中的 -1"]

7. 伪代码

plaintext 复制代码
function emitBytecode(ir):
    pool = new ConstantPool()
    bytecode = []
    labels = Map()
    fixups = []

    for instruction in ir:
        if instruction.op is label:
            labels[instruction.name] = bytecode.length
            continue

        switch instruction.op:
            case load_const:
                index = pool.add(instruction.value)
                push LOAD_CONST, instruction.dst, index

            case binary:
                operator = BINARY_OPS[instruction.operator]
                push BINARY, dst, left, right, operator

            case jump:
                push JUMP, -1
                remember fixup

    for each fixup:
        bytecode[fixup.index] = labels[fixup.target]

    return { bytecode, constantPool: pool.values }

8. 教学版实现代码

javascript 复制代码
const OPCODES = {
  LOAD_CONST: 4,
  LOAD_SLOT: 6,
  INIT_SLOT: 7,
  BINARY: 13,
  JUMP: 15,
  RETURN: 19,
};

const BINARY_OPS = { '+': 1, '-': 2, '*': 3, '/': 4 };

class ConstantPool {
  constructor() {
    this.values = [];
    this.map = new Map();
  }

  add(value) {
    const key = JSON.stringify(value);
    if (this.map.has(key)) return this.map.get(key);
    const index = this.values.length;
    this.values.push(value);
    this.map.set(key, index);
    return index;
  }
}

function emitBytecode(ir) {
  const pool = new ConstantPool();
  const bytecode = [];
  const labels = new Map();
  const fixups = [];

  for (const instruction of ir) {
    if (instruction.op === 'label') {
      labels.set(instruction.name, bytecode.length);
      continue;
    }

    switch (instruction.op) {
      case 'load_const':
        bytecode.push(OPCODES.LOAD_CONST, instruction.dst, pool.add(instruction.value));
        break;

      case 'load_slot':
        bytecode.push(OPCODES.LOAD_SLOT, instruction.dst, 0, instruction.slot);
        break;

      case 'init_slot':
        bytecode.push(OPCODES.INIT_SLOT, 0, instruction.slot, instruction.src);
        break;

      case 'binary':
        bytecode.push(
          OPCODES.BINARY,
          instruction.dst,
          instruction.left,
          instruction.right,
          BINARY_OPS[instruction.operator]
        );
        break;

      case 'jump':
        bytecode.push(OPCODES.JUMP, -1);
        fixups.push({ index: bytecode.length - 1, target: instruction.target });
        break;

      case 'return':
        bytecode.push(OPCODES.RETURN, instruction.src);
        break;

      default:
        throw new Error(`Unsupported IR op: ${instruction.op}`);
    }
  }

  for (const fixup of fixups) {
    const target = labels.get(fixup.target);
    if (target === undefined) throw new Error(`Unknown label: ${fixup.target}`);
    bytecode[fixup.index] = target;
  }

  return { bytecode, constantPool: pool.values };
}

9. 示例输入与输出

IR 输入

javascript 复制代码
const ir = [
  { op: 'load_const', dst: 0, value: 1 },
  { op: 'load_const', dst: 1, value: 2 },
  { op: 'binary', dst: 2, left: 0, right: 1, operator: '+' },
  { op: 'return', src: 2 },
];

输出

javascript 复制代码
{
  bytecode: [4, 0, 0, 4, 1, 1, 13, 2, 0, 1, 1, 19, 2],
  constantPool: [1, 2]
}

10. 执行过程拆解

Step IR Bytecode 变化 Constant Pool
1 load_const r0, 1 [4, 0, 0] [1]
2 load_const r1, 2 [4, 0, 0, 4, 1, 1] [1, 2]
3 binary r2, r0, r1, + [..., 13, 2, 0, 1, 1] [1, 2]
4 return r2 [..., 19, 2] [1, 2]

11. 与原始源码的差异

主题 教学版 正式源码
函数数量 单函数 多函数统一 bytecode
FunctionMeta 简化 记录 entry/end/registerCount/slot 信息
Label 只演示 jump 支持 jump、条件跳转、try 等 fixup
操作符 只支持四则运算 支持更多 JS 二元 / 一元操作
Debug 可输出 debugInfo.instructions

12. 常见问题

Q1:为什么 bytecode 不直接保存字符串操作符?

因为 runtime 更适合消费数字协议。操作符和 opcode 一样,都可以通过映射表编成数字。

Q2:为什么 label 不能一开始就知道地址?

因为 bytecode 是线性生成的,跳转目标可能在后面才出现。

Q3:emit 阶段是不是优化阶段?

本系列里不是。emit 只负责编码,优化可以放在 IR 阶段或单独的优化阶段。

13. 本文小结

本文完成:

plaintext 复制代码
IR -> Bytecode + ConstantPool

下一篇将继续讲:

plaintext 复制代码
第 7 篇:控制流:if、while 与 jump
相关推荐
泯泷15 分钟前
手搓JSVM第 3 篇:从栈式 VM 到寄存器式 VM:为什么我们选择寄存器
前端·javascript·算法
泯泷18 分钟前
第 4 篇:让 VM 支持变量:Slot、Environment 与 TDZ
前端·javascript·算法
Asize1 小时前
54. 螺旋矩阵
算法
Asize1 小时前
73. 矩阵置零
算法
微露清风2 小时前
快慢指针算法学习记录
学习·算法·快慢指针
benchmark_cc2 小时前
1000只ETF的5分钟K线如何批量获取?QuantDash分页策略与高性能Python实践
开发语言·人工智能·爬虫·python·算法·quantdash·量化数据源
粥里有勺糖2 小时前
视野修炼-技术周刊第131期 | Bun 与 pnpm Rust 化
前端·github·agent
Coodor2 小时前
如何在web浏览器使用js操作CPU卡
开发语言·前端·javascript·cpu卡
sel_92 小时前
【PEFT】参数高效微调(PEFT)技术详解:从原理到 LoRA/QLoRA 实战
人工智能·python·深度学习·算法·机器学习·参数高效微调