Commit Graph

256 Commits

Author SHA1 Message Date
19e9535ef4 wcc/ww: reject duplicate top-level decls, aligned to cstage
installdecl routes all four kinds (fn/type/def/let) through installtop,
which turns scopedefineinmodule's nil return into cstage's exact
"duplicate <kind> <name>" reject, keyed (name,mod) so cross-package
same-leaf decls coexist. Builtin redecls are dropped, not dup-errored:
cstage never scopes builtins (lookup_builtin first, check.c:69), so a
user redecl is dead there — wwstage mirrors via scopesamekeysym +
no-source-decl test. -T synth __wwtests installs direct, mirroring
check.c:3079. Closes the silent dup-fn hole (user fn run vs lib/test
run built a broken test binary with no diagnostic). Per-kind reject
rows + cross-package/builtin accept byte-id rows + -T collision parity
row in 910/997. (#23-team, category-A addendum closed)
2026-06-11 00:57:43 +09:00
16c83e70d3 ww test: fork-isolated record-and-continue harness (lib/test, both stages)
lib/test/run.ww: fork+wait4 runner; each @test runs in its own child,
abort/SEGV/FPE decoded from wait-status, failures recorded and the run
continues; exit = fail count. Tests are hermetic: module globals do not
persist test-to-test (fresh fork image; sanctioned divergence from
harec's shared-process __test_main, no setjmp/signal layer needed).
-T synth (both stages) emits a module-global (str,*fn() void) table +
return run(table) instead of straight-line calls. Driver twins bundle
lib/test under test mode and gain ww test -c/-o (go test -c) so the
byte-id gates diff the same artifact the real path builds. Gates
989/910/997 rewired onto it; new 911 pins record-and-continue across
all three fault classes; 949 +3 rows. (#17-team commit-2)
2026-06-11 00:08:39 +09:00
2338ea5d59 ww: lib tests run via -T test mode; bare mains retired (closes @test conversion)
'ww test' gains the istest build path (-T injection in build_one/
buildone) and do_test/dotest accept -I, mirroring do_run - both twins.
The 35 converted lib tests drop their interim bare mains (-T
synthesizes the entry from @test fns and rejects a user main); their
35 C run-drivers flip 'ww run' -> 'ww test'; 989_lib_byteid compiles
lib tests under -T (8 user-main probe fixtures stay non-T, gated on
the fixture field). Abort-on-first-failure stands until the deferred
record-and-continue harness lands with the multi-package arc.
2026-06-10 20:45:46 +09:00
5b212e51cf wcc/cgen: zero-init sub-8-byte bare lets, both stages; bytes test honest (#16-team)
A bare 'let x: T;' with 1 <= size(T) <= 7 matched no zero-fill arm in
either stage (8B and >8B were already zeroed) - 'let c: [3]u8;' read
stack garbage. User-ruled zero-value semantics: cstage gate sz>8 ->
sz>0; wwstage zsz==8 arm hoisted above the fill-run arm (required -
8B would otherwise route into the run and diverge) and run gate
zsz>0. New 840 pin: dirty-frame probe rows, dual-dim (run + cs/ww
byte-id); discriminators fail exit-154 on pre-fix binaries.

Fused with the lib/bytes test conversion (rule 11): either half alone
turns 967 red. The old exit(signalled+10) wrapped a real 1782-count
ltrim failure to exit 0 - green depended on the garbage. Converted to
assert form (completes the 35/35 @test conversion); ltrim rows keep
the bare 'let c: [3]u8;' as the consumer proof of the fix.
2026-06-10 20:08:58 +09:00
3e2bf0612e lib misc+ww tests: hand-main plumbing -> assert (@test conversion B7)
toktest documents the per-row signalled pinpoint loss.
2026-06-10 18:52:53 +09:00
eb891f4007 lib/os+dirs tests: hand-main plumbing -> assert (@test conversion B6)
WW_TEST_* env arrangement logic untouched; only the fail/exit plumbing
converts.
2026-06-10 18:52:53 +09:00
e8096e68e1 lib/io tests: hand-main plumbing -> assert (@test conversion B5) 2026-06-10 18:52:53 +09:00
612e6d149c lib/strings-core tests: hand-main plumbing -> assert (@test conversion B4)
bytes EXCLUDED: its green masked a real signalled corruption (filed;
conversion rides the fix). stringstest documents the per-row signalled
pinpoint loss.
2026-06-10 18:52:53 +09:00
b6d9201ef5 lib/strconv tests: hand-main plumbing -> assert (@test conversion B3)
inttest keeps now-inert per-row id params (rows 1:1 per spec; removal
deferred, ~100 callsites).
2026-06-10 18:52:53 +09:00
56450e68ce lib/encoding+math tests: hand-main plumbing -> assert (@test conversion B2) 2026-06-10 18:52:53 +09:00
6ef2bbcb95 lib/hash tests: hand-main plumbing -> assert (@test conversion B1)
signalled/fail() counters and run-loop plumbing removed; failure sites
become assert(!(cond)); table rows and @test fns 1:1. Bare mains stay
until the -T flip commit. Spec: .ai/drew-t2-conversion-spec.md.
2026-06-10 18:52:53 +09:00
49a5173f3f wcc+ww: //ww:module-reset boundary directive; package-less files keep module "" (#16 PREP-main)
Driver twins emit the line-comment directive only before package-less
files (peekpackage==nil); both lexers tokenize it (TK_MODRESET, appended
=87 so existing token values hold) and both parsers reset curmod — a
package-less file's decls get module "" instead of inheriting the last
bundled package (the sticky-curmod leak, task #11). Withdrawn
alternative: injecting 'package main' flips non-entry symbols
bare->main-prefixed (FFI-visible, broke 764). Codegen-neutral by proof:
bare symbols preserved, both stages emit byte-identical asm for a
directive-bearing combined. Transitional until strict-package rejects
package-less files outright. Includes 737 bad-deep pin for the
PREP-peek >2048 edge + 904/toktest rows for the new token.
2026-06-10 14:07:35 +09:00
12346c35f8 lib/test: rename dotted-self-import tests to <mod>_test (#16 PREP-a2)
package L + import P.L is self-import in Go terms regardless of path
spelling (the parser drops the dotted prefix); the text-level
import-string!=package-name comparison hid these 3 from the PREP-a set.
All three verified black-box (qualifier-only exported-API use) — plain
rename, dotted imports kept as legit cross-package.
2026-06-10 13:27:12 +09:00
99e3393048 lib/test: Go external-test packages (<mod>_test); retire decimaltest (#16 PREP-a)
30 lib test files: package <mod> -> <mod>_test, the sanctioned Go-over-Hare
departure (CLAUDE.md rule-9 carve-out; white-box testing deferred, not
forbidden). decimaltest retired per .ai/rob-16-decimaltest-ruling.md: Hare
ships no decimal_test.ha (engine covered transitively); coverage migrated
losslessly into stoftest rows (all-9s carry, nd>19 pure-decimal) + ftostest
f64_roundtrip mirroring ref/hare strconv ftos_test.ha tcsf64; k=0 micro-gap
documented at site. regex_test keeps its white-box internal probes under a
documented rule-7 divergence (rehome = task #9). 922_decimal_run + its 989
byte-id row removed with the fixture.
2026-06-10 12:11:11 +09:00
c35034df3f lib/fmt: #6 formattable += int|uint — bare-int print, the types::numeric widen (both stages)
ww had narrowed Hare's formattable (types::numeric) to a lone i64 arm, so a bare int/uint was not printable: cstage CORRECTLY rejected it, wwstage leniently accepted via the #128 size-keying. Append int|uint LAST (existing tags 0-4 frozen, zero byte-id churn) so BOTH stages accept by membership — closing the #128 int-path leniency by construction. int renders via the signed i64 path; uint via strconv.u64tos unsigned (+ a rawlenu64 width twin) — i64dec would render a high-bit uint negative. Staged cut: narrower widths + full types::numeric graduate per-caller (mirrors the f32-arm precedent).

fmt is NOT embedded in the selfhost compiler tools (grep-verified: 0 fmt fn-defs in w6c/wwdump combined.ww) — no combined.ww regen, compiler binaries unchanged. Pin: table-driven test/wcc/815_fmt_int_run (bare int, high-bit uint 2^63+1 positive, i64/str regression guards, byte-id).
2026-06-08 15:28:50 +09:00
ba13940b96 lib/path: #153 c3 buffer-ops — abs/isroot-widen/local/parent/split/pop/peek faithful port, table-driven @test (cstage)
Faithful ref/hare/path/buffer.ha port of the buffer-mutating ops. isroot
widened *buffer->(*buffer|str) (existing callers ptr-ident-widen); abs str-arm
strings.hasprefix(p,sepstr) (rides #154); local lifts Hare's `static let buf`
to a module-private [MAX]u8 index-write (no ww static-append, regex.ww:1177).

Three documented respells (never silent): split binds each (str|void) elem to
an ident before return (#22b); peek binds split() to a local (#48); local's
index-write. parent via appendnorm + frombytes.

Tests mirror Hare's pop() @test 1:1 (stack.ha:147) + table-driven
abs/isroot/local/parent rows off a local buffer. cstage-first; wwstage str-arms
ride deferred #146 (path stays M_WWREJECT). lib/path non-embedded (no
combined.ww regen). Completes the c3 stage of the path arc.
2026-06-08 13:37:23 +09:00
3c7f1aa027 lib/path: #138 c3-posix dirname/basename — faithful posix.ha port, table-driven @test (cstage)
dirname/basename ported verbatim from ref/hare/path/posix.ha:17,37,
re-routing Hare's byte scanning to bytes.rtrim/rindex. Additive to the
c2-stack buffer core; no harness/Makefile/module-const changes.

Work-var renamed path->p to avoid Hare's param-shadowing
`let path = toutf8(path)`: ww's cgen N_LET prepends the new local into the
name-keyed localfind chain BEFORE the init is emitted, so the shadow-init
reads the fresh uninit slot — a silent miscompile, both-wrong-identical,
filed #152 and fixed independently next. drew ruled path->p a permanent
acceptable spelling (internal work-var, not API surface; avoids a
param-shadow footgun; not a rule-7 workaround). WHY-comment + #152 pointer
at both sites.

Tests: 10-row dirname/basename table (posix.ha:51-71 verbatim) — root,
no-sep, empty, trailing-sep, multi-sep all covered, expecteds traced.

C-first: cstage @test green; the str-view-vs-literal rows are #146-deferred
(wwstage byte-id, rides #125). Gate: all 324 passed, byte-id 990-997 green,
w6c/w6c_ww unchanged (lib-only additive).
2026-06-08 11:47:43 +09:00
16f47da916 lib/path: #138 c2-stack buffer port — buffer/push/appendnorm/appendlit/string/isroot/set + dot/dotdot, cstage @test
Faithful realignment of lib/path to Hare's buffer-centric API
(ref/hare/path/{stack,buffer}.ha). Replaces the old str-only path.ww
wholesale (zero consumers). Scope = stack-core: init() deferred (returns
~4KB (buffer|error), rides the #40 arc / #147); abs/dirname/basename land
in c3; extension/join dropped.

appendlit uses the #145 slice-copy-assign arm (buf.buf[lo:hi]=bs), no hand
loop. dot/dotdot are faithful module-global []u8 (D2 #148). MAX =
os.PATH_MAX-1. Divergences (size->i32 indices, frombytes, module-global
consts) cited inline.

Tests: lib/path/pathtest.ww (table-driven), wired at test/wcc/989_path_run.c;
push rows mirror stack.ha:107-111 verbatim incl the restored "/d"
intermediate. Slot 989 (overflow bucket) since 970 is taken.

C-first: cstage @test green; wwstage byte-id deferred to the #125 batch.
path classified M_WWREJECT in 989_lib_byteid (w6c_ww rejects the module-
global slice consts = #120/#29; + the #148 twin #151) — self-graduates back
to M_ID the day wwstage accepts. path moved off the 900_stdlib standalone
list (import-dependent: os.PATH_MAX def-dim + match over imported error
types; coverage at 989_path_run), per the bytes/fmt/os precedent.

Gate: all 324 passed, byte-id 990-997 green, w6c/w6c_ww unchanged
(lib-only, non-embedded).
2026-06-08 11:25:11 +09:00
abd97e6c57 lib/getopt: printusage + printhelp
Port ref/hare/getopt/getopts.ha:202-314 (printusage / _printusage / containsh /
printhelp): the two-pass io.empty width measurement + the >72-col indent rule.
Table-driven tests. cstage-only (C-first); wwstage byte-id twin owed (#125).
printsubcmds + parse() deferred (#127 / #126).
2026-06-07 06:19:13 +09:00
f3750ae3ce lib/io: empty() stream; wcc/cgen: #129 sretretsize + #130 global tagged-field store
io.empty (discard+EOF stream, ref/hare/io/empty.ha:4-17) — needed by getopt's
two-pass printusage width measurement. Diverges from Hare's `const empty: *stream`:
a `let _empty_vt` + `fn empty()` that wires the fn-ptr slots per call, because
const-init of a vtable struct with fn-ptr fields is blocked (#118, ruled accept).

Co-discovered while making empty() byte-identical across stages: three
wwstage-only cgen fixes (cstage was already correct; wwstage aligned down):
- #129 sretretsize: consult the same-module pointer-alias before structlookup's
  any-module struct fallback (io.stream = *vtable was mis-sized as memio's 56B
  struct -> spurious sret save).
- #129 callsretsize: swap curmod to the callee's module before sret-size
  classification (cross-module callee context).
- #130 cgassign global-struct tagged-union field store: add the missing arm
  (was a 1-word store) mirroring cstage cgen.c:4893-4912.

The three are inseparable from io.empty here — splitting them out leaves a
divergent-asm intermediate (993/995 red), so they ride one commit per the
one-class gate-repair carve-out (#133-expanded precedent). Regenerates the
embedded combined.ww; 989_lib_byteid pins bufio + fmt graduated to M_ID.
(cgenexpr.ww fix-3 inline comment cites the #129 cluster; narrow to #130 on
next touch to avoid a regen for a comment.)
2026-06-07 06:19:13 +09:00
3ae24c22b8 lib/regex: #123 POSIX character classes [:class:] — charclass_map + compile/exec arms
12-entry charclass_map (str, *fn(rune) bool) + bracket [:class:] recognition
(compile) + charset_class_item predicate dispatch (exec), porting
ref/hare/regex/regex.ha:74-87/190-204/726-733. Table-driven tests cover all 12
classes (±), negation, composition, and the "No character class after '[:'"
error path.

cstage-only (C-first per the speed pivot); wwstage byte-id twin owed in the
batch-converge phase.
2026-06-07 03:10:55 +09:00
4b118fa8f8 cgen: B7 emitter elem chases + tools/peellint gate — #5 alias-arc cs side closed by construction
The last four raw `->under` reads outside the whitelist were the
static-DATA emitters' ELEMENT-type single peels (the outer type already
chased): emit_array_lit_bytes:14356, emit_strarray_data:14574,
emit_slice_data:14788, let_pre_intern:15088 -> type_chase_named.
:15088 is the :14574 row's label-order leg and must flip in the same
commit or _S_ labels intern in emit order, not decl order (the in-tree
comment at the site); the strarr row's byte-id is the coupling proof.

Behavior moves (ken B7 first-position oracle + impl pre-state, all
pre-observed at 05f7af7):
- [N]alias-struct + [N]alias-str globals graduate cs link-ERR
  ("undefined reference") -> 0/0 BYTE-ID (cs emits ww's DATAW).
- zero-consumer latent silence closed: a never-referenced
  2-level-elem-alias global silently lacked DATA (no reference, no
  link error); now emits, pinned by the byte-id cell.
- []alias-str diagnostic routing: the alias escaped the 3-way
  slice-of-{str,slice,tagged} fatal onto the downstream "not a
  foldable constant" text — now the intended 3-way text (== control).
- []alias-tagged DESIGNED NARROWING: the alias dodged the 3-way fatal
  ENTIRELY — cs silently accepted + RAN WRONG for reachable consumer
  shapes (review-verified at base: a len+payload-read probe exits 1;
  the len-only row was luck-correct). Now loud with the 3-way text;
  widen what the gate SEES, never what it ACCEPTS (B6-c2 precedent).
- kb7_slc/slc0 scalar legs byte-NEUTRAL (the synthesized-array
  choke-point already handled them); full kb corpus sweep: movers are
  exactly the two graduation shapes, nothing else.

tools/peellint (sizelint clone, dep of test/test-unit): character-scan
strips comments and string/char literals, then matches the under-token
accessor-spelling-wide — `->under`/`.under` in C (deref-dot is the
same peel), `.under` in ww, optional whitespace after the operator,
and the line-split continuation (operator at EOL, `under` next line).
Scope cmd/wcc + cmd/w6c + selfhost/cmd/wcc + lib/ww (lib/ww/typ.ww
ruled IN — it is type.c's ww mirror, the accessor layer itself);
`peel-ok`/`peellint-ok` annotations exempt a 10-line window. Green at
this tip = zero unwhitelisted raw peels survive; the gate lands in the
commit that deletes the last raw read (the-funnel-completing-commit-
carries-the-gate; sizelint precedent). Whitelist, 27 entries:
  cmd/wcc/type.c    :78 :141 construction, :162 chase body,
                    :180 :193 :214 recursive chase
  cmd/wcc/check.c   :102 :2572 resolve-state probes, :2586 construction
  cmd/w6c/cgen.c    :731 probe-cleared scan peel (B5-c1),
                    :813/:814 :834/:835 peel-ok #218 variant-match
  lib/ww/typ.ww     :316 construction, :374 :385 :410 :437 :447 :463
                    :475 :488 :514 recursive chase
  selfhost/cmd/wcc/cgenutil.ww :1302 chase body (tichase),
                    :2759 probe-cleared peel
  selfhost/cmd/wcc/check.ww    :1815 construction (peellint-ok)

Negative validation wired into 944_peellint_gate (B4 precedent):
re-introduced raw peel (C and ww spellings) REDS the lint; corrupted
annotation (peel-okk-…, token-bounded matcher) REDS the lint; the
check.ww:3683 "io.underread" prose, a code read of a longer field, and
comment-quoted tokens are pinned green regression rows; real tree must
lint clean. 944_alias_emit_b7_run pins all four emit paths
table-driven (14 rows / 36 checks) incl. ken's ww observation cells
(ww checker rejects slice-literal globals, "let: not assignable" —
unmoved; plain []str louds at ww's own emitslicedata 3-way, pinned by
the shared needle).

REVIEW AMENDMENT (reviewer-B7, fix-what-you-find): the frozen tip's
regex matcher passed five compiling evasion spellings green — `t ->
under` spacing, `t->`/EOL + `under` next-line (both stages; ww parses
`t.`/EOL too), C deref-dot `(*t).under`, ww `t. under`, and a string
literal containing a block-comment opener that blinded the regex
comment-strip for the rest of the file. The matcher is now a
character scan (comments + string/char literals stripped before
matching) with the widened token rule above; all six spellings are
pinned RED rows in 944_peellint_gate (checks 10 -> 16). The 10-line
annotation window stays as designed (a peel within an annotation's
window is exempt by construction — the window IS the exemption
mechanism). Lint + test bytes only; zero compiler-source bytes moved
in review.

What this does NOT close, said out loud (f2-ruling): a consumer that
never spells `under` at all — a switch on t->kind that simply never
peels — has no token for the lint to see. The accessor+lint closes the
WRONG-PEEL class (single-peel where chase was needed) by construction;
the NO-PEEL class is closed only at sites where classification routes
through the internalized chasing helpers, and contained elsewhere by
the acceptance-commit-carries-tripwires doctrine, which stays standing
for every future acceptance widening. The gate does not make alias
bugs impossible; it makes the four-times-burned shape unwritable.

Rule-11 note: forced fuse — the four conversions ARE the last raw-read
deletions; peellint cannot be green one commit earlier (consumer-graph
-forces-the-fuse precedent, #61).

Invariants: cs asm byte-NEUTRAL on the whole bootstrap corpus (five
mains + smoke, base-input pre==post); five mains cs==ww byte-id at
tip; _ww binary quartet bit-identical to the W2 baseline (ww changes
are comment-only annotation bytes — codegen-inert, proven by the md5
hold); w6c_ww+wwdump main.combined.ww regen'd via make, idempotent;
989 lib ratchet zero flips (31 byte-id / 9 pinned-divergent / 3
pinned-wwreject across 43 units); sizelint 0; peellint 0;
make test-unit "all 294 tests passed" (292 + the two new suites).
2026-06-06 06:05:46 +09:00
738d7f481c wcc/check: #62 typedecl layout is decl-order-INDEPENDENT — demand-resolve forward refs + loud cycle guard (#69)
check_file resolved typedecl bodies in file order with an eager
under->size copy, so any body referencing a typedecl declared LATER
read its size-0 placeholder and baked it in: alias size 0, tagged-
union maxsz 0 (the F0 m5_match $48-frame under-allocated box), struct
field offsets collapsed, array element stride 0 — a whole cstage-only
family (7 size()-probe rows, all cs-fail/ww-pass pre-fix). wwstage's
demand-driven tinfofornode was order-independent on every row, so this
aligns cstage UP to the measured runtime-correct side (the #263-era
ruling; rule 10's align-down governs acceptance surface, not layout
correctness). Oracle: ken /tmp/ken_62_oracle.md — union size is 8B tag
+ roundup8(max CHASED member size), a fixed point over the module,
never a function of decl order.

resolve_typename now resolves a referenced-but-unresolved typedecl on
demand via resolve_typedecl (cycle-guarded by Type.resolving); the
pass-1.5 loop funnels through the same helper. No consumer can see an
unresolved placeholder by construction.

CYCLE GUARD — #69 ABSORBED into this rider (rob's rider condition):
true typedecl cycles now LOUD-reject on BOTH stages — "circular type
dependency" — mirroring harec's in_progress check (ref/harec/src/
check.c:4767 "Circular dependency for '%s'"). Pre-guard: cs silently
sized cycles 0; wwstage HUNG on an alias cycle (`type a = b; type
b = a` — ken's hang probe /tmp/ken62/c1_cycle.ww, killed at the 20s
timeout) and stack-overflowed on a struct value cycle. The check sits
at the VALUE-position size consumers only (alias root, struct field,
array elem, tuple member, union member), so the legal pointer
self-ref (`type node = struct { next: *node }`, the io.stream shape)
stays accepted, byte-id. wwstage gets the twin tinfo.resolving flag
(lib/ww/typ.ww) + circularnamed in check.ww; its arm loud-STOPS
(os.exit) rather than accumulating — wwstage's AST-level alias
walkers (resolvealias, aliaslookup chains) follow TNAME->TNAME by
name, blind to the tinfo table, and spin on a cyclic alias graph even
after the table edge is cut to tyerr (measured); cstage accumulates,
its single-peel ternaries cannot loop.

TWO-LAYER SPLIT — this is ONE bug number (#62) deliberately split
across THREE commits (this rider + F1 + F2), per ken's sizes-correct ≠
payload-correct proof: in NORMAL decl order both stages size the box
correctly (16/24, frames $64) yet both still run exit 2 — the box
STORE is word0-only, a chase-blind copy-WIDTH lookup in cgen, NOT the
type table. EXPECTED-FAIL after this commit: m5b_match1/m5_match stay
exit-2 both stages (now byte-id BOTH orders; pre-fix the fwd order was
$48-frame divergent). The Layer-2 sites and destinations:
  - F1 (cstage): cg_widen_tagged_store single NAMED peel,
    cmd/w6c/cgen.c ~2464 — the type_chase_named census family.
  - F2 (wwstage): rhsstructpayload bare name-keyed structlookup, no
    alias chase, selfhost/cmd/wcc/cgenutil.ww:3062 (structlookupchain
    :1691 already exists).
Banked runtime payload-readback rows for F1/F2: /tmp/impl62r_layer2_rows.md.

Test 944_alias_decl_order_size_run: every size class pinned in BOTH
decl orders (sizes, named union, struct field offsets, array elem,
2-level chain — norm + fwd twins, prefix-luck-breaking last-word
readbacks), 3 cycle BUILDERR rows + the legal ptr-self-ref row,
(void|base) no-regress control; dual-stage + per-row byte-id (arrelem
rows byte-id exempt: pre-existing #60 index-over-alias divergence,
order-independent, cited at the rows). lib/ww/typ.ww is an embedded
source: both main.combined.ww regen'd + committed (freshness gate).
2026-06-05 10:46:17 +09:00
7545bf7dcd regex: fold 5b — repetition {m,n} (the { arm + run_thread inst_repeat + rep prefill)
All three loud bounds flip: the { metachar (ha:368-402, inclusive
advance bound), the run_thread inst_repeat arm (ha:669-684, is/as
verbatim per the #42 fence), and the search rep_counters prefill
(ha:763-765, count-loop respell of the sized-fill alloc). The
deferred-metachar table EMPTIES — every metacharacter compiles; the
POSIX class body is the only loud surface left in lib/regex.

Two silent compiler finds surfaced and filed, respells drew-signed:
#49 (whole-struct assign from a match binding w/ tagged fields
corrupts them context-dependently — the parse_repetition unwrap goes
field-wise in-arm) and #50 (insert() grows the dst before evaluating
its value arg, +1 split mis-target on Hare's len(insts)+2 payload —
pre-bound, the '?'/'|' arm convention).

Activation table: the {m,n} matrix (+test.ha:443-460) incl. the
open-ended (0,7) and {,0}de rows, the \{ \} escape pair, the :635
5a carve-out, the {0,}/{1,}/{0,1} twins (cross-spelling agreement
with their 5a */+/? siblings), the Various composed rows minus the
[[:class:]] row (POSIX abort, fold-4 ruling), findall fo{2,}, plus
ww-added multibyte {2} and long-input {1,} stress rows.
2026-06-05 04:50:27 +09:00
c0c15945be regex: fold 5b — parse_repetition leaf (ha:486-545)
Dead until the { arm lands (tranche-A precedent); 13-row direct
private-fn table, error texts byte-exact. Hare's verbatim
((void|size),(void|size),size) tuple return can't cross a union
boundary yet — cstage's (tuple|error) return store is cgen-unwired and
wwstage's variant-match rejects the tuple case arm (filed #47) — so it
respells as the private repparts struct per the scope-fold5 §3
pre-signed fallback; graduates back to the tuple when #47 closes.
Riders: call-result .N tuple read loud-rejects (filed #48, bind-first
local); strings.index's standing i32 convention (#8) stays internal,
widened at each size boundary; ha:494's same-name re-bind is rejected,
second local feb.
2026-06-05 04:48:56 +09:00
a2c2bbc6b1 regex: fold 5a — capture groups (compile ( ) arms + run_thread groupstart/groupend + search spread)
compile: '(' (ha:317-323) appends inst_groupstart(capture_idx) and
grows jump_idxs per level; ')' (ha:324-334) appends the void
inst_groupend, fixes up the level's pending alternation jumps (#70
range + #58 assert), range-deletes them (#8) and closes the level;
the loop-exit done arm gains the Unmatched-'(' check (ha:277-282).
The anchors' group_level arms, the postfix inst_groupend/groupstart
arms and find_last_groupstart's success path go live unchanged.

run_thread: inst_groupstart (ha:636-652) fill-grows captures to
idx+1 (count-loop spelling of Hare's 3-arg fill-append) and opens
the group with the SIZE_MAX end sentinel via the #20-fixed indexed
struct store; inst_groupend (ha:653-668) closes the innermost
unclosed capture (ha:655's 2-clause for respelled — ww has no
cond;post form) and slices content from the bytesize span through
the addr-of-element pointer.

search: the ha:820 loud bound flips to the real capture spread
(#35/#25); the pad fill self-activates for unset trailing groups.

Tests: ( ) graduate from the metachar-loud table into real-text
error rows (+ ww-added anchor-in-group / Unused-on-groupstart
re-verify rows); hand-built groupstart/groupend arm cases; the 5a
find table (+test.ha:257-275 group/alternation, :499-503/:607 jump
bugs, :610-621 submatch family, :635/:640 alternation-group,
:649-665 nested minus the 5b {m,n} twins, + ww-added multibyte
row) with len(res) pinned per row; submatch content rows
(+test.ha:704-708 + ww-added multibyte) — the 5a acceptance gate.
2026-06-05 00:49:42 +09:00
0e61db1857 regex: fold 5a — add_thread dups parent captures/rep_counters (ha:568-573)
The loud bound (capture dup not yet portable, #35/#34/#7) flips to the
real dup now that #35's spread place-chain sources landed: fresh slice
header + spread-append, the D3 spelling of Hare's alloc-dup. The
ok/defer-if frees drop (free() is the documented no-op, #27).
Dup-independence rows drive add_thread directly: values carried,
backing independent both directions, empty parent → empty dup.
2026-06-05 00:36:33 +09:00
60e61315bc ww/lex: fold float literals through strconv.stof64 — 1-ULP cs≠ww class (#62)
wwstage's parsef64 (naive i64-accumulator + pow-10 fold) diverged from
cstage's strtod: >19-digit mantissas overflowed the accumulator (sign-bit
garbage), DBL_MIN was +1 ULP, DBL_MAX -2 ULP — the #59.10 ratchet pin.
C-strtod oracle confirms cstage correctly rounded on every vector, so
wwstage aligns to it by dogfooding strconv.stof64 (correctly-rounded
decimal engine, already imported by lex.ww). Overflow literals now
reject in both stages (stof64 overflow -> errat, mirroring ERANGE).

Fix + #59.10 M_DIVERGE->M_ID graduation + pins land together per the
ratchet's designed flow (the gate trips loud demanding graduation):
oracle-pinned vectors in toktest.ww floatfold_cases (lexer-unit) and
989_floatlit_run (compiler fold: runtime bits + byte-id + overflow
reject parity). Retained subnormal accept-set asymmetry filed as task
#21, documented at the lexnum site.
2026-06-04 23:25:07 +09:00
3daf134395 lib: retire os.assert/abort shims — assert/abort are builtins (#58 respell)
The flat checker scope makes ANY decl named assert/abort anywhere in
the combined unit disable the builtin unit-wide (the #45 shadow shape:
scope_lookup_prefer's cross-module fallback finds it). lib carried
three colliding @symbol("rt_abort") shims (os, time, strconv/stof)
plus the os.assert wrapper, so a bare assert(cond) in ANY program
importing os mis-bound os.assert and failed arity — a hard blocker for
regex fold-5 (regex.ha:660/670 bring builtin-assert mass). Ruled
respell-now per the recurrence test (#45 -> #58).

Delete the shims and the os.assert wrapper; every bare abort(msg)
caller (regex, strings, utf8, hash, getopt, encoding/*, time, stof)
now lands on the builtin, and the ~40 os.assert(c, m) sites respell to
the builtin assert(c, m) — restoring the exact Hare spelling the lib
ports diverged from (e.g. ref/hare/bytes/tokenize.ha:23). os.assert
had no Hare counterpart (Hare's assert is a language builtin); rule-9
wrapper removed. temp/dirs/bufio already use the non-colliding rtabort
spelling and keep it.

Now-dead 'import os;' lines kept (pre-existing precedent:
lib/strconv/strconv.ww carries one); a tree-wide dead-import sweep is
a separate concern. regex.ww's if+abort workarounds citing #58 stay
for the fold-5 owner to fold back into assert.

combined.ww regenerated for all five selfhost tools + the smoke
fixture via make.
2026-06-04 22:42:47 +09:00
95fea97868 regex: fold 4 — bracket expressions (handle_bracket + run_thread charset arm)
Port of ref/hare/regex/regex.ha:135-225 (handle_bracket, whole),
265-275 (in_bracket dispatch), 313-314 (the `[` flip), 249-252 (the
bracket state quad) and 704-737 (the consuming charset arm). `[`
graduates from the fold-2a loud set; `(` `)` `{` are the last
three loud metachars. The POSIX-class arm keeps its DETECTION
verbatim but loud-aborts its BODY (charclass_map stays #25-blocked;
falling through to the literal arm would silently compile
[[:alpha:]] as a 9-literal charset). is_consuming_inst already
covered inst_charset.

Spelling divergences, all site-documented: the dispatch propagates
via the explicit D13 match, not `?` (compile's 64B sret return is
the #38b loud-stop; the fold-3 find_last_groupstart precedent);
charset's declaration moves BELOW its member types (cstage sizes a
tagged alias with forward-declared members at a degenerate 8B —
ww-core #69, wwstage is correct); run_thread binds the charset
structurally, not via the alias (alias-typed slice locals mis-scale
their index reads in wwstage — ww-core #68).

Tests: Hare's own bracket block (+test.ha:278-345, the group and
POSIX rows excluded with their loud arms) as the 72-row find/test
table incl. multibyte literal+range brackets and an unanchored
[ab]+ composition row; charsets-table content pins (lit/range
discrimination, first-char ]/[ literals, literal dashes, multibyte
codepoints); program-shape pins ([abc] / ^[abc]$ / [^ab] /
[ab][cd] / [abc]*); exact-text error rows (Unmatched '[' ×3 incl
the escape interaction, descending [z-a]); findall composition.
The [[:alpha:]] abort text is unpinnable in-process (it kills the
runner) — source-audited until the POSIX fold.
2026-06-04 21:09:31 +09:00
da48b29a7c lib/regex: fold-3 — anchors, escape, postfix ?/*/+, alternation
Ports compile()'s fold-3 arms (ref/hare/regex/regex.ha): \ escape
(286-293), ^ (294-300), $ (301-312, goes live with run_thread's
anchored ha:621-624 pin), | (335-367) over the restored jump_idxs
prologue (241-256 subset: jump_idxs + was_prev_rune_pipe +
group_level verbatim-but-0) + the whole-expression fixup (470-473,
the SIZE_MAX-sentinel overwrite 2a dropped), ? (403-420), * (421-443),
+ (444-459); find_last_groupstart (104-119, whole — error arm is the
live one until the group fold) and shift (123-133). run_thread's
inst_split/inst_jump aborts FLIP live (606-611, all 2b-proven
shapes). Still loud: [ ( ) { in compile; groupstart/groupend/repeat/
charset arms; add_thread's capture-dup bound.

Spelling divergences, each cited at site:
- multi-type case (inst_lit | inst_charset | inst_any) (ha:407) is
  loud-rejected both stages (PE5; Hare-parity #13) -> three void arms.
- find_last_groupstart(...)? in the dead groupend arms hits the #38b
  >32B-tagged-return propagate loud-stop -> explicit D13 match
  (harec's own ? desugaring).
- by-value range over an indexed element (for (let x .. jump_idxs[g]))
  SEGFAULTS both stages (NEW, filed ww-core #57) -> D9 index loops at
  ha:351-356 + 470-473.
- assert(cond, msg) is wwstage-broken (CALL assert(SB), undefined at
  link; NEW, filed ww-core #58) -> if+abort at the ha:355 site.
- Hare's match/if EXPRESSIONS (ha:337-346) -> statement spellings
  (the #51 search/scanrune precedent).

Tests: the 11-pattern loud row shrinks to the 4 remaining metachars;
strerror reroutes through a[. New: fold3_programs (exact inst
sequences incl. the a|b sentinel fixup), fold3_compile_errors (12
exact texts incl. Hare's own ab\|^cd ERROR fixture), direct
find_last_groupstart/shift rows, fold3_find_cases (42 rows from
Hare's +test.ha anchors/postfix/alternation blocks, group-free
subset, end=-1 resolved to rune-length; rob's dedup/leftmost/longest
riders now observable: a* over aaaa -> single (0,4), b+ over abab ->
(1,2), b* / ^b* over aaaabbbb -> (4,8)/(0,0); multibyte b+ row keeps
idx != bytesize) + the a*-over-baa findall rider (longest-pick beats
the zero-length candidate; trailing zero-length match takes the
ha:942-945 break). Both drivers green; regex_test.combined.ww cs==ww
byte-identical.
2026-06-04 17:25:05 +09:00
48904e5ef3 lib/regex: fold-2c — findall/result_freeall
findall (regex.ha:923-960) over the memio seeker: one fixed stream
for the whole string, per-call suffix substring, absolute io.seek(SET)
past the scanner readahead after each match. The append-then-mutate
m[0] fix-up is verbatim Hare (the appended header shares m's backing);
the zero-length-match rune advancement guard (ha:946-952) carries the
infinite-loop protection. search's |success|=2 unwrap is the D13
explicit 3-arm match (ww-core #14); nomem propagates.
result_freeall (ha:1119-1124) verbatim, frees no-op (#27).

Tests port Hare's own findall table (+test.ha:719-731, the three
fold-2a-reachable rows) through run_findall_case's checks, plus field
rows pinning adjacency, the one-result overlap pick, multibyte
zero-length advancement (utf8sz step != 1), idx != bytesize, the
tail-match break, and the empty no-match slice. 989's run gets the
conventional timeout-180 wrap: a regression of the zero-length guard
would otherwise hang the gate (no-op frees, so no quick OOM exit).
2026-06-04 16:11:08 +09:00
532b0a88ae lib/memio: wire seeker (io.seek dispatch landed; #5-era deferral stale)
io.error grows errors.invalid (ref/hare/io/types.ha:11 spreads
...errors::error, which includes it; Hare's memio seek returns it on
out-of-bounds, stream.ha:134-136) — appended last so existing member
tags stay put; no exhaustive io.error matches exist in lib.

seekfn mirrors ref/hare/memio/stream.ha:122-140 over the flat header,
shared by fixed/dynamic/dynamicfrom (Hare wires the same seek into
both vtables). The io.off-vs-i64 arithmetic runs on an i64 copy:
cstage binop typing is nominal on aliases, wwstage accepts (filed,
ww-core #54).

791's stream_seek_unsupported row re-pins st_seek's void-arm on a
hand-built seekerless vtable: its old premise (memio wires no seeker)
is retired by this commit; memio seek success is pinned by memiotest.

w6c/wwdump main.combined.ww regen'd: they embed lib/io + lib/memio
(the freshness gates are blind to this — embedded-source discipline);
w6a/w6l/ww don't embed io, verified untouched.
2026-06-04 16:10:34 +09:00
bc048ebe65 lib/regex: fold-2b tranche D — test/find (the exec surface)
Port of test (ref/hare/regex/regex.ha:901-904) and find (ha:907-918)
— the exported exec surface over tranche C's search. fold 2b is
COMPLETE: compile → test/find runs end to end on the 2a literal
programs.

Both are |success|=2 `?` sites in Hare (`search(...)?` over
(void | []capture | nomem)); ww's `?` is gated to single-success
unions (the C6 interim), so each spells the propagation as the
explicit 3-arm match harec lowers `?` into (ref/harec/src/check.c:
2780) — the ratified D13 spelling, on task #14's acceptance list for
reversion when subset-union `?` lands. find's no-match `return [];`
(ha:916) binds a zero header first (#25/#31 ruling) — a valid empty
result the caller still result_frees. io::handle args are the landed
memio→io cast (&strm.vt); import memio added.

PD1 probed first (scratch/pd1.ww): []capture values returning
through (result | nomem) — the nominal-alias member — plus the
(bool | nomem) round-trip; build/run/byte-id green at base, so no
new compiler surface was crossed.

regex_test: +2 @test fns (signalled 23-24). test_matches = 8-row
tcase table (the six search-table inputs → true, thread-drain and
EOF-mid-pattern → false). find_cases = 8-row fcase table: the six
match rows reuse the search table's root-capture expectations (all
four indices + content), the two no-match rows pin the empty result;
result_free on every row, matched or not. Every fcase row also
cross-pins test/find agreement (test(re, s) == find(re, s) matched),
so an arm-swap in either D13 match is caught by the other surface.

Both drivers run the fixture exit 0; w6c vs w6c_ww on the
regenerated combined byte-identical (FC0).
2026-06-04 15:20:34 +09:00
6160277098 lib/regex: fold-2b tranche C — search (first end-to-end match)
Port of search (ref/hare/regex/regex.ha:746-898) per the drew §9c
map: the thread-machine driver over bufio.scanrune — per-rune
dispatch, all_matched best-pick (leftmost-longest), need_captures
early-exit, first-match leftmost trim, same-pc dedup, failed sweep.
compile()'s literal programs now match end to end; test/find (the
exec surface) ride tranche D behind the C6 multi-success `?` gate.

Spelling divergences, each documented at site with its ha cite:
io::handle param → io.stream; alloc([thread{...}])? → decl + append;
defer-block cleanup omitted (single-expr defer, no-op frees, #27);
rep_counters prefill → ratified loud n_reps>0 abort (no 2b program
can set it); newscanner default maxread → types.I32_MAX; scanrune
nomem arm dropped (no such member) and multi-type arms split (#13);
`return [];` → bind-first zero header (#25/#31 ruling); `result`
internals spelled []capture (#20/#38 alias family — reverts with
#47); ha:820's indexed capture spread loud-bounded provably-empty
(#35); `&..` by-ref ranges → index loops; the ha:821 sized
fill-append → count loop (self-activates with the group fold).

Two checker findings surfaced mid-port, probe-isolated, dodged at
site and FILED: #51 (cs≠ww — the cstage checker types a
match-EXPRESSION by its first arm's yield and rejects the io.eof
arm against rune; w6c_ww accepts the expression form and emits
runtime-correct code, review-verified on scratch/r51.ww), so the
scanrune receive is a statement match assigning into a pre-declared
(rune | io.eof); #52 (cs≠ww, wwstage only) — a same-name let in a
CLOSED sibling scope poisons a later for-init rhs (`let j: i64 =
i + 1` resolves i against the dead `let i: size`), so the ha:872
dedup counters are di/dj at site (scratch probes p51/p51b/c/d
isolate the trigger and prove the rename byte-identical).

add_thread's dedup bound reverts to len(*threads) — the FB1/#41
dodge, fix landed at 796d41b. Imports grow bufio + types.

regex_test: +3 @test fns (signalled 20-22) driving private search
directly over memio.fixed streams. search_matches = 6 struct-row
table rows: full match mid-string, mismatch-restart bcd/abcd,
leftmost-longest aa/aaa, zero-length ""/"" (the all_matched path
with matchlen 0 must NOT take the need_captures=false early-exit),
multibyte b.d over "aßbxd" (root 2/3..5/6 — every idx differs from
its bytesize), dedup-heavy aa/aaaa (stable across >=3 same-pc
passes). search_early_exit pins ha:845-847 (empty result, len 0);
search_no_match pins thread-drain void + EOF-mid-pattern void.
Every match row checks all four root indices plus content and
result_frees its result (including the early-exit empty one).

Coverage limit, mutation-verified and documented at the dedup row:
in 2a's fixed-length program space every match ties on match_len,
so the leftmost trim (ha:860-866) and the dedup sweep (ha:872-889)
are result-invisible — disabling either still passes the table;
disabling the failed sweep hangs (caught). Both turn result- and
termination-visible with the split/star fold; result stability is
the only external pin available today (threads is search-local).

Both drivers run the fixture exit 0; w6c vs w6c_ww on the
regenerated combined are byte-identical (FC0). PC1-PC4 + P12 probed
at base; PC2's blocker fix is the separate #49 commit (c34a48a).
2026-06-04 15:04:32 +09:00
e481cb86bd lib/regex: fold-2b tranche B4 — run_thread (thread-machine core)
First end-to-end engine execution in tree: compile("ab")'s program
runs through skip-spawn / lit advance / match capture under the @test
drivers (search and the exec surface stay tranche C/D). Ported
Hare-verbatim from ref/hare/regex/regex.ha:589-742 — the #40 arg
wiring carries the ha:602 loop condition with no let-bind; arm bodies
stay verbatim so the group/repeat fold pastes straight into this
match. Arms compile() cannot emit are one loud not-yet-ported abort
each (the fold boundary); Hare's bare unreachable abort()s carry a
message because os.ww's private abort(msg) shadows the builtin
cross-module (filed, ww-core #45). The (anchored: bool)/(lit: rune)
casts are checker-required (ww aliases are nominal where Hare relies
on transparency), WHY-cited at site.
2026-06-04 13:16:22 +09:00
f3f74c9b21 lib/regex: fold-2b tranche B1-B3 — thread-machine leaf fns (run_thread held on #40)
Port is_consuming_inst (regex.ha:553-555), delete_thread (ha:547-551)
and add_thread (ha:557-587) per the tranche-B scope (drew §9b). D12
chained-|| spells the multi-type is (parity task #13); D9 index loops
replace the &.. ranges (#11); the dedup-scan bound reads .len, not
the len() builtin (FB1, #41 — len(*p) loads the data pointer as the
length). add_thread's capture/rep_counter dup is loud-bounded per the
rob-ratified ruling: every ww route into the dup is blocked at HEAD
(#35 spread source, #34 element source, #7 element let-copy), and
fold-2a compile() cannot emit inst_groupstart/inst_repeat, so both
parent slices are provably empty in every reachable program; the
verbatim dup lands with the group/repeat fold (#3). run_thread (B4)
stays out: its loop condition passes the 56B inst by value from a
slice-element source, gated on the #38b extension (#40, in flight).

Fixture cases 15-17 drive the three fns directly (package regex):
all-10-kind consuming table, delete at middle/last/0-to-empty,
dedup suppress/strict-</matched-guard + inheritance + zeroed headers.
Byte-cmp on the regenerated combined holds the FC0-only baseline;
both drivers run the fixture green.
2026-06-04 12:12:27 +09:00
c801aa7954 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.
2026-06-04 08:54:17 +09:00
b7a4eda40a lib/bufio: newscanner + scanrune (regex fold-2b prereq)
Port Hare's auto-grow newscanner (scanner.ha:72) and scan_rune
(scanner.ha:259). scanner gains a maxread field (== cap for
newscannerbuf, scanner.ha:101); readahead grows by BUFSZ up to
maxread via alloc+copy (Hare appends; ww flat ptr/cap scanner,
old block left to process-exit reclaim). scanbytes' overflow
test gains the avail >= maxread leg (Hare's pending >= readahead
predicate) so a growable scanner refills instead of overflowing.
finish ports the free(scan.buffer) verbatim per the regex #27
precedent. Tests: rune scan over 1/2/3/4-byte UTF-8 + EOF,
invalid initial/truncated/surrogate sequences, newscanner grow
round-trip + scanrune-over-newscanner, maxread overflow.
2026-06-04 07:07:02 +09:00
60ad118da0 lib/regex: finish() ports the Hare frees verbatim (#27 landed)
ref/hare/regex/regex.ha:96-102 body restored word-for-word now that
the free() builtin is a documented no-op: each free evaluates its
operand and reclaims nothing (ww is a no-free runtime, rt/alloc.s:30).
Drops the fold-1 empty-body stub and its held-back note.
2026-06-04 06:07:27 +09:00
64f6cc90f2 lib/regex: pin exact loud-boundary text across all 11 deferred metachars (fold-2a review)
The loud-arm row asserted only a non-empty error on "a*" — a
half-ported arm returning any other error text, or another metachar
falling to the literal default, would have passed. Table over one
pattern per deferred arm ('^' leading, so the r_idx==0 skip gate
composes with the loud arm) compared against the exact boundary
text via strings.compare.
2026-06-04 05:45:52 +09:00
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
3cffe204d1 lib/regex: compile() literal core — lit/any/match + loud fold boundary (regex port fold 2a)
Ports ref/hare/regex/regex.ha:227-263 literal arms: leading unanchored
inst_skip, inst_lit / inst_any, epilogue inst_match(false). Every
deferred metacharacter arm returns a loud not-yet-ported error (the
fold boundary); state serving only deferred arms drops with them.
Hare free()/defer-if cleanup omitted (no-free runtime, #27); bare
append per #36. 4 new @test rows pin the emitted programs incl. the
empty-input and loud-boundary cases; compile()'s >24B tagged return
doubles as a #38 sret consumer. Rides #34/#38/#44/#45/#48 — all five
fold-2a blockers now closed on master.
2026-06-04 05:36:01 +09:00
70fa9e2264 lib: collapse the manual rt_ensure append workarounds onto the fixed builtin (#34 follow-up)
shlex.appendstr, getopt.appendoption, bytes.appendslice and
strings.appendstr existed only because the append builtin stored the
first 8 bytes of the element; each carried its own @symbol("rt_ensure")
bind and a grow-then-store-through-*T body, with comments promising to
"collapse in one go when the append builtin is fixed". The previous
commit fixed the builtin; this removes all four helpers and their
rt_ensure binds and spells every call site as plain append().

Bonus correctness: getopt's appendoption passed a hardcoded membsz of
24, stale since the str 24B redesign made option {rune, str} 32B — the
manual growth under-allocated past 6 options while &opts.ptr[i] strode
32 (latent OOB). The builtin derives membsz from the type table
(probe: MOVQ $32, SI), closing that drift by construction.
2026-06-04 02:22:25 +09:00
b6a41ae063 lib/regex: type model + finish() (regex port fold 1, partial)
Port of ref/hare/regex/regex.ha fold 1 (the data model). Lands the
full type model — error, the inst_* variants + 10-variant inst union
(the nominally-distinct same-underlying size/void aliases included),
result/capture, charset + items, the regex struct — plus finish().
Test 989_regex_run pins variant discrimination, payload extraction,
struct shapes, and finish() on cstage; w6c == w6c_ww byte-identical.

Two fold-1 constructs are held back behind filed compiler/fidelity
gaps, documented at their sites (regex tasks A–D):

  - charclass_map (regex.ha:74-87): const [](str, *fn(rune) bool)
    table — blocked on the array-literal->slice element-coercion
    checker gap (type.c:402-404 #258 borrow uses exact type_eq,
    no element decay). It needs `import ascii;`, so both land with
    the consuming fold (compile) once the gap is fixed.

  - finish() free()s; ww is a no-free runtime (rt/alloc.s:30), so the
    faithful body drops the frees, as the port drops every Hare
    free(). Kept as a no-op for API parity.

DEFERRED to later folds: compile()/exec/find/replace.
2026-06-03 23:44:44 +09:00
5c1e9c0bfb lib/ww/lex: kwlookup if-ladder -> [N]str parallel-table linear scan (kwtab, rides #18/#8)
Replace kwlookup's 30-arm streqn if-ladder with two parallel module-level
tables — kwnames: [30]str + kwkinds: [30]tkind — scanned linearly via
strings.compare, and delete the hand-rolled streqn. Rides #18 (module-
level [N]str static-init + relocations) for the kwnames data and #8
([N]enum element sizing) for the kwkinds[i] read, which is itself the
construct that surfaced the #8 elemsizeofc/array-init-store miscompile.

Two parallel arrays rather than a [N]kwent array-of-struct: a str inside
an aggregate element is the filed #18 follow-up. Mirrors the C twin
cmd/wcc/tok.c kwlookup (N=30, linear, no hash). Source re-applied on top
of the #8 cgen fix and regenerated fresh: the wwstage-compiled toktest
now runs correctly (exit 0, no segfault) where pre-#8 it smashed the
frame on the local [N]tkind init store.
2026-06-03 14:58:23 +09:00
7d2d2c30ef lib/ww/lex: replace putuint with strconv.u64tos (Wave-2 hand-rolled->lib) 2026-06-03 03:50:47 +09:00
96cf957b07 lib/ww/lex: fputq + tokprint if-ladders -> switch (Wave-2 structural)
fputq: 6-deep escape if/else -> switch (c); named escapes \\ " \n \t \r
as explicit cases, empty-label default folds the control-char arms into
(c<' '||c==127)->fputhex2 else fputcbyte, mirroring cmd/wcc/tok.c:172.
tokprint: 4-deep kind if-ladder -> switch (t.kind) with comma-cases
(IDENT,STR,ERR -> fputq; INT,RUNE -> u64tos; default -> nothing),
mirroring tok.c:194.

toktest gains @test tokprint_cases: drives tokprint over a temp fd and
packs every fputq escape (\\ " \n \t \r, c<0x20, c==0x7f, printable)
into a TK_STR text — branches 990_selfhost's corpus never reaches.

w6c + wwdump combined.ww regenerated.
2026-06-03 00:59:13 +09:00
9e82037998 lib/errors: errnotest use os.exit not raw syscall (rule-7)
The local doexit reimplemented os.exit via a raw rt_syscall(60) decl
with no documented divergence, while the file already imports + uses
os. os.exit (lib/os/os.ww:68) is byte-identical (syscall1(nr.EXIT=60));
ostest/stattest siblings already use os.exit.
2026-06-02 22:57:37 +09:00
aa3aae05b9 lib/strings,wcc,w6l: collapse nested-if to &&/|| at named sites (Wave-2 structural)
strings.bytesub two endpoint guards, wcc cgdot/cgassign 4-deep
allptr/N_IDENT/localfindnode pyramids, and w6l isarchive's 8 sequential
magic-byte rejects. The isarchive len<8 read-guard stays a separate
statement before the || chain so the byte reads remain bounded. Not
byte-id-neutral (short-circuit emits tighter branches / renumbered
labels) but functionally identical; cs==ww stage-parity holds.
Regenerated all embedding combined.ww.
2026-06-02 22:53:05 +09:00