从 .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 文件或内存)

编译通道的意义:

为什么要这样设计?

  1. 模块化:每个阶段职责清晰
  2. 可观察性:可以查看任何中间阶段的输出,便于学习和调试
  3. 可扩展性:可以从任何中间点开始编译,方便定制编译流程
  4. 优化机会:多个 Pass 可以在不同层次做优化(Core、Kernel、ASM)
  5. 跨语言支持:其他语言可以生成 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

一组用于操作抽象形式的通用函数——语法工具