系列链接:
一个简单的语言的语法(一):用ANTLR描述语法
[url=http://rednaxelafx.iteye.com/blog/314507]一个简单的语言的语法(二):ANTLR的重写规则[/url]
[url=http://rednaxelafx.iteye.com/blog/315807]一个简单的语言的语法(三):做些小调整,并将生成目标换到CSharp2[/url]
在JavaEye的博客频道逛,看到[url=http://neuronr.iteye.com/]NeuronR[/url]的blog上有关于他的编译器实践的帖子,觉得有点意思,于是平行的用别的方法来做那个编译器。那边要求是用C来实现,我这边就用些方便些的语言来实现吧。
本篇将通过[url=http://www.antlr.org]ANTLR[/url] 3.1描述Jerry语言,并在[url=http://antlr.org/works]ANTLRWorks[/url]里实验,通过生成的解析器来得到Jerry程序代码对应的解析树。
关注过解析器生成器的话,ANTLR应该不会是个陌生的名字才对。Anyway简短说几句。ANTLR在生成解析器方面正得到越来越多的应用,几个实例,[url=http://code.google.com/p/xruby/]XRuby[/url]项目用了,[url=http://www.jython.org/]Jython[/url]现在正在使用,[url=http://www.sapphiresteel.com/]SapphireSteel[/url]的Ruby和ActionScript IDE也用ANTLR来生成编译器前端。
[url=http://www.antlr.org]ANTLR[/url],ANother Tool for Language Recognition,是由[url=http://antlr.org/misc/contact]Terence Parr[/url]教授开发的语言工具,用于协助生成解析器、编译器、解释器等与语言处理相关的程序。它由PCCTS发展而来,特征是可以生成采用LL(*)算法的递归下降式的解析器。它所使用的语法表述格式类似于常见的EBNF(extended Backus–Naur form),易于理解和维护。
读了[url=http://neuronr.iteye.com/blog/309770]编译器构造实践[任务布置][/url]之后,大概琢磨着语法应该是像这样的吧:
Jerry.g:(ANTLR 3.1语法)
[code]grammar Jerry;
program : statementList EOF
;
statementList
: statement*
;
statement
: expressionStatement
| variableDeclaration
| blockStatement
| ifStatement
| whileStatement
| breakStatement
| readStatement
| writeStatement
;
expressionStatement
: expression SEMICOLON
;
variableDeclaration
: typeSpecifier Identifier ( LBRACK Integer RBRACK )* initializer?
( COMMA Identifier ( LBRACK Integer RBRACK )* initializer? )*
SEMICOLON
;
typeSpecifier
: INT | REAL
;
initializer
: EQ ( expression | arrayLiteral )
;
arrayLiteral
: LBRACE
( expression | arrayLiteral ) ( COMMA ( expression | arrayLiteral ) )*
RBRACE
;
blockStatement
: LBRACE statementList RBRACE
;
ifStatement
: IF LPAREN expression RPAREN statement ( ELSE statement )?
;
whileStatement
: WHILE LPAREN expression RPAREN statement
;
breakStatement
: BREAK SEMICOLON
;
readStatement
: READ variableAccess SEMICOLON
;
writeStatement
: WRITE expression SEMICOLON
;
variableAccess
: Identifier ( LBRACK Integer RBRACK )*
;
expression
: assignmentExpression
| logicalOrExpression
;
assignmentExpression
: variableAccess EQ expression
;
logicalOrExpression
: logicalAndExpression ( OROR logicalAndExpression )*
;
logicalAndExpression
: relationalExpression ( ANDAND relationalExpression )*
;
relationalExpression
: additiveExpression ( relationalOperator additiveExpression )?
| BANG relationalExpression
;
additiveExpression
: multiplicativeExpression ( additiveOperator multiplicativeExpression )*
;
multiplicativeExpression
: primaryExpression ( multiplicativeOperator primaryExpression )*
;
primaryExpression
: variableAccess
| Integer
| RealNumber
| LPAREN expression RPAREN
| SUB primaryExpression
;
relationalOperator
: LT | GT | EQEQ | LE | GE | NE
;
additiveOperator
: ADD | SUB
;
multiplicativeOperator
: MUL | DIV
;
// lexer rules
LPAREN : '('
;
RPAREN : ')'
;
LBRACK : '['
;
RBRACK : ']'
;
LBRACE : '{'
;
RBRACE : '}'
;
COMMA : ','
;
SEMICOLON
: ';'
;
ADD : '+'
;
SUB : '-'
;
MUL : '*'
;
DIV : '/'
;
EQEQ : '=='
;
NE : '!='
;
LT : '<'
;
LE : '<='
;
GT : '>'
;
GE : '>='
;
BANG : '!'
;
ANDAND : '&&'
;
OROR : '||'
;
EQ : '='
;
IF : 'if'
;
ELSE : 'else'
;
WHILE : 'while'
;
BREAK : 'break'
;
READ : 'read'
;
WRITE : 'write'
;
INT : 'int'
;
REAL : 'real'
;
Identifier
: LetterOrUnderscore ( LetterOrUnderscore | Digit )*
;
Integer : Digit+
;
RealNumber
: Digit+ '.' Digit+
;
fragment
Digit : '0'..'9'
;
fragment
LetterOrUnderscore
: Letter | '_'
;
fragment
Letter : ( 'a'..'z' | 'A'..'Z' )
;
WS : ( ' ' | '\t' | '\r' | '\n' )+ { $channel = HIDDEN; }
;
Comment
: '/*' ( options { greedy = false; } : . )* '*/' { $channel = HIDDEN; }
;
LineComment
: '//' ~('\n'|'\r')* '\r'? '\n' { $channel = HIDDEN; }
;[/code]
基本上是怎么简单怎么写弄出来的语法而已。有些地方,例如下标表达式(index expression),是可以显式写这么一条规则:
[code]indexExpression
: expression
| indexExpression '[' Integer ']'
;[/code]
然后削除直接左递归:
[code]indexExpression
: expression ( '[' Integer ']' )*
;[/code]
然后看情况再消除间接左递归。
但在Jerry语言里,能使用下标的只有数组变量,那就干脆不定义单独的下标表达式,而直接把下标的操作合并到variableAccess规则的数组变量分支里,也就是:
[code]variableAccess
: Identifier ( '[' Integer ']' )*
;[/code]
另外Jerry语言里的左值也只可能用变量访问来表示,所以也没有针对左值写特殊的规则(赋值表达式和read语句都需要用到左值的概念),直接就用variableAccess了。
这语法只是大概猜的而已。有些细节在NeuronR的帖子里没有提到,所以语法或许与他的课程实践的要求不完全一样。Anyway,我就先以这个理解为基础来平行做后续的实现了。
几个不太肯定的细节:
[color=darkred]1、if和while语句的条件表达式的类型有没有要求?[/color]
我这里是不在语法上对表达式类型做限制,到后面的语义分析的时候再检查。
[color=darkred]2、int/real与逻辑/比较运算表达式的类型(boolean)是否有隐式转换的关系?[/color]
我这里假设是“有”。如果有的话,relationalExpression那里就能偷懒;不然可能得做点麻烦些的处理……
主要是关系到那个一元布尔否定运算符('!'),C-like语言里它应该是跟一元算术求反运算符('-')的优先级一样吧?在这个Jerry语言它的优先级里却比所有算术和比较运算符要低。
看像这样的Java代码:
编译的时候会有错误,因为!的优先级比!=高,而!不能作用在类型为int的1上;
但这样的Jerry代码:
却应该能正确编译,且运行结果为1,因为!的优先级比!=低而比||高,所以1 != 1是false,它的反是true;然后1 == 1是true,跟前面的true做或运算也还是true。
这语法比较的诡异……
[color=darkred]3、变量声明是否一定要一并出现在同一个作用域的其它语句的前面(就像C89那样)?[/color]
我这里假设是“否”。变量可以在任何能出现语句的地方出现。作用域从声明开始,到它的包围块结束为止。
[color=darkred]4、浮点数的字面量是否要求小数点前必须有至少一位数字?浮点数字面量是否允许或要求后缀修饰(例如'r'或者'f'或者'd'之类)?[/color]
我的假设是浮点数只有一种字面量,满足这种形式:(用Perl兼容的正则表达式表示)
也就是小数点前后都必须有数字,而且没有后缀。
[color=darkred]5、多维数组的语义是怎样的?[/color]
能否支持这样的声明和赋值:
[code]int array2d[2][];
int array1d[10];
array2d[0] = array1d;[/code]
这个没办法假设……只能暂时认为数组声明的时候是多少维在访问的时候也必须用多少维来访问;这样比较简单,哈哈。
[color=darkred]6、数组声明的时候,是否允许用数组字面量来初始化?[/color]
我这里假设是“是”。
[color=darkred]7、数组字面量是否允许空元素?同时,数组可否声明为零长度的?[/color]
我这里假设数组字面量不允许内容为空(跟C一样),数组也不可以声明为零长度的。
[color=darkred]8、是否有强制类型转换的表达式?(根据原帖,赋值表达式有类型转换语义)[/color]
这里假设是“否”。有C-style的强制类型转换的话语法会麻烦不少……
暂时就先这样吧。上面的语法里有经典的dangling-else问题;不过ANTLR默认是匹配优先,能自动消除这个二义性,所以就没做特别的处理。
对这样的一段代码来测试:
可以生成这样的解析树(parse tree):
[img]http://rednaxelafx.iteye.com/upload/picture/pic/29333/9ad7ff13-c3fd-3c4c-ae1d-eac27767db90.jpg[/img]
(点击放大)
这个测试是通过ANTLRWorks的Interpreter模式来做的。编写本文的时候,最新版是1.2.2。可以在[url=http://antlr.org/download.html]这里[/url]获取。
观察此解析树:
输入进来的源码是一维的,而现在生成的解析树已经有了层次结构,变成二维的了。在得到层次结构后,原本用来标识层次结构的标点符号就变得冗余了,但这棵解析树仍然含有这些冗余的标点符号。
另外,可以观察到解析树里的许多子树中每个节点都只有一个子节点;这样的子树的中间节点都可以认为是冗余的。出现这种冗余的原因是:为了描述表达式中运算符的优先级高低,用LL语法需要为每一个优先级都写一条规则,每匹配到一个中间规则解析树里就多一个中间节点。
这些冗余对后续处理来说都是不利的。为了得到更干净更便于处理的表示,我们需要消除冗余,把解析树转换为抽象语法树(abstract syntax tree, AST)。这个时候ANTLR的重写规则(rewrite rule)就非常有用了。下一篇就来看看如何应用重写规则来得到抽象语法树。
一个简单的语言的语法(一):用ANTLR描述语法
[url=http://rednaxelafx.iteye.com/blog/314507]一个简单的语言的语法(二):ANTLR的重写规则[/url]
[url=http://rednaxelafx.iteye.com/blog/315807]一个简单的语言的语法(三):做些小调整,并将生成目标换到CSharp2[/url]
在JavaEye的博客频道逛,看到[url=http://neuronr.iteye.com/]NeuronR[/url]的blog上有关于他的编译器实践的帖子,觉得有点意思,于是平行的用别的方法来做那个编译器。那边要求是用C来实现,我这边就用些方便些的语言来实现吧。
本篇将通过[url=http://www.antlr.org]ANTLR[/url] 3.1描述Jerry语言,并在[url=http://antlr.org/works]ANTLRWorks[/url]里实验,通过生成的解析器来得到Jerry程序代码对应的解析树。
关注过解析器生成器的话,ANTLR应该不会是个陌生的名字才对。Anyway简短说几句。ANTLR在生成解析器方面正得到越来越多的应用,几个实例,[url=http://code.google.com/p/xruby/]XRuby[/url]项目用了,[url=http://www.jython.org/]Jython[/url]现在正在使用,[url=http://www.sapphiresteel.com/]SapphireSteel[/url]的Ruby和ActionScript IDE也用ANTLR来生成编译器前端。
[url=http://www.antlr.org]ANTLR[/url],ANother Tool for Language Recognition,是由[url=http://antlr.org/misc/contact]Terence Parr[/url]教授开发的语言工具,用于协助生成解析器、编译器、解释器等与语言处理相关的程序。它由PCCTS发展而来,特征是可以生成采用LL(*)算法的递归下降式的解析器。它所使用的语法表述格式类似于常见的EBNF(extended Backus–Naur form),易于理解和维护。
读了[url=http://neuronr.iteye.com/blog/309770]编译器构造实践[任务布置][/url]之后,大概琢磨着语法应该是像这样的吧:
Jerry.g:(ANTLR 3.1语法)
[code]grammar Jerry;
program : statementList EOF
;
statementList
: statement*
;
statement
: expressionStatement
| variableDeclaration
| blockStatement
| ifStatement
| whileStatement
| breakStatement
| readStatement
| writeStatement
;
expressionStatement
: expression SEMICOLON
;
variableDeclaration
: typeSpecifier Identifier ( LBRACK Integer RBRACK )* initializer?
( COMMA Identifier ( LBRACK Integer RBRACK )* initializer? )*
SEMICOLON
;
typeSpecifier
: INT | REAL
;
initializer
: EQ ( expression | arrayLiteral )
;
arrayLiteral
: LBRACE
( expression | arrayLiteral ) ( COMMA ( expression | arrayLiteral ) )*
RBRACE
;
blockStatement
: LBRACE statementList RBRACE
;
ifStatement
: IF LPAREN expression RPAREN statement ( ELSE statement )?
;
whileStatement
: WHILE LPAREN expression RPAREN statement
;
breakStatement
: BREAK SEMICOLON
;
readStatement
: READ variableAccess SEMICOLON
;
writeStatement
: WRITE expression SEMICOLON
;
variableAccess
: Identifier ( LBRACK Integer RBRACK )*
;
expression
: assignmentExpression
| logicalOrExpression
;
assignmentExpression
: variableAccess EQ expression
;
logicalOrExpression
: logicalAndExpression ( OROR logicalAndExpression )*
;
logicalAndExpression
: relationalExpression ( ANDAND relationalExpression )*
;
relationalExpression
: additiveExpression ( relationalOperator additiveExpression )?
| BANG relationalExpression
;
additiveExpression
: multiplicativeExpression ( additiveOperator multiplicativeExpression )*
;
multiplicativeExpression
: primaryExpression ( multiplicativeOperator primaryExpression )*
;
primaryExpression
: variableAccess
| Integer
| RealNumber
| LPAREN expression RPAREN
| SUB primaryExpression
;
relationalOperator
: LT | GT | EQEQ | LE | GE | NE
;
additiveOperator
: ADD | SUB
;
multiplicativeOperator
: MUL | DIV
;
// lexer rules
LPAREN : '('
;
RPAREN : ')'
;
LBRACK : '['
;
RBRACK : ']'
;
LBRACE : '{'
;
RBRACE : '}'
;
COMMA : ','
;
SEMICOLON
: ';'
;
ADD : '+'
;
SUB : '-'
;
MUL : '*'
;
DIV : '/'
;
EQEQ : '=='
;
NE : '!='
;
LT : '<'
;
LE : '<='
;
GT : '>'
;
GE : '>='
;
BANG : '!'
;
ANDAND : '&&'
;
OROR : '||'
;
EQ : '='
;
IF : 'if'
;
ELSE : 'else'
;
WHILE : 'while'
;
BREAK : 'break'
;
READ : 'read'
;
WRITE : 'write'
;
INT : 'int'
;
REAL : 'real'
;
Identifier
: LetterOrUnderscore ( LetterOrUnderscore | Digit )*
;
Integer : Digit+
;
RealNumber
: Digit+ '.' Digit+
;
fragment
Digit : '0'..'9'
;
fragment
LetterOrUnderscore
: Letter | '_'
;
fragment
Letter : ( 'a'..'z' | 'A'..'Z' )
;
WS : ( ' ' | '\t' | '\r' | '\n' )+ { $channel = HIDDEN; }
;
Comment
: '/*' ( options { greedy = false; } : . )* '*/' { $channel = HIDDEN; }
;
LineComment
: '//' ~('\n'|'\r')* '\r'? '\n' { $channel = HIDDEN; }
;[/code]
基本上是怎么简单怎么写弄出来的语法而已。有些地方,例如下标表达式(index expression),是可以显式写这么一条规则:
[code]indexExpression
: expression
| indexExpression '[' Integer ']'
;[/code]
然后削除直接左递归:
[code]indexExpression
: expression ( '[' Integer ']' )*
;[/code]
然后看情况再消除间接左递归。
但在Jerry语言里,能使用下标的只有数组变量,那就干脆不定义单独的下标表达式,而直接把下标的操作合并到variableAccess规则的数组变量分支里,也就是:
[code]variableAccess
: Identifier ( '[' Integer ']' )*
;[/code]
另外Jerry语言里的左值也只可能用变量访问来表示,所以也没有针对左值写特殊的规则(赋值表达式和read语句都需要用到左值的概念),直接就用variableAccess了。
这语法只是大概猜的而已。有些细节在NeuronR的帖子里没有提到,所以语法或许与他的课程实践的要求不完全一样。Anyway,我就先以这个理解为基础来平行做后续的实现了。
几个不太肯定的细节:
[color=darkred]1、if和while语句的条件表达式的类型有没有要求?[/color]
我这里是不在语法上对表达式类型做限制,到后面的语义分析的时候再检查。
[color=darkred]2、int/real与逻辑/比较运算表达式的类型(boolean)是否有隐式转换的关系?[/color]
我这里假设是“有”。如果有的话,relationalExpression那里就能偷懒;不然可能得做点麻烦些的处理……
主要是关系到那个一元布尔否定运算符('!'),C-like语言里它应该是跟一元算术求反运算符('-')的优先级一样吧?在这个Jerry语言它的优先级里却比所有算术和比较运算符要低。
看像这样的Java代码:
public class Test {
public static void main(String[] args) {
if (! 1 != 1 || 1 == 1 ) { System.out.println("true"); }
else { System.out.println("false"); }
}
}
编译的时候会有错误,因为!的优先级比!=高,而!不能作用在类型为int的1上;
但这样的Jerry代码:
if (! 1 != 1 || 1 == 1 ) write 1;
else write 0;
却应该能正确编译,且运行结果为1,因为!的优先级比!=低而比||高,所以1 != 1是false,它的反是true;然后1 == 1是true,跟前面的true做或运算也还是true。
这语法比较的诡异……
[color=darkred]3、变量声明是否一定要一并出现在同一个作用域的其它语句的前面(就像C89那样)?[/color]
我这里假设是“否”。变量可以在任何能出现语句的地方出现。作用域从声明开始,到它的包围块结束为止。
[color=darkred]4、浮点数的字面量是否要求小数点前必须有至少一位数字?浮点数字面量是否允许或要求后缀修饰(例如'r'或者'f'或者'd'之类)?[/color]
我的假设是浮点数只有一种字面量,满足这种形式:(用Perl兼容的正则表达式表示)
/\d+\.\d+/
也就是小数点前后都必须有数字,而且没有后缀。
[color=darkred]5、多维数组的语义是怎样的?[/color]
能否支持这样的声明和赋值:
[code]int array2d[2][];
int array1d[10];
array2d[0] = array1d;[/code]
这个没办法假设……只能暂时认为数组声明的时候是多少维在访问的时候也必须用多少维来访问;这样比较简单,哈哈。
[color=darkred]6、数组声明的时候,是否允许用数组字面量来初始化?[/color]
我这里假设是“是”。
[color=darkred]7、数组字面量是否允许空元素?同时,数组可否声明为零长度的?[/color]
我这里假设数组字面量不允许内容为空(跟C一样),数组也不可以声明为零长度的。
[color=darkred]8、是否有强制类型转换的表达式?(根据原帖,赋值表达式有类型转换语义)[/color]
这里假设是“否”。有C-style的强制类型转换的话语法会麻烦不少……
暂时就先这样吧。上面的语法里有经典的dangling-else问题;不过ANTLR默认是匹配优先,能自动消除这个二义性,所以就没做特别的处理。
对这样的一段代码来测试:
// line comment
// declare variables with/without initializers
int i = 1, j;
int x = i + 2 * 3 - 4 / ( 6 - - 7 );
int array[2][3] = {
{ 0, 1, 2 },
{ 3, 4, 6 }
};
/*
block comment
*/
while (i < 10) i = i + 1;
while (!x > 0 && i < 10) {
x = x - 1;
if (i < 5) break;
else read i;
}
write x - j;
可以生成这样的解析树(parse tree):
[img]http://rednaxelafx.iteye.com/upload/picture/pic/29333/9ad7ff13-c3fd-3c4c-ae1d-eac27767db90.jpg[/img]
(点击放大)
这个测试是通过ANTLRWorks的Interpreter模式来做的。编写本文的时候,最新版是1.2.2。可以在[url=http://antlr.org/download.html]这里[/url]获取。
观察此解析树:
输入进来的源码是一维的,而现在生成的解析树已经有了层次结构,变成二维的了。在得到层次结构后,原本用来标识层次结构的标点符号就变得冗余了,但这棵解析树仍然含有这些冗余的标点符号。
另外,可以观察到解析树里的许多子树中每个节点都只有一个子节点;这样的子树的中间节点都可以认为是冗余的。出现这种冗余的原因是:为了描述表达式中运算符的优先级高低,用LL语法需要为每一个优先级都写一条规则,每匹配到一个中间规则解析树里就多一个中间节点。
这些冗余对后续处理来说都是不利的。为了得到更干净更便于处理的表示,我们需要消除冗余,把解析树转换为抽象语法树(abstract syntax tree, AST)。这个时候ANTLR的重写规则(rewrite rule)就非常有用了。下一篇就来看看如何应用重写规则来得到抽象语法树。