手搓JSVM第 7 篇:控制流:if、while 与 jump

87 阅读3分钟

控制流封面:指令路径的分岔与回环

源码仓库

1. 本文目标

前面我们已经能编译和执行直线代码:

let a = 1 + 2;
a;

但真实程序会有分支和循环:

let x = 1;
if (x) {
  x = 2;
}
x;

本篇目标是让 VM 支持控制流:

  1. label

  2. jump

  3. jump_if_false

  4. if

  5. while

2. 为什么控制流需要 jump

直线代码像一条直路:

1 -> 2 -> 3 -> 4

ifwhile 像岔路和回环:

如果条件成立,走 A 路;否则跳到 B 路。

形象化比喻:游戏地图传送门

程序计数器 pc 像玩家在地图上的位置,普通指令让玩家向前走一步。jump 像传送门:

不走下一格,直接传送到某个 label。

jump_if_false 像条件传送门:

如果钥匙不对,就传送到出口。

传送门比喻:jump 跨过中间的格子,直达 label 目标

3. 前置知识

概念说明
Label给 bytecode 某个位置起名
Jump无条件跳转
Conditional Jump条件不满足时跳转
Basic Block一段顺序执行的指令
Control Flow Graph基本块之间的跳转关系

4. 源码中的对应位置

概念源码位置说明
jump / jump_if_falsesrc/compiler/ir.tsIR 控制流指令
label / fixupsrc/compiler/emit.tslabel 记录地址,fixup 回填跳转目标
runtime jumpsrc/compiler/runtime-gen.ts修改 pc 完成跳转
lowering if/whilesrc/compiler/lowering.ts把高级语句降级成 label 和 jump

5. 核心数据结构

Label

它解决什么问题

跳转目标先用名字表示,emit 阶段再统一变成数字地址。

教学版简化实现

{ op: 'label', name: 'end_if' }

Jump

它解决什么问题

让 VM 不再顺序执行下一条指令,而是直接把 pc 改成目标地址。

教学版简化实现

{ op: 'jump', target: 'loop_start' }

Jump If False

它解决什么问题

根据条件的真假决定是否跳转:条件为假时跳走,为真时继续往下执行。

教学版简化实现

{ op: 'jump_if_false', condition: 0, target: 'end_if' }

6. Mermaid 图解

if 控制流

flowchart TD
    A["计算 condition"] --> B{"condition truthy?"}
    B -->|true| C["执行 then block"]
    B -->|false| D["跳到 end_if"]
    C --> D
    D["end_if"]

if 分支:条件为真走 then block,为假直接跳到 end_if

while 控制流

flowchart TD
    A["loop_start"] --> B["计算 condition"]
    B --> C{"condition truthy?"}
    C -->|false| D["loop_end"]
    C -->|true| E["执行 body"]
    E --> A

while 回环:body 执行完跳回 loop_start,条件为假时离开循环

7. 伪代码

编译 if

compileIf(node):
    endLabel = newLabel('if_end')
    conditionReg = compileExpression(node.test)
    emit jump_if_false conditionReg, endLabel
    compileStatement(node.consequent)
    emit label endLabel

编译 while

compileWhile(node):
    startLabel = newLabel('while_start')
    endLabel = newLabel('while_end')

    emit label startLabel
    conditionReg = compileExpression(node.test)
    emit jump_if_false conditionReg, endLabel
    compileStatement(node.body)
    emit jump startLabel
    emit label endLabel

8. 教学版实现代码

function compileIf(node, builder) {
  const end = builder.label('if_end');
  const condition = compileExpression(node.test, builder);
  builder.emit({ op: 'jump_if_false', condition, target: end });
  compileStatement(node.consequent, builder);
  builder.emit({ op: 'label', name: end });
}

function compileWhile(node, builder) {
  const start = builder.label('while_start');
  const end = builder.label('while_end');

  builder.emit({ op: 'label', name: start });
  const condition = compileExpression(node.test, builder);
  builder.emit({ op: 'jump_if_false', condition, target: end });
  compileStatement(node.body, builder);
  builder.emit({ op: 'jump', target: start });
  builder.emit({ op: 'label', name: end });
}

Runtime 执行 jump

case OPCODES.JUMP:
  pc = bytecode[pc];
  break;

case OPCODES.JUMP_IF_FALSE: {
  const conditionReg = bytecode[pc++];
  const target = bytecode[pc++];
  if (!regs[conditionReg]) pc = target;
  break;
}

9. 示例输入与输出

let x = 1;
if (x) {
  x = 2;
}
x;

简化 IR:

load_const r0, 1
init_slot slot0, r0
load_slot r1, slot0
jump_if_false r1, if_end
load_const r2, 2
store_slot slot0, r2
label if_end
load_slot r3, slot0
return r3

输出:

2

10. 执行过程拆解

StepInstructionx说明
1load_const 1uninit准备初始值
2init_slot x1初始化 x
3load_slot x1读取条件
4jump_if_false1条件为真,不跳
5store_slot x = 22更新 x
6return x2返回 2

11. 与原始源码的差异

主题教学版正式源码
if只支持 then支持 alternate
while基本循环还处理 break/continue/label
label字符串 label带函数 ID 的 label 命名
jump fixup简化emit 阶段统一回填
truthy!value使用 JS truthiness

12. 常见问题

Q1:为什么不能直接把 if 编成一个 JS if?

因为 VM 执行的是 bytecode,不是源码。源码里的 if 到了 bytecode 层面,只能表达成 pc 的变化。

Q2:label 是 runtime 真的存在的指令吗?

不是。label 只是编译期标记,emit 阶段会被移除,最终 bytecode 里只留下数字地址。

label 在 emit 后消失,只留下数字地址

13. 本文小结

本文让 VM 从直线执行升级到控制流执行:ifwhile 被降级成 label 加 jump,最终都落在 pc 的跳转上。

下一篇将继续讲:

第 8 篇:函数调用:参数、返回值与调用帧