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

源码仓库

1. 本文目标

现在 VM 已经支持表达式、变量和控制流。下一步是函数:

javascript 复制代码
function add(a, b) {
  return a + b;
}

add(1, 2);

本篇要实现:

  1. Function Object

  2. FunctionMeta

  3. Call Frame

  4. 参数传递

  5. Return Address / Return Completion

  6. CALLRETURN

2. 为什么函数调用更复杂

函数调用不只是一次跳转。调用函数时,VM 需要创建一套全新的运行时状态:

plaintext 复制代码
新的 registers
新的 environment
新的参数绑定
函数自己的 pc 范围
返回值

形象化比喻:派一个分队去完成任务

主程序像总部,函数像临时派出的分队。

总部给分队:

plaintext 复制代码
任务说明:add
物资:1 和 2
返回地点:总部

分队独立工作,互不打扰总部的现场,完成后带着结果回到出发点。

3. 前置知识

概念 说明
Function Object 可调用的运行时对象
FunctionMeta 函数的 bytecode 边界和寄存器信息
Call Frame 一次函数调用的运行时状态
Parameter Slot 参数绑定位置
Return Value 函数执行结果
Return Address 调用结束后回到哪里

4. 源码中的对应位置

概念 源码位置 说明
FunctionMeta src/compiler/types.ts 函数元信息
MAKE_FUNCTION src/runtime/opcodes.ts 创建函数对象
CALL src/runtime/opcodes.ts 函数调用
RETURN src/runtime/opcodes.ts 返回值
createClosure() src/compiler/runtime-gen.ts 创建捕获环境的 JS 函数
executeSync() src/compiler/runtime-gen.ts 执行一个函数的 bytecode

5. 核心数据结构

FunctionMeta

它解决什么问题

告诉 runtime 函数的 bytecode 在哪里、需要多少寄存器、有哪些参数和 slot。

教学版简化实现

javascript 复制代码
const fnMeta = {
  id: 1,
  name: 'add',
  entry: 0,
  end: 10,
  registerCount: 4,
  params: ['a', 'b'],
};

Function Object

它解决什么问题

把函数 ID 和它捕获的环境包装成一个可调用的运行时对象。

教学版简化实现

javascript 复制代码
function createFunction(functionId, env) {
  return { type: 'function', functionId, env };
}

Call Frame

它解决什么问题

保存一次函数调用的独立状态:寄存器、环境、执行位置,各调用之间互不干扰。

教学版简化实现

javascript 复制代码
const frame = {
  regs: new Array(meta.registerCount),
  env: createEnv(meta.slotNames, meta.slotKinds, parentEnv),
  pc: meta.entry,
};

6. Mermaid 图解

flowchart TD A["Global Frame"] --> B["CALL add(1,2)"] B --> C["Create Function Frame"] C --> D["Bind params a=1 b=2"] D --> E["Run add bytecode"] E --> F["RETURN result"] F --> G["Back to Global Frame"]
flowchart TD A["Function Frame"] --> B["regs"] A --> C["env"] A --> D["pc"] A --> E["return value"]

7. 伪代码

plaintext 复制代码
function callFunction(fn, args):
    meta = functions[fn.functionId]
    env = createEnv(meta)
    bind params into env
    regs = new Array(meta.registerCount)
    pc = meta.entry
    run until RETURN
    return returned value

8. 教学版实现代码

javascript 复制代码
function createFunction(functionId, parentEnv) {
  return { type: 'function', functionId, parentEnv };
}

function executeFunction(metadata, functionId, parentEnv, args) {
  const meta = metadata.functions[functionId];
  const env = createEnv(meta.slotNames, meta.slotKinds, parentEnv);
  const regs = new Array(meta.registerCount);

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

  return runRange(metadata.bytecode, metadata.constantPool, meta.entry, meta.end, regs, env);
}

function callValue(metadata, value, args) {
  if (value.type !== 'function') throw new TypeError('not callable');
  return executeFunction(metadata, value.functionId, value.parentEnv, args);
}

9. 示例输入与输出

javascript 复制代码
function add(a, b) {
  return a + b;
}

add(1, 2);

简化 IR:

plaintext 复制代码
make_function r0, function#1
load_const r1, 1
load_const r2, 2
call r3, r0, [r1, r2]
return r3

function#1 add:
load_slot r0, slot0(a)
load_slot r1, slot1(b)
binary r2, r0, r1, '+'
return r2

输出:

javascript 复制代码
3

10. 执行过程拆解

Step Frame Instruction 说明
1 global make_function add 创建函数对象
2 global call add(1,2) 创建 add frame
3 add bind params a=1, b=2
4 add binary a+b 得到 3
5 add return 3 返回 global
6 global return result 程序结果 3

11. 与原始源码的差异

主题 教学版 正式源码
Function Object 普通对象 createClosure() 返回 JS function
Return Address 概念解释 借助 JS 调用栈和 completion
this 不支持 CALL 包含 thisReg
arguments 不支持 env 保存 args
async/generator 不支持 正式 runtime 有多套 execute 函数

12. 常见问题

Q1:为什么函数需要新的 frame?

因为每次调用都有自己的局部变量、参数和临时值。共用一套状态的话,递归和嵌套调用会互相覆盖。

Q2:返回地址在哪里?

传统 VM 会显式保存 return address。当前项目生成的是 JS runtime,很多返回行为直接由宿主 JS 调用栈承载。

13. 本文小结

函数调用让 VM 拥有了组合能力:Call Frame 隔离每次调用的状态,参数和返回值负责在 frame 之间传递数据。

下一篇将继续讲:

plaintext 复制代码
第 9 篇:闭包:函数如何记住外部变量
相关推荐
泯泷3 小时前
那段文字是谁删的?Yjs 14 正式版之前,一套删除归属方案的实现与边界
前端·javascript·算法
hold?fish:palm3 小时前
34 合并K个升序链表
javascript·算法·链表
野槐5 小时前
前端项目优化(有了我,会发生...)
前端
幸运小圣6 小时前
PointerEvent 常用属性与事件整理【JavaScript】
开发语言·javascript·ecmascript
aa小小6 小时前
【无标题】
前端
计算机魔术师7 小时前
还在单线程跟 AI 聊天?Claude Code 并行多会话让你一个人干三个人的活
前端
志尊宝7 小时前
Vue3 零基础每日笔记(038):亲手封装 useDebounceFn 与 useThrottleFn——高频事件的流量阀
前端·javascript·vue·html·vue3
JavaGuide7 小时前
对标 MinIO!全新一代分布式文件系统正式发布
前端·后端
风骏时光牛马7 小时前
AI驱动自动化业务工作流搭建实践
前端
码云之上8 小时前
Skill 里的脚本终于能跑了,星悟接 CubeSandbox 的纪实
前端·人工智能·前端框架