编译原理
概念
BNF范式
自然语言存在不同程度的二义性。这种模糊、不确定的方式无法精确定义一门程序设计语言。必须设计一种准确无误地描述程序设计语言的语法结构,这种严谨、简洁、易读的形式规则描述的语言结构模型称为文法。
最著名的文法描述形式是由Backus定义Algol60语言时提出的Backus-Naur范式(Backus-Naur Form, BNF)及其扩展形式EBNF。
BNF能以一种简洁、灵活的方式描述语言的语法。
- BNF
BNF范式是一种用递归的思想来表述计算机语言符号集的定义规范
书写规范(生产式规则),形式如下:symbol := alternative1 | alternative2 ...
每条规则申明:=左侧的符号必须被右侧的某一个可选项代替。替换项用“|”分割(有时用“::=”替换“:=”,但意思是一样的)。替换项通常有符号和终结符构成。
BNF语法的另一个变化是把终结符放在引号中,把他们与符号区别开来。
下面是BNF语法的一个实例:
S := '-' FN | FN
FN := DL | DL '.' DL
DL := D | D DL
D := '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
这里的各种符号都是缩写的: S是开始符,FN产生一个分数,DL是一串数字列表,D是各个数字。
该语法描述的语言的有效句子都是数字,可能是分数,也可能是负数。
- EBNF 在DL中,我们已经用到了递归(如DL能产生新的DL)来表达许多数字D的情况。这有点不太灵活,使BNF难以阅读。扩展BNF(EBNF)通过引入下列操作符解决了这个问题:
- l ?:意思是操作符左边的符号(或括号中的一组符号)是可选项(可以出现0到多次)。
- l *:是指可以重复多次。
- l +:是指可以出现多次。
一个EBNF语法实例 上面的例子用EBNF可以写成:
S := '-'? D+ ('.' D+)?
D := '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
提示:EBNF在定义语言方面并不比BNF更强大,只是更方便。凡是用EBNF写的东西都可以转换成BNF的形式。
符号表、符号串、推导式、句子和语法树
- 文法 下面例子中用文法定义了人类语言的语法规则:
语言 =>(句子)+
句子 => 主语 谓语
谓语 => 动词 宾语
主语 => 名词
宾语 => 名词
名词 =>‘张三’| ‘代码’
动词 =>‘编写’
如,‘张三编写代码’这句话在文法中的推导过程是:
语言 => 主语 谓语
=> 张三 动词 宾语
=> 张三 编写 名词
=> 张三 编写 代码
另外编译原理中一般用大写字母表示一个文法的名称,再加上文法的启始规则组成文法的表示符号。如上面的文法如果名称为G,可以表示为G[语言]文法。
-
符号表 组成语言的基本符号加上推导出基本符号的抽象符号集合在一起称为符号表,用V来表示,符号表是不允许为空的。
如G[语言]文法的符号表是:{语言,句子,主语,谓语,宾语,名词,动词,‘张三’,‘代码’,‘编写’} -
非终结符、终结符 符号表中可以继续推导的中间符号称为非终结符,用Vn表示
不能再继续推导的符号称为终结符,用Vt表示。
G[语言]文法的非终结符集合为:{语言,句子,主语,谓语,宾语,名词,动词},终结符集合为{‘张三’,‘代码’,‘编写’}。 -
符号串 符号表中符号的任意有穷组合序列称为符号串。‘张三张三’、‘张三代码编写’、‘张三语言句子宾语宾语’都是G[语言]文法符号串。
很明显一种文法的符号串不一定是这种文法的合法句子。 -
推导式 文法是定义语法规则的工具,语法规则简称规则(rule)又称推导式或产生式。
假设a和b都是一个文法的符号串,我们用a => b表示一个规则,其中a不能为空。一个文法要由至少要有一个规则。 规则a => b使用b来替换a的过程叫做推导,反用b来替换a的过程叫归约。 -
句子 G[S]是一个文法,S为启始规则,从S推导若干次后形成的符号串叫做G[S]文法的句型。
如果推导出的符号串全都由终结符组成此符号串叫做G[S]的句子。
前面示例中“张三 动词 宾语”是G[语言]文法的句型,而“张三 编写 代码”是G[语言]文法的句子。
编译原理中也使用四元组来表示文法G[Vn,Vt,P,S],其中G为文法句称,Vn为非终结符的集合,Vt为终结符的集合,P是文法规则的集合,S为启始规则。 -
解析树 编译技术中用解析树来更直观的表示一个句型的推导过程。
给定上下文无关文法G[S],它的解析树的每一个节点都有一个G[S]文法的符号与之对应。S为解析树的根节点。
如果一个节点有子节点,则这个节点对应的符号一定是非终结符。
如果一个节点对应的符号为A,它的子节点对应的符号分别为A1,A2,A3…..Ak,那么G[S]文法中一定有一个规则为:A=>A1 A2 A3 …..Ak。
满足这些规定的语法树也叫推导树。 -
语法树 抽象语法树的形式便于进一步操作。它以一种对理解程序含义的人有意义的方式表示事物,而不仅仅是理解程序的编写方式。
原理
编译过程
编译过程一般可分为五个阶段:
-
词法分析 从左向右逐行扫描源程序的字符,识别出各个单词,确定单词的类型。 将识别出的单词转换成统一的机内表示——词法单元(token)形式
token:< 种别码,属性值 > -
语法分析 语法分析器(parser)从词法分析器输出的token序列中 识别出各类短语,并构造语法分析树(parse tree)
语法分析树描述了句子的语法结构
赋值语句经语法分析生成语法分析树
识别单词
有穷自动机 ( Finite Automata,FA )由两位神经物理学家 MeCuloch和Pitts于1948年首先提出,是对一类处理系统建立的数学模型
- 这类系统具有一系列离散的输入输出信息和有穷数目的 内部状态(状态:概括了对过去输入信息处理的状况)
- 系统只需要根据当前所处的状态和当前面临的输入信息 就可以决定系统的后继行为。每当系统处理了当前的输 入后,系统的内部状态也将发生改变
FA的表示
转换图 (Transition Graph)
- 结点:FA的状态
- 初始状态(开始状态):只有一个,由start箭头指向
- 终止状态(接收状态):可以有多个,用双圈表示
- 带标记的有向边:如果对于输入a,存在一个从状态p到状 态q的转换,就在p、q之间画一条有向边,并标记上a
最长子串匹配原则(Longest String Matching Principle): 当输入串的多个前缀与一个或多个模式匹配时, 总是选择最长的前缀进行匹配
FA的分类
- 确c的FA (Deterministic finite automata, DFA)
M = ( S,Σ ,δ,s0,F )
S: 有穷状态集
Σ: 输入字母表,即输入符号集合。
δ: 将S×Σ映射到S的转换函数。
s∈S, a∈Σ, δ(s,a)表示 从状态s出发,沿着标记为a的边所能到达的状态。
s0:开始状态 (或初始状态),s0∈ S
F: 接收状态(或终止状态)集合,F⊆ S
DFA的算法实现
- 输入:以文件结束符eof结尾的字符串x。DFA D的开始状态s0,接收状态集F,转换函数move。
- 输出:如果D接收 x,则回答“yes”,否则回答“no”。
- 方法:将下述算法应用于输入串 x。
s = s0 ;
c = nextChar();
while(c! = eof ){
s = move ( s , c ) ;
c = nextChar ( ) ;
}
if (s在F中) return“yes”;
else return “no”;
- 非确定的FA (Nondeterministic finite automata, NFA)
识别单词的DFA
r = (a|b)*abb
自顶向下的分析(Top-Down Parsing)
- 从分析树的顶部(根节点)向底部(叶节点)方向构造分析树
- 可以看成是从文法开始符号S推导出词串w的过程
每一步推导中,都需要做两个选择
-
替换当前句型中的哪个非终结符
-
用该非终结符的哪个候选式进行替换 由此,推导分为最左推导和最右推导
-
最左推导(Left-most Derivation)
- 在最左推导中,总是选择每个句型的最左非终结符进行替换
- 在最左推导中,总是选择每个句型的最左非终结符进行替换
-
最右推导(Right-most Derivation)
- 在最右推导中,总是选择每个句型的最右非终结符进行替换
自顶向下的语法分析采用最左推导方式
- 总是选择每个句型的最左非终结符进行替换
- 根据输入流中的下一个终结符,选择最左非终结 符的一个候选式
递归下降分析 (Recursive-Descent Parsing)
- 由一组过程组成,每个过程对应一个非终结符
- 从文法开始符号S对应的过程开始,其中递归调用文法中其它非终结符对应的过程。如果S对应的过程体恰好扫描了整个输入串,则成功完成语法分析
void A( ) {
选择一个A产生式, A →X1 X2 ... Xk ;
for ( i = 1 to k ) {
if ( Xi是一个非终结符号)
调用过程 Xi ( ) ;
else if ( Xi 等于当前的输入符号a)
读入下一个输入符号;
else /* 发生了一个错误 */ ;
} }
可能需要回溯(backtracking), 导致效率较低 左递归文法会使递归下降分析器陷入无限循环
-
左递归 形如A => Ab的规则,A的定义是递归的可以推导出Abbbb…b,左侧的非终结符A可以不断地推导出Ab,这种处于规则左侧的递归叫左递归。递归也可能出现在多个非终结符之间A=>Bd,B=>Bc这里的A=>Bd也是左递归。
例如我们要定义一个整型数其规则为:INT => INT Digital,Digital => 0|1|2|3|4|5|6|7|8|9,规则INT用左递归实现了多位整型数的定义。 -
右递归 相反形如A => bA的规则,A的定义也是递归的但和左递归相反非终结符A在规则的右侧这样递归叫做右递归。
我们可以把整型数定义的规则用右递归的方法定义为INT => Digital INT,Digital => 0|1|2|3|4|5|6|7|8|9。使用这两种递归的方法时,要看语法分析程序的分析方式,如果语法分析
程序是从左向右分析的,那么使用右递归比较适合,反之使用左递归比较适合。
预测分析
- 预测分析是递归下降分析技术的一个特例,通过在输入中向前看固定个数(通常是一个)符号来选择正确的A-产生式。
- 可以对某些文法构造出向前看k个输入符号的预测分析器,该类文法有时也称为LL(k) 文法类
- 预测分析不需要回溯,是一种确定的自顶向下分析方法
自底向上的分析 (LR)
- 从分析树的底部(叶节点)向顶部(根节点)方向构造分析树
- 可以看成是将输入串w归约为文法开始符号S的过程
LR 分析法
- L: 对输入进行从左到右的扫描
- R: 反向构造出一个最右推导序列
LR(k)分析
- 需要向前查看k个输入符号的LR分析
LR可以解析的语法形式更多,LL的语法定义更简单易懂。
antlr
ANTLR 是一种自动生成词法分析器、语法分析器的工具。利用它可以不用关注词法分析和语法分析的细节,只需要描述语言的文法,将其作为输入传递给ANTLR,便可以自动生成词法分析器和语法分析器。
ANTLR 由 Java 语言写成,但是可以支持其它的目标语言,可以生成 C++、JavaScript 等语言的词法分析和语法分析程序。
antlr3 vs antlr4
- 引入访问者、监听器模式,使解析与应用代码分离;
- 改进LL()算法,使用新的Adative LL()算法,在运行时动态分析语法,而LL()需要静态分析语法,考虑各种语法的可能性。
- v4支持直接左递归 允许左递归可以极大的简化语法文件,比如使用支持左递归的ANTLR 4重写的Java语法只有91行,而之前的版本则有172行。
- 语法差别,比如v3的tokens以分号分隔,v4版本中使用逗号分隔;比如skip方法在v4中->skip使用,v3中{skip();}使用
- 性能
原理
工作流程
语言识别器(ANTLR 库)在执行的过程中,输入用户的 DSL 字符串,然后经过词法分析,分割成独立的 token 单元,然后经过语法解析器生成解析树。解析树上包含了文法规则以及词法单元。
生成的分析器代码
grammar E;
options { output=AST; }
expression : multExpr (('+' |'-' ) multExpr)*;
multExpr : atom (('*' | '/') atom)*;
atom : INT | '(' expression ')';
INT : '0'..'9' + ;
WS : (' ' |'\t' |'\n' |'\r' )+ {skip();};
下面是此文法生成的代码,我们只给出到 expression 规则为止的部分代码:
public class EParser extends Parser {
......
protected TreeAdaptor adaptor = new CommonTreeAdaptor();
public void setTreeAdaptor(TreeAdaptor adaptor) {
this.adaptor = adaptor;
}
public TreeAdaptor getTreeAdaptor() { return adaptor; }
public static class expression_return extends ParserRuleReturnScope {
Object tree;
public Object getTree() { return tree; }
};
public final expression_return expression() throws RecognitionException {
expression_return retval = new expression_return();
Object root_0 = null;
Token set2=null;
multExpr_return multExpr1 = null; multExpr_return multExpr3 = null;
Object set2_tree=null;
try {
root_0 = (Object)adaptor.nil();
......
multExpr1=multExpr();
adaptor.addChild(root_0, multExpr1.getTree());
do {
......
switch (alt1) {
case 1 :
{
......
set2=(Token)input.LT(1);
adaptor.addChild(root_0, adaptor.create(set2));
......
multExpr3=multExpr();
adaptor.addChild(root_0, multExpr3.getTree());
}
break;
}
} while (true);
retval.tree = (Object)adaptor.rulePostProcessing(root_0);
adaptor.setTokenBoundaries(retval.tree, retval.start, retval.stop);
}
......
return retval;
}
}
}
ouput=AST 只影响语法分析类代码,在语法分析类 EParser.java 中增加了有关生成语法树的代码。
- 首先增加了一个 CommonTreeAdaptor 类型的成员 adaptor。
public class CommonTreeAdaptor : BaseTreeAdaptorCommonTreeAdaptor 类是 ANTLR 中默认的对语法树适配器实现类,它从 BaseTreeAdaptor 抽象类继承。
adaptor 对象作为EParser的成员使语法分析类具有语法树适配器的功能。接下来 setTreeAdaptor 方法用于向语法分析器设置自定义的语法树适配器,我们可以自定义自己的适配器来替换默认的适配器实现一些自定义的语法树操作。 getTreeAdaptor方法用来获得语法分析器的语法树适配器。
public static class expression_return extends ParserRuleReturnScope {
Object tree;
public Object getTree() { return tree; }
};
-
语法分析器类中定义了 expression_return 类,是用来返回规则的返回值。如果加入 output=AST 的设置后所有的语法分析器中的规则函数都会返回 XXX_return 类,目的是返回相应规则的语法树。
在expression()规则函数中multExpr (('+' |'-' ) multExpr)*规则部分分别对应着multExpr1=multExpr()、set2=(Token)input.LT(1)和multExpr3=multExpr()语句。
文法中第一个 multExpr 规则对应 multExpr1=multExpr()代码,multExpr()规则函数返回的是 multExpr_return 类的实例,multExpr_return 类与 expression_return 类是 相同的概念。
set2=(Token)input.LT(1)中利用 LT 方法获取当前的一个 Token 对象。它对应的应该是“+”或“-”号,词法规则用 Token 类来表示。 -
前面讲到了规则是语法树的模板,ANTLR 在每一个在规则函数中都会生成与本规则相同的语法树,做为启始规则的 expression()返回的语法树就是整个输入生成的语法树,它是由 multExpr1=multExpr()、set2=(Token)input.LT(1)和 multExpr3=multExpr() 这三个语法树组成的,每一个规则生成的语法树都是由下一级规则函数生成的语法树组成 的,就这样一级一级从下向上生成了一棵完整的语法树。下面我们看一下 expression()函 数中如何将 multExpr1、multExpr3 和 set2 组成语法树。
root_0 = (Object)adaptor.nil();
语法树适配器 adaptor 调用 nil 方法生成一个空的树节点作为根节点。在没有对语法树的结构进行指定时,ANTLR 使用一个空节点作为语法树的根节点。
adaptor.addChild(root_0, multExpr1.getTree());
语法树适配器 adaptor 调用 addChild 方法将 multExpr1 的语法树加到根节点中, addChild 方法有两个参数第一个参数是根节点,第二个参数是子树的根节点。multExpr3 的语法树也是用相同的方法加到 root_0 根节点的这里不在重叙。对词法规则的 Set2 的操作与 multExpr1 和 multExpr3 有 些 不 同 , adaptor.addChild(root_0, adaptor.create(set2));中 addChild 的第二个参数使用了 create 方法。create 方法可以 根据 Token 对象生成语法树节点类型 BaseTree。这样就可以将词法规则信息也加入到语法树中。
usage
语法结构
所有的语法是这种格式:
/** This is a grammar doc comment */
grammar-type grammar name;
options { name1 = value; name2 = value2; ... }
import delegateName1=grammar1, ..., delegateNameN=grammarN; // can omit delegateName
tokens { token-name1; token-name2 = value; ... }
scope global-scope-name-1 { «attribute-definitions» }
scope global-scope-name-2 { «attribute-definitions» }
...
@header { ... }
@lexer::header { ... }
@members { ... }
«rules»
一般如果语法非常复杂,会基于Lexer和Parser写到两个不同的文件中
文件命名必须和grammar命名相同,如 grammar T,文件名必须命名为T.g4.
options,imports,token,action的声明顺序没有要求, 但一个文件中options,imports,token最多只能声明一次.
grammar是必须声明的,同时必须至少声明一条规则(rule),其余的部分都是可选的.
-
grammar grammar 名称和文件名要一致
- Lexer定义词法分析规则;
- Parser 定义语法分析规则;
- Tree用于遍历语法分析树;
- Combine既可以定义语法分析规则,也可定义词法分析规则,规则名称遵循上述规则;
-
options
- language: 代码生成的目标语言,默认java
- tokenVocab: Antlr可以找到预定义的token和token类型的地方。
- output: 解析生成返回的结果,可选项:AST、template
- ASTLabelType:设置所有tree标签和tree值表达式的类型。默认Object。
- superClass:设置识别器的父类,java默认是org.antlr.runtime.Parser
- k: 指定子规则用到的向前看的精确深度
- greedy: 贪婪模式是在保证整体匹配成功的情况下,尽可能多地匹配,非贪婪模也是在保证整体匹配成功的情况下尽可能少的匹配,默认是true
- backtrack: 回溯, 默认false
-
import 使用Import语法规则分类,可以使语法规则更加清晰;并且可以采用面向对象的思想设计规则文件,使其具有多态及继承的思想。值得注意的是,当前规则的优先级高于导入规则。
-
tokens tokens区域是一些会被合并到整体词法符号集合中的词法符号定义
-
scope 属性作用域是一个按以下格式定义的属性的集合:
scope name {
type1 attribute-name1;
type2 attribute-name2;
}
默认的scope-name是语法解析器的名字,对于语法实例 @header 和 @parser::header是一样的
合法的scope-name因目标代码而不同,但大多数的目标代码应该支持语法解析器和词法解析器。两个公共的action-name是header和members。
- Action
主要有@header 和@members,用来定义一些需要生成到目标代码中的行为
- @header在生成的目标代码中的类定义之前注入代码
- @members在生成的目标代码中的类定义里注入代码(例如类的属性和方法) 例如,可以通过@header设置生成的代码的package信息,@members可以定义额外的一些变量到Antlr4语法文件中;
词法规则
词法规则必须以大写字母开头
WS : (' '|'\r'|'\t'|'\n') {$channel=HIDDEN;}
;
COMMENT : '/*' . * '*/' {skip();} ;
//------ Identifiers
ID : ID_LETTER (ID_LETTER | DIGIT)* ;
fragment ID_LETTER : 'a'..'z' | 'A'..'Z' | '_' ;
fragment DIGIT : '0'..'9';
- 终结符定义方法
LETTER : ‘A’| ‘B’| ‘C’| ‘D’| ‘E’| ‘F’| ‘G’| ‘H’| ‘I’| ‘J’| ‘K’| ‘L’| ‘M’| ‘N’| ‘O’| ‘P’| ‘Q’| ‘R’| ‘S’| ‘T’| ‘U’| ‘V’| ‘W’| ‘X’| ‘Y’| ‘Z’| ‘a’| ‘b’| ‘c’| ‘d’| ‘e’| ‘f’| ‘g’| ‘h’| ‘i’| ‘j’| ‘k’| ‘l’| ‘m’| ‘n’| ‘o’| ‘p’| ‘q’| ‘r’| ‘s’| ‘t’| ‘u’| ‘v’| ‘w’| ‘x’| ‘y’| ‘z’;
-
“..”符号 “..”符号,从上面的 LETTER 示例可以看出,定义一个表示英文字母的符号写起来非常繁琐。为了使定义变得简单 ANTLR 加入“..”符号通过指定首尾的字符可以很方便的定义 一定范围内的字符。
LETTER : ‘A’ .. ‘Z’ | ‘a’ .. ‘z’; -
“
”符号 “”符号,如果我们想表示除某些符号以外的符号时,可以使用“”符号。“”代表取反的意思。A : ~ ‘B’;符号 A 匹配除字符“B”以外的所有字符。A : ~ (‘A’ | ‘B’); B : ~(‘A’ .. ‘B’); C : ~‘\u00FF';这个的例子中定义三个符号。符号 A 匹配除字符“A”和“B”以外的所有字符,符号 B 匹 配除大写字符母以外的所有字符。符号 C 匹配除编码为“u00FF”的字符以外的所有字符。 -
“.”符号 “.”符号,ANTLR 中可以用“.”表示单个任意字符,起通配符的作用。
A : .; B : .*; C : .* ‘C’; D : ~ .;//error这个例子中符号 A 匹配一个任意字符,符号 B 符号匹配 0 到多个任意字符,符号 C 匹 配 0 到多个任意字符直到遇到字符“C”为止。D 的定义是错误的,不能定义任意字符以外的 字符。 -
channel 很多信息,例如注释、空格等,是结果信息生成不需要处理的,但是我们又不适合直接丢弃,安全地忽略掉注释和空格的方法是把这些发送给语法分析器的记号放到一个“隐藏通道”中,语法分析器仅需要调协到单个通道即可。
下面是运行代码,运行代码中加入了 tokenStream.SetTokenTypeChannel 方法。
TestSkipLexer lex = new TestSkipLexer(new ANTLRFileStream("f1.txt"));
CommonTokenStream tokenStream = new CommonTokenStream(lex);
TestSkipParser parser = new TestSkipParser(tokenStream);
tokenStream.SetTokenTypeChannel(TestSkipLexer.COMMENT, Token.DEFAULT_CHANNEL);
tokenStream.SetTokenTypeChannel(TestSkipLexer.LINE_COMMENT, Token.DEFAULT_CHANNEL);
TestSkipParser.a_return bReturn = parser.a();
SetTokenTypeChannel 方法是将指定的频道中的符号加入到分析程序关心的范围内 , 第一个参数是词法符号,第二个参数是这个符号所处频道。两条 SetTokenTypeChannel 语句将 DEFAULT_CHANNEL 频道中的 COMMENT、LINE_COMMENT 加入到语法分析程序。分析程序会分析注释的内容并将其加入到了语法树中。
- fragment 在词法规则中那些不会被语法规则直接调用的词法规则可以用一个fragment关键字来标识,fragment标识的规则只能为其它词法规则提供基础。
- skip 有些字符是不属于源程序范畴内的,这些字符在分析过程中应该忽略掉。在ANTLR中可以在词法定义中加入skip();。在规则的定义的之后与表示定义结束的分号之前加入“{skip();}”。例如
文法规则
文法规则必须以小写字母开头
/** rule comment */
access-modifier rule-name[«arguments»] returns [«return-values»] throws name1, name2, ...
options {...}
scope {...}
scope global-scope-name, ..., global-scope-nameN;
@init {...}
@after {...}
: «alternative-1» -> «rewrite-rule-1»
| «alternative-2» -> «rewrite-rule-2»
...
| «alternative-n» -> «rewrite-rule-n»
;
catch [«exception-arg-1»] {...}
catch [«exception-arg-2»] {...}
finally {...}
所有规则中若有冲突,先出现的规则优先匹配
-
规则元素
- r [«args»]: 规则引用,一个带可选参数的小写的标识符
- {«action»} :用目标代码语言编写的语义动作,。在前一个元素之后和下一个元素之前执行。
- {«action»}? :语义断言。它的形式跟语义动作一样,区别在于,1、它的最终执行结果是boolean值的。2、如果是false它会抛出一个断言异常。
- {«action»}?=>:门语义断言。它的作用犹如一个门开关,只有断言成真的时候,其所在的选择分支才会被可见,请注意是可选分支。它跟语义断言不同的地方是,它的假值不会抛出异常,而是不进行后面的语法规则元素的识别
- («subrule»)=>: 句法断言. 这个跟前面三个不一样的地方在于,它类似于预先搜索这个规则,如果成立才会继续这个规则元素,同时,断言内的语义动作会被忽略。
-
子规则 规则中包含的可选块称为子规则(被封闭在括号中).子规则也可以看做规则(rule),但是没有显式的命名.子规则不能定义局部变量,也没有返回值.如果子规则只有一个元素,括号可以省略.子规则有四种.例如:
- (x|y|z) 只匹配一个选项
- (x|y|z)? 匹配一个或者不匹配
- (x|y|z)* 匹配零次或多次
- (x|y|z)+ 匹配一次或多次
-
规则属性定义 可以像编程语言中的函数那样,在规则中定义参数,返回值,局部变量,定义的这些属性会保存在规则上下文对象中(rule context object).
rulename [args] returns [retvals] locals [localvars]: ...;
//[..]中定义的变量可以在定义之后使用
add [int x] returns [int result] : '+=' INT {$result = $x + $INT.int;};
- 规则层面的动作 和语法层面的动作(action)一样,可以定义规则层面的动作. 合法的动作名是:init,after. 像这些动作的命名一样,解析器会在匹配相应的规则之前执行init动作,在规则匹配完成之后执行after动作.
row[String[] columns] returns [Map<String,String> values]
locals [int col=0]
@init {
$values = new HashMap<String,String>();
}
@after {
if ($values!=null && $values.size()>0) {
System.out.println("values = "+$values);
}
}
- 符号引用:在 Action 中可以引用规则符号来获得符号当前的信息,如: 获得变量的具体和变量名。
variable : type ID ';' {System.out.println($type.text + " " + $ID.text);};
程序运行后输入:int x; 输出:int x。 引用规则符号时使用字符“”是一个标识,表明了它是一个符号引用而不是普通的嵌入式代码。
- 使用变量:除了直接使用“$”符号加规则名的方法引用规则以外,ANTLR 文法可以将规则符号赋 值到一个变量中,然后引用变量就等于引用规则符号
variable : t=type id=ID ';'
{System.out.println("type: " + $t.text + " ID: " + $id.text);};
- +=的用法:文法中加入了“+=”操作符功能是将所有定义的变量收集到一个集合当中。
grammar SimpleAction;
variable : type ids+=ID (',' ids+=ID)* ';'
{
System.out.println($type.text);
for(Object t : $ids)
System.out.print(" " + ((Token)t).getText());
}
;
- 捕获异常
当规则中出现语法错误,ANTLR可以捕获异常,报告错误和尝试恢复(possibly by consuming more tokens),然后从规则中返回.
ANLTR通过策略模式来处理所有的异常,也可以为某个规则通过指定特定的异常处理:在规则末尾添加catch语句.
树语法规则
语法是^(root child1 child2 … childn)。 v3中有两种机制用于构建抽象语法树(ast):操作符和重写规则。
- 操作符 !: 在子树中不包括该节点或该子树
^: 为整个封闭规则创建子树的节点根
- 重写规则
重写语法比操作符更强大。它满足了大多数常见的树转换。
解析器语法指定如何识别输入,而重写是一种生成语法,指定如何生成输出。ANTLR指出如何将输入映射到输出语法。要创建一个虚拟节点,只需要像下面的例子那样提到它(UNIT是一个从虚拟令牌创建的节点,用于对编译单元块进行分组):
grammar Expr;
options {
output = AST;
ASTLabelType = CommonTree;
}
prog : (stat {System.out.println($stat.tree.toStringTree());})+
;
stat : expr NEWLINE -> expr
| ID '=' expr NEWLINE -> ^('=' ID expr)
| NEWLINE ->
;
expr returns [int value]
: multExpr (('+'^ | '-'^) multExpr)*
;
multExpr : atom ('*'^ atom)*
;
atom : INT
| ID
| '('! expr ')'!
;
ID : ('a'..'z'|'A'..'Z')+;
INT :('0'..'9')+;
NEWLINE : '\r'?'\n';
WS : (' '|'\t'|'\n'|'\r')+ {skip();};
虚拟节点UNIT
a : INT ; // duplicate INT node and return
a : ID -> ; // delete ID node from tree
a : INT ID -> ID INT ; // reorder nodes
a : ^(ID INT) -> ^(INT ID) ; // flip order of nodes in tree
a : (^(ID INT))+ -> INT+ ID+ ; // break apart trees into sequences
a : ^(ID INT) -> {some test}? ^(ID["ick"] INT)
-> INT
;//Predicates can be used to choose between rewrites as well
消除左递归
由于 ANTLR 的语法分析器是 LL 型的,所以当它遇到左递归的文法时,会导致解析器无限递归,最终堆栈溢出,所以最好在文法层面使用 * + 等规则来表示重复。也可以将规则代入公式,算出等价的非左递归文法。
- 消除直接左递归
A →Aα|β(α≠ε,β不以A开头)
//转化:
A →βA′
A′ → α A′|ε
这种消除过程实际就是把左递归转换成了右递归
- 消除间接左递归
S→Aa|b
A→Ac|Sd|ε
//将S的定义代入A-产生式,得:
A→Ac|Aad|bd|ε
//消除A-产生式的直接左递归,得:
A → b d A’ | A’
A’ → c A’ | a d A’ | ε
runtime API
运行antlr3生成“ELexer.java”、“EParser.java”、“E.tokens”和“E__.g”四个文件 其中有两个java源文件。“ELexer.java”为词法分析部分的代码,“EParser.java”为语法分析部分的代码。
import org.antlr.runtime.*;
import org.antlr.runtime.tree.*;
public class run
{
public static void main(String[] args) throws Exception
{
ANTLRInputStream input = new ANTLRInputStream(System.in);
ELexer lexer = new ELexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
EParser parser = new EParser(tokens);
EParser.expression_return r = parser.expression();
System.out.println(((BaseTree)r.getTree()).toStringTree());
}
}
把这段代码存入run.java文件中,main方法功能是从命令行接收输入的表达式,通过词法分析和语法分析两个步骤来获得这个表达式的语法树,并以字符串的形式输出语法树的内容。
ANTLRInputStream input = new ANTLRInputStream(System.in);
ELexer lexer = new ELexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
词法分析步骤是从命令行接收输入的表达式,通过ANTLR内部ANTLRInputStream类,生成一个ANTLRInputStream类的实例input,再将input传给ELexer类。ELexer类是词法分析类,把input中的输入内容进行词法分析,词法分析后会产生词号流(token stream)交给语法分析类,为语法分析提拱前提。
EParser parser = new EParser(tokens);
EParser.program_return r = parser.program();//此处进行了语法分析
System.out.println(((BaseTree)r.getTree()).toStringTree());
语法分析步骤是根据词法分析产生的词号流生成语法分析类的实例parser。然后调用parser的方法expression()。
这个方法名和我们文法中的第一条规则expression : multExpr (('+' |'-' ) multExpr)*;的名字是一致的,这说明我们要用整个文法进行分析,expression是文法的启点。
调用expression()方法后就进行了语法分析,expression()方法返回语法分析的信息其中包括语法树。r.getTree()可以返回语法树,getTree()返回的是Object类型所以这里做一个类型转换(BaseTree)r.getTree()并调用其toStringTree()方法获得语法树的字符串形式输出。
hive-sql
sql编译过程
hiveSQL转换成MapReduce的执行计划包括如下几个步骤: HiveSQL ->AST(抽象语法树) -> QB(查询块) ->OperatorTree(操作树)->优化后的操作树->mapreduce任务树->优化后的mapreduce任务树
- SQL Parser:Antlr定义SQL的语法规则,完成SQL词法,语法解析,将SQL转化为抽象语法树AST Tree;
- Semantic Analyzer:遍历AST Tree,抽象出查询的基本组成单元QueryBlock;
- Logical plan:遍历QueryBlock,翻译为执行操作树OperatorTree;
- Logical plan optimizer: 逻辑层优化器进行OperatorTree变换,合并不必要的ReduceSinkOperator,减少shuffle数据量;
- Physical plan:遍历OperatorTree,翻译为MapReduce任务;
- Physical plan optimizer:物理层优化器进行MapReduce任务的变换,生成最终的执行计划;
SQL Parser
Hive中通过antlr3定义的词法文件、语法文件存放在Hive源码src/org/apache/hadoop/hive/ql/parse/ 目录中,以.g后缀结尾,其中包含了5个文件。
- HiveLexer.g :定义Hive关键字,及组成词组的合法字符
- SelectClauseParser.g :定义select语句的语法规则
- FromClauseParser.g :定义from语句的语法规则
- IdentifiersParser.g :定义函数、group等的语法规则
- HiveParser.g:定义语法规则文件,引入了其他语法规则文件
词法文件
HiveLexer.g是Hive的词法文件,其中定义了Hive所有的关键字、保留字以及可以被Hive识别的合法字符,不在其中定义的字符,将被Hive认为是非法字符输入。词法规则示例:
KW_SELECT : 'SELECT';
KW_WHERE : 'WHERE';
KW_FROM : 'FROM';
fragment
Letter
: 'a'..'z' | 'A'..'Z'
;
语法文件
语法文件中定义了HiveQL的语法规则,用户写的HiveQL将会按照在语法文件中定义的语法规则进行重写,将HiveQL转换为抽象语法树。
Hive的查询语句由以下几个部分构成: Select语句:查询映射部分; From语句:描述查询的数据源,数据源主要包括表、子查询语句、join语句等; Body语句:查询语句的主体部分,包括group by,distribute by,order by,sort by,limit,having,where,clusterby等。
语法文件中 atomSelectStatement 部分是查询语句的基本结构,如下:
atomSelectStatement
:
s=selectClause
f=fromClause?
w=whereClause?
g=groupByClause?
h=havingClause?
win=window_clause?
-> ^(TOK_QUERY $f? ^(TOK_INSERT ^(TOK_DESTINATION ^(TOK_DIR TOK_TMP_FILE))
$s $w? $g? $h? $win?))
|
LPAREN! selectStatement RPAREN!
;
语法文件中,将输入的HiveQL按照语法规则进行重写,生成如下结构语法树:
-
语法树由from 和 TOK_INSERT 两部分组成。from 代表了 from子句的语法树;TOK_INSERT 子树是查询的主体部分,包含了查询结果目的数据源TOK_DESTINATION子树、select子句语法树、body子句(where, group,having等)语法树。TOK_DESTINATION节点是在语法改写中特意增加了的一个节点。原因是Hive中所有查询的数据均会保存在HDFS临时的文件中,无论是中间的子查询还是查询最终的结果,Insert语句最终会将数据写入表所在的HDFS目录下。
-
以上是Hive查询语句的语法树主体结构,所有的查询语句都会转换成这样结构的语法树,不同的是from、select等子树的不同,子树的生成同样也是根据语法文件中定义的语法规则,对各个子句进行重写,生成对应的语法树子树,拼接到语法树的主体结构上,最终生成HiveQL对应的完整抽象语法树。
parser grammar HiveParser;
options
{
tokenVocab=HiveLexer; //词汇表来源于 HiveLexer
output=AST; //输出 抽象语法树
ASTLabelType=CommonTree; //抽象语法树类型为 commonTtree
backtrack=false; //不回溯
k=3; //前向窥看3个token的长度。
}
import SelectClauseParser, FromClauseParser, IdentifiersParser;
//整个规则由statement开始。
//statement由解释语句explainStatement或执行语句execStatement组成。
statement
: explainStatement EOF
| execStatement EOF
;
//解释语句explainStatement由KW_EXPLAIN开始,中间有可选项KW_EXTENDED KW_FORMATTED KW_DEPENDENCY KW_LOGICAL,后面紧跟着执行语句,KW_ 开始的token代表关键字。
//语法形式:
//@init{} 表示进入规则时执行后面的{}里的动作,例中压入trace的消息。
//@after{} 表示规则完成后执行{}里面的动作,例中弹出trace的消息。
//->构建语法抽象树 ^(rootnode leafnode1 leafnode2...) 如例表示构建一个以TOK_EXPLAIN为根节点,execStatement为第一个叶结点,可选项为第二个叶结点,如果有可选项的话。
//explainOptions=KW_EXTENDED定义了explainOptions作为别名引用KW_EXTENDED,引用形式为$explainOptions.
explainStatement
@init { msgs.push("explain statement"); }
@after { msgs.pop(); }
: KW_EXPLAIN (explainOptions=KW_EXTENDED|explainOptions=KW_FORMATTED|explainOptions=KW_DEPENDENCY|explainOptions=KW_LOGICAL)? execStatement
-> ^(TOK_EXPLAIN execStatement $explainOptions?)
;
//执行语句execStatement由查询、装载、导出、导入、数据定义四大语句组成
execStatement
@init { msgs.push("statement"); }
@after { msgs.pop(); }
: queryStatementExpression
| loadStatement
| exportStatement
| importStatement
| ddlStatement
;
//装载语句只关注路径、表或分区、是否本地、是否重写。
loadStatement
@init { msgs.push("load statement"); }
@after { msgs.pop(); }
: KW_LOAD KW_DATA (islocal=KW_LOCAL)? KW_INPATH (path=StringLiteral) (isoverwrite=KW_OVERWRITE)? KW_INTO KW_TABLE (tab=tableOrPartition)
-> ^(TOK_LOAD $path $tab $islocal? $isoverwrite?)
;
//导出语句只关注表或分区、导出路径
exportStatement
@init { msgs.push("export statement"); }
@after { msgs.pop(); }
: KW_EXPORT KW_TABLE (tab=tableOrPartition) KW_TO (path=StringLiteral)
-> ^(TOK_EXPORT $tab $path)
;
//导入语句只关注导入路径、表或分区、是否是外部
importStatement
@init { msgs.push("import statement"); }
@after { msgs.pop(); }
: KW_IMPORT ((ext=KW_EXTERNAL)? KW_TABLE (tab=tableOrPartition))? KW_FROM (path=StringLiteral) tableLocation?
-> ^(TOK_IMPORT $path $tab? $ext? tableLocation?)
;
//以此类推,不再累述。
解析流程
HiveLexerX,HiveParser都是由Antlr词法、语法文件编译后自动生成的词法、语法解析类。
HiveLexerX解析类进行字符流->Token流的转换,其进行HQL词法识别;
HiveParser解析类进行字符流->Token流的转换,其进行HQL语法识别,语法识别会对原语进行翻译,并通过一些标识符标示一些特定的语法(如,TOK_QUERY标示一个查询块);
- 使用自定义的抽象语法树节点类型
public static final TreeAdaptor adaptor = new CommonTreeAdaptor() {
/**
* Creates an ASTNode for the given token. The ASTNode is a wrapper around
* antlr's CommonTree class that implements the Node interface.
*
* @param payload
* The token.
* @return Object (which is actually an ASTNode) for the token.
*/
@Override
public Object create(Token payload) {
return new ASTNode(payload);
}
@Override
public Object dupNode(Object t) {
return create(((CommonTree)t).token);
};
@Override
public Object errorNode(TokenStream input, Token start, Token stop, RecognitionException e) {
return new ASTErrorNode(input, start, stop, e);
};
};
...
parser.setTreeAdaptor(adaptor);
- 包装了antlr的antlrStringStream 成为ANTLRNoCaseStringStream。消除了大小写敏感。
public class ANTLRNoCaseStringStream extends ANTLRStringStream {
public ANTLRNoCaseStringStream(String input) {
super(input);
}
@Override
public int LA(int i) {
int returnChar = super.LA(i);
if (returnChar == CharStream.EOF) {
return returnChar;
} else if (returnChar == 0) {
return returnChar;
}
return Character.toUpperCase((char) returnChar);
}
}
...
HiveLexerX lexer = new HiveLexerX(new ANTLRNoCaseStringStream(command));
TokenRewriteStream tokens = new TokenRewriteStream(lexer);