第 4 篇:让 VM 支持变量:Slot、Environment 与 TDZ

源码地址:

github.com/MoMeak9/scr...

1. 本文目标

前三篇我们已经让 VM 能执行表达式:

javascript 复制代码
1 + 2

到第 3 篇为止,我们手里是一台寄存器式的表达式计算机。它能把临时值放进 r0r1r2

plaintext 复制代码
LOAD_CONST r0, const[0]
LOAD_CONST r1, const[1]
BINARY r2, r0, r1, '+'
RETURN r2

但 JavaScript 不只有表达式。真正的代码会写变量:

javascript 复制代码
let a = 1;
a;

本篇要解决的问题是:

VM 如何保存变量、读取变量,并区分"临时值"和"变量绑定"?

我们会从 0 实现:

  1. Slot

  2. Environment

  3. LOAD_SLOT

  4. INIT_SLOT

  5. STORE_SLOT

  6. 一个最小 TDZ 检查

最终支持:

javascript 复制代码
let a = 1;
a;

输出:

javascript 复制代码
1

2. 为什么寄存器还不够

上一篇我们有了寄存器:

javascript 复制代码
regs[0] = 1;
regs[1] = 2;
regs[2] = regs[0] + regs[1];

寄存器适合保存表达式计算中的临时值,但变量不是临时值。

例如:

javascript 复制代码
let a = 1;
let b = a + 2;
a;

这里的 a 需要跨多条语句长期存在。如果只用寄存器,会遇到几个问题:

问题 说明
变量名丢失 a 到底对应哪个寄存器?
生命周期不同 表达式临时值很快就没用,变量可能要存很久
作用域不同 不同 block / function 里可以有同名变量
TDZ let / const 在初始化前不能读取

所以我们需要把"临时计算位置"和"变量绑定位置"分开。

plaintext 复制代码
Register:表达式计算的临时工作台
Slot:变量绑定的固定储物格
Environment:一组 slot 组成的作用域盒子

3. 形象化比喻:工作台、储物柜与房间

3.1 Register 像临时工作台

寄存器像厨师面前的临时工作台:

plaintext 复制代码
r0 放刚切好的葱
r1 放刚打好的蛋
r2 放刚炒出来的结果

这些东西都只服务于"当前这道菜"。菜一做完,工作台就可以清空,或被下一道菜复用。

3.2 Slot 像贴了名字的储物柜

变量更像储物柜:

plaintext 复制代码
slot0: a
slot1: b
slot2: count

只要变量还在作用域里,储物柜就存在。

所以 let a = 1 的意思不是"把 1 放到某个临时工作台",而是:

plaintext 复制代码
找到 a 对应的储物柜,把 1 放进去。

3.3 Environment 像一整个房间

一个作用域可以包含很多变量,就像一个房间里排着一整排储物柜:

plaintext 复制代码
Global Environment
  slot0: a
  slot1: b
  slot2: add

函数作用域、块级作用域也各有自己的房间。后续讲闭包时,我们会把多个房间串成链,也就是 scope chain。

4. 前置知识

概念 说明
Register 表达式临时值位置,例如 r0
Slot 变量绑定位置,例如 slot0
Environment 一组 slot 的运行时容器
TDZ Temporal Dead Zone,let / const 初始化前不能读取
Binding 变量名到 slot 的映射
Scope 变量可见范围

5. 源码中的对应位置

正式源码中已经有完整的变量与环境模型:

概念 源码位置 说明
BindingRef src/compiler/ir.ts 变量可以解析成 slot 或 global
SlotKind src/compiler/ir.ts 区分 varletconstparam
load_slot / init_slot / store_slot src/compiler/ir.ts IR 层的变量读写指令
ScopeFrame src/compiler/lowering.ts 编译期作用域帧
declareBinding() src/compiler/lowering.ts 给变量分配 slot
resolve() src/compiler/lowering.ts 把变量名解析成 slot depth
createEnv() src/compiler/runtime-gen.ts 创建运行时环境
readSlot() / writeSlot() src/compiler/runtime-gen.ts 运行时读写变量 slot

其中 BindingRef 表示变量的解析结果,要么是 { kind: 'slot', depth, slot },要么是全局变量 { kind: 'global', name }SlotKind 则区分 varletconstparamfunctioncatch 等绑定类型。

6. 核心数据结构

6.1 Slot

它解决什么问题

Slot 解决变量"放在哪里"的问题。

源码:

javascript 复制代码
let a = 1;

编译期可以把变量名 a 分配到 slot0

plaintext 复制代码
a -> slot0

运行时只需要读写 slot0

它的数据结构

教学版:

javascript 复制代码
const bindings = new Map([
  ['a', 0],
]);

也可以记录变量种类:

javascript 复制代码
const slotKinds = ['let'];
const slotNames = ['a'];

它在源码中的对应位置

正式源码的 FunctionIR 中保存了 slotNamesslotKinds。IR 指令中也定义了 load_slotinit_slotstore_slot

教学版简化实现

javascript 复制代码
const slot = 0;
env.values[slot] = 1;

使用示例

javascript 复制代码
console.log(env.values[0]); // a 的值

6.2 Environment

它解决什么问题

Environment 解决"这一批变量由谁保存"的问题。

一个环境至少需要:

  1. values:slot 对应的值;

  2. states:slot 是否已经初始化;

  3. slotNames:调试和报错用;

  4. slotKinds:区分 letconst 等;

  5. parent:指向外层环境。

它的数据结构

教学版:

javascript 复制代码
function createEnv(slotNames, slotKinds, parent = null) {
  return {
    values: new Array(slotNames.length),
    states: new Array(slotNames.length).fill(false),
    slotNames,
    slotKinds,
    parent,
  };
}

它在源码中的对应位置

正式源码的 createEnv() 会创建包含 valuesstatesslotKindsslotNamesparentthisValueargs 的环境对象。

教学版简化实现

javascript 复制代码
const env = createEnv(['a'], ['let']);

使用示例

javascript 复制代码
env.values[0] = 1;
env.states[0] = true;

6.3 TDZ State

它解决什么问题

JavaScript 中:

javascript 复制代码
console.log(a);
let a = 1;

会报错。

原因是 a 已经进入作用域,但还没有初始化。这段"进入作用域到完成初始化之间"的区域就叫 TDZ:Temporal Dead Zone。

它的数据结构

教学版用布尔值表示:

javascript 复制代码
states[slot] = false; // 未初始化
states[slot] = true;  // 已初始化

它在源码中的对应位置

正式源码里,assertInitialized() 会检查 targetEnv.states[slot]。如果 slot 还没有初始化,就抛出 ReferenceError

教学版简化实现

javascript 复制代码
function assertInitialized(env, slot) {
  if (!env.states[slot]) {
    throw new ReferenceError(`Cannot access '${env.slotNames[slot]}' before initialization`);
  }
}

使用示例

javascript 复制代码
assertInitialized(env, 0);

6.4 Runtime Context

它解决什么问题

当 VM 执行时,需要同时保存:

  • bytecode;

  • constant pool;

  • registers;

  • environment;

  • pc.

这些合起来就是一个最小运行时上下文。

它的数据结构

教学版:

javascript 复制代码
const ctx = {
  bytecode,
  constantPool,
  regs: new Array(2),
  env,
  pc: 0,
};

它在源码中的对应位置

正式源码中这些状态分散在 executeSync() 的局部变量中:metaenvregscodepc

教学版简化实现

javascript 复制代码
function createRuntimeContext(bytecode, constantPool, env) {
  return {
    bytecode,
    constantPool,
    regs: new Array(4),
    env,
    pc: 0,
  };
}

使用示例

javascript 复制代码
ctx.regs[0] = ctx.constantPool[0];

7. Mermaid 图解

7.1 Register 与 Slot 的分工

flowchart LR A[&#34;表达式临时值&#34;] --> B[&#34;Register<br>r0 r1 r2&#34;] C[&#34;变量绑定&#34;] --> D[&#34;Slot<br>slot0 slot1&#34;] D --> E[&#34;Environment<br>values/states/names/kinds&#34;]

7.2 Environment 结构图

flowchart TD A[&#34;Environment&#34;] --> B[&#34;values<br>[1]&#34;] A --> C[&#34;states<br>[true]&#34;] A --> D[&#34;slotNames<br>['a']&#34;] A --> E[&#34;slotKinds<br>['let']&#34;] A --> F[&#34;parent<br>null&#34;]

7.3 let a = 1; a; 执行流程

flowchart TD A[&#34;LOAD_CONST r0, const[0]&#34;] --> B[&#34;regs[0] = 1&#34;] B --> C[&#34;INIT_SLOT slot0, r0&#34;] C --> D[&#34;env.values[0] = regs[0]<br>env.states[0] = true&#34;] D --> E[&#34;LOAD_SLOT r1, slot0&#34;] E --> F[&#34;regs[1] = env.values[0]&#34;] F --> G[&#34;RETURN r1&#34;]

7.4 TDZ 检查流程

flowchart TD A[&#34;LOAD_SLOT slot0&#34;] --> B{&#34;env.states[slot0] ?&#34;} B -->|true| C[&#34;读取 env.values[slot0]&#34;] B -->|false| D[&#34;抛出 ReferenceError&#34;]

8. 伪代码

8.1 创建环境

plaintext 复制代码
function createEnv(slotNames, slotKinds):
    env.values = array with slotNames.length
    env.states = array filled with false
    env.slotNames = slotNames
    env.slotKinds = slotKinds
    env.parent = null
    return env

8.2 初始化变量

plaintext 复制代码
case INIT_SLOT:
    slot = read bytecode
    src = read bytecode
    env.values[slot] = regs[src]
    env.states[slot] = true

8.3 读取变量

plaintext 复制代码
case LOAD_SLOT:
    dst = read bytecode
    slot = read bytecode

    if env.states[slot] is false:
        throw ReferenceError

    regs[dst] = env.values[slot]

8.4 写入变量

plaintext 复制代码
case STORE_SLOT:
    slot = read bytecode
    src = read bytecode

    if env.states[slot] is false:
        throw ReferenceError

    if env.slotKinds[slot] is const:
        throw TypeError

    env.values[slot] = regs[src]

9. 教学版实现代码

9.1 定义 opcode

javascript 复制代码
const OPCODES = {
  LOAD_CONST: 1,
  LOAD_SLOT: 2,
  INIT_SLOT: 3,
  STORE_SLOT: 4,
  RETURN: 5,
};

9.2 创建环境

javascript 复制代码
function createEnv(slotNames, slotKinds, parent = null) {
  return {
    values: new Array(slotNames.length),
    states: new Array(slotNames.length).fill(false),
    slotNames,
    slotKinds,
    parent,
  };
}

9.3 读写 slot

javascript 复制代码
function assertInitialized(env, slot) {
  if (!env.states[slot]) {
    throw new ReferenceError(
      `Cannot access '${env.slotNames[slot]}' before initialization`
    );
  }
}

function readSlot(env, slot) {
  assertInitialized(env, slot);
  return env.values[slot];
}

function writeSlot(env, slot, value, isInit) {
  const kind = env.slotKinds[slot];

  if (isInit) {
    env.values[slot] = value;
    env.states[slot] = true;
    return value;
  }

  assertInitialized(env, slot);

  if (kind === 'const') {
    throw new TypeError(`Assignment to constant variable '${env.slotNames[slot]}'`);
  }

  env.values[slot] = value;
  return value;
}

9.4 VM 主循环

javascript 复制代码
function run(bytecode, constantPool, env, registerCount) {
  const regs = new Array(registerCount);
  let pc = 0;

  while (pc < bytecode.length) {
    const opcode = bytecode[pc++];

    switch (opcode) {
      case OPCODES.LOAD_CONST: {
        const dst = bytecode[pc++];
        const constIndex = bytecode[pc++];
        regs[dst] = constantPool[constIndex];
        break;
      }

      case OPCODES.INIT_SLOT: {
        const slot = bytecode[pc++];
        const src = bytecode[pc++];
        writeSlot(env, slot, regs[src], true);
        break;
      }

      case OPCODES.LOAD_SLOT: {
        const dst = bytecode[pc++];
        const slot = bytecode[pc++];
        regs[dst] = readSlot(env, slot);
        break;
      }

      case OPCODES.STORE_SLOT: {
        const slot = bytecode[pc++];
        const src = bytecode[pc++];
        writeSlot(env, slot, regs[src], false);
        break;
      }

      case OPCODES.RETURN: {
        const src = bytecode[pc++];
        return regs[src];
      }

      default:
        throw new Error(`Unknown opcode: ${opcode}`);
    }
  }
}

9.5 执行 let a = 1; a;

javascript 复制代码
const env = createEnv(['a'], ['let']);
const constantPool = [1];

const bytecode = [
  // r0 = const[0]
  OPCODES.LOAD_CONST, 0, 0,

  // slot0(a) = r0; initialized = true
  OPCODES.INIT_SLOT, 0, 0,

  // r1 = slot0(a)
  OPCODES.LOAD_SLOT, 1, 0,

  // return r1
  OPCODES.RETURN, 1,
];

console.log(run(bytecode, constantPool, env, 2)); // 1

10. 示例输入与输出

10.1 示例输入

javascript 复制代码
let a = 1;
a;

10.2 教学版变量分配

plaintext 复制代码
a -> slot0

10.3 Constant Pool

javascript 复制代码
[1]

10.4 Bytecode

plaintext 复制代码
LOAD_CONST r0, const[0]
INIT_SLOT slot0, r0
LOAD_SLOT r1, slot0
RETURN r1

10.5 输出结果

javascript 复制代码
1

11. 执行过程拆解

Step PC Instruction Registers Before Environment Before Registers After Environment After
1 0 LOAD_CONST r0, const[0] [_, _] a: uninit [1, _] a: uninit
2 3 INIT_SLOT slot0, r0 [1, _] a: uninit [1, _] a: 1
3 6 LOAD_SLOT r1, slot0 [1, _] a: 1 [1, 1] a: 1
4 9 RETURN r1 [1, 1] a: 1 [1, 1] a: 1

12. TDZ 示例

如果 bytecode 先读取 a,再初始化 a

javascript 复制代码
const env = createEnv(['a'], ['let']);
const constantPool = [1];

const bytecode = [
  OPCODES.LOAD_SLOT, 0, 0,
  OPCODES.RETURN, 0,
];

run(bytecode, constantPool, env, 1);

会抛出:

plaintext 复制代码
ReferenceError: Cannot access 'a' before initialization

这就是 TDZ 的最小模型。

13. 与原始源码的差异

主题 教学版 正式源码
作用域层数 只有当前 env 支持 depth 查找外层 env
slot 类型 let / const 简化 支持 varletconstparamfunctioncatch
TDZ 用 boolean state 正式源码也使用 states
全局变量 不支持 支持 LOAD_GLOBAL / STORE_GLOBAL
块级作用域 不支持 支持 ENTER_SCOPE / LEAVE_SCOPE
编译期绑定 手动写 a -> slot0 declareBinding() 自动分配 slot
运行时查找 直接读当前 env resolveEnv(env, depth) 查找环境链

正式源码中,IR 已经包含 load_slotinit_slotstore_slot 三种变量操作,运行时则通过 readSlot()writeSlot()resolveEnv() 完成变量的读取、初始化和赋值。

14. 常见问题

Q1:为什么变量不直接放寄存器里?

寄存器是表达式计算的临时工作台,变量是作用域里的长期储物柜。二者生命周期不同,所以要分开。

Q2:slot0 为什么比变量名更适合 VM?

变量名适合人读,slot 编号适合机器执行。编译期一次性完成映射:

plaintext 复制代码
a -> slot0

运行时就不需要反复查字符串 a

Q3:TDZ 是不是只要 states 就够了?

最小模型里够用。正式 JavaScript 的 TDZ 还涉及作用域进入、声明提升、块级绑定等细节,后续讲 scope chain 时会继续扩展。

Q4:var 为什么初始化规则不同?

var 有声明提升,进入函数环境时通常已经是 undefined。正式源码在 createEnv() 中会把 varfunction 这类 binding 初始化为 undefined,而 let / const 初始状态为未初始化。

Q5:constlet 的区别在哪里体现?

writeSlot() 中体现。const 可以初始化一次,但初始化后再次赋值应抛出 TypeError

15. 本文小结

这一篇我们让 VM 从"只能算表达式"升级到"能保存变量"。

新增能力:

能力 指令 / 结构
声明变量位置 slot
保存变量集合 environment
初始化变量 INIT_SLOT
读取变量 LOAD_SLOT
修改变量 STORE_SLOT
TDZ 检查 states[slot]

现在 VM 可以执行:

javascript 复制代码
let a = 1;
a;

下一篇将继续讲:

plaintext 复制代码
第 5 篇:写第一个编译器:从 AST 生成 IR

到那时,我们不再手写 bytecode,而是开始把源码结构自动翻译成 VM 指令。

相关推荐
Asize1 小时前
54. 螺旋矩阵
算法
Asize1 小时前
73. 矩阵置零
算法
微露清风1 小时前
快慢指针算法学习记录
学习·算法·快慢指针
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·深度学习·算法·机器学习·参数高效微调
我星期八休息2 小时前
Linux I/O多路转接—epoll
java·linux·运维·服务器·开发语言·jvm·算法