我还在琢磨那些小玩意儿。它们是怎么运作的?里面有什么?我需要胰腺吗?如果我不想让我的胰岛素抵抗正常化呢?懒惰算美德吗?
Haskell风格的语言看起来可能很相似,但它们在许多方面却截然不同:
大多数实现都使用标准编译阶段:
- 词法分析:源 → 词法流
- 解析:词法单元 → 表面抽象语法树
- 脱糖:表面AST → 核心AST
- 类型推断:核心抽象语法树 → 类型化抽象语法树
- 模式匹配编译:类型化抽象语法树 → 案例树
- 标准化(ANF/K) :分型 AST → 标准化 IR
- 优化:归一化IR → 归一化IR
- 闭包转换:归一化IR → 闭包显式IR
- 代码生成:Closure IR → 目标(汇编/字节码/C/LLVM)
- 寄存器分配:虚拟寄存器 → 物理寄存器(如果是本机寄存器)
- 运行时系统:垃圾回收器、原语、入口点
严格型 vs. 懒惰型
严格求值是指在将参数传递给函数之前先对其进行求值。惰性求值是指只有在实际需要参数值时才对其进行求值;求值结果会被缓存,因此最多只执行一次。
-- lazy eval returns `3` without applying `foo` length [ 1, foo 2, 4 ]
| 方面 | 严格(ML,OCaml) | 懒惰(Haskell) |
|---|---|---|
| 正常化 | ANF / K-正常形式 | STG / thunks 需要 |
| 关闭转换 | 标准扁平扣 | 关闭 + thunk + 更新框架 |
| 代码生成 | 直截了当 | 需要执行 eval/apply 或 push/enter 操作 |
| 内存管理 | 数值总是会被评估的。 | 可能包含未经评估的 thunks |
| 尾部呼叫 | 简单(跳跃) | 复杂(进入,更新) |
| 调试 | 简单(调用堆栈有意义) | 困难(发出晦涩的控制流) |
| 运行时复杂度 | 更简单(约 200 行代码) | 更复杂(约 500–2000 行代码 C) |
严格求值是最简单的选择。如果想要惰性求值,Peyton Jones 的STG 机器是标准方法。MicroHs 则绕过了 STG 机器,直接编译成带有图归约的组合逻辑。
惰性求值还可以实现无限集合——您可以定义一个无限列表,并且只使用您需要的内容。
咖喱味 vs. 淡味
| 风格 | 示例 | 实施成本 |
|---|---|---|
| 咖喱 | Haskell、Ben Lynn、MicroHs | 在组合器后端中是免费的;原生后端需要进行参数数量分析,以避免为每个参数分配一个闭包。 |
| 平淡 | MinCaml、OCaml(内部使用)、Grace、EYG | 更简单的代码生成器——多参数函数只是接受元组或多个参数的函数。 |
在柯里化语言中, fxy表示((f) x) y :即两次函数调用。如果你的后端没有检测到f总是接受两个参数(参数数量分析),那么每次多参数调用都需要进行堆内存分配。
自举式与托管式
我尝试自学吉他,但我教得很糟糕——因为我根本不会弹吉他。
——米奇·赫德伯格
大多数编译器都是用现有语言(例如 C、Rust、Haskell、OCaml)编写的,并依赖于宿主的生态系统来进行解析库、构建工具和包管理。
自举编译器可以自行编译。你用编译器所编译的语言编写编译器,然后使用早期版本的编译器(或最小种子运行时)来构建下一个版本。你的语言变得能够自我维持;编译器本身就是一个测试套件。
有很多优秀的自托管语言值得学习:
- MicroHs是一个 Haskell 编译器,它将 Haskell 代码编译成组合子。组合子归约器是用 C 语言实现的。编译器本身是用 Haskell 编写的,并且可以自我编译。启动时只需要一个 C 编译器,无需预先安装 Haskell。
- Ben Lynn首先用 C 语言编写了一个最小运行时环境(约 350 行代码),然后构建功能越来越强大的编译器,每个编译器都使用前一个编译器能够编译的子集语言编写。每个阶段大约需要 100 到 300 行所定义语言的代码。整个过程大约需要 2000 行代码加上 350 行 C 代码。
C runtime (350 LOC) → compiler₁: lambda calculus + integers → compiler₂: + let, letrec, ADTs → compiler₃: + type inference → compiler₄: + pattern matching → compiler₅: + type classes → ... → compilerₙ: near-Haskell-98
- Newt是一种依赖类型语言,其编译器是用 Newt 编写的,目标平台是 JavaScript。它通过将生成的 JS 代码检入代码库来实现引导。当目标平台是高级运行时环境(JS、JVM)而非原生代码时,这种方式效果最佳。
解释型与汇编型
解释器通过遍历抽象语法树(AST)或单步执行字节码来直接执行程序。编译器则将程序翻译成另一种语言(例如 x86、C、JS),并让目标平台处理执行。
这里的界限很模糊。字节码虚拟机编译成中间形式。“转译器”则编译成源代码,而不是机器指令。
| 战略 | 示例 | LOC 估算 | 权衡 |
|---|---|---|---|
| 树行翻译员 | PLZoo poly ,Eff,Frank,Grace,1ML |
50–200 | 最简单。无需代码生成,无需运行时环境。速度慢(比原生速度慢 10-100 倍)。 |
| 字节码虚拟机 | OCaml (ZINC)、Tao、PLZoo miniml |
200–500 | 中等价位。便携,速度适中。可编写约 30-50 条指令 |
| 本地编译 | MinCaml、mlml、AQaml | 500–1500 | 执行速度快,但您需要自行决定寄存器分配、调用约定和应用程序二进制接口 (ABI)。 |
| 转译为 C | Koka、Scrapscript、Chicken、Austral | 200–500 | 两全其美——既有本地设备的速度,又有 C 编译器完成的复杂部分。 |
| 转译为 JS/Go | 纽特,SOSML,博尔戈 | 200–400 | Web/生态系统部署,但您将继承目标平台的性能模型 |
| 组合子简化 | 本·林恩,MicroHs | 100–300 | 没有闭包,没有寄存器。C 语言编写的图归约求值器。简单但速度慢。 |
一些小型趣味语言通常是解释器。无需编译,就可以跳过闭包转换、寄存器分配和运行时系统。从解释器到编译器的飞跃大约需要 500 到 2000 行代码。
名义类型与结构类型
type Meters = Int type Seconds = Int -- Nominal: Meters ≠ Seconds (different names) -- Structural: Meters = Seconds (same shape)
| 风格 | 示例 | 结果 |
|---|---|---|
| 名义上的 | OCaml、Haskell、Austral | 名称即身份——形状相同并不意味着类型相同 |
| 结构 | EYG、Grace、TypeScript、Simple-sub | 形状即标识——相同的字段/变体意味着相同的类型 |
大多数 ML 系列语言对于代数数据类型是名义类型的,而对于记录类型(如果已实现)则是结构类型的。行多态(EYG、Grace、Koka)本质上是结构化的——它作用于“至少包含这些字段的任何记录”。简单子类型更进一步:它支持并集和交集类型,并且保留了主类型推断。
漂亮的错误与丑陋的错误
-- Ugly: Error: type mismatch: int vs string -- Pretty: 3 │ let x = 1 + "hello" │ ^^^^^^^^ Error: I expected an `int` here, but got a `string`. The left side of `+` is `int`, so the right side must be too.
漂亮的错误信息并非涂上油漆就能实现的。要指出代码中的某一行/某一段,必须逐个遍历编译器的每个阶段。一个最小可行错误系统:
- 源跨度遍历每个抽象语法树 (AST) 节点。每个表达式、模式和类型都包含
{ file, start_line, start_col, end_line, end_col }。这导致每个节点额外占用一个字段。 - 通过去糖化保留跨度。当你降低
wheretolet时,新的let节点会继承 `where的跨度。 - 通过类型推断保留跨度。当合一失败时,您需要两种冲突类型的跨度。
- 格式错误及上下文。请显示源行,用下划线标出相关段落,并解释错误原因。
| 质量 | 示例 | 成本 |
|---|---|---|
| 榆树层 | 榆树,澳大利亚 | 针对每种故障模式定制错误信息。力求做到最好,用户体验最佳。 |
| 足够好了 | Tao、Ante、OCaml | 源跨度 + 通用格式。涵盖 90% 的情况 |
| 位置 | MinCaml,大多数小型编译器 | 行号,但没有跨度高亮显示或解释 |
| 德布鲁因指数 | 详述动物园(故意) | 变量名丢失——这对研究来说没问题,但对用户来说很糟糕。 |
莱克星
| 方法 | 使用者 | LOC 估算 | 笔记 |
|---|---|---|---|
| 手写递归 | MinCaml(Rust 端口)、Tao、Ante | 100–300 | 完全掌控,最佳失误 |
| ocamllex / mlllex | MinCaml(原始)、HaMLet、PLZoo | 50–100 | OCaml/SML 主机标准 |
| 亚历克斯(哈斯克尔) | MicroHs,许多 Haskell 托管的 | 50–100 | Haskell 主机标准 |
| 解析器组合器(集成) | 本·林恩,一些教育 | 0(解析器的一部分) | 无词法分析器语法分析 |
可选增强功能:
- 布局/缩进敏感性(Haskell 风格的越位规则):Ben Lynn 在后续的引导阶段实现了此功能。MicroHs 包含完整的布局解析。代码量增加 100-300 行。该算法在Haskell 报告的 2.7 节中有详细描述。
- Unicode 标识符:大多数小型编译器完全忽略这一点。Koka 支持它。
- 插入字符串:类似
"hello ${name}"这样的语法在 ML 系列语言中不是标准语法,但一些较新的语言添加了它。
解析
解析将扁平的词法单元流转换为树状结构。表面语法被解析为具体语法树 (CST) 或直接解析为抽象语法树 (AST)。ML 系列语言拥有良好的语法,几乎是LL(1)的。
| 方法 | 使用者 | LOC 估算 | 笔记 |
|---|---|---|---|
| 递归下降 + 普拉特/优先爬升 | MinCaml(Rust 端口)、Tao、Ante | 200–500 | 最佳错误信息,最易于扩展 |
| ocamlyacc / mlyacc (LALR) | MinCaml(原版),HaMLet | 100–200(语法文件) | 标准但较差的错误恢复 |
| 解析器组合器(Parsec 风格) | Ben Lynn,MicroHs,PLZoo | 100–400 | 优雅、构图、回溯 |
| PEG / Packrat | 在ML家族中罕见 | 100–300 | 线性时间保证 |
后续的每个阶段都会转换这种类型。在 ML 系列语言中,抽象语法树 (AST) 通常如下所示:
type expr = | Lit of literal (* 42, 3.14, "hello", true *) | Var of name (* x *) | App of expr * expr (* fx *) | Lam of name * expr (* fun x -> e *) (or \x -> e) | Let of name * expr * expr (* let x = e1 in e2 *) | LetRec of name * expr * expr (* let rec f = e1 in e2 *) | If of expr * expr * expr (* if c then t else f *) | Tuple of expr list (* (a, b, c) *) | Match of expr * branch list (* match e with p1 -> e1 | ... *) | Ann of expr * type (* (e : t) *)
名称解析与去糖化
在类型推断之前,表面抽象语法树(AST)被简化:
- 字母重命名:每个绑定器都被分配一个唯一的标识符,以消除类型遮蔽。MinCaml 的 Rust 移植版在类型检查期间执行此操作。大多数版本在解析期间或单独的解析过程中执行此操作。
- 固定性解析:中缀运算符会根据声明的优先级和结合性重新关联。HaMLet 会单独执行此操作。许多小型编译器会在解析器中硬编码运算符优先级。
- 脱糖:表面结构被降低到核心结构中:
-
where子句 →let - 模式匹配中的守卫 → 嵌套
if -
do法(单子)→>>=链 - 列表推导式 →
concatMap - 运算符部分 → lambda:
(+ 1)变为fun x -> x + 1 - 记录语法 → 位置构造函数 + 访问器函数
- 类型类实例 → 字典传递(详述)
-
类型推断
这是 ML 系列语言的核心。“标准”算法是 Hindley-Milner (HM) 类型推断,具体来说是算法 W或算法 J。
核心组件:
- 类型表示:类型是由类型变量、类型构造器和函数箭头构建的项:
type ty = TVar of tvar | TCon of string | TArr of ty * ty | TTuple of ty list - 合一:给定两个类型,找到使它们相等的最一般替换。实现方式为对类型变量进行并查集结构体处理,并带有条件检查。
- 概括:在
let边界处,类型中的自由类型变量被普遍量化,以产生多态类型方案:∀α. α → α。 - 实例化:当使用多态名称时,其方案会使用新的类型变量进行实例化。
-- Given: let id = fun x -> x in (id 1, id true) -- Type inference trace: -- 1. id : α → α (infer: x has fresh type α, body is x) -- 2. generalize: id : ∀α. α → α (α is free at let boundary) -- 3. id 1: instantiate α=β, unify β→β with int→γ, get int -- 4. id true: instantiate α=δ, unify δ→δ with bool→ε, get bool -- 5. result: (int, bool)
| 方法 | 使用者 | LOC 估算 | 笔记 |
|---|---|---|---|
| 算法 W(基于替换) | 算法 W 逐步详解,PLZoo | 150–400 | 最容易理解的是,积极地进行替换 |
| 算法 J(可变引用) | MinCaml,大多数生产编译器。 | 100–300 | 效率更高,使用可变统一变量 |
| 基于约束的(HM(X)) | GHC,一些研究编译器 | 500–2000 | 将约束生成与求解分离;可扩展 |
| 双向类型检查 | 精细化动物园,一些依赖类型系统 | 200–500 | 交替使用检查/推理模式;可扩展至依赖类型 |
但高级的字体系统功能并非免费:
| 增强 | 增加了复杂性 | 使用者 |
|---|---|---|
| 类型类/特征 | +500–2000 LOC | Haskell、MicroHs、Ben Lynn(后期)、陶 |
| 行多态性(可扩展记录/变体) | +300–800 LOC | Koka、1ML、EYG、Grace |
| 高等类型 | +200–500 行 | 哈斯克尔,科卡 |
| GADTs | +500–1500 LOC | GHC,OCaml 4.x+ |
| 代数效应(已键入) | +500–1500 LOC | Koka、Eff、Frank |
| 从属类型(完整) | +1000–5000 LOC | 精益动物园,伊德里斯,精益 |
| 代数子类型(并集/交集) | +500 LOC | 简单子程序,MLscript |
| 一等多态性(系统 F) | +300–1000 行 | 1毫升,MLF |
| 模块系统(函子、签名) | +1000–5000 LOC | HaMLet、OCaml、1ML |
其他策略:
- 多态性:仅进行单态类型推断(不使用
∀量化)。每个类型都完全确定。这通过完全消除泛化和实例化,将类型检查器的代码量减少到约 100 行。类似id x = x的函数在每个使用点都会获得一个具体的类型。 - 细化:现代类型检查器越来越倾向于将细化(将表面语法转换为完全显式的核心)与合一(解决类型约束)分开。细化动物园清晰地展示了这一点:每个阶段都是一个包含 200 到 800 行代码的 Haskell 文件,逐步添加功能。
- 通过字典传递进行类型类脱糖: Haskell 风格的类型类通过将类约束转换为显式字典参数来实现。`sort
sort :: Ord a => [a] -> [a]变为sort :: OrdDict a -> [a] -> [a]。Ben Lynn 的编译器和 MicroHs 都采用了这种方法。
模式匹配汇编
通过推断类型,可以将模式匹配编译成高效的决策树或案例树。
| 方法 | 使用者 | LOC 估算 | 笔记 |
|---|---|---|---|
| 决策树(马朗热算法) | 大多数现代编译器,Tao,Ante | 200–600 | 最佳方案——无冗余测试,代码质量高。 |
| 回溯自动机 | 老旧的编译器,简单的实现 | 100–300 | 更简单但可能重复工作 |
| 嵌套 if/switch (简单) | 许多教育编译器 | 50–100 | 正确,但在最坏情况下会呈指数级恶化 |
| 完全省略 | MinCaml,PLZoo poly |
0 | 仅支持基本类型的if/then/else语句 |
| 去功能化 | 一些教育编译器 | 50–150 | 一系列带有传递过程的部分函数;更简单但效率较低 |
关键阶段:
- 穷尽性检查:如果匹配项未涵盖所有情况,则发出警告/报错。
- 冗余检查:如果模式不可达,则发出警告。
- 守卫汇编:守卫增加了一项“回溯”义务。
- 嵌套模式扁平化:
(Cons (x, Cons (y, Nil)))→ 测试序列。
权威参考文献是《编译模式匹配生成优秀决策树》 。Luc Maranget 的算法能够生成在测试次数方面可证明最优的决策树。OCaml 和 Rust 都采用了这种方法。
正常化
-- Before (nested expression): f (gx) (hy) -- After (A-normal form): let a = gx in let b = hy in fab
每个中间值都会被赋予一个名称。每个函数参数都变得无关紧要。let 链中let求值顺序现在是明确的。
标准化策略:
| 战略 | 使用者 | 特点 |
|---|---|---|
| K-范式(MinCaml 的 ANF 变体) | MinCaml及其衍生产品 | 直接式;用let命名所有中间值 |
| A-正常形式(ANF) | Flanagan等人,1993年,许多现代编译器 | 本质上与 K 范式相同;标准名称 |
| 延续传递风格(CPS) | Appel’s SML/NJ、Rabbit、CertiCoq | 每个函数都需要一个额外的延续参数;所有调用都是尾调用。 |
| 没有标准化 | 本·林恩 | 类型化抽象语法树 (AST) → 直接用于组合逻辑。适用于图归约,不适用于原生代码生成。 |
| 社会保障署直接 | Scrapscript | 跳过 ANF/CPS;采用 SCCP + DCE 的 SSA IR。其余部分由 LLVM/C 处理。 |
| 单子范式 | 一些依赖类型系统(Bowman,2024) | 类似于 ANF,但使用单子绑定而不是 let;在某些优化方面更简洁。 |
优化
如果程序处于标准形式,优化过程可以简化它。在小型编译器中,优化程度会保持在最低限度——目标是避免运行速度慢得令人尴尬,而不是与 GCC 竞争。
MinCaml 的优化过程(总计约 300 行代码):
| 经过 | LOC(MinCaml) | 影响 |
|---|---|---|
| β还原 | 约50 | 内联let x = y in ... x ... → ... y ... |
| 让扁平化(关联) | 22 | let x = (let y = e1 in e2) in e3 → let y = e1 in let x = e2 in e3 |
| 在线扩容 | 约100 | 用函数体替换对小函数的调用 |
| 持续折叠 | 约50 | 3 + 4 → 7 |
| 死代码消除 | 约50 | 移除当x不在e2中let x = e1 in e2 |
| 常见亚表达消除 | 约50 | (在 MinCaml 中是可选的,通过哈希函数实现) |
对于小型编译器而言,这六次迭代可以覆盖 80% 以上的优化值。它们会迭代应用,直到达到一个不动点(通常需要 2-3 次迭代)。
超越基础知识:
| 优化 | 复杂 | 影响 |
|---|---|---|
| 尾部调用优化 | +50–100 LOC | 循环是函数式语言的必备要素;循环本质上是递归调用。 |
| 已知呼叫优化 | +50 LOC | 当调用目标静态已知时,跳过闭包间接寻址。 |
| 开箱(专业化) | +200–500 行 | 避免对多态函数的单态用法进行装箱。 |
| 锥化 | +100–300 LOC | 将始终在尾部位置调用的函数转换为局部跳转 |
| 需求分析(严格程度) | +500–2000 LOC | 对于惰性语言:确定哪些参数始终会被求值。 |
| 工作/包装器转换 | +200–500 行 | 将严格参数与惰性参数分开,以提高代码生成效率。 |
| 森林砍伐/融合 | +500–2000 LOC | 消除中间数据结构(例如, map f . map g → map (f . g) ) |
| 全程序优化 | 变化 | JHC 通过 GRIN 实现这一点;它会消除未使用的构造函数,并进行全局特化。 |
关闭转换
-- Before: let f = \ x -> x + y -- After: let f = { fun = \ env x -> x + env.y , env = { y = y } }
优化后的中间表示仍然包含带有自由变量的函数。闭包转换会将所有函数“封闭”——因为硬件无法理解词法作用域。每个函数都会变成一个二元组:(代码指针,环境记录)。环境记录会在函数定义时捕获其自由变量。
| 方法 | 使用者 | 权衡取舍 |
|---|---|---|
| 平扣 | MinCaml、OCaml、大多数编译器 | 环境是一个包含捕获值的扁平向量。访问复杂度为 O(1),每个闭包只需一次内存分配。标准选择。 |
| 链接/共享闭合 | 一些较旧的Scheme编译器 | 环境是一个帧的链表。闭包之间共享结构。分配更多内存,访问速度更慢。 |
| Lambda提升 | GHC(选择性地),一些教育编译器 | 通过添加额外参数,完全消除了闭包。闭包本身无需堆内存分配。但调用者必须传递更多参数,并且调用点必须更新。 |
| 去功能化 | 雷诺兹(1972),MLton | 将高阶函数替换为基于求和类型的一阶分派。完全消除函数指针。需要进行全程序分析。 |
| 组合逻辑(括号抽象) | 本·林恩,MicroHs | 将 lambda 表达式替换为 SKI 组合子(或其变体)。不使用闭包,也不使用环境。通过图简化进行评估。 |
代码生成
代码生成完全取决于您选择的目标平台:
| 目标 | 使用者 | LOC 估算 | 权衡取舍 |
|---|---|---|---|
| 本地汇编(x86-64、ARM 等) | MinCaml、mlml、AQaml | 300–800 | 最佳性能,最多工作量,平台特定 |
| C 源 | Koka、Scrapscript、Chicken、JHC、Austral | 200–500 | 可移植,利用了 C 编译器的优化器,但间接寻址 |
| LLVM IR | Ante、gocaml、Harrop 的 MiniML | 200–500 | 原生性能好,跨平台,但依赖项较多 |
| 起重机 | MinCaml(Rust 移植版),以及一些新语言 | 200–500 | 编译速度比 LLVM 快,代码生成效果好,原生支持 Rust。 |
| 字节码(自定义虚拟机) | OCaml(ZINC 机器),PLZoo miniml |
200–500 | 便携、简单,但执行速度较慢 |
| JavaScript / Wasm | MinCaml-wasm、SOSML、Newt、各种 | 200–400 | Web部署,但性能模型有限 |
| 前往来源 | 博尔戈 | 200–500 | 继承 Go 的生态系统、工具和并发模型 |
| 组合逻辑 | 本·林恩,MicroHs | 100–300 | 无需分配寄存器,但执行速度较慢 |
| 规范化器(无运行时目标) | 达尔 | 200–500 | “编译”= 简化为范式。无可执行输出 |
寄存器分配
程序可以使用任意数量的变量,但CPU的寄存器数量是固定的。寄存器分配决定了哪些变量存储在寄存器中,哪些变量溢出到内存中。
如果目标是原生汇编,则需要自行实现。如果目标是 C/LLVM/Cranelift 等,后端会自动处理这些。
| 方法 | 使用者 | LOC 估算 | 质量 |
|---|---|---|---|
| 图着色(Chaitin-Briggs) | MinCaml,Appel 的教科书 | 200–500 | 大多数情况下都是最佳选择,标准配置 |
| 线性扫描 | 一些即时编译器,简单的编译器 | 100–200 | 编译速度快,但代码质量略差。 |
| 天真(什么都说了) | 一些教育编译器 | 50 | 正确但糟糕的表现 |
| 不适用 | 面向 C/LLVM/字节码的编译器 | 0 | 委托给后端 |
运行时系统
最简配置包括:
| 成分 | 复杂 | 笔记 |
|---|---|---|
| 入口点/堆栈设置 | 10–30 LOC C | 设置初始堆指针和栈指针 |
| 垃圾收集员 | 100–1000 LOC C | 见下文 |
| 基本操作 | 50–200 LOC C/asm | 输入/输出、数学运算、字符串操作 |
| 分配程序 | 10–50 LOC | 提升分配器(如果垃圾回收器处理垃圾回收) |
| 闭包表示 | 代码生成器的一部分 | 闭包在内存中的布局方式 |
一些有趣的编程语言会频繁分配内存——每个闭包、每个 cons 单元、每个局部变量调用都会分配内存。如果没有内存回收机制,内存很快就会耗尽。你需要防止内存垃圾堆积:
| 战略 | 使用者 | 复杂 | 笔记 |
|---|---|---|---|
| 无垃圾回收(内存泄漏) | 一些教育编译器,MinCaml 基准测试 | 0 | 适用于短期项目 |
| 切尼复制(半空格) | 许多小型编译器,Appel 的教科书 | 100–300 LOC C | 简单、快速,但占用两倍内存 |
| 标记清除 | 各种各样的 | 100–300 LOC C | 不移动物体,无需转发 |
| 参考计数 | 科卡(珀尔修斯),鲤鱼,雨燕状 | 200–500 LOC | 无需暂停时间;Perceus 通过编译时插入精确实现这一点,且没有任何额外开销。 |
| 基于区域的 | MLKit,一些研究语言 | 300–1000 LOC | 编译时内存管理,无垃圾回收暂停 |
| 竞技场/堆叠 | 非常简单的编译器 | 20–50 LOC | 在竞技场中分配,全部免费 |
| 所有权/仿射类型 | 锈迹、鲤鱼、瘦肉 4 | 0(编译时) | 无需运行时垃圾回收,但限制了语言 |
如果你的语言具有代数效应(例如 Eff、Frank、Koka 或 Ante),则运行时需要支持分隔延续或 CPS 转换后的调用约定。效应处理程序本质上需要第二个栈或分段栈来捕获延续。Koka 通过证据传递来处理这一点;Eff 和 Frank 则使用解释。