Skip to content

Commit 023b1e7

Browse files
ahrzbclaude
andcommitted
feat(specializer): wave-B regexp family — rust-regex behind an RE2 translation layer, corpus 484 -> 505 (TASK-53)
The engine is regex 1.x with the measured parity recipe (pins-waveB/ re2-vs-rust-regex.json): RegexBuilder::octal(true), default Unicode mode (unicode(false) BREAKS (?i) folding parity — measured), and a bind-time pattern rewrite in retrans.rs closing the whole Perl-class gap (\d -> (?-u:\d) etc, in-class variants included) plus a reject list for the irreconcilables (\B, (?<name>), duplicate group names, bounds > 1000, stacked quantifiers — the last silently WRONG in rust, not just error-shaped different). Replacement templates translate \N -> ${N}, $ -> $$, with RE2's invalid-rewrite quirks resolved at bind (out-of- range backref = identity; global bad escape = consume-with-prefix). Program grows a prepare-time regex table (print/parse round-trips it); ReMatch/ReExtract/ReReplace run on both backends. Frontend serves regexp_matches (SEARCH) / regexp_full_match / regexp_extract (''-on-miss, flat 0..9 group check, NULL-group -> '') / regexp_replace (backslash backrefs, 'g', NULL options -> NULL — the pinned asymmetry); ~ / !~ are FULL match (NOT the Postgres search — measured, the DuckDB binder error names regexp_full_match); SIMILAR TO passes the RAW pattern (no %% translation) full-anchored; star * SIMILAR TO filters names by unanchored search while NOT SIMILAR TO negates a full match (independent predicates — pinned asymmetry); bare COLUMNS('re')/COLUMNS(*) expand in declared order with alias stamping. List-valued forms stay clean-unsupported. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b003122 commit 023b1e7

20 files changed

Lines changed: 1494 additions & 37 deletions

Cargo.lock

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ cranelift-frontend = "0.126"
1616
cranelift-jit = "0.126"
1717
cranelift-module = "0.126"
1818
pyo3 = { version = "0.29", features = ["abi3-py314"] }
19+
regex = "1"
1920
sqlparser = "0.62"
2021
target-lexicon = "0.13.5"

src/specializer/exec/cranelift.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,18 @@ struct Cx {
5757
input: *const Batch,
5858
statics: *const PreparedStatic,
5959
statics_len: usize,
60+
regexes: *const CompiledRe,
61+
regexes_len: usize,
6062
trap: Option<Trap>,
6163
}
6264

65+
/// One program regex, compiled, with its replace template (if any) — owned
66+
/// by the [`CraneliftFn`], read by the h_re* helpers through [`Cx`].
67+
pub(super) struct CompiledRe {
68+
rx: std::rc::Rc<regex::Regex>,
69+
template: Option<String>,
70+
}
71+
6372
impl Cx {
6473
fn arena(&mut self) -> &mut Arena {
6574
unsafe { &mut *self.arena }
@@ -591,6 +600,63 @@ extern "C" fn h_srepeat(p: *mut Cx, ao: i64, al: i64, n: i64, len_out: *mut i64)
591600
}
592601
}
593602

603+
extern "C" fn h_rematch(p: *mut Cx, re: i64, ao: i64, al: i64) -> u8 {
604+
let c = unsafe { cx(p) };
605+
let rx = &unsafe { std::slice::from_raw_parts(c.regexes, c.regexes_len) }[re as usize];
606+
let arena = unsafe { &*c.arena };
607+
rx.rx.is_match(arena.get(span(ao, al))) as u8
608+
}
609+
610+
extern "C" fn h_reextract(
611+
p: *mut Cx,
612+
re: i64,
613+
group: i64,
614+
ao: i64,
615+
al: i64,
616+
len_out: *mut i64,
617+
) -> i64 {
618+
let c = unsafe { cx(p) };
619+
let rx = &unsafe { std::slice::from_raw_parts(c.regexes, c.regexes_len) }[re as usize];
620+
let arena = unsafe { &mut *c.arena };
621+
// No match / non-participating group -> '' (wave-B pins).
622+
let out = {
623+
let s = arena.get(span(ao, al));
624+
rx.rx
625+
.captures(s)
626+
.and_then(|caps| caps.get(group as usize))
627+
.map(|m| m.as_str().to_string())
628+
.unwrap_or_default()
629+
};
630+
let r = arena.push_str(&out);
631+
unsafe { *len_out = r.len as i64 };
632+
r.off as i64
633+
}
634+
635+
extern "C" fn h_rereplace(
636+
p: *mut Cx,
637+
re: i64,
638+
global: i64,
639+
ao: i64,
640+
al: i64,
641+
len_out: *mut i64,
642+
) -> i64 {
643+
let c = unsafe { cx(p) };
644+
let rx = &unsafe { std::slice::from_raw_parts(c.regexes, c.regexes_len) }[re as usize];
645+
let arena = unsafe { &mut *c.arena };
646+
let template = rx.template.as_deref().unwrap_or_default();
647+
let out = {
648+
let s = arena.get(span(ao, al));
649+
if global != 0 {
650+
rx.rx.replace_all(s, template).into_owned()
651+
} else {
652+
rx.rx.replace(s, template).into_owned()
653+
}
654+
};
655+
let r = arena.push_str(&out);
656+
unsafe { *len_out = r.len as i64 };
657+
r.off as i64
658+
}
659+
594660
extern "C" fn h_ishl(p: *mut Cx, x: i64, y: i64) -> i64 {
595661
match interp::duck_shl(x, y) {
596662
Ok(v) => v,
@@ -836,6 +902,8 @@ pub struct CraneliftFn {
836902
row_fn: RowFn,
837903
interp: InterpFn,
838904
trap_msgs: Vec<String>,
905+
/// Program regexes compiled once, read by helpers through Cx.
906+
regexes: Vec<CompiledRe>,
839907
/// Owned backing for absolute addresses baked into the code.
840908
_const_strs: Vec<Box<str>>,
841909
_probe_descs: Vec<Box<ProbeDesc>>,
@@ -1084,11 +1152,21 @@ pub fn compile(p: &Program, statics: Vec<super::StaticData>) -> Result<Cranelift
10841152
let code = module.get_finalized_function(fid);
10851153
let row_fn: RowFn = unsafe { std::mem::transmute(code) };
10861154

1155+
let regexes = interp::compile_regexes(p)
1156+
.map_err(CompileError::Regex)?
1157+
.into_iter()
1158+
.zip(&p.regexes)
1159+
.map(|(rx, spec)| CompiledRe {
1160+
rx,
1161+
template: spec.rewrite.clone(),
1162+
})
1163+
.collect();
10871164
Ok(CraneliftFn {
10881165
_module: module,
10891166
row_fn,
10901167
interp,
10911168
trap_msgs,
1169+
regexes,
10921170
_const_strs: const_strs,
10931171
_probe_descs: probe_descs,
10941172
})
@@ -1122,6 +1200,8 @@ impl CraneliftFn {
11221200
input,
11231201
statics: statics.as_ptr(),
11241202
statics_len: statics.len(),
1203+
regexes: self.regexes.as_ptr(),
1204+
regexes_len: self.regexes.len(),
11251205
trap: None,
11261206
};
11271207
match (self.row_fn)(&mut cx) {
@@ -1456,6 +1536,30 @@ fn translate_inst(
14561536
let len = b.ins().stack_load(types::I64, slot_out, 0);
14571537
vals.insert(dst.0, V::Str(off, len));
14581538
}
1539+
Inst::ReMatch { re, dst, a } => {
1540+
let (ao, al) = vals[&a.0].str2();
1541+
let rev = icon(b, *re as i64);
1542+
let v = call_h(b, module, "h_rematch", &[cxp, rev, ao, al]).unwrap();
1543+
vals.insert(dst.0, V::S(v));
1544+
}
1545+
Inst::ReExtract { re, group, dst, a } => {
1546+
let (ao, al) = vals[&a.0].str2();
1547+
let rev = icon(b, *re as i64);
1548+
let gv = icon(b, *group as i64);
1549+
let lp = b.ins().stack_addr(types::I64, slot_out, 0);
1550+
let off = call_h(b, module, "h_reextract", &[cxp, rev, gv, ao, al, lp]).unwrap();
1551+
let len = b.ins().stack_load(types::I64, slot_out, 0);
1552+
vals.insert(dst.0, V::Str(off, len));
1553+
}
1554+
Inst::ReReplace { re, global, dst, a } => {
1555+
let (ao, al) = vals[&a.0].str2();
1556+
let rev = icon(b, *re as i64);
1557+
let gv = icon(b, *global as i64);
1558+
let lp = b.ins().stack_addr(types::I64, slot_out, 0);
1559+
let off = call_h(b, module, "h_rereplace", &[cxp, rev, gv, ao, al, lp]).unwrap();
1560+
let len = b.ins().stack_load(types::I64, slot_out, 0);
1561+
vals.insert(dst.0, V::Str(off, len));
1562+
}
14591563
Inst::Sord {
14601564
empty_zero,
14611565
dst,
@@ -1850,6 +1954,9 @@ const HELPERS: &[(&str, *const u8)] = &[
18501954
("h_sjaccard", h_sjaccard as *const u8),
18511955
("h_str3", h_str3 as *const u8),
18521956
("h_srepeat", h_srepeat as *const u8),
1957+
("h_rematch", h_rematch as *const u8),
1958+
("h_reextract", h_reextract as *const u8),
1959+
("h_rereplace", h_rereplace as *const u8),
18531960
("h_ishl", h_ishl as *const u8),
18541961
("h_sextract", h_sextract as *const u8),
18551962
("h_spad", h_spad as *const u8),
@@ -1887,6 +1994,9 @@ fn helper_sig(name: &str, sig: &mut cranelift_codegen::ir::Signature, ptr: types
18871994
"h_sjaccard" => (&[ptr, I64, I64, I64, I64], Some(F64)),
18881995
"h_str3" => (&[ptr, I64, I64, I64, I64, I64, I64, I64, I64], Some(I64)),
18891996
"h_srepeat" => (&[ptr, I64, I64, I64, I64], Some(I64)),
1997+
"h_rematch" => (&[ptr, I64, I64, I64], Some(I8)),
1998+
"h_reextract" => (&[ptr, I64, I64, I64, I64, I64], Some(I64)),
1999+
"h_rereplace" => (&[ptr, I64, I64, I64, I64, I64], Some(I64)),
18902000
"h_ishl" => (&[ptr, I64, I64], Some(I64)),
18912001
"h_sextract" => (&[ptr, I64, I64, I64, I64], Some(I64)),
18922002
"h_spad" => (&[ptr, I64, I64, I64, I64, I64, I64, I64], Some(I64)),

src/specializer/exec/interp.rs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ pub enum CompileError {
4141
Verify(Vec<VerifyError>),
4242
/// The static data does not match the program's static declarations.
4343
Static(String),
44+
/// A ReSpec pattern failed to compile — the frontend validates patterns
45+
/// at bind, so this only fires on hand-written IR.
46+
Regex(String),
4447
}
4548

4649
impl std::fmt::Display for CompileError {
@@ -54,10 +57,28 @@ impl std::fmt::Display for CompileError {
5457
Ok(())
5558
}
5659
CompileError::Static(msg) => write!(f, "static data mismatch: {msg}"),
60+
CompileError::Regex(msg) => write!(f, "regex table entry failed to compile: {msg}"),
5761
}
5862
}
5963
}
6064

65+
/// Compile every [`ir::ReSpec`] in the program with the wave-B builder
66+
/// settings (octal(true), default Unicode — see retrans.rs).
67+
pub(super) fn compile_regexes(p: &Program) -> Result<Vec<std::rc::Rc<regex::Regex>>, String> {
68+
p.regexes
69+
.iter()
70+
.map(|r| {
71+
regex::RegexBuilder::new(&r.pattern)
72+
.case_insensitive(r.ci)
73+
.dot_matches_new_line(r.dotall)
74+
.octal(true)
75+
.build()
76+
.map(std::rc::Rc::new)
77+
.map_err(|e| e.to_string())
78+
})
79+
.collect()
80+
}
81+
6182
/// One compiled instruction: reads registers/input/statics, writes registers
6283
/// or output builders. `Ok(())` or a call-aborting trap.
6384
type InstFn = Box<dyn for<'a> Fn(&mut Ctx<'a>) -> Result<(), Trap>>;
@@ -120,6 +141,7 @@ pub struct InterpFn {
120141
pub fn compile(p: &Program, statics: Vec<StaticData>) -> Result<InterpFn, CompileError> {
121142
verify(p).map_err(CompileError::Verify)?;
122143
let prepared = prepare_statics(p, statics)?;
144+
let regexes = compile_regexes(p).map_err(CompileError::Regex)?;
123145

124146
// Register slots are assigned densely in definition order, decoupling
125147
// the frame size from raw value ids — a verified program with sparse ids
@@ -143,7 +165,7 @@ pub fn compile(p: &Program, statics: Vec<StaticData>) -> Result<InterpFn, Compil
143165
for b in &p.blocks {
144166
let mut insts: Vec<InstFn> = Vec::with_capacity(b.insts.len());
145167
for inst in &b.insts {
146-
insts.push(compile_inst(p, inst, &slots));
168+
insts.push(compile_inst(p, inst, &slots, &regexes));
147169
}
148170
blocks.push(CBlock {
149171
insts,
@@ -1432,8 +1454,59 @@ fn sl(slots: &HashMap<u32, u32>, v: Value) -> usize {
14321454
slots[&v.0] as usize
14331455
}
14341456

1435-
fn compile_inst(p: &Program, inst: &Inst, slots: &HashMap<u32, u32>) -> InstFn {
1457+
fn compile_inst(
1458+
p: &Program,
1459+
inst: &Inst,
1460+
slots: &HashMap<u32, u32>,
1461+
regexes: &[std::rc::Rc<regex::Regex>],
1462+
) -> InstFn {
14361463
match inst.clone() {
1464+
Inst::ReMatch { re, dst, a } => {
1465+
let rx = regexes[re as usize].clone();
1466+
let (dst, a) = (sl(slots, dst), sl(slots, a));
1467+
Box::new(move |ctx| {
1468+
let s = ctx.arena.get(as_str(ctx.regs[a]));
1469+
let m = rx.is_match(s);
1470+
ctx.regs[dst] = RegVal::I1(m);
1471+
Ok(())
1472+
})
1473+
}
1474+
Inst::ReExtract { re, group, dst, a } => {
1475+
let rx = regexes[re as usize].clone();
1476+
let (dst, a) = (sl(slots, dst), sl(slots, a));
1477+
Box::new(move |ctx| {
1478+
// No match / non-participating group -> '' (wave-B pins).
1479+
let out = {
1480+
let s = ctx.arena.get(as_str(ctx.regs[a]));
1481+
rx.captures(s)
1482+
.and_then(|c| c.get(group as usize))
1483+
.map(|m| m.as_str().to_string())
1484+
.unwrap_or_default()
1485+
};
1486+
ctx.regs[dst] = RegVal::Str(ctx.arena.push_str(&out));
1487+
Ok(())
1488+
})
1489+
}
1490+
Inst::ReReplace { re, global, dst, a } => {
1491+
let rx = regexes[re as usize].clone();
1492+
let template = p.regexes[re as usize]
1493+
.rewrite
1494+
.clone()
1495+
.expect("verified: rereplace has a template");
1496+
let (dst, a) = (sl(slots, dst), sl(slots, a));
1497+
Box::new(move |ctx| {
1498+
let out = {
1499+
let s = ctx.arena.get(as_str(ctx.regs[a]));
1500+
if global {
1501+
rx.replace_all(s, template.as_str()).into_owned()
1502+
} else {
1503+
rx.replace(s, template.as_str()).into_owned()
1504+
}
1505+
};
1506+
ctx.regs[dst] = RegVal::Str(ctx.arena.push_str(&out));
1507+
Ok(())
1508+
})
1509+
}
14371510
Inst::Const { dst, lit } => {
14381511
let dst = sl(slots, dst);
14391512
match lit {

src/specializer/exec/tests.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,9 @@ entry:
188188
);
189189
match compile(&p, vec![]) {
190190
Err(CompileError::Verify(errs)) => assert!(!errs.is_empty()),
191-
Err(CompileError::Static(m)) => panic!("wrong error kind: {m}"),
191+
Err(CompileError::Static(m)) | Err(CompileError::Regex(m)) => {
192+
panic!("wrong error kind: {m}")
193+
}
192194
Ok(_) => panic!("compile accepted an unverified program"),
193195
}
194196
}
@@ -224,7 +226,9 @@ fn rejects_mismatched_statics() {
224226
Err(CompileError::Static(msg)) => {
225227
assert!(msg.contains(needle), "expected '{needle}' in '{msg}'")
226228
}
227-
Err(CompileError::Verify(_)) => panic!("fixture failed verify?"),
229+
Err(CompileError::Verify(_)) | Err(CompileError::Regex(_)) => {
230+
panic!("fixture failed verify?")
231+
}
228232
Ok(_) => panic!("compile accepted bad statics (wanted '{needle}')"),
229233
}
230234
}
@@ -451,6 +455,7 @@ fn sparse_value_ids_use_dense_register_slots() {
451455
use super::super::ir::{Block, Col, ColTy, Inst, Lit, Program, Term, Ty, Value};
452456
let p = Program {
453457
statics: vec![],
458+
regexes: vec![],
454459
name: "sparse".into(),
455460
in_cols: vec![],
456461
out_cols: vec![Col {

0 commit comments

Comments
 (0)