手搓JSVM第 12 篇:完整最小 JSVM 实现与源码设计复盘

0 阅读4分钟

手搓JSVM第 12 篇:完整最小 JSVM 实现与源码设计复盘

12-cover.png

1. 本文目标

来到最后一篇,我们把前面逐步实现的零件重新组装成一台完整的最小 JSVM。

这台教学版最小 JSVM 至少支持:

  • 数字常量

  • 字符串常量

  • 加减乘除

  • 变量声明

  • 变量读取

  • 表达式执行

  • 简单函数调用

  • return

  • 简单 if

示例:

function add(a, b) {
  return a + b;
}

let x = add(1, 2);
x;

输出:

3

2. 最小 JSVM 总览

flowchart LR
    A["Source"] --> B["Parser/AST"]
    B --> C["Compiler AST->IR"]
    C --> D["Emitter IR->Bytecode"]
    D --> E["Runtime"]
    E --> F["Result"]

12-production-line.png

形象化比喻:一条小型生产线

  • Source:客户订单;

  • AST:订单结构表;

  • IR:生产步骤;

  • Bytecode:机器按钮编号;

  • Runtime:机器本体;

  • Result:最终产品。

3. 支持语法范围

把范围收窄后,教学版最小实现支持:

能力示例
数字1
字符串'a'
四则运算1 + 2 * 3
变量声明let a = 1
变量读取a
函数声明function add(a,b){...}
函数调用add(1,2)
returnreturn a + b
ifif (x) { x = 2 }

不支持或作为扩展:

  • 完整 JS parser;

  • 完整块级作用域;

  • 闭包完整语义;

  • 对象和 class 完整语义;

  • async/generator;

  • module bundling。

4. 核心代码骨架

const OPCODES = {
  LOAD_CONST: 1,
  LOAD_SLOT: 2,
  INIT_SLOT: 3,
  STORE_SLOT: 4,
  BINARY: 5,
  JUMP: 6,
  JUMP_IF_FALSE: 7,
  MAKE_FUNCTION: 8,
  CALL: 9,
  RETURN: 10,
};

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

5. Runtime 最小实现

function binary(op, left, right) {
  switch (op) {
    case BINARY_OPS['+']: return left + right;
    case BINARY_OPS['-']: return left - right;
    case BINARY_OPS['*']: return left * right;
    case BINARY_OPS['/']: return left / right;
    default: throw new Error('bad op');
  }
}

function createEnv(slotNames, parent = null) {
  return {
    values: new Array(slotNames.length),
    states: new Array(slotNames.length).fill(false),
    slotNames,
    parent,
  };
}

function readSlot(env, slot) {
  if (!env.states[slot]) throw new ReferenceError(`${env.slotNames[slot]} not initialized`);
  return env.values[slot];
}

function writeSlot(env, slot, value, init) {
  if (!init && !env.states[slot]) throw new ReferenceError(`${env.slotNames[slot]} not initialized`);
  env.values[slot] = value;
  env.states[slot] = true;
}

12-runtime-core.png

6. 执行函数

function execute(metadata, functionId, parentEnv, args = []) {
  const fn = metadata.functions[functionId];
  const env = createEnv(fn.slotNames, parentEnv);
  const regs = new Array(fn.registerCount);
  const code = metadata.bytecode;

  for (let i = 0; i < fn.params.length; i++) {
    env.values[i] = args[i];
    env.states[i] = true;
  }

  let pc = fn.entry;

  while (pc < fn.end) {
    const op = code[pc++];

    switch (op) {
      case OPCODES.LOAD_CONST:
        regs[code[pc++]] = metadata.constantPool[code[pc++]];
        break;

      case OPCODES.LOAD_SLOT:
        regs[code[pc++]] = readSlot(env, code[pc++]);
        break;

      case OPCODES.INIT_SLOT:
        writeSlot(env, code[pc++], regs[code[pc++]], true);
        break;

      case OPCODES.STORE_SLOT:
        writeSlot(env, code[pc++], regs[code[pc++]], false);
        break;

      case OPCODES.BINARY: {
        const dst = code[pc++];
        const left = code[pc++];
        const right = code[pc++];
        const bop = code[pc++];
        regs[dst] = binary(bop, regs[left], regs[right]);
        break;
      }

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

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

      case OPCODES.MAKE_FUNCTION:
        regs[code[pc++]] = { functionId: code[pc++], parentEnv: env };
        break;

      case OPCODES.CALL: {
        const dst = code[pc++];
        const fnValue = regs[code[pc++]];
        const argc = code[pc++];
        const argv = [];
        for (let i = 0; i < argc; i++) argv.push(regs[code[pc++]]);
        regs[dst] = execute(metadata, fnValue.functionId, fnValue.parentEnv, argv);
        break;
      }

      case OPCODES.RETURN:
        return regs[code[pc++]];

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

12-function-call.png

7. 示例 Artifact

const metadata = {
  constantPool: [1, 2],
  functions: [
    {
      id: 0,
      name: null,
      entry: 0,
      end: 17,
      registerCount: 4,
      slotNames: ['add', 'x'],
      params: [],
    },
    {
      id: 1,
      name: 'add',
      entry: 17,
      end: 26,
      registerCount: 3,
      slotNames: ['a', 'b'],
      params: ['a', 'b'],
    },
  ],
  bytecode: [
    OPCODES.MAKE_FUNCTION, 0, 1,
    OPCODES.INIT_SLOT, 0, 0,
    OPCODES.LOAD_SLOT, 1, 0,
    OPCODES.LOAD_CONST, 2, 0,
    OPCODES.LOAD_CONST, 3, 1,
    OPCODES.CALL, 2, 1, 2, 2, 3,
    OPCODES.INIT_SLOT, 1, 2,
    OPCODES.LOAD_SLOT, 3, 1,
    OPCODES.RETURN, 3,

    OPCODES.LOAD_SLOT, 0, 0,
    OPCODES.LOAD_SLOT, 1, 1,
    OPCODES.BINARY, 2, 0, 1, BINARY_OPS['+'],
    OPCODES.RETURN, 2,
  ],
};

8. run(source) 的位置

这里还缺一个关键入口:parser。真正完整的 run(source) 需要先解析源码;教学版可以把 parse 简化为“输入已知示例时返回固定 AST”:

function runKnownProgram() {
  return execute(metadata, 0, null, []);
}

console.log(runKnownProgram()); // 3

因此,如果要扩展成真正的 run(source),就需要把 parser 接到现有编译与执行链路之前:

source -> parse -> AST -> compile -> IR -> emit -> metadata -> execute

12-run-source-pipeline.png

9. 测试用例

教学版目标测试:

run('1 + 2') === 3;
run('let a = 1; a;') === 1;
run('let a = 1 + 2; a;') === 3;
run('function add(a, b) { return a + b; } add(1, 2);') === 3;
run('let x = 1; if (x) { x = 2; } x;') === 2;

如果当前教学代码还没有完整 parser,这些测试可以先作为目标用例,随着 parser 能力补齐再逐步完成。

10. 源码设计复盘

教学版正式源码
手写 / 简化 ASTBabel parser
简化 FunctionBuilder完整 FunctionBuilder / ScopeFrame
简化 IRsrc/compiler/ir.ts
简化 emitsrc/compiler/emit.ts
单一同步 runtimeruntime-gen.ts 中 sync/async/generator runtime
简化 packsrc/compiler/pack.ts
无 bundlersrc/compiler/bundler.ts

12-source-review.png

11. 安全与设计提醒

JSVM 并不是安全沙箱。即使源码被编成 bytecode,runtime 仍然在宿主 JS 环境中执行。如果暴露 globalObjectrequireimport 或原型对象,仍然可能产生任意代码执行、原型污染或沙箱逃逸风险。

因此,教学版不要声称具备安全隔离能力。正式工程若要用于不可信代码,需要把沙箱边界作为独立问题设计。

12. Mermaid 总复盘

flowchart TD
    A[&#34;JavaScript Source&#34;] --> B[&#34;Parser&#34;]
    B --> C[&#34;AST&#34;]
    C --> D[&#34;Lowering&#34;]
    D --> E[&#34;IR&#34;]
    E --> F[&#34;Emit&#34;]
    F --> G[&#34;Bytecode + ConstantPool&#34;]
    G --> H[&#34;Runtime Context&#34;]
    H --> I[&#34;Execute&#34;]
    I --> J[&#34;Result&#34;]

13. 本文小结

至此,我们已经从 0 到 1 串起一台最小 JSVM 的完整链路:

  1. 最小栈式 VM;

  2. 数字 bytecode 与 constant pool;

  3. 寄存器式 VM;

  4. slot、environment 与 TDZ;

  5. AST 到 IR;

  6. IR 到 bytecode;

  7. 控制流;

  8. 函数调用;

  9. 闭包;

  10. 对象和属性访问;

  11. 打包输出;

  12. 完整最小复盘。

后续可以继续扩展:

  • 完整 parser;

  • 更完整的作用域链;

  • 对象 / class 完整语义;

  • async / generator;

  • module bundling;

  • 调试器;

  • 混淆和保护策略。