From fe430d4bcbac2ff3b22c9ff926e1407508d9629f Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 14 Oct 2026 08:05:13 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=90=8C=E5=90=8D=E6=8D=95=E8=8E=B7?= =?UTF-8?q?=E7=BB=84=E5=9B=9E=E5=BC=95=E4=B8=8E=20groups=20=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E6=94=B9=E6=9C=AB=E6=AC=A1=E5=B7=B2=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 42 +++--- Cargo.toml | 2 +- crates/oxide_builtins/src/regexp.rs | 77 ++++++++++- third_party/regress/src/api.rs | 120 +++++++++++++++++- third_party/regress/src/classicalbacktrack.rs | 21 +++ third_party/regress/src/emit.rs | 8 ++ third_party/regress/src/insn.rs | 5 + third_party/regress/src/ir.rs | 14 +- third_party/regress/src/optimizer.rs | 5 +- third_party/regress/src/parse.rs | 12 +- third_party/regress/src/pikevm.rs | 21 +++ third_party/regress/src/startpredicate.rs | 2 +- 12 files changed, 287 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dfc3c4db..4fb7b5a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.19.7" +version = "0.19.8" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.19.7" +version = "0.19.8" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.19.7" +version = "0.19.8" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.19.7" +version = "0.19.8" dependencies = [ "ansi_term", "bumpalo", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.19.7" +version = "0.19.8" dependencies = [ "lru", "oxide_bytecode", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.19.7" +version = "0.19.8" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1258,7 +1258,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.19.7" +version = "0.19.8" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1272,7 +1272,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.19.7" +version = "0.19.8" dependencies = [ "num-bigint", "oxc_allocator", @@ -1288,7 +1288,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.19.7" +version = "0.19.8" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1300,7 +1300,7 @@ dependencies = [ [[package]] name = "oxide_fold" -version = "0.19.7" +version = "0.19.8" dependencies = [ "num-bigint", "oxide_bytecode", @@ -1312,7 +1312,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.19.7" +version = "0.19.8" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1321,7 +1321,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.19.7" +version = "0.19.8" dependencies = [ "bumpalo", "dashmap", @@ -1335,7 +1335,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.19.7" +version = "0.19.8" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1347,7 +1347,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.19.7" +version = "0.19.8" dependencies = [ "tracing", "tracing-appender", @@ -1356,11 +1356,11 @@ dependencies = [ [[package]] name = "oxide_net" -version = "0.19.7" +version = "0.19.8" [[package]] name = "oxide_parser" -version = "0.19.7" +version = "0.19.8" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1375,7 +1375,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.19.7" +version = "0.19.8" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1391,7 +1391,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.19.7" +version = "0.19.8" dependencies = [ "num-bigint", "num-traits", @@ -1404,7 +1404,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.19.7" +version = "0.19.8" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1424,7 +1424,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.19.7" +version = "0.19.8" dependencies = [ "bumpalo", "dashmap", @@ -1437,7 +1437,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.19.7" +version = "0.19.8" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 401ee661..15d12df6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ ] [workspace.package] -version = "0.19.7" +version = "0.19.8" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_builtins/src/regexp.rs b/crates/oxide_builtins/src/regexp.rs index 9074ea73..a6c21a4d 100644 --- a/crates/oxide_builtins/src/regexp.rs +++ b/crates/oxide_builtins/src/regexp.rs @@ -18,7 +18,7 @@ pub(crate) fn build_groups_object(vm: &mut H, m: ®ress::Match, tex let groups_obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); let groups_ptr = unsafe { &mut *groups_obj }; - // 遍历所有命名捕获组,按名称设置属性(重复名称取首个已定义的出现)。 + // 遍历所有命名捕获组,按名称设置属性(重复名称取末次已定义的出现)。 for (name, range) in named { let name_si = vm.perm_intern(name); let value = match range { @@ -2222,3 +2222,78 @@ pub fn regexp_symbol_match_all(vm: &mut H, args: &[u8]) -> NativeResu vm.set_or_create_prop_value(wrapper_obj, unicode_si, JsValue::bool(is_unicode)); NativeResult::Ok(JsValue::from_js_object(wrapper)) } + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + + use oxide_vm::vm::Vm; + + /// 编译并执行单脚本,返回 VM 与完成值。 + fn eval(source: &str) -> Result<(Vm, JsValue), String> { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("parse: {:?}", e))?; + let module = oxide_compiler::compiler::Compiler::new() + .compile(&program) + .map_err(|e| format!("compile: {e}"))?; + let mut vm = Vm::new(); + let result = vm.run(&Arc::new(module))?; + Ok((vm, result)) + } + + /// 读对象自身属性值(按名称),无则 undefined。 + fn obj_prop(vm: &Vm, obj: &JsObject, name: &str) -> JsValue { + let si = vm.perm_intern(name); + match vm.get_own_property_slot(obj, si) { + Some(idx) => obj.get_prop_at(idx), + None => JsValue::undefined(), + } + } + + /// 读 exec 结果数组第 i 个元素的字符串值(按值)。 + fn arr_str(vm: &Vm, arr: &JsObject, i: usize) -> Option { + vm.lookup_str(arr.get_prop_at(i)) + } + + /// groups.x 取末次已定义出现;result 数组各出现独立交付(首出现被迭代 2 + /// 重入清槽,末次出现命中 b)。 + #[test] + fn groups_last_defined() { + let (vm, result) = eval(r"/(?:(?a)|(?b)){2}/.exec('ab')").unwrap(); + let arr = unsafe { &*result.as_js_object_ptr() }; + assert_eq!(arr_str(&vm, arr, 0).as_deref(), Some("ab")); + assert!(arr.get_prop_at(1).is_undefined()); + assert_eq!(arr_str(&vm, arr, 2).as_deref(), Some("b")); + let groups = obj_prop(&vm, arr, "groups"); + let groups_obj = unsafe { &*groups.as_js_object_ptr() }; + assert_eq!(vm.lookup_str(obj_prop(&vm, groups_obj, "x")).as_deref(), Some("b")); + } + + /// 同名回引读末次已定义:迭代 2 中末次出现命中 b,回引读 b 命中 "aabb"。 + #[test] + fn backref_last_defined_aabb() { + let (vm, result) = eval(r"/(?:(?:(?a)|(?b))\k){2}/.exec('aabb')").unwrap(); + let arr = unsafe { &*result.as_js_object_ptr() }; + assert_eq!(arr_str(&vm, arr, 0).as_deref(), Some("aabb")); + assert!(arr.get_prop_at(1).is_undefined(), "首出现重入未命中"); + assert_eq!(arr_str(&vm, arr, 2).as_deref(), Some("b")); + let groups = obj_prop(&vm, arr, "groups"); + let groups_obj = unsafe { &*groups.as_js_object_ptr() }; + assert_eq!(vm.lookup_str(obj_prop(&vm, groups_obj, "x")).as_deref(), Some("b")); + } + + /// 全部出现未定义时回引空匹配;末次已定义值不匹配时整体失败(不回落空匹配)。 + #[test] + fn backref_undefined_empty_and_no_fallthrough() { + let (vm, result) = eval(r"/^(?:(?x)|(?y)|z)\k$/.exec('z')").unwrap(); + let arr = unsafe { &*result.as_js_object_ptr() }; + assert_eq!(arr_str(&vm, arr, 0).as_deref(), Some("z")); + assert!(arr.get_prop_at(1).is_undefined()); + assert!(arr.get_prop_at(2).is_undefined()); + + // 末次已定义为 x(位 2 为 y,不匹配 x),整体应失败。 + let (_, result) = eval(r"/(?:(?y)|(?x)){2}\k/.exec('yxy')").unwrap(); + assert!(result.is_null(), "末次已定义值不匹配时整体应失败"); + } +} diff --git a/third_party/regress/src/api.rs b/third_party/regress/src/api.rs index e5f89d50..be8e1ec7 100644 --- a/third_party/regress/src/api.rs +++ b/third_party/regress/src/api.rs @@ -339,15 +339,14 @@ impl<'m> Iterator for NamedGroups<'m> { continue; } - // This is the first occurrence of this name. Find the best range value. - // Prefer a Some value over None when there are duplicate names. + // This is the first occurrence of this name. Resolve the value to + // the last occurrence of this name that matched: a later duplicate + // overrides an earlier one only when it is defined. let mut best_range = self.mat.captures[idx].clone(); for check_idx in (idx + 1)..end { if self.mat.group_names[check_idx].as_ref() == name { - // Found a duplicate name. Prefer a Some value over None. - if best_range.is_none() && self.mat.captures[check_idx].is_some() { - best_range = self.mat.captures[check_idx].clone(); - break; // Stop once we find a Some value + if let Some(range) = self.mat.captures[check_idx].as_ref() { + best_range = Some(range.clone()); } } } @@ -1272,3 +1271,112 @@ mod utf16_tests { assert_eq!(str_matches(&re, text), utf16_matches(&re, &to_units(text))); } } + +#[cfg(test)] +mod named_groups_tests { + use super::Regex; + + /// 同名双定义(循环内各迭代命中不同出现):named_groups 取末次已定义出现, + /// 键序为首现源序。 + #[test] + fn named_groups_last_defined_wins() { + let re = Regex::new(r"(?:(?a)|(?b)){2}").unwrap(); + let m = re.find("ab").unwrap(); + let named: Vec<_> = m.named_groups().collect(); + assert_eq!(named, vec![("x", Some(1..2))], "末次已定义出现(b)胜出"); + // 首出现被迭代 2 重入清槽,末次出现命中 b。 + assert_eq!(m.captures, vec![None, Some(1..2)], "result 数组各出现独立交付"); + } + + /// 末次出现未定义时回退至更早已定义出现。 + #[test] + fn named_groups_falls_back_to_earlier_defined() { + let re = Regex::new(r"(?:(?a)|(?b)|c){2}").unwrap(); + let m = re.find("ca").unwrap(); + let named: Vec<_> = m.named_groups().collect(); + assert_eq!(named, vec![("x", Some(1..2))], "末次出现未定义,回退首出现"); + assert_eq!(m.captures, vec![Some(1..2), None]); + } + + /// 全部出现未定义时取 undefined。 + #[test] + fn named_groups_all_undefined() { + let re = Regex::new(r"(?:(?a)|(?b)|c){2}").unwrap(); + let m = re.find("cc").unwrap(); + let named: Vec<_> = m.named_groups().collect(); + assert_eq!(named, vec![("x", None)]); + assert_eq!(m.captures, vec![None, None]); + } + + /// 同名回引读末次已定义出现:迭代 2 中末次出现命中 b,回引读 b 命中 "aabb"。 + #[test] + fn backref_reads_last_defined() { + let re = Regex::new(r"(?:(?:(?a)|(?b))\k){2}").unwrap(); + let m = re.find("aabb").unwrap(); + assert_eq!(m.range(), 0..4); + assert_eq!(m.captures, vec![None, Some(2..3)], "各出现独立交付"); + } + + /// 单轮同名回引:末次已定义分别为首出现(aa)与次出现(bb)。 + #[test] + fn backref_single_round() { + let re = Regex::new(r"(?:(?a)|(?b))\k").unwrap(); + let m = re.find("aa").unwrap(); + assert_eq!(m.range(), 0..2); + assert_eq!(m.captures, vec![Some(0..1), None]); + let m = re.find("bb").unwrap(); + assert_eq!(m.range(), 0..2); + assert_eq!(m.captures, vec![None, Some(0..1)]); + } + + /// 全部出现未定义时回引空匹配;末次已定义值不匹配时整体失败(不回落空匹配, + /// 也不回落更早已定义出现)。 + #[test] + fn backref_undefined_empty_and_no_fallthrough() { + let re = Regex::new(r"^(?:(?x)|(?y)|z)\k$").unwrap(); + let m = re.find("z").unwrap(); + assert_eq!(m.range(), 0..1); + assert_eq!(m.captures, vec![None, None]); + assert!(re.find("zz").is_none(), "末次已定义值不匹配时整体失败"); + + // 末次已定义为 x(位 2 为 y,不匹配 x);更早已定义为 y(位 2 亦不匹配)。 + let re = Regex::new(r"(?:(?y)|(?x)){2}\k").unwrap(); + assert!(re.find("yxy").is_none(), "末次已定义值不匹配时整体失败"); + // 对照:末次已定义值匹配时命中。 + let m = re.find("yxx").unwrap(); + assert_eq!(m.range(), 0..3); + } + + /// 循环重入清槽后全部未定义:回引空匹配,结果数组各出现 undefined。 + #[test] + fn backref_loop_reentry_clears() { + let re = Regex::new(r"^(?:(?x)|(?y)|z){2}\k$").unwrap(); + let m = re.find("xz").unwrap(); + assert_eq!(m.range(), 0..2); + assert_eq!(m.captures, vec![None, None]); + assert!(re.find("xzx").is_none()); + } + + /// 未定义组回引空匹配(单出现面不变)。 + #[test] + fn backref_single_occurrence_undefined_empty() { + let re = Regex::new(r"(?x)|(?:zy\k)").unwrap(); + let m = re.find("zy").unwrap(); + assert_eq!(m.range(), 0..2); + assert_eq!(m.captures, vec![None]); + } + + /// PikeVM 执行器与回退执行器对同名回引口径一致。 + #[cfg(feature = "backend-pikevm")] + #[test] + fn pikevm_backref_agrees() { + use crate::backends::PikeVMExecutor; + use crate::exec::{Executor, Matches}; + let re = Regex::new(r"(?:(?:(?a)|(?b))\k){2}").unwrap(); + let m = Matches::new(PikeVMExecutor::new(&re.cr, "aabb"), 0) + .next() + .expect("pikevm 应命中 aabb"); + assert_eq!(m.range(), 0..4); + assert_eq!(m.captures, vec![None, Some(2..3)]); + } +} diff --git a/third_party/regress/src/classicalbacktrack.rs b/third_party/regress/src/classicalbacktrack.rs index 5d4ce9d2..fd30d193 100644 --- a/third_party/regress/src/classicalbacktrack.rs +++ b/third_party/regress/src/classicalbacktrack.rs @@ -886,6 +886,27 @@ impl<'a, Input: InputIndexer> MatchAttempter<'a, Input> { next_or_bt!(matched) } + Insn::BackRefLastDefined(cg_indices) => { + // Scan the occurrences in reverse source order and match the + // text of the last occurrence that is defined; a later + // occurrence overrides an earlier one only when it matched. + let mut matched = true; + for &cg_idx in cg_indices.iter().rev() { + let cg = self.s.groups.mat(cg_idx as usize); + if let Some(orig_range) = cg.as_range() { + if re.flags.icase { + matched = matchers::backref_icase(input, dir, orig_range, &mut pos); + } else { + matched = matchers::backref(input, dir, orig_range, &mut pos); + } + break; + } + } + // No occurrence is defined and so the match succeeds (ES6 + // 21.2.2.9). + next_or_bt!(matched) + } + &Insn::Lookahead { negate, start_group, diff --git a/third_party/regress/src/emit.rs b/third_party/regress/src/emit.rs index 376e2035..6cc15d0e 100644 --- a/third_party/regress/src/emit.rs +++ b/third_party/regress/src/emit.rs @@ -340,6 +340,14 @@ impl Emitter { self.emit_insn(Insn::BackRef(group - 1)) } + Node::BackRefLastDefined(groups) => { + debug_assert!(!groups.is_empty(), "Should not be empty"); + // -1 because group indices are 1-based in the IR. + let indices: Vec = + groups.iter().map(|g| (*g - 1) as CaptureGroupID).collect(); + self.emit_insn(Insn::BackRefLastDefined(indices)) + } + Node::ByteSet(bytes) => self.emit_byte_set_insn(bytes), Node::CharSet(chars) => { diff --git a/third_party/regress/src/insn.rs b/third_party/regress/src/insn.rs index 3e802083..ebe5d7a0 100644 --- a/third_party/regress/src/insn.rs +++ b/third_party/regress/src/insn.rs @@ -102,6 +102,11 @@ pub enum Insn { /// Perform a backreference match. BackRef(u32), + /// Perform a backreference match against the last-defined occurrence of a + /// name shared by multiple capture groups. Indices are 0-based, in source + /// order; if no occurrence is defined the match succeeds. + BackRefLastDefined(Vec), + /// Match the next character against the bracket contents, stored at the given index in the CompiledRegex. Bracket(usize), diff --git a/third_party/regress/src/ir.rs b/third_party/regress/src/ir.rs index 55d5cfa7..673aa627 100644 --- a/third_party/regress/src/ir.rs +++ b/third_party/regress/src/ir.rs @@ -84,10 +84,14 @@ pub enum Node { /// A backreference. /// Indices are 1-based. That is, it matches JS syntax: \1 is the first capture group, /// not everything. - /// A backreference that logically may match multiple capture groups (through shared names) - /// are emitted through alternations of backreferences. BackRef(u32), + /// A backreference to a name shared by multiple capture groups. + /// Indices are 1-based, one per occurrence in source order. At match time it + /// matches the text of the last occurrence that is defined; if none is + /// defined, it matches the empty string. + BackRefLastDefined(Vec), + /// A bracket. Bracket(BracketContents), @@ -232,6 +236,7 @@ impl Node { } &Node::WordBoundary { invert } => Node::WordBoundary { invert }, &Node::BackRef(idx) => Node::BackRef(idx), + Node::BackRefLastDefined(groups) => Node::BackRefLastDefined(groups.clone()), Node::Bracket(bc) => Node::Bracket(bc.clone()), // Do not reverse into lookarounds, they already have the right sense. Node::LookaroundAssertion { @@ -308,6 +313,7 @@ where | Node::CharSet(..) | Node::WordBoundary { .. } | Node::BackRef { .. } + | Node::BackRefLastDefined { .. } | Node::Bracket { .. } | Node::MatchAny | Node::MatchAnyExceptLineTerminator @@ -381,6 +387,7 @@ where | Node::Anchor { .. } | Node::WordBoundary { .. } | Node::BackRef { .. } + | Node::BackRefLastDefined { .. } | Node::Bracket { .. } => {} Node::Cat(nodes) => { nodes.iter_mut().for_each(|node| self.process(node)); @@ -548,6 +555,9 @@ fn display_node(node: &Node, depth: usize, f: &mut fmt::Formatter) -> fmt::Resul &Node::BackRef(group) => { writeln!(f, "BackRef {:?} ", group)?; } + Node::BackRefLastDefined(groups) => { + writeln!(f, "BackRefLastDefined {:?} ", groups)?; + } Node::Bracket(contents) => { writeln!(f, "Bracket {:?}", contents)?; } diff --git a/third_party/regress/src/optimizer.rs b/third_party/regress/src/optimizer.rs index 39a23884..58a34d37 100644 --- a/third_party/regress/src/optimizer.rs +++ b/third_party/regress/src/optimizer.rs @@ -160,7 +160,10 @@ fn remove_empties(n: &mut Node, _w: &Walk) -> PassAction { // Capture groups could in principle be optimized if they only match empties. PassAction::Keep } - Node::WordBoundary { .. } | Node::BackRef { .. } | Node::Bracket { .. } => PassAction::Keep, + Node::WordBoundary { .. } + | Node::BackRef { .. } + | Node::BackRefLastDefined { .. } + | Node::Bracket { .. } => PassAction::Keep, Node::LookaroundAssertion { negate, contents, .. } => { diff --git a/third_party/regress/src/parse.rs b/third_party/regress/src/parse.rs index a4057064..49aa7445 100644 --- a/third_party/regress/src/parse.rs +++ b/third_party/regress/src/parse.rs @@ -1689,15 +1689,9 @@ where ir::Node::BackRef(group_indices[0] + 1) } _ => { - // Unusual case of multiple groups sharing a name: the backref should try each in turn. - // Lower to alternations of backreferences. Reverse to keep it right-associative: a | (b | (c | d))... - let backrefs = group_indices - .iter() - .rev() - .map(|group_index| ir::Node::BackRef(*group_index + 1)); - backrefs - .reduce(|right, left| ir::Node::Alt(Box::new(left), Box::new(right))) - .unwrap() + // Multiple groups sharing a name: the backref reads the + // last occurrence that is defined at match time. + ir::Node::BackRefLastDefined(group_indices.iter().map(|i| i + 1).collect()) } }; Ok(node) diff --git a/third_party/regress/src/pikevm.rs b/third_party/regress/src/pikevm.rs index 84ce89c3..2fa120cb 100644 --- a/third_party/regress/src/pikevm.rs +++ b/third_party/regress/src/pikevm.rs @@ -238,6 +238,27 @@ fn try_match_state( nextinsn_or_fail!(matched) } + Insn::BackRefLastDefined(group_indices) => { + // Scan the occurrences in reverse source order and match the text of + // the last occurrence that is defined; a later occurrence overrides + // an earlier one only when it matched. + let mut matched = true; + for &group_idx in group_indices.iter().rev() { + let group = &s.groups[group_idx as usize]; + if let Some(orig_range) = group.as_range() { + if re.flags.icase { + matched = matchers::backref_icase(input, dir, orig_range, &mut s.pos); + } else { + matched = matchers::backref(input, dir, orig_range, &mut s.pos); + } + break; + } + } + // No occurrence is defined, and therefore the match succeeds + // (ES6 21.2.2.9). + nextinsn_or_fail!(matched) + } + &Insn::Lookahead { negate, start_group: _, diff --git a/third_party/regress/src/startpredicate.rs b/third_party/regress/src/startpredicate.rs index 217327ce..aee8740c 100644 --- a/third_party/regress/src/startpredicate.rs +++ b/third_party/regress/src/startpredicate.rs @@ -123,7 +123,7 @@ fn compute_start_predicate(n: &Node) -> Option { Node::Empty => arbitrary, Node::Goal => arbitrary, - Node::BackRef(..) => arbitrary, + Node::BackRef(..) | Node::BackRefLastDefined(..) => arbitrary, Node::CharSet(chars) => { // Pick the first bytes out.