从零开始的编译原理(5):语义分析与中间代码生成

一、前言 我语言的局限,即是我世界的局限 —— 路德维希·约瑟夫·约翰·维特根斯坦 二、从文法树到语义 经过语法分析阶段的处理,我们将词序列组织成了一棵文法树。而在语义分析阶段,我们的目标就是理解该文法树的含义,并按其含义将该文法树翻译含义等价的另一种语言。 上面的表述十分宽泛。这是因为语义分析阶段不像前两个阶段有着明确的输入输出和确定的算法,根据使用场景的不同采用各自的处理方法。 举个例子,对比 C 语言和 SQL 语言,虽然它们都会采用编译技术进行语句的处理,但是 C 语言的语义分析产物是类似汇编的中间代码;而 SQL 语言的语义分析的结果则是对数据库中数据的处理操作。 因此,语义分析并没有通用的算法,就算同样是编程语言,面向过程、面向对象、函数式等不同的语言类别在语义分析上也有许多不同的处理。所以在本篇文章中,我们只会介绍最为简单和普遍的一种语义分析方法,那就是语法制导翻译。 三、语法制导翻译 (1)属性翻译文法 通过前面的文章,我们知道文法树表达了句子的结构。这种结构从语义上来说有两种含义: 一是指整体由部分组成。这反映了文法树中父节点的含义。例如对于表达式会 $1 + 2$,表达式本身是文法树中的父节点,而 $1$ $+$ 和 $2$ 则是子节点。 二是指部分处于整体之中。这反映了文法树中子节点的含义。 在使用语法制导翻译进行语义分析时,这两种含义我们称为属性。第一种属性由 “父” 传递到 “子”,称之为继承属性;第二种属性由 “子” 传递到 “父”,称之为综合属性。 (2)符号表管理 四、中间代码

三月 11, 2024 · 1 分钟 · 39 字 · Wokron

从零开始的编译原理(4):语法分析与下推自动机

一、前言 从这个精神王国的圣餐杯里, 他的无限性给他翻涌起泡沫。 —— 格奥尔格·威廉·弗里德里希·黑格尔《精神现象学》下卷 第八章 绝对知识 二、从正则文法到上下文无关文法 词法分析阶段,我们可以使用正则文法描述词的定义,这是因为词的结构是无嵌套的。也就是说,词只会由例如前缀、后缀以及结构的顺序重复等概念描述,不可能出现需要前后匹配的地方。举个例子,正则文法不能定义形如 $12344321$ 的回文数;或形如 $(()(()))$ 的括号结构。 然而,所有的高级语言却都是嵌套的。通过嵌套,高级语言能够表示更加复杂的结构,实现控制流、子程序、作用域、类等等高级概念。因此,正则文法并不能描述高级语言的语法,我们需要使用 2 型文法,也即上下文无关文法描述高级语言。2 型文法区别于 3 型文法就在于其嵌套性。 举个例子,我们可以将上述括号结构用如下 2 型文法定义: $$ S \rightarrow (S)S | \epsilon $$ 其中规则 $S \rightarrow S(S)$ 中左括号和右括号同时出现,这样就保证了对于一个左括号,必定有一个右括号与其匹配。因而我们说上面的文法具有嵌套性。 而 3 型文法则不同。由于左线性和右线性的约束,3 型文法中不可能出现一条规则中右侧有多个终结符的情况。 不过虽然 2 型文法相较于 3 型文法具有更强的表达能力,但是这也为其解析提供了一定的困难。我们可以选择用前面定义的文法给出 $(()(()))$ 的文法树: 可以看出,相较于 3 型文法的文法树,2 型文法的文法树更像是树:并不是只向一侧增长,而是在任意地方都可能产生子树。因此,2 型文法的推导或归约过程不再能看作状态的转移,我们需要用有穷自动机之外的另一种模型描述,该模型应当具有接受嵌套结构的能力。这就是下推自动机。 三、下推自动机 (1)例子:判断有效括号问题 初学编程的时候各位应该都做过类似的题目,那就是判断有效括号。实际上这是一个编译问题。我们可以使用编译理论的语言将该问题表述为:对于符号集 $\Sigma = \{`(`, `)`, `[`, `]`, `\{`, `\}`\}$,对给定的括号文法 $G$,对于任意 $s \in \Sigma^*$,给出一个算法,判断是否有 $s \in L(G)$(即 $s$ 是否被文法 $G$ 接受)。...

二月 15, 2024 · 18 分钟 · 3631 字 · Wokron

从零开始的编译原理(3):词法分析与有穷自动机

一、前言 因而,每一生物的有机形体都是一个神圣的机器,或一台无限地优越于任何人造的自动机器的自然的自动机器(automata)。因为人的技艺所制造的机器的每一部分并非机器。……而自然的机器,即有机体,在其无限小的部分仍是机器。 —— 戈特弗里德·威廉·莱布尼茨《单子论》第 64 节 本章将从词法分析所用的 3 型文法入手,引入自动机的概念,并详细介绍有穷自动机的原理及实现。最终实现编译程序的词法分析模块。 二、正则文法的解析过程 在第 1 章的最后我们介绍了 3 型文法。3 型文法也称正则文法,特征是对于文法中的每一条规则,其右侧要么是一个终结符,要么是一个终结符加上一个非终结符,且非终结符只位于终结符的一侧。根据非终结符位置的不同分为左线性和右线性。 或许有些人会这样讲:“左线性是终结符在右侧;右线性是终结符在左侧”。但是这样实在太别扭了。应该说 “左线性是非终结符在左侧;右线性是非终结符在右侧”。 为什么通过这一约束,3 型文法就能区别于 2 型文法了呢?我们可以通过正则文法的解析过程来理解。 现在有一左线性正则文法 $G_1[Z]$: $$ \begin{align*} Z & \rightarrow A1 \\ A & \rightarrow B1 | 1 \\ B & \rightarrow A0 \end{align*} $$ 现在可能不太能看得出来,但是 $G_1[Z]$ 能够接受符号串 $101011$。我们可以画出此符号串对应的文法树。 通过文法树我们可以看出,对于左线性正则文法 $G_1[Z]$,其文法树只向左增长。当然这一点从规则中也可以看出,只不过不那么直观罢了。 可为什么具有这样特性的文法就特殊了呢?接下来我们根据文法树,以归约的视角看待符号串 $101011$ 的解析过程: 读入 1,1 归约得到 A; 读入 0,A0 归约得到 B; 读入 1,B1 归约得到 A; …… 以此类推,我们就可以发现其中的规律。对于这样的文法,读入一次即归约一次。因为文法中最多只有两个符号,所以一旦读入了一个新的符号,则必定进行一次归约。 同理的,我们考虑右线性正则文法 $G_2[S]$: $$ \begin{align*} S & \rightarrow 1A \\ A & \rightarrow 0B | 1 \\ B & \rightarrow 1A \end{align*} $$ $101011$ 也能被 $G_2[S]$ 接受。其文法树如下,对于右线性正则文法 $G_2[Z]$,其文法树只向右增长...

一月 10, 2024 · 9 分钟 · 1724 字 · Wokron

从零开始的编译原理(2):编译程序架构

一、前言 本质是存在的真理,是自己过去了的或内在的存在。 —— 格奥尔格·威廉·弗里德里希·黑格尔《小逻辑》第二篇 本质论 §112 本篇文章是一个间章。旨在衔接文法理论与编译过程,从架构上概述整个编译程序。文法理论旨在解释自然语言,而编译过程却要创造新的语言。编译程序通过将文法规则机械化,创造出易于理解的高级语言,实现了计算的高级抽象。 二、何为 “编译” 为了理解何为 “编译”,我们可以从一个具体的编译器开始。这里我们以 C 语言的编译过程为例: 现在有一个简单的由 C 语言文法写成的文本文件 test.c // test.c #include <stdio.h> int main() { printf("hello, world.\n"); return 0; } 想要将其编译为可执行文件,当然可以使用 gcc test.c -o test 或 clang test.c -o test,但是这样就不能反映编译的具体过程了。所以这里我编写了一个 Makefile,用来指明 gcc 编译时所经历的具体步骤。 # Makefile srcname = test cc = gcc # Default target all: $(srcname) @echo "Finish!" # Linking stage $(srcname): $(srcname).o @echo "Linking stage: Creating executable '$(srcname)'" $(cc) $(srcname).o -o $(srcname) # Assembly stage $(srcname)....

一月 6, 2024 · 3 分钟 · 442 字 · Wokron

从零开始的编译原理(1):文法理论

一、前言 一个人进行推理时…… 这种过程如果是用语词进行的,他便是在心中把各部分的名词序列连成一个整体的名词或从整体及一个部分的名词求得另一个部分的名词。 —— 托马斯·霍布斯《利维坦》第一部分 论人类 第五章 论推理与学术 当霍布斯将他的机械唯物主义运用于人的思想时,语言便成为了人脑的齿轮。人的精神当然并非机械,可这种认识却也敏锐地察觉到了语言的一些本质,那就是语言是一些规则,通过这些规则的拼凑,我们得以表述我们的思想。如今,这种规则我们称之为文法。 语言学家的一部分工作是从现有的自然语言中总结文法,让语言成为机械。但是这并不是我们的重点,我们所要探索的是一个更加年轻的领域,这个领域的研究从机械出发,却要去创造千变万化的语言,让机械成为语言。 二、自然语言的文法:一个例子 但是要开始我们的旅程,还需要从自然语言开始。当我们听别人说话,或者阅读一段句子的时候,我们究竟在理解什么?比如说,现在有这样一句话,你知道我的叉子被放在哪里吗。让我们尝试理解这个句子。 这部分默认各位已有了对词性的基本认识,至少知道名词、形容词、动词、副词等等的大致含义。否则的话,这部分内容实难描述。 另外注意对于自然语言的相关表述可能并不完全符合人的认知的真实情况,这一部分只是为了方便理解。 首先需要明确一点,抛开我们所有的经验来看,语言就是符号的序列。也就是说语言是在时间上存在先后顺序的许多符号。这意味着我们的理解过程必定是从前到后的。对于上述句子来说,我们先读到 你,之后才能读到 知,再之后 道、我 等等。 另外,在这样不断读到一个个符号的时候,我们也并非单纯的按照字母或音节分别认识,而是首先明确哪些音节或符号共同组成相同含义,并将其作为一个单词来识别。在自然语言处理领域,这称为“分词”;而对于编译来说,这称为“词法分析”。 我们理解这个句子的过程可能是这样的: 读到 你,这指代一个事物,为名词 读到 知道,这指代一个行为,为动词 读到 我的,这描述了事物的属性,为形容词 读到 叉子,这指代了另一个事物,为名词 形容词 我的 和名词 叉子 组成 我的叉子,也是个名词 读到 被放,这是动词。 读到 在,这表达了一个关系,为介词 读到 哪里,这是名词 介词 在 和 名词 哪里 组成 在哪里,用于描述行为,为副词 动词 被放 和副词 在哪里 组成 被放在哪里,也是个动词。 名词 我的叉子 和动词 被放在哪里 组成 我的叉子被放在哪里,用于陈述一个事实,称为陈述句 名词 你、动词 知道 和句子 我的叉子被放在哪里 组成了 你知道我的叉子被放在哪里,是另一个陈述句。 读到 吗,这是一个疑问语气词,用于表达疑问。 句子 你知道我的叉子被放在哪里 和疑问语气词 吗 组成 你知道我的叉子被放在哪里吗,表达对被陈述的事情真实性的询问,是疑问句。 读完了所有的单词,最后得到的疑问句就是整个句子。 在上述过程中,我们不断将小的句子成分归纳为大的句子成分,在这样归纳的过程中,我们将句子中的每个单词相互关联起来,最终理解句子表达的含义。从中我们可以知道这是一个疑问,想要得到的回答是“你”与“我的叉子被放”两个事物间是否存在“知道”的关系等等。(这样说话好别扭。)...

一月 4, 2024 · 8 分钟 · 1629 字 · Wokron

了解 LLVM IR,从手写开始

一、总览 如果你想要了解 LLVM,那么有几种可能呢?你或许是想要创造一门新的编程语言的技术爱好者;或是陷入编译课的泥沼不能自拔的大学生,但不管怎么样,你都只希望使用 LLVM 做一件事:生成目标代码。 但是这一过程并不容易。你大概率经过了编译前端的艰辛历程,但是现在在你面前的是另一座大山,LLVM IR。在最开始就看到什么基本块、虚拟寄存器、phi 节点之类的概念使你心烦意乱;各种各样的代码示例中充满了和重点并不相关的细节。这些都使你无从下手…… 或许一个更好的学习方式是将 LLVM IR 视为一种特殊的编程语言,而非编译器的 “中间表示”。这种语言站在了高级与低级语言之间,既具备了一定的抽象能力,又反映了底层汇编的工作原理。 本篇文章,就将以编程语言的视角粗略介绍 LLVM IR。通过直接手写 LLVM IR,逐步分析其特点和原理。希望能够有所帮助。 (1)准备工作 LLVM IR 虽然是中间代码,但是 LLVM 也提供了 lli 工具用于解释/即时编译执行 LLVM IR 文件。这样 LLVM IR 又与 Java 字节码有了一些相似之处。使用 lli 可以直接运行我们手写的中间代码,很是方便。使用 lli 这需要安装 LLVM。 sudo apt-get install llvm 另外,LLVM 中是没有内建输入输出的,因为 LLVM 的工作在操作系统之下。这就导致了一个问题,我们不能得知程序的执行结果。一种方法是可以用函数的返回值来输出,但是这样输出只能是单个数值。为了方便起见,可以先用 c 语言编写一个输出库。并使用 clang 将其编译成 LLVM IR。为此需要安装 clang sudo apt-get install clang 对于输出库,这里只简单定义 putint 函数。 // lib.c #include <stdio.h> void putint(int i) { printf("%d\n", i); } 使用 clang 将其翻译成中间代码。不要担心,我们并不需要关心生成的 lib....

十一月 3, 2023 · 17 分钟 · 3612 字 · Wokron