问候,
本周的文章部分讨论了用于我们的小语言的解析器。
我们将根据在中定义的语法规则来实现解析器
本文的第二部分。 您很快就会看到,实施
语法分析器几乎是这些语法规则的一对一翻译。
递归下降解析语法规则是高度递归的,即一个规则提到另一条规则
提到另一条规则等,直到一条规则提到第一条规则
再次。 (例如,请参见嵌套表达式的定义)。
由于实现非常严格地遵循这些语法规则,因此这些方法
构成解析器的元素也是高度递归的。 这种解析方式
(编程)语言被称为“递归下降解析”。 方法尝试
找出根据当前令牌应该做什么,然后他们“预测”哪个
(其他)方法,直到解析完整个令牌流为止。
请注意,在使用递归下降解析策略时,方法A会调用
B,B调用C,C调用D等等,直到Z再次调用方法A,无论如何
递归方法调用链的长短是在该链中的某个位置
在循环再次关闭之前,至少有一个令牌
令牌输入流必须被“处理”,即至少一次令牌生成器的
skip()方法必须被调用。 否则递归会更深入
不会发生任何事情的更深层次,即解析器会“咀嚼”同一东西
令牌一遍又一遍。
对于语法规则,这可以归结为以下事实:
规则可能会在规则循环中出现,作为任何规则中最左边的规则名称
该规则名称之前的令牌,例如
A:B <其余规则A>
B:C <规则B的其余部分>
C:D <规则C的其余部分>
...
Z:<规则Z的其余部分>
...这是不允许的; 更正式地说:没有左递归语法规则
在递归下降解析方案中被允许。 我不想深入
目前更多的理论方面,但不允许左递归
语法规则被认为是递归下降解析器的主要缺点,
这正是解析器生成器应用其他解析器技术的原因
直到最近,Yacc和Bison之类的产品才受到青睐。 就在几年前
发现了一种新的解析技术,其中仍然没有左递归规则
允许,但是这项新技术的好处远远超过了以前
Yacc,Bison等人采用的最流行的其他技术。
如果您密切注意我们的小语言的语法规则,
如果您检查解析器的所有方法,就会看到至少一个令牌
在致命的递归循环关闭之前被消耗掉。
无需担心递归:那些递归很自然
语法规则。 解析器的实现包括三个子解析器
及其基类:
1)解析器:实现语法的顶级规则;
2)DefinitionParser:实现语法的定义规则;
3)ExpressionParser:实现定义表达式的规则。
第一个解析器类执行除法运算:它检查是否
当前令牌引入了用户功能定义或声明,或者
当前标记引入表达式的开始。 当整个令牌
流已正确解析,它检查是否所有用户功能都具有
实际定义。
第二个解析器类解析用户函数定义或声明。
对于函数体,它使用第三个解析器的实例:
第三个解析器类将是解析器中最大的一部分; 它解析
整个表达式,无论表达式多么复杂。
所有解析器对象都使用Generator对象,该对象可以为
解析器; 一系列指令构成了编译后的代码。 解析时
并完成代码生成后,可以将此代码传递给解释器
解释说明。
这三个解析器是以下抽象类的派生类:
基本解析器类
public abstract class AbstractParser {
protected Tokenizer tz;
protected User user;
public AbstractParser(Tokenizer tz, User user) {
this.tz= tz;
this.user= user;
}
Abstract类通过其构造函数获取Tokenizer和User对象。
的
Tokenizer提供了令牌流(请参阅本文的第二部分)。 用户
对象基本上是一个Map,它将用户定义函数的名称映射到
代表用户定义功能的指令。
这个抽象类实现派生类使用的一些方法。 什么时候
Tokenizer扫描一个名字,它所能知道的就是它已经扫描了一个名字,即
令牌生成器不知道该名称代表什么; 它可能是一个变量名
或某种内置函数名称,或用户定义的函数或
关键字,但是令牌生成器知道什么? 抽象基类中的方法
照顾到这一点:
protected int type(Token token) {
int type= token.getTyp();
if (type != TokenTable.T_NAME) return type;
String str= token.getStr();
if (ParserTable.funcs.contains(str)) return ParserTable.T_FUNC;
if (ParserTable.quots.contains(str)) return ParserTable.T_QUOT;
if (ParserTable.rword.contains(str)) return ParserTable.T_WORD;
if (user.containsKey(str)) return ParserTable.T_USER;
return type;
}
如果令牌类型不是名称,则此方法不执行任何操作;
否则一个
ParserTable或User映射中的表的哪个可以告诉该名称实际
是:内置函数,带引号的对象,保留字或用户定义的
功能。
两个“特殊”物体引用的objest在语法上等效于内置函数,但它们
自己评估他们的论点。 一种引用的机制
对象,“ if”引用的对象。 我们已经看到了一个引用“ if”的例子
在本文的语法部分末尾已经有一个对象:
function fac(n) =
if (n < 2
, 1
, n*fac(n)
);
“ if”对象的第一个参数为“ n <2”。
如果表达式是
true'if'对象评估其第二个参数,否则评估其第二个参数
第三个论点。 将对这两个对象进行更详细的说明
当我们讨论代码生成和指令集类时。 也许以后
我们是否开发了其他引用对象,例如“ while”对象?
再次使用基本解析器类抽象基类实现了一些其他方便的方法,这些方法是
由派生的解析器类使用:
protected boolean expect(String token) throws InterpreterException {
if (token.equals(tz.getToken().getStr())) {
tz.skip();
return true;
}
return false;
}
protected void skipDemand(String str) throws InterpreterException {
tz.skip();
demand(str);
}
protected void demand(String str) throws InterpreterException {
if (!expect(str))
throw new ParserException(tz, "expected: "+str);
}
解析器使用“期望”方法来确定下一步该做什么。
这种方法
如果当前令牌等于预期令牌(并跳过该令牌),则返回true,
否则,此方法返回false。 第二和第三种方法称为
当解析器已经知道要解析的内容时,假设a的名称
内置功能刚刚被令牌化程序扫描; 解析器“知道”
该名称后应加上左括号。 对于这种情况,第二
或调用第三个方法。 当应该先跳过先前的令牌时,
调用第二个方法,否则调用第三个方法。
其他派生的解析器对象根据定义的方法做出决策
在这个基类中。 下几段描述派生的解析器类。
主解析器这又是第一个语法规则:
program: ((definition | expression) ';')*
主解析器对象检查要解析的内容:定义或表达式。
这是课程:
public class Parser extends AbstractParser {
public static Code parse(String name) throws InterpreterException {
FileReader fr= null;
try {
Parser parser= new Parser();
return parser.parse(r= new FileReader(name));
}
catch (InterpreterException ie) {
throw ie;
}
catch (Exception e) {
throw new InterpreterException("can't compile: "+name, e);
}
finally {
try { fr.close(); } catch (Exception e) { }
}
}
private ExpressionParser expressionParser;
private DefinitionParser definitionParser;
public Parser() {
super(new Tokenizer(), new User());
expressionParser= new ExpressionParser(tz, user);
definitionParser= new DefinitionParser(tz, user);
}
第一种(静态)方法是一种便捷方法:它以文件名作为其名称。
参数并尝试将其打开以进行读取; 它为阅读器提供了一个新的解析器
并返回结果。 当您要编译时,此方法很方便
文件的源文本。
此类的构造方法不带参数; Tokenizer被构造
以及另外两个解析器:一个ExpressionParser,可以解析
表达式(令人惊讶!)和可以解析定义的DefinitionParser
(也是一个惊喜?)。
它将新创建的Tokenizer和User对象传递给另一个对象
两个解析器,因为它们将需要它们。
看看第二个语法规则:
definition: ( 'function' | 'listfunc' ) 'name' defordecl
定义以保留字开头:“ function”或“ listfunc”。
如果当前令牌不等于所有这些,则解析器“预测”
表达式必须被解析; 这是怎么做的:
public Code parse(Reader r) throws InterpreterException {
tz.initialize(r);
Generator gen= new Generator();
for (Token token= tz.getToken();
token.getTyp() != TokenTable.T_ENDT;
token= tz.getToken()){
if (expect("function"))
definitionParser.parse(gen, false);
else if (expect("listfunc"))
definitionParser.parse(gen, true);
else
expressionParser.parse(gen);
demand(";");
}
user.checkDefinitions();
return gen.getCode();
}
“ parse”方法返回动态生成的代码。
第一
此方法生成一个新的Generator并使用来初始化Tokenizer
传递给此方法的阅读器
for循环看起来很复杂,但事实并非如此:它只是一直循环直到
已读取文件令牌结尾。 请注意,令牌流可以完全
如果为空,则循环立即停止; 将此行为与
第一语法规则。
在循环的主体中,确定两个保留字之一
已被扫描。 如果是这样,则将DefinitionParser与适当的对象一起使用
参数:代码生成器和一个标志,指示功能或函数
扫描了listfunc保留字。
如果未扫描这两个保留字中的任何一个,则假定表达式为
应该被扫描并使用ExpressionParser。 最后解析器
在令牌流中要求使用分号; 参见上面的第一个语法规则
将这种方法的主体与语法规则进行比较:它们几乎是
相同:语法规则描述令牌流的语法和此方法
完全一样。
扫描完文件令牌结尾后,将要求用户对象检查
是否定义了所有用户定义的功能。 会抛出异常
如果至少已经声明了一个用户定义的函数,但是没有
定义。 如果一切正常,则此方法返回已生成的代码
在此成功的解析过程中。
此方法可以引发解释器异常; 它不会抛出这样的
异常本身,但“需求”方法(请参见上文)可能会抛出一个或一个
语法错误时,另一个解析器可能会抛出一个
发生。 解析器实际上抛出一个ParserException,它是派生类
来自InterpreterException。
我在课堂上增加了一种便捷的方法。 我们将使用它
后来。 它仅解析一个表达式。 这里是:
public Code expression(Reader r) throws InterpreterException {
tz.initialize(r);
Code code= expressionParser.binaryExpression(new Generator());
if (tz.getToken().getTyp() != TokenTable.T_ENDT)
throw new ParserException(tz, "expected: <eof>");
return code;
}
该方法需要一个表达式;
令牌输入流必须为空
当整个表达式已被解析时。 表达式的编译代码
最后返回。 稍后,当我们要构建一个简单的表达式时
评估者,我们将需要这种方法。
定义解析器当扫描到保留字“ function”或“ listfunc”之一时,
DefinitionParser必须解析声明或定义。 这里有
语法规则再次描述了此类定义或声明的语法:
definition: ( 'function' | 'listfunc' ) 'name' defordecl
defordecl: ('(' paramlist ')' ( '=' expression )?) | ( '=' expression )
paramlist ( 'name' ( ',' 'name' )* )?
请注意,保留文件已经被Parser对象扫描并跳过
(往上看)。 传递给“定义”扫描器的布尔参数告诉哪个
需要扫描功能,尽管语法上没有区别。
这是DefinitionParser的第一部分:
public class DefinitionParser extends AbstractParser {
public DefinitionParser(Tokenizer tz, User user) { super(tz, user); }
此类将Tokenizer以及User对象用作其构造函数参数。
该类还扩展了AbstractParser抽象类。 这是“解析”
由Parser对象调用的方法:
public void parse(Generator gen, boolean asList) throws InterpreterException {
Token token= tz.getToken();
String name= token.getStr();
int type= type(token);
if (type != TokenTable.T_NAME && type != ParserTable.T_USER)
throw new ParserException(tz,
"user function name expected: "+name);
tz.skip();
由Parser类扫描的关键字后面的第一个标记必须是
T_NAME或T_USER令牌。 如果尚未声明用户定义的函数
令牌类型将为T_NAME令牌,否则为T_USER。 一个例外
如果扫描了另一种令牌,则抛出该异常; 否则被认为
“处理”并跳过。
UserInstruction func= user.get(name);
if (func != null) {
if (func.getBody() != null)
throw new ParserException(tz,
"user function already defined: "+name);
else {
demand("=");
func.setBody(parseBody());
}
}
else {
List<String> params= parseArgs();
user.put(name, func=
new UserInstruction(name, params, null, asList));
if (expect("=")) func.setBody(parseBody());
}
}
是否已经声明了用户功能,请咨询User对象
那个名字 最外面的“ if”语句决定要做什么:
已经宣布; 如果它也被定义,那就是一个错误并抛出一个异常。
否则,DefineParser要求看到和等号,后跟
函数的主体。
如果甚至在最外面的“ else”之前都没有声明用户定义的函数
执行部分:解析函数的参数并声明
已在User对象中注册。 如果等号的期望,则下一步
是对的,函数的主体也会被解析,在这种情况下,函数
定义刚刚被解析; 否则,如果令牌中没有看到等号
流功能声明刚刚被解析。
请注意,在两种情况下(临时定义或常规定义),
用户定义的函数主体生成的代码传递给该神秘对象
用户指令; 它知道如何处理。 用户指令和名称
用户定义函数的参数传递到将存储的User对象
它们放在Map <String,Instruction>中供以后检索。 形式参数
如您所见,列表也将传递给UserInstruction。 我们将讨论那些
后续文章部分中的说明。
还要注意,当声明和用户定义函数的定义时
分两部分提供,如下所示:
listfunc foo(x);
function foo = x+42;
声明部分告诉解析器foo是一个listfunc。
的
定义可以从保留字“ function”或“ listfunc”开始; 它没有
无论使用哪个。
解析一个正式的参数列表实际上是很多工作。 以下
方法尝试生成一个List <String>,它表示该对象的名称。
形式参数 根据语法规则(见上文),
形式参数由逗号分隔,整个列表由包含
括弧。 解析正式参数列表的方法如下:
private List<String> parseArgs() throws InterpreterException {
demand("(");
List<String> params= new ArrayList<String>();
for (Token param;
this.type(param= tz.getToken()) == TokenTable.T_NAME; ) {
tz.skip();
params.add(param.getStr());
if (!expect(",")) break;
if (tz.getToken().getTyp() != TokenTable.T_NAME)
throw new ParserException(tz, "parameter name expected");
}
demand(")");
return params;
}
首先,该方法需要左括号。
for循环继续循环为
只要名称已由Tokenizer扫描即可。 令牌被跳过,
添加到List <String>。 名称后面可以带有两个标记:逗号后跟
用另一个名字或别的名字。 如果参数名称后没有逗号,
循环停止。
循环完成后,此方法要求令牌中带有右括号
流。 最后,如果一切顺利,则形式的List <String>表示形式
返回参数。
我们已经看到了大多数DefinitionParser。 有一种方法需要解释
左:这是函数主体的解析方式。 用户的主体定义
函数是一个表达式,而这正是它的解析方式:
private Code parseBody() throws InterpreterException {
return new ExpressionParser(tz, user).parse(new Generator());
}
此方法仅实例化另一个ExpressionParser并要求其解析
二进制表达式。 无论其他方法发生什么,无论是编译
返回表达式或出现错误时引发异常。
此方法是递归下降解析的一个很好的例子。
另一种选择是解析器对象已经传递了自己的
此parseBody()方法可能具有的对象的ExpressionParser对象
用它来解析用户定义函数的主体。 它将需要
传递了更多的参数,但可以避免创建另一个参数
需要解析功能主体时ExpressionParser类的实例。
我不知道哪种选择会更好。
间奏曲我们快要准备好了; 只有ExpressionParser要实现,
解释。 ExpressionParser是所有解析器类中最大的类
并且它值得拥有自己的文章部分(请参见下文)。 仍然有一些松散
最后解释一下:该代码生成器如何工作? 该用户如何
对象做它的工作? 这些指令是什么?
但是首先尝试理解本文的一部分和递归下降技术
用于这些解析器类的实现。 准备好之后
我会在本文的下一部分看到您,其中ExpressionParser
将详细说明。
亲切的问候,
乔斯
From: https://bytes.com/topic/java/insights/739693-compilers-5a-parsers