`
RednaxelaFX
  • 浏览: 3048118 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

一个简单的语言的语法(二):ANTLR的重写规则

阅读更多
系列链接:
一个简单的语言的语法(一):用ANTLR描述语法
一个简单的语言的语法(二):ANTLR的重写规则
一个简单的语言的语法(三):做些小调整,并将生成目标换到CSharp2

上一篇我们使用ANTLR来描述了Jerry语言的基本语法,并通过ANTLRWorks来实验该语法对样本代码生成的解析树。但如同上一篇最后所述,这样得到的解析树中有太多对后续处理来说无用的冗余信息。我们需要消除这些冗余信息,得到抽象语法树(AST)。
本篇将以之前做的语法为基础,通过添加树重写规则来将ANTLR默认生成的解析树简化整理为抽象语法树。

本文涉及的源码和运行时库打包在附件里了,懒得复制粘贴的话就直接下载附件的版本,用ANTLRWorks来查看和编辑语法文件吧~

修改后的语法文件如下:
Jerry.g(ANTLR 3.1语法文件,以Java为生成目标语言)
grammar Jerry;

options {
	language = Java;
	output = AST;
	ASTLabelType = CommonTree;
}

tokens {
	// imaginary tokens
	VAR_DECL;
	SIMPLE_TYPE;
	ARRAY_TYPE;
	ARRAY_LITERAL;
	SIMPLE_VAR_ACCESS;
	ARRAY_VAR_ACCESS;
	UNARY_MINUS;
	BLOCK;
	EXPR_STMT;
}

// parser rules

program	:	statementList EOF!
		{
			System.out.println(
				null == $statementList.tree ?
				"null" :
				$statementList.tree.toStringTree());
		}
	;

statementList
	:	statement*
	;

statement
	:	expressionStatement
	|	variableDeclaration
	|	blockStatement
	|	ifStatement
	|	whileStatement
	|	breakStatement
	|	readStatement
	|	writeStatement
	;

expressionStatement
	:	expression SEMICOLON
			-> ^( EXPR_STMT expression )
	;

variableDeclaration
	:	typeSpecifier
			( Identifier
				(	-> ^( VAR_DECL ^( SIMPLE_TYPE typeSpecifier ) Identifier )
				| ( LBRACK Integer RBRACK )+
					-> ^( VAR_DECL ^( ARRAY_TYPE typeSpecifier Integer+ ) Identifier )
				| EQ expression
					-> ^( VAR_DECL ^( SIMPLE_TYPE typeSpecifier ) Identifier expression )
				| ( LBRACK Integer RBRACK )+ EQ arrayLiteral
					-> ^( VAR_DECL ^( ARRAY_TYPE typeSpecifier Integer+ ) Identifier arrayLiteral )
				)
			)
			( COMMA id=Identifier
				(	-> $variableDeclaration ^( VAR_DECL ^( SIMPLE_TYPE typeSpecifier ) $id )
				| ( LBRACK dim1+=Integer RBRACK )+
					-> $variableDeclaration ^( VAR_DECL ^( ARRAY_TYPE typeSpecifier $dim1+ ) $id )
				| EQ exp=expression
					-> $variableDeclaration ^( VAR_DECL ^( SIMPLE_TYPE typeSpecifier ) $id $exp )
				| ( LBRACK dim2+=Integer RBRACK )+ EQ al=arrayLiteral
					-> $variableDeclaration ^( VAR_DECL ^( ARRAY_TYPE typeSpecifier $dim2+ ) $id $al )
				)
				{ if (null != $dim1) $dim1.clear(); if (null != $dim2) $dim2.clear(); }
			)*
		SEMICOLON
	;

typeSpecifier
	:	INT | REAL
	;

arrayLiteral
	:	LBRACE
			arrayLiteralElement ( COMMA arrayLiteralElement )*
		RBRACE
			-> ^( ARRAY_LITERAL arrayLiteralElement+ )
	;

arrayLiteralElement
	:	expression
	|	arrayLiteral
	;

blockStatement
	:	LBRACE statementList RBRACE
			-> ^( BLOCK statementList )
	;

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
		(	-> ^( SIMPLE_VAR_ACCESS Identifier )
		| ( LBRACK Integer RBRACK )+
			-> ^( ARRAY_VAR_ACCESS Identifier Integer+ )
		)
	;

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!
	|	MINUS primaryExpression
			-> ^( UNARY_MINUS primaryExpression )
	;

relationalOperator   
	:	LT | GT | EQEQ | LE | GE | NE
	;

additiveOperator
	:	PLUS | MINUS
	;

multiplicativeOperator
	:	MUL | DIV
	;

// lexer rules

LPAREN	:	'('
	;

RPAREN	:	')'
	;

LBRACK	:	'['
	;

RBRACK	:	']'
	;

LBRACE	:	'{'
	;

RBRACE	:	'}'
	;

COMMA	:	','
	;

SEMICOLON
	:	';'
	;

PLUS	:	'+'
	;

MINUS	:	'-'
	;

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; }
	;


稍微说明一下修改点。应该观察到lexer rules部分是完全没有改变的,修改的主要是一些选项和parser rules。

首先,在文件的开头添加了一组选项:
options {
	language = Java;
	output = AST;
	ASTLabelType = CommonTree;
}

ANTLR会知道应该使用生成AST的模式,以CommonTree作为AST的节点类型,并以Java作为生成的解析器源码的语言。上一篇是在ANTLRWorks里编辑和实验语法的,这次我们需要生成实际能运行的解析器,所以需要指定这些选项(默认就是生成Java源码,不过后续文章中我应该会换用CSharp2目标。这个以后再说)。

接下来,可以看到除了原本在lexer rules里定义的实际存在的token类型之外,这次我们在语法文件的开头还增加了一组虚拟的token类型。这些token类型是为了让生成出来的抽象语法树易于解析而添加的。
例如,观察VAR_DECL这个token类型。在原本的语法中,没有任何关键字能清楚的标识出当前处理的内容是一个变量声明。为了方便后续分析,我们可以“制造”出一个虚构的token作为一个变量声明语句的根元素,然后以变量的类型、标识符和初始值为子元素。

然后就是最重要的部分,树重写规则了。有两种形式来表述树重写规则:一是直接在原本的语法规则上添加树生成用的运算符(^和!),二是在原本的语法规则后添加一个箭头("->"),并在箭头后显式指定需要生成的节点的结构。
看两个例子:
while语句。原本的语法是:
whileStatement : 'while' '(' expression ')' statement ;

这里我们想让生成出来的子树以'while'为根节点,以expression和statement为子节点。
可以直接在该语法上添加树生成运算符:在某个元素后加上帽子符号('^')来表示它是生成的子树的根节点,在某个元素后加上叹号('!')来表示生成的子树中应该忽略该元素。于是修改得到的语法是:
whileStatement : 'while'^ '('! expression ')'! statement ;

也可以显式指定树重写规则。一棵子树用这种方式来表示:
^( root element1 element2 ... )

这里我们要的就是:
whileStatement : 'while' '(' expression ')' statement
    -> ^( 'while' expression statement )
  ;

这种形式我们能一目了然看到最终生成的子树的结构。
两种形式是等价的,可以根据具体情况来选择能简单而清晰的表示出树改写规则的版本。

对表达式相关的语法规则,我们几乎都是用添加运算符的形式来表示树改写规则,因为对左结合的双目运算符,这样是最简洁的。
ANTLR生成的解析器使用LL(*)算法;与一般的LL解析器一样,ANTLR不支持左递归的语法规则。这使得书写左结合的双目运算符时,一般得写成这样的形式:
exprWithHigherPrecedence
  : exprWithLowerPrecedence ( op exprWithLowerPrecedence )*
  ;

而不能以左递归来指定左结合。(但右结合还是可以用右递归来指定的。)
那么在表示树改写规则的时候,使用运算符来修饰语法就是这样:
exprWithHigherPrecedence
  : exprWithLowerPrecedence ( op^ exprWithLowerPrecedence )*
  ;

只是在op的后面添加了一个帽子符号('^'),表明在没有匹配到op运算符时就直接返回exprWithLowerPrecedence规则所生成的树;而如果匹配到了op运算符,则每匹配到一次就生成一个新的以op为根节点的、前后两个较低优先级的表达式节点为子节点的树。
这个树改写规则如果要显式指定,就得写成:
exprWithHigherPrecedence
  : exprWithLowerPrecedence
      ( op exp=exprWithLowerPrecedence
          -> ^( op $exprWithHigherPrecedence $exp )
      )*
  ;

后者相比之下麻烦多了,所以一般都会使用前者。

可惜C风格的变量声明语句的语法很麻烦,结果variableDeclaration在修改后膨胀了好多 T T
最不爽的地方就是C风格的数组变量声明是把数组的维度写在变量名后面的。这就使得语句开头的类型(例如int、char等)可能只是变量的实际类型的一部分,而另一部分要在变量名的之前(例如表示指针的星号('*'))或之后(例如表示数组的方括号('[' ']'))。
就不能把整个类型写在一起么……T T 于是衍生出来的Java和C#明显都吸取了这个教训。

在语法的program规则中,我们添加了一条嵌入语法动作,让生成的解析器在匹配完program规则后将其对应的抽象语法树以字符串的形式输出出来。

如果是在ANTLRWorks里编辑该语法文件,可以在菜单里选择Generate -> Generate Code来生成出解析器的源码。这里例子中我们会得到JerryLexer.java和JerryParser.java。
要运行这个解析器,还需要写一个简单的启动程序来调用生成出来的JerryLexer和JerryParser。源码如下:
TestJerry.java
import org.antlr.runtime.*;

public class TestJerry {
    public static void main(String[] args) throws Exception {
        // Create an input character stream from standard in
        ANTLRInputStream input = new ANTLRInputStream(System.in);
        // Create an JerryLexer that feeds from that stream
        JerryLexer lexer = new JerryLexer(input);
        // Create a stream of tokens fed by the lexer
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        // Create a parser that feeds off the token stream
        JerryParser parser = new JerryParser(tokens);
        // Begin parsing at rule prog
        parser.program();
    }
}

它指定从标准输入流得到要解析的Jerry代码,然后通过JerryLexer将代码解析成token流,再将token流交给JerryParser进行句法分析。

将JerryLexer.java、JerryParser.java和TestJerry.java放在跟ANTLRWorks同一目录下,然后编译它们:
引用
javac -Xlint:unchecked -cp antlrworks-1.2.2.jar JerryLexer.java JerryParser.java TestJerry.java

(因为ANTLRWorks里含有ANTLR的运行时库,而我正好又是用ANTLRWorks来编辑语法文件的,所以直接用ANTLRWorks的JAR包放在classpath里来得到需要的ANTLR运行时类。实际开发的话可以从ANTLR官网获得只含有ANTLR运行时库的JAR包并在编译和运行的时候将其添加到classpath里。)

上一篇的最后有这样的一段Jerry例子:
// 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;

(语法是符合要求的,至于代码的意义就别追究了,只是用来演示各种语法结构随便写的)

用本篇的ANTLR语法文件生成的解析器,我们可以解析这个例子,得到对应的抽象语法树的字符串表示。表示方法是:
(root element1 element2 ...)

跟LISP的S-expression非常类似。

于是执行测试程序。将要解析的代码保存到JerrySample.txt中,然后执行下面的命令:
引用
java -cp ".;antlrworks-1.2.2.jar" TestJerry < JerrySample.txt

得到输出:
(VAR_DECL (SIMPLE_TYPE int) i 1) (VAR_DECL (SIMPLE_TYPE int) j) (VAR_DECL (SIMPLE_TYPE int) x (- (+ (SIMPLE_VAR_ACCESS i) (* 2 3)) (/ 4 (- 6 (UNARY_MINUS 7))))) (VAR_DECL (ARRAY_TYPE int 2 3) array (ARRAY_LITERAL (ARRAY_LITERAL 0 1 2) (ARRAY_LITERAL 3 4 6))) (while (< (SIMPLE_VAR_ACCESS i) 10) (= (SIMPLE_VAR_ACCESS i) (+ (SIMPLE_VAR_ACCESS i) 1))) (while (&& (! (> (SIMPLE_VAR_ACCESS x) 0)) (< (SIMPLE_VAR_ACCESS i) 10)) (BLOCK (= (SIMPLE_VAR_ACCESS x) (- (SIMPLE_VAR_ACCESS x) 1)) (if (< (SIMPLE_VAR_ACCESS i) 5) break (read (SIMPLE_VAR_ACCESS i))))) (write (- (SIMPLE_VAR_ACCESS x) (SIMPLE_VAR_ACCESS j)))

这样太乱了看不清楚。将其格式稍微整理一下得到:
(VAR_DECL
  (SIMPLE_TYPE int)
  i
  1
)
(VAR_DECL
  (SIMPLE_TYPE int)
  j
)
(VAR_DECL
  (SIMPLE_TYPE int)
  x
  (-
    (+ (SIMPLE_VAR_ACCESS i) (* 2 3))
    (/ 4 (- 6 (UNARY_MINUS 7)))
  )
)
(VAR_DECL
  (ARRAY_TYPE
    int
    2
    3
  )
  array
  (ARRAY_LITERAL
    (ARRAY_LITERAL 0 1 2)
    (ARRAY_LITERAL 3 4 6)
  )
)

(while
  (< (SIMPLE_VAR_ACCESS i) 10)
  (= (SIMPLE_VAR_ACCESS i) (+ (SIMPLE_VAR_ACCESS i) 1))
)
(while
  (&&
    (! (> (SIMPLE_VAR_ACCESS x) 0))
    (< (SIMPLE_VAR_ACCESS i) 10)
  )
  (BLOCK
    (= (SIMPLE_VAR_ACCESS x) (- (SIMPLE_VAR_ACCESS x) 1))
    (if
      (< (SIMPLE_VAR_ACCESS i) 5)
      break
      (read (SIMPLE_VAR_ACCESS i))
    )
  )
)
(write
  (- (SIMPLE_VAR_ACCESS x) (SIMPLE_VAR_ACCESS j)))

可以跟原本的代码对比一下,看看是否保持了原本的结构。

得到这棵抽象语法树之后,接下来就可以对树来做匹配和分析了。由于树本身已经有了结构,下面就可以用更干净的描述方式来表述我们要对树做的处理。下一篇就来看看ANTLR的tree grammar在这里的应用。
7
0
分享到:
评论

相关推荐

    antlr-2.7.7.jar

    1. **语法定义**:用户使用ANTLR提供的语法描述语言(通常称为ANTLR语法规则)编写一个输入语言的语法规则文件(例如,ANTLR的.g文件)。 2. **生成解析器和词法分析器**:ANTLR读取这个语法规则文件,然后自动生成...

    tiny-language-antlr4:ANTLR 4的小语言

    在我不久前写的,我演示了如何使用 3创建一种称为Tiny Language的小型动态类型编程语言。 但是,ANTLR 4现在是流行的解析器生成器的更精简(更原始)的版本。 由于从v3到v4的变化很大,因此使用ANTLR 4使Tiny ...

    example-antlr:Antlr示例

    1. **定义语法规则**:首先,我们需要使用ANTLR提供的语法描述语言来编写一个语法文件,例如`ExampleGrammar.g4`。这个文件定义了语言的结构,包括关键字、符号、表达式规则等。 2. **生成解析器和词法分析器**:...

    antlr4-short-course

    - 算术表达式语言的构建:通过ANTLR构建一个能够解析和计算算术表达式的简单语言。 - 语法设计:了解常用的语法模式,例如优先级、左递归以及相关性。 - 常用词法结构:掌握如何使用词法分析器识别文本中的各种记号...

    java-antrl-example.rar_ANTLR JAVA_Antlr Rewrite_ant_antlr_antrl

    ANTLR(ANother Tool for Language Recognition)是一个强大的解析器生成器,用于读取、处理、执行或翻译结构化文本或二进制文件。ANTLR被广泛应用于Java、C#、Python、JavaScript等多种编程语言,用于构建词法分析...

    剥离的Parser模块,用于查看Spark SQL语法解析SQL后生成的语法树

    ANTLR4是一个强大的解析器生成器,广泛应用于各种语言的解析,包括SQL。在"ANTLR4-SqlBase-master"这个压缩包中,包含了使用ANTLR4工具生成的SqlBase解析器的相关代码,SqlBase是Apache Calcite项目的一个子项目,...

    hive词法语法分析草稿0.3

    ANTLR是一个强大的解析器生成器,常用于处理语言和协议的解析任务。 在Hive的源码分析中,第一章前言部分通常会概述分析的目的和背景,为后续章节提供基础。准备工作可能包括安装和配置ANTLR工具,理解Hive的基本...

    nginx-java-parser:基于ANTLR4语法的Nginx配置解析器

    特征使用ANTLR4解析功能将配置文件转换为AST树JavaCC同样可用(不建议使用) 重建配置文件并将其转储回* .conf 嵌套块支持如果语句支持位置/重写/如果语句支持内未引用的正则表达式评论支持安装将以下依赖项添加到您...

    antlrv4_groovy_grammar:Google Summer of Code 项目

    我想用 Antlr v4 术语重写 Groovy 的语法并编写一个解析器,它使用生成的一个构造 Groovy 的抽象语法树。 作为这项工作的一部分,我计划改进 Groovy Console AST 浏览器,因为它可以在研究阶段帮助我很多。 我还...

    NUAA南航 形式语言与自动机编程作业 上下文无关文法消除二义性.zip

    在这个领域的学习中,上下文无关文法(Context-Free Grammar,CFG)是一个核心概念,它被广泛用于编程语言的语法分析。本编程作业的主题是“消除上下文无关文法的二义性”,这是一项关键任务,因为二义性可能导致...

    iro4cli:语法生成器Iro的开源重写,支持自动VSCode和Atom扩展生成

    这是一个易于使用的命令行工具,它将捆绑提供的Iro语法并从中创建各种不同的语法目标,自动生成扩展以上传到市场。 一些可用的目标包括: 文字伴侣 原子 高手 皮格 胭脂 崇高3 CSS 要同时创建一个Atom包和VSCode...

    snowstar:这是Snow *编程语言的代码,目前正在重写中

    总之,Snow*编程语言的重写项目是一个涵盖编译原理、语言设计、解析技术、性能优化等多个领域的复杂工程。这个过程不仅是技术上的挑战,也是对社区协作和项目管理能力的考验。随着项目的进展,我们期待看到Snow*编程...

    java8源码-java_antlr4:java_antlr4

    本程序是在语法分析阶段处理的程序,通过使用提供的Java8文法,生成Java8语言的语法分析器,并通过Antlr4生成的模板,重写其监听器实现上述功能。 目前实现功能 代码格式化规范以为准 代码格式化 换行规范 运算符...

    OGNL语言指南

    - **发展**: 最初命名为KVCL(Key-Value Coding Language),后来经过Luke Blanshard使用ANTLR重写以及名称更改,最终形成了现在的OGNL。 - **维护**: 经过几次重写,包括使用JavaCC进行的重写,后续维护工作由Drew ...

    sql-parser-master.zip_Parser_SQL parser

    这通常通过解析树(也叫语法树或抽象语法树AST)的形式来完成,其中每个节点代表一个SQL语句的一部分。如果输入的SQL语句不符合语法规则,解析器就会抛出错误。 3. **语义分析**:在构建了语法树之后,解析器会进行...

    java SQL查询分析器开发

    在IT行业中,SQL查询分析器是一个非常重要的工具,它主要用于解析和优化SQL语句,以提高数据库系统的性能。本文将详细探讨如何开发一个基于Java的SQL查询分析器,结合项目实践,深入理解其核心概念和技术。 首先,...

    OGNL中文参考手册.pdf

    - Luke使用ANTLR重写了该语言,并在Drew的建议下改名为现在的OGNL。 - 后续Luke使用JavaCC进行了重写,并由Drew负责后续的代码维护工作。 #### 四、OGNL语法基础 - **基本单位**:OGNL表达式的最基本单位被称为...

    查询分析器(C#源码)

    C#中可以实现一个简单的基于规则的优化器,或者利用第三方库,如Microsoft.EntityFrameworkCore,它内置了更复杂的查询优化。 5. **执行计划的生成与执行**:最后,将优化后的执行计划转化为数据库操作,如SQL命令...

Global site tag (gtag.js) - Google Analytics