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 } Dictionary<int, List<int> > 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.md:ANTLR 在遇到 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); Map<String,Integer> tokens = vparser.load(); ... } }对应的集成测试可参考 tool-testsuite/test/org/antlr/v4/test/tool/TestCompositeGrammars.java 中的testTokensFileInOutputDirAndImportFileInSubdir:它演示了lexer grammar MLexer与parser grammar MParser通过options {tokenVocab=MLexer;}关联,并分别用-o(输出目录)与-lib(词法文件目录)参数完成生成。同文件testImportedTokenVocabIgnoredWithWarning(L386-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 { superClass=Python3ParserBase; }superClass会把生成识别器的父类替换为指定基类,从而让语法规则内对基类方法的调用可以被解析。其精确定义见 doc/options.md:对于组合语法(combined grammar),该选项只作用于生成的解析器。文档中还演示了命令行传参方式:
$ antlr4 -DsuperClass=XX 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.java:parserOptions集合包含了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.改写成各目标的调用语法,而不需要理解动作内部结构。
步骤 5:C++/PHP 目标添加@header占位注释
对 C++ 与 PHP 这类需要显式包含/引入源码文件才能编译的目标,在语法第一个规则之前放置一行占位注释,等待脚本替换成真正的@header命名 action:
// Insert here @header for lexer include.以及
// Insert here @header for parser include.@header是 ANTLR 的命名 action,作用是把代码注入生成识别器类文件、类定义之前(详见 doc/grammars.md);@header::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 { tokenVocab=Python3Lexer; superClass=Python3ParserBase; },并在规则内以this.调用基类方法; - 编写
Python3LexerBase.py、Python3ParserBase.py,把真正的谓词逻辑(如列位置检查、>>上下文判断)实现在基类方法中; - 编写
transformGrammar.py,针对目标语言改写this.与 header 占位注释; - 依次执行:
$ python transformGrammar.py python Python3Lexer.g4 Python3Parser.g4 $ antlr4 -Dlanguage=Python3 Python3Lexer.g4 $ antlr4 -Dlanguage=Python3 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 语法拆分、命名 action
- doc/options.md:
superClass与tokenVocab选项说明 - tool/src/org/antlr/v4/tool/Grammar.java:选项合法性集合与
doNotCopyOptionsToLexer - tool/src/org/antlr/v4/codegen/model/Recognizer.java:
superClass注入生成代码的模型实现 - tool-testsuite/test/org/antlr/v4/test/tool/TestCompositeGrammars.java:
tokenVocab与 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),仅供参考