diff --git a/lib/regex/regex.ww b/lib/regex/regex.ww index d07dcadd..ef26ff68 100644 --- a/lib/regex/regex.ww +++ b/lib/regex/regex.ww @@ -1,8 +1,16 @@ // regex — POSIX extended regular expressions. Port of // 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. +// compile() literal core (lit/any/match + the leading skip); fold 2b +// tranche A = the thread-machine scaffolding that compiles cleanly +// today (thread/newmatch types, result_free, strerror — +// dead-imported until the engine lands, fold-1 precedent). Every +// other metacharacter arm, the engine itself (delete_thread/ +// add_thread/is_consuming_inst/run_thread/search) and the exec +// surface (test/find/replace) are DEFERRED behind compiler fixes: +// every *[]thread-mediated shape the engine needs miscompiles today +// (append-through-ptr #15, deref-spine element reads #17), and the +// 56B-slot inst by-value arg ABI is unwired both sides (#19) — +// probed pre-port, pA*/pB*/pC* probes. They land with those fixes. // // One fold-1 construct is held back behind a filed compiler/fidelity // gap (see the charclass_map site below): @@ -62,6 +70,22 @@ export type capture = struct { end_bytesize: size, }; +// ref/hare/regex/regex.ha:55-64. +type thread = struct { + pc: size, + start_idx: size, + start_bytesize: size, + root_capture: capture, + captures: []capture, + rep_counters: []size, + matched: bool, + failed: bool, +}; + +// Discriminates a fresh match from plain void at run_thread's return +// boundary. ref/hare/regex/regex.ha:66. +type newmatch = void; + // ref/hare/regex/regex.ha:68-72. export type charset = [](charset_lit_item | charset_range_item | charset_class_item); @@ -181,3 +205,28 @@ export fn compile(expr: str) (regex | error | nomem) = { n_reps = n_reps, }; }; + +// delete_thread (regex.ha:547-551), is_consuming_inst (regex.ha: +// 553-555) and add_thread (regex.ha:557-587) belong here in Hare +// order but are deferred behind compiler fixes: delete_thread/ +// add_thread on append-through-ptr (#15) / deref-spine reads (#17); +// is_consuming_inst on the >48B tagged by-value ABI (#19 — inst is a +// 56B slot; the call side is a loud unwired boundary and the callee +// receive diverges cs≠ww, so even landing it dead would break rule +// 10). See the module header. + +// Frees a [[result]]. +// +// ref/hare/regex/regex.ha:1113-1116, verbatim — the free() builtin is +// the documented no-op (#27; see finish()). +export fn result_free(s: result) void = { + free(s); +}; + +// Converts an [[error]] into a user-friendly string. +// +// ref/hare/regex/regex.ha:1126-1127 (expression-bodied in Hare; ww +// fns take block bodies). +export fn strerror(err: error) str = { + return err; +}; diff --git a/lib/regex/regex_test.ww b/lib/regex/regex_test.ww index cd31de06..ce47637c 100644 --- a/lib/regex/regex_test.ww +++ b/lib/regex/regex_test.ww @@ -1,7 +1,15 @@ // regex_test — exercises the lib/regex fold-1 data model (the type -// model + finish()) and the fold-2a compile() literal core. Run with +// model + finish()), the fold-2a compile() literal core, and the +// fold-2b tranche-A thread-machine scaffolding (thread/newmatch + +// result_free + strerror; the engine fns are deferred behind +// compiler fixes #15/#17/#19 — see regex.ww). Run with // `out/bin/ww run lib/regex/regex_test.ww`. // +// Private symbols (thread, newmatch) are reached unqualified: this +// file declares `package regex`, so the import unifies it with the +// lib sources (the decimaltest precedent, +// lib/strconv/test/decimaltest.ww). +// // 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. @@ -272,6 +280,154 @@ fn fail() void = { os.exit(signalled + 10); }; }; }; +// The thread struct (regex.ha:55-64) is in tree ahead of its engine +// consumers so the pending #15/#17 fix probes exercise the real type. +// Pin the field layout via the P6-proven wide-literal append + a +// depth-1 read-back row per appended thread; the Hare `...` partial +// fill (P5) must zero everything the second row's literal omits. +// root_capture has NO row: every read route into it is +// compiler-blocked today — the depth-2 chain behind the index links +// the field as a global (#6 F4), the element let-copy is #7 F5, and +// a probed `&threads[i].root_capture` deref segfaults byte-id on +// both stages — so its row lands with those fixes. +type texp = struct { + pc: size, + start_idx: size, + start_bytesize: size, + matched: bool, + failed: bool, + // .len reads as i32 (check.c:1239), so the count columns match it + ncaps: i32, + nreps: i32, +}; + +@test fn thread_shape() void = { + let rc: capture = capture { + content = "ab", start = 1, start_bytesize = 1, + end = 2, end_bytesize = 2, + }; + let pcaps: []capture = []; + append(pcaps, rc); + let prep: []size = []; + append(prep, (7: size)); + let threads: []thread = []; + append(threads, thread { + pc = 5, + start_idx = 6, + start_bytesize = 7, + root_capture = rc, + captures = pcaps, + rep_counters = prep, + matched = false, + failed = true, + }); + append(threads, thread { pc = 9, ... }); + let want: [2]texp = [ + texp { pc = 5, start_idx = 6, start_bytesize = 7, + matched = false, failed = true, + ncaps = 1, nreps = 1 }, + texp { pc = 9, start_idx = 0, start_bytesize = 0, + matched = false, failed = false, + ncaps = 0, nreps = 0 }, + ]; + if (len(threads) != len(want)) { fail(); }; + let i: i32 = 0; + for (i < len(want)) { + if (threads[i].pc != want[i].pc) { fail(); }; + if (threads[i].start_idx != want[i].start_idx) { fail(); }; + if (threads[i].start_bytesize != want[i].start_bytesize) { + fail(); + }; + if (threads[i].matched != want[i].matched) { fail(); }; + if (threads[i].failed != want[i].failed) { fail(); }; + if (threads[i].captures.len != want[i].ncaps) { fail(); }; + if (threads[i].rep_counters.len != want[i].nreps) { fail(); }; + i += 1; + }; +}; + +// newmatch (regex.ha:66) must discriminate nominally against plain +// void — and against nomem, the third payload-free member — across +// run_thread's (void | newmatch | nomem) return boundary: the P8 +// shape on the real lib type, one row per returned member. +fn nm_probe(x: i32) (void | newmatch | nomem) = { + if (x == 1) { + let nm: newmatch; + return nm; + }; + if (x == 2) { + let n: nomem; + return n; + }; + return; +}; + +type nmexp = struct { + arg: i32, + want_nm: bool, + want_void: bool, + want_nomem: bool, +}; + +@test fn newmatch_discriminates() void = { + let rows: [3]nmexp = [ + nmexp { arg = 1, want_nm = true, want_void = false, + want_nomem = false }, + nmexp { arg = 0, want_nm = false, want_void = true, + want_nomem = false }, + nmexp { arg = 2, want_nm = false, want_void = false, + want_nomem = true }, + ]; + let i: i32 = 0; + for (i < len(rows)) { + let r: (void | newmatch | nomem) = nm_probe(rows[i].arg); + if ((r is newmatch) != rows[i].want_nm) { fail(); }; + if ((r is void) != rows[i].want_void) { fail(); }; + if ((r is nomem) != rows[i].want_nomem) { fail(); }; + i += 1; + }; +}; + +// result_free (regex.ha:1114-1116) accepts a built result; free() is +// the documented no-op (no-free runtime), so the header must stay +// readable after — a future real free changes this row consciously. +// The local is spelled []regex.capture, not the regex.result alias: +// wwstage falsely loud-bails appending a struct literal onto an +// alias-typed dst (#20); the alias + signature stay exercised by the +// result_free call itself. Reverts to `regex.result` when #20 lands. +@test fn result_free_noop() void = { + let res: []regex.capture; + append(res, capture { + content = "x", start = 0, start_bytesize = 0, + end = 1, end_bytesize = 1, + }); + regex.result_free(res); + if (len(res) != 1) { fail(); }; + if (res[0].end != (1: size)) { fail(); }; + // The zero-header edge: find()'s no-match path returns an empty + // result (regex.ha:915-916) the caller still result_free()s. The + // bare decl is alias-typed — the #20 dodge above is append-only, + // so the alias stays exercised in value position here. + let empty: regex.result; + regex.result_free(empty); + if (len(empty) != 0) { fail(); }; +}; + +// strerror (regex.ha:1127) is identity on the boundary text — routed +// through a REAL compile() error, completing the exported error +// surface end to end. +@test fn strerror_identity() void = { + match (regex.compile("a*")) { + case let e: regex.error => { + if (strings.compare(regex.strerror(e), + "regex: metacharacter not yet ported") != 0) { + fail(); + }; + }; + case => fail(); + }; +}; + export fn main() i32 = { signalled = 1; lit_and_match(); signalled = 2; size_aliases_distinct(); @@ -283,5 +439,9 @@ export fn main() i32 = { signalled = 8; compile_any_program(); signalled = 9; compile_empty_program(); signalled = 10; compile_metachar_loud(); + signalled = 11; thread_shape(); + signalled = 12; newmatch_discriminates(); + signalled = 13; result_free_noop(); + signalled = 14; strerror_identity(); return 0; }; diff --git a/test/wcc/989_regex_run.c b/test/wcc/989_regex_run.c index 219496ab..6b5fc84a 100644 --- a/test/wcc/989_regex_run.c +++ b/test/wcc/989_regex_run.c @@ -1,6 +1,7 @@ /* - * 989_regex_run — execute the lib/regex fold-1 @test fixture under the - * C-side `ww run` driver and assert exit 0. + * 989_regex_run — execute the lib/regex @test fixture (fold-1 data + * model + fold-2a compile() + fold-2b tranche-A scaffolding) under + * the C-side `ww run` driver and assert exit 0. * * Sibling to 984_base64_run / 989_sha256_run (9xx is full so this * shares the 989 prefix — the `short` name keys the binary, cf the