百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 文章教程 > 正文

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

xsobi 2024-12-06 20:28 19 浏览

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所示。

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

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

  • 基于需求按照 ANTLR4 的规则编写自定义语法的语义规则, 保存成以 g4 为后缀的文件。
  • 使用 ANTLR4 工具处理 g4 文件,生成词法分析器、句法分析器代码、词典文件。
  • 编写代码继承 Visitor 类或实现 Listener 接口,开发自己的业务逻辑代码。

Listener 模式和 Visitor 模式的区别

Listener 模式:

Visitor 模式:

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

2.代码工程

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

pom.xml

Bash
<?xml version="1.0" encoding="UTF-8"?>
<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">
    <parent>
        <artifactId>springboot-demo</artifactId>
        <groupId>com.et</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>ANTLR</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <antlr4.version>4.9.1</antlr4.version>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.antlr</groupId>
            <artifactId>antlr4-runtime</artifactId>
            <version>${antlr4.version}</version>
        </dependency>

    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.antlr</groupId>
                <artifactId>antlr4-maven-plugin</artifactId>
                <version>${antlr4.version}</version>
                <configuration>
                    <sourceDirectory>src/main/java</sourceDirectory>
                    <outputDirectory>src/main/java</outputDirectory>
                    <arguments>
                        <argument>-visitor</argument>
                        <argument>-listener</argument>
                    </arguments>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>antlr4</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

元语言LabeledExpr.g4

Bash
grammar LabeledExpr; // rename to distinguish from Expr.g4

prog:   stat+ ;

stat:   expr NEWLINE                # printExpr
    |   ID '=' expr NEWLINE         # assign
    |   NEWLINE                     # blank
    ;

expr:   expr op=('*'|'/') expr      # MulDiv
    |   expr op=('+'|'-') expr      # AddSub
    |   INT                         # int
    |   ID                          # id
    |   '(' expr ')'                # parens
    ;

MUL :   '*' ; // assigns token name to '*' used above in grammar
DIV :   '/' ;
ADD :   '+' ;
SUB :   '-' ;
ID  :   [a-zA-Z]+ ;      // match identifiers
INT :   [0-9]+ ;         // match integers
NEWLINE:'\r'? '\n' ;     // return newlines to parser (is end-statement signal)
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插件生成

<plugin>
    <groupId>org.antlr</groupId>
    <artifactId>antlr4-maven-plugin</artifactId>
    <version>${antlr4.version}</version>
    <configuration>
        <sourceDirectory>src/main/java</sourceDirectory>
        <outputDirectory>src/main/java</outputDirectory>
        <arguments>
            <argument>-visitor</argument>
            <argument>-listener</argument>
        </arguments>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>antlr4</goal>
            </goals>
        </execution>
    </executions>
</plugin>

执行命令

mvn antlr4:antlr4

使用ideal插件生成


实现运算逻辑

第一种:基于visitor实现

package com.et.antlr;

import java.util.HashMap;
import java.util.Map;

public class EvalVisitor extends LabeledExprBaseVisitor<Integer> {
    // Store variables (for assignment)
    Map<String, Integer> memory = new HashMap<>();

    /** stat : expr NEWLINE */
    @Override
    public Integer visitPrintExpr(LabeledExprParser.PrintExprContext ctx) {
        Integer value = visit(ctx.expr()); // evaluate the expr child
       // System.out.println(value);         // print the result
        return value;                          // return dummy value
    }

    /** stat : ID '=' expr NEWLINE */
    @Override
    public Integer visitAssign(LabeledExprParser.AssignContext ctx) {
        String id = ctx.ID().getText(); // id is left-hand side of '='
        int value = visit(ctx.expr());  // compute value of expression on right
        memory.put(id, value);          // store it in our memory
        return value;
    }

    /** expr : expr op=('*'|'/') expr */
    @Override
    public Integer visitMulDiv(LabeledExprParser.MulDivContext ctx) {
        int left = visit(ctx.expr(0));  // get value of left subexpression
        int right = visit(ctx.expr(1)); // get value of right subexpression
        if (ctx.op.getType() == LabeledExprParser.MUL) return left * right;
        return left / right; // must be DIV
    }

    /** expr : expr op=('+'|'-') expr */
    @Override
    public Integer visitAddSub(LabeledExprParser.AddSubContext ctx) {
        int left = visit(ctx.expr(0));  // get value of left subexpression
        int right = visit(ctx.expr(1)); // get value of right subexpression
        if (ctx.op.getType() == LabeledExprParser.ADD) return left + right;
        return left - right; // must be SUB
    }

    /** expr : INT */
    @Override
    public Integer visitInt(LabeledExprParser.IntContext ctx) {
        return Integer.valueOf(ctx.INT().getText());
    }

    /** expr : ID */
    @Override
    public Integer visitId(LabeledExprParser.IdContext ctx) {
        String id = ctx.ID().getText();
        if (memory.containsKey(id)) return memory.get(id);
        return 0; // default value if the variable is not found
    }

    /** expr : '(' expr ')' */
    @Override
    public Integer visitParens(LabeledExprParser.ParensContext ctx) {
        return visit(ctx.expr()); // return child expr's value
    }

    /** stat : NEWLINE */
    @Override
    public Integer visitBlank(LabeledExprParser.BlankContext ctx) {
        return 0; // return dummy value
    }
}

第二种:基于listener实现

package com.et.antlr;

import org.antlr.v4.runtime.tree.ParseTreeProperty;
import org.antlr.v4.runtime.tree.TerminalNode;

import java.util.HashMap;
import java.util.Map;

public class EvalListener extends LabeledExprBaseListener {
    // Store variables (for assignment)
    private final Map<String, Integer> memory = new HashMap<>();
    // Store expression results
    private final ParseTreeProperty<Integer> values = new ParseTreeProperty<>();
    private int result=0;
    @Override
    public void exitPrintExpr(LabeledExprParser.PrintExprContext ctx) {
        int value = values.get(ctx.expr());
        //System.out.println(value);
        result=value;
    }
    public int getResult() {
       return result;
    }
    @Override
    public void exitAssign(LabeledExprParser.AssignContext ctx) {
        String id = ctx.ID().getText();
        int value = values.get(ctx.expr());
        memory.put(id, value);
    }

    @Override
    public void exitMulDiv(LabeledExprParser.MulDivContext ctx) {
        int left = values.get(ctx.expr(0));
        int right = values.get(ctx.expr(1));
        if (ctx.op.getType() == LabeledExprParser.MUL) {
            values.put(ctx, left * right);
        } else {
            values.put(ctx, left / right);
        }
    }

    @Override
    public void exitAddSub(LabeledExprParser.AddSubContext ctx) {
        int left = values.get(ctx.expr(0));
        int right = values.get(ctx.expr(1));
        if (ctx.op.getType() == LabeledExprParser.ADD) {
            values.put(ctx, left + right);
        } else {
            values.put(ctx, left - right);
        }

    }

    @Override
    public void exitInt(LabeledExprParser.IntContext ctx) {
        int value = Integer.parseInt(ctx.INT().getText());
        values.put(ctx, value);
    }

    @Override
    public void exitId(LabeledExprParser.IdContext ctx) {
        String id = ctx.ID().getText();
        if (memory.containsKey(id)) {
            values.put(ctx, memory.get(id));
        } else {
            values.put(ctx, 0); // default value if the variable is not found
        }
    }

    @Override
    public void exitParens(LabeledExprParser.ParensContext ctx) {
        values.put(ctx, values.get(ctx.expr()));
    }
}

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

代码仓库

  • https://github.com/Harries/springboot-demo

3.测试

测试vistor方式

package com.et.antlr; /***
 * Excerpted from "The Definitive ANTLR 4 Reference",
 * published by The Pragmatic Bookshelf.
 * Copyrights apply to this code. It may not be used to create training material, 
 * courses, books, articles, and the like. Contact us if you are in doubt.
 * We make no guarantees that this code is fit for any purpose. 
 * Visit http://www.pragmaticprogrammer.com/titles/tpantlr2 for more book information.
***/
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.ParseTree;

import java.io.FileInputStream;
import java.io.InputStream;

public class CalcByVisit {
    public static void main(String[] args) throws Exception {
     /*   String inputFile = null;
        if ( args.length>0 ) inputFile = args[0];
        InputStream is = System.in;
        if ( inputFile!=null ) is = new FileInputStream(inputFile);*/
        ANTLRInputStream input = new ANTLRInputStream("1+2*3\n");
        LabeledExprLexer lexer = new LabeledExprLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        LabeledExprParser parser = new LabeledExprParser(tokens);
        ParseTree tree = parser.prog(); // parse

        EvalVisitor eval = new EvalVisitor();
        int result =eval.visit(tree);
        System.out.println(result);
    }
}

测试listener方式

package com.et.antlr;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * @author liuhaihua
 * @version 1.0
 * @ClassName CalbyLisenter
 * @Description todo
 * @date 2024年06月06日 16:40
 */

public class CalbyLisener {
    public static void main(String[] args) throws IOException {
      /*  String inputFile = null;
        if ( args.length>0 ) inputFile = args[0];
        InputStream is = System.in;
        if ( inputFile!=null ) is = new FileInputStream(inputFile);*/
        ANTLRInputStream input = new ANTLRInputStream("1+2*3\n");
        LabeledExprLexer lexer = new LabeledExprLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        LabeledExprParser parser = new LabeledExprParser(tokens);
        ParseTree tree = parser.prog(); // parse

        ParseTreeWalker walker = new ParseTreeWalker();
        EvalListener evalListener =new EvalListener();
        walker.walk(evalListener, tree);
        int result=evalListener.getResult();
        System.out.println(result);
    }
}

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

4.引用

  • https://www.antlr.org/
  • http://www.liuhaihua.cn/archives/710686.html

相关推荐

常见的15个shell脚本案例,请收藏

1.**HelloWorld**-最简单的脚本示例:#!/bin/bashecho"Hello,World!"2.**计算目录下文件数目**-统计当前目录下文件...

如何在Linux下的进行多进程编程(初步)

Linux下的多进程编程初步1引言对于没有接触过Unix/Linux操作系统的人来说,fork是最难理解的概念之一:它执行一次却返回两个值。fork函数是Unix系统最杰出的成就之一,它是七十年...

Linux系统Shell脚本编程之whiptail图形化工具设置网卡IP地址

摘要:在Linux环境里开发软件的时候,有时候得输入一长串的交互命令,才能拿到想要的数据或者得出想要的结果。可每次都得重新输入这些指令,实在是太机械、太麻烦啦!要是能有一种特别直观的交互式界面工具...

吃货必备:与“吃”有关的俚语

人们在日常生活和每日聊天的对话中总会用到俚语,能掌握一些地道的俚语表达,并将这些表达应用到口语中,定会为你的回答增色不少。下面给大家整理出了一些与“吃”有关的地道俚语,快来看看你了解多少?1.Top...

ROS2 Jazzy:启动多个节点

目标使用命令行工具一次性启动多个节点。背景知识在大多数入门教程里,每当运行一个新节点时,你都需要打开新的终端。当你构建更复杂的系统,同时运行越来越多的节点时,不断打开终端并重新输入配置细节会变得十分繁...

shell中如何确定脚本的位置?这篇文章告诉你

我想从同一个位置读取一些配置文件,如何确定脚本的位置?。这个问题的出现主要是由两个原因引发的:一是您希望将脚本的数据或配置进行外部化,因此需要一种方式来寻找这些外部资源;二是您的脚本需要对某些捆绑资源...

linux shell 脚本 入门到实战详解

—、shell入门简介1.1什么是shellshell脚本简介#为啥介绍shell上次出了一篇linux命令详解,得到了很多小伙伴的认可,有部分粉丝私信我,让我出一份shell编程。进过...

一文搞懂shell脚本

一文搞懂shell脚本1、shell脚本介绍什么是shell脚本一系列的shell命令的集合,还可以加入一些逻辑操作(ifelsefor)将这些命令放入一个文件中.什么是shell命令?ls...

风小逍陪你一起学习SHELL编程 - 重用和模块化

Shell编程中的函数编程指的是在Shell脚本中创建和使用函数以实现代码模块化和可重用性。Shell脚本是一种用于自动化任务和编写脚本的脚本语言,它在Unix和类Unix系统上广泛使...

Shell编程:编写sum函数,计算所有参数之和

Shell编程:编写sum函数,计算所有参数之和今天有网友求助,说是正在考试,有一道题不会做。题目不是很难:编写sum函数,计算所有参数之和。这道题的编码思路很简单,只涉及数学运算和shell参数。花...

shell常用命令之awk用法介绍

一、awk介绍awk的强大之处,在于能生成强大的格式化报告。数据可以来自标准输入,一个或多个文件,或者其他命令的输出。他支持用户自定义函数和动态正则表达式等先进功能,是Linux/unix一个强大的文...

Shell脚本编程:系统性能监控实战示例

Shell脚本编程:系统性能监控实战示例一、开篇:轻量级监控的艺术在服务器运维中,系统监控如同脉搏检测,而Shell脚本则是我们的"听诊器"。无需复杂的监控系统,一个精心编写的脚本就能...

Shell 脚本基础知识

作为一名DevOps工程师,您可能一直在寻找自动化任务和简化工作流程。这里就涉及到shell脚本了。Shell脚本编写的先决条件:Linux基础知识。让我们开始吧!什么是内核?Linux...

shell里面的一些特殊符号

特殊符号在linuxshell编程里非常常见各种符号说明当你编写Linuxshell脚本时,你会经常用到各种符号来完成不同的操作。下面是一些常用的Linuxshell编程符号及其说明:#...

运维必备!10 个实战的 Shell 脚本编程,酷到封神?

在运维日常工作中,shell脚本堪称运维神器。随着AI大模型的迅猛发展,其智能编码能力让脚本实现质的飞跃,大模型写出来的脚本,也需要经验丰富的运维工程师把关审核,确保脚本的安全性、稳定性和适配性...