lufei's Studio.

语法分析

字数统计: 1.4k阅读时长: 4 min
2019/12/03 Share

前言

学习了词法分析(Lexical Analysis),并且“装模作样”的实现了大概的词法分析工程,仅仅为了学习而已,接下来接下来就是语法分析(Syntactic Analysis, or Parsing)了。

开始

语法分析是在词法分析的基础上识别出程序的语法结构。这个结构是一个树状结构,是计算机容易理解和执行的.

以自然语言为例。自然语言有定义良好的语法结构,比如,“我喜欢又聪明又勇敢的你”这个句子包含了“主、谓、宾”三个部分。主语是“我”,谓语是“喜欢”,宾语部分是“又聪明又勇敢的你”。其中宾语部分又可以拆成两部分,“又聪明又勇敢”是定语部分,用来修饰“你”。定语部分又可以分成“聪明”和“勇敢”两个最小的单位.

这样拆下来,会构造一棵树,里面的每个子树都有一定的结构,而这个结构要符合语法。比如,汉语是用“主谓宾”的结构,日语是用“主宾谓”的结构。这时,我们说汉语和日语的语法规则是不同的。

程序也有定义良好的语法结构,它的语法分析过程,就是构造这么一棵树。一个程序就是一棵树,这棵树叫做抽象语法树(Abstract Syntax Tree,AST)。树的每个节点(子树)是一个语法单元,这个单元的构成规则就叫“语法”。每个节点还可以有下级节点。

了解 Clang AST

所以,语法分析的结果就是生成AST。

生成ATS

如果我们要语法分析

  • int age = 45

那么这个过程可以简单这样来概括:

解析变量声明语句时,我先看第一个 Token 是不是 int。如果是,那我创建一个 AST 节点,记下 int 后面的变量名称,然后再看后面是不是跟了初始化部分,也就是等号加一个表达式。我们检查一下有没有等号,有的话,接着再匹配一个表达式。

通常会对产生式的每个部分建立一个子节点,比如变量声明语句会建立四个子节点,分别是 int 关键字、标识符、等号和表达式。

我们把解析变量声明语句和表达式的算法分别写成函数。在语法分析的时候,调用这些函数跟后面的 Token 串做模式匹配。匹配上了,就返回一个 AST 节点,否则就返回 null。如果中间发现跟语法规则不符,就报编译错误。

在这个过程中,上级文法嵌套下级文法,上级的算法调用下级的算法。表现在生成 AST 中,上级算法生成上级节点,下级算法生成下级节点。

生成的ATS大概是这样:

1
2
3
4
Programm Calculator    
IntDeclaration age
AssignmentExp =
IntLiteral 45

当然这些都还在正则表达式的文法规则之内,没有超出词法分析时使用的文法。

我们解析算术表达式的时候,会遇到更复杂的情况,这时,正则文法不够用,我们必须用上下文无关文法来表达。

算术表达式要包含加法和乘法两种运算(简单起见,我们把减法与加法等同看待,把除法也跟乘法等同看待),加法和乘法运算有不同的优先级。我们的规则要能匹配各种可能的算术表达式:

  • 2+3*5

  • 2*3+5

  • 2*3

  • ….

思考一番之后,我们可以把规则分成两级:第一级是加法规则,第二级是乘法规则。把乘法规则作为加法规则的子规则,这样在解析形成 AST 时,乘法节点就一定是加法节点的子节点,从而被优先计算。

这种文法已经没有办法改写成正则文法了,它比正则文法的表达能力更强,叫做“上下文无关文法”。正则文法是上下文无关文法的一个子集。它们的区别呢,就是上下文无关文法允许递归调用,而正则文法不允许。

那有没有上下文相关的情况需要处理呢?也是有的,但那不是语法分析阶段负责的,而是放在语义分析阶段来处理的。

实现表达式求值

其实,要实现一个表达式计算,需要基于 AST 做求值运算,也就是对这棵树做深度优先的遍历。

深度优先的遍历也是一个递归算法。以“2 + 3 * 5”的 AST 为例看一下:

  • 对表达式的求值,等价于对 AST 根节点求值

  • 首先求左边子节点,算出是 2。

  • 接着对右边子节点求值,这时候需要递归计算下一层。计算完了以后,返回是 15(3*5)。

  • 把左右节点相加,计算出根节点的值 17。

总结

虽然我的目标是能通过这些学习写一个简单能用的解释器,起码可以看作一个玩具,但是,说实话,我有些地方,还不是很理解,只能说大概了解这个过程,所以要是写的话,也只能写个连玩具都算不上的东西,这很无聊,也不是我的本意,所以,还是先囫囵吞枣的学习一遍吧QAQ

为自己的菜而感到惭愧!

CATALOG
  1. 1. 前言
  2. 2. 开始
  3. 3. 生成ATS
  4. 4. 实现表达式求值
  5. 5. 总结