Antlr (ANother Tool for Language Recognition) 是一个强大的跨语言语法解析器,可以用来读取、处理、执行或翻译结构化文本或二进制文件。
它被广泛用来构建语言,工具和框架。。ANTLR 根据语法定义生成解析器,解析器可以构建和遍历解析树。
所有编程语言的语法,都可以用ANTLR来定义。ANTLR提供了大量的官方 grammar 示例,
包含了各种常见语言,比如Java、SQL、Javascript、PHP等等。
谁在使用
Twitter搜索使用ANTLR进行语法分析,每天处理超过20亿次查询;
Hadoop生态系统中的Hive、Pig、数据仓库和分析系统所使用的语言都用到了ANTLR;
Lex Machina将ANTLR用于分析法律文本;Oracle公司在SQL开发者IDE和迁移工具中使用了ANTLR;
NetBeans公司的IDE使用ANTLR来解析C++;
Hibernate对象-关系映射框架(ORM)使用ANTLR来处理HQL语言
其他还有Oracle、Presto、Elasticsearch、Spark
官网地址:https://www.antlr.org
基本概念
词法分析器 (Lexer)词法分析是指在计算机科学中,将字符序列转换为单词(Token)的过程。词法分析器(Lexer)一般是用来供语法解析器(Parser)调用的。
语法解析器 (Parser) 语法解析器通常作为编译器或解释器出现。它的作用是进行语法检查,并构建由输入单词(Token)组成的数据结构(即抽象语法树)。
语法解析器通常使用词法分析器(Lexer)从输入字符流中分离出一个个的单词(Token),并将单词(Token)流作为其输入。实际开发中,语法解析器可以手工编写,也可以使用工具自动生成。
抽象语法树 (Abstract Syntax Tree,AST) 抽象语法树是源代码结构的一种抽象表示,它以树的形状表示语言的语法结构。抽象语法树一般可以用来进行代码语法的检查,
代码风格的检查,代码的格式化,代码的高亮,代码的错误提示以及代码的自动补全等等。
其他常见的语法分析器 JavaCC
JavaCC,即Java Cmopiler Compiler,为了简化基于Java语言的词法分析器或者语法分析器的开发,Sun公司的开发人员开发了JavaCC(Java Compiler Compiler)。
JavaCC是一个基于Java语言的分析器的自动生成器。用户只要按照JavaCC的语法规范编写JavaCC的源文件,然后使用JavaCC的编译器编译,
就能够生成基于Java语言的某种特定语言的分析器。JavaCC已经成为最受欢迎的Java解析器创建工具。
YACC
YACC(Yet Another Compiler-Compiler): 1975 年由贝尔实验室 Mike Lesk & Eric Schmidt 开发,UNIX 标准实用工具 (utility)。
SqlParser
位于Alibaba的Druid库中,只能解析sql语句,功能比较单一。
安装方式 有多种安装Antlr4的方式:
使用pip方式安装antlr4-tools
Maven工程中引入antlr4的依赖
IDEA中安装antlr4的插件
我平时都是喜欢用maven开发代码,因此选择第二种maven方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > com.xncoding</groupId > <artifactId > antlr4-demo</artifactId > <version > 1.0.0</version > <packaging > jar</packaging > <name > antlr4-demo</name > <description > antlr4 java source</description > <url > https://maven.apache.org</url > <properties > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > </properties > <dependencies > <dependency > <groupId > org.antlr</groupId > <artifactId > antlr4</artifactId > <version > 4.13.2</version > </dependency > </dependencies > <build > <finalName > antlr4</finalName > <sourceDirectory > src/main/java</sourceDirectory > <plugins > <plugin > <groupId > org.antlr</groupId > <artifactId > antlr4-maven-plugin</artifactId > <version > 4.13.2</version > <executions > <execution > <id > antlr4</id > <goals > <goal > antlr4</goal > </goals > <configuration > <includes > <include > **/*.g4</include > </includes > <listener > true</listener > <visitor > true</visitor > </configuration > </execution > </executions > </plugin > </plugins > </build > </project >
基本使用步骤 通过一个简单额hello world的例子来介绍Antlr4语法解析器开发的基本使用步骤。
定义一个G4文件 需要创建一个.g4文件,用于定义词法分析器(lexer)和语法解析器(Parser)。具体语法参见官方文档。下面是一个简单的例子:Hello.g4。
1 2 3 4 5 6 7 // file Hello.g4 // Define a grammar called Hello grammar Hello; // 1. grammer name @header { package pers.me.expression.parser; } // 2. java package r : 'hello' ID ; // 3. match keyword hello followed by an identifier ID : [a-z]+ ; // match lower-case identifiers WS : [ \t\r\n]+ -> skip ; // 4. skip spaces, tabs, newlines
定义了 grammar 的名字,名字需要与文件名对应
定义生成的Java类的package
r 定义的语法,会使用到下方定义的正则表达式词法
定义了空白字符,后面的 skip 是一个特殊的标记,标记空白字符会被忽略
生成代码 执行 mvn clean compile
可自动生成Antlr的代码。
测试 我们可以利用下面这段代码来测试一下ParseTree。
1 2 3 4 5 6 7 8 9 public class HelloTest { public static void main (String[] args) throws Exception { HelloLexer lexer = new HelloLexer (CharStreams.fromString("hello world" )); CommonTokenStream tokens = new CommonTokenStream (lexer); HelloParser parser = new HelloParser (tokens); ParseTree tree = parser.r(); System.out.println(tree.toStringTree(parser)); } }
运行上面的代码可以得到如下输出,程序识别出输入的字符串符合r的语法。
Listener和Visitor ANTLR提供了两种方法来访问ParseTree:
一种是通过Parse-Tree Listener的方法
另一种是通过Parse-Tree Visitor的方法
Listener有以下特点:
访问AST的所有节点
重写(Override)进入时(enterXXX方法)和退出时(exitXXX方法)要执行的方法
要重写的方法没有返回值,因此需要在属性中保留所需的值
Visitor有以下特点:
并非所有 AST 节点都被访问
根据目的重写进入节点时要执行的过程(visitXXX方法)
重写方法有一个返回值,因此您不必在属性中保存所需的值
最大的区别是Listener会自动访问 AST 的所有节点,而Visitor如果要访问当前节点的子树,则需要手工实现。
Visitor 较为简单方便,继承 HelloBaseVisitor 类即可,内部的方法与 g4 文件定义相对应,对照看即可理解。实现了 visitor 之后,
就可以完成一个简单的自定义解析器了。自动生成的HelloBaseVisitor.java如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import org.antlr.v4.runtime.tree.AbstractParseTreeVisitor;public class HelloBaseVisitor <T> extends AbstractParseTreeVisitor <T> implements HelloVisitor <T> { @Override public T visitR (HelloParser.RContext ctx) { return visitChildren(ctx); } }
计算器的实现 接下来进入实战环节,我们来实现一个简单的计算器。
定义语法和词法 创建Expression.g4。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 // 定义了 grammar 的名字,名字需要与文件名对应 grammar Expression; @header { // 定义package package pers.me.expression.parser; } /** * parser * calc 和 expr 就是定义的语法,会使用到下方定义的词法 * 注意 # 后面的名字,是可以在后续访问和处理的时候使用的。 * 一个语法有多种规则的时候可以使用 | 来进行配置。 */ calc : (expr)* EOF # calculationBlock ; // 四则运算分为了两个非常相似的语句,这样做的原因是为了实现优先级,乘除是优先级高于加减的。 expr : BR_OPEN expr BR_CLOSE # expressionWithBr | sign=(PLUS|MINUS)? num=(NUMBER|PERCENT_NUMBER) # expressionNumeric | expr op=(TIMES | DIVIDE) expr # expressionTimesOrDivide | expr op=(PLUS | MINUS) expr # expressionPlusOrMinus ; /** * lexer */ BR_OPEN: '('; BR_CLOSE: ')'; PLUS: '+'; MINUS: '-'; TIMES: '*'; DIVIDE: '/'; PERCENT: '%'; POINT: '.'; // 定义百分数 PERCENT_NUMBER : NUMBER ((' ')? PERCENT) ; NUMBER : DIGIT+ ( POINT DIGIT+ )? ; DIGIT : [0-9]+ ; // 定义了空白字符,后面的 skip 是一个特殊的标记,标记空白字符会被忽略 SPACE : ' ' -> skip ;
实现Visitor 生成Java文件之后,我们来实现自己的Visitor,用于支持BigDecimal。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 public class BigDecimalCalculationVisitor extends ExpressionBaseVisitor <BigDecimal> { private static final BigDecimal HUNDRED = BigDecimal.valueOf(100 ); private static final MathContext MATH_CONTEXT = MathContext.DECIMAL128; @Override public BigDecimal visitCalculationBlock (ExpressionParser.CalculationBlockContext ctx) { BigDecimal calcResult = null ; for (ExpressionParser.ExprContext expr : ctx.expr()) { calcResult = visit(expr); } return calcResult; } @Override public BigDecimal visitExpressionTimesOrDivide (ExpressionParser.ExpressionTimesOrDivideContext ctx) { BigDecimal left = visit(ctx.expr(0 )); BigDecimal right = visit(ctx.expr(1 )); switch (ctx.op.getType()) { case ExpressionLexer.TIMES: return left.multiply(right, MATH_CONTEXT); case ExpressionLexer.DIVIDE: return left.divide(right, MATH_CONTEXT); default : throw new RuntimeException ("unsupported operator type" ); } } @Override public BigDecimal visitExpressionPlusOrMinus (ExpressionParser.ExpressionPlusOrMinusContext ctx) { } @Override public BigDecimal visitExpressionWithBr (ExpressionParser.ExpressionWithBrContext ctx) { return visit(ctx.expr()); } @Override public BigDecimal visitExpressionNumeric (ExpressionParser.ExpressionNumericContext ctx) { BigDecimal numeric = numberOrPercent(ctx.num); if (Objects.nonNull(ctx.sign) && ctx.sign.getType() == ExpressionLexer.MINUS) { return numeric.negate(); } return numeric; } private BigDecimal numberOrPercent (Token num) { String numberStr = num.getText(); switch (num.getType()) { case ExpressionLexer.NUMBER: return decimal(numberStr); case ExpressionLexer.PERCENT_NUMBER: return decimal(numberStr.substring(0 , numberStr.length() - 1 ).trim()).divide(HUNDRED, MATH_CONTEXT); default : throw new RuntimeException ("unsupported number type" ); } } private BigDecimal decimal (String decimalStr) { return new BigDecimal (decimalStr); } }
调用解析器 在Calculator类中调用Expression。
1 2 3 4 5 6 7 8 9 10 11 public class Calculator { public BigDecimal execute (String expression) { CharStream cs = CharStreams.fromString(expression); ExpressionLexer lexer = new ExpressionLexer (cs); CommonTokenStream tokens = new CommonTokenStream (lexer); ExpressionParser parser = new ExpressionParser (tokens); ExpressionParser.CalcContext context = parser.calc(); BigDecimalCalculationVisitor visitor = new BigDecimalCalculationVisitor (); return visitor.visit(context); } }
测试 最后,我们用一个jUnit来测试一下我们的计算器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class CalculatorUnitTest { private final Calculator calculator = new Calculator (); @DisplayName("Test Calculator") @ParameterizedTest @CsvSource({ "1 + 2, 3", "3 - 2, 1", "2 * 3, 6", "6 / 3, 2", "6 / (1 + 2) , 2", "50%, 0.5", "100 * 30%, 30.0" }) void testCalculation (String expression, String expected) { assertEquals(expected, calculator.execute(expression).toPlainString()); } }
推荐阅读
Antlr4官方指南
Antlr4官方示例:Grammars-v4