参考来源:聊聊编译原理(二) - 语法分析
自顶向下分析方法
自顶向下分析方法:递归下降分析法,LL1分析法。其实本质上核心思想是一样的,也就是LL,从左至右,最左推导,因而我觉得其实可以把前一个称为LL0分析法,即不使用向前看符号,这也是他们的不同点,具体实现不同罢了,递归下降需要用回溯和分治,调用递归函数,因为递归调用,耗费时间当然要长一点,而LL1使用first和follow函数(当然前者也用到了)创建了一个预测分析表,可以知道,这个算法不需要再去遍历尝试每一种语法树组合,而是根据表肯定能选出最优的组合去匹配,这也就是典型的牺牲空间换时间的算法。
TINY文法
为简单起见,丢弃了read和write语句(但实际上也就加两条语法的问题,可直接补充)
program -> stmt-sequence
stmt-sequence -> stmt-sequence;statement | statement
statement -> if-stmt | repeat-stmt | assign-stmt //丢弃了读写语句
if-stmt -> if exp then stmt-sequence end //条件语句
| if exp then stmt-sequence else stmt-sequence end
repeat-stmt -> repeat stmt-sequence until exp //循环语句
assign-stmt -> id := exp //赋值语句
exp -> simple-exp compar-op simple-exp | simple-exp //算数表达式
compar-op -> < | =
simple-exp -> simple-exp add-op term | term
add-op -> + | -
term -> term mul-op factor | factor
mul-op -> * | /
factor -> ( exp ) | number | id
消左提左、构造first follow
基本思想
- 为每个非终结符构造一个分析函数
- 用前看符号指导产生式规则的选择
第一点简单来说就是A -> Bc B -> d
构造函数A(),B()
第二点即是说会用到first(A)和first(B) 即上图中的红色部分
python构造源码
对于文法来说分三种情况
- A -> B C
没有其他的终结符和|,则直接调用两个产生式右部的非终结符函数
如: stmt-sequence -> statement ST
- A -> B | C
存在 | ,则需要看A的first,| 等价于if-else,所以需要看first(A)的结果去分类判断该调用哪个非终结符函数,同时,这里需要putback,因为这个token是多读的,需要将指针调回去
如:statement -> if-stmt | repeat-stmt | assign-stmt
- A -> B | 𝜀
存在𝜀 空,则需要回溯,因为A可以在此不做任何事情,因而读取的token不满足first(B)时,则同样需要putback,将指针倒回去重新读取token
如:ST -> ;statement ST | 𝜀
主要函数:
- 基本的函数定义
init初始化函数,得到已经特意写好的token流,指针位置初始化为0
get_next_token 得到下一个token
put_token_back 回溯上一个token
- main函数
预置好了token流,用空格分开,因为上面init是根据空格切开为数组的
匹配过程中如果有错误,会直接抛出异常,则程序是不会走到最后的print的,故输出匹配成功了则是匹配无误
- 非终结符函数
运行结果
故意改错语法 tiny语言赋值语法为’ := ’ 而非’ = ’
完整源码:parseTINY