lib/types limit consts were bare `def`s, so the .wwi (sep-compile's
interface) correctly omitted them while the flat combined.ww let a
cross-package user (lib/strings splitn → types.I32_MAX) reach the
private def — sep-compile then failed (wwstage `asserttyped: dot
'I32_MAX'`; cstage undefined-ref). Hare exports types::I32_MAX and the
whole limit family (ref/hare/types/limits.ha, arch+x86_64.ha); ww not
exporting them was the divergence.
export the 24 existing limit defs ({I,U}{8,16,32,64}_{MIN,MAX},
INT/UINT/SIZE/UINTPTR_{MIN,MAX}) and the existing RUNE_MIN, and add
exported RUNE_MAX. ww's derived machine-word int/uint/size/uintptr
VALUES are kept verbatim (user-ratified 64-bit-int divergence); fidelity
here is the NAME SET + export-visibility, not the values. RUNE_MAX is
written `0x10ffff: rune` — same codepoint as Hare's '\U0010ffff', forced
because ww's lexer has no \u/\U escape (#50).
Exporting the consts made `w6c -I` walk them and fatal on RUNE_MIN
('\0'): the .wwi const-expr unparser had no N_RUNELIT arm. Add one,
both stages (wwi_rune / wwirune), rendering a \xHH-escaped rune literal
(>0xFF fails loud, #50). Const casts need no arm — the checker folds
them to integer literals before the producer runs. 989_m2wwi_run gains
a types.wwi gate (byte-id + re-parse + asserts export def I32_MAX and
RUNE_MAX reach the interface). byte-id-neutral: a def emits no symbol.
Switch symbol mangling from the import leaf clause to the full dotted import path for directory packages; single-file imports keep package-clause mangling (isdir-gate: imported<=>directory-import). The root build unit's fn main stays bare, every other top-level decl mangles, closing #31's duplicate-main hazard by construction (#32). Both stages, byte-identical.
Single commit, not split: the bare rename (f244af3) is red on its own because it unmasks cross-module resolution gaps that do not reproduce pre-M1, so the fixes are intrinsic to making the rename correct. Included: wwstage fnret/fnparamslookupmod map import alias->path (#199b cross-module union-variant scrutinee resolved the wrong fn's union); cstage use_path prefers the referencing module's import for an ambiguous leaf alias (sha256 crypto.math vs strconv math). Tests table-driven: 989_m1mangle_run/_sym, 989_m1union_run (gate-visible per-arm exit codes + cs==ww byte-id).
prev()'s walk-back decremented offs (i32) past 0 to -1 and returned
`more`; a subsequent next() then passed the signed `-1 < len` guard and
read d.src[-1] — a silent OOB decode of a garbage rune (no runtime
bounds net). Hare's decoder.offs is `size`: the underflow wraps to
SIZE_MAX so every `offs < len` guard exits safely (next returns more,
not a rune). Change offs to size and spell prev's loop as the Hare-form
`offs < len` guard; index sites take an i32 temp (ww's slice index is
i32 and `[...]` reads ':' as the slice separator).
No-runtime-net residual: remaining() would silently build a ptr-1/len+1
OOB view when called in the post-`more` state; guard it with a loud
abort (caller contract: don't call after `more`). The offs type ripples
into strings.ww's iterator<->decoder bridge (move/slice) — cast at the
four sites, safe on the rune-return path where offs is in range.
utf8/strings embed into all five selfhost combined.ww snapshots plus the
smoke.combined.ww test amalgamation; all regen'd. utf8test gains
prev_more_then_next_no_oob pinning the closed OOB.
`ww test <file> <pattern>` runs only the @test fns whose names match the
fnmatch glob; no pattern runs all (byte-for-byte the pre-filter path);
zero matches prints "No tests run" and exits 0 (Hare ground truth
ref/hare/test/+test.ha:114-117). A pattern in directory mode is rejected
"ww test: pattern needs a single test file" (rc 2), identical wording in
both twins (cmd/ww/main.c do_test + selfhost/cmd/ww/main.ww dotest).
Mechanism (a): rt/start.s stashes argc/argv into rt_argc/rt_argv getters
(rt_envp twin shape, -T synth untouched so 990-997 byte-id holds);
lib/os.args() rebuilds the []str view, build-once-cached; lib/test/run.ww
imports fnmatch and filters av[1..] (argv[0] is the binary path). The
driver forwards the 2nd positional as argv[1] via fork/execv (cstage) /
procrun (wwstage) so glob metachars aren't shell-expanded.
os.args() is the first `alloc`-caller in the base os module, so os.ww now
imports rt — the `alloc` builtin's malloc lowers to rt_malloc only when
the rt binding is bundled (mirror lib/strings/strings.ww:30); without it a
plain `ww build` of any os-importing program links bare libc `malloc`
(undefined). os is bundled by ~every program, so this is load-bearing.
The lib/test floor rises os-only -> os+fnmatch+ascii+strings in every -T
build; the bundled `ascii` module vs a `@test fn ascii` collision that
exposed is closed by the preceding #30 promote commit. 989_test_filter
pins the full matrix on both twins byte-identically; 949 gains the
dir-mode reject row. (#17)
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.
The follow-up collapse (70fa9e2) regenerated the 5 main.combined.ww but
missed the tracked smoke amalgamation, which also embeds lib/shlex,
lib/bytes and lib/strings — caught by the combined_ww_fresh gate.
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.
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 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).
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.
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 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).
Port Hare's stof_data.ha tables: `let left_shift_table: [65]u16`
(decimal-expansion metadata for leftshift_newdigits) + `let pow5_table:
[0x051C]u8` (digits of 5^k for k=1..60). Cite ref/hare/strconv/
stof_data.ha. Literal-suffix init form (`0x0000u16`, `5u8`) — the only
form cstage and wwstage both accept (cstage rejects bare-int literals
in [N]u8 init as "not assignable", candidate #130). Module-level inits
emit DATAW (raw .data) so bypass candidate #128's runtime store-width
divergence. `powers_of_ten: [596][2]u64` (Eisel-Lemire fast-path)
deferred to consumer-driven port — only stof.ha references it.
Prerequisite for fold-3 (decimal.ha port) where leftshift_newdigits
consumes both tables.
ww's int/uint are machine words (8B on amd64, type.c:58), not the 4B
Hare gives them on amd64 (arch+x86_64.ha maps INT_MAX->I32_MAX). So the
limits can't alias a per-arch literal; they DERIVE from size(int) the
Go way (cf math.MaxInt), staying correct on any word width:
INT_MAX: int = (1 << (size(int)*8 - 1)) - 1
INT_MIN: int = -1 << (size(int)*8 - 1)
UINT_MIN: uint = 0
UINT_MAX: uint = ~(0: uint)
All four const-fold in def-init; on amd64 they evaluate to I64_MAX,
I64_MIN, 0, U64_MAX. UINT_MAX uses the all-ones complement to dodge the
1<<64 overflow. Per the user ruling (2026-05-26): derived, not literal.
Probe 959_types_intlim_run asserts each value vs both the literal and
the i64/u64 limit const, plus wrap-through-i32 arithmetic usability.
combined.ww regenerated for all 5 selfhost tools + smoke (all embed
lib/types).
Faithful port of ref/hare/types/arch+x86_64.ha:16-26. SIZE_MAX is the
no-cast `def SIZE_MAX: size = U64_MAX;` — size is in the unsigned class
and 8B on amd64, so the u64->size init coerces without a cast (#113);
UINTPTR_MAX keeps Hare's explicit `U64_MAX: uintptr` since uintptr is
outside the unsigned class. Probe 958_types_sizelim_run asserts MIN==0,
MAX==U64_MAX, and arithmetic usability for both types.
INT_MIN/MAX + UINT_MIN/MAX deferred to #114 (ww int=8B vs Hare 4B on
amd64 leaves the value open); RUNE_MAX deferred to #112 (no \U lexer).
combined.ww regenerated for all 5 selfhost tools + smoke.combined.ww
(all embed lib/types).
A ww `str` becomes a 24-byte {ptr,len,cap} value, identical in layout to
[]u8 -- the enabling prerequisite for the Phase 2 `str == []u8` collapse.
Both stages, atomically:
- ty_str 16->24B; str value flows 3-reg AX/BX/CX (was 2-reg); str literals
emit cap (=len).
- str in a tagged union grows to a 32B slot, using the AX/DX/CX/R8 4th-word
path already used by 32B slice-variant unions -- str-variant is now
structurally identical.
- tuple (scalar,str) return: 4-reg AX/DX/CX/R8 + 32B receive, extending the
existing type-keyed return (no sret).
- str == []u8 for index and .ptr/.len/.cap, kind-gated where size-based
dispatch collided at 24B; cstage and wwstage mirror exactly.
- table-driven runtime coverage: test/wcc/928_str_abi_run.c.
Cannot be split (rule 10/11): a 24B str and a 16B str cannot coexist across
the two compiler stages without breaking byte-identity, so the size change
and every dependent ABI/codegen site land in one atomic commit, both stages.
Known follow-ups (zero corpus impact, tracked): str-literal global .cap
static-init; >16B struct by-value (pre-existing); tagged-union
match-scrutinee stage divergence (pre-existing).
CLAUDE.md rule 9 amended with the explicit carve-out: ww is C/Plan-9-
lineage — no GC, no "safe" baseline to be unsafe relative to — so the
Hare `_unsafe` suffix flags an axis ww doesn't have. The convention
is dropped wholesale in lib/.
Concrete changes:
- lib/strings: `fromutf8_unsafe` → `frombytes` (pure reinterpret). The
validating sibling `fromutf8` is deleted entirely (28 lines, plus its
84-line fromutf8_cases test). Callers that need validation write the
two lines inline at the IO source: `utf8.validate(b)?;
let s = strings.frombytes(b);`. `fromutf8` name reserved for a future
true validating helper.
- lib/strings α-batch: concat/join/lpad/rpad migrate from
`rt.malloc(N): *u8` to `alloc([], N)!` + `buf.len = N;` +
`return frombytes(buf);`. Same dup-pilot pattern (4c07ef0). Task #41.
- lib/memio header comment trimmed: drops a stale reference to
"lib has no fromutf8 today"; cites the rule-9 carve-out instead.
- Caller renames across selfhost combined.ww files (auto-regen) +
cgenutil.ww comment ref.
Rule-11 disclosure on the bundle: the rename and the α-batch are
nominally separable concerns (symbol-naming policy vs amalloc→
alloc-slice migration), but they touch the same 4 functions in
lib/strings/strings.ww — the α-batch's first emission of `frombytes`
postdates the rename. The α-batch was applied on top of the rename
sweep mid-flight by the pre-commit reviewer; splitting them back
out is fiddly text surgery for marginal bisect value. The rename is
the primary concern; α-batch is one entry in #8's sized-slice
migration.
Verified: make test 132/132, 995_self_rebuild byte-identity holds.
Closes#42; advances #41.
Pilot for task #8 (runtime-N alloc API). `alloc([], n)!` yields a
slice with cap=n, len=0; explicit `buf.len = s.len;` lifts the len
before the fromutf8_unsafe reinterpret. Same shape as
ref/hare/strings/dup.ha:15 modulo ww not yet having `append`
(task #36) — open-coded byte loop in lieu of static-append.
Verified 132/132 + 995_self_rebuild byte-identity. Pattern is the
template for the next α-category sites (concat/join/lpad/rpad/...).
Hare's canonical runtime allocator is rt::malloc with linker symbol
rt.malloc (ref/hare/rt/malloc.ha:27,78). ww kept the dot→underscore
Plan 9 convention (CLAUDE.md rule 4) so the linker symbol becomes
rt_malloc; the lib/rt exported function name becomes malloc; ww
callers say rt.malloc(...).
The language builtin keyword stays `alloc(T)!` — unchanged from Hare
(ref/hare/hare/lex/token.ha:21 ltok::ALLOC, parse/expr.ha:398
builtin()). The rename only touches the lowered linker symbol and the
exported function name behind it; the user-facing syntax for
heap-allocation is identical to Hare.
Surface:
- rt/alloc.s: TEXT rt_alloc → TEXT rt_malloc, labels updated
- lib/rt/malloc.ww: @symbol("rt_malloc") fn malloc(...) (was rt_alloc/alloc)
- rt/ensure.ww: local FFI decl + call site updated to malloc; `!` dropped
on the direct FFI call (rt_malloc returns *void, not a tagged union)
- 18 .ww callers: rt.alloc(...) → rt.malloc(...)
- cstage cmd/wcc/check.c + wwstage selfhost/cmd/wcc/check.ww
alloc-builtin suppression gate routes through ffi_resolve("malloc")
for the lowering; the user-shadow check still keys on the BUILTIN
KEYWORD "alloc" since that is what `alloc(...)` parses as. Adding
"malloc" to the user-shadow check was unnecessary and was reverted
during pre-commit review.
- cstage cmd/w6c/cgen.c: 2× ffi_resolve("alloc") → ffi_resolve("malloc")
- wwstage cgenexpr/cgenstmt: 2× ffiresolve(c, "alloc") → ffiresolve(c, "malloc")
- Test fixtures (700_e2e, 758_cgalloc_str_field, 990_selfhost, 992_w6l_ww,
selfhost/test/tagged_ptr_ret.ww): updated inline ww sources to the new
decl + call form
This is commit 2 of 3 in the lib/rt extraction (#38). Commit 3 closes
the OOM contract — return type becomes nullable *void and the builtin
lowering null-checks + propagates nomem.
Verified 132/132 + 995_self_rebuild byte-identity (5 wwstage tools
round-trip identical) + make clean cold rebuild.
Hare puts runtime allocation in rt::, not os:: (ref/hare/rt/malloc.ha:27,
README). ww's `@symbol("rt_alloc") fn alloc(n: u64) *void;` lived at
lib/os/os.ww as a historical bootstrap shortcut; this commit relocates
it to a new lib/rt/malloc.ww and sweeps every site that depended on
`import os` for the alloc decl over to `import rt`.
This is commit 1 of 3 in the lib/rt extraction (#35):
1. (this) move decl, sweep imports — preserves shape
2. rename rt_alloc → rt_malloc (#38)
3. nullable return type + OOM-propagating builtin lowering (#39)
No rename here. Symbol stays rt_alloc, function stays `alloc`, return
stays *void. Behavior identical — same ffi resolution outcome, just
sourced from a different module file. The rt::ensure runtime helper at
selfhost/rt/ensure.ww is its own compilation unit with a local decl and
is untouched.
Side effect: every wcc cgen file used `rt` as a local *node variable
name for "return type." `import rt;` shadows the module, so each
selfhost/cmd/wcc/{check,cgenstmt,cgenexpr,cgenutil}.ww site renamed
to `rtyp`. Mechanical follow-through; only the wcc module-import was
forced to do this rename.
Verified 132/132 + 995_self_rebuild byte-identity (5 wwstage tools
round-trip byte-identical).
Phase A.5's tupleelemslot / fieldslotsize hardcoded 16u64 for TY_STR
and 24u64 for TY_SLICE — bypassing the tinfo.size SSoT seeded by
lib/ww/typ.ww:189 (the very pivot they were introduced to consult).
Route those four arms through pt.size / ft.size so #1 (str→24) and
#34 (slice graduation) land as a one-line bump at the seed.
lib/strings/stringstest.ww carried 12 `(cap: u64) * 16u64` strides
missed by #43's sweep over strings.ww + shlex.ww; convert to
`* size(str): u64` so the #42 fold owns the constant. Doc comments
in strings.ww (freeall + splitn) updated to the same SSoT form.
No-op at today's str.size=16 / slice=24: tinfo.size already matches
the literals these arms had baked in. Reviewer's pre/post asm-identity
probe (struct{i64,str,i64} + (i32,str,i32) tuple + bare str) shows
zero-byte diff. 131/131 + 994 + 995 + bootstrap (ww2==ww3==ww4) green.
Forward-link to #1 (str→24B bump) and #64 (sizelint pre-commit gate);
#65 filed for lib/bytes + lib/getopt sibling sites the reviewer
surfaced. Forward of #64 will catch any future regressions of this
class.
Audit §1.1/§1.2 cataloged 17 wwstage sites hardcoding 16 for sizeof(str)
and ~10 hardcoding 24 for sizeof(slice), plus 4 cstage str-size sites
and the cstage let_emit_size str/slice arms. Each new size constant
required ~30 edits in both stages to bump cleanly — task #1 (str → 24B
{ptr,len,cap}) can't land until the literal sweep is done.
Track A — wwstage codegen (selfhost/cmd/wcc/*):
- check.ww introduces two stateless helpers next to astsize:
primtypesize(nm) — primitive-name → byte size (i64; -1 unknown)
tyslicesize() — slice-header bytes (i64; 24 today)
astsize now reads both for its N_TNAME-primitive and N_TSLICE arms,
so the size(T) fold gets the SSoT for free.
- cgen.ww, cgenutil.ww, cgenstmt.ww, cgendecl.ww: every `return 16`
/ `esz = 16` / `sz0 = 16` for str, every `return 24` /
`localadd(c, _, 24, _)` for slice, plus the matching `sz == 16` /
`sz == 24` / `for (i < 16/24)` gates in the global-let DATAW emit,
route through primtypesize / tyslicesize.
- Direct delegation slotsize→astsize would require restructuring
astsize to drop its *checker dep (resolvealias) — the leaf
primitive/slice cases factor out cleanly, the alias-chain leaves
diverge because cgen's aliaslookup/structlookup tables and check's
scope chain aren't unified yet (§1.8, task #50 follow-up). Sharing
the leaf table satisfies the SSoT promise without that refactor.
Track B — cstage (cmd/w6c/cgen.c):
- let_emit_size's TY_STR/TY_SLICE arms drop the hardcoded 16/24 and
fall to `(int)u->size` like the existing TY_STRUCT/TUPLE/TAGGED arms.
- N_LET cgstmt's per-kind `sz` cascade collapses to a single
`if (lu->kind ∈ {ARRAY,SLICE,STR,STRUCT,TUPLE,TAGGED}) sz = lu->size`.
- N_LET cgexpr's match-bind primitive sizing: `bsz = (int)bu->size`
drops the TY_STR/TY_SLICE special-cases (same outcome — ty_str/
ty_slice already have ->size set by type.c).
- Three `sz == 16` / `let_emit_size(d->type) != 16` gates against the
str slot width route through ty_str->size.
Cap-offset sites (cgen.c:2440/1994/3206/5517 `delta = 16` for
slice's .cap field-write) intentionally NOT touched: 16 there is the
*offset of .cap inside a slice header*, structurally always 16
regardless of str.size. #1 doesn't move the slice layout.
Track C — lib/ user code:
- lib/strings.freeall + appendstr, lib/shlex.freepartial + appendstr:
the four `16u64` literals (per-str-element stride for rt_ensure and
os.free) become `size(str): u64`. Check-time fold via #42's
intercept resolves to 16 today; #1 reroutes via the bumped tinfo.
After this commit, bumping ty_str to 24B for task #1 requires editing
exactly two places (cmd/wcc/type.c:64 ty_str.size, plus check.ww
primtypesize's "str" arm) for the SSoT to propagate.
Verification:
- 131/131 tests pass. 994_w6c_ww + 995_self_rebuild byte-identity
holds — each replacement evaluates to the same constant the
literal had today, so cgen output is unchanged.
- selfhost source's `size(str): u64` folds at check time (cstage
cmd/wcc/check.c:907-960 for the C-bootstrap of selfhost; wwstage
check.ww:898-942 for the rebuild path), no runtime call introduced.
Mirror cstage cmd/wcc/check.c:907-960. Three typed-builtin
intercepts that cstage already had:
- size(T) — folds to a literal integer at check time from a
newly-introduced astsize walker over the type AST. Mirrors the
size computation in cstage resolve_type at check.c:286-528.
- align(T) — same, via astalign.
- offset(e.f) — folds the byte offset of field f in e's struct
type via astoffset. Peels exactly one N_TPTR for `p.field`.
seedprimitives registers the three names as SK_FN nil; exprtype's
N_CALL arm gates on a same-module shadow check (per #23 alloc
precedent) and consumes the parser-planted type-expression arg.
The fold is in-place — foldtointlit mutates N_CALL into N_INTLIT
so cgen sees a plain integer. resolvewalk's N_CALL trigger
invokes exprtype so the fold fires from non-let contexts too
(e.g. inside `if (size(T) != …)`).
selfhost/test/smoke.ww gains a probe-8 block: size/align/offset
assertions across str, primitive widths, ptrs, slices, and
two structs (`point`, `mixalign`) covering both no-padding and
i8+i64 natural-align padding cases.
Known divergences NOT in #42 scope:
- size((*T|void)) ≠ 8 on the cstage nullable-ptr fold (#13 family,
unreachable through current grammar).
- 8B-struct bare-let zero-init wwstage skip vs cstage emit (#59).
- Same-module shadow gate added here, cstage has none — sibling
shape to #26 (free/append/len gates).
Closes the original chain that started with the user's call to
fix the structural debt — six precondition fixes (#51, #52, #53,
#55, #56, #50) landed before this fold could safely live in the
check pass. Unblocks #43 (sweep literal 16s → size(str)) and #1
(str → 24B becomes one line).
ref/hare/strings/replace.ha:46-66. Two-pass byte scan: pass 1 counts
non-overlapping needle hits via bytes.hasprefix, pass 2 allocs the
result []u8 at exact size and copies chunks + replacement. Single
nomem propagation site at the alloc — ww's append builtin aborts
on OOM (#11), so the per-chunk append(...)? form Hare uses is not
available; the exact-size single alloc is equivalent in spec.
total==0 returns {nil,0} to dodge rt_alloc(0) per #47.
Empty needle is intentionally ungated and loops forever — that's
Hare's behavior at ref/hare/strings/replace.ha:31 (i += len(needle)
is 0; hasprefix("") always matches). Hare-faithful divergence,
documented at the site.
multireplace deferred to #49 — its (str, str) variadic gather hits
#39 in variadic-param position. Filed and blocked accordingly.
Wwstage's slotsize had a catch-all `return 8` for any N_TNAME where
primsize's `> 0` guard failed. `primsize("void") == 0` (correct —
void is zero-sized per cmd/wcc/type.c:46), so void landed on the
catch-all. (void | !void) then sized as `8 (tag) + max(8, 8) = 16`
instead of `8 + 0 = 8`, and the phantom payload word made
cgwidentaggedstore spill DX for the let-init — diverging from
cstage's `8`-byte slot.
Two narrow additions per rule 10 (align wwstage DOWN to cstage):
1. N_TBANG case at the top of slotsize, recurse on .lhs. Mirrors
cstage resolve_type N_TBANG which copies the underlying type's
size unchanged.
2. `void => 0` in N_TNAME BEFORE the primsize guard, so the SSoT
matches cmd/wcc/type.c:46.
757_letbind_void_bang_void exercises three shapes — void-arm,
invalid-arm, full natural-form fromutf8 — and pins cstage/wwstage
asm byte-identity per row.
lib/strings/strings.ww fromutf8 WHY-comment drops the Bug-B
SIGSEGV caveat (measurement artifact: original test linked without
rt/start.s; RET popped argc). Keeps #19 dependency for the
eventual collapse to `utf8.validate(in)?`.
Hare matches ww's design (void zero-sized, !T inherits T's
layout); this is a pure wwstage implementation gap, not a
divergence to argue about.
fromutf8(in: []u8) (str | utf8.invalid) and the bytesub form per
ref/hare/strings/utf8.ha:22 and sub.ha:59. bytesub keeps its byte
asserts (ww extension over Hare; predates #7).
fromutf8 walks the utf8 decoder via utf8.next rather than the
shorter `utf8.validate(in)?` form. Two compiler bugs in the way:
cross-shape `(void | invalid) → (str | invalid)` propagation is
#19, and (void | !void) match-bind locals diverge between stages /
str→union lift SIGSEGVs in cstage — both filed as #48. The
decoder-walk form bypasses both and matches what
ref/hare/strings/utf8.ha actually does in source.
getopt.ww:314 caller updated to match the new (str | invalid)
return; bi+1 cannot hit a continuation byte in well-formed argv
(bi is a just-matched ASCII flag), so abort spells the precondition.
bytesub_cases rewritten as exhaustive match; new rows cover
start-on-continuation and end-on-continuation invalid arms plus an
end==s.len bypass. fromutf8_cases is new — Hare vector + edge
bytes + multibyte parity rows.
ref/hare/strings/dup.ha:26-35. Returns ([]str | nomem); duplicates
every str in the input slice via the now-graduated alloc-slice
builtin (#45 unblocked `let s: []str = alloc([], n)?`). Loop body
uses appendstr because `[]str` element is 16B and the bare `append`
builtin truncates (#11) — pre-allocated cap=s.len means rt_ensure's
grow branch never fires.
Defer-rollback omitted: with `dup()` still unchecked (graduation
tracked by #46), the only nomem source is the initial slice alloc,
so there is no partial state to roll back. Will revisit when #46
lands.
Empty-input early-return short-circuits via {nil,0,0} because
rt_alloc(0) is an mmap of 0 bytes which the kernel rejects with
-EINVAL — Hare hands back a sentinel. Localized at the call site
pending #47.
Tests assert independent allocations at every index of multi-element
inputs, including a multibyte row.
Old shape ran byteindex then rewound to count runes — two passes,
different algorithm from Hare. New `indexstring` mirrors
ref/hare/strings/index.ha:59-81: one outer iterator over the
haystack, an inner iterator re-seated from it for each candidate
match, both walking rune-by-rune. Returns the rune-index of the
first match, or void.
Rest-iterator copy is field-wise rather than `let rest_iter =
s_iter;` because the local-to-local copy of the 3-field iterator
struct diverges between stages today (#41 — 993_ww_ww and
995_self_rebuild byte-diverge when written the natural way).
WHY-comment cites #41 with the precise failing tests.
Tests pin the rune-vs-byte distinction at i=2 and i=4 with 3-byte
kana, plus self-match, empty-needle, empty-haystack, and a no-match
multibyte row from ref/hare/strings/index.ha:119.
The 0-arg ltrim/rtrim/trim used to return input unchanged. Hare's
0-arg form strips [' ', '\n', '\t', '\r'] (ref/hare/strings/trim.ha:6).
Aligned by delegating to bytes.ltrim/bytes.rtrim with the whitespace
set spread inline at the call site — the obvious `let ws = whitespace[0:4]`
shape produces a slice whose ptr does NOT alias storage (#40).
N-arg forms (strip-specific-runes) untouched.
Test rows retargeted to Hare's canonical inputs from trim.ha:78/85
so '\r' is exercised alongside ' '/'\t'/'\n'.
Hare's strings::compare returns int (ref/hare/strings/compare.ha:12).
Result is a sign, not an index, so the i32 was cargo-culted from the
str-index type. Callsites already compared against 0, so callers
needed no migration. Widened the two return-site casts (u8→int,
i32→int) — the latter avoids i32 underflow on adversarial length
diffs. Added a multibyte test row contrasting ASCII vs UTF-8 lead
byte to exercise the high-bit-operand path.
The byte-indexed silent-clamp sub from ww was Hare's bytesub wearing
the wrong name. Renamed accordingly; added the real rune-indexed sub
per ref/hare/strings/sub.ha:30-42, with utf8bytelenbounded helper
per :10. Both forms assert on start>end; bytesub also asserts
end<=len(s).
lib/getopt/getopt.ww:314 migrated to bytesub — its bi index is a
byte offset over the arg's bytes.
Tests cover ASCII parity, multi-byte UTF-8 (こんにちは / héllo) where
rune index ≠ byte index, and a row contrasting identical args to
make the distinction explicit. OOB-abort coverage deferred until
the assert_aborts harness lands (#38).
Selfhost combined.ww snapshots regenerated — they're bootstrap-stage
inputs and would otherwise compile the old byte-wise sub. Two-arg
default form omitted (#37, ww has no default parameter values).