0、为什么你要学习编译原理?
1、理解代码:编译器的前端技术
1.1 词法分析
Lexical Analysis
通常,编译器的第一项工作叫做词法分析
。就像阅读文章一样,文章是由一个个的中文单词组成的。程序处理也一样,只不过这里不叫单词,而是叫做词法记号
,英文叫 Token
。
也可以用词法分析器的生成工具来生成,比如 Lex
(或其 GNU
版本,Flex
)。这些生成工具是基于一些规则来工作的,这些规则用正则文法
表达,符合正则文法的表达式称为正则表达式
。生成工具可以读入正则表达式,生成一种叫有限自动机
的算法,来完成具体的词法分析工作。
不要被正则文法(Regular Grammar)
和有限自动机(Finite-state Automaton,FSA,or Finite Automaton)
吓到。正则文法
是一种最普通、最常见的规则,写正则表达式的时候用的就是正则文法
。
1.2 语法分析
Syntactic Analysis, or Parsing
程序也有定义良好的语法结构,它的语法分析过程,就是构造这么一棵树。一个程序就是一棵树,这棵树叫做抽象语法树(Abstract Syntax Tree,AST)
。树的每个节点(子树)是一个语法单元,这个单元的构成规则就叫语法
。每个节点还可以有下级节点。
clang -cc1 -ast-dump hello.c // 查看语法树
形成 AST
以后有什么好处呢?就是计算机很容易去处理。比如,针对表达式形成的这棵树,从根节点遍历整棵树就可以获得表达式的值。
你现在已经有了一定的经验,大可以去找找看有没有现成的工具,比如 Yacc
(或 GNU
的版本,Bison
)、Antlr
、JavaCC
等。 Comparison of parser generators。
1.3 语义分析
Semantic Analysis
语义分析就是要让计算机理解我们的真实意图,把一些模棱两可的地方消除掉。其实语义分析没那么复杂,因为计算机语言的语义一般可以表达为一些规则,你只要检查是否符合这些规则就行了。比如:
- 某个表达式的计算结果是什么数据类型?如果有数据类型不匹配的情况,是否要做自动转换?
- 如果在一个代码块的内部和外部有相同名称的变量,我在执行的时候到底用哪个? 就像“我喜欢又聪明又勇敢的你”中的“你”,到底指的是谁,需要明确。
- 在同一个作用域内,不允许有两个名称相同的变量,这是唯一性检查。你不能刚声明一个变量
a
,紧接着又声明同样名称的一个变量a
,这就不允许了。
1.4 总结
- 词法分析是把程序分割成一个个
Token
的过程,可以通过构造有限自动机来实现。 - 语法分析是把程序的结构识别出来,并形成一棵便于由计算机处理的抽象语法树。可以用递归下降的算法来实现。
- 语义分析是消除语义模糊,生成一些属性信息,让计算机能够依据这些信息生成目标代码。
2、正则文法和有限自动机
解析 age >= 45
我们来描述一下标识符、比较操作符和数字字面量这三种Token
的词法规则。
- 标识符:第一个字符必须是字母,后面的字符可以是字母或数字。
- 比较操作符:
>
和>=
(其他比较操作符暂时忽略)。 - 数字字面量:全部由数字构成(像带小数点的浮点数,暂时不管它)。
上面的例子涉及了 4
种 Token
,这 4
种 Token
用正则表达式
表达,是下面的样子:
Id : [a-zA-Z_] ([a-zA-Z_] | [0-9])*
IntLiteral: [0-9]+
GT : '>'
GE : '>='
2.1 代码实现
代码实现:SimpleLexer
3、语法分析1
递归下降算法(Recursive Descent Parsing
),语法分析的结果是生成 AST
。算法分为自顶向下和自底向上算法,其中,递归下降算法是一种常见的自顶向下(遍历AST
的)算法。
我们首先把变量声明语句的规则,用形式化的方法表达一下。它的左边是一个非终结符(Non-terminal
)。右边是它的产生式(Production Rule
)。在语法解析的过程中,左边会被右边替代。如果替代之后还有非终结符,那么继续这个替代过程,直到最后全部都是终结符(Terminal
),也就是 Token
。只有终结符才可以成为 AST
的叶子节点。这个过程,也叫做推导(Derivation
)过程:
intDeclaration : Int Identifier ('=' additiveExpression)?;
我们把解析变量声明语句和表达式的算法分别写成函数。在语法分析的时候,调用这些函数跟后面的 Token
串做模式匹配。匹配上了,就返回一个 AST
节点,否则就返回 null
。如果中间发现跟语法规则不符,就报编译错误。
在这个过程中,上级文法嵌套下级文法,上级的算法调用下级的算法。表现在生成 AST
中,上级算法生成上级节点,下级算法生成下级节点。这就是“下降”的含义。
程序结构基本上是跟文法规则同构的。这就是递归下降算法
的优点,非常直观。
// AST
Programm Calculator
IntDeclaration age
AssignmentExp =
IntLiteral 45
用代码解析Int
申明的代码如下:
fn int_declare<T: TokenReader>(&self, tokens: &mut T) -> Result<Option<SimpleASTNode>, io::Error> {
let token = tokens.peek();
if token.is_none() || token.unwrap().get_type() != TokenType::Int {
return Ok(None);
}
// token.Type = TokenType::Int
let _ = tokens.read(); // 消耗掉int
let e = io::Error::new(io::ErrorKind::InvalidInput, "variable name expected");
let token = tokens.peek().ok_or(e)?;
if token.get_type() != TokenType::Identifier {
let e = io::Error::new(io::ErrorKind::InvalidInput, "variable name expected");
return Err(e);
}
// token.Type = TokenType::Identifier
let token = tokens.read().unwrap(); // 消耗掉 Identifier
// 创建当前节点,并把变量名记到AST节点的文本值中,这里新建一个变量子节点也是可以的
let mut node = SimpleASTNode::new(ASTNodeType::IntDeclaration, token.get_text());
let token = tokens.peek();
if !token.is_none() && token.unwrap().get_type() == TokenType::Assignment {
let _ = tokens.read(); // 消耗掉 =
let e = io::Error::new(io::ErrorKind::InvalidInput,
"invalid variable initialization, expecting an expression");
let child = self.additive(tokens)?.ok_or(e)?;
node.add_child(RefCell::new(Rc::new(child)));
}
let token = tokens.peek();
if token.is_none() || token.unwrap().get_type() != TokenType::SemiColon {
let e = io::Error::new(io::ErrorKind::InvalidInput, "invalid statement, expecting semicolon");
return Err(e);
}
let _ = tokens.read(); // 消耗掉 ;
Ok(Some(node))
}
用上下文无关文法描述算术表达式
我们把规则分成两级:第一级是加法规则,第二级是乘法规则。把乘法规则作为加法规则的子规则,这样在解析形成 AST 时,乘法节点就一定是加法节点的子节点,从而被优先计算
// 递归下降算法
additiveExpression
: multiplicativeExpression
| additiveExpression Plus multiplicativeExpression
;
multiplicativeExpression
: IntLiteral
| multiplicativeExpression Star IntLiteral
;
PS :
这个实际上就是语法规则,是用BNF表达的。以addtive为例,它有两个产生式。
产生式1:一个乘法表达式
产生式2:一个加法表达式 + 乘法表达式。
通过上面两个产生式的组合,特别是产生式
2
的递归调用,就能推导出所有的加减乘数算术表达式。
比如,对于2*3
这个表达式,运用的是产生式1
。对于2+3*5
,运用的是产生式2
。
我上面用的语法规则的写法,实际上是后面会用到的Antlr工具的写法。你也可以这样书写,就是一般教材上的写法:
A -> M | A + MM -> int | M * int
我们每个非终结符只用了一个大写字母代表,比较简洁。我在文稿中用比较长的单词,是为了容易理解其含义。
其中的竖线,是选择其一。你还可以拆成最简单的方式,形成4条规则:A -> M
A -> A + M
M -> int
M -> M * int
上面这些不同的写法,都是等价的。你要能够看习惯,在不同的写法中自由切换。
应该注意的是,加法规则中还递归地又引用了加法规则。通过这种递归的定义,我们能展开、形成所有各种可能的算术表达式。比如2+3*5
的推导过程:
-->additiveExpression + multiplicativeExpression
-->multiplicativeExpression + multiplicativeExpression
-->IntLiteral + multiplicativeExpression
-->IntLiteral + multiplicativeExpression * IntLiteral
-->IntLiteral + IntLiteral * IntLiteral
这种文法已经没有办法改写成正则文法
了,它比正则文法的表达能力更强,叫做上下文无关文法
。正则文法
是上下文无关文法
的一个子集。它们的区别呢,就是上下文无关文法
允许递归调用,而正则文法
不允许。
3.1 左递归下降无限循环问题
为了简单化,我们采用下面这个简化的文法,去掉了乘法的层次:
additiveExpression
: IntLiteral
| additiveExpression Plus IntLiteral
;
在解析 2 + 3
这样一个最简单的加法表达式的时候,我们直观地将其翻译成算法,结果出现了如下的情况:
- 首先匹配是不是整型字面量,发现不是;
- 然后匹配是不是加法表达式,这里是递归调用;
- 会重复上面两步,无穷无尽。
additiveExpression Plus multiplicativeExpression
这个文法规则的第一部分就递归地引用了自身,这种情况叫做左递归
。通过上面的分析,我们知道左递归是递归下降算法无法处理的,这是递归下降算法最大的问题。
就这个推导说说我目前的理解,其中最开始不能理解的根本原因就是没能理解语法规则之间的相互关系,以及与此相关的token的消耗。
比如例子 A->Int | A + Int
在最开始的理解中,错误以为,这两条是顺序关系,与此相应就想当然认为token的消耗是像字符串匹配一样“一个接一个”的进行。这种错误思路是这样的:2+3, 首先看token 2, 它是int所以消耗掉,然后类推。
而实际上,这两条规则是从某种程度上是“互斥”的关系。也就是说,2+3 要么是 Int, 要么是 A+Int,在没有找到合适的规则前,token 是不会被消耗的。由此,在深度优先实现中,就有老师所说的推导实现过程。总的要解决的问题是,2+3 是不是 A,能不能用这条 A 规则来解释。那么就看它是否满足 A 的具体规则。首先,2+3 显然不是 Int,因此没有 token 消耗。然后,在匹配 A + Int 时,上来就要看 2+3 是不是 A,不断要解决原来的问题,从而就产生了所谓左递归。
所以在深度优先情况下,打破无穷递归,就把规则改为A-> Int | Int + A。这时,推导, 2+3 显然不是Int。于是看 Int + A。2 显然是Int,于是消耗掉;再看 +,消耗掉;再看 3 是不是 A,3 显然是 Int,所以返回。
怎么解决?
additiveExpression
: multiplicativeExpression
| additiveExpression Plus multiplicativeExpression
;
multiplicativeExpression
: IntLiteral
| IntLiteral Star multiplicativeExpression
;
现在我们貌似解决了左递归问题,运行这个算法解析 2+3*5
,得到下面的 AST
:
Programm Calculator
AdditiveExp +
IntLiteral 2
MulticativeExp *
IntLiteral 3
IntLiteral 5
是不是看上去一切正常?可如果让这个程序解析2+3+4
呢?
Programm Calculator
AdditiveExp +
IntLiteral 2
AdditiveExp +
IntLiteral 3
IntLiteral 4
问题是什么呢?计算顺序发生错误了。连续相加的表达式要从左向右计算,这是加法运算的结合性规则。但按照我们生成的 AST
,变成从右向左了,先计算了3+4
,然后才跟2
相加。这可不行!
- 首先调用乘法表达式匹配函数
multiplicative()
,成功,返回了一个字面量节点2
。 - 接着看看右边是否能递归地匹配加法表达式。
- 匹配的结果,真的返回了一个加法表达式
3+4
,这个变成了第二个子节点。错误就出在这里了。这样的匹配顺序,3+4
一定会成为子节点,在求值时被优先计算。
3.2 实现表达式求值 - 代码实现
深度优先的遍历也是一个递归算法。以上文中2 + 3 * 5
的AST
为例看一下。
- 对表达式的求值,等价于对 AST 根节点求值。
- 首先求左边子节点,算出是 2。
- 接着对右边子节点求值,这时候需要递归计算下一层。计算完了以后,返回是 15(3*5)。
- 把左右节点相加,计算出根节点的值 17。
代码实现:SimpleCalculator
fn calculate_and_print<T: ASTNode>(&self, node: &Rc<T>, indent: &str) -> i32 {
let mut result = 0;
println!("{} Calculating: {}", indent, node.get_type());
match node.get_type() {
ASTNodeType::Program => {
for child in node.get_children().iter() {
result = self.calculate_and_print(child, format!("{}\t", indent).as_str());
}
}
ASTNodeType::Additive | ASTNodeType::Multiplicative => {
let children = node.get_children();
let child1 = children.get(0).expect("child 1 not found");
let child2 = children.get(1).expect("child 2 not found");
let num1 = self.calculate_and_print(child1, format!("{}\t", indent).as_str());
let num2 = self.calculate_and_print(child2, format!("{}\t", indent).as_str());
match node.get_text() {
"+" => result = num1 + num2,
"-" => result = num1 - num2,
"*" => result = num1 * num2,
"/" => result = num1 / num2,
_ => println!("found unsupported operator: {}", node.get_text()),
}
}
ASTNodeType::IntLiteral => {
result = i32::from_str(node.get_text()).unwrap_or_else(|e| {
panic!("parse {} failed {}", node.get_text(), e);
});
}
_ => { println!("found unhandled node: {}", node.get_type()) }
};
println!("{}Result: {}", indent, result);
result
}
/*
加法表达式
additiveExpression
: multiplicativeExpression
| additiveExpression Plus multiplicativeExpression
;
*/
fn additive<T: TokenReader>(&self, tokens: &mut T) -> Result<Option<SimpleASTNode>, io::Error> {
let child1 = self.multiplicative(tokens)?;
let token = tokens.peek();
if token.is_none() {
return Ok(child1);
}
let token = token.unwrap();
if token.get_type() != TokenType::Plus && token.get_type() != TokenType::Minus {
return Ok(child1);
}
let e = io::Error::new(io::ErrorKind::InvalidInput, "invalid additive expression, expecting the right part.");
let node = SimpleASTNode::new(ASTNodeType::Multiplicative, tokens.read().unwrap().get_text());
let child1 = child1.unwrap();
let child2 = self.additive(tokens)?.ok_or(e)?;
node.add_child(RefCell::new(Rc::new(child1)));
node.add_child(RefCell::new(Rc::new(child2)));
// let node_rc = Rc::new(node);
// *child1.parent.borrow_mut() = Rc::downgrade(&node_rc);
Ok(Some(node))
}
/*
语法解析:乘法表达式
multiplicativeExpression
: IntLiteral
| IntLiteral Star multiplicativeExpression
;
*/
fn multiplicative<T: TokenReader>(&self, tokens: &mut T) -> Result<Option<SimpleASTNode>, io::Error> {
let child1 = self.primary(tokens)?;
let token = tokens.peek();
if token.is_none() {
return Ok(child1);
}
let token = token.unwrap();
if token.get_type() != TokenType::Star && token.get_type() != TokenType::Slash {
return Ok(child1);
}
let node = SimpleASTNode::new(ASTNodeType::Multiplicative, tokens.read().unwrap().get_text());
let e = io::Error::new(io::ErrorKind::InvalidInput, "invalid additive expression, expecting the right part.");
let child1 = child1.unwrap();
let child2 = self.multiplicative(tokens)?.ok_or(e)?;
node.add_child(RefCell::new(Rc::new(child1)));
node.add_child(RefCell::new(Rc::new(child2)));
// let node_rc = Rc::new(node);
// *child1.parent.borrow_mut() = Rc::downgrade(&node_rc);
Ok(Some(node))
}
// 语法解析:基础表达式
// add -> mul | add + mul
// mul -> pri | mul * pri
// pri -> Id | Num | (add)
fn primary<T: TokenReader>(&self, tokens: &mut T) -> Result<Option<SimpleASTNode>, io::Error> {
let token = tokens.peek();
if token.is_none() {
return Ok(None);
}
let token = token.unwrap();
match token.get_type() {
TokenType::IntLiteral => { // 整型字面量
let token = tokens.read().unwrap();
Ok(Some(SimpleASTNode::new(ASTNodeType::IntLiteral, token.get_text())))
}
TokenType::Identifier => { // 变量名
let token = tokens.read().unwrap();
Ok(Some(SimpleASTNode::new(ASTNodeType::Identifier, token.get_text())))
}
TokenType::LeftParen => { // (
let token = tokens.read().unwrap(); // 消耗掉 (
let node = self.additive(tokens)?;
if node.is_none() {
return Err(simple_calculator::invalid_input_err("expecting an additive expression inside parenthesis"));
}
let token = tokens.peek();
if token.is_none() {
return Err(simple_calculator::invalid_input_err("expecting right parenthesis"));
}
let token = token.unwrap();
if token.get_type() == TokenType::RightParen {
let _ = tokens.read(); // 消耗掉 )
return Ok(node);
}
Err(simple_calculator::invalid_input_err("expecting right parenthesis"))
}
_ => {
// invalid_input_err("unknown token type")
Err(simple_calculator::invalid_input_err("unknown token type"))
}
}
}
3.3 总结
递归算法
是很好的自顶向下
解决问题的方法,是计算机领域的一个核心的思维方式。拥有这种思维方式,可以说是程序员相对于非程序员的一种优势。
4、语法分析2
我们已经知道,语法规则是由上下文无关文法表示的,而上下文无关文法是由一组替换规则(又叫产生式)组成的,比如算术表达式的文法规则可以表达成下面这种形式:
add ::= mul | add + mul
mul ::= pri | mul * pri
pri ::= Id | Num | (add)
这种写法叫做巴科斯范式
,简称BNF
。Antlr
和Yacc
这两个工具都用这种写法。为了简化书写,我有时会在课程中把::=
简化成一个冒号。你看到的时候,知道是什么意思就可以了。
你有时还会听到一个术语,叫做扩展巴科斯范式
(EBNF
)。它跟普通的BNF
表达式最大的区别,就是里面会用到类似正则表达式的一些写法。比如下面这个规则中运用了 *
号,来表示这个部分可以重复 0
到多次:
add -> mul (+ mul)*
4.1 确保正确的优先级
我们由加法规则推导到乘法规则,这种方式保证了 AST
中的乘法节点一定会在加法节点的下层,也就保证了乘法计算优先于加法计算。
exp -> or | or = exp
or -> and | or || and
and -> equal | and && equal
equal -> rel | equal == rel | equal != rel
rel -> add | rel > add | rel < add | rel >= add | rel <= add
add -> mul | add + mul | add - mul
mul -> pri | mul * pri | mul / pri
pri -> Id | Literal | (exp)
这里表达的优先级从低到高是:赋值运算、逻辑运算(or
)、逻辑运算(and
)、相等比较(equal
)、大小比较(rel
)、加法运算(add
)、乘法运算(mul
)和基础表达式(pri
)。
4.2 确保正确的结合性
在上一讲中,我针对算术表达式写的第二个文法是错的,因为它的计算顺序是错的。2+3+4
这个算术表达式,先计算了3+4
然后才和2
相加,计算顺序从右到左,正确的应该是从左往右才对。
根据这个 AST
做计算会出现计算顺序的错误。不过如果我们将递归项写在左边,就不会出现这种结合性的错误。于是我们得出一个规律:对于左结合的运算符,递归项要放在左边;而右结合的运算符,递归项放在右边。
所以你能看到,我们在写加法表达式的规则的时候,是这样写的:
add -> mul | add + mul
这样写是有左递归问题。那我们如何解决左递归问题呢?
4.3 消除左递归
消除左递归,用一个标准的方法,就能够把左递归文法改写成非左递归的文法。以加法表达式规则为例,原来的文法是add -> add + mul
,现在我们改写成:
add -> mul add'
add' -> + mul add' | ε
文法中,ε
(读作 epsilon
)是空集的意思。接下来,我们用刚刚改写的规则再次推导一下2+3+4
这个表达式,得到了下图中左边的结果:
左边的分析树是推导后的结果。问题是,由于 add’
的规则是右递归的,如果用标准的递归下降算法,我们会跟上一讲一样,又会出现运算符结合性的错误。我们期待的 AST
是右边的那棵,它的结合性才是正确的。那么有没有解决办法呢?
如果用EBNF
方式表达,也就是允许用*
号和+
号表示重复,上面两条规则可以合并成一条:
add -> mul (+ mul)*
// 伪代码如下:
mul();
while(next token is +){
mul()
createAddNode
}
我们扩展一下话题。在研究递归函数的时候,有一个概念叫做尾递归
,尾递归函数的最后一句是递归地调用自身。
编译程序通常都会把尾递归转化为一个循环语句,使用的原理跟上面的伪代码是一样的。相对于递归调用来说,循环语句对系统资源的开销更低,因此,把尾递归转化为循环语句也是一种编译优化技术。
4.4 代码实现
代码实现:SimpleParser
// 解析脚本,并返回根节点
fn parse(&self, code: &str) -> Result<SimpleASTNode, io::Error> {
let lexer = simple_lexer::SimpleLexer::new();
let mut tokens = lexer.tokenize(code);
self.get_root(&mut tokens)
}
// 语法解析:根节点
fn get_root<T: TokenReader>(&self, tokens: &mut T) -> Result<SimpleASTNode, io::Error> {
let node = SimpleASTNode::new(ASTNodeType::Program, "SimpleParser");
while !tokens.peek().is_none() {
// 先看下,是不是 int 变量声明 e.g. int a = 1;
let mut child = self.int_declare(tokens).expect("get int declare statement failed"); // 整形字面量 node
if child.is_none() {// 不是 int 变量,看下是不是 普通的表达式。
child = self.expression_statement(tokens).expect("get expression statement failed");
}
if child.is_none() { // 不是表达式,看下是不是赋值语句 e.g. a = 100;
child = self.assignment_statement(tokens).expect("get assignment statement failed");
}
if child.is_none() {
return Err(simple_calculator::invalid_input_err("unknown statement"));
}
let child = child.unwrap();
node.add_child(RefCell::new(Rc::new(child)))
}
Ok(node)
}
5、语法分析3
5.1 实现一个简单的 REPL
脚本语言一般都会提供一个命令行窗口,让你输入一条一条的语句,马上解释执行它,并得到输出结果,比如 Node.js
、Python
等都提供了这样的界面。这个输入、执行、打印的循环过程就叫做 REPL(Read-Eval-Print Loop)
。你可以在 REPL
中迅速试验各种语句,REPL
即时反馈的特征会让你乐趣无穷。所以,即使是非常资深的程序员,也会经常用 REPL
来验证自己的一些思路,它相当于一个语言的 PlayGround
(游戏场),是个必不可少的工具。
一个简单脚本解释器,上下文无关文法如下:
programm: statement+;
statement
: intDeclaration
| expressionStatement
| assignmentStatement
;
// 变量声明语句以 int 开头,后面跟标识符,然后有可选的初始化部分,也就是一个等号和一个表达式,最后再加分号:
intDeclaration : 'int' Id ( '=' additiveExpression)? ';';
// 表达式语句目前只支持加法表达式,未来可以加其他的表达式,比如条件表达式,它后面同样加分号:
expressionStatement : additiveExpression ';';
// 赋值语句是标识符后面跟着等号和一个表达式,再加分号:
assignmentStatement : Identifier '=' additiveExpression ';';
// 为了在表达式中可以使用变量,我们还需要把 primaryExpression 改写,除了包含整型字面量以外,还要包含标识符和用括号括起来的表达式:
primaryExpression : Identifier| IntLiteral | '(' additiveExpression ')';
5.2 代码实现
代码实现:SimpleScript
6、编译器前端工具 - Antlr
编译器前端工具有很多,比如Lex
(以及GNU
的版本Flex
)、Yacc
(以及GNU
的版本Bison
)、JavaCC
等等。
使用Antlr
原因:
- 第一个原因是
Antlr
能支持更广泛的目标语言,包括Java
、C#
、JavaScript
、Python
、Go
、C++
、Swift
。无论你用上面哪种语言,都可以用它生成词法和语法分析的功能。而我们就使用它生成了Java
语言和C++
语言两个版本的代码。 - 第二个原因是
Antlr
的语法更加简单。它能把类似左递归的一些常见难点在工具中解决,对提升工作效率有很大的帮助。这一点,你会在后面的课程中直观地感受到。
在Antlr
的规则文件中,越是前面声明的规则,优先级越高。所以,我们把关键字的规则放在ID
的规则前面。算法在执行的时候,会首先检查是否为关键字,然后才会检查是否为ID
,也就是标识符。
环境配置:
brew install antlr@4
// 设置 CLASSPATH
export CLASSPATH=/opt/homebrew/Cellar/antlr/4.12.0/antlr-4.12.0-complete.jar:$CLASSPATH
// demo1
antlr Hello.g4
javac *.java
grun Hello tokens -tokens hello.play
// demo2 https://github.com/fanlv/play-with-complier/tree/main/antlr-test/play-script
antlr PlayScript.g4
javac *.java
grun PlayScript expression -gui
// 进入命令行可以输入表达式
age + 10 * 2 + 10
// 再按 ctrl + D 进入 gui
7、编译器前端工具2 - Antlr
antlr
一些正则文法基本编写- 使用
visitor
方式遍历节点,然后计数。antlr -visitor PlayScript.g4