编译原理-语法分析3

803 阅读4分钟

这一节用于完善解释器的功能及分析回溯的功能和抛出异常对解释器的优化

需要的一些语法规则

在解释一行代码的时候,我们需要制定一些规则用于构建AST,例如:int 开启一个赋值语句然后后面一定要跟一个变量名 如果后面继续跟一个 ‘=’,那‘=’的后面一定是一个表达式也就是additive()可以识别的一串表达式,表达式结束后需要以‘;’结尾。

上面是一个常见的赋值语句的规则,其大体可以表示如下:

programm: statement+; 
statement
: intDeclaration
| expressionStatement
| assignmentStatement
;

这是由扩展巴科斯范式书写的规则,+匹配多次statement。 statement可以依次尝试 int定义 expression表达式语句 assignment赋值语句,进入递归下降尝试,目的是获取一颗完整的AST树并消耗完所有Token,可以看出,上面的 intDeclaration,expressionStatement, assignment Statement都是非终结符。 举例:

intDeclaration : 'int' Identifier ( '=' additiveExpression)? ';';

变量声明语句: int定义语句的EBNF可以翻译为 先匹配‘int’关键字,然后使用indentifier匹配定义的变量名 ,()?是正则表达式中匹配‘= 表达式’这样的形式一次或零次,一次就是类似 int a = 5;这样的赋值语句,零次就是 int a; 这样的未指明a值得创建变量语句。整个Token流需要以‘;’结尾。

下面的就不再一一分析,只列举。

表达式语句:

expressionStatement : additiveExpression ';';

赋值语句:

assignmentStatement : Identifier '=' additiveExpression ';';

变量支持

我们需要开辟一块空间用来存储变量。

目前可以简单的开辟一块map空间用于变量存储<string,value>构成变量名到值得映射即可,当涉及变量作用域的时候情况会变复杂,后面会学习。

回溯

回溯是指,我们在一个个尝试非终极符的时候为了防止错误的尝试消耗掉Token使得下一个尝试获得的Token流缺少Token而出错,需要在每次错误的尝试后退回消耗掉的Token,在下次尝试开始前准备好完整的Token流。 举例: 尝试 a = 5; 我们会先进入变量声明语句判断,这里没有消耗掉Token但是进入表达式判断后会消耗掉a,这时就需要回溯到原来的Token流去尝试正确的 assignment语句判断,一般我们会在开始消耗Token流前记录下读取位置,然后在递归下降算法返回发现Token流没有消耗完也即这一次尝试不能匹配所有Token流需要尝试另一个的时候,回溯Token流至之前记录的位置。

private SimpleASTNode expressionStatement() throws Exception { 
int pos = tokens.getPosition(); //记下初始位置
SimpleASTNode node = additive(); //匹配加法规则
if (node != null) 
{ 
Token token = tokens.peek();
if (token != null && token.getType() == TokenType.SemiColon) 
{ 
//要求一定以分号结尾 tokens.read();
} 
else 
{ 
node = null; tokens.setPosition(pos); // 回溯
} 
} 
return node; }

这里是宫文学老师写的java的定点回溯。 setposition(pos)完成了回溯, peek()用于预读取,预读取不消耗Token,read()读取消耗Token

简化回溯

我们可以自己定义使程序抛出异常,这样遇到完全错误的语句的时候我们不需要每次都回溯去尝试下一个选项,因为我们知道这个语句是完全错误的,没有任何一个尝试可以匹配。 比如: int = 10;

我们可以在变量声明语句中判断,如果int后面在判断 Identifier 的时候取得空或取得‘=’,可以认为这个语句完全错误,使用throw抛出异常,终止程序报错,java和c+都可以try检测异常,再用catch后接处理异常的代码。

REPL

脚本语言一般都会提供一个命令行窗口,让你输入一条一条的语句,马上解释执行它,并得到输出结果,比如 Node.js、Python 等都提供了这样的界面。这个输入、执行、打印的循环过程就叫做 REPL(Read-Eval-Print Loop)。

在实现上就是每次读取以‘;’结尾的一些语句然后作为Token流送入解释器获得AST树,在evaluate这个AST树计算Token流对应的输出值和保存的变量。

这里还是粘贴宫文学老师的代码:

SimpleParser parser = new SimpleParser();
SimpleScript script = new SimpleScript();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); //从终端获取输入
String scriptText = "";
System.out.print("\n>");//提示符
while (true) {//无限循环 
try { 
String line = reader.readLine().trim(); //读入一行
if (line.equals("exit();")) 
{//硬编码退出条件 System.out.println("good bye!"); break; } 
scriptText += line + "\n"; 
if (line.endsWith(";")) 
{ //如果没有遇到分号的话,会再读一行 
ASTNode tree = parser.parse(scriptText); //语法解析
if (verbose) { parser.dumpAST(tree, ""); } 
script.evaluate(tree, ""); //对AST求值,并打印 
System.out.print("\n>"); //显示一个提示符 
scriptText = ""; 
} 
} 
catch (Exception e)
{ //如果发现语法错误,报错,然后可以继续执行 System.out.println(e.getLocalizedMessage());
System.out.print("\n>"); //提示符
scriptText = ""; }

}