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.
233 lines
8.2 KiB
Plaintext
233 lines
8.2 KiB
Plaintext
// 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); 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):
|
|
// - charclass_map (regex.ha:74-87) — a module-level const slice of
|
|
// (str, *fn(rune) bool) tuples. Blocked on the array-literal→slice
|
|
// element-coercion checker gap (#25; type.c:402-404 #258 borrow
|
|
// uses exact type_eq, no element decay).
|
|
package regex;
|
|
|
|
import strings;
|
|
import encoding.utf8;
|
|
|
|
// ref/hare/regex/regex.ha:14 — an error string describing a compilation
|
|
// error.
|
|
export type error = !str;
|
|
|
|
// ref/hare/regex/regex.ha:16-30.
|
|
export type inst_lit = rune;
|
|
export type inst_charset = struct { idx: size, is_positive: bool };
|
|
export type inst_any = void;
|
|
export type inst_split = size;
|
|
export type inst_jump = size;
|
|
export type inst_skip = void;
|
|
export type inst_match = bool;
|
|
export type inst_groupstart = size;
|
|
export type inst_groupend = void;
|
|
export type inst_repeat = struct {
|
|
id: size,
|
|
origin: size,
|
|
min: (void | size),
|
|
max: (void | size),
|
|
};
|
|
|
|
// ref/hare/regex/regex.ha:32-35.
|
|
export type inst = (inst_lit | inst_any | inst_split | inst_jump |
|
|
inst_skip | inst_match | inst_charset |
|
|
inst_groupstart | inst_groupend |
|
|
inst_repeat);
|
|
|
|
// The resulting match of a [[regex]] applied to a string.
|
|
//
|
|
// The first [[capture]] corresponds to the implicit zeroth capture
|
|
// group, i.e. the whole expression.
|
|
//
|
|
// The rest of the [[capture]]s correspond to the rest of the capture
|
|
// groups, i.e. the sub-expressions.
|
|
// ref/hare/regex/regex.ha:44.
|
|
export type result = []capture;
|
|
|
|
// A (sub)match corresponding to a regular expression's capture group.
|
|
// ref/hare/regex/regex.ha:47-53.
|
|
export type capture = struct {
|
|
content: str,
|
|
start: size,
|
|
start_bytesize: size,
|
|
end: size,
|
|
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);
|
|
export type charset_lit_item = rune;
|
|
export type charset_range_item = (u32, u32);
|
|
export type charset_class_item = (str, *fn(c: rune) bool);
|
|
|
|
// ref/hare/regex/regex.ha:74-87 — charclass_map: the const
|
|
// [](str, *fn(rune) bool) table mapping POSIX class tokens to the
|
|
// matching ascii predicate. DEFERRED: the array-literal→slice
|
|
// assignability check (type.c:402-404, the #258 borrow) compares
|
|
// element types with exact type_eq and applies NO element coercion, so
|
|
// the literal `[(":alnum:]", &ascii.isalnum), ...]` (typed
|
|
// `[N](untyped_str, *fn(rune) bool)`) is rejected against the declared
|
|
// `[](str, *fn(rune) bool)`. Minimal repro: `let xs: [](size, size) =
|
|
// [(1, 2)];`. Reshaping to a fixed `[12](...)` array would compile but
|
|
// is an unfaithful workaround (CLAUDE.md rule-7), so the table — and
|
|
// the `import ascii;` it needs — land with the consuming fold (compile)
|
|
// once the checker gap is fixed.
|
|
|
|
// ref/hare/regex/regex.ha:89-93.
|
|
export type regex = struct {
|
|
insts: []inst,
|
|
charsets: []charset,
|
|
n_reps: size,
|
|
};
|
|
|
|
// Frees resources associated with a [[regex]].
|
|
//
|
|
// ref/hare/regex/regex.ha:96-102, verbatim. The free() builtin is a
|
|
// documented no-op (#27 landed): ww is a no-free runtime (rt/alloc.s:30
|
|
// — the bump allocator can't reclaim, process-exit does), so each free
|
|
// below evaluates its operand and reclaims nothing. Kept verbatim for
|
|
// API + source parity with the Hare surface.
|
|
export fn finish(re: *regex) void = {
|
|
free(re.insts);
|
|
for (let charset .. re.charsets) {
|
|
free(charset);
|
|
};
|
|
free(re.charsets);
|
|
};
|
|
|
|
// 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 the free()
|
|
// builtin is a no-op anyway (#27 — ww is a no-free runtime), so the
|
|
// cleanup would reclaim nothing.
|
|
export fn compile(expr: str) (regex | error | nomem) = {
|
|
// Hare `let insts: []inst = [];` — a bare ww slice declaration
|
|
// zeroes the header (cgen.c:9836 no-rhs multi-word composite
|
|
// zero-fill, symmetric in cgenstmt.ww).
|
|
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.
|
|
// Hare appends the bare type name (`inst_skip`) as
|
|
// the void-variant value; in ww that resolves as a
|
|
// symbol ref (cf the alloc(T) rule), so void
|
|
// variants go through a typed let.
|
|
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
|
|
append(insts, (r: inst_lit));
|
|
case '\\', '^', '$', '[', '(', ')', '|', '{', '?', '*', '+':
|
|
// fold-2a boundary: regex.ha:286-459 arms deferred.
|
|
return "regex: metacharacter not yet ported": error;
|
|
case: // regex.ha:462-463
|
|
append(insts, (r: inst_lit));
|
|
};
|
|
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)) {
|
|
append(insts, (false: inst_match));
|
|
};
|
|
// regex.ha:479-484. The alternation fixup (ha:470-473) drops with
|
|
// jump_idxs.
|
|
return regex {
|
|
insts = insts,
|
|
charsets = charsets,
|
|
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;
|
|
};
|