本文翻译来自谷歌翻译
在我开始尝试学习计算机科学的旅程之前,有一些术语和短语使我想朝另一个方向倒退。
然而我没有奔走,而是装作常识,在交谈中点点头,伪装我晓得有人在指什么,只管事实是我不晓得并且实际上在我听到《Super Scary Computer Science Term™》时就齐全进行了收听。在本系列的整个过程中,我设法涵盖了很多畛域,其中许多术语实际上曾经变得不那么吓人了!
不过,有一个大的问题我曾经防止了一段时间。直到现在,每当我听到这个词时,我都会感到瘫痪。它是在团聚时偶然进行的交谈中呈现的,有时是在会议中呈现的。每一次,我都会想到机器在旋转,计算机吐出了难以了解的代码串,除了我四周的其他人实际上都能够解密它们之外,所以实际上只有我一个人不晓得产生了什么(哇,这是怎么产生的?!)。
兴许我不是惟一一个有这种感觉的人。然而,我想我应该通知你这个词的理论含意,对吗?好吧,请做好筹备,因为我指的是“难以捉摸的 ”和看似令人困惑的“ 形象语法树”或简称“AST”。通过多年的吓唬,我很快乐终于不再胆怯这个词,并真正理解它到底是什么。
当初是时候面对形象语法树的根了,并降级咱们的解析游戏!
从具体到形象
每一个好的谋求都必须有一个松软的根底,而咱们揭开这个构造神秘色彩的使命应该以完全相同的形式开始:当然要有一个定义!
形象语法树(通常仅称为 AST)实际上只是解析树的简化模式。
在编译器设计的上下文中,术语“AST”与“语法树”能够调换应用。
与语法树对应的语法树相比,咱们常常思考语法树(以及语法树的结构形式),语法树咱们曾经相当相熟。咱们晓得“解析树”是蕴含咱们代码的语法结构的树数据结构;换句话说,它们蕴含呈现在代码“句子”中的所有语法信息,并且间接从编程语言自身的语法中得出。
另一方面,“形象语法树”疏忽了分析树本来将蕴含的大量语法信息。
相比之下,AST 仅蕴含与剖析源文本无关的信息,并且跳过解析文本时应用的任何其余额定内容。
如果咱们专一于 AST 的“抽象性”,那么这种区别将变得更加有意义。
咱们会记得,“稠密树 ”是一个句子的语法结构的图解图示版本。换句话说,咱们能够说语法分析树精确地示意了表达式,句子或文本的外观。它基本上是文本自身的间接翻译;咱们采纳该句子,并将句子的每个小片段(从标点符号到表达式再到标记)都转换为树形数据结构。它揭示了文本的具体语法,这就是为什么它也被称为 具体语法树 或 CST 的起因。咱们应用“ 具体”一词来形容此构造,因为它是咱们的代码的语法正本,以树的模式一一标记。
然而,是什么使具体的货色变得形象呢?好吧,形象语法树并没有“确切地”向咱们展现表达式的样子,即解析树的样子。
相同,形象的语法树向咱们展现了“重要”位,这是咱们真正关怀的事物,这些意义使咱们的代码“句子”自身具备意义。语法树向咱们展现了表达式的重要局部,或咱们的源文本的“形象”语法。因而,与具体的语法树相比,这些构造是咱们代码的形象示意(在某些方面,它的准确性较低),这正是它们的名称。
当初,咱们理解了这两种数据结构之间的区别以及它们示意代码的不同形式,值得提出一个问题:形象语法树在编译器中的地位如何?首先,让咱们想起到目前为止咱们所晓得的无关编译过程的所有信息。
假如咱们有一个超短而甘甜的源文本,看起来像是:5 +(1 x 12)
。
咱们会记得,在编译过程中产生的第一件事是对文本进行扫描,这是扫描程序执行的一项工作,这导致文本被分解成可能的最小局部,称为词素。这部分将与语言无关,咱们将取得原始文本的精简版。
接下来,将这些词素传递给词法分析器 / 令牌生成器,后者将源文本的那些小示意模式转换为 tokens ,这将特定于咱们的语言。咱们的令牌看起来像这样:[[5,+,(,1,x,12,)]
。扫描程序和令牌生成器的共同努力形成了编译的“词法剖析”。
而后,在对输出进行标记后,将其生成的标记传递给解析器,解析器随后将获取源文本并从中构建一个解析树。下图以解析树格局示例了标记化代码的外观。
将令牌转换为解析树的工作也称为解析,称为 语法分析 阶段。语法分析阶段间接取决于词法分析阶段。因而,词法剖析必须始终在编译过程中排在首位,因为咱们的编译器解析器只有在分词器实现工作后能力实现工作!
咱们能够将编译器的各个局部视为好敌人,它们彼此依赖,以确保咱们的代码正确地从文本或文件转换为解析树。
然而回到咱们最后的问题:形象语法树在这个敌人组中适宜什么中央?好吧,为了答复这个问题,首先能够帮忙您理解 AST 的需要。
将一棵树压缩成另一棵
好吧,当初咱们有两棵树能够直立在脑海中。咱们曾经有了一个解析树,以及如何学习另一个数据结构!显然,此 AST 数据结构只是一个简化的分析树。那么,为什么咱们须要它呢?甚至有什么意义呢?
好吧,让咱们看一下解析树,对吧?
咱们曾经晓得解析树在程序的最不同局部代表了咱们的程序。确实,这就是扫描器和令牌生成器如此重要的工作,将咱们的表白合成为最小的局部的起因!
真正由程序的最不同局部示意程序意味着什么?
事实证明,有时程序的所有不同局部实际上并不是始终对咱们有用。
让咱们看一下此处显示的插图,它以解析树格局示意了咱们的原始表达式 “5 +(1 x 12)”
。如果咱们以挑剔的眼光仔细观察这棵树,就会发现在某些状况下,一个节点恰好有一个孩子,这也称为 单后继节点,因为它们只有一个子节点(或一个“继任者”)。
在咱们的分析树示例的状况下,单个 - 后继节点具备一个“表达式”或“Exp”的父节点,它们具备某个值的单个后继,例如“5”,“1”或“12“。然而,这里的“Exp”父节点实际上并没有为咱们减少任何价值,是吗?咱们能够看到它们蕴含令牌 / 终端子节点,但咱们实际上并不关怀“表达式”父节点;咱们真正想晓得的是表达式是什么?
解析树后,父节点基本不会向咱们提供任何其余信息。相同,咱们真正关怀的是 单后继节点。的确,这是向咱们提供重要信息的节点,对咱们而言很重要的局部:数量和价值自身!思考到这些父节点对咱们来说是不必要的事实,很显著,该解析树是一种简短的构造。
所有这些单后继节点对咱们来说都是多余的,基本对咱们没有帮忙。所以,让咱们解脱它们!
如果咱们在解析树中压缩单后继节点,则最终将失去具备雷同准确构造的压缩版本。查看上图,咱们会发现咱们依然放弃与以前完全相同的嵌套,并且节点 / 令牌 / 终端依然呈现在树中的正确地位。然而,咱们设法将其放大了一点。
咱们也能够修剪更多的树。例如,如果咱们看一下以后的解析树,就会发现其中有一个镜像构造。(1 x 12
)的子表达式嵌套在括号 ()
中,括号自身就是令牌。
然而,一旦咱们将树固定好,这些括号并没有真正帮忙咱们。咱们曾经晓得 1
和12
是将传递给乘法 x
操作的参数,因而括号在这一点上通知咱们的不多。实际上,咱们能够进一步压缩解析树,并解脱这些多余的叶节点。
一旦咱们压缩并简化了语法分析树并解脱了多余的语法“尘埃”,咱们最终将取得一个看起来显著不同的构造。实际上,该构造是咱们新的和备受期待的敌人:形象语法树。
上图显示了与解析树完全相同的表达式:“5 +(1 x 12”
。不同之处在于,它已将表达式从具体语法中形象进去。因为没有必要,因而咱们在此树中看不到任何括号()
。同样,咱们也没有看到像 Exp 这样的非终端,因为咱们曾经弄清楚了“表达式”是什么,并且咱们可能提取出对咱们真正重要的 value ,例如,数字“5”。
这恰好是 AST 和 CST 之间的区别因素。咱们晓得,形象语法树会疏忽语法分析树蕴含的大量语法信息,并会跳过语法分析中应用的“额定内容”。然而当初咱们能够确切地看到 如何 产生!
当初,咱们曾经压缩了本人的解析树,咱们将更好地把握将 AST 与 CST 区别开来的某些模式。
有几种办法能够使形象语法树与解析树在视觉上有所不同:
- AST 绝不会蕴含语法细节,例如逗号,括号和分号(当然取决于语言)。
- AST 将具备折叠的版本,否则将显示为单个 \ 后继节点。它永远不会蕴含带有单个子节点的“链”节点。
3. 最初,所有运算符(例如 +,-,x 和 /)将成为树中的外部(父)节点,而不是终止于解析树中的叶子。
从外观上看,AST 总是比解析树更紧凑,因为按定义,它是解析树的压缩版本,语法细节较少。
那么,如果 AST 是解析树的压缩版本,那是有情理的,只有当咱们有足够的货色来构建解析树时,咱们能力真正创立形象语法树!
实际上,这就是形象语法树如何适宜 更大 编译过程的形式。AST 与咱们曾经理解的解析树有间接分割,同时依赖于 lexer 在创立 AST 之前先实现其工作。
创立形象语法树作为语法分析阶段的最终后果。语法分析过程中,语法分析器作为次要的“字符”位于最后面,居中,它可能会或可能不会始终生成语法分析树或 CST。依据编译器自身及其设计形式,解析器可能会间接间接构建语法树或 AST。然而,解析器将始终生成 AST 作为其输入,无论它是否在两者之间创立了一个解析树,或者解析器可能须要进行多少次传递能力这样做。
AST 的解剖
既然咱们晓得形象语法树很重要(但不肯定令人生畏!),咱们就能够开始分析它。对于 AST 的结构形式,一个乏味的方面与该树的节点无关。
下图举例说明了形象语法树中单个节点的解剖构造。
咱们会留神到,该节点与之前见过的其余节点类似,因为它蕴含一些数据(“令牌”及其“值”)。然而,它也蕴含一些十分具体的指针。AST 中的每个节点都蕴含对其 next 兄弟节点及其 first 子节点的援用。
例如,咱们的 “5 +(1 x 12)”
的简略表达式能够构建为 AST 的可视化插图,如下图所示。
咱们能够设想,读取,遍历或“解释”此 AST 可能是从树的最低层开始,而后逐渐返回以建设一个值或在最初返回“后果”。
它还有助于查看解析器输入的编码版本,以帮忙补充咱们的可视化成果。咱们能够依附各种工具并应用事后存在的解析器,以疾速查看示例在通过解析器运行时表达式的外观。以下是咱们的源文本 “5 +(1 * 12)”
的示例,它贯通 Esprima,ECMAScript 解析器及其生成的形象语法树,随后是其不同标记的列表
应用 JavaScript 对咱们的 AST 表达式进行代码可视化。
以这种格局,如果咱们查看嵌套的对象,便能够看到树的嵌套。咱们会留神到,蕴含“1”和“12”的值别离是父操作“”的“left”和“right”子代。咱们还将看到乘法运算(*
)形成了整个表达式自身的 right 子树 *,这就是为什么它被嵌套在较大的BinaryExpression
对象中,位于键“right”之下的起因。相似地,“5”的值是较大的“BinaryExpression”对象的单个““left””子代。
然而,形象语法树最令人着迷的方面是这样的事实:即便它们是如此紧凑和简洁,它们也不总是容易尝试构建的简略数据结构。实际上,依据解析器解决的语言,构建 AST 可能会非常复杂!
大多数解析器通常会结构一个解析树(CST),而后将其转换为 AST 格局,因为有时这可能会更容易 - 只管这意味着须要更多步骤,并且通常来说,从源文本传递更多内容。解析器晓得要解析的语言的语法后,构建 CST 实际上非常容易。它不须要做任何简单的工作来弄清楚令牌是否“重要”;取而代之的是,它仅依照其看到的程序齐全获取它所看到的内容,而后将它们全副吐出到树上。
另一方面,有些解析器将尝试以单个步骤实现所有这些操作,间接跳到结构形象语法树。
间接建设 AST 可能会很辣手,因为解析器不仅必须找到标记并正确示意它们,而且还要确定哪些标记对咱们很重要,哪些标记对咱们不重要。
在编译器设计中,因为多个起因,AST 最终变得十分重要。是的,结构起来可能很辣手(并且可能很容易弄乱),然而,这是词法剖析和语法分析阶段相结合的最初后果!词法剖析和语法分析阶段通常独特称为编译器的“分析阶段”或“前端”。
咱们能够将形象语法树视为编译器前端 / 终端的“最终我的项目”。这是最重要的局部,因为这是前端必须展示的最初一件事。此技术术语称为 中间代码示意 或 IR,因为它成为最终由编译器用来示意源文本的数据结构。
形象语法树是 IR 的最常见模式,但有时也是最容易被误会的模式。然而,既然咱们对它的理解有所提高,咱们就能够开始扭转对这种可怕构造的认识!心愿当初对咱们的威逼有所加重。