lib/regex: fold-2b tranche A — thread/newmatch types + leaf fns

thread (regex.ha:55-64) and newmatch (ha:66) land verbatim ahead of
their engine consumers; result_free (ha:1113-1116) and strerror
(ha:1126-1127) complete the exported error/result surface.

delete_thread/add_thread deferred behind #15 (append-through-ptr)
and #17 (deref-spine element reads); is_consuming_inst deferred
behind #19 (>48B tagged by-value call boundary unwired + divergent
callee receive) — noted at the Hare-order site.

Tests are row-table driven: thread_shape reads back both appended
threads against a [2]texp want table (root_capture rows deferred —
every read route is compiler-blocked, #6/#7, probed at HEAD);
newmatch_discriminates drives one row per (void|newmatch|nomem)
member, incl. a nomem-vs-newmatch row; result_free also covers the
zero-header empty result (find()'s ha:915-916 no-match shape).
Byte-id re-verified on the regenerated combined: the pre-existing
FC0 regex.finish hunk is the only divergence.
This commit is contained in:
2026-06-04 08:54:17 +09:00
parent b7a4eda40a
commit c801aa7954
3 changed files with 216 additions and 6 deletions

View File

@@ -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;
};

View File

@@ -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;
};