文法产生式
语法和语义的区别
- 语法:描述该语言的程序的正确形式
- 语义:定义了程序的含义,即每个程序在运行时做什么
抽象语法树和三地址指令
三地址指令可以理解为只有3个成分的指令:2个操作数和一个操作符,最多执行一个操作。恰好对应一颗二叉树的2个子节点和其父亲节点。
抽象语法树如下:
笔记后续更新,可以关注github https://github.com/dslu7733/Compilers
对于抽象语法树的“翻译”,是从叶子节点开始向上翻译的。
文法产生式
->表示推导,可由前者推导出后者,也可以理解为某种“等价替换”
- 加减运算的文法产生式 (且该文法是左递归的,且仅针对个位数加减)
//list叫开始符,总是从它开始
list -> list + digit
list -> list - digit
list -> digit
digit -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
左递归指,该文法可以一直向左边推导,等价成一颗不断向左子树延申的抽象语法树。
- 二义性
同样,对于上面的例子,可能有人会想到下面的文法生成式
string -> string + string
string -> string - string
string -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
这种文法的缺点在于它对应的语法树是二义性的,即存在2颗不同的语法树对应着同一句话,在某些情况下,这是含糊的。
但是细心可以发现这2颗树还是有所不同的,前一颗是左递归,后一颗是右递归。可以人为的赋予一层解释给它们,子树的运算级别更高,即添加了一层“隐性的括号”。
于是对于前一颗语法树,它将被翻译为 (9-5)+2 = 6
后一颗语法树被翻译为 9-(5+2) = 2
- 左结合与右结合
看了上面对于左递归和右递归的解释,人们会想第一个例子能不能写成右递归呢?答案是可以的
list -> digit suffix | digit suffix
suffix -> - digit suffix | + digit suffix | epsilon
digit -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
//epsilon表示空字符串, ’|‘表示‘或’
那它们有什么区别吗?就像上面那个string的例子,解释不同,对于右递归的文法,是右结合的。
例如加减运算是左结合的,所以3+2-1=(3+2)-1; 而等号是右结合的,所以a=b=2等同于a=(b=2)
- 函数调用中参数列表的文法产生式
call -> id (optparams)
optparams -> epsilon | params
params -> params , param | param
上面的参数列表文法产生式中用id表示标识符,epsilon表示空字符串,param表示形参。
这个文法产生式某种意义上来说是”不完整“的, 因为它没有将id与params推导到字符级别,但有时候为了方便理解,就这么写了。
除此之外,文法产生式是很严谨的,连参数之间的逗号和函数调用的括号都要考虑在内,因为param仅代表一个字符串,不包含逗号在内。
- 运算符的优先级
到现在可以看到文法生成式算得上是一种严谨的”数学推导“了,而且它还能表示运算符号的优先级。
考虑
+-:左结合
*/:左结合
优先级 */ > +-
expr -> expr - term | expr + term | term
term -> term * factor | term / factor | factor
factor -> digit | (expr)
语法树的翻译是从叶子节点开始的,因此可以区分出优先级,子树的运算优先级比其父节点高。
factor是最小的运算单位,表示不能被其它任何运算符分开的表达式。
上面这个文法可以实现*/优先级比+-高,但是他也能表示2 *(1+3)这类语句。
- 后缀表达式
上面的文法都是中缀形式,后缀可以写成E -> E E op | digit
C语句的子集的文法
说了这么多,来看一个涉及C语言部分语句的文法
stmt -> id = expression;
| if(expression) stmt
| if(expression) stmt else stmt
| do stmt while (expression);
| while(expression) stmt
| {stmts}
stmts -> stmts stmt | epsilon
上面用id表示标识符,expression表示实际程序语句。注意这个文法中的分号的使用,非常严谨,不多不少。