从 .erl 文件中的源代码模块编译为二进制 .beam 文件。
可以使用 shell 快捷命令 c()(它也会在编译后加载模块),
或者通过调用 compile:file()(编译但不加载)
参数P:预编译, 让你看到 Erlang 预处理器做完工作后、但还未被真正编译前的源代码样子。这对于理解宏和 include 文件如何被处理非常有用!
编译时不同参数的意义(P,E,S) 可以看到不同阶段时,编译的源码是什么情况
编译通道:简单来说就是.erl在编译时会经过不同的处理程序,看图来说好像有:
前端流程:
从 .erl 源码到 Core Erlang 的处理流程:
.erl 文件
↓
[1] Scanner (erl_scan)
• 将源代码文本转换为标记流(tokens)
• 去除空格和注释
输出: 标记序列
↓
[2] Preprocessor (epp)
• 处理 -include() 包含文件
• 展开 -define() 宏定义
• 处理 -ifdef/-ifndef 条件编译
输出: 展开后的标记 → (.P) [P参数输出]
↓
[3] Parser (erl_parse)
• 根据 Erlang 语法检查标记
• 将标记转换为 AST(Abstract Syntax Tree)
输出: AST 数据结构
↓
[4] Parse Transforms
• 用户定义的 AST 转换(可选)
• 使用 -compile({parse_transform, module})
输出: 修改后的 AST → (.Pbeam) [makedep选项]
↓
[5] Linter (erl_lint)
• 检查语法错误
• 检查未定义的变量/函数
• 生成警告信息
输出: AST → (.P) [P参数] / (.AST) [AST, binary参数]
↓
[6] Save AST for debug info
• 保存完整的 AST(如果指定 +debug_info)
• 用于调试器设置断点、单步执行
输出: 调试信息
↓
[7] Expand Imports and Records
• 展开 import 导入(转为明确的远程调用)
• 展开 record 记录(转为 tuple 元组操作)
• 添加 module_info/{1,2} 内置函数
输出: 展开后的 Erlang → (.E) [E参数] / (.E, binary参数)
↓
[8] Core Erlang
• 转换为更规范的函数式语言
• 引入 let/letrec 表达式
• 优化前的最后一步
输出: Core Erlang → (.core) / (.core, binary参数)
↓
↓ 进入后端流程:
从 Core Erlang 到 .beam 二进制的处理流程:
Core Erlang
↓
[1] Core Erlang 优化通道
Core Pass 0
Core Pass 1
...
Core Pass N
• 多次优化遍历
• 常量折叠、死代码消除等
输出: (.core) / (.core, binary参数)
可中断点:from_core .erl 文件重新开始 ←→
↓
[2] Kernel Erlang
• 将 Core Erlang 平化
• 每个变量唯一化
• 编译模式匹配到原始操作
输出: 内核表示(无标准文件格式)
↓
[3] Kernel 优化通道
Kernel Pass 0
Kernel Pass 1
...
Kernel Pass N
• 继续优化和简化
↓
[4] BEAM Assembly Code (ASM)
• 生成 BEAM 虚拟机指令
• 每条指令都是 Erlang 术语
• {move, Src, Dst}、{call, Arity} 等
输出: (.S) [S参数] / (.S, binary参数)
可中断点:from_asm .S 文件 ←→
↓
[5] ASM 优化通道
ASM Pass 0
ASM Pass 1
...
ASM Pass N
• 死代码消除
• 窥视孔优化(peephole optimization)
↓
[6] BEAM Binary Format
• 将 ASM 指令打包成二进制格式
• 可以写入 .beam 文件
• 可以通过网络发送
• 可以通过 code:load_binary/3 加载
↓
(.beam 文件或内存)
编译通道的意义:
为什么要这样设计?
- 模块化:每个阶段职责清晰
- 可观察性:可以查看任何中间阶段的输出,便于学习和调试
- 可扩展性:可以从任何中间点开始编译,方便定制编译流程
- 优化机会:多个 Pass 可以在不同层次做优化(Core、Kernel、ASM)
- 跨语言支持:其他语言可以生成 Core Erlang 或直接生成 BEAM ASM
为什么需要分成前端和后端的说法?
前端(语言相关)、后端(机器相关),前端处理各种语言(像Elixir这样的)最终变成Core Erlang的流程,后端仅需要处理BEAM二进制,这样来说分开之后后端就不需要管前端是怎么生成Code Erlang的了,其实就是分离设计,前端怎么变,后端可以复用就可以适应各种情况,CodeErlang也可以说是中间产物
2.3 输出中间生产
编译器可以在每个编译器通道之后显示中间代码和最终 beam 代码,看得到编译器每一步生产的代码可以便于我们理解虚拟机的运作
.S文件的解析,这是可读的"汇编器"形式
{labels, 7},它告诉 VM 代码中标签的数量,以便Vm在一次遍历中
提前知道这个文件有多少个标签,来为所有标签分配空间
2.4 编译器通道
2.4.1 Erlang 预处理器(epp)
分词器 erl_scan 将文本扩展为单独标记的序列,丢弃空格和注释
重点,扫描器只管标记级别的、不管字符级别,对于扫描器来说,标记级别才是他关注的东西,字符串级别的东西看不懂,比如说 ++ ,扫描器看起来是这样的:{symbol, +}, {symbol, +},不会自动变成++, 只会变成+ +
2.4.2 Erlang 解析器
从预处理器获取最终标记序列,并对其进行 Erlang 语法检查
生成代表程序是一个数据结构的 抽象语法树AST,而不是顺序文本
函数:hello() -> world.
AST(树形结构)
{function, 1, hello, 0,
[{clause, 1,
[], % 参数列表(空)
[], % guard(保护)
[{atom, 1, world}] % 函数体
}]
}
树形表示的好处:
- 清晰表现代码结构
- 容易遍历和分析
- 便于代码变换
2.4.3 解析转换
对AST 进行操作的函数。在编译器完成初始标记化、预处理和解析之后,假设到目前为止
没有错误,如果已声明了解析转换函数,它将调用该函数,将当前 AST 传递给它,并期望获得修改后的
AST 返回,可以自定义解析转换-compile({parse_transform, my_pt}). 在2.5章节有详细介绍
2.4.4 Linter
在解析和转换完成后,检查样式错误和反模式以及编译器本身没有检查的其他内容
linter 检查错误,如未定义的变量和函数,以及为正确但可疑的代码生成警告,如"导出全部标志
已启用"。
2.4.5 保存 AST
为了启用模块的调试,你必须"调试编译"模块,即将 debug_info 选项传递给编译器。抽象语法树
将由"保存 AST"处理通道保存,直到编译结束时将其包含在 .beam 文件中,用于设置断点、单步执行
等目的。然后代码运行时被解释,不是编译的。
2.4.6 展开
使用编译 'E' 来输出就可以了
在展开阶段,源级别的 Erlang 构造(如记录)被展开为较低级别的 erlang 构造。这删除编译器指令,
用元组上的操作替换记录语法,将函数导入的使用展开为显式远程调用,并将内置函数 module_info/{1,2}
添加到代码中。
2.4.7 Core Erlang
Core Erlang是适用于编译器优化的一个严格函数语言,通过减少表达相同操作的方式数量来简化代码转换
用编译参数 to_core 来生成.core文件,这就是Core Erlang,.core是人类可读的格式(默认),如果你想改成erlang term的格式可以加上 binary 的参数来编译,虽然我觉得也是人类可读就是了
.core 默认文本格式, 加上binary就是 erlang term的格式
两者有区别:文本格式:纯文本、缩进清晰,易读
erlang term格式:erlang数据结构(非文本)、可以直接用应用程序操作、速度快(已经解析)
2.4.8 核心 Erlang 的内核表示
核心 Erlang 的内核是一个平化版本,有一些差异。例如,每个变量都是唯一的,作用域是整个函数。
模式匹配被编译为更原始的操作。内核表示没有良好定义的文件格式,你不应该期望它是稳定的。
2.4.9 BEAM 汇编代码
正常编译的最后阶段是外部 BEAM 汇编代码格式,作为表示为 Erlang 术语的单个 BEAM 指令的序列。
一些低级优化(如死代码消除和窥视孔优化)在这个级别进行。
BEAM 代码在第 7 章和附录 B 中详细描述。
2.4.10 BEAM 二进制格式
BEAM 汇编代码最后被打包成二进制传输格式,可以写入 .beam 文件、通过网络发送或直接加载到内存中
通过 code:load_binary/3。有关详细信息,请参见第 6 章。
2.5 编写 Parse Transform 解析转换
不建议,相当于在编译器中自己编写一个额外的处理通道,而且是在发明自己的语法,让其他人很难去读懂,
但是是有其他官方的框架再用,比如说QLC(Mnesia 查询语言、EUnit(单元测试框架),已经经过大量测试的,而且是OTP或者其他官方推荐的技术的标准库的一部分
如果你非要用,只推荐在这些情况用:库级别的实现或者工具
2.6. 其他编译器工具
三个最有用的代码工具:
词法分析器——Leex、
解析器生成器——Yecc
一组用于操作抽象形式的通用函数——语法工具