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

文章来源声明: 原文作者:泯泷; 来源站点:掘金; 原文链接:https://juejin.cn/post/7684261460002029594; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

适合想理解 JS 引擎编译执行链路的前端开发者按篇跟做,教学版代码骨架清晰,可直接作为自研脚本引擎或面试复习的起点。

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

12-cover.png

1. 本文目标

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

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

  • 数字常量
  • 字符串常量
  • 加减乘除
  • 变量声明
  • 变量读取
  • 表达式执行
  • 简单函数调用
  • return
  • 简单 if

示例:

<span>function</span> <span>add</span>(<span>a, b</span>) {
  <span>return</span> a + b;
}

<span>let</span> x = <span>add</span>(<span>1</span>, <span>2</span>);
x;

输出:

<span>3</span>

2. 最小 JSVM 总览

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

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)`
return`return a + b`
if`if (x) { x = 2 }`

不支持或作为扩展:

  • 完整 JS parser;
  • 完整块级作用域;
  • 闭包完整语义;
  • 对象和 class 完整语义;
  • async/generator;
  • module bundling。

4. 核心代码骨架

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

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

5. Runtime 最小实现

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

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

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

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

12-runtime-core.png

6. 执行函数

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

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

  <span>let</span> pc = fn.<span>entry</span>;

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

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

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

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

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

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

      <span>case</span> <span>OPCODES</span>.<span>JUMP</span>:
        pc = code[pc];
        <span>break</span>;

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

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

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

      <span>case</span> <span>OPCODES</span>.<span>RETURN</span>:
        <span>return</span> regs[code[pc++]];

      <span>default</span>:
        <span>throw</span> <span>new</span> <span>Error</span>(<span>`unknown opcode <span>${op}</span>`</span>);
    }
  }
}

12-function-call.png

7. 示例 Artifact

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

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

8. run(source) 的位置

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

<span>function</span> <span>runKnownProgram</span>(<span></span>) {
  <span>return</span> <span>execute</span>(metadata, <span>0</span>, <span>null</span>, []);
}

<span>console</span>.<span>log</span>(<span>runKnownProgram</span>()); <span>// 3</span>

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

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

12-run-source-pipeline.png

9. 测试用例

教学版目标测试:

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

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

10. 源码设计复盘

教学版正式源码
手写 / 简化 ASTBabel parser
简化 FunctionBuilder完整 `FunctionBuilder` / `ScopeFrame`
简化 IR`src/compiler/ir.ts`
简化 emit`src/compiler/emit.ts`
单一同步 runtime`runtime-gen.ts` 中 sync/async/generator runtime
简化 pack`src/compiler/pack.ts`
无 bundler`src/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;
  • 调试器;
  • 混淆和保护策略。