编译器科谱

编译器科谱

前言

  编译器素来以复杂和强大著称,不论编译型语言还是解释型语言,都需要一款强大的编译器在背后支撑,本文不打算详述整套编译理论(说白了,我研究的也不深),仅从实用的角度科谱一下编译原理里面非常重要的两个过程,词法解析和语法解析。

词法解析

  我们在编写程序时,源代码大都是以文本的形式存在,我们会按照一定的语法去描述你心目中定义的程序。文本中一般会由关键字和非关键字组成。这就迎来了编译过程的第一步,词法分析。
  词法分析就是将源码中的单词符号一个个拆分过来送到语法解析器,拆分的方法很简单,一般而言,会为每个符号(TOKEN)制定一些正则规则,从头扫描源码时,如果匹配上一个正则规则,则说明产生了一个 TOKEN,进而输入到语法解析器。
  以一个简单的四则运算计算器举个例子:

"+" { return **PLUS**; }
"-" { return **MINUS**; }
"*" { return **TIMES**; }
"/" { return **DIVIDE**; }
"|"     { return **ABS**; }
[0-9]+  { return **NUMBER**; }
\n      { return **NEWLINE**; }
[ \t] { }
.   { }

  上面的例子中, 加粗的部分就是语法解析器识别的 TOKEN,而左边就是产生该 TOKEN 的正则表达式,当词法解析器扫描源码时,匹配到左边的某一个正则式,则就返回一个 TOKEN 给到语法解析器。这样源码中的文本就将被分解成为一个个 TOKEN,至此词法解析完成。

语法解析

  语法解析是编译过程中非常重要的一部分,每一个语言自己的语法特性各不相同,但肯定都会和自己的编译器遵守一种规则,这样写出来的代码才能够被编译器识别。词法解析的过程大致可以分为,语法规则表达式的制定,抽象语法树(AST)的形成。

语法规则

  语法规则是一个语法描述的核心,它会用一个个表达式来表现出方法的结构,样子如下例如示:  

%token NUMBER
%token ADD SUB MUL DIV
%token EOL

%%

calclist: /* nothing */
 | calclist exp EOL { printf("= %d\n> ", $2); }
 | calclist EOL { printf("> "); } /* blank line or a comment */
 ;

exp: factor
 | exp ADD exp { $$ = $1 + $3; }
 | exp SUB factor { $$ = $1 - $3; }
 ;

factor: term
 | factor MUL term { $$ = $1 * $3; }
 | factor DIV term { $$ = $1 / $3; }
 ;

term: NUMBER
 ;
%%

  上面的代码中,首先声明了语法解析器中识别的 TOKEN,下面是该语法制定的规则,在每一个语法规则中,每个表达式后面的大括号里的代码,就会在该表达式匹配完成时被执行。
  简单介绍一个表达式,表达式由非终结符和终结符组成,终结符就是我们上面提到的 TOKEN,非终结符就是一个简单符号。表达式左边都是非终结符,表达式右边是由终结符和非终结符组成的表达式。意思为,右边的表达式可以组合成左边的一个符号,即为左边的符号可以替换右边的表达式,这种行为叫做规约。
  这里会提到两个概念,移进 (Shift)和规约(Reduction)。
  可能到这里,思路开始不清晰了,如何替换,怎么规约,我们可以理解为,语法解析器有一个语法栈,既然是栈,就具备栈的特性 FILO。让我们举个例子说明一下它的工作机制。
  以 2+3×4 为例:
  当词法解析器读到2时,发现它与正则中的一条匹配,而它对应的 TOKEN 是一个 NUMBER,则就往语法栈中压入一个 NUMBER 符号,此时语法解析引擎发现该符号满足一个表达式 term: NUMBER,即它可以规则成一个符号 term, 语法解析器就会将 NUMBER 符号 pop 出来,把它替换成 term,再 push 进去,这个过程就叫做规约(Reduction)。紧接着 term 还可以归约为 factor, factor 又可以归约为 expr,而 expr 暂时不能归约为其它的符号。此时栈中,只有 expr。接下来读入 “+”,词法解析器返回 ADD,此时语法分析器会看 expr ADD 能不能规约,发现没有表达式满足,所以 ADD 被压入栈,这个过程就叫移入(Shift)。
  继续,读入 3,3 可以归约为 expr,移进栈,在栈中形成 expr ADD expr 序列,语法解析器发现它可以有表达式归约为一个 expr,这就相当于先执行加法运算,与我们正常的先乘除后加减相悖,此时就遇到了问题。
  我们以比较流行的 LALR(1) 为阐述,一般编译器如何解决这样的问题。LALR(1),的意思其实是 LA(1)LR。LA 的意思是 Look Ahead (1), LR 的问题是 Left-to-Right, Rightmost-Derivation 即最右推导,也就是最左归约。LALR(1) 是自底向上的一个分析模型,这里我不想花篇幅讲解自底向上的分析模型是如何工作,也不想讲解最右推导是啥意思,我们可以简单的理解为,在栈中,如果表达式可以归约,则尽可能让它们归约成为一个新的符号。那 expr ADD expr 也应该尽量归约成为新的 expr 呀,但这个时候,LA 分析就起作用了,LA(1) 的意思是向前看一个 TOKEN,这就是 1 的意思,也有向前看 n 的 TOKEN 的,但不常用。运用到上例中,当读个 3 后,暂时不会归约,此时会向前预读下一个 TOKEN,即 “x” 之后再作决定,此时是移进 “x”,还是先归约 “2+3”,就产生了冲突,即移进-归约冲突。
  优先级,解决这种冲突比较有效的方式就是优先级,TOKEN 是可以制定优先级的,如果给 “x” 制定的优先级比较高,那么看到 “x” 后,由于 MUL 的优先级比现存的 ADD 高,即先会移进 “x”,归约为 expr。否则,则会先归约 “2+3” 形成 expr 压入栈,再移进 MUL。我们选择前者,此时栈中形成 expr ADD expr MUL 的情况,接着读入 “4”,归约为 expr,进而 “3x4” 归约为 expr, 再 “2+12”,最后运算出结果。
  刚才说了移进-归约冲突,还有规约-规约冲突,即有两个表达式满足规约条件,可以制定规则来解决,这里不详解了。
  有了以上理论后,就可以根据这些理论来构建自己的 AST。

       +------+
       | expr |
       +------+
     /     |     \
  +---+  +---+  +------+
  | 2 |  | + |  | expr |
  +---+  +---+  +------+
               /   |    \
           +---+ +---+ +---+
           | 3 | | x | | 4 |
           +---+ +---+ +---+

  构建也很简单,在每一个表达式满足后,设计自己的语法树结点,然后添加上去,这样很容易形成一个 AST ,对于四则运算,也可以很简单地形成逆波兰表达式,即后缀表达式来处理。如果这棵树再大点,其实就可以感受到自底向上分析,以及最左规约的理念了。
  现在有很多工具可以直接进行分词以及制定可以作为输入的表达式格式的文件,如 sqlite 使用的 lemon 语法分析器,以及在 linux 上常用的 flex 分词器以及 bison 语法分析器,都是实现 LALR(1) 型的语法分析成功案例,有精力的可以阅读它们的源码来更深层次地理解其实现。
  有了这些理论基础,不止是四则运算,可以实现更为复杂的语法树,如 C 语法,和比较灵活的 SQL 语法分析,有兴趣的可以实现一个它们的语法解析器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值