ANTLR4 词法规则(Lexer Rules)完全指南:Token 定义、词法模式、递归规则与 Lexer 命令实战
【免费下载链接】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/lexer-rules.md 展开,系统讲解如何用词法规则(lexer rules)定义 Token:从最基础的规则语法、字符集与区间运算符,到词法模式(lexical modes)拆分多子词法器、递归词法规则匹配嵌套结构,再到skip、more、mode()、type()、channel()等 Lexer 命令的实战用法,以及caseInsensitive选项的规则级覆盖机制。读完本文,你将能够独立编写健壮的词法语法,理解 Token 从字符流到Token对象的完整产生过程,并能对照 Lexer.java 等运行时源码解释命令的执行原理。
一、词法规则基础:Token 的定义方式
词法语法(lexer grammar)由若干词法规则组成,词法规则定义了 Token,其语法与语法规则(parser rules)基本一致,但有三个关键限制:
- 词法规则不能有参数、返回值或局部变量;
- 词法规则名必须以大写字母开头,以此与以小写字母开头的语法规则区分;
- 词法规则可以按需拆分进多个词法模式(lexical modes)。
最基本的规则形式如下:
/** 可选文档注释 */ TokenName : alternative1 | ... | alternativeN ;ANTLR4 中的词法规则与语法规则在文件组织上也有区分:.g4文件可以是lexer grammar(纯词法)、parser grammar(纯语法)或 combined grammar(词法+语法混合),而词法模式只允许出现在纯词法语法中。
fragment 规则:辅助识别、不产生 Token
除了 Token 规则,还可以定义 fragment 规则。fragment 规则不产生对解析器可见的 Token,仅用于辅助 Token 的识别,其语法是在规则名前加fragment关键字:
fragment HelperTokenRule : alternative1 | ... | alternativeN ;最经典的例子是用DIGIT片段组合出INT:
INT : DIGIT+ ; // 引用 DIGIT 辅助规则 fragment DIGIT : [0-9] ; // DIGIT 本身不是 Tokenfragment 规则的典型价值是复用与可读性:把重复出现的字符集合抽象成命名片段,多个 Token 规则共享引用,避免重复书写字符集。
二、词法模式(Lexical Modes):一个词法器拆成多个子词法器
词法模式允许把单个词法语法按上下文拆分为多个子词法器。最典型的场景是 XML:标签内部和标签外部的词法规则完全不同。词法器只能返回由当前模式中的规则匹配出的 Token。
词法器启动时处于所谓的default mode(默认模式)。所有规则默认都属于默认模式,除非用mode命令显式声明。模式的语法如下:
rules in default mode ... mode MODE1; rules in MODE1 ... mode MODEN; rules in MODEN ...要点:
- 词法模式不允许出现在 combined grammar 中,只能用于纯词法语法(
lexer grammar); - 模式名通常全部大写(如
INSIDE、STR、PROC_INSTR),这是 ANTLR 社区的惯例命名,便于与规则名区分; - 切换模式依赖
mode()、pushMode()、popMode()等 Lexer 命令(见下文第五节)。
从源码结构看,模式在工具端是规则集合的天然分组:工具在构建 ATN(增强型转换网络)时按模式组织状态机(参见 automata/LexerATNFactory.java),运行时则通过Lexer实例中的_mode字段与_modeStack模式栈决定下一次匹配使用哪组规则(见 Lexer.java)。
三、词法规则元素:字符集、区间与特殊结构
词法规则拥有两种语法规则不具备的构造:..区间运算符和方括号[characters]字符集表示法。注意不要将[characters]与语法规则中的参数混淆——在词法规则里[ ]只表示字符集。
下表完整汇总了所有词法规则元素(原文表格的完整展开):
| 语法 | 说明 |
|---|---|
T | 在当前输入位置匹配 TokenT。Token 总是以大写字母开头。 |
'literal' | 匹配该字符或字符序列,如'while'或'='。 |
[char set] | 匹配字符集中指定的一个字符。x-y表示从x到y的闭区间字符集合。转义字符\n、\r、\b、\t、\f、\uXXXX、\u{XXXXXX}被解释为单个特殊字符。]和\必须用\转义;-一般也需要转义,但当-位于字符集首尾时无需转义。 |
'x'..'y' | 匹配x到y闭区间内的任意单个字符,如'a'..'z',等价于[a-z]。 |
T(调用规则) | 调用词法规则T;允许递归,但禁止左递归。T可以是普通 Token 规则或 fragment 规则。 |
. | 单字符通配符,匹配任意单个字符。 |
{«action»} | 词法动作。自 4.2 起可出现在规则任意位置,不再局限于最外层候选式末尾;词法器会按动作在规则中的位置在对应输入位置执行动作。 |
{«p»}? | 语义谓词。运行时若«p»求值为 false,所在规则变为"不可见"(非可行)。谓词应放在规则末尾以获得最高效率,且必须位于词法动作之前。 |
~x | 匹配不在集合x中的任意单个字符。x可以是单个字符字面量、区间,或子规则集合,如~('x'|'y'|'z')、~[xyz]。 |
字符集与 Unicode 支持
字符集内的转义与 Unicode 能力是词法规则的强项:
- 转义字符:
\n、\r、\b、\t、\f、\uXXXX(如\u000D表示回车)、\u{XXXXXX}(支持 > U+FFFF 的码点)。 - Unicode 属性:
\p{PropertyName}或\p{EnumProperty=Value},反选用\P{PropertyName}或\P{EnumProperty=Value}。合法属性名见 Unicode 标准附录 UAX #44(本文不展开外部链接,仓库内 scripts/parse-extended-pictographic 展示了这类属性数据的解析处理)。 - 支持长短通用类别名,如
\p{Lu}、\p{Z}、\p{Symbol}、\p{Blk=Latin_1_Sup}、\p{Block=Latin_1_Supplement}。 - Unicode 区块快捷写法:用
In前缀 + 下划线替换空格,如\p{InLatin_1_Supplement}、\p{InYijing_Hexagram_Symbols}、\p{InAncient_Greek_Numbers},等价于\p{Block=Latin_1_Supplement}。 - 额外支持的属性:
\p{Extended_Pictographic}(UTS #35)、\p{EmojiPresentation=EmojiDefault}(默认彩色 emoji 呈现但也可文本呈现)、\p{EmojiPresentation=TextDefault}(默认文本呈现但也可 emoji 呈现)、\p{EmojiPresentation=Text}(仅文本呈现)。 - 属性名不区分大小写,
_与-视为等价。
以下示例逐一说明这些写法(原文完整保留):
WS : [ \n\u000D] -> skip ; // 等价于 [ \n\r] UNICODE_WS : [\p{White_Space}] -> skip; // 匹配全部 Unicode 空白 ID : [a-zA-Z] [a-zA-Z0-9]* ; // 常规标识符 UNICODE_ID : [\p{Alpha}\p{General_Category=Other_Letter}] [\p{Alnum}\p{General_Category=Other_Letter}]* ; // 完整 Unicode 字母标识符 EMOJI : [\u{1F4A9}\u{1F926}] ; // 注意:> U+FFFF 的码点 DASHBRACK : [\-\]]+ ; // 匹配 - 或 ] 一次以上 DASH : [---] ; // 匹配单个 -:即"-"与"-"之间的"任意字符"(首尾 - 不转义)注意DASH : [---]是一个容易令人困惑的例子:首尾的-不需转义,中间的-作为区间运算符,因此[---]实际表示-到-的区间,结果仍是单个-。
词法规则中的动作(Actions)
词法动作的语法遵循目标语言语法,ANTLR 会把动作内容原样复制进生成的代码,不做$x.y之类的属性翻译(这与语法动作不同,也不同于 ANTLR v3)。多候选式的规则若要只执行一次动作,可用圆括号把候选式括起来,再把动作放在其后:
END : ('endif'|'end') {System.out.println("found an end");} ;两个重要限制:
- 只有最外层 Token 规则中的动作会被执行:如果
STRING调用了ESC_CHAR,而ESC_CHAR里有动作,那么当词法器从STRING开始匹配时,ESC_CHAR中的动作不会执行; - 一个词法规则无论有多少候选式,最多只能有一个动作。
子规则与 EBNF 运算符
与语法规则一样,词法规则支持圆括号子规则和 EBNF 运算符?、*、+,且这些运算符还可以加非贪婪后缀?(如.*?)。COMMENT规则展示了*和?:
COMMENT : '#' ~[\r\n]* '\r'? '\n' -> skip ;[0-9]+是+的常见用法,用于匹配整数。
四、递归词法规则:匹配嵌套结构
大多数词法工具不支持递归,而ANTLR 的词法规则允许递归(但禁止左递归),这让它能够直接匹配嵌套 Token,例如嵌套的 action 块{...{...}...}:
lexer grammar Recur; ACTION : '{' ( ACTION | ~[{}] )* '}' ; WS : [ \r\t\n]+ -> skip ;这里ACTION在自身定义内部递归引用自己:要么再次进入ACTION匹配内层花括号块,要么用~[{}]匹配非花括号字符,从而实现对任意深度嵌套块(如 JSON 对象、带花括号的代码块)的完整匹配。从 ATN 构建角度,工具端把这种自引用规则转换为内部环状状态图(参见 automata/LexerATNFactory.java 对规则引用的处理),运行时即支持任意深度的递归匹配。
五、冗余字符串字面量:多个规则声明同一字面量的陷阱
不要在多个词法规则的右侧出现相同的字符串字面量——它们是有歧义的,可能匹配多种 Token 类型。遇到这种情况,ANTLR 会让该字面量对解析器不可用;跨模式出现同样的问题。例如:
lexer grammar L; AND : '&' ; mode STR; MASK : '&' ;此时解析器语法无法引用字面量'&',但可以引用 Token 名:
parser grammar P; options { tokenVocab=L; } a : '&' // 工具报错:no such token AND // 没问题 MASK // 没问题 ;对应的构建与测试序列(L.g4会生成解析器tokenVocab选项所需的L.tokens文件):
$ antlr4 L.g4 # 生成 P.g4 中 tokenVocab 选项需要的 L.tokens 文件 $ antlr4 P.g4 error(126): P.g4:3:4: cannot create implicit token for string literal '&' in non-combined grammar实战结论:同一字符序列只能绑定一个 Token 类型;若多个上下文需要匹配相同文本,应通过模式切换 + 命名 Token(如AND/MASK)的方式区分,而不是重复字面量。
六、词法规则动作与 Token 的产生过程
理解 Token 的产生过程有助于正确编写动作。ANTLR 词法器在匹配一条词法规则后创建Token对象;每次请求 Token 都从Lexer.nextToken开始,识别出 Token 后调用emit。
在 Java 运行时实现中(Lexer.java),nextToken的核心循环为:
- 记录字符流标记位置,初始化
_channel、_tokenStartCharIndex、_tokenStartCharPositionInLine、_tokenStartLine、_text; - 进入
do { ... } while ( _type == MORE )循环,调用getInterpreter().match(_input, _mode)在当前模式下匹配; - 匹配出错时上报
LexerNoViableAltException并recover,将ttype置为SKIP; - 若
_type == SKIP则继续外层循环取下一个 Token;若_type == MORE则继续匹配;否则调用emit()并返回 Token。
emit()会从词法器当前状态收集字段:_type、_text、_channel、_tokenStartCharIndex、_tokenStartLine、_tokenStartCharPositionInLine,并通过 Token 工厂创建Token对象(Lexer.java)。
这意味着动作可以通过setType等方法改写这些状态。例如,以下规则在enumIsKeyword为 false 时把enum改写为标识符:
ENUM : 'enum' {if (!enumIsKeyword) setType(Identifier);} ;要点回顾:
- ANTLR不会在词法动作中做
$x属性翻译(不同于 v3); - 一个词法规则最多只能有一个动作,无论其候选式数量;
- 动作只能出现在谓词之后(见下文谓词部分)。
七、Lexer 命令(Lexer Commands):不绑定目标语言的 Token 后处理
为避免语法绑定特定目标语言,ANTLR 提供 Lexer 命令。与任意嵌入动作不同,命令遵循固定语法,且数量有限。Lexer 命令出现在词法规则定义最外层候选式的末尾,与动作一样,每条 Token 规则只能有一个命令。语法为->后跟一个或多个可带参数的命令名:
TokenName : «alternative» -> command-name TokenName : «alternative» -> command-name («identifier or integer»)一个候选式可以有多个命令,用逗号分隔。合法命令名:
skipmorepopModemode( x )pushMode( x )type( x )channel( x )
skip:丢弃当前文本
skip命令让词法器获取下一个 Token 并丢弃当前文本:
ID : [a-zA-Z]+ ; // 匹配标识符 INT : [0-9]+ ; // 匹配整数 NEWLINE:'\r'? '\n' ; // 将换行返回给解析器(语句结束信号) WS : [ \t]+ -> skip ; // 丢弃空白从运行时看,skip对应 Lexer.java 中的skip()方法:将_type置为SKIP,nextToken的外层循环检测到SKIP后continue outer重新开始匹配。
mode()、pushMode()、popMode、more:模式栈与文本累积
模式命令用于修改模式栈,从而改变词法器当前模式;more命令则让词法器继续获取下一个 Token 但不丢弃当前文本,最终 Token 的类型取"最终"匹配的规则(即没有more或skip命令的那条)。
// 默认模式:标签外部的所有内容 COMMENT : '<!--' .*? '-->' ; CDATA : '<![CDATA[' .*? ']]>' ; OPEN : '<' -> pushMode(INSIDE) ; ... XMLDeclOpen : '<?xml' S -> pushMode(INSIDE) ; SPECIAL_OPEN: '<?' Name -> more, pushMode(PROC_INSTR) ; // ----------------- 标签内部的所有内容 --------------------- mode INSIDE; CLOSE : '>' -> popMode ; SPECIAL_CLOSE: '?>' -> popMode ; // 关闭 <?xml...?> SLASH_CLOSE : '/>' -> popMode ;再看字符串拼接的经典例子——more与mode组合实现跨 Token 的字符串收集:
lexer grammar Strings; LQUOTE : '"' -> more, mode(STR) ; WS : [ \r\t\n]+ -> skip ; mode STR; STRING : '"' -> mode(DEFAULT_MODE) ; // 解析器希望看到的 Token TEXT : . -> more ; // 持续收集字符串文本这个例子中,LQUOTE用more保留开引号文本并切入STR模式,TEXT用. -> more逐字符累积,直到STRING匹配到闭合引号并切回DEFAULT_MODE,最终产出一个完整的字符串 Token。
运行时语义对应 Lexer.java:
more():_type = MORE,使nextToken循环继续匹配;mode(m):直接设置_mode;pushMode(m):先把当前模式压入_modeStack再切换;popMode():弹栈并恢复模式;若栈为空会抛出EmptyStackException。
注意事项(原文明确):
- 弹出模式栈底层会抛出异常;
mode切换的是当前栈顶,而pushMode/popMode维护整个栈;- 多个
more等价于一个more,且位置无关。
type():改写 Token 类型
type()用于在匹配后将 Token 类型改写为指定值,常用于把多个形态统一为一个 Token:
lexer grammar SetType; tokens { STRING } DOUBLE : '"' .*? '"' -> type(STRING) ; SINGLE : '\'' .*? '\'' -> type(STRING) ; WS : [ \r\t\n]+ -> skip ;注意:多个type()命令时,只有最右边的一个生效。
channel():把 Token 发送到隐藏通道
channel()将 Token 送入指定通道,最典型的是HIDDEN通道,使注释、空白对解析器不可见但保留在 Token 流中:
BLOCK_COMMENT : '/*' .*? '*/' -> channel(HIDDEN) ; LINE_COMMENT : '//' ~[\r\n]* -> channel(HIDDEN) ; // ---------- // 空白 // // 对解析器无意义的字符,仅用于提高语法可读性 // WS : [ \t\r\n\f]+ -> channel(HIDDEN) ;自 4.5 起,还可以像定义枚举一样在词法规则上方自定义通道名:
channels { WSCHANNEL, MYHIDDEN }这样生成的代码中会包含对应通道常量,供解析器或监听器按名引用。
八、词法规则选项:caseInsensitive
caseInsensitive选项定义当前词法规则是否大小写不敏感,参数为true或false;若语法级已定义caseInsensitive,规则级选项会覆盖其值:
options { caseInsensitive=true; } STRING options { caseInsensitive=false; } : 'N'? '\'' (~'\'' | '\'\'')* '\''; // 小写 n 不被允许在语法级caseInsensitive=true的前提下,STRING规则自身声明false,因此该规则匹配时不忽略大小写,小写n前缀不被接受,其他规则仍保持大小写不敏感。
工具端的行为在 semantics/BasicSemanticChecks.java 中可验证:
caseInsensitive是词法语法选项、词法规则选项以及解析器语法选项(parserOptions、lexerRuleOptions)的合法成员(见 tool/Grammar.java);- 值必须是
true或false,否则报ILLEGAL_OPTION_VALUE; - 若规则级值与语法级值相同,工具会报告
REDUNDANT_CASE_INSENSITIVE_LEXER_RULE_OPTION(冗余声明)警告。
从实现层面看,该选项在 ATN 构建阶段生效:工具在构建字符集时,对启用caseInsensitive的规则把字符区间扩展为包含大小写两种形态(参见 automata/LexerATNFactory.java 中checkRangeAndAddToSet对caseInsensitive的处理,以及Rule类中以final boolean caseInsensitive字段保存该选项值)。因此它是静态编译期的大小写折叠,而非运行时的逐字符转换。
九、进阶注意:语义谓词与词法动作的次序
语义谓词{«p»}?在词法规则中可出现在任意位置,但有两个实践要点(详见 doc/predicates.md):
- 词法器倾向于把谓词放在规则右边缘:因为词法器是在看到 Token 完整文本之后才选择规则,右边缘谓词更高效;
- 谓词必须位于词法动作之前;谓词不能依赖当前候选规则中动作的副作用,但可以依赖上一个 Token识别过程中动作产生的副作用(动作只在词法器确定匹配某规则后执行)。
例如,把enum按条件改写成标识符的谓词写法(原文 doc/predicates.md 的示例思路):
ENUM : 'enum' {_input.LA(1) != 'x'}? IDENTIFIER ;这种写法把enum当作普通标识符的子集处理,由谓词决定是否单独成 Token。
十、小结:一份词法语法的最佳实践清单
综合本文内容,编写 ANTLR4 词法规则时的关键实践:
- 命名与组织:Token 规则大写开头,fragment 规则加
fragment关键字,公共字符集合尽量抽象为 fragment; - 上下文拆分用模式:多上下文场景(如 XML、字符串、模板语言)用
mode/pushMode/popMode拆分子词法器,但记住模式仅限纯词法语法; - 字符集优先:能用
[a-zA-Z]、\p{...}、区间'a'..'z'表达的不要写成一长串|; - 避免冗余字面量:同一字面量只绑定一个 Token;跨模式同名文本用
type()统一或命名区分; - 命令优于动作:
skip/more/channel/type/mode与目标语言无关,优先使用;嵌入动作要遵守"每个规则最多一个"且"位于谓词之后"的约束; - 嵌套结构用递归:花括号块等嵌套语法用递归词法规则(
'{' ( ... | ~[{}] )* '}')直接匹配,避免手工计数; - 大小写策略:语法级
options { caseInsensitive=true; }统一生效,个别规则用规则级选项覆盖,避免冗余声明; - 验证:多规则、多模式、命令与选项的组合行为可通过运行 runtime-testsuite 中对应的词法执行用例(如 descriptors/LexerExec 下的描述符)进行回归验证。
词法规则是语法分析流水线的第一道关卡,掌握上述元素、模式与命令,就能为任何结构化的文本或二进制输入写出高效、可维护的 Token 定义。
【免费下载链接】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),仅供参考