ANTLR4规则解析生成器(三):遍历语法分析树

news2024/10/7 20:25:38

文章目录

      • 1 词法分析
      • 2 语法分析
      • 3 遍历语法分析树
        • 3.1 Listener
        • 3.2 Visitor
      • 4 总结

1 词法分析

词法分析就是对给定的字符串进行分割,提取出其中的单词。

在antlr4中,词法规则的名称的首字母需要大写,右侧必须是终结符,通常将词法规则的名称全部大写。

例如,要匹配C语言中的变量名,就需要知道C语言中的变量名的规范:

  • 变量只能由字母、数字、下划线组成
  • 变量名的第一个字符必须是字母或者下划线,不能是数字

于是,变量名的词法规则就可以是:

VARNAME: [a-zA-Z_]+[a-zA-Z_0-9]*;

antlr4提供关键字fragment用于定义词法片段,可以把比较常用的正则表达式的一部分定义为片段进行复用,它本身并不会生成任何标记,但是能够提高可读性。

fragment DIGIT : [0-9]
fragment ALPHABET: [a-zA-Z]

UNDERLINE: '_';

VARNAME: (ALPHABET|UNDERLINE)+(ALPHABET|UNDERLINE|DIGIT)*

首先分别定义字母和数字的fragment,然后定义下划线的词法规则,再结合字母、数字、下划线定义变量名的词法规则。

2 语法分析

词法分析得到单词流后,就可以通过语法分析生成语法分析树。

语法规则的民成的首字母需要小写,以区分词法规则,通常将语法规则的名称全部小写。

例如,要匹配一个赋值表达式,就需要知道赋值表达式由哪些部分构成。

赋值表达式通常由四部分构成:

  • 等号左侧,左侧通常是变量名,但是也可能在定义变量时进行初始化,不过也可能赋值给指针
  • 等号
  • 等号右侧,右侧可能是常量,也可能是函数调用
  • 表达式结尾的分号

这里为了简化处理,只实现以下的赋值语句:

  • 等号左侧只是变量名
  • 等号右侧只有整数常量

语法规则就是:

ASSIGN: '=';
SEMI: ';';
NUMBER: DIGIT+;

assign_expr: VARNAME ASSIGN NUMBER SEMI;

3 遍历语法分析树

在编写antlr4的语法规则文件时,可以在其中加入特定语言的动作,例如,需要在访问到某个规则的时候输出得到的字符串,就可以直接将该逻辑写在语法规则文件中,但是这种方式会使得语法规则文件和解析程序耦合,不同语言需要不同的节点访问逻辑,因此,编写完成语法规则文件后,通常生成对应语言的解析程序。利用该解析程序可以实现对语法分析树的遍历。

下一步就是对语法分析树进行遍历,为了方便对,antlr4提供了两种遍历语法分析树的模式:

  • Listener
  • Visitor

在使用antlr4工具生成对应语言的程序时,可以通过-listener-visitor选项生成对应的遍历语法分析树的代码,-listener是默认选项,也就是默认得到Listener类型的遍历器。

在说明Listener和Visitor之前需要了解下语法分析树的结构和相关类型,以下的{GrammarName}表示语法名称,也就是grammar语句中的名称,{RuleName}表示语法文件中的规则名称,也就是冒号前面的单词。

antlr4为我们生成的解析器{GrammarName}Parser继承自antlr4.Parser{GrammarName}Parser中会对每条规则定义一个类,类继承自antlr4.ParserRuleContext,例如,如果规则名是expr,就会创建一个类:class ExprContext(ParserRuleContext),同时对每条规则也会创建一个对应的方法,用于返回对应的类对象,例如,如果规则名是expr,那么方法就是def expr(self),该方法会返回ExprContext的对象。在创建{GrammarName}Parser对象时需要传递antlr4.TokenStream作为参数,antlr4.TokenStream可以理解为词法分析的单词流,解析器就是对单词流进行分析。

请添加图片描述

在创建{GrammarName}Parser解析器对象后,就可以通过第一条规则的规则名作为方法名得到语法分析树的根节点,接下来就可以通过Listener或者Visitor机制遍历该语法分析树。语法分析树的每个节点对应一条规则,都是继承自antlr4.ParserRuleContext的对象,而叶子节点则是词法分析的单词,类型是TerminalNodeImpl

请添加图片描述

因此,在实际的开发者解析语法分析树的主函数代码通常是:

# 使用输入的字符串创建输入流,然后将输入流给到词法分析器
# 使用词法分析器就得到TokenStream对象
input_stream = InputStream("10+1+1+2\n")
lexer = exprLexer(input_stream)
token_stream = CommonTokenStream(lexer)

# 使用TokenStream对象创建antlr4为我们生成的解析器对象
parser = exprParser(token_stream)

# prog就是g4语法规则文件的第一条规则
# 因此,调用解析器的prog方法得到语法分析树的根节点
# 在其他程序中也需要使用第一条规则的规则名进行调用
tree = parser.prog()

# 此处使用Listenser的方式遍历得到的语法分析树
listener = Listener()
walker = ParseTreeWalker()
walker.walk(listener, tree)
3.1 Listener

Listener的基本思想是给树的节点定义回调函数,当访问语法分析树时自动调用定义的回调函数,实现树的自动访问。

在使用antlr4工具生成对应语言的程序时,会默认生成{GrammarName}Listener.py的代码文件(这里以python为例)。

{GrammarName}Listener.py的文件中会生成一个继承自antlr4.tree.Tree.ParseTreeListener{GrammarName}Listener类,该类中会每条规则定义了两个方法:

  • enter{RuleName}()方法:例如,规则名为expr,则方法名为enterExpr,参数为{GrammarName}Parser.ExprContext
  • exit{RuleName}()方法:例如,规则名为expr,则方法名为exitExpr,参数为{GrammarName}Parser.ExprContext

看方法名可以知道,enterExpr()就是在访问到expr这个规则的子树时调用的方法,函数的参数就是子树的树根,exitExpr()就是在访问完expr这个子树时调用的方法,函数的参数就是子树的树根。

一般情况下,开发者不会去修改antlr4生成的Listener文件,而是重新继承{GrammarName}Listener,然后在其中重写enter/exit方法。

在enter/exit方法中,参数是当前遍历的节点,因此,可以使用继承自antlr4.ParserRuleContext的方法访问当前节点的属性和数据:

  • getChild(i):获取第i个子节点
  • getChildren():获取所有孩子节点
  • getChildCount():获取孩子节点的个数
  • getText():获取节点的字符串,通常只会在叶子节点才会调用,中间节点调用得到的就是本条规则匹配的字符串
  • getParent():获取父节点,只有叶子节点才能调用
  • depth():获取当前节点在树中的深度

由于参数是当前节点,并且没有返回值,无法将当前节点的处理后得到的数据往上传递,因此,如果在遍历语法分析树时需要进行数据的传递,需要使用额外的数据结构,例如第一篇文章中的计算器程序,在得到当前节点计算的值后将数据保存到字典中,在处理上层节点时再从字典中读取。

对于Listener,需要使用antlr4.tree.Tree.ParseTreeWalker进行遍历,首先创建一个ParseTreeWalker的对象,然后调用对象的walk()函数遍历,函数的参数是ParseTreeListenerParseTree,在每个节点类型的ParserRuleContext的派生类{RuleName}Context中,都会有定义两个函数enterRuleexitRule,在调用walk()时其实就会用深度优先遍历的方式遍历每个节点,然后在开始遍历孩子节点前执行enterRule,在遍历完成孩子节点后执行exitRule

class ParseTreeWalker(object):

    DEFAULT = None

    def walk(self, listener:ParseTreeListener, t:ParseTree):
        """
        对树执行深度优先遍历,
        在遍历当前节点的孩子节点之前,先判断当前节点是否是叶子节点,这里分为错误节点和正确的叶子节点,
        分别调用listener的visitErrorNode和visitTerminal,因此,可以在我们自己的Listener中
        增加visitErrorNode和visitTerminal,用于处理叶子节点
        在遍历孩子节点之前,先执行enterRule,在遍历完所有孩子节点后执行exitRule
        """
        if isinstance(t, ErrorNode):
            listener.visitErrorNode(t)
            return
        elif isinstance(t, TerminalNode):
            listener.visitTerminal(t)
            return
        self.enterRule(listener, t)
        for child in t.getChildren():
            self.walk(listener, child)
        self.exitRule(listener, t)
    
    def enterRule(self, listener:ParseTreeListener, r:RuleNode):
        """
	    执行当前节点的enterRule
        """
        ctx = r.getRuleContext()
        listener.enterEveryRule(ctx)
        ctx.enterRule(listener)

    def exitRule(self, listener:ParseTreeListener, r:RuleNode):
        """
        执行当前节点的exitRule
        """
        ctx = r.getRuleContext()
        ctx.exitRule(listener)
        listener.exitEveryRule(ctx)

可以看到,使用这种方式的好处是,开发者只需要编写遍历节点的回调函数,即可自动实现树的遍历,但是没办法控制遍历过程以及实现值的传递。

3.2 Visitor

将g4文件使用命令antlr4 -Dlanguage=Python3 -visitor -no-listener expr.g4就可以得到只有Visitor的代码,生成的代码与Listener的区别就是有一个exprVisitor.py的文件。

exprVisitor.py中是默认生成的vistor:exprVisitorexprVisitor继承自antlr4.tree.Tree.ParseTreeVisitorexprVisitor中对于每条规则会生成一个visitor{RuleName}()方法,参数还是{RuleName}Context

与Listener有所不同,Visitor模式下{RuleName}Context中没有定义enter/exit方法,而是定义了accept(ParseTreeVisitor)方法:

def accept(self, visitor:ParseTreeVisitor):
    if hasattr( visitor, "visitProg" ):
        return visitor.visitProg(self)
    else:
        return visitor.visitChildren(self)

accept(ParseTreeVisitor)中,如果ParseTreeVisitor中有visit{RuleName}方法,就会调用visit{RuleName},否则,就会访问孩子节点。

class ParseTreeVisitor(object):

    # 提供给外部调用的函数
    # 参数就是树的根节点
    def visit(self, tree):
        # 调用的就是节点的accept()函数
        return tree.accept(self)
    
    # 访问孩子节点
    def visitChildren(self, node):
        result = self.defaultResult()
        n = node.getChildCount()
        for i in range(n):
            if not self.shouldVisitNextChild(node, result):
                return result

            c = node.getChild(i)
            childResult = c.accept(self)
            result = self.aggregateResult(result, childResult)

        return result

    def visitTerminal(self, node):
        return self.defaultResult()

    def visitErrorNode(self, node):
        return self.defaultResult()

    def defaultResult(self):
        return None

    def aggregateResult(self, aggregate, nextResult):
        return nextResult

    def shouldVisitNextChild(self, node, currentResult):
        return True

开发者需要基于exprVisitor创建自己的Visitor,重写里面的visitor{RuleName}()方法,在主逻辑中就可以调用Visitor.visit()访问语法分析树:

# 使用输入的字符串创建输入流,然后将输入流给到词法分析器
# 使用词法分析器就得到TokenStream对象
input_stream = InputStream("10+1+1+2\n")
lexer = exprLexer(input_stream)
token_stream = CommonTokenStream(lexer)

# 使用TokenStream对象创建antlr4为我们生成的解析器对象
parser = exprParser(token_stream)

# prog就是g4语法规则文件的第一条规则
# 因此,调用解析器的prog方法得到语法分析树的根节点
# 在其他程序中也需要使用第一条规则的规则名进行调用
tree = parser.prog()

# 创建Visitor,并调用visit访问tree
visitor = Visitor()
print(visitor.visit(tree))

下面是一个简易的计算器程序的Visitor,visitExpr就是访问表达式,如果这个表达式只有一个孩子节点,说明它的孩子节点应该是个叶子节点,也就是经过词法分析匹配得到的操作数,则直接返回该操作数即可,如果这个表达式有3个节点,说明当前子树是个需要计算的表达式,访问第1个孩子,获取操作符左边的值,访问第3个孩子,获取操作符右边的值,然后根据第2个孩子的值进行对应的计算并返回。最后,在主逻辑中打印visitor.visit(tree)就可以得到表达式最终的结果。

class Visitor(exprVisitor):

    def visitProg(self, ctx:exprParser.ProgContext):
        if ctx.getChildCount() == 2:
            return self.visit(ctx.getChild(0))
        return self.visitChildren(ctx)

    def visitExpr(self, ctx:exprParser.ExprContext):
        if ctx.getChildCount() == 1:
            return ctx.getChild(0).getText()
        elif ctx.getChildCount() == 3:
            left = self.visit(ctx.getChild(0))
            right = self.visit(ctx.getChild(2))
            if ctx.getChild(1).getText() == "+":
                return int(left) + int(right)
            elif ctx.getChild(1).getText() == "-":
                return int(left) - int(right)
            elif ctx.getChild(1).getText() == "*":
                return int(left) * int(right)
            elif ctx.getChild(1).getText() == "/":
                return int(left) / int(right)

        return self.visitChildren(ctx)

可以看到跟Listener的区别在于,使用Visitor的方式可以自己控制是否访问子树,并且可以得到子树的值。

4 总结

  • 定义好词法和语法规则,就可以对提供的输入串进行词法分析和语法分析,得到语法分析树
  • 语法分析树的非叶子节点的类型是{RuleName}Context,叶子节点的类型为TerminalNodeImpl,然后使用解析器的第一条规则名作为函数得到树的根节点
  • antlr4提供了Listener和Visitor两种机制遍历语法分析树
  • Listener中生成的{RuleName}Context中会增加enterRule()exitRule()方法,它们分别调用listener中定义的enter{RuleName}exit{RuleName}方法,ParseTreeWalker.walk(ParseTreeListener, ParseTree)使用深度优先搜索遍历树的根节点,在遍历当前节点的孩子节点之前会调用当前节点的enterRule()方法,在遍历完当前节点的孩子节点后会调用当前节点的exitRule()方法,从而来调用listener中定义的enter{RuleName}exit{RuleName}方法。这种方式适合不需要传递值的场景,例如,语法检查
  • Visitor中生成的{RuleName}Context中会增加accept(ParseTreeVisitor),在使用ParseTreeVisitor.visit(ParseTree)遍历语法分析树时,就会访问根节点的accept方法,从而访问Visitor中的visit{RuleName}方法,开发人员在重写visit{RuleName}方法时可以使用visit方法得到子树的值,这种方式适合需要在子树和父节点传递值的场景

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1479457.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Tomcat布署及优化

1.Tomcat简介 Tomcat 是 Java 语言开发的,Tomcat 服务器是一个免费的开放源代码的 Web 应用服务器,Tomcat 属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试 JSP 程序的首选。一般来说&…

数据结构:队列 || oj(l两个队列实现栈)

[TOC](数据结构:队列 || oj(l两个队列实现栈)) 一、队列的概念 1.什么是队列? //先说一下,队列跟栈一样都是很重要的数据结构,重要的不是说这个数据结构怎么实现,重要的是结构的优势! //栈:是…

上海亚商投顾:沪指终结月线6连阴 北向资金净买入超160亿

上海亚商投顾前言:无惧大盘涨跌,解密龙虎榜资金,跟踪一线游资和机构资金动向,识别短期热点和强势个股。 一.市场情绪 三大指数昨日低开高走,沪指重新站上3000点,深成指、创业板指大涨超3%。半导体产业链全…

协议和序列化反序列化

“协议”和序列化反序列化 “协议”的概念: “协议”本身是一种约定俗成的东西,由通讯双方必须共同遵从的一组约定,因此我们一定要将这种约定用计算机语言表达出来,此时双方计算机才能识别约定的相关内容 我们把这个规矩叫做“…

仿真科普|CAE技术赋能无人机 低空经济蓄势起飞

喝一杯无人机送来的现磨热咖啡;在拥堵的早高峰打个“空中的士”上班;乘坐水陆两栖飞机来一场“陆海空”立体式观光……曾经只出现在科幻片里的5D城市魔幻场景,正逐渐走进现实。而推动上述场景实现的,就是近年来越来越热的“低空经…

包管理工具之npm也慌了?

起因 因为npm的种种问题,我很早就换成了pnpm和yarn(但是其实npm也在使用),已经很久没有关注npm的功能更新了。最近无意间进入Node18版本的安装目录,发现其除了常规的node,npm等默认安装了一个新的包corepack,这个就是今天我要分享的东西了。 注: 我因为18版本的node上…

Docker技术概论(3):Docker 中的基本概念

Docker技术概论(3) Docker 中的基本概念 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at: https://jclee95.blog.csdn.netMy WebSite:http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://…

MySQL 用户账号迁移

文章目录 前言1. 工具安装1.1 下载安装包1.2 编译安装 2. 用户迁移后记 前言 有一个典型的使用场景,就是 RDS 下云大多数都是通过 DTS 进行数据传输的,用户是不会同步到自建数据库的。需要运维人员在自建数据库重新创建用户,如果用户数量很多…

kyuubi整合spark on yarn

目录 概述实践下载配置启动 结束 概述 目标: 1.实现kyuubi spark on yarn2.实现 kyuubi spark on yarn 资源的动态分配 注意:版本 kyuubi 1.8.0 、 spark 3.4.2 、hadoop 3.3.6 前置准备请看如下文章 文章链接hadoop一主三从安装链接spark on yarn链接 实践 …

【音视频处理】使用ffmpeg实现多个视频合成一个视频(按宫格视图)

先上结果 环境 硬件:通用PC 系统:Windows 测试有效 软件:ffmpeg 解决 0、命令 ffmpeg.exe -i input1.mp4 -i input2.mp4 -i input3.mp4 -i input4.mp4 -filter_complex "[0:v]scaleiw/2:ih/2,pad2*iw:2*ih[a]; [1:v]scaleiw/2:ih/2…

看待事物的层与次 | DBA与架构的一次对话交流

前言 在计算机软件业生涯中,想必行内人或多或少都能感受到系统架构设计与数据库系统工程的重要性,也能够清晰地认识到在计算机软件行业中技术工程师这个职业所需要的专业素养和必备技能! 背景 通过自研的数据库监控管理工具,发现 SQL Server 数据库连接数在1-2K之间,想…

Java进阶-反射

来学习一下Java的反射,通过Class实例获取class信息的方法称为反射(Reflection),内容如下 一、反射机制 1、概述 在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一…

UDP数据报套接字编程入门

目录 1.TCP和UDP的特点及区别 1.1TCP的特点 1.2UDP的特点 1.3区别 2.UDP Socket的api的介绍 2.1DatagramSocket API 2.2DatagramPacket API 3.回显客户端与服务器 3.1回显服务器 3.1.1UdpEchoServer类的创建 3.1.2服务器的运行方法start() 3.1.3main部分 3.1.4.完整…

LeetCode #104 二叉树的最大深度

104. 二叉树的最大深度 题目 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例 1: 输入:root [3,9,20,null,null,15,7] 输出:3 示例 2: 输入:root [1,null,2] 输出:2 分析 …

CMake、OpenCV 和单元测试

我写了很多关于 CMake 的文章,如果你感兴趣,可以点击以下链接阅读: CMake VS MakeCMake:在构建世界掀起风暴现代 CMake 使用技巧CMake 交叉编译CMake 生成器已开启 我们将继续对 CMake 的探索,这篇文章技术性高&…

云商大宗商品使用的是什么系统,有什么功能

云商大宗商品使用的系统主要包括大宗商品贸易系统、大宗商品仓储管理系统。 大宗商品贸易系统则是一种基于互联网技术的交易平台,旨在为买卖双方提供高效、安全、透明的交易环境。该系统具备商品管理、交易管理、资金管理、数据分析和风险管理等功能模块&#xff0…

2024年四川媒体新闻发布渠道,媒体邀约资源表

传媒如春雨,润物细无声,大家好,我是51媒体网胡老师。 四川有哪些媒体新闻发布渠道,媒体邀约资源表? 2024年四川媒体新闻发布渠道,媒体邀约资源表 四川本地媒体:如四川日报、华西都市报、成都商…

2024年最好的 5 款免费的数据恢复软件

数据意外损坏或丢失?别担心,今天我为大家带来了实用的免费硬盘数据恢复软件! 在数字化时代,数据的重要性不言而喻。但是,由于种种原因,数据意外损坏或丢失的情况时有发生。此时,一款实用的免费…

SpringBoot 自定义映射规则resultMap association一对一

介绍 例:学生表,班级表,希望在查询学生的时候一起返回该学生的班级,而一个实体类封装的是一个表,如需要多表查询就需要自定义映射。 表结构 班级表 学生表 SQL语句 SELECT a.id,a.name,a.classes,b.id classes…

JS(JavaScript)中如何实现,复选框checkbox多选功能

起始界面&#xff1a; 代码元素&#xff1a; <p><input type"checkbox" id"checkedAll"> 全选按钮</p><p><input type"checkbox" class"cl"> 选项1</p><p><input type"checkbox&qu…