编译器-5B:解析器

问候,

欢迎回到解析器文章章节的续篇。 这部分是专用的

到ExpressionParser,这是我们的小语言最大的解析器类。

此类解析完整的表达式,就像其他解析器类一样

快速调用生成器。 解析成功完成后,

返回生成的代码。 否则,解析将中止,并且异常

抛出中止的原因。

ExpressionParser

ExpressionParser类的强制性序言再次变得无聊:


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方法进行比较(请参见上文)。 他们很相似

唯一的区别是他们找到该函数的Arity的方式。

带引号的对象表达式

带有引号的神秘对象(认为是“ 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。

有一种很好的便捷方法可以单次运行:

给定文件名,它将打开一个流并解析整个文本。 我们

当我们想将解析器用作常规编译器时,也需要这种行为

编译文件,并可能将生成的代码再次保存到文件中。

在编译器文章的这一部分中,有很多代码,但是值得

它。 我们已经为编译器实现了整个解析器。 我们仍然做不到

用这种小语言有用的东西,因为有发电机

实现,最后有一个口译员; 告诉我们是否

在构建之前的阶段中,我们是否做得正确

编译器系统。

希望您能在那段完整的代码雪崩之后继续与我在一起

解析器。 如果您仍然是:祝贺您; 您已经看到解析器如何

根据形式的语法规则一对一地构建,并带有一些

细节撒满了。我们已经制作了分词器,以便我们可以解析源文本

并立即检查语法正确性。 本文的下一部分

将包含到目前为止的源代码。 我们将看看那些

表达式被编译为等同于后缀形式的表达式,并且

我们将开始处理实际上执行我们指令的指令

希望他们执行,即评估组成我们程序的表达式。

下周见

亲切的问候,

乔斯

From: https://bytes.com/topic/java/insights/739691-compilers-5b-parsers

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值