Files
ww/lib/regex/regex.ww
Hojun-Cho de7dc36da3 lib/regex: verbatim cast-expr appends; correct bare-decl zero cite (fold-2a review)
The lit/match appends went through a let-temp; the direct Hare
spelling append(insts, (r: inst_lit)) compiles and runs correctly
(probed at the real 48B-payload inst shape), so the temps were an
undocumented reshape. Void variants (skip/any) keep the typed let —
a bare type name is a symbol ref in ww — now documented at-site.
The bare-slice-decl zeroing cite pointed at shlex.ww:215, which
zeroes its header EXPLICITLY and so proves nothing; the real
mechanism is cgen.c:9836's no-rhs multi-word composite zero-fill.
2026-06-04 05:45:52 +09:00

181 lines
6.5 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); 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):
// - 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).
// - 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;
// 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: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 frees re.insts / each charset /
// re.charsets. ww is a no-free runtime (rt/alloc.s:30 — rt_free is a
// no-op; the bump allocator can't reclaim, process-exit does), so the
// faithful ww body drops the frees, matching how the port drops every
// Hare free(). Kept for API parity with the Hare surface. Temporary
// empty body: #27 makes the free() builtin compile to a documented
// 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 (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,
};
};