GraalVM TRegex 版本演进与语言集成指南:从 Changelog 到源码的完整解读
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 与大多数基于回溯的正则引擎不同,其核心思路是"编译"而非"匹配":
- 正则表达式被解析为语法树;
- 语法树被翻译成非确定性有限状态自动机(NFA);
- 通过 powerset 构造(子集构造法)将 NFA 展开为确定性有限状态自动机(DFA);
- 匹配输入串时执行该 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:新增编译器动作日志(见上文"调试日志器"一节)。
实用建议与版本迁移要点
- 升级到 25.x 前:检查语言实现中是否依赖源码内嵌选项语法(
options/regex/flags),若依赖则需迁移到 Truffle Source Options 方式,新源码格式为/regex/flags。 - Python 集成:旧式
Flavor=PythonStr/Flavor=PythonBytes已废弃,应改为Flavor=Python配合Encoding=UTF-32/LATIN-1;若需要execBytes能力需寻找替代方案(该入口点已在 23.0.0 移除);注意lastGroup仅在 Python 方言下被跟踪。 - 安全性:如需保证匹配耗时可控,可启用
ForceLinearExecution(25.0.0+),对含反向引用、负向 lookaround、超大数量词等无法线性执行的正则直接拒绝;对于已有正则可通过isBacktracking成员(21.3.0+)在运行时判断其是否走回溯路径。 - 安装方式:自 23.0.0 起 TRegex 不再是 GraalVM 默认组件,需要执行
gu install regex安装后再集成。
以上能力均可通过 regex 测试目录下的 JsTests、PythonTests、RubyTests、JavaUtilPatternTests、RegexOptionsTest 等测试用例进一步验证,它们是理解 TRegex 行为边界的第一手资料。