编程语言如何工作
了解编译器的内部将使您能够更有效地使用它。 在这个按时间顺序排列的摘要中,概述了编程语言和编译器的工作原理。 我们输入了大量链接、示例代码和图表来帮助您理解。
作者注
了解编译器 - 是我上一篇文章第二篇文章的后续文章,浏览量超过 21000 次。 我很高兴能够对人们的教育产生积极的影响,并且很高兴能够根据我从原始文章中收到的反馈进行完整的重写。
- 为了
您是否点击了绿色的 run ,但不知道幕后发生了什么? 你想知道如何...
我选择 Rust 作为这项工作的主要语言。 它冗长、高效、现代,而且对于作者来说设计起来似乎非常简单。 我喜欢用它:
写这篇文章的目的是为了吸引读者的注意力,而不是读20页令人头脑麻木的文章。 本文包含许多链接,可将您引导至您感兴趣的主题以进行更深入的探索。 大多数链接都会带您到那里。
有任何问题或建议欢迎在底部评论区留言。 感谢您的关注,希望您喜欢这篇文章。
介绍
一般来说,你所说的编程语言实际上是一种软件,称为编译器,它读取文本文件,进行大量处理,并生成二进制文件。 由于计算机只能读取 1 或 0,因此人们将 Rust 编写为更高质量而不是二进制,并且使用编译器将人类可读的文本转换为计算机可读的机器代码。
编译器是将一种文本翻译成另一种文本的任何程序。 例如,这是一个用 Rust 编写的编译器,它将所有 0 转换为 1,将 1 转换为 0。
// An example compiler that turns 0s into 1s, and 1s into 0s.
fn main() {
let input = "1 0 1 A 1 0 1 3";
// iterate over every character `c` in input
let output: String = input.chars().map(|c|
if c == '1' { '0' }
else if c == '0' { '1' }
else { c } // if not 0 or 1, leave it alone
).collect();
println!("{}", output); // 0 1 0 A 0 1 0 3
}
尽管此编译器不读取文件,不生成 AST,也不生成二进制文件,但它仍然被视为编译器,因为它会转换输入。
简单来说,编译器读取源代码并生成二进制文件。 由于将复杂的、人类可读的代码直接转换为 1 和 0 非常复杂,因此编译器在程序可运行之前需要执行以下几个步骤:
当我说编译器立即从操作树转换为二进制时,它实际上生成汇编代码,然后将其汇编/编译为二进制。 汇编就像一个更高级别的、人类可读的二进制文件。 在这里阅读有关组装的更多信息:
解释器很像编译器,它们读取语言并处理它。 然而,解释器会跳过代码生成并即时执行 AST。 解释器的最大优点是在调试期间运行程序所需的时间。 编译器在执行程序之前可能会花费一秒到几分钟的时间来编译程序,而解释器则无需编译即可立即执行。 解释器最大的缺点是它需要在用户的计算机上安装解释器才能执行程序。
本文主要讨论编译器,但您应该了解它们之间的差异以及它们与编译器的关系。
##1. 词法分析
第一步是逐个字符地分割输入字符。 此步骤称为词法分析或标记化。 词法分析的主要思想是将字符组合在一起形成单词、标识符、符号等。词法分析大多不处理像 2+2 这样的逻辑——它只是说有三个符号:一个数字:2、一个加号和另一个数字:2。
假设你正在解析一个像 12+3 这样的字符串:它将读取字符 1、2、+ 和 3。我们有单独的字符,但我们必须将它们放在一起,这是符号化的主要任务之一。 例如,我们得到 1 和 2 作为单独的字母,但我们需要将它们放在一起并将它们解析为单个整数。 + 也需要被视为加号,而不是其字面字符值 - 字符代码 43。
如果你能看到代码并通过这种方式赋予它更多的含义,那么,根据 Rust 符号化,你可以将数字 32 位整数和加号组合起来作为 Token 值 Plus。
锈
您可以点击Rust界面左上角的“运行”按钮,在浏览器中编译并执行代码。
在编程语言的编译器中,词法分析器可能需要几种不同类型的符号。 例如:符号()、数字、标识符、字符串、运算符等。完全由语言本身知道需要从源代码中提取什么样的符号。
int main() {
int a;
int b;
a = b = 4;
return a - b;
}
Scanner production:
[Keyword(Int), Id("main"), Symbol(LParen), Symbol(RParen), Symbol(LBrace), Keyword(Int), Id("a"), Symbol(Semicolon), Keyword(Int), Id("b"), Symbol(Semicolon), Id("a"), Operator(Assignment), Id("b"),
Operator(Assignment), Integer(4), Symbol(Semicolon), Keyword(Return), Id("a"), Operator(Minus), Id("b"), Symbol(Semicolon), Symbol(RBrace)]
这是一段经过词法分析的C语言源代码,并打印出了它的符号。
##2. 解析
解析器是语法(分析)的核心。 解析器使用词法分析器生成的符号,尝试确定它们是否处于某种排列中,然后将这些排列与表达式相关联,例如调用函数、调用变量或数学运算。 解析器是一种从字面上定义语言的语法。
int a = 3 和 a: int = 3 之间的区别在于解析器。 解析器决定语法的外观。 它定义了括号和花括号的平衡,每个语句都以分号结尾,每个函数都有一个名称。 当符号与默认样式不匹配时,解析器就知道某些东西出了问题。
您可以编写几种不同类型的解析器。 其中最常见的是自上而下的递归下降解析器。 递归下降解析是最容易使用和理解的。 我编写的许多解析器示例都是基于递归解析。
语法可以用来概括解析器的解析。 诸如 EBNF 之类的语法可以描述解析器的简单数学运算,例如 12+3:
expr = additive_expr ;
additive_expr = term, ('+' | '-'), term ;
term = number ;
简单加减表达式的 EBNF 语法
请记住,语法文件不是解析器,但它确实概述了解析器的功能。 您可以按照此语法构造一个类似的解析器。 它由人类使用,比直接查看解析器的代码更容易阅读和理解。
该语法的解析器是 expr 解析器,因为它是顶级项目,基本上所有内容都与其相关。 唯一有效的条目必须是任意数字,后跟一个加号或减号,最后跟任意数字。 expr 期望为 1,这就是加法和减法主要发挥作用的地方。首先期望一个项(数字),后跟一个加号或减号,最后是另一个项。
解析12+3生成的AST示例
解析器解析时生成的树称为抽象语法树(tree),简称AST。 AST 包含所有操作。 解析器不需要计算(解析的)操作,它只是以正确的顺序收集它们。
我之前添加了词法分析器代码来匹配我们的语法并生成 AST,如图所示。 我用 //BEGIN// 和 //END// 标记了新解析器代码的开始和结束。
锈
我们实际上可以更深入。 假设我们只想支持数字而不对输入进行任何操作,或者添加乘法或除法,甚至添加优先级。 这允许快速更改语法文件并调整我们的解析器代码以反映这一点。
expr = additive_expr ;
additive_expr = multiplicative_expr, { ('+' | '-'), multiplicative_expr } ;
multiplicative_expr = term, { ("*" | "/"), term } ;
term = number ;
新语法
锈
C 语言的扫描器(又名词法分析器)和解析器的示例。 从字符序列“if(net>0.0)total+=net*(1.0+tax/100.0);”开始,扫描器构建符号序列并对它们进行分类,例如标识符、保留字、数字字符或运算符。 后续序列由解析器转换为语法树,然后由生成的编译阶段进行处理。 扫描器和解析器处理 C 语言语法中常规且正确的上下文无关部分。 版权所有: 。
原文链接:
生成代码
代码生成器使用 AST 以及等效的代码或汇编代码。 代码生成器必须以递归降序遍历 AST 中的每个项目(就像解析器一样),然后发出等效的代码。
- Rust(rustc 1.29.0)
pub fn main() { 让 a = 10 * 3; }
如果打开上面的链接,可以看到左侧示例代码生成的汇编代码。 汇编代码的第 3-4 行显示了编译器在遇到常量时如何生成常量代码。
是一个非常好的工具,可以让你用高级语言编写代码并查看它生成的汇编代码。 您可以对它进行一些有趣的实验,看看它会生成什么样的代码,但不要忘记向编译器添加优化标志以显示它是多么智能。 (Rust 语言使用 -o)
如果您对 ASM 中编译器如何将局部变量保存到内存感兴趣,这篇文章(代码生成部分)详细解释了堆栈。 大多数时候,当变量不是本地变量时,高级编译器会在堆上为变量分配内存并将其存储在那里而不是堆栈上。 您可以在答案中阅读有关存储变量的更多信息。
由于装配是一个完全不同的、复杂的主题,因此我不会特别谈论它。 我只是想强调代码生成器的工作和重要性。 此外,代码生成器可以生成的不仅仅是程序集。 Haxe 的后端可以生成六种以上不同的编程语言; 包括 C++、Java 和 .
后端是指编译器的代码生成器或评估器; 因此,前端是词法分析器和解析器。 也有中频,主要是进行优化,并且本节之后将解释 IR。 后端大部分与前端无关,只需考虑接收到的 AST。 这意味着相同的后端可以重复用于多种不同的前端或语言。 著名的 GNU 就是其中之一。
我的 C 编译器后端是代码生成器的最佳示例; 你可以在这里找到它。
生成程序集后,会将其写入新的程序集文件(.s 或 .asm)。 该文件被传递给汇编器,汇编器是进行汇编并生成等效二进制文件的编译器。 然后,二进制代码被写入一个称为目标文件的新文件 (.o)。
目标文件是机器代码,但不可执行。 为了使它们可执行,需要将目标文件链接在一起。 链接器获取此通用机器代码并使其成为可执行文件、共享库或静态库。 有关连接器的更多信息,请单击此处。
链接器是基于操作系统的可变有效程序。 单独的第三方链接器应该能够编译后端生成的目标代码。 在制作编译器时,无需创建自己的链接器。
编译器可能有中间表示(,IR)。 IR 用于无损地表示原始指令以进行优化或翻译成另一种语言。 IR不是原始源代码; IR 是一种无损简化,用于发现代码中潜在的优化。 循环展开和矢量化是使用 IR 完成的。 更多 IR 优化示例可在此 PDF 中找到。
总结
当您了解编译器时,您可以更有效地使用您的编程语言。 也许有一天您会想要制作自己的编程语言? 我希望这篇文章可以帮助你。
参考与延伸阅读
原文链接
— 对于 (2)
- 结尾 -
看雪ID:
本文由看雪翻译组整理校对
来源卢克@