资讯中心

ANTLR4 词法规则(Lexer Rules)完全指南:Token 定义、词法模式、递归规则与 Lexer 命令实战

📅 2026/10/27 22:29:33
ANTLR4 词法规则(Lexer Rules)完全指南:Token 定义、词法模式、递归规则与 Lexer 命令实战
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片段组合出INTINT : 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}支持 UFFFF 的码点。Unicode 属性\p{PropertyName}或\p{EnumPropertyValue}反选用\P{PropertyName}或\P{EnumPropertyValue}。合法属性名见 Unicode 标准附录 UAX #44本文不展开外部链接仓库内 scripts/parse-extended-pictographic 展示了这类属性数据的解析处理。支持长短通用类别名如\p{Lu}、\p{Z}、\p{Symbol}、\p{BlkLatin_1_Sup}、\p{BlockLatin_1_Supplement}。Unicode 区块快捷写法用In前缀 下划线替换空格如\p{InLatin_1_Supplement}、\p{InYijing_Hexagram_Symbols}、\p{InAncient_Greek_Numbers}等价于\p{BlockLatin_1_Supplement}。额外支持的属性\p{Extended_Pictographic}UTS #35、\p{EmojiPresentationEmojiDefault}默认彩色 emoji 呈现但也可文本呈现、\p{EmojiPresentationTextDefault}默认文本呈现但也可 emoji 呈现、\p{EmojiPresentationText}仅文本呈现。属性名不区分大小写_与-视为等价。以下示例逐一说明这些写法原文完整保留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_CategoryOther_Letter}] [\p{Alnum}\p{General_CategoryOther_Letter}]* ; // 完整 Unicode 字母标识符 EMOJI : [\u{1F4A9}\u{1F926}] ; // 注意 UFFFF 的码点 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 { tokenVocabL; } 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.javanextToken的核心循环为记录字符流标记位置初始化_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置为SKIPnextToken的外层循环检测到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.javamore()_type MORE使nextToken循环继续匹配mode(m)直接设置_modepushMode(m)先把当前模式压入_modeStack再切换popMode()弹栈并恢复模式若栈为空会抛出EmptyStackException。注意事项原文明确弹出模式栈底层会抛出异常mode切换的是当前栈顶而pushMode/popMode维护整个栈多个more等价于一个more且位置无关。type()改写 Token 类型type()用于在匹配后将 Token 类型改写为指定值常用于把多个形态统一为一个 Tokenlexer 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 }这样生成的代码中会包含对应通道常量供解析器或监听器按名引用。八、词法规则选项caseInsensitivecaseInsensitive选项定义当前词法规则是否大小写不敏感参数为true或false若语法级已定义caseInsensitive规则级选项会覆盖其值options { caseInsensitivetrue; } STRING options { caseInsensitivefalse; } : N? \ (~\ | \\)* \; // 小写 n 不被允许在语法级caseInsensitivetrue的前提下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 { caseInsensitivetrue; }统一生效个别规则用规则级选项覆盖避免冗余声明验证多规则、多模式、命令与选项的组合行为可通过运行 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),仅供参考

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案