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.
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.
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).
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).
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.
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.
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.
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.
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).
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).
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.
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.
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.
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).
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.
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.
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.
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.
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.
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).
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.
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.
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.
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).
Hare-faithful port of errors::errno (ref/hare/errors/{rt,common,opaque}.ha): the 13 named common error conditions, opaque_data/opaque_ (the type-erased tail whose strerror fn-ptr defers to os.strerror), and errno(os.errno) error mapping the ~12 mapped errnos to named conditions and wrapping the unmapped tail in opaque_. The raw errno type (!i32, kernel-int width, distinct from oserror's !i64 negative raw return), the E* constants, and the strerror message table live in lib/os: ww folds Hare's sys role into os, so os is the import floor that lib/io and lib/errors build on -- documented in lib/CLAUDE.md (os never imports io or errors). errors.error is explicitly enumerated, matching Hare; the ...errors::error spread is only io.error's (blocked by #199b). Prereq for post-eFinal #5's faithful io error mapping; retires the nomem-collapse interim. Adds errnotest (mapping / opaque-tail / strerror) + test/wcc/902_errno_run. Landing required two wwstage cgen fixes (#9 struct-variant-large-union return, #11 deref-store alias narrow). Divergences cited at-site: bare-type-name return -> let+return; switch fall-through vs Hare's exhaustiveness-only default; opaque_ const dropped.
The Option-C parallel _v vstream API was scaffolding to bring the io stack up alongside the old surface; carrying both permanently is a rule-9 divergence from ref/hare, which has exactly one io surface. Collapse onto that surface (stream = *vtable, ref/hare/io/stream.ha) and rename the _v symbols to their Hare names (io vstream->stream, fmt vfprint->fprint, bufio/memio/log surfaces, log.new). Deletes the 4 lib/*/vstream.ww scaffold files; regenerates w6c/wwdump combined.ww. cstage and wwstage stay byte-identical and combined_ww_fresh holds; all 220 tests pass.
The outer widen of a NAMED multi-variant union value into an enclosing union
mis-tagged: the store took the tagged-subset path (inner value at slot+0 plus
a sub-variant remap, collapsing every inner sub-variant onto outer tag 0),
while the match-extract reads the nested layout (outer tag at +0, inner 16B
value at +8). Store and extract disagreed, so the match selected the first
arm. Pre-existing silent miscompile, latent because error-origination sites
(`let e: io.error = <leaf>; return e`) were gate-blind — no test discriminated
a freshly-originated error at a branched caller; the io vstream surface is the
first to do so.
Fix, both stages, byte-identical: cg_variant_match (cmd/w6c/cgen.c) and its
wwstage mirror cgvariantmatch (cgenutil.ww) fall back to structural equality
of the unwrapped tagged unions when the alias collapse loses nominal identity
(a NAMED outer variant vs an unwrapped-tagged source); the widen store now
writes the inner value at slot+8 and the outer tag at +0, matching the
extract. The inner union's build/payload/extract already worked (a destructure
through the outer round-trip recovers the inner payload) — only the
outer-widen store was wrong.
Collision guard (the fallback is unsound without it): structural matching
cannot disambiguate two nominally-distinct same-shape variants in one outer
union. That is unreachable under today's nominal-lossy collapse but inverts
the moment #199b lands the nominal layer, so if >=2 outer variants
structurally match the source we hard-error at compile time citing #199b —
both stages, an enforced invariant rather than a "rare, trust it" assumption.
Folds #219: the wwstage tinfo typeeq (lib/ww/typ.ww) had no TY_TAGGED branch
and fell through to `return true` (any two tagged unions compared equal);
cstage type_eq (type.c:269) has the structural branch. The structural fallback
above is the first and only caller to compare two bare tagged unions, so #219
is unexercised — and therefore ungateable — in isolation; it folds here per
the rule-11 couldn't-split carve-out (same structural reason as #206's
N_TTUPLE fold). The added branch mirrors cstage type_eq, tightening wwstage
into alignment.
test/wcc/925_nested_union_widen_run: outer-arm select, destructure-after-
propagation (payload survives the round-trip), destructure-let, single-variant
control, and the collision-guard compile-error, each with a cstage==wwstage
byte-id check (the path is gate-blind). Interim until #199b/B-full lands the
true nominal wrapped-slot layout.
The #94 Option-C vstream surface was left incomplete and structurally
divergent from Hare: constructors heap-allocated and returned (X | nomem)
or used out-params instead of Hare's by-value stack ownership; memio lacked
reset/buffer/borrowedread; bufio's scanner was never ported to the vtable.
This is the additive half of the eFinal collapse — OLD surface stays fully
live; the destructive FLIP (delete OLD + drop _v + repoint) is the next
commit.
Reshape all constructors to VALUE-RETURN (field-by-field sret; the heap +
nomem was an unnecessary crutch — wide slice-bearing struct return-by-value
is byte-id-proven, cf 925_sret_struct_return_run). memio fixed/dynamic/
dynamicfrom, bufio init, log new now return the struct by value; the nomem
is gone with the alloc that forced it.
memio: unify the per-flavour ctx structs onto one `stream` (vt at offset 0);
collapse fixed_string + dynamic_string into a single string() over the common
header (bare-str return is the ratified rule-9 frombytes carve-out, cited at
the site per ref/hare/memio/stream.ha:81); port reset/buffer/borrowedread as
single fns over the header.
bufio: collapse the EXISTING scanner subset (newscannerbuf/scanbyte/scanbytes/
scanline/finish + setflush/flush/unread/isbuffered) onto the vtable, with src
now io.vstream so reads go through io.st_read. The Hare scanner functions ww
never implemented (scanrune/scanstring-arbitrary-delim/readtok/readline/
auto-grow newscanner) are out of scope and deferred to #217 — eFinal is a
collapse, not a feature expansion.
Keep the explicit (&fn): *io.T casts on vtable-slot stores (cgen-neutral;
avoids the #214 (X|void) over-acceptance surface; dropping the casts is a
deferred #206 payoff gated on #214).
Self-gate: 776 (memio) 18/18 and 778 (bufio) 27/27, every row carrying a
cs.s == ww.s byte-id check — bufio/fmt/log are not compiler-embedded, so
these rows are their only byte-id coverage. 779/781 stay STAGE_CS-only
pending #209. Regen w6c+wwdump combined.ww (io+memio are the embedded
modules).
V had vfprint / vfprintf but no compositions over them, so callers
needing the newline / printf-newline / bounded-buffer / heap-grow
shapes still routed through the OLD io.stream-shaped fprintln /
fprintfln / bsprintf / asprintf. Port the four compositions into
vstream.ww as the v* twins: vfprintln + vfprintfln chain a
"\n" vputbytes after the underlying primitive; vbsprintf threads a
caller buffer through memio.fixed_vstream and returns the prefix view;
vasprintf grows through memio.dynamic_vstream and shrink-copies to a
tight allocation before io.st_close.
Bundles the two memio enablers (fixed_string / dynamic_string in
lib/memio/vstream.ww) that vbsprintf / vasprintf depend on directly,
per drew-approved exception to one-class-one-commit
(feedback_refactor_routing_same_class_drops applies — helpers are
direct prereqs, not unrelated churn; the bus-routing site lives in
v* fmt code, not in memio). They mirror OLD memio.string (memio.ww:
102) over the per-flavour *fixed_ctx / *dynamic_ctx intrusive cast,
same shape as the read/write callback split at memio/vstream.ww:144.
Mirror sites:
vfprintln fmt.ww:240 fprintln ref/hare/fmt/wrappers.ha:48
vfprintfln fmt.ww:740 fprintfln ref/hare/fmt/wrappers.ha:69
vbsprintf fmt.ww:839 bsprintf ref/hare/fmt/wrappers.ha:42
vasprintf fmt.ww:873 asprintf ref/hare/fmt/wrappers.ha:29
Divergence vs Hare on vbsprintf: Hare returns `(const str | nomem)`;
ww collapses to `(str | io.error)` so the underlying vfprintf io.error
arm stays uniform. The fixed_vstream nomem widens into io.error
explicitly (no `memio.fixed_vstream(buf)?`) because #173 (TRY-on-
tagged-return both-stages broken) is still open — same shape memio/
vstream.ww adopted at line 87-99 for fixed_vstream itself. vasprintf
keeps OLD's bare `str` return (no nomem variant on public surface).
ken cs==ww mechanical: additive only, both stages compile identically.
fmt is NOT embedded in any selfhost main.combined.ww (grep verified
pre-impl: zero `^package fmt;` hits in selfhost/cmd/*/main.combined.
ww). memio.vstream.ww IS embedded in w6c + wwdump combined.ww (lib/
ww/cgen.ww uses memio.dynamic for buffer growth); the two memio
helpers regen-and-commit via ww build per #110 SSoT.
test/wcc/781_fmt_vstream_compositions_run.c (cstage-only per #209): 4
rows pin all four V wrappers — fdprintln_v_run_basic (newline shape),
fdprintfln_v_run_fmt ({n}-placeholder + newline), bsprintf_v_basic
(fixed buffer + returned view + caller bytes), asprintf_v_basic
(owned heap str + os.free roundtrip). Mirror of 777/780 cstage carve-
out (#209 wwstage formattable match-arm bail). Byte-id graduates with
#209 close. 214 total tests green (was 213).
V's vfprintf parsed mods via scanmods but dropped them after parse —
vformatfield routed straight to vwriteone (no width / alignment / pad
/ sign / base / prec honoured). Port the OLD modifier path (fmt.ww:
443-641 rawlen* / formatraw / formatone) into vstream.ww as the v*
twins, widen vformatfield to take *mods, and pass &m through vfprintf
at the call site.
The v* helpers mirror OLD verbatim (compute body identical; vputbytes
+ (size | io.error) routing replacing putbytes + (i32 | io.closed));
shared compute helpers (signof / digitsu64 / basenum) and modifier
enums (neg / alignment / mods) are reused directly from fmt.ww via
package scope. fmt.ww UNCHANGED — fold-eFinal (#50) collapses both
surfaces and dedupes the rawlen-family.
drew NaN/Inf signoff: strconv.f64tos / f32tos already render
"nan"/"infinity" with no leading '-', so the sign-peel in vrawlenf64
+ vformatraw f64 arm is a no-op on those views (same OLD path at
fmt.ww:520-562).
ken cs==ww mechanical: both stages compile the new V-side identically;
990-997 byte-id gates + combined_ww_fresh stay green (fmt is not
embedded in any selfhost main.combined.ww — grep verified pre-impl).
test/wcc/780_fmt_vstream_mods_run.c (cstage-only per #209): 5 rows
covering width / precision / base_hex / sign_plus / zero_pad. STAGE_WW
blocked by #209 (wwstage formattable match-arm bail), same carve-out
as 777_fmt_vstream_run.
Last per-caller migration before fold-eFinal (#50). Adds the 10 _v
variants of OLD log.ww's surface (new_v / lprintln_v / println_v /
lprintfln_v / printfln_v / lfatal_v / fatal_v / lfatalf_v / fatalf_v /
setlogger_v) alongside a vlogger vtable + vstdlogger over io.vstream.
Default sink is a module-static stderrsink_ctx_g with vt FIRST field for
the intrusive vstream cast and fd=2 — only scalars/ptrs beyond vt per
ken's mandate, no nested aggregates that would bite #18, no f32 per
#165b. Zero-init at link time per #129 A.2/A.3 SSoT; ensureinit_v wires
vt.reader / vt.writer / fd lazily on first dispatch (mirror of OLD
ensureinit at log.ww:122 + lib/temp's rnginit pattern).
OLD lib/log/log.ww UNCHANGED. fold-eFinal (#50) atomically retires the
OLD logger / stdlogger / globals + the pre-vtable stderrsink and drops
the `_v` suffix wholesale to match Hare's bare names.
Two `export` bumps on lib/fmt/vstream.ww (vfprint, vfprintf) so log's
stdprintln_v / stdprintfln_v dispatch through the existing vstream-side
formatters; additive exposure, eFinal collapses fprint over the unified
surface.
Bootstrap-embed check: log is NOT in any selfhost/cmd/*/main.combined.ww
(grep `package log\|import log` returns empty pre-impl). The fmt
vstream.ww changes are also non-embedded. 990-997 byte-id gates stay
green by virtue of log being test-only and the touched fmt symbols not
being embedded.
Probe wcc/779_log_vstream_run pins the additive surface across 4 rows
(println_v_default_stderr / printfln_v_default_stderr /
lprintln_v_custom_sink / branched_lprintln_v) cstage-only per #209
(wwstage formattable match-arm bail; bites OLD log.println identically).
Byte-id graduates when #209 lands.
Cite refs: ref/hare/log/{logger,funcs,global,silent}.ha; drew acks on
module-static stderrsink_ctx + intrusive vt + 10-fn _v parity; ken
mandates on bootstrap byte-id mechanical + simple-ctx + #129 static-init.
Sibling tasks parked (filed, NOT fixed): eFinal #50; #206 (2 cast sites
at ensureinit_v); #173 (stderrwrite_v constructs nomem + widens to
io.error); #209 (cstage-only).
Adds bufio_vstream + isbuffered_v alongside the pre-vtable
bufio.init / bufio.isbuffered surface, mirroring fold-e2's
lib/memio and fold-e3's lib/fmt parallel-API shape. The OLD
bufio.ww surface stays untouched; fold-eFinal (#50) atomically
flips the package shape, drops the `_v` suffix, and retires the
legacy callbacks.
bufio_ctx wraps an underlying *io.stream (OLD API) — bufio_vstream
src parameter type stays *io.stream until io fold-2 lands
`handle = (file | int)` (drew-deferred). vt is the first field
for the intrusive vstream→*bufio_ctx cast, same shape as
memio/fmt vstream wrappers.
Sibling task filed:
#210 struct-lit slice-typed field silently drops under
alloc(T{slice = val})?. Parallel to #207 for slice fields;
scalar/ptr fields in the same alloc-struct-lit populate
correctly. Workaround: post-alloc field-assign
c.slicefield = val. Documented inline; drops out on close.
Other deferrals retained inline: #206 cast wrappers (3 vtable
wire-up + 2 isbuffered_v comparand), #173 nomem-widen for the
io.closed → io.error boundary, #207 alloc-zero-chain for vt.
bufio is not embedded in any selfhost combined.ww (test-only);
no Makefile regen needed (#110-blind safe).
test/wcc/778_bufio_vstream_run pins the 5-row scenario set:
write+flush, read+refill, isbuffered_v discriminator, OLD/NEW
boundary check, and a branched-callee #105 row. cs+ww+byte-id
green on all 5 rows.
make test: 211 passed (was 210).
Adds lib/fmt/vstream.ww with four new wrappers — fdprint_v /
fdprintln_v / fdprintf_v / fdprintfln_v — that take a raw fd, stack-
allocate an fd_ctx whose first field is `vt: io.vtable`, and
dispatch through io.st_write on a vstream pointing at &c.vt
(intrusive offset-0 cast — same shape as lib/memio/vstream.ww's
fixed_ctx / dynamic_ctx in fold-e2). Coexists with the pre-vtable
fdprint / fdprintln / fdprintf / fdprintfln in fmt.ww.
ken's escape-risk discipline: every wrapper owns the fd_ctx slot
for its frame only; the &c.vt vstream pointer is consumed inside
the same function (passed through internal vfprint / vfprintf
helpers) and never returned. Single tagged field (vt) lets the
local default-zero plus chained `c.vt.X = …` assigns sidestep the
multi-tagged-field copy drop (sibling #207) — no struct-lit init
needed and c is a stack value, not an aliased pointer, so #195's
chained-store carve-out doesn't bite either.
Drew defer cited at the file head: io.handle (= file | int) is
out-of-scope for this fold per fold-d/fold-e3 precedent; Hare's
ref/hare/fmt/wrappers.ha:9-25 routes through the handle sum, and
fdNNN_v collapses into bare fNNN_v once io fold-2 lands the port
(filed inline as "io fold-2 handle port" backlog).
Internal vfprint / vfprintf duplicate the per-arg and {n}-
placeholder loops from fmt.ww (fprint:177, fprintf:676) because
the OLD versions take *io.stream (the legacy struct) and fmt.ww
stays UNCHANGED this fold. Shared bits — i64dec, modsinit,
scandigits, scanmods, formattable, field, mods, fmtabort — are
reused directly from fmt.ww. vformatfield mirrors the inline-per-
arm dispatch shape OLD formatfield (fmt.ww:648) uses to dodge #18
silent miscompile of 24B return-by-value in for-loop context.
Cast workaround per #206 at each vtable-fn-ptr-slot init (2 sites
per wrapper, 8 total): bare `&fn_name` does not type-check as
`(*<alias> | void)`. Same `(&fn): *io.<role>` cast shape that
lib/memio/vstream.ww uses. Drops out wholesale when #206 closes.
#173 workaround at fdsinkwrite_v: constructs nomem and widens to
io.error explicitly rather than `os.trywrite(...)?` — same shape
memio.vstream.ww line 71-74 note adopted.
OLD fmt surface is UNCHANGED. fold-eFinal (task #50) atomically
flips the package shape: deletes OLD wrappers + callbacks, renames
_v suffix off, and migrates the few callers (with io fold-2's
handle sum landing in the same flip).
Probe test/wcc/777_fmt_vstream_run.c: 4 rows
(fdprintf_v_int / fdprintln_v_multi / fdprint_v_raw /
branched_fdprintf_v) open per-row /tmp output files, dispatch one
V wrapper per row, reopen the file, read the bytes back, and
assert both the exact byte content and a unique row-tagged exit
constant. 4/4 fixtures total, all green via cstage.
Cstage-only per row (no STAGE_WW, no byte_id) — pre-existing
wwstage match-arm bug (sibling of #190, filed inline as #209): the
wwstage checker bails `case: not a variant of scrutinee (X)` /
`match: variant not handled (formattable)` on the OLD
fmt.fdprint's match arms whenever any probe `import fmt;`s the
package. The bug bites the OLD surface identically — even
`fmt.errorln("hi")` from a probe trips the same trace. 970
fmttest dodges via cstage-only ww run; 995 self-rebuild dodges
because no selfhost cmd transitively pulls fmt (err.ww imports
fmt but no main.ww in cmd/{ww,w6c,w6a,w6l,wwdump} pulls err.ww in).
Byte-id graduates when #209 closes — out-of-scope for the additive
fold-e3.
Combined.ww regen NO-OP: none of the five tracked combined.ww
files (cmd/{ww,w6c,w6a,w6l,wwdump}/main.combined.ww) embed
`package fmt;` — fmt is not in the dep graph of any selfhost
binary. combined_ww_fresh stays green untouched.
210/210 tests passing (was 209; +1 for 777_fmt_vstream_run).
Adds lib/memio/vstream.ww with three new constructors —
fixed_vstream / dynamic_vstream / dynamicfrom_vstream — that return
io.vstream (= *io.vtable, from lib/io/stream.ww) alongside the
pre-vtable memio.fixed / dynamic / dynamicfrom shape in memio.ww.
Hare's memio::fixed/dynamic return a `stream` whose first field IS
io::stream (= *vtable); ww mirrors that intrusively with fixed_ctx
+ dynamic_ctx structs whose first field is `vt: io.vtable`. A
heap-alloc'd *fixed_ctx is castable to vstream via `&c.vt`, and
callbacks recover the outer ctx via `s: *fixed_ctx` (same pattern
as lib/bufio.stream over io.stream and lib/log.stdlogger over
logger). ptr/len/cap kept flat (memio.ww:39 SOP) to dodge the
chained-dot-through-pointer-into-slice-subfield miscompile family.
Constructor flow: alloc with vt zero-initialised via a local, then
chained `c.vt.X = …` field-assigns through the *ctx pointer.
Struct-lit init via `vt = local_vt` (with local pre-set) silently
drops tagged-union slots past the first — sibling task filed,
workaround is the alloc-then-assign route (proven byte-id between
both stages). *ctx is a plain pointer-to-struct, not an aliased
pointer, so the chained-store path doesn't hit #195.
Cast workaround per #206 at each vtable-fn-ptr-slot init (8 sites
across the 3 constructors): bare `&fn_name` does not type-check as
`(*<alias> | void)`. Same `(&fn): *io.<role>` shape that
test/wcc/775_io_vtable_run.c uses. Drops out when #206 closes.
OLD memio surface is UNCHANGED. fold-eFinal (task #50) atomically
flips the package shape: deletes OLD constructors + callbacks and
renames `_vstream` suffix off.
Probe test/wcc/776_memio_vstream_run.c: 4 rows
(fixed_read_5 / dynamic_write_grow / dynamicfrom_alt_rw /
branched_fixed) exercise both vtable flavours through the
io.st_read / st_write / st_close dispatchers. Each row drives
cs runtime + ww runtime + cs.s == ww.s byte-id — 12 fixtures
total, all green.
Pre-existing wwstage gap surfaced + documented inline: ww_ww's
combined.ww concat order trips the wwstage checker on os.tryread
/ trywrite / tryopen's bare `return r;` over `(int | oserror)`
when os is checked after rt/io. Each probe row places `import os;`
FIRST to match the ordering selfhost uses (time → os → rt → …)
where the checker resolves cleanly. Sibling task; resolves the
ordering-sensitivity in the wwstage checker drops the workaround.
Additive: keeps lib/io/io.ww's pre-vtable `stream` struct +
`read`/`write`/`close` wrappers (fold-e2-eN migrates the legacy
surface to vtable-backed implementations and retires it).
lib/io/stream.ww — vtable struct (reader/writer/closer slots,
spelled `(*T | void)` per #192 — ww parser rejects `nullable *T`),
vstream = *vtable, and the st_read/st_write/st_close dispatchers
per ref/hare/io/stream.ha:33-68. Void-arm `return e;` chains two
direct widens: concrete `errors.unsupported` → `error` (#199 α)
then `error` → (size|eof|error) (#205 NAMED-variant nominal at
tagged→tagged subset). Hare's `?`-propagating st_close collapses
to a direct `return (*c)(s);` because #173 is still open; the
surface stays Hare-shaped.
lib/io/types.ww — retarget reader/writer/closer fn-aliases from
*stream to vstream. Extend `error` union to include
`errors.unsupported` explicitly (no spread — per ken's #204-block
the wrapper-vs-flatten layout asymmetry would mis-widen; the
deferred fix is filed as #199b layout-extension).
test/wcc/775_io_vtable_run.c — 7-row sentinel: reader/writer/
closer × {set, void} happy paths + branched-callee runtime.
Rows verify the call runs + the constant exit; the void-arm
rows do NOT inspect the resulting variant tag (deferred #199b
wrapped-slot tag-remap mis-routes to dst tag 0). Cstage and
wwstage emit byte-identical asm on every row.
test/wcc/768_io_types_run.c — track the alias retarget; rows
now build a vtable, pass `&vt` (= vstream), and call through
the fn-VALUE param shape.
Combined.ww regen for w6c + wwdump per #110: lib/errors lands
transitively via the new `import errors;` in types.ww.
Sibling filed inline (NOT fixed): the checker rejects bare
`&fn_name` / `let p: *alias = &fn` assignment to a
`(*alias | void)` field — the structural `*fn(...)` value isn't
accepted as the `*alias` NAMED variant. Both stages reject.
Probes route around via explicit `(&fn): *io.reader` cast at
each vtable-field assignment.
New lib/io/types.ww mirrors ref/hare/io/types.ha — the surrounding
port that lives alongside the existing lib/io/io.ww (pre-vtable
stream + eof + underread). Hare splits the same way (stream.ha +
types.ha share `module io`); ww does the equivalent via dir-enum.
Each type cites Hare per CLAUDE.md rule 9:
- mode (enum u8) — ref/hare/io/types.ha:29-34. RDWR=3 (not
Hare's `READ | WRITE`) because ww enum-value
positions don't fold expressions; bitfield
value SSoT preserved, divergence inline.
- whence (enum i32) — ref/hare/io/types.ha:37-41. Hare leaves the
underlying implicit; ww requires one. i32
matches the `off` type fold-e wires in.
- error — ref/hare/io/types.ha:11. Hare spreads
`errors::error`; lib/errors not ported, so
the union carries the two tags observable
in this fold: underread (from io.ww) and
the predeclared `nomem` (#29, type.c:72 /
check.ww:78). NOT redefined here.
- reader/writer/closer — ref/hare/io/types.ha:46/51/55. EOF=eof
(not Hare's `done` singleton) per #93
and the io.ww:8 rationale. `*stream`
forward-refs the existing pre-vtable
struct in io.ww; same cross-file pattern
Hare uses.
Drew signoff (this fold only): seeker, copier, strerror, and the
EOF=done singleton DEFERRED to fold-e — they need the `handle` sum
and #93's done landing. Hare's `_unsafe` carve-out unaffected.
eof / underread / stream re-used from io.ww (NOT redefined); io.ww
keeps the pre-vtable struct unchanged, ditto its WHY-comments.
Combined.ww regen (#110): selfhost/cmd/{w6c,wwdump}/main.combined.ww
auto-pulled the new types.ww via dir-enum (+52 lines each, same
package io). Makefile dep lines for wwdump_ww + w6c_ww add the new
source so editing it triggers rebuild.
Probe: test/wcc/768_io_types_run.c — 5 rows × 2 stages = 10
invocations. Pins enum value/underlying + the three fn-type aliases
at the param slot. Both siblings filed inline in the probe header:
- #189 wwstage `let r: io.reader = fn_name;` bails "let: not
assignable". cstage accepts. Param + struct-field paths work
in both stages, so io vtable port is unblocked. Probe uses
the alias only at the param slot.
- #190 wwstage match-arm on cross-module variant tag bails
"case: not a variant of scrutinee (io.eof | io.error)". Likely
same family as #178. cstage accepts. Probe uses `is` instead
of `match` for the variant gate.
make test: 201/201 (was 200; +1 from 768). 990-997 byte-id +
combined_ww_fresh + sizelint all green.
The wwstage checker resolved a module-qualified call/access mod.x by the
same-module preference in scopelookupprefer, so when the importing package's
name collides with a type/fn of the same leaf (package fnmatch with fn fnmatch;
package random with type random), the dot-lhs mod resolved to the same-leaf
SK_TYPE/SK_FN instead of the coexisting SK_USE import — the N_DOT module-qual
arm never fired and the call went nil-stamped (the D class of the asserttyped
gap audit: fnmatch 2, random 16). cstage resolves this via Sym.use_alias; this
ports the equivalent to wwstage.
Add scopelookupuselocal (a single-scope SK_USE lookup, twin of scopelookuptype)
and prefer SK_USE for a dot-lhs in exprtype's N_CALL and N_DOT arms, keyed on
the scope where scopelookupprefer landed so a local binding sharing a module's
leaf keeps value semantics. Scope-layer only — no type-identity touch (cstage
use_alias never reaches type_eq).
Drives the 901 gap-corpus D count to 0 (random_test now byte-id cs==ww).
Compiler binary unchanged (no such collision in its own source); 990-997 hold.
The separate fnmatch bare-enum-member cgen cs!=ww is unrelated (filed).
The two local-binds were #168 dodges: a CSE before `% 10u32` to avoid the
signed-IDIVQ-on-call-result shape that #168 has now fixed. Inline to the
natural form, faithful to ftos_ryu.ha:418-421,444-445 — this exercises #168
in real ported code. The dividends are zero-extended u32 (always positive as
64-bit), so IDIVQ and DIVQ agree on the value; the fix is a cs==ww byte-id
shape correction, not a value change. strconv is compiler-imported, so this
regenerates the w6c/wwdump/smoke amalgamations.
The f32 coda of Drew's strconv 5-fold plan — f64tos shipped in fold-5a
(0e66073); this completes the plan. Re-lands the f32-exclusive Ryū path
that fold-5a removed under the dead-code rule (it was #143-blocked):
pow5fac32/pow5multiple32/pow2multiple32, mulshift32, mulpow5inv_divpow2/
mulpow5_divpow2, decf32, f32todecf32 (ftos_ryu.ha), and the f32tos driver
(ftos.ha:448). Plus F32_POW5_*_BITCOUNT in ftos_data.ww.
The f32 path REUSES the shared u64 core (mulshiftall64/u128mul/u128rshift/
log*) and f64computeinvpow5/f64computepow5 — and thus the f64 SPLIT2
tables — exactly as ftos_ryu.ha does; there are no separate f32 tables.
Unblocked by #143 (aff7725): f32tos calls math.f32bits(n), passing an f32
arg, which now spills MOVSS (4B) in both stages. Verified: f32tos's arg
push/pop is MOVSS, w6c vs w6c_ww 0-diff on the strconv-embedding
combined.ww (w6c/wwdump/smoke regenerated).
Dodges (cgen bugs still deferred, each cited at-site): decf32.exponent:i64
sidesteps the #169 narrow-second-field struct-return unpack (byte-id
gate-confirmed, not an ABI guarantee); #168 div/mod local-bind on the two
`%10` sites; *decimal pointer field reads (#170); [32]u8 buffer reuses
f64tos's byte-id-clean band over Hare's [14] (#43). mulshift32's U32_MAX
bound inlines the literal — ww's types.U32_MAX is package-private (#172).
Test: ftostest.ww gains f32 vectors — the tcs G/void rows (shared f32/f64
shortest), the f32-exclusive tcsf32 extremes (1e-45 / 1.1754944e-38 /
3.4028235e38, full 24-bit mantissa), specials, a negative-normal, and
33554432 (the sole e2>=0/q<=9 runtime cover). make test 187/187 incl
908_ftos_run + 990-997 byte-id + combined_ww_fresh.
Add a sub-bullet to the 8-spelling-divergences list documenting two
implementation sub-cases reviewer-fold3 surfaced during the 07e57ff
decimal.ha port: (1) `i_sz` per-iteration size-cast hoist inside
leftshift_newdigits' for-loop (decimal.ww:93); (2) `lowbit_lit`
stepwise boolean decomposition in should_round_up (decimal.ww:242)
dodging ww parser precedence on Hare's `(nd > 0 && d.digits[nd - 1]
& 1 != 0)` (ref/hare/strconv/decimal.ha:158). Both are in-file
instances of the documented hoist+restructure patterns — rule-9
doc-completeness, not new divergence. Combined.ww regen for
lib/strconv (compiler-imported into w6c + wwdump + smoke) uses the
build's include paths (`-I lib/ww -I lib/ww/lex -I lib/ww/parse
-I selfhost/cmd/wcc`) for transitive import closure; bare `ww build`
without these flags produces truncated output (reviewer-32c2 +
reviewer-fold3 both hit this).