ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ANTLR 4 Parse Tree Listeners 实战指南:语法树遍历与解析期监听机制深度解析

ANTLR 4 Parse Tree Listeners 实战指南:语法树遍历与解析期监听机制深度解析 ANTLR 4 Parse Tree Listeners 实战指南语法树遍历与解析期监听机制深度解析【免费下载链接】antlr4ANTLR (ANother Tool for Language Recognition) is a powerful parser generator for reading, processing, executing, or translating structured text or binary files.项目地址: https://gitcode.com/gh_mirrors/an/antlr4ANTLRANother Tool for Language Recognition生成的解析器默认会构建一棵记录输入句子结构完整信息的语法树Parse Tree / Syntax Tree而Parse Tree Listener语法树监听器正是遍历这棵树、在进入/退出每个语法规则时触发回调的标准机制。本文以 ANTLR 4 官方文档 doc/listeners.md 为主体结合仓库内 Java 运行时与工具源码系统讲解语法树的结构、监听器接口的自动生成、ParseTreeWalker的遍历原理以及通过addParseListener()在解析过程中实时监听并正确进行异常处理的实战方案。读完本文你将能够为任何 ANTLR 生成的解析器编写健壮的监听器实现从语法树提取信息、构建解释器或翻译器的完整能力。一、Parse Tree解析器默认构建的“完整记录”默认情况下ANTLR 生成的解析器会构建一种称为**语法树parse tree或句法树syntax tree**的数据结构它记录了解析器如何识别输入句子的结构以及各个组成部分短语的方式。语法树具有如下结构特征内部节点是短语名称phrase names用于分组并标识其子节点树的根节点是最抽象的短语名。在文档示例中根节点是statstatement 的缩写叶子节点永远是输入的词法单元input tokens语法树位于语言识别器与解释器或翻译器实现之间是极其高效的数据结构——它既包含全部输入内容又完整记录了解析器如何将符号分组为短语的全部知识它易于理解且由解析器自动生成——除非你显式关闭该行为调用parser.setBuildParseTree(false)即可关闭语法树构建这通常用于追求极致解析速度、不需要树的场景。由于短语结构是由一组语法规则grammar rules描述的因此语法树子树的根节点与语法规则名一一对应。基于这一事实ANTLR 提供ParseTreeWalker它知道如何遍历这些语法树并在你创建的监听器实现对象中触发事件。二、自动生成的监听器接口与基类ANTLR 工具会为你的语法自动生成监听器接口除非你用命令行选项关闭。例如对于一份Java.g4语法ANTLR 会生成类似如下的接口public interface JavaListener extends ParseTreeListenerToken { void enterClassDeclaration(JavaParser.ClassDeclarationContext ctx); void exitClassDeclaration(JavaParser.ClassDeclarationContext ctx); void enterMethodDeclaration(JavaParser.MethodDeclarationContext ctx); ... }即语法中每一条规则都对应一对enterXxx()/exitXxx()方法。同时 ANTLR 还会生成一个基类例如JavaBaseListener为接口中的所有方法提供空实现。你只需继承该基类并重写感兴趣的方法即可构建自己的监听器——这正是“默认基类 按需重写”的典型扩展模式。这两个文件的生成行为由命令行选项控制。在 tool/src/org/antlr/v4/Tool.java 中可以找到对应的选项定义-listener生成语法树监听器默认开启-no-listener不生成语法树监听器-visitor生成语法树访问器默认关闭-no-visitor不生成语法树访问器默认。也就是说监听器默认总是生成而访问器需要显式通过-visitor开启。三、用 ParseTreeWalker 遍历语法树假设你已经创建了一个名为MyListener的监听器对象以下是调用 Java 解析器并遍历语法树的标准流程JavaLexer lexer new JavaLexer(input); CommonTokenStream tokens new CommonTokenStream(lexer); JavaParser parser new JavaParser(tokens); JavaParser.CompilationUnitContext tree parser.compilationUnit(); // parse a compilationUnit MyListener extractor new MyListener(parser); ParseTreeWalker.DEFAULT.walk(extractor, tree); // initiate walk of tree with listener in use of default walker这里ParseTreeWalker.DEFAULT是仓库 runtime/Java/src/org/antlr/v4/runtime/tree/ParseTreeWalker.java 中定义的单例public class ParseTreeWalker { public static final ParseTreeWalker DEFAULT new ParseTreeWalker();walk()方法采用深度优先、递归下降的方式遍历整棵树其核心逻辑ParseTreeWalker.java#L23-L39为若节点是ErrorNode语法错误节点调用listener.visitErrorNode(...)后返回若节点是TerminalNode词法单元叶子调用listener.visitTerminal(...)后返回否则节点是RuleNode先enterRule(listener, r)触发进入规则事件随后递归遍历所有子节点最后exitRule(listener, r)触发退出规则事件。enterRule与exitRule的内部实现ParseTreeWalker.java#L47-L64进一步揭示了事件触发顺序protected void enterRule(ParseTreeListener listener, RuleNode r) { ParserRuleContext ctx (ParserRuleContext)r.getRuleContext(); listener.enterEveryRule(ctx); // 先触发通用事件 ctx.enterRule(listener); // 再触发规则专属事件enterXxx } protected void exitRule(ParseTreeListener listener, RuleNode r) { ParserRuleContext ctx (ParserRuleContext)r.getRuleContext(); ctx.exitRule(listener); // 先触发规则专属事件exitXxx listener.exitEveryRule(ctx); // 再触发通用事件 }可见每个规则的访问顺序是enterEveryRule→enterXxx→递归子节点→exitXxx→exitEveryRule。其中enterEveryRule/exitEveryRule是运行时接口 ParseTreeListener 定义的四个基础方法之一public interface ParseTreeListener { void visitTerminal(TerminalNode node); void visitErrorNode(ErrorNode node); void enterEveryRule(ParserRuleContext ctx); void exitEveryRule(ParserRuleContext ctx); }四个方法分别覆盖了词法单元节点、错误节点、任意规则进入、任意规则退出四类事件构成了监听器机制的“最小核心”。监听器与访问器Visitor的区别是理解这两套机制的关键监听器方法由 ANTLR 提供的 walker 对象独立自动调用你无需关心如何驱动遍历而访问器方法必须由你显式调用visit()访问子节点——如果忘记对某节点的子节点调用 visit 方法那些子树就不会被访问到。四、在解析过程中实时监听addParseListener除了解析完成后用ParseTreeWalker遍历整棵树我们还可以使用监听器在解析过程中同步执行代码无需等待解析树生成。假设我们有如下简单的表达式语法grammar CalcNoLR; s : expr EOF ; expr: add ((MUL | DIV) add)* ; add : atom ((ADD | SUB) atom)* ; atom : INT ; INT : [0-9]; MUL : *; DIV : /; ADD : ; SUB : -; WS : [ \t] - channel(HIDDEN);我们可以像之前一样实现监听器接口创建一个在解析期间执行的监听器class CountListener extends CalcNoLRBaseListener { public int nums 0; public boolean execExitS false; Override public void exitS(CalcNoLRParser.SContext ctx) { execExitS true; } Override public void exitAtom(CalcNoLRParser.AtomContext ctx) { nums; } }然后通过addParseListener()将其注册到解析器String input 2 8 / 2; CalcNoLRLexer lexer new CalcNoLRLexer(new ANTLRInputStream(input)); CalcNoLRParser parser new CalcNoLRParser(new CommonTokenStream(lexer)); CountListener counter new CountListener(); parser.addParseListener(counter); // Check that the purses valid first CalcNoLRParser.SContext context parser.s(); String parseTreeS context.toStringTree(parser); assertEquals((s (expr (add (atom 2) (atom 8)) / (add (atom 2))) EOF), parseTreeS); assertEquals(3, counter.nums); assertEquals(true, counter.execExitS);解析完成后断言验证2 8 / 2中exitAtom被触发 3 次三个atom2、8、2exitS在根规则退出时执行。该机制的实际落地实现在 runtime/Java/src/org/antlr/v4/runtime/Parser.javapublic void addParseListener(ParseTreeListener listener) { if (listener null) { throw new NullPointerException(listener); } if (_parseListeners null) { _parseListeners new ArrayListParseTreeListener(); } this._parseListeners.add(listener); }对应的管理方法还包括removeParseListener(listener)、removeParseListeners()与getParseListeners()Parser.java#L310-L387支持在解析过程中动态增删监听器。值得注意的是Parser 自身也利用了该机制启用setTrimParseTree(true)时会注入TrimToSizeListener.INSTANCE来缩减上下文子节点列表Parser.java#L294-L297而setTrace(true)时则会注册一个_tracer监听器Parser.java#L934-L940——监听器机制同时是 Parser 内部功能的基石。事件触发由两个受保护方法完成Parser.java#L394-L413protected void triggerEnterRuleEvent() { for (ParseTreeListener listener : _parseListeners) { listener.enterEveryRule(_ctx); _ctx.enterRule(listener); } } protected void triggerExitRuleEvent() { // reverse order walk of listeners for (int i _parseListeners.size()-1; i 0; i--) { ParseTreeListener listener _parseListeners.get(i); _ctx.exitRule(listener); listener.exitEveryRule(_ctx); } }注意两个细节进入事件按注册顺序正序遍历而退出事件按逆序遍历每个规则的enterXxx/exitXxx与enterEveryRule/exitEveryRule成对触发与ParseTreeWalker中的顺序保持一致。重要限制解析过程中不应执行过于复杂的工作因为解析器在遇到语法错误时会抛出异常。如果你的监听器代码抛出了不同类型的异常会干扰解析流程导致行为异常。若希望在监听器代码中捕获并正确处理异常应重写Parser中的以下方法protected boolean listenerExceptionOccurred false; /** * Notify any parse listeners of an exit rule event. * * see #addParseListener */ Override protected void triggerExitRuleEvent() { if ( listenerExceptionOccurred ) return; try { // reverse order walk of listeners for (int i _parseListeners.size() - 1; i 0; i--) { ParseTreeListener listener _parseListeners.get(i); _ctx.exitRule(listener); listener.exitEveryRule(_ctx); } } catch (Throwable e) { // If an exception is thrown in the users listener code, we need to bail out // completely out of the parser, without executing anymore user code. We // must also stop the parse otherwise other listener actions will attempt to execute // almost certainly with invalid results. So, record the fact an exception occurred listenerExceptionOccurred true; throw e; } }这段来自文档的经典实现展示了“带异常保护”的退出事件处理一旦监听器代码抛出Throwable立即记录listenerExceptionOccurred true并重新抛出后续事件不再触发解析彻底退出——避免其余监听器在无效结果上继续执行。需要说明的是当前仓库的 Parser.java 已将triggerExitRuleEvent()简化为直接逆序遍历触发异常会自然向上传播导致解析中止语义与文档版本一致文档版本适合作为需要“一次性拦截并中止”场景下的重写模板。五、监听器异常导致解析中止的验证现在如果在某个监听器方法内部抛出异常// Now throw an exception in the listener class ErrorListener extends CalcNoLRBaseListener { public boolean execExitS false; public boolean execExitAtom false; Override public void exitS(CalcNoLRParser.SContext ctx) { execExitS true; } Override public void exitAtom(CalcNoLRParser.AtomContext ctx) { execExitAtom true; throw new NullPointerException(bail out); } }异常会正确导致解析器 bailout中止并沿调用链向上传播java.lang.NullPointerException: bail out at org.antlr.v4.test.runtime.java.api.TestParseListener$2ErrorListener.exitAtom(TestParseListener.java:102) at org.antlr.v4.test.runtime.java.api.CalcNoLRParser$AtomContext.exitRule(CalcNoLRParser.java:311) at org.antlr.v4.runtime.Parser.triggerExitRuleEvent(Parser.java:412) at org.antlr.v4.runtime.Parser.exitRule(Parser.java:654) at org.antlr.v4.test.runtime.java.api.CalcNoLRParser.atom(CalcNoLRParser.java:336) at org.antlr.v4.test.runtime.java.api.CalcNoLRParser.add(CalcNoLRParser.java:261) at org.antlr.v4.test.runtime.java.api.CalcNoLRParser.expr(CalcNoLRParser.java:181) at org.antlr.v4.test.runtime.java.api.CalcNoLRParser.s(CalcNoLRParser.java:123)从栈追踪可以清晰地看到完整的调用链ErrorListener.exitAtom→AtomContext.exitRule→Parser.triggerExitRuleEvent→Parser.exitRule→atom()→add()→expr()→s()。这一栈结构同时印证了文档中的结论在解析期监听器中抛出的异常会中止解析因此在监听器里应当只做轻量、不会抛异常的辅助工作如计数、收集信息、构建符号表而把重量级逻辑放到解析完成后对整棵语法树的ParseTreeWalker遍历中。六、设计建议与相关资源监听器与访问器机制的共同价值在于它们把应用特定代码从语法文件中分离出来使语法更易阅读避免语法与某个特定应用纠缠在一起。这也是 ANTLR 4 推荐在解释器、翻译器、代码分析器等场景中使用监听器/访问器的根本原因。如需在遍历过程中为上下文节点附加自定义数据可配合运行时提供的 ParseTreeProperty 使用。如果你想深入了解相关主题仓库内还有更多文档可供继续研读doc/interpreters.md解析器解释器与监听器/访问器的配合使用doc/tree-matching.md基于语法树模式匹配的高级用法doc/parse-trees.mdFAQ 中关于语法树的常见问题监听器与访问器的运行时实现源码位于 runtime/Java/src/org/antlr/v4/runtime/tree/ParseTreeWalker、ParseTreeListener、IterativeParseTreeWalker等。总而言之掌握 Parse Tree Listener 机制你就掌握了在 ANTLR 4 生态中“读懂解析结果、提取语法信息”的核心能力既能事后用ParseTreeWalker.DEFAULT深度优先遍历整棵树也能用addParseListener()在解析进行中实时响应规则事件还能通过重写triggerExitRuleEvent()正确处理监听器异常。把这套机制与语法文件解耦你的解释器、翻译器或代码分析工具将保持清晰、可维护的架构。【免费下载链接】antlr4ANTLR (ANother Tool for Language Recognition) is a powerful parser generator for reading, processing, executing, or translating structured text or binary files.项目地址: https://gitcode.com/gh_mirrors/an/antlr4创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表