- 编程语言
- 编译器
- 开发工具
【免费下载链接】grammars-v4
Grammars written for ANTLR v4; expectation that the grammars are free of actions.
本篇技术指南围绕 python/python/Cpp/README.md 展开,讲解如何在 ANTLR v4 的 grammars-v4 仓库中,把通用 Python 语法(PythonLexer.g4、PythonParser.g4)适配到 C++ 目标:通过transformGrammar.py自动改写语法文件,并借助PythonLexerBase/PythonParserBase基类补齐缩进(INDENT/DEDENT)与 Python 2/3 版本切换等目标特有逻辑。读完本文,你将掌握完整的 C++ 目标构建命令、脚本改写原理、基类实现细节,以及如何将生成的解析器集成进自己的 C++ 工程。
一、为什么需要 "Target-specific grammar instructions"
grammars-v4 仓库的 python/python 目录维护的是一套"通用(Universal)"Python 语法,宣称适用于 Python 2 与 Python 3,且不含任何目标语言动作(actions)。但 ANTLR 的 Python 语法有一个无法回避的难点:缩进不是普通文法能直接表达的东西——INDENT、DEDENT这类"人造 token"必须由词法分析器在运行时动态生成,而生成规则又与具体目标语言(C++、Java、C#、Python3 等)的运行时 API 绑定。
为此,仓库在 python/python 下按目标语言拆分了多个目录(Cpp/、CSharp/、Java/、Python3/),每个目录内提供:
- 目标对应的Base 类源码(如 C++ 的
PythonLexerBase/PythonParserBase); - 一个
transformGrammar.py脚本,负责把通用.g4文件改写成适合该目标的形式。
python/python/Cpp/README.md 正是对这套流程的官方说明:它本身只有 4 条核心内容——目录用途、两条构建命令、脚本职责说明、更新记录(2022-08-03,Ken Domino)。本文将以它为骨架,结合目录内真实源码逐层展开。
二、Cpp 目录结构与两条核心命令
Cpp 目录 下共 6 个文件,职责清晰:
| 文件 | 作用 |
|---|---|
transformGrammar.py | 改写PythonLexer.g4/PythonParser.g4,为 C++ 目标注入@header并修正语法 |
PythonLexerBase.h/.cpp | 词法器基类:TabSize 配置、缩进栈、INDENT/DEDENT/LINE_BREAK生成 |
PythonParserBase.h/.cpp | 解析器基类:Python 2/3 版本切换与校验 |
README.md | 使用说明(本文关联文档) |
官方给出的构建流程只有两条命令,在python/python/Cpp目录下执行:
python transformGrammar.py antlr4 -Dlanguage=Cpp -o gen *.g4- 第一步
python transformGrammar.py:必须在生成代码之前运行,它就地改写两个.g4文件(详见第三节); - 第二步
antlr4 -Dlanguage=Cpp -o gen *.g4:调用 ANTLR v4 工具,以 C++ 为目标语言,把改写后的语法生成到gen/子目录。-o gen指定输出目录,*.g4会同时匹配改写后的PythonLexer.g4与PythonParser.g4。
注意一个细节:脚本改写的是当前目录下的PythonLexer.g4/PythonParser.g4(脚本内部使用相对文件名),而这两个文件默认并不存在于Cpp/目录中,它们位于 python/python 根目录。因此实际运行前需先确保这两个.g4文件位于当前工作目录(例如先拷贝过去,或在仓库约定的构建流程中执行),脚本找不到文件时会打印Could not find file: ...并以sys.exit(1)终止——这是 transformGrammar.py 中的显式保护逻辑。
三、transformGrammar.py 源码级剖析:脚本到底改了什么
transformGrammar.py 全文仅 31 行,核心是fix(file_path)函数,对每个语法文件执行三步改写:
3.1 备份原文件
shutil.move(file_path, file_path + ".bak")每个.g4在被改写前都会先被移动为.bak备份(即生成PythonLexer.g4.bak、PythonParser.g4.bak),脚本随后逐行读取备份并写回原文件名。这保证了语法文件可以反复运行脚本而不会累积破坏,也让使用者随时能对比改写前后差异。
3.2 注入 C++ 目标的 @header
if '// Insert here @header for C++ lexer.' in x: x = x.replace('// Insert here @header for C++ lexer.', '@header {#include "PythonLexerBase.h"}') if '// Insert here @header for C++ parser.' in x: x = x.replace('// Insert here @header for C++ parser.', '@header {#include "PythonParserBase.h"}')这两个占位注释在通用语法文件中真实存在:
- PythonLexer.g4:
// Insert here @header for C++ lexer. - PythonParser.g4:
// Insert here @header for C++ parser.
改写后,生成的 C++ 代码文件头部将包含#include "PythonLexerBase.h"/#include "PythonParserBase.h",从而把基类声明引入生成的词法器/解析器实现中。这与两个.g4文件options块里的superClass = PythonLexerBase;(PythonLexer.g4)和superClass = PythonParserBase;(PythonParser.g4)配合:ANTLR 生成类继承自PythonLexerBase/PythonParserBase,而后者又继承自antlr4::Lexer/antlr4::Parser。
3.3 修正成员访问语法:this. → this->
if 'this.' in x: x = x.replace('this.', 'this->')通用语法文件中使用this.xxx形式的谓词/动作代码(这是 Java/C# 风格),但 C++ 中成员访问必须使用this->xxx。脚本对所有包含this.的行做字符串替换,这是 C++ 目标下语法文件能够编译通过的关键一步。这也是 README 所说 "The transformGrammar.py script modifies the grammar for the target." 的具体含义。
整个脚本由if __name__ == '__main__': main(sys.argv)入口驱动,fix("PythonLexer.g4")与fix("PythonParser.g4")依次执行,并在每个文件处理时打印Altering <file>与Writing ...日志,方便确认执行进度。
四、PythonLexerBase:C++ 下如何实现缩进敏感词法
词法层面最大的挑战是 Python 的缩进语义。Python 官方规范要求:Tab 从左到右被替换为 1~8 个空格,使替换后的总字符数是 8 的倍数(tab stops)。PythonLexerBase.cpp 将这套逻辑完整落地。
4.1 构造与默认配置
PythonLexerBase::PythonLexerBase(antlr4::CharStream *input) : Lexer(input) { TabSize = 8; _number_of_elements = 32; _buffer = new std::unique_ptr<antlr4::Token>[_number_of_elements]; ... _lastTokenNull = true; _opened = 0; }构造时TabSize默认8(与 python/python/README.md 中 "UseTabSizeproperty inPythonLexerBaseto configure tab size (8 spaces by default)" 一致),并初始化一个容量为 32 的 token 环形缓冲(用于前瞻/回溯场景)。_opened记录当前未闭合的括号层级,用于判断缩进是否生效。
4.2 缩进计算与 INDENT/DEDENT 生成
HandleSpaces()中,当一行空格后不是换行/注释(IsNotNewLineOrComment)时,逐字符计算缩进宽度:
indent += c == '\t' ? TabSize - indent % TabSize : 1;即:普通空格记 1,Tab 补齐到下一个TabSize的整数倍——这与官方文档的 tab-stop 规则完全一致。
ProcessNewLine(indent)则负责维护一个缩进栈_indents:
- 当前缩进
> previous:入栈并Emit(INDENT); - 当前缩进
< previous:循环出栈,每弹出一层Emit(DEDENT),从而支持一次退回多级缩进(如从三层缩进直接回到顶层)。
此外,HandleNewLine()会把换行以NEWLINE类型、HIDDEN通道发出,而LINE_BREAK、INDENT、DEDENT这三个"人造 token"在 PythonLexer.g4 的tokens {}块中预先声明,供解析器侧直接引用。
4.3 文件结尾的 DEDENT 补发
Python 要求在文件末尾补齐所有未闭合的缩进。nextToken()中专门处理了 "end-of-file ahead but DEDENTS still pending" 的场景:
if (_input->LA(1) == antlr4::Token::EOF && _indents.size() > 0) { if (... != PythonLexer::LINE_BREAK) Emit(PythonLexer::LINE_BREAK); // 先补一个行结束符 while (_indents.size() != 0) { Emit(PythonLexer::DEDENT); _indents.pop(); } }4.4 token 缓冲与前瞻
PythonLexerBase通过_buffer(环形数组)、_firstTokensInd/_lastTokenInd实现了自定义的 token 缓冲:emit()在调用父类Lexer::emit的同时把 token 副本存入缓冲,缓冲满时自动扩容(_number_of_elements * 2并搬移数据);nextToken()从缓冲头部取 token 返回。这一机制保证了解析器做任意前瞻(如if/while行尾判断)时,INDENT/DEDENT等动态 token 的时序依然正确。
4.5 括号内的缩进豁免
IncIndentLevel()/DecIndentLevel()维护_opened计数,IsNotNewLineOrComment()中_opened == 0的条件意味着:只要处于未闭合括号内,就跳过缩进处理。这正是 Python "括号内可任意换行缩进" 语义的词法实现。
五、PythonParserBase:解析期的 Python 2/3 版本控制
PythonParserBase.h 定义了版本枚举:
enum PythonVersion { Autodetect, Python2 = 2, Python3 = 3 };构造时默认Autodetect(PythonParserBase.cpp)。两个核心方法:
CheckVersion(int version):当版本为Autodetect时始终返回true;否则要求version与当前Version一致,用于语法中区分 Python 2 与 Python 3 独有的语法构造;SetVersion(int requiredVersion):在解析过程中(例如识别到print语句或with语句等版本分叉点时)把Version固定为实际值。
配合 python/python/README.md 的说明:若选择Autodetect,版本会在解析完某段代码片段后被切换为确定值——也就是说同一份语法、同一套生成代码,可以同时正确解析 Python 2 和 Python 3 的源码。语法文件中凡是版本相关的谓词,都会经脚本改写为this->CheckVersion(...)/this->SetVersion(...)形式的 C++ 调用。
六、把生成的解析器集成进 C++ 工程
参照 python/python/README.md 的用法说明,集成步骤为:
- 生成代码:按第二节的两条命令执行(先
transformGrammar.py,再antlr4 -Dlanguage=Cpp -o gen *.g4); - 拷贝产物:将
gen/下生成的 lexer/parser 源码,连同本目录的PythonLexerBase.h/.cpp、PythonParserBase.h/.cpp一并加入工程; - 链接 ANTLR 运行时:确保包含并链接
antlr4-runtime(#include "antlr4-runtime.h"); - 配置参数(可选):
- 通过
PythonLexerBase的TabSize属性自定义 Tab 宽度(默认 8 空格); - 通过
PythonParserBase的Version属性预置Python2/Python3,或保持Autodetect让解析器自动判定。
- 通过
基类头文件中的公开成员(TabSize、Version、CheckVersion、SetVersion等,见 PythonLexerBase.h、PythonParserBase.h)即可作为与生成代码交互的入口点。
七、注意事项与适用前提
- 脚本会就地改写 .g4 文件:运行
transformGrammar.py前请确认工作目录下存在PythonLexer.g4与PythonParser.g4,且允许产生.bak备份文件;不要在只读目录下直接运行。 - 命令需在正确的目录执行:README 的命令假设
transformGrammar.py、两个.g4文件位于同一目录;若从仓库其他位置调用,需自行组织文件布局。 - 基类依赖 ANTLR C++ 运行时:
PythonLexerBase继承antlr4::Lexer并包含antlr4-runtime.h,编译生成的 lexer/parser 时必须链接与之匹配版本的antlr4-runtime库。 - 版本能力以当前仓库为准:上述
TabSize默认值、Autodetect行为均出自本仓库源码(PythonLexerBase.cpp、PythonParserBase.cpp),如使用其他版本或分叉仓库,请以实际代码为准。
如需对比其他目标语言的实现思路,可参阅 python/python/Java(Java 版PythonLexerBase.java/PythonParserBase.java)与 python/python/Python3(纯 Python 版基类),它们与 C++ 版共享同一套通用语法,仅在目标 API 与细节实现上有所差异。
- 编程语言
- 编译器
- 开发工具
【免费下载链接】grammars-v4
Grammars written for ANTLR v4; expectation that the grammars are free of actions.
相关推荐
grammars-v4 仓库 Python3 目标文法解析器构建指南:transformGrammar 脚本与词法基类深入解析
grammars v4 仓库 Python3 目标文法解析器构建指南:transformGrammar 脚本与词法基类深入解析 本指南以 grammars v4
编程语言编译器开发工具如何解决Mediamtx中RTSP SETUP请求的路径验证问题?技术深度解析
如何解决Mediamtx中RTSP SETUP请求的路径验证问题?技术深度解析 RTSP协议在媒体流传输中扮演着关键角色,而Mediamtx作为一款支持SRT、
编程语言编译器开发工具ANTLR v4 自举语法深度解析:grammars-v4 中 ANTLRv4Lexer 与 ANTLRv4Parser 的实现与验证
ANTLR v4 自举语法深度解析:grammars v4 中 ANTLRv4Lexer 与 ANTLRv4Parser 的实现与验证 导读 grammars
编程语言编译器开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考