问候,
上周的技巧是一些娱乐时间,我们在其中构建了Sudoku求解器。 这个
一周,我们将构建一些复杂的东西:编译器。 编译器
构造是CS的一个困难分支,我不希望这篇文章成为
书的大小,因此我们必须使事情保持简单。
另一方面,整个事物或多或少必须具有实际用途,因此
要开发的代码必须是可扩展的,以便您可以播放和实验
如果愿意,可以尝试一下。
我们还希望看到一些结果,所以我们不能只是开发编译器,我们必须
还开发了一种解释器以查看实际运行情况。 但首先
我们必须仔细研究一些理论和定义:
介绍编译器通常将源语言翻译成目标语言。 这俩
源语言和目标语言可以是任何东西:源语言可以是
高级编程语言,可能是虚拟的汇编代码语言
机器,甚至是自然语言,例如英语,荷兰语或斯瓦希里语。 最
(但不是全部)时间,目标语言将是比
源语言。
编译器读取源语言并编写目标语言。 它是
翻译者基本上。 人类语言及其作为交流手段的用法
很难,尤其是涉及演讲时。 为了简单起见,我们
既不处理语音识别也不处理语音的困难方面
本身作为音素,词素,双语-唇语-牙齿-
口渴,声音,无声和其他有趣的声音。
但是即使涉及编程语言,事情也相当复杂。
最简单的说,编程语言是一系列字符,其中
某些字符组合组成一个“令牌”。 令牌是原子的
编程语言的实体。 想想保留字和符号,例如
如括号,数学运算符,数字等。但是空格可以
也是一个重要的象征; 大多数编程语言都将空格视为
分隔更有意义的标记的字符,但是它们非常
在字符串文字中很重要,例如“ helloworld”与“ hello world”不同
尽管对于人类来说,根据我们的启发式方法和
迷人的模式匹配能力,尽管我们无法真正解释*如何*
我们做到了。
编译器比人类愚蠢得多。 需要固定的规则来确定
字符序列是否构成有效令牌。 这是
编译器第一部分的任务:
分词器标记器(或“词法分析器”)尝试从序列中形成标记
字符。 看看“ x +++++ y”。 它可以代表
在语法上有效的标记序列[x] [++] [+] [++] [y]
Java,但这不是Java的标记器的工作原理(尝试一下)。
表示法[[...]'表示单个标记,由字符形成
序列 '...'。
Java令牌生成器将该字符序列视为一系列令牌,例如
这:[x] [++] [++] [+] [x]
为什么? 因为Java的标记器是“贪婪的”,即它总是扫描字符
从左到右的顺序,试图“切断”尽可能长的时间
有效令牌。 为什么? 因为它需要一个简单的规则,如何从
字符序列。 分词器对实际语言了解不多
需要编译; 它只知道有效的令牌,然后尝试
尽可能高效地产生一系列令牌。 所以人类用户
必须在某处放置空间以帮助令牌生成器:“ x ++ + ++ y”。 Java考虑
其他所有失败时,将空格分隔为令牌分隔符。
在其他情况下,令牌生成器可以正常工作而无需插入空格
到处都是:“ if(true)x = 1;”。 '('字符不能是令牌的一部分
[if(]或[if(t]或[if(tr]等),因为此类标记在Java中不存在。
这里最长的第一个令牌是[if],并且这里不需要空间。 的
当然,出于可读性的考虑,它可以帮助人们在此处和此处放置空间。
在C和C ++中,甚至需要括号的情况也更糟。
看一下这个C示例:
if (x) * (y=z);
对于y合适的指针值,这在C和C ++中是有意义的。
离开了
括号:
if x * (y=z);
这不再有意义:是否应该是一个(丑陋的)条件,例如:
“ x *(y = z)”后跟一个空语句,或者应该是条件“ x”
后跟一个(丑陋的)语句“ *(y = z);” 两种变体都没有多大意义
从语义上讲,但是编译器知道什么? 应该是
翻译这些字符序列,并产生另一种(机器代码)语言。
如果我写:“ <S-F6> <S-F7> fsggf *&^&^ *&kjgf(&”,您将不会理解我
因为以上*在词法上*没有意义,所以是分词器的主题
必须处理。 令牌生成器必须将字符序列切碎,然后
它必须从中形成令牌。
您可能从未意识到这一点,但其中很多括号,分号
大括号就可以帮助编译器的另一部分:
解析器解析器期望由令牌生成器生成的令牌流。 例如,
Java tokenizer处理以下字符序列:“)if(x;)(y”
作为令牌的完全有效序列:[]] [if] [(] [x] [;] [)] [(] [y]
但这对解析器没有多大意义。 令牌的顺序可以是
对令牌生成器有效,*语法上*,令牌序列没有意义。
人类非常善于理解语法错误
句子; 我们每天都写/说和听。
如果我写:“我现在喝啤酒要喝!” 你最有可能了解我
尽管句子在语法上是错误的 你不会
当我写信时明白我的意思:“船屋现在很想得到那个”
当然是这样。 如果我不知道我在说什么
写道:“房子现在要装船了”。
房屋不破坏船只。 虽然这句话在语法上是正确的
是没有道理的。 以“语义”方式没有意义。
人类擅长“理解”语法错误的句子,但我们不是
当涉及到毫无意义的句子时,它有什么优点; 我们发现
他们充其量不过是幽默,但我们并不十分在意语法的正确性。
然后,我们需要正确的语义。 编译器也这样做:
语义分析器编译器几乎无法处理语义:它们可以处理类型检查
就是这样。 大多数语义会转移到解释器或
虚拟机还是真实计算机。 编译后(翻译)
已经结束并完成了运行时环境(以任何形式)的处理
具有以下语义:检查是否除以零,检查是否
存在文件,如果可以打开套接字等。
编译器可以检查是否声明了每个使用的方法,如果每个
使用的成员变量已定义,它可以检查是否
用于定义在某处。 它还可以执行以下简单操作:
int x= "hello world";
这是一个完美的令牌流,从语法上讲一切都很好,但是令牌的类型
有两件事是不正确的:我们不能将字符串分配给int; 该程序
片段在语义上不正确。 但是在运行时系统可以做之前
首先,必须生成“任何东西”:
代码生成器代码生成就像将字节发送到某个地方一样简单(机器代码
代),但它也可能很复杂; 看这个小例子:
double x= 1/42.0;
x= Math.sin(x)/Math.cos(x)+Math.log(x)-3.14159;
x= 0;
初始化真的有必要吗?
那复杂的表情怎么样?
显然不是因为最后x始终会被设置为0。
那么为什么要为所有这些生成代码? 也许用户想要调试此代码,所以
无论如何,所有代码都应生成。 也许用户想要优化的代码,所以
几乎根本不应该生成任何代码:应该进行简单的“ x = 0”初始化
足够。 代码生成器的工作是在
它所知道的上下文。 代码生成器对字符一无所知
或令牌序列。 分词器和解析器负责这一点。 解析器确实
需要时调用代码生成器,但解析器不知道是否
并非真正需要该代码。
通常,代码生成器由解析器调用; 他们知道需要什么代码
是否生成,是否冗余。 代码生成器应该找出
是否真的需要所有这些代码。
结束语本文的第一部分介绍了以下几个阶段(或模块)
编译器。 严格来说,解释器或运行时系统不是一部分
编译器。 首先,字符流被转换为令牌流。
检查令牌流在语法上是否有意义。
解析器知道何时调用代码生成器。 代码生成器必须
生成有效的代码。 完成所有操作后,口译员便开始工作
要做的事情:执行用户最初使用字符流时要执行的操作。
实际上,相之间的分离可能会有点模糊,即
解析器更改令牌生成器的行为,代码生成器查询
解析器了解一些语义细节,令牌生成器再次与解析器对话
等。但是模块之间的区别有助于开发
新的编译器。
本文的这一部分没有任何有用的代码。 那留给
接下来的部分。 本文的第二部分介绍了分词器和
以下部分深入分析器,代码生成器和解释器。
我们的分词器使用正则表达式来切分字符序列
成令牌序列。
这一切的源代码故意不使用任何外部工具
标记器部分或解析器部分,仅用于教育目的。
有很多非常好的工具可用于生成令牌生成器和
为我们解析器,但这就是从头开始构建它自己。
现在我必须弄清楚那种语言会是什么样子
应该有能力 保持双手交叉;-)
下周在本文的第二部分见。
亲切的问候,
乔斯
From: https://bytes.com/topic/java/insights/649396-compilers-1-introduction