diff --git a/lib/regex/regex.ww b/lib/regex/regex.ww index 9caa4ca7..e3fc8d4c 100644 --- a/lib/regex/regex.ww +++ b/lib/regex/regex.ww @@ -1,6 +1,8 @@ // regex — POSIX extended regular expressions. Port of -// ref/hare/regex/regex.ha. Fold 1 = the data model only; compile() / -// exec / find / replace are DEFERRED to later folds. +// ref/hare/regex/regex.ha. Fold 1 = the data model; fold 2a = the +// compile() literal core (lit/any/match + the leading skip); every +// other metacharacter arm, exec / find / replace are DEFERRED to +// later folds. // // Two fold-1 constructs are held back behind filed compiler/fidelity // gaps (see the charclass_map and finish() sites below): @@ -11,6 +13,9 @@ // - finish() (regex.ha:96-102) — frees; ww is a no-free runtime (#27). package regex; +import strings; +import encoding.utf8; + // ref/hare/regex/regex.ha:14 — an error string describing a compilation // error. export type error = !str; @@ -96,3 +101,80 @@ export type regex = struct { // no-op, after which this ports the Hare frees VERBATIM (the no-op // builtin reclaims nothing, same end state). export fn finish(re: *regex) void = { }; + +// Compiles a regular expression string into a [[regex]]. +// +// ref/hare/regex/regex.ha:227-263. Fold 2a ports the literal core: +// inst_lit / inst_any / inst_match plus the leading inst_skip +// (regex.ha:261-263 — unanchored exec depends on it). Every other +// metacharacter arm is one loud not-yet-ported error — the explicit +// fold boundary; falling to literal would be a silent semantic lie. +// State serving only the deferred arms is dropped with them: +// jump_idxs (ha:241-248), the bracket quad (ha:249-252), +// was_prev_rune_pipe / group_level / capture_idx (ha:253-256). +// +// Hare's `defer if (!ok) free(...)` cleanup (ha:231-237) is omitted: +// ww has no `defer if` (cf lib/strings/strings.ww:85) and is a no-free +// runtime (#27) — the port drops every Hare free(). +export fn compile(expr: str) (regex | error | nomem) = { + // Hare `let insts: []inst = [];` — a bare ww slice declaration + // zeroes the header (cf lib/shlex/shlex.ww:215). + let insts: []inst; + let charsets: []charset; // stays empty until the '[' fold + let iter: strings.iterator = strings.iter(expr); + let r_idx: size = 0; + let n_reps: size = 0; + + for (true) { + let next: (rune | utf8.done) = strings.next(&iter); + + if (r_idx == 0 && next is rune && (next as rune) != '^') { + // Bare append: ww append returns void; Hare's + // `append(...)?` nomem propagation is filed #36. + let sk: inst_skip; + let v: inst = sk; + append(insts, v); + }; + + // regex.ha:277-284 minus the group_level check (it rides + // the deferred '(' arm's state). + let r: rune = match (next) { + case utf8.done => break; + case let x: rune => yield x; + }; + + switch (r) { + case '.': { // regex.ha:460-461 + let av: inst_any; + let v: inst = av; + append(insts, v); + }; + case ']': { // regex.ha:315-316 — literal outside a bracket + let v: inst = (r: inst_lit); + append(insts, v); + }; + case '\\', '^', '$', '[', '(', ')', '|', '{', '?', '*', '+': + // fold-2a boundary: regex.ha:286-459 arms deferred. + return "regex: metacharacter not yet ported": error; + case: { // regex.ha:462-463 + let v: inst = (r: inst_lit); + append(insts, v); + }; + }; + r_idx += 1; + }; + + // regex.ha:475-477. `$` appends true: inst_match — deferred, so + // the guard can only see no-match today; kept verbatim. + if (insts.len == 0 || !(insts[insts.len - 1] is inst_match)) { + let m: inst = (false: inst_match); + append(insts, m); + }; + // regex.ha:479-484. The alternation fixup (ha:470-473) drops with + // jump_idxs. + return regex { + insts = insts, + charsets = charsets, + n_reps = n_reps, + }; +}; diff --git a/lib/regex/regex_test.ww b/lib/regex/regex_test.ww index 63b6c31e..b08f4281 100644 --- a/lib/regex/regex_test.ww +++ b/lib/regex/regex_test.ww @@ -1,7 +1,10 @@ // regex_test — exercises the lib/regex fold-1 data model (the type -// model + finish()). Run with `out/bin/ww run lib/regex/regex_test.ww`. +// model + finish()) and the fold-2a compile() literal core. Run with +// `out/bin/ww run lib/regex/regex_test.ww`. // -// Fold 1 ports the data model only; compile()/exec live in later folds. +// Fold 2a ports compile()'s lit/any/match arms only; exec lives in +// later folds, so the compile_* cases pin the emitted inst PROGRAM +// (shape + payloads via indexed match-extraction), not matching. // charclass_map's fn-ptr table is deferred behind the array→slice // element-coercion checker gap (see regex.ww), so this test does not // exercise the POSIX-class predicate dispatch yet — it pins variant @@ -152,6 +155,104 @@ fn fail() void = { os.exit(signalled + 10); }; regex.finish(&re); }; +// compile("abc") emits the 5-inst literal program: the leading +// unanchored inst_skip (regex.ha:261-263), one inst_lit per rune, the +// epilogue inst_match(false) (ha:475-477). compile()'s +// (regex | error | nomem) return is the first >24B tagged payload in +// the tree — the receive shapes here double as #38 sret consumers +// (typed-let + match here; scrutinee-direct in compile_empty_program). +@test fn compile_literal_program() void = { + let c: (regex.regex | regex.error | nomem) = regex.compile("abc"); + match (c) { + case let re: regex.regex => { + if (re.insts.len != 5) { fail(); }; + match (re.insts[0]) { + case let k: regex.inst_skip => void; + case => fail(); + }; + match (re.insts[1]) { + case let l: regex.inst_lit => { if ((l: rune) != 'a') { fail(); }; }; + case => fail(); + }; + match (re.insts[2]) { + case let l: regex.inst_lit => { if ((l: rune) != 'b') { fail(); }; }; + case => fail(); + }; + match (re.insts[3]) { + case let l: regex.inst_lit => { if ((l: rune) != 'c') { fail(); }; }; + case => fail(); + }; + match (re.insts[4]) { + case let m: regex.inst_match => { if ((m: bool)) { fail(); }; }; + case => fail(); + }; + if (re.charsets.len != 0) { fail(); }; + if (re.n_reps != (0: size)) { fail(); }; + regex.finish(&re); + }; + case => fail(); + }; +}; + +// '.' compiles to inst_any between the literals (regex.ha:460-461): +// [skip, lit 'a', any, lit 'c', match(false)]. +@test fn compile_any_program() void = { + let c: (regex.regex | regex.error | nomem) = regex.compile("a.c"); + match (c) { + case let re: regex.regex => { + if (re.insts.len != 5) { fail(); }; + match (re.insts[0]) { + case let k: regex.inst_skip => void; + case => fail(); + }; + match (re.insts[1]) { + case let l: regex.inst_lit => { if ((l: rune) != 'a') { fail(); }; }; + case => fail(); + }; + match (re.insts[2]) { + case let a: regex.inst_any => void; + case => fail(); + }; + match (re.insts[3]) { + case let l: regex.inst_lit => { if ((l: rune) != 'c') { fail(); }; }; + case => fail(); + }; + match (re.insts[4]) { + case let m: regex.inst_match => { if ((m: bool)) { fail(); }; }; + case => fail(); + }; + regex.finish(&re); + }; + case => fail(); + }; +}; + +// compile("") is exactly [inst_match(false)]: the leading skip must +// not fire on immediate done (regex.ha:261 gates on `next is rune`), +// and the epilogue guard must fire on the empty program. +@test fn compile_empty_program() void = { + match (regex.compile("")) { + case let re: regex.regex => { + if (re.insts.len != 1) { fail(); }; + match (re.insts[0]) { + case let m: regex.inst_match => { if ((m: bool)) { fail(); }; }; + case => fail(); + }; + regex.finish(&re); + }; + case => fail(); + }; +}; + +// Any deferred metacharacter is a LOUD error — pins the fold-2a +// boundary so the fold that ports '*' consciously deletes this row. +@test fn compile_metachar_loud() void = { + match (regex.compile("a*")) { + case let e: regex.error => { if ((e: str).len == 0) { fail(); }; }; + case => fail(); + }; +}; + export fn main() i32 = { signalled = 1; lit_and_match(); signalled = 2; size_aliases_distinct(); @@ -159,5 +260,9 @@ export fn main() i32 = { signalled = 4; charset_payload(); signalled = 5; repeat_payload(); signalled = 6; struct_shapes_and_finish(); + signalled = 7; compile_literal_program(); + signalled = 8; compile_any_program(); + signalled = 9; compile_empty_program(); + signalled = 10; compile_metachar_loud(); return 0; };