ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ANTLR4 目标无关语法编写指南:用语义谓词、superClass 与 transformGrammar.py 实现一份语法多语言复用

ANTLR4 目标无关语法编写指南:用语义谓词、superClass 与 transformGrammar.py 实现一份语法多语言复用 ANTLR4 目标无关语法编写指南用语义谓词、superClass 与 transformGrammar.py 实现一份语法多语言复用【免费下载链接】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本指南基于 ANTLR4 官方文档 doc/target-agnostic-grammars.md系统讲解如何编写目标无关语法target-agnostic grammars当语法需要借助语义谓词semantic predicates处理上下文相关语法时如何避免为每个目标语言 fork 一份语法文件而是通过基类superClass 文本改写脚本transformGrammar.py让同一份.g4语法同时产出 Java、C、Python、PHP 等多个目标的解析器。读完本文你将掌握谓词语言绑定的根源、五步式的目标无关写法、以及 ANTLR 工具链中superClass与tokenVocab选项的底层落地机制。为什么需要目标无关语法ANTLR4 的语义谓词semantic predicates形如{...}?是写在目标语言中的布尔表达式用于指示沿着被谓词守卫的解析路径继续是否有效。官方文档 doc/predicates.md 指出ANTLR 的总体决策策略是找出所有可行viable分支然后忽略那些谓词当前求值为 false 的分支若仍有多个可行分支则选择语法中先出现的那个。问题在于ANTLR 没有一种通用的谓词语言。谓词、action 中的代码必须用生成解析器的目标语言书写。这意味着同一个语法里一旦出现谓词它就直接绑定到了某种具体语言上要为多个目标Java、C、Python、PHP……维护多份近乎相同的语法文件fork 与合并的维护负担非常沉重。文档给出了两个典型场景来说明这种上下文相关需求场景一Fortran90 的列 1 注释Fortran90 中第 1 列以C开头的行是注释应被放入非默认的 token 流但如果C不在第 1 列则输入非法应当报错。这种位置决定语义的规则必须借助谓词判断c Hello World. c This is a syntax error because c does not start in column 1 program hello print *, Hello World! end场景二C# 的连续两个大于号C# 中两个既可以表示右移表达式也可以是带泛型嵌套类型声明的一部分。由于 ANTLR 的词法分析器不感知解析器上下文词法分析器只能把两个切成两个独立 token是否允许它们之间出现空格必须由谓词在具体上下文中裁决class Foo { void Func() { int x 1000 2; // syntax error if a space exists in the double greater-than sign } Dictionaryint, Listint mapping; // nested template declaration, valid }目标无关语法的总体思路既然谓词必须写目标语言又不想为每个目标 fork 语法ANTLR 官方给出的方案是语法文件本身保持中立把语言相关的差异收敛到两个可替换点基类将谓词的具体实现放进目标语言的基类*Base源文件中语法通过options { superClass...; }继承该基类语法内部只写一次方法调用。文本改写脚本由于不同目标语言对对象方法调用的写法不同this.、this-、$this-、self.用一个名为transformGrammar.py的 Python 脚本在生成解析器之前对.g4文件做机械化的字符串替换。这样.g4源语法只有一份针对不同目标只需运行不同的改写规则与生成命令即可。编写目标无关语法的分步指南原文档 doc/target-agnostic-grammars.md 给出了完整的分步规则下面逐条展开并补充细节。步骤 1拆分语法并声明tokenVocab将语法拆分为独立的词法语法lexer grammar与语法语法parser grammar然后在语法语法中添加options { tokenVocab...; }让两者共享同一套 token 类型编号。按 doc/grammars.md 的说明纯语法语法与纯词法语法的文件头分别是parser grammar Name; ...与lexer grammar Name; ...tokenVocab选项的语义见 doc/options.mdANTLR 在遇到 token 时会为它们分配类型编号而tokenVocab让语法分析器从某个词法语法生成的.tokens文件中读取既有的编号映射。工具链中的实现位于 tool/src/org/antlr/v4/tool/Grammar.java 的importTokensFromTokensFile()它读取tokenVocab选项值通过TokenVocabParser加载.tokens文件并把其中字符串字面量与 token 名逐一注册进当前语法public void importTokensFromTokensFile() { String vocab getOptionString(tokenVocab); if ( vocab!null ) { TokenVocabParser vparser new TokenVocabParser(this); MapString,Integer tokens vparser.load(); ... } }对应的集成测试可参考 tool-testsuite/test/org/antlr/v4/test/tool/TestCompositeGrammars.java 中的testTokensFileInOutputDirAndImportFileInSubdir它演示了lexer grammar MLexer与parser grammar MParser通过options {tokenVocabMLexer;}关联并分别用-o输出目录与-lib词法文件目录参数完成生成。同文件testImportedTokenVocabIgnoredWithWarningL386-L413还验证了被 import 的语法中声明的tokenVocab选项会被忽略并产生警告OPTIONS_IN_DELEGATE。步骤 2为目标语言编写包含谓词实现的基类创建目标特定的源码文件其中包含供词法/语法语法调用的方法谓词逻辑全部写在基类里。例如 C 目标需要Python3LexerBase.{cpp,h}与Python3ParserBase.{cpp,h}Python 目标则是Python3LexerBase.py与Python3ParserBase.py。这些文件与.g4语法同目录存放随后由superClass选项挂接到生成的识别器上。步骤 3用options { superClass...; }挂接基类在语法词法、语法均可中添加options { superClassPython3ParserBase; }superClass会把生成识别器的父类替换为指定基类从而让语法规则内对基类方法的调用可以被解析。其精确定义见 doc/options.md对于组合语法combined grammar该选项只作用于生成的解析器。文档中还演示了命令行传参方式$ antlr4 -DsuperClassXX Hi.g4 $ grep public class HiParser.java public class HiParser extends XX { $ grep public class HiLexer.java public class HiLexer extends Lexer {注意-D会覆盖语法文件内的同名选项。从源码层面看选项的合法性定义在 tool/src/org/antlr/v4/tool/Grammar.javaparserOptions集合包含了superClass、contextSuperClass、TokenLabelType、tokenVocab、language、accessLevel、exportMacro与caseInsensitive而doNotCopyOptionsToLexer明确列出了superClass、TokenLabelType、tokenVocab三项——这正是组合语法中superClass不影响词法分析器这一行为的工具实现。生成器一侧tool/src/org/antlr/v4/codegen/model/Recognizer.java 把superClass选项值包装为ActionText注入输出模型最终渲染进生成的识别器类声明。步骤 4语法内只写单次方法调用在语法中编写对基类方法的调用调用点必须统一使用this.前缀写法例如词法规则OPEN_PAREN : ( {this.openBrace();};动作代码受严格限制不得引用 ANTLR 属性、变量或类型不得出现分号作为语句分隔符不得包含任何控制流语句。之所以如此克制是为了让transformGrammar.py可以用纯文本替换的方式把this.改写成各目标的调用语法而不需要理解动作内部结构。步骤 5C/PHP 目标添加header占位注释对 C 与 PHP 这类需要显式包含/引入源码文件才能编译的目标在语法第一个规则之前放置一行占位注释等待脚本替换成真正的header命名 action// Insert here header for lexer include.以及// Insert here header for parser include.header是 ANTLR 的命名 action作用是把代码注入生成识别器类文件、类定义之前详见 doc/grammars.mdheader::lexer/header::parser则用于把 action 限定到词法或语法识别器。步骤 6编写transformGrammar.py做目标化改写新增一个名为transformGrammar.py的 Python 脚本在生成解析器之前运行把.g4语法中的占位写法改写为目标语言语法a) C把this.替换为this-b) PHP把this.替换为$this-c) Python把this.替换为self.、l.或p.——具体取决于 action/谓词在语法中的位置d) C把// Insert here header for lexer include.或 parser 版本替换为header::lexer {#include ...}或对应 parser 版本e) PHP把同样的占位注释替换为header::lexer {require ...}f) 最后在生成词法与语法解析器之前运行python transformGrammar.py *.g4。一个符合上述规则的最小脚本示意按目标分发、只做字符串替换#!/usr/bin/env python3 # transformGrammar.py —— 最小实现示意按目标语言改写 .g4 语法 import re import sys def transform_cpp(text): text text.replace(this., this-) text text.replace(// Insert here header for lexer include., header::lexer {#include \Python3LexerBase.h\}) text text.replace(// Insert here header for parser include., header::parser {#include \Python3ParserBase.h\}) return text def transform_php(text): text text.replace(this., $this-) text text.replace(// Insert here header for lexer include., header::lexer {require \Python3LexerBase.php\;}) text text.replace(// Insert here header for parser include., header::parser {require \Python3ParserBase.php\;}) return text def transform_python(text): # 按 action 所在位置选择 self./l./p.这里给出常用替换 text text.replace(this., self.) return text TARGETS {cpp: transform_cpp, php: transform_php, python: transform_python} if __name__ __main__: target sys.argv[1] # cpp / php / python for grammar in sys.argv[2:]: # 传入 *.g4 文件列表 with open(grammar, r) as f: text f.read() with open(grammar, w) as f: f.write(TARGETStarget)实际项目中一个常见做法是把改写逻辑做成幂等且可回归校验的比如用# !target: cpp之类的注释标记当前目标以确保同一份源语法在不同目标之间切换时不会互相污染。从目标无关语法到完整构建流程综合以上步骤一份语法支撑多目标的完整流程如下以 Python3 目标为例在源码目录放置Python3Lexer.g4词法语法与Python3Parser.g4语法语法后者声明options { tokenVocabPython3Lexer; superClassPython3ParserBase; }并在规则内以this.调用基类方法编写Python3LexerBase.py、Python3ParserBase.py把真正的谓词逻辑如列位置检查、上下文判断实现在基类方法中编写transformGrammar.py针对目标语言改写this.与 header 占位注释依次执行$ python transformGrammar.py python Python3Lexer.g4 Python3Parser.g4 $ antlr4 -DlanguagePython3 Python3Lexer.g4 $ antlr4 -DlanguagePython3 Python3Parser.g4将生成的词法/语法解析器与基类一起编译、运行。需要强调的是transformGrammar.py必须在 ANTLR 工具生成代码之前运行因为生成器读取的是改写后的.g4内容例如 C 目标需要先看到this-与header::lexer {#include ...}才能产出可编译代码。这也是原文档把运行python transformGrammar.py *.g4列为生成前置步骤的原因。注意事项与适用边界动作与谓词的位置影响可见性按 doc/predicates.md 的说明解析器中只有位于分支左边缘、且在 action 与 token 引用之前的谓词才可能参与分支预测visible predicates位于 action 之后的谓词在预测阶段会被忽略。词法规则中的谓词则通常放在规则右边缘且动作必须出现在谓词之后。基类仍按目标各写一份目标无关方案消除的是.g4语法的 fork并没有消除基类源码的目标语言实现——每个目标仍需维护对应的*Base文件但其体量远小于整份语法。动作代码的约束是硬性的this.之后必须是单次方法调用不能出现分号、控制流或 ANTLR 属性引用否则文本改写脚本无法可靠工作。组合语法中superClass只作用于解析器若词法分析器也需要自定义基类需将词法部分拆成独立的lexer grammar并在其中单独声明superClass参见 doc/options.md 与 tool/src/org/antlr/v4/tool/Grammar.java 的doNotCopyOptionsToLexer。.tokens文件的存放与查找tokenVocab通过-lib参数或语法文件所在目录定位.tokens文件生成输出可通过-o指定见 tool-testsuite/test/org/antlr/v4/test/tool/TestCompositeGrammars.java 的测试用法。参考doc/target-agnostic-grammars.md本文所依据的官方文档原文doc/predicates.md语义谓词的完整行为细则可见谓词、上下文相关谓词、词法谓词doc/grammars.md语法结构、parser/lexer 语法拆分、命名 actiondoc/options.mdsuperClass与tokenVocab选项说明tool/src/org/antlr/v4/tool/Grammar.java选项合法性集合与doNotCopyOptionsToLexertool/src/org/antlr/v4/codegen/model/Recognizer.javasuperClass注入生成代码的模型实现tool-testsuite/test/org/antlr/v4/test/tool/TestCompositeGrammars.javatokenVocab与 import 行为的集成测试【免费下载链接】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),仅供参考
返回列表