• Spring Boot集成antlr实现词法和语法分析


    1.什么是antlr?

    Antlr4 是一款强大的语法生成器工具,可用于读取、处理、执行和翻译结构化的文本或二进制文件。基本上是当前 Java 语言中使用最为广泛的语法生成器工具。Twitter搜索使用ANTLR进行语法分析,每天处理超过20亿次查询;Hadoop生态系统中的Hive、Pig、数据仓库和分析系统所使用的语言都用到了ANTLR;Lex Machina将ANTLR用于分析法律文本;Oracle公司在SQL开发者IDE和迁移工具中使用了ANTLR;NetBeans公司的IDE使用ANTLR来解析C++;Hibernate对象-关系映射框架(ORM)使用ANTLR来处理HQL语言

    基本概念

    语法分析器(parser)是用来识别语言的程序,本身包含两个部分:词法分析器(lexer)和语法分析器(parser)。词法分析阶段主要解决的关键词以及各种标识符,例如 INT、ID 等,语法分析主要是基于词法分析的结果,构造一颗语法分析树。大致的流程如下图参考2所示。

    concept

      因此,为了让词法分析和语法分析能够正常工作,在使用 Antlr4 的时候,需要定义语法(grammar),这部分就是 Antlr 元语言。

    parser-tree

       

    使用 ANTLR4 编程的基本流程是固定的,通常分为如下三步:

    • 基于需求按照 ANTLR4 的规则编写自定义语法的语义规则, 保存成以 g4 为后缀的文件。

    • 使用 ANTLR4 工具处理 g4 文件,生成词法分析器、句法分析器代码、词典文件。

    • 编写代码继承 Visitor 类或实现 Listener 接口,开发自己的业务逻辑代码。

    Listener 模式和 Visitor 模式的区别

    Listener 模式:

    49

    Visitor 模式:

    56

     

    • Listener 模式通过 walker 对象自行遍历,不用考虑其语法树上下级关系。Vistor 需要自行控制访问的子节点,如果遗漏了某个子节点,那么整个子节点都访问不到了。
    • Listener 模式的方法没有返回值,Vistor 模式可以设定任意返回值。
    • Listener 模式的访问栈清晰明确,Vistor 模式是方法调用栈,如果实现出错有可能导致 StackOverFlow。

    2.代码工程

    实验目的:实现基于antlr的计算器

    pom.xml

    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <project xmlns="http://maven.apache.org/POM/4.0.0"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    5. <parent>
    6. <artifactId>springboot-demo</artifactId>
    7. <groupId>com.et</groupId>
    8. <version>1.0-SNAPSHOT</version>
    9. </parent>
    10. <modelVersion>4.0.0</modelVersion>
    11. <artifactId>ANTLR</artifactId>
    12. <properties>
    13. <maven.compiler.source>8</maven.compiler.source>
    14. <maven.compiler.target>8</maven.compiler.target>
    15. <antlr4.version>4.9.1</antlr4.version>
    16. </properties>
    17. <dependencies>
    18. <dependency>
    19. <groupId>org.springframework.boot</groupId>
    20. <artifactId>spring-boot-starter-web</artifactId>
    21. </dependency>
    22. <dependency>
    23. <groupId>org.springframework.boot</groupId>
    24. <artifactId>spring-boot-autoconfigure</artifactId>
    25. </dependency>
    26. <dependency>
    27. <groupId>org.springframework.boot</groupId>
    28. <artifactId>spring-boot-starter-test</artifactId>
    29. <scope>test</scope>
    30. </dependency>
    31. <dependency>
    32. <groupId>org.antlr</groupId>
    33. <artifactId>antlr4-runtime</artifactId>
    34. <version>${antlr4.version}</version>
    35. </dependency>
    36. </dependencies>
    37. <build>
    38. <plugins>
    39. <plugin>
    40. <groupId>org.antlr</groupId>
    41. <artifactId>antlr4-maven-plugin</artifactId>
    42. <version>${antlr4.version}</version>
    43. <configuration>
    44. <sourceDirectory>src/main/java</sourceDirectory>
    45. <outputDirectory>src/main/java</outputDirectory>
    46. <arguments>
    47. <argument>-visitor</argument>
    48. <argument>-listener</argument>
    49. </arguments>
    50. </configuration>
    51. <executions>
    52. <execution>
    53. <goals>
    54. <goal>antlr4</goal>
    55. </goals>
    56. </execution>
    57. </executions>
    58. </plugin>
    59. </plugins>
    60. </build>
    61. </project>

    元语言LabeledExpr.g4

    1. grammar LabeledExpr; // rename to distinguish from Expr.g4
    2. prog: stat+ ;
    3. stat: expr NEWLINE # printExpr
    4. | ID '=' expr NEWLINE # assign
    5. | NEWLINE # blank
    6. ;
    7. expr: expr op=('*'|'/') expr # MulDiv
    8. | expr op=('+'|'-') expr # AddSub
    9. | INT # int
    10. | ID # id
    11. | '(' expr ')' # parens
    12. ;
    13. MUL : '*' ; // assigns token name to '*' used above in grammar
    14. DIV : '/' ;
    15. ADD : '+' ;
    16. SUB : '-' ;
    17. ID : [a-zA-Z]+ ; // match identifiers
    18. INT : [0-9]+ ; // match integers
    19. NEWLINE:'\r'? '\n' ; // return newlines to parser (is end-statement signal)
    20. WS : [ \t]+ -> skip ; // toss out whitespace

    简单解读一下 LabeledExpr.g4 文件。ANTLR4 规则是基于正则表达式定义定义。规则的理解是自顶向下的,每个分号结束的语句表示一个规则 。例如第一行:grammar LabeledExpr; 表示我们的语法名称是 LabeledExpr, 这个名字需要跟文件名需要保持一致。Java 编码也有相似的规则:类名跟类文件一致。

    • 规则 prog 表示 prog 是一个或多个 stat。
    • 规则 stat 适配三种子规则:空行、表达式 expr、赋值表达式 ID’=’expr。
    • 表达式 expr 适配五种子规则:乘除法、加减法、整型、ID、括号表达式。很显然,这是一个递归的定义。

    最后定义的是组成复合规则的基础元素,比如:规则 ID: [a-zA-Z]+表示 ID 限于大小写英文字符串;INT: [0-9]+; 表示 INT 这个规则是 0-9 之间的一个或多个数字,当然这个定义其实并不严格。再严格一点,应该限制其长度。

    在理解正则表达式的基础上,ANTLR4 的 g4 语法规则还是比较好理解的。

    定义 ANTLR4 规则需要注意一种情况,即可能出现一个字符串同时支持多种规则,如以下的两个规则:

    ID: [a-zA-Z]+;

    FROM: ‘from’;

    很明显,字符串” from”同时满足上述两个规则,ANTLR4 处理的方式是按照定义的顺序决定。这里 ID 定义在 FROM 前面,所以字符串 from 会优先匹配到 ID 这个规则上。

    其实在定义好与法规中,编写完成 g4 文件后,ANTLR4 已经为我们完成了 50%的工作:帮我们实现了整个架构及接口了,剩下的开发工作就是基于接口或抽象类进行具体的实现。实现上有两种方式来处理生成的语法树,其一 Visitor 模式,另一种方式是 Listener(监听器模式)。

    生成词法和语法解析器

    基于maven插件生成

    1. <plugin>
    2. <groupId>org.antlr</groupId>
    3. <artifactId>antlr4-maven-plugin</artifactId>
    4. <version>${antlr4.version}</version>
    5. <configuration>
    6. <sourceDirectory>src/main/java</sourceDirectory>
    7. <outputDirectory>src/main/java</outputDirectory>
    8. <arguments>
    9. <argument>-visitor</argument>
    10. <argument>-listener</argument>
    11. </arguments>
    12. </configuration>
    13. <executions>
    14. <execution>
    15. <goals>
    16. <goal>antlr4</goal>
    17. </goals>
    18. </execution>
    19. </executions>
    20. </plugin>

    执行命令

    mvn antlr4:antlr4

    802

    使用ideal插件生成

    847

    100%

    实现运算逻辑

    第一种:基于visitor实现

    1. package com.et.antlr;
    2. import java.util.HashMap;
    3. import java.util.Map;
    4. public class EvalVisitor extends LabeledExprBaseVisitor<Integer> {
    5. // Store variables (for assignment)
    6. Map<String, Integer> memory = new HashMap<>();
    7. /** stat : expr NEWLINE */
    8. @Override
    9. public Integer visitPrintExpr(LabeledExprParser.PrintExprContext ctx) {
    10. Integer value = visit(ctx.expr()); // evaluate the expr child
    11. // System.out.println(value); // print the result
    12. return value; // return dummy value
    13. }
    14. /** stat : ID '=' expr NEWLINE */
    15. @Override
    16. public Integer visitAssign(LabeledExprParser.AssignContext ctx) {
    17. String id = ctx.ID().getText(); // id is left-hand side of '='
    18. int value = visit(ctx.expr()); // compute value of expression on right
    19. memory.put(id, value); // store it in our memory
    20. return value;
    21. }
    22. /** expr : expr op=('*'|'/') expr */
    23. @Override
    24. public Integer visitMulDiv(LabeledExprParser.MulDivContext ctx) {
    25. int left = visit(ctx.expr(0)); // get value of left subexpression
    26. int right = visit(ctx.expr(1)); // get value of right subexpression
    27. if (ctx.op.getType() == LabeledExprParser.MUL) return left * right;
    28. return left / right; // must be DIV
    29. }
    30. /** expr : expr op=('+'|'-') expr */
    31. @Override
    32. public Integer visitAddSub(LabeledExprParser.AddSubContext ctx) {
    33. int left = visit(ctx.expr(0)); // get value of left subexpression
    34. int right = visit(ctx.expr(1)); // get value of right subexpression
    35. if (ctx.op.getType() == LabeledExprParser.ADD) return left + right;
    36. return left - right; // must be SUB
    37. }
    38. /** expr : INT */
    39. @Override
    40. public Integer visitInt(LabeledExprParser.IntContext ctx) {
    41. return Integer.valueOf(ctx.INT().getText());
    42. }
    43. /** expr : ID */
    44. @Override
    45. public Integer visitId(LabeledExprParser.IdContext ctx) {
    46. String id = ctx.ID().getText();
    47. if (memory.containsKey(id)) return memory.get(id);
    48. return 0; // default value if the variable is not found
    49. }
    50. /** expr : '(' expr ')' */
    51. @Override
    52. public Integer visitParens(LabeledExprParser.ParensContext ctx) {
    53. return visit(ctx.expr()); // return child expr's value
    54. }
    55. /** stat : NEWLINE */
    56. @Override
    57. public Integer visitBlank(LabeledExprParser.BlankContext ctx) {
    58. return 0; // return dummy value
    59. }
    60. }

    第二种:基于listener实现

    1. package com.et.antlr;
    2. import org.antlr.v4.runtime.tree.ParseTreeProperty;
    3. import org.antlr.v4.runtime.tree.TerminalNode;
    4. import java.util.HashMap;
    5. import java.util.Map;
    6. public class EvalListener extends LabeledExprBaseListener {
    7. // Store variables (for assignment)
    8. private final Map<String, Integer> memory = new HashMap<>();
    9. // Store expression results
    10. private final ParseTreeProperty<Integer> values = new ParseTreeProperty<>();
    11. private int result=0;
    12. @Override
    13. public void exitPrintExpr(LabeledExprParser.PrintExprContext ctx) {
    14. int value = values.get(ctx.expr());
    15. //System.out.println(value);
    16. result=value;
    17. }
    18. public int getResult() {
    19. return result;
    20. }
    21. @Override
    22. public void exitAssign(LabeledExprParser.AssignContext ctx) {
    23. String id = ctx.ID().getText();
    24. int value = values.get(ctx.expr());
    25. memory.put(id, value);
    26. }
    27. @Override
    28. public void exitMulDiv(LabeledExprParser.MulDivContext ctx) {
    29. int left = values.get(ctx.expr(0));
    30. int right = values.get(ctx.expr(1));
    31. if (ctx.op.getType() == LabeledExprParser.MUL) {
    32. values.put(ctx, left * right);
    33. } else {
    34. values.put(ctx, left / right);
    35. }
    36. }
    37. @Override
    38. public void exitAddSub(LabeledExprParser.AddSubContext ctx) {
    39. int left = values.get(ctx.expr(0));
    40. int right = values.get(ctx.expr(1));
    41. if (ctx.op.getType() == LabeledExprParser.ADD) {
    42. values.put(ctx, left + right);
    43. } else {
    44. values.put(ctx, left - right);
    45. }
    46. }
    47. @Override
    48. public void exitInt(LabeledExprParser.IntContext ctx) {
    49. int value = Integer.parseInt(ctx.INT().getText());
    50. values.put(ctx, value);
    51. }
    52. @Override
    53. public void exitId(LabeledExprParser.IdContext ctx) {
    54. String id = ctx.ID().getText();
    55. if (memory.containsKey(id)) {
    56. values.put(ctx, memory.get(id));
    57. } else {
    58. values.put(ctx, 0); // default value if the variable is not found
    59. }
    60. }
    61. @Override
    62. public void exitParens(LabeledExprParser.ParensContext ctx) {
    63. values.put(ctx, values.get(ctx.expr()));
    64. }
    65. }

    以上只是一些关键代码,所有代码请参见下面代码仓库

    代码仓库

    3.测试

    测试vistor方式

    1. package com.et.antlr; /***
    2. * Excerpted from "The Definitive ANTLR 4 Reference",
    3. * published by The Pragmatic Bookshelf.
    4. * Copyrights apply to this code. It may not be used to create training material,
    5. * courses, books, articles, and the like. Contact us if you are in doubt.
    6. * We make no guarantees that this code is fit for any purpose.
    7. * Visit http://www.pragmaticprogrammer.com/titles/tpantlr2 for more book information.
    8. ***/
    9. import org.antlr.v4.runtime.*;
    10. import org.antlr.v4.runtime.tree.ParseTree;
    11. import java.io.FileInputStream;
    12. import java.io.InputStream;
    13. public class CalcByVisit {
    14. public static void main(String[] args) throws Exception {
    15. /* String inputFile = null;
    16. if ( args.length>0 ) inputFile = args[0];
    17. InputStream is = System.in;
    18. if ( inputFile!=null ) is = new FileInputStream(inputFile);*/
    19. ANTLRInputStream input = new ANTLRInputStream("1+2*3\n");
    20. LabeledExprLexer lexer = new LabeledExprLexer(input);
    21. CommonTokenStream tokens = new CommonTokenStream(lexer);
    22. LabeledExprParser parser = new LabeledExprParser(tokens);
    23. ParseTree tree = parser.prog(); // parse
    24. EvalVisitor eval = new EvalVisitor();
    25. int result =eval.visit(tree);
    26. System.out.println(result);
    27. }
    28. }

    测试listener方式

    1. package com.et.antlr;
    2. import org.antlr.v4.runtime.ANTLRInputStream;
    3. import org.antlr.v4.runtime.CommonTokenStream;
    4. import org.antlr.v4.runtime.tree.ParseTree;
    5. import org.antlr.v4.runtime.tree.ParseTreeWalker;
    6. import java.io.FileInputStream;
    7. import java.io.IOException;
    8. import java.io.InputStream;
    9. /**
    10. * @author liuhaihua
    11. * @version 1.0
    12. * @ClassName CalbyLisenter
    13. * @Description todo
    14. * @date 2024年06月06日 16:40
    15. */
    16. public class CalbyLisener {
    17. public static void main(String[] args) throws IOException {
    18. /* String inputFile = null;
    19. if ( args.length>0 ) inputFile = args[0];
    20. InputStream is = System.in;
    21. if ( inputFile!=null ) is = new FileInputStream(inputFile);*/
    22. ANTLRInputStream input = new ANTLRInputStream("1+2*3\n");
    23. LabeledExprLexer lexer = new LabeledExprLexer(input);
    24. CommonTokenStream tokens = new CommonTokenStream(lexer);
    25. LabeledExprParser parser = new LabeledExprParser(tokens);
    26. ParseTree tree = parser.prog(); // parse
    27. ParseTreeWalker walker = new ParseTreeWalker();
    28. EvalListener evalListener =new EvalListener();
    29. walker.walk(evalListener, tree);
    30. int result=evalListener.getResult();
    31. System.out.println(result);
    32. }
    33. }

    运行上述测试用例,计算结果符合预期

    4.引用

  • 相关阅读:
    给文件添加可读可写可执行权限
    C语言c89(c90)的所有的32个关键字分类
    决策树可视化方法与技巧汇总(1)(分类决策树)(含Python代码示例)
    osg实现三次样条Cardinal曲线
    【全网最细致】SpringBoot整合Spring Security + JWT实现用户认证
    【ASM】字节码操作 MethodWriter
    API网关之Nginx作为网关的优势及实战
    C#替换字符串中花括号的参数
    如何进行前后端交互
    【云原生】Docker小工具:runlike与whaler(打印容器的启动命令与导出镜像的dockerfile)
  • 原文地址:https://blog.csdn.net/dot_life/article/details/139641019