Nirvana Studio » 编译器 :: 分享知识,传播技术

Archive for the '编译器' Category

理解 ANTLR 语法文件

Posted by Nicholas Ding on 26th 四月 2006

理解 ANTLR 语法文件

译者:Nicholas @ NirvanaStudio

原文出处:http://www.placidsystems.com/articles/article-grammarlayout/grammarLayout.htm

你是否被Antlr语法文件的不同部分搞的晕头转向呢,你是否很想知道这些部分的含义呢?我们在这里将从另一个方面来了解它们,这里我们使用Antlr Studio来演示。

不管你信不信,Antlr语法文件在某种程度上和Java源代码很类似。什么,你看不出任何与Java代码相似的地方?好,就让我给你展示……

语法(Grammar)

1

有放在header部分的内容将会出现在Antlr生成的Java代码的头部。站在Java文件的层次上思考,你通常在这个部分里面放置包定义。你还可以把一部分import的内容放在这里。

2这个部分的代码对于文件中的每个语法来说是唯一的。这个部分会被放在类声明之前。以上的例子将只对CalcParser引入ArrayList和MyClass这两个类。

3 width="20" />然后我们开始定义语法,这看起来像是在声明一个类。

4 width="20" />在这个options部分,你可以制定语法生成的一些参数。在Antlr Studio中你可以使用Ctrl+Space看看哪些选项可用。

5 width="20" />Token部分用来制定“假想的”记号,那些通常没有声明在lexer中。这些是在TreeParsers中使用的“假想的”记号。

6 width="20" />另一个行为部分。这个部分的内容被放在类内部。你可以为你的解析器定义一些自定义方法。

规则(RULES)

7 width="746" />

Antlr语法文件中的规则定义对应生成的Java代码中的一个方法定义。

1 width="20" />2 width="20" />3 width="20" />4 width="20" />正如你所看到的,这里我们可以在规则中做任何事,这些可以通过一个函数完成。我们可以为规则指定参数,上面的例子显示了这一点,甚至可以指定返回值和异常。

5 width="20" />这个options部分允许你指定一些可选参数。

7 width="20" />我们可以指定自定义异常处理机。

Java and all Java-based trademarks and logos are
trademarks or registered trademarks of Sun Microsystems, Inc. in the United
States, other countries, or both.

Posted in 编译器 | 1 Comment »

ANTLR 介绍

Posted by Nicholas Ding on 17th 四月 2006

ANTLR 介绍

作者: Terence
Parr

译者:Nicholas @ NirvanaStudio


原文出处:http://www.cs.usfca.edu/~parrt/course/652/lectures/antlr.html

介绍

自1980年以来我手工编写了很多识别程序(recognizer)和翻译程序(translator)但最终我感到很恶心并且尝试将这个过程自动化:来源于我的座右铭:


Why program by hand in five days what you can spend five years of your life automating.

手工编写过很多程序之后你就可以发现一些共性,并且这些共性可以合理地格式化并且自动生成。我当时对yacc不是很熟悉但是想要一些东西去代替我原本需要手工

编码的工作。ANTLR就是这个最终的结果(实际上原来它叫做PCCTS)。我现在已经为之工作了十年了。

ANTLR, ANother Tool for Language Recognition, 是一个可以接受含有语法描述的语言描述符并且生成程序能够识别这些语言所产生的句子。作为一个翻译程序的

一部分,你可以给你的语法附上简单的操作符和行为并且告诉ANTLR如何构造AST并且如何输出它们。ANTLR知道如何使用Java,C++,C#或者Python来生成它们。

ANTLR知道如何构造识别程序并且将语法结构应用到三种不同的输入上:(i) 字符流,
(ii) 标记(token)流,(iii) 二维树结构。本质上这些对应了词法分析器(Lexer),解析器(Parser)和Tree Walker。用于制定这些语法(Grammar)的句法(Syntax),被称

meta-language,在所有情况下是独立的。

一旦你适应了ANTLR或者相应的工具,你将会以另一种眼光来看待编程。很多任务期待一种不同于传统编程语言流派的语言解决方案。举个例子,这些课程的笔记就是用TML编写

的,Terence’s Markup Language。我讨厌输入HTML所以我用ANTLR编写了一个简单的翻译程序来转换文本成为HTML或者PDF或者其他我讨厌直接编写的东西。

最后,情让我指出ANTLR仅仅是一个工具!它帮你通过自动生成单调乏味的组件来构造程序,但并不试图让你创造一个完整的编译器,举个例子,单行的描述。

在2003年以前,ANTLR的下载量一度达到5000每月。当时ANTLR直接暴露在公共域而且没有一个明确的版权但是附带了完整的源代码。

这些笔记假设你已经熟悉了基本的语言识别和翻译概念。那么现在你就需要熟悉ANTLR的元语言以及如何生成它。之后,我们将把焦点集中在构造复杂的翻译器上。

一个对 ANTLR 句法的简单介绍

了解ANTLR最好的办法就是通过例子。一个简单的计算器经常被用来作为起步教程,并且有一个很好的理由支持这么做:它容易理解且实现简单。还有一些ANTLR例子和教程,但

是在这里我将会用自己的语言来描述一个计算器。首先我们将要做一些东西可以直接计算这些简单的表达式。然后我们将会生成树并且计算这课树得到相同的结果。

当你知道最后需要将输入的字符流断开成为一个个的记号(token),思考表达式的语法结构是一个良好的开端。

直接执行句法

识别程序

让我们编写一个程序来接受一个带有加、减、乘,例如3+4*5-1的表达式,或者带有括号的,用来强行限制计算顺序的如(3+4)*5的表达式。

所有ANTLR语法都是LexerParser或者TreeParser的子类,因此你需要从语法的层次上来思考这些东西,你将会构造一个Parser的子类

。在类声明之后,内需要用EBNF符号来制定规则:


class ExprParser extends Parser;

expr: mexpr ((PLUS|MINUS) mexpr)*
;

mexpr
: atom (STAR atom)*
;

atom: INT
| LPAREN expr RPAREN
;

这个词法分析器遵循了一个相似的模式并且只需要定义一些操作符和空格。将这些词法放入一个文件中,expr.g,是最简单的方法:


class ExprLexer extends Lexer;

options {
k=2; // needed for newline junk
charVocabulary=’\u0000′..’\u007F’; // allow ascii
}

LPAREN: ‘(’ ;
RPAREN: ‘)’ ;
PLUS : ‘+’ ;
MINUS : ‘-’ ;
STAR : ‘*’ ;
INT : (’0′..’9′)+ ;
WS : ( ‘ ‘
| ‘\r’ ‘\n’
| ‘\n’
| ‘\t’
)
{$setType(Token.SKIP);}
;

要生成一个采用Java解释的语法,用如下方式:


$ java antlr.Tool expr.g
ANTLR Parser Generator Version 2.7.2 1989-2003 jGuru.com
$

ANTLR 生成了什么?

ANTLR生成的识别程序模仿了你需要手工编写的递归的解析器;yacc和它的朋友,另一方面,生成了一个满是整数的表来模拟有限状态机的行为。

ANTLR 将会生成以下文件:


ExprLexer.java
ExprParser.java
ExprParserTokenTypes.java
ExprParserTokenTypes.txt

如果你看下生成的代码,举个例子,ExprParser.java,你将会看到对语法解析文件expr.g中的每个规则生成了一个函数。举个例子,mexpr

atom的代码应该是这样:


public void mexpr() {
atom();
while ( LA(1)==STAR ) {
match(STAR);
atom();
}
}

public void atom() {
switch ( LA(1) ) { // switch on lookahead token type
case INT :
match(INT);
break;
case LPAREN :
match(LPAREN);
expr();
match(RPAREN);
break;
default :
// error
}
}

注意这些规则引用被翻译成了函数,记号引用被翻译成了match(TOKEN)调用。从一个语法文件构造解析器的唯一难点就是计算lookahead信息。

记号类型这个类定义了记号类型常量,以便于词法分析和语法分析之用:


// $ANTLR 2.7.2: “expr.g” -> “ExprParser.java”$

public interface ExprParserTokenTypes {
int EOF = 1;
int NULL_TREE_LOOKAHEAD = 3;
int PLUS = 4;
int MINUS = 5;
int STAR = 6;
int INT = 7;
int LPAREN = 8;
int RPAREN = 9;
int WS = 10;
}

测试词法分析和语法分析

要使用生成的解析器,在ExprParser.java中,使用如下main()函数:


import antlr.*;
public class Main {
public static void main(String[] args) throws Exception {
ExprLexer lexer = new ExprLexer(System.in);
ExprParser parser = new ExprParser(lexer);
parser.expr();
}
}

$ java Main
3+(4*5)
$

或者针对无效输入:


$ java Main
3++
line 1:3: unexpected token: +
$

或者


$ java Main
3+(4
line 1:6: expecting RPAREN, found ‘null’
$

表达式计算

要实际计算表达式,只需要给解析器增加行为:


class ExprParser extends Parser;

expr returns [int value=0]
{int x;}
: value=mexpr
( PLUS x=mexpr {value += x;}
| MINUS x=mexpr {value -= x;}
)*
;

mexpr returns [int value=0]
{int x;}
: value=atom ( STAR x=atom {value *= x;} )*
;

atom returns [int value=0]
: i:INT {value=Integer.parseInt(i.getText());}
| LPAREN value=expr RPAREN
;

词法分析也是一样,除了增加一个print语句在主函数中:


import antlr.*;

public class Main {
public static void main(String[] args) throws Exception {
ExprLexer lexer = new ExprLexer(System.in);
ExprParser parser = new ExprParser(lexer);
int x = parser.expr();
System.out.println(x);
}
}

现在,当你运行程序,你会得到一下结果:


$ java Main
3+4*5
23
$ java Main
(3+4)*5
35
$

ANTLR 如何翻译动作

动作通常会被放入生成的解析器的代码中:

像下面的return规则


mexpr returns [int value=0]
: …
;

被翻译为


public int mexpr() {
int value=0;

return value;
}

如果你增加一个参数,这个参数同样复制到了方法的定义:


mexpr[int x] returns [int value=0]
: … {value = x;}
;

生成


public int mexpr(int x) {
int value=0;

value = x;
return value;
}

所以,完整的mexpratom翻译规则看起来像下面的代码:


public int mexpr() {
int value=0;
int x; // local variable def from rule mexpr
value = atom();
while ( LA(1)==STAR ) {
match(STAR);
x = atom();
value *= x;
}
return value;
}

public int atom() {
int value=0;
switch ( LA(1) ) { // switch on lookahead token type
case INT :
Token i = LT(1); // make label i point to next lookahead token object
match(INT);
value=Integer.parseInt(i.getText()); // compute int value of token
break;
case LPAREN :
match(LPAREN);
value = expr(); // return whatever expr() computes
match(RPAREN);
break;
default :
// error
}
return value;
}

通过AST中间形式计算结果

现在你看到了一个基本的直接基于句法的翻译/计算描述,里面的语法/句法制定了何时执行动作
一个强有力的策略就是构造一个间接的表示形式持有所有或者大多数编码过的输入符号,在数据结构中,包含这些记号的关系。
举个例子,输入“3+4”可以通过一个抽象语法树(AST)来表示:


+
/ \
3 4

针对这种树,内需要一个TreeWalker(由ANTLR从一个树语法中生成)来计算之前的相同值,但是采用了一个不同的方式。

AST的用法变得很清晰,就是当你需要从多次遍历这棵树来指出什么需要计算或者重写,或将树转变为另一种语言的时候就需要用AST。

构造AST

用ANTLR来生成一个有用的AST非常容易。在我们的例子中,打开buildAST选项并且增加一些后缀操作符告诉ANTLR何种记号需要构成子树的根节点。


class ExprParser extends Parser;

options {
buildAST=true;
}

expr: mexpr ((PLUS^|MINUS^) mexpr)*
;

mexpr
: atom (STAR^ atom)*
;

atom: INT
| LPAREN! expr RPAREN!
;

然后,词法不需要改变,用来计算树结果的主程序如下:


import antlr.*;
import antlr.collections.*;

public class Main {
public static void main(String[] args) throws Exception {
ExprLexer lexer = new ExprLexer(System.in);
ExprParser parser = new ExprParser(lexer);
parser.expr();
AST t = parser.getAST();
System.out.println(t.toStringTree());
}
}


$ java Main
3+4
( + 3 4 )
$ java Main
3+4*5
( + 3 ( * 4 5 ) )
$ java Main
(3+4)*5
( * ( + 3 4 ) 5 )
$

AST 解析与计算

通过以上的解析器构造的树非常简单。在TreeParser中一条规则就够了。


class ExprTreeParser extends TreeParser;

options {
importVocab=ExprParser;
}

expr returns [int r=0]
{ int a,b; }
: #(PLUS a=expr b=expr) {r = a+b;}
| #(MINUS a=expr b=expr) {r = a-b;}
| #(STAR a=expr b=expr) {r = a*b;}
| i:INT {r = (int)Integer.parseInt(i.getText());}
;

主程序被修改成了使用新的TreeParser来实现计算功能:


import antlr.*;
import antlr.collections.*;

public class Main {
public static void main(String[] args) throws Exception {
ExprLexer lexer = new ExprLexer(System.in);
ExprParser parser = new ExprParser(lexer);
parser.expr();
AST t = parser.getAST();
System.out.println(t.toStringTree());
ExprTreeParser treeParser = new ExprTreeParser();
int x = treeParser.expr(t);
System.out.println(x);
}
}

现在你得到了树结构以及计算结果。


$ java Main
3+4
( + 3 4 )
7
$ java Main
3+(4*5)+10
( + ( + 3 ( * 4 5 ) ) 10 )
33
$

Posted in 编译器 | No Comments »

初识 ANTLR

Posted by Nicholas Ding on 17th 四月 2006

由于并非科班出生,所以对于喜欢计算机程序设计的我来说,尽管通过自己的努力学习了相当多的实用技术,但是让我总觉得遗憾的除了高等数学之外就是汇编和编译原理了。从C到Java的过渡,让我从指针和数据结构中脱离出来,长时间的使用Java让我很少去接触系统底层,开发中几乎不会遇到任何汇编代码,除了能够理解基本的汇编指令之外,对于汇编,我已经没有多少感情了。除了汇编之外、数学和编译原理一直是我最想去学习的,但是自学编译原理让人很难上手,虽然可以大体上理解书上的理论,可是依然感觉要自己亲手写个解释器则是难上加难,直到遇到了ANTLR,我才渐渐开始理解《编译原理》这本书的真正内涵。

在《编译原理》这本书上的介绍非常理论性,对于一个非科班出生人来说很难简单的读懂,他在一个很高的高度上涵盖了编译器(Complier)和解释器(Interpreters)的内容。编译器与解释器的界限是非常模糊的,ANTLR的作者Terence Parr在它的文章An Overview of Language Implementation上给出了一个比较浅显的解释。

Anything that executes natively via machine instructions you can safely say is compiled; everything else is interpreted. A more useful way to describe the difference is that compilers translate source to an intermediate representation (IR) and then translate that to machine code usually by making several passes over the IR. An interpreter on the other hand stops the compilation process before machine code generation at the IR–it “executes” or interprets the IR, emulating an abstract high-level machine architecture.

总体来说,要实现一个Compiler的难度要比Interpreter大的多,虽然经过编译的代码在执行效率上要比动态解释的代码高,但是解释型的语言具有更快的加载速度和扩展性,况且基于虚拟机的动态语言的盛行也证实了解释型语言的强大生命力。

下面主要讨论一个解释器的原理。一般来说,解释器接受一段文本,对文本进行词法分析,经过词法分析之后在进行语法分析,在检测无误后根据语法规则开始解释。

下面看一个最基本的词法分析的例子。这个例子是一个最简单的词法分析器,来匹配文本中的字符串和数字。这个文件采用EBNF描述。例子取自于Terence Parr所写的关于ANTLR的例子。

class SimpleLexer extends Lexer

options { k=1; filter=true; }

ALPHA
    : ('a'..'z'|'A'..'Z')+
    {System.out.println(“Found alpha: “ + getText());}
    ;

NUMERIC
    : ('0'..'9')+
    {System.out.println(“Found numeric: “ + getText());}
    ;

EXIT
    : '.' {System.exit(0);} ;

将这个文件保存为simple.g,然后使用ANTLR生成相应的词法分析器,java antlr.Tool simple.g,并且使用以下代码可以观察结果。

import java.io.*;
public class Main {
    public static void main(String[] args) {
        SimpleLexer simpleLexer = new SimpleLexer(System.in);
        while(true) {
            try {
                simpleLexer.nextToken();
            } catch(Exception e) {}
        }
    }
}

如果你输入:This Lexer recognises strings and numbers: hello 22 goodbye 33

那么程序将会输出:

Found alpha: This
Found alpha: Lexer
Found alpha: recognises
Found alpha: strings
Found alpha: and
Found alpha: numbers
Found alpha: hello
Found numeric: 22
Found alpha: goodbye
Found numeric: 33
It ignores everything else: -=+/#
Found alpha: It
Found alpha: ignores
Found alpha: everything
Found alpha: else
.

这个例子演示了使用ANTLR如何编写一个简单词法分析器,一般来说词法分析是配合语法分析一起使用的,在ANTLR的站点上有很多例子可以参考。

ANTLR : http://www.antlr.org/

 

Posted in 编译器 | No Comments »

SmaCC 指南

Posted by ShiningRay on 4th 四月 2006

这是一个用于演示一些SmaCC(Smalltalk编译器的编译器)的简要指南。在这个例子中,我们会逐步开发一个简易的计算器。

如果你已经做过这种东西,你可以先 载入代码 。你载入了代码之后,你需要打开SmaCC解释器生成器。在VisualWorks 和 VisualAge中,它在Tools菜单下。Dolphin的在一个额外的工具目录中。它会打开一个类似下面的窗口:

SmaCC Window

我们第一个计算器相对比较简单。它只要能读取两个数字并把它们相加。开始之前,我们首先要告诉扫描程序如何辨认一个数字。数字由一个或多个数字打头,后面可能还有一个小数点加上0或者更多的数字。扫描程序对这个标记的定义是:

<number>        :       [0-9]+ (\. [0-9]*) ? ;

把这行代码输入界面上的scanner标签页中。让我们逐个看每一个部分:



<number>

指出记号的名字。在<>中的名称必须是合法的Smalltalk变量名。

:

分隔记号名称和记号定义。

[0-9]

匹配任何一个在’0′到’9′(一个数字)范围中的字符。

+

匹配前面的表达式一次或多次。在这种情况下,我们要匹配一个或多个数字。

( … )

标示子表达式组。

\.

匹配 ‘.’ 字符(. 在正则表达式中有特殊的含义,使用 \ 来转义)。

*

匹配前一个表达式零次或多次。

?

匹配前面的表达式零次或一次。(也就是,前面表达式是可选的)。

;

终止一个记号说明。

我们不想去关心我们语言中的空白符,所以我们需要定义什么是空白符并且忽略它,输入下面的记号说明:

<whitespace>    :       \s+;

\s 会匹配任何空白字符(空格、制表符、换行、回车等等)。然后我们怎么告诉扫描程序去忽略它呢?
如果你看一下SmaCCScanner类,你会发现一个叫做’whitespace’的方法。如果一个扫描程序有一个方法的名称和某个标记一样,那么一旦扫描程序匹配了这类标记就会调用这个方法。正如你所见,whitespace方法会吃掉空白符。同样还有一个’comment’方法会作类似的处理。

说到我们的语法,现在让我们来定义它吧。在Parser表格中输入以下语法说明:

Expression :
Expression "+" Number
| Number ;
Number : <number>;

这基本上指出了一个表达式可以是一个数字或者是另一个表达式加上一个数字。

我们现在应该可以编译一个分析器了。切换到Compile标签页。你要输入扫描器和分析器的类的名称。这里我们相应使用CalculatorScanner 和CalculatorParser。当类名输完之后,我们就准备编译分析器了。点击 ‘Compile LARLR(1)’
按钮(你应该总是点这个按钮除非你知道你要做什么。一般来说,他会生成比另一个选项更小的分析器)。这时就会生成新的CalculatorScanner和CalculatorParser的Smalltalk类同时会编译这两个类中的一些方法。所有的SmaCC编译出来的方法会按照”generated-*”的格式。你不可以更改这些方法因为每次你重新编译他们都会被覆盖。

不管SmaCC何时创建新类,这些类都会被放到默认的应用程序/包中。如果你使用的是VisualAge,你要确保默认应用程序是开放的版本而且SmaCCRuntion应用程序已经安装(prereq)。

如果你已经生成了扫描器和分析器类,你可以通过类名旁边的”…”按钮来载入他们的定义。如果在出现的对话框中你回答”Yes”,那么在Scanner/Parser标签页中的文本就会被替换为上一次编译过的定义(假设”Generate definition comments”在上一次编译中被选中了)。

现在我们要测试我们的分析器了。进入“test”面板,输入“ 3 + 4”(不要加双引号),并且点击“parse”按钮;你会看到分析器正确分析了它。如果你点击“Parse and Inspect”你会看到一个检视器(inspector),里面有一个包含了被解析的记号的顺序集合(OrderedCollection)。这是因为我们没有指明当分析器在解析的时候要怎么处理记号。你也可以是如一个不正确的内容。例如尝试解析“3 + + 4”或者“3 + a”。应该会出现一个错误信息。

现在我们要定义当我们分析我们的表达式的时候要产生的动作。当前的情况是,我们的分析器仅仅验证表达式是一些相加的数字。一般来说你要创建一些结果来表示你已经解析了什么内容(比如,一个棵分析树)。然而,在这个情况下,我们不关心结构,我们只关心结果(表达式的值)。在我们的例子中,你需要把语法定义改成如下:

Expression :
Expression "+" Number {’1′ + ‘3′}
| Number {’1′};
Number : <number> {’1′ value asNumber};

括号中的文本是Smalltalk代码,当应用规则的时候,就会执行这个代码。有一个数字的字符串会被替换成相应的解析节点。在第一个Expression的规则中,’1′会被替换成匹配Expression的ParseNode同时’3′会被替换成匹配Number的ParseNode。在规则中的第二个东西是’+'记号。因为我们已经知道它是什么了,所以我们对他不感兴趣。编译新的分析器。现在当你从Test面板中执行’Parse and Inspect’时,你应该看到这个结果:7。

前面的代码有一个问题是如果你需要更改一个规则,你可能也要跟着修改规则内的代码。例如,假设你你在规则的开头添加了一个新的记号,那么你就要更改所有在Smalltalk代码中的引用了。我们可以通过使用命名表达式来减少这种问题。在规则的每个部分后面,我们可以指明它的名称。名称是通过单引号来标明的,它同样必须是一个合法的Smalltalk变量名。象下面这个:

Expression :
Expression ‘expression’ “+” Number ‘number’ {expression + number}
| Number ‘number’ {number};
Number : <number> ‘numberToken’ {numberToken value asNumber};

它和前面解析的语言的结果是一样的,但它同时让你更容易维护的你分析器。让我们现在扩展我们的语言并加入减法功能。这里是新的语法:

Expression :
Expression 'expression' "+" Number 'number' {expression + number}
| Expression ‘expression’ “-” Number ‘number’ {expression - number}
| Number ‘number’ {number};
Number : <number> ‘numberToken’ {numberToken value asNumber};

你编译了这个代码之后,’3 + 4 - 2′就会返回’5′了。下面,再让我们加入乘法和减法:

Expression :
Expression 'expression' "+" Number 'number' {expression + number}
| Expression 'expression' "-" Number 'number' {expression - number}
| Expression ‘expression’ “*” Number ‘number’ {expression * number}
| Expression ‘expression’ “/” Number ‘number’ {expression / number}
| Number ‘number’ {number};
Number : <number> ‘numberToken’ {numberToken value asNumber};

这时我们遇到一个问题.如果你计算” 2 + 3 * 4“,最后的结果将是 20。这个问题是因为在标准的数学中,乘法比加法有更高的优先级。我们的语法是严格按照从左到右的方式运算的。这个问题一般的解决方法是定义加法的非终结符来强制计算的顺序。这个解决方法的语法类似:

Expression :
Term 'term' {term}
| Expression 'expression' "+" Term 'term' {expression + term}
| Expression 'expression' "-" Term 'term' {expression - term};
Term :
Number 'number' {number}
| Term 'term' "*" Number 'number' {term * number}
| Term 'term' "/" Number 'number' {term / number};
Number : <number> 'numberToken' {numberToken value asNumber};

这时候如果你编译这个语法,你会看到” 2 + 3 * 4 “的计算结果是14,正如我们所期望的那样。现在,正如你可以想象的,当优先级规则的数量增加时,语法也越来越复杂(例如,C语言)。我们可以使用歧义语法和优先级规则来简化这种情况。这里有一段使用优先级来限制计算顺序的一段语法:

%left “+” “-”;
%left “*” “/”;

Expression :
Expression ‘exp1′ “+” Expression ‘exp2′ {exp1 + exp2}
| Expression ‘exp1′ “-” Expression ‘exp2′ {exp1 - exp2}
| Expression ‘exp1′ “*” Expression ‘exp2′ {exp1 * exp2}
| Expression ‘exp1′ “/” Expression ‘exp2′ {exp1 / exp2}
| Number ‘number’ {number};
Number : <number> ‘numberToken’ {numberToken value asNumber};

注意我们更改了语法所以操作符两边都是Expression。我们在语法顶部添加的两行表示“+”和“-”是从左至右运算的而且优先级相同,同时他们的优先级比“*”和“/”低。类似的,第二行表示“*”和“/”有同样的优先级。这个形式的语法通常更加直观,特别是当有很多优先级要处理的时候。我们再来一个例子,现在加入指数运算和括号:

%left "+" "-";
%left "*" "/";
%right “^”;
Expression :
Expression ‘exp1′ “+” Expression ‘exp2′ {exp1 + exp2}
| Expression ‘exp1′ “-” Expression ‘exp2′ {exp1 - exp2}
| Expression ‘exp1′ “*” Expression ‘exp2′ {exp1 * exp2}
| Expression ‘exp1′ “/” Expression ‘exp2′ {exp1 / exp2}
| Expression ‘exp1′ “^” Expression ‘exp2′ {exp1 raisedTo: exp2}
| “(” Expression ‘expression’ “)” {expression}
| Number ‘number’ {number};
Number : <number> ‘numberToken’ {numberToken value asNumber};

当你编译了这个语法之后,你就可以正确计算” 3 + 4 * 5 ^ 2 ^ 2“得到2503了。由于这个指数操作是右结合的,所以这个表达式是象这样计算的3 + (4 * (5 ^ (2 ^ 2)))。我们也可以计算带括号的表达式。例如,计算 ” (3 + 4) * (5 - 2) ^ 3 “将得到189。

Posted in Smalltalk, 编译器 | No Comments »