无法解析的编译问题:
问候,
欢迎回到解析器文章章节的续篇。 这部分是专用的
到ExpressionParser,这是我们的小语言最大的解析器类。
此类解析一个完整的表达式,就像其他解析器类一样
快速调用生成器。 解析成功完成后,
返回生成的代码。 否则,解析将中止,并且异常
抛出中止的原因。
ExpressionParserExpressionParser类的强制性序言再次变得无聊:
public class ExpressionParser extends AbstractParser {
public ExpressionParser(Tokenizer tz, User user) { super(tz, user); }
就像Parser和DefinitionParser类一样,Tokenizer和User对象
传递给此类的构造函数。 这是
这个对象:
public Code parse(Generator gen) throws InterpreterException {
binaryExpression(gen, 0);
return gen.getCode();
}
您已经看到第一个方法已被使用:DefinitionParser调用
需要为用户定义的函数解析整个表达式时使用它
身体。 该方法调用另一个完成该工作的私有方法。 它会来
稍后(请参阅下文)。 该方法返回已编译的代码。
在解析表达式时即时生成的。
二进制表达式这又是一个二进制表达式的语法规则:
expression0: expression1 ( operator0 expression1 )*
expression1: expression2 ( operator1 expression2 )*
expression2: expression3 ( operator2 expression3 )*
expression3: expression4 ( operator3 expression4 )*
expression4: expression5 ( operator4 expression5 )*
expression4: unary ( operator5 unary )*
operaor 0: ':'
operator1: '==' | '!='
operator2: '<=' | '<' | '>' | '>='
operator3: '+' | '-'
operator4: '*' | '/'
operator5: '^'
注意数字0、1、2、3、4和5;
我们将使用这些数字作为索引
二进制表达式的数字。 通常,规则“ i”如下所示:
expression(i): expression(i+1) ( operator(i) expression(i+1) )*
...除非'i'== 5而是使用'一元'规则。
它翻译
像这样的Java代码:
private void binaryExpression(Generator gen, int i)
throws InterpreterException {
Token token;
if (i == ParserTable.binops.size())
unaryExpression(gen);
else {
for (binaryExpression(gen, i+1);
ParserTable.binops.get(i).
contains((token= tz.getToken()).getStr()); ) {
tz.skip();
binaryExpression(gen, i+1);
gen.makeFunctionInstruction(token, 2);
}
}
}
索引值“ i”表示优先级值及其运算符
此二进制表达式中涉及的优先级。 最高优先
二进制表达式只是一元表达式; 否则优先
级别“ i”只是具有更高优先级级别操作数的二进制表达式
由优先级“ i”运算符分隔。
Generator类中的“ makeFunctionInstruction”方法生成一个二进制文件
操作员指令并将其添加到已编译的代码列表中。 另请参阅
本文“语法”部分中的解释。 在鸟瞰中
递归(!)方法解析二进制表达式,如索引中所述
上面的语法规则。 如果您了解此方法,则其余的ExpressionParser
方法将是小菜一碟。 此方法照顾二进制运算符
优先规则,即递归地解析需要更高和
更高优先级的运算符,直到唯一的一元表达式
剩下。 一元表达式在语法上有点混乱,因为
可以是它们的许多形式。 我们将拆分一下。
一元表达式再次是一元表达式的语法规则:
unary: ( unaryop unary ) | atomic
unaryop: '+' | '-' | '!' | '++' | '--'
基本上,一元表达式以零个或多个一元运算符为前缀
然后是原子表达式。 运算符从以下位置应用(或评估)
从右到左,尽管它们是从左到右解析的。 这是什么
unaryExpression看起来像:
private void unaryExpression(Generator gen) throws InterpreterException {
Token token= tz.getToken();
if (ParserTable.unaops.contains(token.getStr())) {
tz.skip();
unaryExpression(gen);
gen.makeFunctionInstruction(token.mark(), 1);
}
else
atomicExpression(gen);
}
同样,这是一种高度递归的方法:只要存在一元运算符
在令牌流中,此方法递归调用自身并生成已编译
此后为一元运算符编写代码。 如果没有一元表达式
令牌流(再有),则将工作委托给atomicExpression方法。
这是一个示例:假设输入流是'-!x',首先是'-'运算符
被解析,该方法再次调用自身。 输入流现在包含“!x”。
'!' 运算符被解析,该方法再次调用自身。 输入流
现在是由atomicExpression方法处理的“ x”。 在第二次通话中
为'!'生成了unaryExpression方法代码 操作员 方法
返回并在此方法的第一次调用中为“-”生成代码
操作员。 该代码可以象征性地表示为'x! -'
当我们可以使用解析器时,我们会看到编译后的代码类似于
要编译的中缀表达式的后缀表示。
当我们在递归下降解析中达到这一深度时,再也没有了
二进制或一元运算符在表达式的这一部分中进行解析。 所有的
左边是“原子”表达。
原子表达从语法上讲,原子表达是一团糟。 基本上是原子表达
以常数,左括号,左花括号或名称开头。
第一种选择相对容易。 它是麻烦的部分。
令牌生成器只知道它已经扫描了一个名称。 但名字可以是
任何东西:
1)内置功能;
2)用户定义的函数或listfunc;
3)带引号的对象(请参阅本章的第一部分);
4)一个简单的变量,或者
5)给变量赋值。
让我们深入研究这个烂摊子。 这是atomicExpression方法:
private void atomicExpression(Generator gen) throws InterpreterException {
Token token= tz.getToken();
switch (type(token)) {
case TokenTable.T_NUMB: constantExpression(token, gen); break;
case TokenTable.T_NAME: nameExpression(token, gen); break;
case ParserTable.T_USER: userExpression(token, gen); break;
case ParserTable.T_FUNC: functionExpression(token, gen); break;
case ParserTable.T_QUOT: quoteExpression(token, gen); break;
default:
if (expect("("))
nestedExpression(gen);
else if (expect("{"))
listExpression(gen);
else
throw new ParserException(tz,
"expression expected");
}
}
在给定当前令牌类型的情况下,此方法尝试预测下一步该做什么。
它将所有工作委托给应该执行该工作的其他方法。
常数表达式常数表达式很简单:只需生成处理
当前常数。 用我们的小语言,唯一的常数是双精度。 这是
处理常量的方法:
private void constantExpression(Token token, Generator gen) {
gen.makeConstantInstruction(token);
tz.skip();
}
此方法要求代码生成器生成ConstantInstruction(无论
(可能是),并且它将当前令牌视为已处理,因此将其跳过。
带括号的表达式括号表达式也相对容易解析:左括号
已经被处理过(请参见atomicExpression方法),因此只是一个二进制文件
表达式后跟右括号需要进行解析:
private void nestedExpression(Generator gen) throws InterpreterException {
binaryExpression(gen, 0);
demand(")");
}
观察到此深度嵌套的方法再次调用了最外层的方法之一:
再次使用解析所有具有所有优先级的二进制表达式
只是一个递归方法调用。 我们已经达到递归下降的核心
解析,我们将再次达到它。
对于像'(1 + 1)'这样简单的表达式,以下方法是递归的
称为:
-binaryExpression(0)
-binaryExpression(1)
-binaryExpression(2)
-binaryExpression(3)
-binaryExpression(4)
-一元表达式
-原子表达
-nestedExpression
然后只有左括号被识别并解析。 为了那个原因
首先是“ 1”,我们必须再次进行整个行程:
-binaryExpression(0)
-binaryExpression(1)
-binaryExpression(2)
-binaryExpression(3)
-binaryExpression(4)
-一元表达式
-原子表达
-constantExpression
然后递归展开到可识别的binaryExpression(2)级别
令牌流中的“ +”号,整个马戏团又重新开始。
无需为之感到难过或害怕:递归很快
如今,堆栈的深度也不再是问题。 我们去吧
讨厌的最后部分:
名称表达解析名称时,它不是某种函数或带引号的对象
它既可以是名称,也可以是赋值。 解析方式如下:
private void nameExpression(Token token, Generator gen)
throws InterpreterException {
tz.skip();
String assign= tz.getToken().getStr();
if (ParserTable.asgns.contains(assign)) {
tz.skip();
gen.preAssignmentInstruction(token, assign);
binaryExpression(gen, 0);
gen.makeAssignmentInstruction(token, assign);
}
else {
gen.makeNameInstruction(token);
if (ParserTable.pstops.contains(assign)) {
tz.skip();
gen.makeConstantInstruction(Token.ONE);
gen.makeAssignmentInstruction(token, assign.charAt(0)+"=");
}
}
}
此方法跳过名称(它仍将Token作为其第一个参数)并
检查该名称后面是否有赋值运算符。 如果是这样的话,
它会跳过令牌,并再次调用最顶层的binaryExpression方法
赋值的右侧值。 接下来,让代码生成器
为其生成一个AssignmentInstruction。
如果没有赋值运算符,它将使代码生成器生成一个
简单的NameInstruction用于刚刚解析的名称,除非后缀
++或-令牌的名称如下:在这种情况下,适当的表达式和
分配已生成。
稍后您将看到,我们的后缀减量和增量运算符的行为
与C / C ++ / Java的前缀递增和递减运算符相同。 令人困惑,
承认,但对于我们的简单,少量编程而言,这种方法的解析较少
语言; 毕竟,我们可以自由地定义和实现我们想要的任何东西;-)
用户函数表达式用户函数调用很容易解析。 这是如何完成的:
private void userExpression(Token token, Generator gen)
throws InterpreterException {
parseArguments(gen, getUserArity(token));
gen.makeInstruction(user.get(token.getStr()));
}
鉴于以下情况,此方法解析用户函数调用的参数
用户功能。 然后,它使代码生成器创建一条指令
它从User对象中检索(请参阅本章的第一部分)。
“ getUserArity”方法如下所示:
[code = java]
私人int getUserArity(令牌令牌)抛出InterpreterException {
skipDemand(“(”);
返回user.get(token.getStr())。getArity();
}
[码]
首先,它在跳过用户名后需要左括号
定义的功能。 然后,它再次查询该用户对象以获取
用户定义的功能。
解析用户定义函数的实际参数的过程如下:
private int parseArguments(Generator gen, int n) throws InterpreterException {
for (int i= 0; i < n; i++) {
binaryExpression(gen, 0);
if (i < n-1) demand(",");
}
demand(")");
return n;
}
'n'参数被解析;
每个参数都可以是整个二进制表达式
再次。 除了参数之外,每个参数表达式后都必须是逗号。
最后一个参数,在这种情况下,应使用正确的括号。
内置函数表达式解析内置函数,无论listfuncs还是函数都是在
与解析用户定义函数的方式类似。 看一看:
private void functionExpression(Token token, Generator gen)
throws InterpreterException {
gen.makeFunctionInstruction(token,
parseArguments(gen, getArity(token)));
}
再次解析参数(请参见上文)并创建FunctionIstruction
由代码生成器。 唯一的区别是解析已经知道如何
内置函数需要许多参数,即必须从中检索
ParserTable,其中预定义了内置令牌的元素:
private int getArity(Token token) throws InterpreterException {
skipDemand("(");
return ParserTable.arity.get(token.getStr());
}
将此方法与getUserArity方法进行比较(请参见上文)。
他们很相似
唯一的区别是他们找到该功能的重要性的方式。
带引号的对象表达式带有引号的神秘对象(认为是“ if”对象)看起来类似于
普通函数调用,但不是:为它们的参数生成的代码
不是由普通代码生成器生成的,而是由另一个生成器生成的
用于此目的,并将参数的已编译代码传递给
编译引用对象。 这是完成的过程:
private void quoteExpression(Token token, Generator gen)
throws InterpreterException {
List<Code> args= new ArrayList<Code>();
for (int i= 0, n= getArity(token); i < n; i++) {
Generator arg= new Generator();
binaryExpression(arg, 0);
if (i < n-1) demand(",");
args.add(arg.getCode());
}
demand(")");
gen.makeQuoteInstruction(token, args);
}
对于每个单个参数,使用新的代码生成器;
所有代码已收集
在已编译的代码列表中。 语法检查类似于语法检查
用于用户定义或内置函数。 最后创建QuoteInstruction
由“正常”主代码生成器生成。 利用这种神秘行为
在本章的前面部分已作了部分解释,我们将回到
当我们讨论说明时。
列出表达式这里只有一部分要解释。 我们的小语言可以处理列表。
列表是一堆用大括号括起来的表达式。 注意一个表达式
是一个列表,因此列表可以将列表作为元素,而列表可以作为
他们的元素等等。
这是解析列表的方式:
private void listExpression(Generator gen) throws InterpreterException {
int arity;
for (arity= 0;; ) {
if(expect("}"))
if (arity == 0) break;
else throw new ParserException(tz,
"list expression error");
binaryExpression(gen, 0);
arity++;
if (expect(",")) continue;
demand("}");
break;
}
gen.makeListInstruction(arity);
}
基本上,此方法继续调用外部binaryExpression方法以
收集列表中的所有元素。 注意语法正确
通过在正确的位置调用期望和需求方法(选中它),然后
最后,代码生成器被要求创建一个给定编号的ListInstruction
列表中刚刚解析的元素的数量。 请注意,列表可以包含
其他列表作为其元素只是因为使用了所有递归。
码我们已经看过几次这个神秘的Code对象。 一个Code对象是
由代码生成器管理。 一个Code对象就是这样:
public class Code extends ArrayList<Instruction> {
private static final long serialVersionUID = 3728486712655477743L;
public static Code read(InputStream is) throws InterpreterException {
try {
return (Code)new ObjectInputStream(is).readObject();
}
catch (Exception e) {
throw new InterpreterException("can't read code", e);
}
}
public static Code read(String name) throws InterpreterException {
FileInputStream fis= null;
try {
return read(fis= new FileInputStream(name));
}
catch (IOException ioe) {
throw new InterpreterException("can't read code from: "+name, ioe);
}
finally {
try { fis.close(); } catch (Exception e) { }
}
}
public void write(OutputStream os) throws InterpreterException {
try {
new ObjectOutputStream(os).writeObject(this);
}
catch (Exception e) {
throw new InterpreterException("can't write code");
}
}
public void write(String name) throws InterpreterException {
FileOutputStream fos= null;
try {
write(fos= new FileOutputStream(name));
}
catch (IOException ioe) {
throw new InterpreterException("can't write code to: "+name, ioe);
}
finally {
try { fos.close(); } catch (Exception e) { }
}
}
}
如您所见,Code对象是一个包含指令的列表。
班上
包含两种便捷方法,即它可以将自身序列化为
OutputStream或一个文件以及两个静态方法可以读取序列化的Code对象
从InputStream或再次从文件。 除了这些方法之外,Code对象只是
是指令列表。 我们将在下一部分讨论指令
文章。
请注意,每当读写失败时,便捷方法便将抛出的异常很好地包装在另一个InterpreterException中,并让其冒泡
这些方法的调用者。 如果方法传递了一个字符串,那应该是
作为文件名; 否则,流将用于读取或写入
但他们没有关闭。 由调用者来处理。
这是正常的行为:如果某个对象“拥有”某物,则应对此负责;
否则,其他一些对象(所有者)应承担责任。 当一个文件
在Code对象中传递的名称是它创建的流的所有者,并且
完成后应将其关闭。
当然,读取方法是静态方法(尚无Code对象),但
write方法不是静态的:Code对象可以序列化自己。 最后,注意
这个有趣的数字:序列化框架需要使用
Code类的不同版本。
结束语您可能已经注意到,解析器可以重用,即它们可以解析多个
给读者一个令牌流; 他们一遍又一遍地重复使用相同的Tokenizer。
我们需要这种行为,因为解析器存储了wrt用户的信息
定义的函数,我们想在交互式环境中使用解析器
用户逐行输入文本的位置。 解析器构造
他们自己使用的分词器; 只有同一类(其对象)
封装,因为Tokenizer类所在的位置可以实例化Tokenizer。
有一种很好的便捷方法可以单次运行:
给定文件名,它将打开一个流并解析整个文本。 我们
当我们想将解析器用作常规编译器时,也需要这种行为
编译文件,并可能将生成的代码再次保存到文件中。
在编译器文章的这一部分中,有很多代码,但是值得
它。 我们已经为编译器实现了整个解析器。 我们仍然做不到
用这种小语言有用的东西,因为有发电机
实现,最后有一个口译员; 告诉我们是否
在构建之前的阶段中,我们是否做得正确
编译器系统。
希望您能在那段完整的代码雪崩之后继续与我在一起
解析器。 如果您仍然是:恭喜! 你已经看到解析器可以
根据形式的语法规则一对一地构建,并带有一些
细节散布。我们已经制作了分词器,因此我们可以解析源文本
并立即检查语法正确性。 本文的下一部分
将包含到目前为止的源代码。 我们将看看那些
表达式被编译为等同于后缀形式的表达式,并且
我们将开始处理实际上执行我们指令的指令
希望他们执行,即评估组成我们程序的表达式。
下周见
亲切的问候,
乔斯
翻译自: https://bytes.com/topic/java/insights/739691-compilers-5b-parsers
无法解析的编译问题: