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:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user