Commit Graph

227 Commits

Author SHA1 Message Date
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
11e41ecea3 lib/ww/ast: nkname if-ladder -> switch (Wave-2 structural)
Fold the 69-arm `if (k == nkind.N_X) return "..."` ladder in nkname to a
single `switch (k)` with the terminal `return "?"` as the fall-past
default. The other ast.ww ladders stay: pr()'s kind dispatch is
side-effecting (emits output, recurses) and uses ||-grouped multi-kind
predicates, not a pure value->value mapping a switch can express.

Not byte-id-neutral (if-chain -> switch dispatch changes the asm), so
the ladder->switch equivalence is pinned by a new table-driven test:
lib/ww/asttest.ww drives nkname over every nkind plus the out-of-band
"?" fallback, wired as 905_nkname_run (same `ww run` @test shape as
904_tok_run). The 990_selfhost wwdump diff only covers kinds that
appear in its corpus.

Regenerates the w6c + wwdump combined.ww amalgamations (nkname region
only).
2026-06-02 22:18:19 +09:00
e027d3eec4 lib/ww/lex: tokname if-ladder -> switch (Wave-2 structural)
Fold the ~88-arm `if (k == tkind.TK_X) return "..."` ladder in tokname
to a single `switch (k)` with the terminal `return "<?>"` as the
fall-past default. kwlookup stays an if-ladder: it dispatches on
streqn() string compares over distinct literals, which a value-switch
can't express.

Not byte-id-neutral (if-chain -> switch dispatch changes the asm), so
the ladder->switch equivalence is pinned by a new table-driven test:
lib/ww/lex/toktest.ww drives tokname over every tkind plus the
out-of-band "<?>" fallback, and kwlookup over every keyword plus
non-keywords, wired as 904_tok_run (same `ww run` @test shape as
904_ascii_run). The 990_selfhost wwdump diff only covers kinds that
appear in its corpus.

Regenerates the w6c + wwdump combined.ww amalgamations (tokname region
only).
2026-06-02 21:58:39 +09:00
79e647b34d lib/ww/parse: drop redundant type annotations (Wave-1)
Drop 112 redundant `let x: T = rhs` annotations where the rhs already
infers T (newnode→*node, p.curfile/curtext→str, p.curline/curcol→i32,
accepttok/== →bool, parse*→*node). stmt.ww (75) + parse.ww (37).
Regenerate the two embedders' combined.ww (w6c, wwdump). Byte-id-neutral:
cstage-w6c asm of each combined.ww is identical pre/post.
2026-06-02 21:27:09 +09:00
b45f7c40ac lib/math/checked,log: drop redundant type annotations (Wave-1)
61 over-annotations dropped where the rhs unambiguously infers the
declared type: 22 overflow:bool comparison binds in checked.ww, and
the mem/s/sl memio.stream/io.stream/log.stdlogger triplet across 13
@test sites in logtest.ww. Sub-word res:/fullres: binds with casts or
truncation are kept. Byte-identical asm in both stages, both files
non-embedded.
2026-06-02 21:14:51 +09:00
688275bfd6 lib,wcc,w6a,w6l: char-literals for remaining source magic decimals (Wave-1) 2026-06-02 21:02:26 +09:00
5987e389b9 lib/fmt,strconv: char-literals for fmt/ftos magic decimals (Wave-1) 2026-06-02 20:21:42 +09:00
fe32dedb08 lib/ww/lex: char-literals for lexer magic decimals (Wave-1)
Pure byte-id-neutral substitution of ASCII magic decimals with char
literals across lex.ww (131) + tok.ww (19); regen w6c + wwdump
combined.ww (lex is embedded in those two only).

escape() out-values for \a (7) and \b (8) are now '\a'/'\b' — both
the C bootstrap lexer (cmd/wcc/lex.c:160-161) and ww's own escape()
map them, so the literals are value-equivalent and stage-symmetric.

Digit-value arithmetic (parseint/parsef64/hexchar: c - '0' over u8)
is left decimal: a rune/i32 char literal would promote the u8 operand
and is not byte-id-neutral. Multi-byte type-suffix comments (i8../f64)
are kept — they label more than a single char.
2026-06-02 19:56:48 +09:00
d1310a03ad lib: char-literals for ascii/fnmatch/shlex magic decimals (Wave-1)
Replace magic ASCII decimals with char literals in ascii/fnmatch/shlex
predicates (e.g. `c < 48` → `c < '0'`). Byte-id-neutral: ascii params are
rune, so rune<rune emission is unchanged; fnmatch/shlex compare u8 against
value-preserving (<=126) rune constants. Range bounds (0/31/127), the ±32
case offset, the 128 high-bit mask, and fnmatch 0u8 sentinels stay decimal.

Regenerate the three combined.ww that embed ascii (w6c, wwdump, smoke).

Add functional rows pinning predicates reachable only via fnmatch ctype
classes / shlex split: [[:space:]]/[[:print:]]/[[:graph:]] + the '\t' arm
of [[:blank:]] (fnmatchtest), '\t'/'\n' split separators + issafe's
special-char set (shlextest) — so a wrong substitution would be caught.
2026-06-02 19:24:40 +09:00
dfa9771f42 lib/crypto/sha256: cite #269 for the def-array-dim divergence (rule-7)
The literal array dimensions ([64]u8/[8]u32/[64]u32) where Hare uses the
BLOCKSZ def / [_]u32 are forced by ww rejecting a def in array-dimension
position. rule-7 requires a retained divergence carry a filed-task
pointer; add the #269 cite to the header divergence list and the state.x
at-site note (previously described the limitation but cited no task).
2026-06-02 09:52:29 +09:00
e9bd06a193 lib/crypto/sha256: restore faithful re-entrant sum() (#265 unblocked)
The port shipped sum() single-shot — mutating the live hash state —
because Hare's state snapshot `let copy = *h; let h = &copy;` (a
deref-rhs aggregate let-init of an array-containing struct) miscompiled
in cgen. #265 fold-1 (master 4d3f846) landed the full-size aggregate
copy for that axis, so restore the faithful form: pad+finalize the
snapshot, leave the live state untouched, close() the copy.

sum() is now non-destructive — summing twice yields the same digest and
writing after a sum() continues the stream. Pinned by a new reentrant()
@test (sum-twice identical + write-after-sum continuity). NIST vectors
unchanged.
2026-06-02 09:48:39 +09:00
8e9e28e357 lib/crypto/sha256: port SHA-256 over hash::hash; NIST test (989)
Port of ref/hare/crypto/sha256/sha256.ha — block-processed [64]u8
chunks, u32 modular arithmetic, hash::hash + io.writer surface. The
state embeds hash.hash (inline vtable at offset 0); the vtable + sum/
reset slots are wired post-construction (base64/memio convention).

u32 WRAPPING + vtable dispatch CONFIRMED CLEAN: all NIST vectors verify
byte-identical — empty, "abc", the 56-byte block-boundary case, and the
one-million-'a' multi-block stream (1000-byte chunks across many blocks,
stressing write()'s partial-block carry). cgen truncates u32 add/shift/
rotate to 32 bits correctly; no masking workaround needed.

Semantics-preserving spelling divergences (slice-copy as byte loops,
close()/digest loops) are noted at-site per CLAUDE.md rule 5/13.

ONE BEHAVIORAL DIVERGENCE, blocked on a cgen bug (flagged for ken/drew):
Hare's sum() snapshots the state (`let copy = *h`) so it is re-entrant.
That deref-copy of an array-containing struct miscompiles in ww cgen
(copied array fields come back zeroed). So sum() runs on the live state
and is SINGLE-SHOT until the cgen fix lands; every current caller does
one terminal sum(), so the digests are unaffected. Minimal repro:
  type t = struct { h: [4]u32 };
  let c: t = *(&s);   // c.h reads back wrong
A sibling bug (array return-by-value zeroes the result) was also found
and is avoided in the test's buffer-based helper. Both filed for ken.

The hash/crypto modules are dead-imported (no selfhost combined.ww
regen). 9xx test numbers are full, so the run-test shares the 989
prefix with siphash (distinct `short` name; 949_* multi-file precedent).
2026-06-02 09:44:38 +09:00
fdd87e56bf lib/crypto/math: add rotl32/rotr32 (sha256 prereq)
Subset port of ref/hare/crypto/math/bits.ha: the 32-bit rotations
sha256's message schedule and compression need. The wider bits.ha
surface (rotl64/rotr64, the constant-time compare family, xor) lands as
callers arrive. rotr32 is exercised end-to-end by the sha256 NIST
digest vectors, so no standalone @test ships here.
2026-06-02 09:44:38 +09:00
165dd7388d lib/hash: add hash::hash interface (vtable + sum/reset/sz/bsz)
Port of ref/hare/hash/hash.ha — the general-purpose hashing-function
interface that crypto/sha256 (and, later, the lib/hash/* checksums)
embed as their first field. Rides the proven io.stream + inline-vtable
shape: a hash is an io-write-only stream plus sum()/reset()/sz()/bsz().

One divergence (documented at-site): Hare's `stream: io::stream` field
becomes an inline `vt: io.vtable` at offset 0. ww's io collapse (#94)
makes the dispatchers take the vtable pointer directly, so the vtable
must be embedded inline for a *state to recover from the dispatch arg —
the base64/memio/io convention.
2026-06-02 09:44:38 +09:00
6d8434002d lib/encoding/base64: clear() via array-decay now that #258 lands; drop stale comment 2026-06-02 06:17:59 +09:00
a2659c7942 test/base64: table-driven decode_invalid; cover len%4==1/3, excess '=', mid-quad '=' 2026-06-02 05:01:02 +09:00
4d0d3b58e6 lib/encoding/base64: Hare base64/base64url on io-streaming surface
Rewrite the buffer-based base64 placeholder as a faithful port of
ref/hare/encoding/base64/base64.ha over the just-landed io-streaming
surface (mirrors lib/encoding/hex).

Ships: std_encoding/url_encoding (module-level `def` consts; decmap
trailing 0xff run spelled out, no '...', to stay on #251 and avoid the
#250 repeat-fill sugar); the streaming encoder newencoder/encode/
encodeslice/encodestr with a padding closer wired into the inline
vtable; encodedsize/decodedsize; and decodestr as a direct in-memory
decode via decmap (the same divergence hex took for its direct path —
its return union carries errors.invalid, unconstrained by io.error).

Deferred (at-site notes): the streaming decoder newdecoder/decode_reader
(#247-sibling, blocked on #199b — io.error lacks errors.invalid).

clear() wipes the work buffers with explicit full-length slices
(`[0:len(...)]`) rather than Hare's bare-array decay (pending #258
[N]T->[]T coercion) to preserve the whole-array hygiene wipe.

base64 graduates off 900_stdlib (cross-module refs resolve only via
driver concatenation, as hex did); coverage at 984_base64_run over the
RFC 4648 §10 vectors for std and url.
2026-06-02 04:55:08 +09:00
e3f49234f9 lib/encoding/hex: align to Hare io-streaming surface
The old buffer surface (encodedsize/decodedsize + encode(dst,src) i32 +
decode(dst,src) (i32|invalid)) does not exist in Hare — it predates the
#94 io vtable and mis-cited hex.ha:175 while implementing a different
signature. Replace it with Hare's real surface
(ref/hare/encoding/hex/hex.ha):

  - newencoder(out: io.handle) (:28) — write-only encoder stream.
  - encode(out: io.handle, in) (size | io.error) (:91).
  - encodestr(in) str (:68).
  - decodestr(s) ([]u8 | errors.invalid) (:175).

Divergences (documented at-site):

  - The streaming DECODER (newdecoder/decode_reader, :120,:129) is
    DEFERRED to #247, blocked on #199b: Hare's decode_reader returns
    errors::invalid, which fits Hare's io::error (spreads
    ...errors::error). ww's io.error (lib/io/types.ww:55-62) does not
    carry errors.invalid, and io.read's (size|eof|error) can't propagate
    it, so a hex decoder *stream* can't faithfully report invalid hex
    through io.read yet. decodestr ships as a direct transform meanwhile.
  - nomem dropped from encodestr/decodestr returns (ww memio.dynamic has
    no failure path — same memio.string rule-9 carve-out, memio.ww:208).
  - The local hex.invalid type is deleted in favor of errors.invalid
    (that was the original divergence).
  - encode uses a single io.write rather than Hare's io::writeall (ww has
    none — fmt.fprint:498-501: callers drive write-all over raw io.write;
    encode_writer is whole-slice so a single write is equivalent).
  - dump (:212) deferred: ww has no default-arg support and fmt's
    formattable lacks u64 (#209), so the address column can't be ported
    faithfully yet.

hex is now import-bearing, so it moves off the 900_stdlib standalone-
compile list (like fmt/os/strings/bufio/bytes/errors before it); coverage
stays at 979_hex_run.c. The stale "mirrors lib/encoding/hex.encode"
comments in lib/encoding/utf8/utf8.ww are updated, which regenerates the
6 selfhost combined.ww (5 cmd + test/smoke) (comment-only, byte-id-neutral).
2026-06-02 00:40:04 +09:00
9d383288d2 lib/ww: hash-index the tinfo cache, kills O(n2) compile (perf)
tinfocachelookup walked a flat prepend-only association list on every
cache miss -> O(N) scan x O(N) calls = O(N2) (91% of all wwstage
instructions on a 5k-line input; w6c_ww ~265x slower than its C twin).

Replace the single list head with a node-ptr hash index, mirroring
sym.ww scope.buckets (rule-12): NBUCKETS_TINFO=8192 power-of-two
buckets, ptr hashed via (key>>4)&(N-1) (>>4 drops the always-zero
aligned low bits so buckets don't cluster), cnext now chains within a
bucket. First-match-in-bucket preserves the old most-recent-bind-wins
order -> identical *tinfo per node -> byte-identical asm.

cstage (cmd/wcc C) has no such cache, so this is wwstage-internal:
no emitted-asm change, no cstage-symmetry obligation. Verified
byte-identical output (baseline vs new binary, same 32k-line input)
and 52.6s -> 0.54s (~97x). combined.ww regenerated for w6c + wwdump
(only tools embedding typ.ww). test-unit (235) + smoke green.
2026-06-02 00:08:52 +09:00
db5c5b6149 lib/strconv: add itos/utos integer format (strconv-int fold-2)
Graduates the integer FORMAT side to verbatim Hare ports, completing the
round-trip whose parse half landed in fold-1, and adds the machine-word
entry points.

  - u64tos: ref/hare/strconv/utos.ha:10-42. Replaces the pre-graduation
    basedigit() helper with Hare's rune LUT (lut_upper/lut_lower), single
    static buffer + bytes.reverse, and strings.frombytes for the
    `*(&s: *str)` reinterpret (rule-9 carve-out; ww's lib/types has no
    `string` struct). basedigit deleted (now dead).
  - i64tos: ref/hare/strconv/itos.ha:10-32. Now `if (i >= 0) u64tos(i)`
    else negate-and-prefix via `u64tos((-i): u64)`. This fixes the
    i64tos-on-I64_MIN bug (cgen.ww #144): the old `n = -n; for (n > 0)`
    left n at the I64_MIN bit pattern (still negative), emitting just
    "-". The `(-i): u64` two's-complement reinterpret yields the true
    magnitude 9223372036854775808.
  - itos/utos/ztos/uptrtos: int/uint/size/uintptr 8B machine-word
    wrappers (itos.ha:52, utos.ha:62/67/72), parallel to fold-1's
    stoi/stou/stoz. The existing iN/uN width wrappers are unchanged.

Divergences documented at-site: no static assert; LUT-select + base
normalize via the existing basenum() (ww has no if-expression); explicit
copy loop for Hare's slice-assign.

Probes (drew PROBE-BEFORE-COMMIT, all green on BOTH stages):
  - i64tos(I64_MIN) == "-9223372036854775808": cstage `ww run` exit 0 +
    wwstage-compiled binary exit 0; cs==ww .s byte-identical on the real
    combined (30190 lines).
  - static `[0...]` fill + rune LUT static-init emit byte-identically
    cross-stage (isolated smoke probe + the combined byte-id).
  - frombytes (not a types::string mirror) per rule 9.

Tests: extend inttest.ww with test_u64tos[_bases] / test_i64tos[_bases]
(verbatim utos.ha:74-103 / itos.ha:54-87, flat assert sequences;
feedback_test_match_hare_source) + test_word_wrappers. I64_MIN inputs
spelled -I64_MAX-1 (proj #245: wwstage mis-lexes the 2^63 literal).

combined.ww regen: strconv is compiler-imported via fmt, so w6c +
wwdump main.combined.ww + smoke.combined.ww are regenerated.
2026-06-01 23:36:36 +09:00
a11785273a lib/strconv: stoi/stou/stoz machine-word int parse (strconv-int fold-1 C2)
Add the int/uint/size entry points (ref/hare/strconv/stoi.ha:53,
stou.ha:107,113). Hare clamps to types::INT_MIN/MAX, UINT_MAX, SIZE_MAX
via stoiminmax/stoumax; ww's int/uint/size are 8B machine words
(INT/UINT/SIZE limits == I64/U64 per lib/types/types.ww:30-37), so the
clamp is a no-op — the full i64/u64 range parses with no spurious
overflow. Documented at-site (the bound consts are package-private, so
inlining them would just re-encode I64/U64_MAX).

Tests: extend inttest.ww with test_stoi_stou_stoz — value path, sign,
overflow pass-through, and the no-clamp fidelity (I64_MAX/U64_MAX parse
without overflow) plus hex/bin bases through the shared parseint core.

combined.ww regen: w6c + wwdump main.combined.ww.
2026-06-01 22:27:00 +09:00
6a5cdbd779 lib/strconv: parseint sign+overflow core; stoi64/stou64 fidelity (strconv-int fold-1 C1)
Port ref/hare/strconv/stou.ha:8-65 (rune_to_integer + parseint) and the
stoi64/stou64 fidelity rewrite (stoi.ha:9-17, stou.ha:70-76) over the old
digval loop. parseint is the shared sign + per-digit + multiply-overflow
core returning ((bool, u64) | invalid | overflow); stoi64/stou64 destructure
its `(sign, u)` tuple-in-union result — the shape unblocked by #242/#241.

Wins over the prior ad-hoc parse: leading '+' accepted, '-' on stou64 is
overflow (not silently dropped), wraparound overflow detection (n < old),
and the invalid payload carries the offending byte index per Hare.

Tests: lib/strconv/test/inttest.ww (run via test/wcc/922_strconv_int_run.c),
inline per-case checks mirroring Hare's assert sequences stoi.ha:56-86 /
stou.ha:116-138 (Hare's strconv int tests are flat sequences, not row
tables; feedback_test_match_hare_source). Covers valid dec/hex/oct/bin,
+/- sign, invalid+index, overflow, and U64_MAX / I64_MAX / I64_MIN
boundaries. The I64_MIN expectation is spelled -I64_MAX-1 (Hare's own
two's-complement identity) to isolate the test from #245 (wwstage mis-lexes
the literal 9223372036854775808 -> 0); the parse INPUT is unaffected and
yields the correct value on both stages.

combined.ww regen: strconv is compiler-imported (via fmt), so w6c +
wwdump main.combined.ww are regenerated.
2026-06-01 22:26:53 +09:00
38a906cd9b lib/strings: add cut and rcut 2026-06-01 16:33:45 +09:00
2b893b9353 lib/ascii: add strlower_buf/strupper_buf (#11)
Restore Hare's two-tier delegation: strlower/strupper alloc a buffer
then delegate to strlower_buf/strupper_buf, which fold ASCII case
into a caller-provided buffer. Too-small buffer returns nomem via the
`let nm: nomem` value form. ref/hare/ascii/string.ha:21,43.

Regen w6c/wwdump/smoke combined.ww — all three embed lib/ascii.
2026-06-01 16:12:05 +09:00
cdb74e8a49 lib/bytes: add cut and rcut (#4)
Port bytes::cut / bytes::rcut from ref/hare/bytes/tokenize.ha:392,413.
Both return borrowed (before, after) views split on the first / last
delimiter instance; void-case yields (whole input, empty). Needle order
is ww's (u8 | []u8), matching index/rindex (bytes.ww:57/91) rather than
Hare's ([]u8 | u8).

Unblocked by #10 (wide tuple-return / sret): ([]u8, []u8) is 48B,
over-cap, returned via sret and received by the call-site destructure
the tests exercise. combined.ww amalgamations regenerated (bytes is
compiler-imported via strings).
2026-06-01 15:41:10 +09:00
07fed80fab lib/ascii: add strlower/strupper
Port ref/hare/ascii/string.ha strlower/strupper as the allocating entry
points: byte-wise ASCII case fold, equivalent to Hare's rune fold since
case-folding only touches bytes <0x80 and every UTF-8 multibyte byte is
>=0x80 (passes through unchanged, length-preserving). nomem arises only
from the allocation's `?`.

strlower_buf/strupper_buf are deferred: ww has no nomem-value form or
capacity-bounded static-append to express Hare's too-small-buffer path
(#230); restore the two-tier delegation when those land.

Divergence (rule 7): the empty-input fast path returns a nil/0 str
because ww's alloc([], 0) routes through nomem, whereas Hare allocs a
zero-length buffer and zero-loops; documented at the bypass site.

Test vectors mirror Hare's @test (ABC/abc/[[[/こ/empty/aB1z). Adds
lib/ascii/asciitest.ww + test/wcc/904_ascii_run.c (registered in the
Makefile TESTS list and a build rule). Regenerates the ascii-embedding
selfhost combined.ww amalgams (#110 freshness); the wwdump amalgam also
reorders the ascii block after strings to satisfy the new import edge.
2026-06-01 09:24:59 +09:00
497f1fa0d3 lib: fmt fprint family over io.handle; remove the fdsink workaround (#5)
Graduates the fmt fprint family (fprint/fprintf/fprintln/fprintfln + internal putbytes/writeone/format*) from io.stream to io.handle, so a file (fd) prints directly through io.write's file-arm (commit-1). Removes the fdsink placeholder -- the fake-stream-vtable-over-os.write shim that stood in for the missing handle. The 8 stdio wrappers route over os.STD{OUT,ERR}_FILENO (new i32 filenos in lib/os; os is the import floor, so it can't hold an io.file-typed handle like Hare's os::stdout_file -- consumers cast i32 to io.file). Migrates the fd-shim sentinel tests 777/780/781 to fprint-over-handle as their headers designed, cstage-only per the pre-existing #209 (fmt is wwstage-uncompilable). Regenerates the 6 os-embedding combined.ww.
2026-05-31 18:51:12 +09:00
06c00e0e1b lib: io.handle union + seeker + handle-typed read/write/close/seek dispatch (#5)
Ports ref/hare/io handle.ha: a handle is (file | *stream); ww stream is already *vtable (#94 collapse) so the payload is (file | stream). file=i32 (Hare int is 32-bit, ww int is 8B word -- width-faithful, USER-ruled). read/write/close/seek/tell match on the handle: file-arm to os.read/write/close/lseek (os plays Hare sys role), stream-arm to the unchanged st_* vtable bodies; seeker is the 4th vtable slot (copier deferred). On a file-arm syscall error the stub returns errors.unsupported with a #199b marker -- faithful errno to io.error needs io.error to spread ...errors.error (#204/#199b-blocked); only the error value is lossy until then, the type stays faithful. Regenerates w6c/wwdump combined.ww.
2026-05-31 17:30:55 +09:00
2e760d070d Revert "wwstage: add tinfo.module field, P0 of (module,name) NAMED interning (#10)"
This reverts commit f68a4c185b.
2026-05-31 15:39:13 +09:00
f68a4c185b wwstage: add tinfo.module field, P0 of (module,name) NAMED interning (#10)
Append module:str to tinfo and set "" in newtype (the sole tinfo
constructor); the field is set-not-keyed here — P1 keys typeeq's NAMED
arm on (module,name) to collapse cross-module same-nominal duplicates.

Byte-id neutral: only w6c/wwdump combined.ww regenerated (sole typ.ww
embedders); cs==ww and 990-997 hold; neutrality proven on the tinfo-free
corpus (w6a/w6l/ww/smoke).
2026-05-30 15:00:53 +09:00