GraalVM TRegex 版本演进与语言集成指南:从 Changelog 到源码的完整解读

原创2026-09-20 23:08:411,455 阅读
文章标签:编译器JIT编译语言运行时

GraalVM TRegex 版本演进与语言集成指南:从 Changelog 到源码的完整解读

TRegex 是 GraalVM 中基于有限状态自动机的通用正则表达式引擎,为 Truffle 语言(如 JavaScript、Python、Ruby)提供正则表达式支持。本文以 regex/CHANGELOG.md 为主线,结合 TRegex 源码(regex/README.md、RegexLanguage.java、RegexOptions.java)深入讲解其架构原理、API 演进、配置选项与集成方式,帮助语言实现者与引擎维护者全面理解 TRegex 的能力边界与演进脉络。

TRegex 核心架构:为什么选择自动机而不是回溯

从 NFA 到 DFA 的编译流程

TRegex 与大多数基于回溯的正则引擎不同,其核心思路是"编译"而非"匹配":

  1. 正则表达式被解析为语法树;
  2. 语法树被翻译成非确定性有限状态自动机(NFA);
  3. 通过 powerset 构造(子集构造法)将 NFA 展开为确定性有限状态自动机(DFA);
  4. 匹配输入串时执行该 DFA,并借助 GraalVM 编译器与 Truffle API 将 DFA 解释执行编译为高效的机器码。

从源码看,这一流程在 RegexLanguage.java 的解析入口中体现:RegexParser 负责解析与构造 AST,TRegexCompiler 负责生成执行节点,RegexObject.java 中的 getLabel() 方法按执行节点类型返回 literal、backtracker、nfa 或 dfa,直观展示了 TRegex 内部可选择的多种执行路径。

线性时间匹配的优势与代价

自动机方案的显著优势是:判断是否匹配只需对输入串做单次扫描,所有可行的备选匹配路径被同时考虑,运行时稳定可预测(与输入长度线性相关),避免了回溯引擎在对抗性输入下可能出现的指数级执行时间。其代价是无法覆盖回溯引擎的某些高级特性,例如反向引用(backreference,如 /([a-z]+)=\1/)和非字面量的 lookbehind(如 /(?<=a.+)b/)。因此 TRegex 只处理 ECMAScript 正则的子集,其余特性必须交给回溯引擎处理:TRegex 自带 ECMAScript 回溯 fallback 引擎,其他语言可能需要自行提供自定义 fallback 引擎(详见 regex/README.md 的 Feature Support 一节)。

从测试代码可以印证这一边界,例如 JsTests.java 中的 testForceLinearExecution:

  • (a*)b\1 含反向引用,在 ForceLinearExecution 选项下被拒绝(expectUnsupported);
  • .*a{1,200000}.* 的大数量词被拒绝;
  • .*b(?!a_) 的负向先行断言被拒绝。

语言集成:通过 Truffle interop 使用 TRegex

解析正则表达式

TRegex 的 API 通过 Truffle 的 interop 机制暴露,TruffleObject 和 AST 节点归属于一个新的 Truffle 语言 RegexLanguage(ID 为 regex,MIME 类型为 application/tregex)。解析一个正则时,TRegex 返回一个 CallTarget,调用后可能产生三种结果:

  • TruffleNull 对象:表示 TRegex 无法处理该正则,应回退到其他引擎;
  • 已编译正则 RegexObject:可用于后续匹配;
  • RegexSyntaxException(AbstractTruffleException,异常类型为 PARSE_ERROR):表示语法错误。

解析示例(来自 RegexLanguage.java 的 javadoc):

Object regex;
try {
    regex = getContext().getEnv().parseInternal(
        Source.newBuilder("regex", "Flavor=ECMAScript/(a|(b))c/i", "myRegex")
              .mimeType("application/tregex").internal(true).build()).call();
} catch (AbstractTruffleException e) {
    if (InteropLibrary.getUncached().getExceptionType(e) == ExceptionType.PARSE_ERROR) {
        // handle parser error
    } else {
        // fatal error, this should never happen
    }
}
if (InteropLibrary.getUncached().isNull(regex)) {
    // regex is not supported by TRegex, fall back to a different regex engine
}

TRegex 从 Graal JavaScript 实现中独立出来,现在可以供其他语言的实现者直接使用(详见 regex/README.md)。语言集成时可参考 Graal JavaScript 的接入方式:通过 RegexCompilerInterface#compile 发起编译请求,在 JSRegExpExecBuiltinNode 中执行已编译正则,TRegexUtil 则展示了如何为 TruffleLanguage 对象的成员编写 interop 访问包装。

已编译正则对象 RegexObject 的成员

RegexObject(RegexObject.java)暴露以下只读成员(KEYS 常量):

成员 类型 说明
pattern String 已编译正则的源字符串
flags TruffleObject 传给编译器的标志集合,类型随 flavor 不同而异(RegexFlags/JavaFlags/OracleDBFlags/PythonFlags/RubyFlags)
groupCount int 捕获组数量(含组 0)
groups TruffleObject 具名捕获组名到组号的映射;无具名组时为 null。Ruby 等允许重名时值为数组
exec executable 执行匹配的方法,返回 RegexResult
execBoolean executable 布尔匹配模式下的执行方法,只返回是否匹配
isBacktracking boolean 该正则是否使用回溯执行,可能带来指数级运行时间

exec 方法支持两种签名:exec(TruffleString, int) 与 exec(TruffleString, int, int, int, int),后者的参数依次为 input、fromIndex(搜索起始位置,lookbehind 允许越过该索引回看至 regionFrom)、toIndex(搜索终止位置,当前必须等于 regionTo)、regionFrom(硬起始边界,^/\A 会匹配该索引)、regionTo(硬结束边界,$/\Z/\z 会匹配该索引)。利用 regionFrom/regionTo 可将搜索限制在输入串的子串范围内。所有索引参数都会转换为 int。exec 的首次调用可能因为按需编译而出现较长的延迟。

结果对象 RegexResult 的成员

RegexResult 表示一次匹配的结果,来自 exec 的返回值:

  • boolean isMatch:是否找到匹配;
  • int getStart(int groupNumber):捕获组起始位置;无匹配时未定义;组 0 表示整个表达式;某组未匹配时返回 -1;
  • int getEnd(int groupNumber):捕获组结束位置;语义同 getStart。

匹配用法伪代码示例:

regex = <matcher from previous example>
assert(regex.pattern == "(a|(b))c")
assert(regex.flags.ignoreCase == true)
assert(regex.groupCount == 3)

result = regex.exec("xacy", 0)
assert(result.isMatch == true)
assertEquals([result.getStart(0), result.getEnd(0)], [ 1,  3])
assertEquals([result.getStart(1), result.getEnd(1)], [ 1,  2])
assertEquals([result.getStart(2), result.getEnd(2)], [-1, -1])

result2 = regex.exec("xxx", 0)
assert(result2.isMatch == false)
// result2.getStart(...) and result2.getEnd(...) are undefined

RegexOptions 配置体系:从源码选项到 Source 内嵌选项

RegexOptions(RegexOptions.java)定义 TRegex 如何解释一次解析请求。除了源码中的 @Option 注解(可通过 regexDummyLang.Xxx 等引擎选项配置)外,还有一部分选项可以内嵌在正则源码字符串中。

Source 内嵌选项格式的演进

TRegex 期望的源码格式为 options/regex/flags,其中 options 是逗号分隔的键值对列表,/regex/flags 等价于 JavaScript/Ruby 中常见的正则字面量格式。

重要演进(Version 25.0.0):TRegex 新增了对 Truffle Source Options 的支持,内嵌在正则源码字符串中的选项被标记为废弃。源码格式相应地从 options/regex/flags 变为 /regex/flags——也就是说,不带内嵌选项头的普通正则字面量成为推荐形式,而选项改由 Truffle 的 Source Options / 引擎选项机制提供。从 RegexOptions.java 的 Builder.parseOptions() 可以看出兼容性处理:当 src 以 / 开头时,直接走新路径,所有布尔选项从 optionValues(Truffle OptionValues)读取;否则仍解析旧的内嵌 key=value 语法。

旧式内嵌选项的解析逻辑支持如下前缀(源码 parseOptions() 的 switch 分支):A(AlwaysEager)、B(BooleanMatch)、D(DumpAutomata)、E(Encoding)、F(Flavor)、G(GenerateDFAImmediately/GenerateInput)、I(IgnoreAtomicGroups)、J(JavaJDKVersion)、M(MaxBackTrackerJITSize/MaxDFASize/MatchingMode/MustAdvance)、P(PythonMethod/PythonLocale)、R(RegressionTestMode)、S(StepExecution)、U(U180EWhitespace/UTF16ExplodeAstralSymbols)、V(Validate)。布尔选项取 true/false,数值选项(如 MaxDFASize、JavaJDKVersion)有范围校验(如 JavaJDKVersion 必须 ≥ 21,短整型不超过 Short.MAX_VALUE)。

选项清单

以下选项除 Flavor、Encoding 外均为布尔型且默认 false:

用户选项(OptionCategory.USER)

选项 取值 说明
Flavor ECMAScript(默认)、Python、Ruby、OracleDB、JavaUtilPattern 指定正则方言;从 FlavorOption 枚举及 FLAVOR_CACHE 可见其实现位于 flavor 目录下的各 *FlavorProvider(JS、Python、Ruby、OracleDB、Java)
Encoding UTF-8、UTF-16、UTF-16-RAW(默认)、UTF-32、BYTES、LATIN-1 指定匹配的字符串编码;BYTES 等价于 LATIN-1。注意 EncodingOption 的默认值源码中为 UTF_16_RAW
MatchingMode search(默认)、match、fullmatch 隐式锚定模式:search 在输入任意位置搜索;match 锚定在起始索引;fullmatch 同时锚定起始与结束索引。其前身为 Python 方言下的 PythonMethod
BooleanMatch true/false 不报告捕获组,只返回布尔结果(对应 execBoolean 与 isBooleanMatch())
ForceLinearExecution true/false (Version 25.0.0 新增,实验性)对 TRegex 无法线性时间执行的正则一律 bailout 拒绝。测试见 JsTests.java 与 PythonTests.java
JavaJDKVersion 21-25(默认 24) Java flavor 的 JDK 兼容版本

专家/性能选项(OptionCategory.EXPERT)

  • Validate:不生成匹配器,仅检查语法错误(isValidate());
  • U180EWhitespace:将 0x180E MONGOLIAN VOWEL SEPARATOR 视为 \s 的一部分,兼容 Unicode 6.3 之前的旧标准(如 ECMAScript 6 及更早),仅适用于 ECMAScript flavor;
  • AlwaysEager:不生成惰性匹配器(惰性指可能延迟计算 RegexResult 的属性);
  • IgnoreAtomicGroups:将原子组视为普通组(Ruby 正则中常见),见 Version 21.3.0 的变更记录;
  • GenerateDFAImmediately:解析后立即生成 DFA 匹配器(默认 DFA 是惰性生成);
  • MaxDFASize:DFA bailout 阈值(默认 TRegexOptions.TRegexMaxDFATransitions);
  • MaxBackTrackerJITSize:回溯器 JIT 编译 bailout 阈值(默认 TRegexMaxBackTrackerMergeExplodeSize);
  • MaxParserTreeSize:解析树 bailout 阈值(默认 TRegexParserTreeMaxSize);
  • QuantifierUnrollLimitSingleCC / QuantifierUnrollLimitGroup:单字符类量词 / 组量词展开上限;
  • PythonLocale:Python flavor 的区域敏感特性使用的 locale(实验性,默认空字符串)。

调试/内部选项(OptionCategory.INTERNAL)

  • RegressionTestMode:运行所有支持的匹配器变体并校验结果一致(内部测试用);
  • DumpAutomata:将解析树、NFA、DFA 以 JSON、GraphViz、LaTeX 格式导出到磁盘(内部测试用);
  • StepExecution:导出所有 DFA 匹配器运行的追踪信息;
  • MustAdvance:强制匹配器至少前进一个字符——要么找到非零宽匹配,要么跳过至少一个字符后再匹配。该选项在 RegexOptions.java 中有 isMustAdvance() 说明,Python 测试 PythonTests.java 中通过 regexDummyLang.MustAdvance=true 使用,Python 正则中对应 re 模块的"匹配必须推进"语义;
  • GenerateInput:尝试生成该正则能匹配的字符串并返回,用于内部测试。

已废弃选项:UTF16ExplodeAstralSymbols(在源码中标记为 deprecated,isUTF16ExplodeAstralSymbols() 仍有实现,含义是为每个 16 位 char 生成 DFA 状态而非每个码点,可能提升特定场景性能但增加状态爆炸风险)。

调试日志器

Version 1.0.0 RC10 引入编译器动作日志能力,regex 命名空间下的日志器定义在 Loggers.java,可通过如下方式启用(对应 RC10 中的 --log.regex.Phases.level=ALL 形式):

-Dpolyglot.log.regex.SwitchToEager.level=ALL
-Dpolyglot.log.regex.TotalCompilationTime.level=ALL
-Dpolyglot.log.regex.Phases.level=ALL
-Dpolyglot.log.regex.BailoutMessages.level=ALL
-Dpolyglot.log.regex.AutomatonSizes.level=ALL
-Dpolyglot.log.regex.CompilerFallback.level=ALL
-Dpolyglot.log.regex.InternalErrors.level=ALL
-Dpolyglot.log.regex.TRegexCompilations.level=ALL

这些日志器(LOG_SWITCH_TO_EAGER、LOG_TOTAL_COMPILATION_TIME、LOG_BAILOUT_MESSAGES、LOG_AUTOMATON_SIZES、LOG_COMPILER_FALLBACK、LOG_INTERNAL_ERRORS、LOG_TREGEX_COMPILATIONS 等)分别对应"切换到 eager 模式"、"总编译耗时"、"编译阶段"、"bailout 消息"、"自动机规模"、"编译器回退"、"内部错误"、"编译事件"等调试维度,是排查 TRegex 编译行为的重要工具。

语言特性演进时间线(按版本解读)

以下按 regex/CHANGELOG.md 梳理对语言实现者(integrator)有影响的 API 与能力变更:

25.x:Unicode 17 与线性执行保障

  • 25.1.3:JS 与 Python 正则 flavor 更新至 Unicode 17.0。
  • 25.0.0:布尔匹配模式下 DFA 匹配器支持有界量词(嵌套有界量词及与部分 lookaround 的组合仍不支持);新增 Truffle Source Options 支持,源码内嵌选项废弃,源码格式从 options/regex/flags 变为 /regex/flags;新增 ForceLinearExecution 选项(对无法线性时间执行的正则 bailout)。

24.x:正则修饰符与原子组

  • 24.2.0:实现 ECMAScript 正则的模式修饰符(Regular Expression Pattern Modifiers)提案,即内联修饰符 (?ims-ims:...) 之类能力。
  • 24.0.0:Python 正则支持原子组(atomic groups)与占有量词(possessive quantifiers)。

23.x:Unicode Sets 与具名捕获组

  • 23.1.0:ECMAScript 正则支持 Unicode sets 模式(v 标志);支持重复的具名捕获组。
  • 23.0.0:Unicode 数据(大小写折叠、字符属性)更新至 Unicode 15;Flavor=PythonStr/Flavor=PythonBytes 废弃,改用 Flavor=Python,Encoding=UTF-32 与 Flavor=Python,Encoding=LATIN-1;移除 execBytes(byte[] input, int fromIndex) 入口点;移除将 TruffleObject 的字符/码点作为数组元素的支持;Python 与 Ruby 正则支持条件反向引用(如 (foo)(?(1)bar|no_foo))与大小写不敏感反向引用;支持区域敏感的 Python 正则;TRegex 不再默认包含在 GraalVM 发行版中,需通过 gu install regex 作为组件安装。

22.x:回溯引擎能力扩展与布尔匹配

  • 22.2.0:Ruby 正则通过回溯支持原子组与占有量词。
  • 22.1.0:RegexResult 新增 lastGroup 字段(Python 方言下跟踪,表示最后匹配的捕获组,测试见 PythonTests.java 的 lastGroupNotSet 与 lookbehind 交互用例);新增布尔匹配模式(execBoolean,在确定不会查询捕获组时省去 RegexResult 对象);新增 MustAdvance 与 PythonMode 选项;Python 支持具名字符转义;Ruby 支持有限的 \G 锚点;移除 Python 正则的 sticky 标志;Ruby 支持非递归子表达式调用。

21.x 及更早:从结果对象重构到 ECMAScript 2020

  • 22.0.0:新增 ASCII 编码(将码点匹配范围限制在 [0x00, 0x7f]);Unicode 数据更新至 Unicode 14。
  • 21.3.0:Ruby 正则支持大小写不敏感匹配;新增 IgnoreAtomicGroups 选项;编译后的正则导出 isBacktracking 布尔成员,用于判断该正则是否可能回溯并导致指数级运行时间。
  • 21.2.0:ECMAScript 正则支持 hasIndices(d)标志。
  • 20.2.0:引入 UTF-16 字符串的即时解码(on-the-fly decoding)。
  • 20.1.0:通过新的内置回溯引擎完整支持 ECMAScript 2020 正则特性。
  • 1.0.0 RC15:从结果对象中移除 regex、input、start、end 属性,改用 getStart(groupNumber)/getEnd(groupNumber) 方法;正则语法异常改为普通 TruffleException,语言实现者不应再对 TRegex 引入源码级依赖;groupCount 属性从结果对象移至已编译正则对象。
  • 1.0.0 RC10:新增编译器动作日志(见上文"调试日志器"一节)。

实用建议与版本迁移要点

  1. 升级到 25.x 前:检查语言实现中是否依赖源码内嵌选项语法(options/regex/flags),若依赖则需迁移到 Truffle Source Options 方式,新源码格式为 /regex/flags。
  2. Python 集成:旧式 Flavor=PythonStr/Flavor=PythonBytes 已废弃,应改为 Flavor=Python 配合 Encoding=UTF-32/LATIN-1;若需要 execBytes 能力需寻找替代方案(该入口点已在 23.0.0 移除);注意 lastGroup 仅在 Python 方言下被跟踪。
  3. 安全性:如需保证匹配耗时可控,可启用 ForceLinearExecution(25.0.0+),对含反向引用、负向 lookaround、超大数量词等无法线性执行的正则直接拒绝;对于已有正则可通过 isBacktracking 成员(21.3.0+)在运行时判断其是否走回溯路径。
  4. 安装方式:自 23.0.0 起 TRegex 不再是 GraalVM 默认组件,需要执行 gu install regex 安装后再集成。

以上能力均可通过 regex 测试目录下的 JsTests、PythonTests、RubyTests、JavaUtilPatternTests、RegexOptionsTest 等测试用例进一步验证,它们是理解 TRegex 行为边界的第一手资料。

登录后查看全文
graal