ANTLR4 入门
- Java
- 2023-06-09
- 907热度
- 0评论
导航
简介
ANTLR(ANother Tool for Language Recognition)是一个强大的解析器生成器,它根据语法定义生成解析器,解析器可以构建和遍历解析树。
所有编程语言的语法,都可以用ANTLR来定义。ANTLR提供了大量的官方 grammar 示例。
核心概念
理解三个核心概念:Lexer、Parser、AST。
- 词法分析器(Lexer):将
字符序列
转换为单词(Token
) - 语法分析器(Parser):将单词(
Token
)转换为 AST - 抽象语法树(Abstract Syntax Tree,AST):源代码解析之后形成的树状结构,一般可以用来进行
代码语法的检查
,代码风格的检查
,代码的格式化
,代码的高亮
,代码的错误提示
以及代码的自动补全
等等
例如,对于以下的代码:
使用方法
从实战角度出发,我们直接使用 IDEA,在项目代码中进行操作。跟着我一步步来。
Step1 用 IDEA 创建一个空的 SpringBoot 项目
过程不赘述。
Step2 Maven 配置
引入依赖
<!-- antl4 --> <dependency> <groupId>org.antlr</groupId> <artifactId>antlr4-runtime</artifactId> <version>4.9.3</version> </dependency>
加入 ANTLR maven 插件
<plugin> <groupId>org.antlr</groupId> <artifactId>antlr4-maven-plugin</artifactId> <version>4.9.3</version> <executions> <execution> <id>antlr</id> <goals> <goal>antlr4</goal> </goals> </execution> </executions> <configuration> <arguments> <argument>-no-listener</argument> <argument>-visitor</argument> </arguments> <treatWarningsAsErrors>true</treatWarningsAsErrors> </configuration> </plugin>
Step3 定义语法和词法
需要创建一个.g4
文件,用于定义词法分析器(lexer)和语法解析器(Parser)。具体语法参见官方文档。
这个文件要放置到 src/main/antlr4 目录下(这是 maven 插件用的默认位置),然后根据需要的包名创建内层文件夹。
下面是一个简单的例子:Hello.g4
。
// file Hello.g4 // Define a grammar called Hello grammar Hello; // 1. grammer name r : 'hello' ID ; // 2. match keyword hello followed by an identifier ID : [a-z]+ ; // match lower-case identifiers WS : [ \t\r\n]+ -> skip ; // 3. skip spaces, tabs, newlines
- 定义了 grammar 的名字,名字需要与文件名对应
r
定义的语法,会使用到下方定义的正则表达式词法- 定义了空白字符,后面的 skip 是一个特殊的标记,标记空白字符会被忽略
Step4 生成解析器
执行 maven 的 antlr4 命令,生成解析器代码。生成的结果如图(如果没有识别为源码,需要手动将 antlr4 标记为源码目录):
- HelloBaseVisitor.java; // 实现 Visitor 的一个基类 我们业务可以继承这个类来访问 AST 实现自己需要的
- HelloLexer.java; // 词法解析器
- HelloParser.java; // 语法解析器
- HelloVisitor.java // Visitor 用于访问 AST (抽象语法树)
Step5 写一个测试类
public class HelloTest { public static void main(String[] args) { HelloLexer lexer = new HelloLexer(CharStreams.fromString("hello part")); CommonTokenStream tokens = new CommonTokenStream(lexer); HelloParser parser = new HelloParser(tokens); ParseTree tree = parser.r(); System.out.println(tree.toStringTree(parser)); } }
运行后得到输出:
(r hello part)
Step 6 实现 Visitor
ANTLR提供了 Listener 和 Visitor 两种方法来访问 ParseTree。最大的区别是Listener会自动访问 AST 的所有节点,而 Visitor 如果要访问当前节点的子树,则需要手工实现。
Visitor 较为简单方便,继承 HelloBaseVisitor 类即可。
创建一个 Visitor:
public class HelloVisitorImpl extends HelloBaseVisitor<String> { @Override public String visitR(HelloParser.RContext ctx) { return ctx.ID().getText(); } }
改写 HelloTest 的代码,加上 Visitor 调用逻辑:
public class HelloTest { public static void main(String[] args) { HelloLexer lexer = new HelloLexer(CharStreams.fromString("hello part")); CommonTokenStream tokens = new CommonTokenStream(lexer); HelloParser parser = new HelloParser(tokens); ParseTree tree = parser.r(); HelloVisitorImpl visitor = new HelloVisitorImpl(); String ans = visitor.visit(tree); System.out.println(ans); } }
输出的结果是
part
实践 | 做一个计算器
我们来实现一个简单的计算器。
Step1 定义语法和词法
创建 Calculator.g4,内容如下:
// 定义了 grammar 的名字,名字需要与文件名对应 grammar Calculator; /** * 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 ;
Step2 实现 Visitor
生成解析器代码之后,我们来实现 Visitor,支持 BigDecimal。BigDecimalCalculationVisitor,内容如下:
public class BigDecimalCalculationVisitor extends CalculatorBaseVisitor<BigDecimal> { /** * 100 */ private static final BigDecimal HUNDRED = BigDecimal.valueOf(100); /** * DECIMAL128のMathContext (桁数34、RoundingMode.HALF_EVEN) */ private static final MathContext MATH_CONTEXT = MathContext.DECIMAL128; @Override public BigDecimal visitCalculationBlock(CalculatorParser.CalculationBlockContext ctx) { BigDecimal calcResult = null; for (CalculatorParser.ExprContext expr : ctx.expr()) { calcResult = visit(expr); } return calcResult; } @Override public BigDecimal visitExpressionTimesOrDivide(CalculatorParser.ExpressionTimesOrDivideContext ctx) { BigDecimal left = visit(ctx.expr(0)); BigDecimal right = visit(ctx.expr(1)); switch (ctx.op.getType()) { case CalculatorLexer.TIMES: return left.multiply(right, MATH_CONTEXT); case CalculatorLexer.DIVIDE: return left.divide(right, MATH_CONTEXT); default: throw new RuntimeException("unsupported operator type"); } } @Override public BigDecimal visitExpressionPlusOrMinus(CalculatorParser.ExpressionPlusOrMinusContext ctx) { BigDecimal left = visit(ctx.expr(0)); BigDecimal right = visit(ctx.expr(1)); switch (ctx.op.getType()) { case CalculatorLexer.PLUS: return left.add(right, MATH_CONTEXT); case CalculatorLexer.MINUS: return left.subtract(right, MATH_CONTEXT); default: throw new RuntimeException("unsupported operator type"); } } @Override public BigDecimal visitExpressionWithBr(CalculatorParser.ExpressionWithBrContext ctx) { return visit(ctx.expr()); } @Override public BigDecimal visitExpressionNumeric(CalculatorParser.ExpressionNumericContext ctx) { BigDecimal numeric = numberOrPercent(ctx.num); if (Objects.nonNull(ctx.sign) && ctx.sign.getType() == CalculatorLexer.MINUS) { return numeric.negate(); } return numeric; } private BigDecimal numberOrPercent(Token num) { String numberStr = num.getText(); switch (num.getType()) { case CalculatorLexer.NUMBER: return decimal(numberStr); case CalculatorLexer.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); } }
Step3 业务类中调用解析器
public class Calculator { public BigDecimal execute(String expression) { CharStream cs = CharStreams.fromString(expression); CalculatorLexer lexer = new CalculatorLexer(cs); CommonTokenStream tokens = new CommonTokenStream(lexer); CalculatorParser parser = new CalculatorParser(tokens); CalculatorParser.CalcContext context = parser.calc(); BigDecimalCalculationVisitor visitor = new BigDecimalCalculationVisitor(); return visitor.visit(context); } }
Step4 测试用例
class CalculatorTest { 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()); } }