Commit Graph

312 Commits

Author SHA1 Message Date
8e93b31088 cmd/w6c+selfhost/wcc+lib: route sizeof(str)/sizeof(slice) through SSoT
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.
2026-05-20 08:50:40 +09:00
3ec944a67f selfhost/cmd/wcc/check+test: fold size(T)/align(T)/offset(e.f) at check time
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).
2026-05-20 05:22:57 +09:00
c58d3511e8 selfhost/cmd+test: run check before cgen in wwstage drivers
w6c_ww and wwdump_ww (-c mode) silently passed source into cgen
without type-checking it; any type error flowed through as broken
asm with exit 0. Mirror cstage cmd/w6c/main.c:73-75: between the
parse-error gate and cgeninit, install typesinit + checkinit +
checkfile + `if (ck.errs > 0) return 1;`. Cgen path unchanged —
drivers own check, cgen owns emit (checkfile not idempotent due to
installdecl scopedefine).

The wire-up was blocked by five latent check.ww divergences from
cstage, all landed first: cross-module type refs (#51), enum-int
reinterprets (#52), per-block scoping (#53), nominal-first
tagged-variant inclusion (#55), and same-module N_IDENT callee
preference (#56). With those clear, the broader exercise of
check across every selfhost driver reaches 131/131 first try.

994_w6c_ww grows by one row: a trivially-wrong `let x: i32 =
"hello";` smoke pins both stages to exit-non-zero. Pre-#50 it
emitted 202 bytes of broken asm with exit=0.

Unblocks #42 (size/align/offset wwstage intercepts) and the
audit-§1.8 UP-polarity refactor (node.type_ population can now
land in the same check pass we just wired up).
2026-05-20 04:52:16 +09:00
3fa5ccd5ae selfhost/cmd/wcc/check: same-module preference on N_IDENT callee lookup
exprtype's N_CALL callee resolution used flat scopelookup, returning
the first match in the bucket regardless of caller module. Two
modules exporting fns with the same leaf name (e.g. alpha.foo i64
+ beta.foo str) caused bare-leaf callees inside one of them to pick
the other's fn, then false-positive at return type.

Cstage cexpr N_IDENT routes through scope_lookup_prefer(c->cur,
c->cur_mod, name) which short-circuits to the same-module hit
before falling through to flat scope. Mirror at check.ww:675 —
splits N_IDENT vs N_DOT so the latter keeps flat scopelookup and
the explicit module qualifier path stays distinct (tracked as #58).

c.curmod is already tracked by checkfile pass 2 (check.ww:1240-1241),
so this is a one-call swap on the N_IDENT branch. No plumbing.

Reviewer cascade probe across all 131 .ww/.combined.ww files in
lib/ + selfhost/ shows lib/memio/memiotest.combined.ww drops 4
spurious "let: not assignable" lines as a side effect, with no new
errors. Net improvement.
2026-05-20 04:42:50 +09:00
9fed4ca492 selfhost/cmd/wcc/check: nominal-first compare in tagged-variant inclusion
isassignable's tagged-variant inclusion was resolvealias-unwrapping
both src and each variant before typeeqast. Two NAMED structs (e.g.
`(void | err)` with src=`err`) both flattened to N_TSTRUCT and
typeeqast's conservative struct branch returned false — false
positive on the assignability.

Cstage variant_match (cmd/wcc/check.c:90-100) compares TY_NAMED
pointer-identically, so the nominal name short-circuits before any
body inspection. Mirror: try typeeqast on unwrapbang'd src vs
unwrapbang'd variant first (catches the N_TNAME nominal match),
fall through to resolvealias + structural compare for anonymous-
union variants only.

Reviewer's negative probe (different types modA.err vs modB.err
with same leaf name) still correctly rejects — the parser joins
pkg.alias into one TNAME string, so modA.err ≠ modB.err at the
nominal level.

Bare-vs-qualified residual (cstage admits `(void | M.err)` ← bare
`err` inside module M; wwstage still rejects) tracked as #57. Not
hit by any current fixture; unblocks #50 (after #56) and #42.

131/131 + 4 lines of pre-existing pessimism cleared in
selfhost/cmd/wcc/check.ww's own resolution.
2026-05-20 04:34:57 +09:00
01775f2ccc selfhost/cmd/wcc/check: per-block scope push/pop in resolvewalk
resolvewalk had no per-block scoping: inner-block `let i: u64`
persisted past the block end and shadowed the outer `let i: i32`,
which then false-positived as u64→i32 not-assignable on the next
reference. The TODO at the N_LET tail explicitly deferred per-block
scoping; this discharges it.

N_BLOCK case mirrors cstage cmd/wcc/check.c:1559-1566: save c.cur,
newscope under saved, walk body via n.list, restore. Sole exit is
the return after restore — push/pop balanced by structure.

All 5 selfhost main.combined.ww files (wwdump, w6c, w6a, w6l, ww)
now resolve clean via wwdump_ww -r. Reviewer's independent probe
across every .combined.ww outside ref/ confirmed no cascade: only
selfhost/cmd/ww went 1→0 (the targeted bug); the other 14
files-with-errors are pre-existing assignability/match-typing
issues unrelated to scope resolution.

Discharges TODO at N_LET tail. Same-scope dup detection
(`let a=1; let a=2;` in one block) stays queued behind #11.
Unblocks #50.
2026-05-20 04:13:49 +09:00
1b7fde21cd selfhost/cmd/wcc/check: enum<->int reinterpret bypasses tagged check
Wwstage checkisas fell straight through to the tagged-union arm on
`enum_val as i32` reinterprets, false-positiving on every enum→int
cast in lib/ (lib/time/instant.ww, lib/os, lib/os/lseek). Cstage
admits these at cmd/wcc/check.c:1346-1357: when N_TYPEASSERT has
LHS-or-RHS enum AND both ends are integer-typed, the target type
returns without the tagged check. `is` (TYPETEST) stays rejected —
cstage gates only N_TYPEASSERT.

isinttypeast helper covers N_TENUM + i8..i64/u8..u64/int/uint/
uintptr/rune. Excludes floats so `enum as f64` still rejects.
N_TYPEASSERT branch in checkisas detects enum on either side via
resolvealias-unwrap, gates on both-ends-int, returns target type
before the tagged-union check.

4/5 selfhost main.combined.ww files now resolve clean via
wwdump_ww -r. Residual on selfhost/cmd/ww tracked as #53
(separate checkletassign u64→i32 path).
2026-05-20 03:57:47 +09:00
c7c756d9c8 selfhost/cmd/wcc/check: resolve cross-module type refs in is/as
resolvealias only walked N_TNAME with unqualified names; cross-module
type aliases (parser emits them as one TNAME with str="pkg.alias"
via parse.ww:258-265 joindotted) returned the AST verbatim, and
checkisas at :1098-1108 then flagged "operand is not a tagged union"
on every `match (x: lib.maybe) { ... }` shape.

resolvealias now recognizes the joined-dotted form: split on the
rightmost '.', scopelookupinmodule(c.cur, head, leaf), recurse if
the body is itself an alias. Mirrors cstage resolve_typename at
cmd/wcc/check.c:74-83.

scruttype gains an N_DOT scrutinee arm — `match (pkg.var) { ... }`
or `pkg.var is T` now resolve through scopelookupinmodule. Module
head gating distinguishes top-level imported sym refs from struct
field access (both spell as N_DOT in the AST).

Standalone correctness fix; surfaces no current fixture failure
(those were enum-int reinterprets, tracked separately as #52). Sets
up #50 to wire checkfile into the wwstage cgen pipeline once #52
also lands.
2026-05-20 03:42:13 +09:00
cd44cc1893 lib/strings+test: port replace per Hare
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.
2026-05-20 02:46:22 +09:00
f8770d1502 selfhost/cmd/wcc/cgenutil+test: slotsize zero for void, recurse N_TBANG
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.
2026-05-20 02:27:17 +09:00
6d006da26c lib/strings+test: graduate fromutf8 + bytesub to validating return
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.
2026-05-20 02:02:28 +09:00
e03a6281d6 lib/strings+test: port dupall from Hare
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.
2026-05-20 01:30:12 +09:00
4d4ad36b70 cmd+selfhost+test: relax alloc-slice element-type pin via LHS retype
`alloc([], n)` synthesizes ([]u8 | nomem) at expression level — that's
fine, since the slice form only legitimately appears in let-init
position where the LHS carries the real element type. In clet, after
type-checking the rhs, peel any N_TRYPROP/N_TRYUNW wrapper, match the
alloc-slice AST shape with the same-module shadow gate (from #23),
and retype the call's tagged return to ([]T | nomem) where T is the
declared LHS element. Then assignability sees []T vs []T and accepts.

Cgen N_LET shortcut gains a viatryprop arm next to the existing
viatryunw — on rt_alloc returning null, emits the tagged-return
nomem propagation (MOVQ $nidx, AX; epilogue) instead of exit(1).
nidx comes from cg_tag_for_variant on the enclosing fn's return type,
matching the existing TRYPROP propret path.

Wwstage mirrors all four hunks (check.ww + cgenstmt.ww). Promotes the
previously-silent conf=false skip into a confident accept.

Unblocks #6 (dupall) and lays the path for #4/#7. Byte-identity
holds modulo the pre-existing #44 alloc/rt_alloc symbol divergence.
2026-05-20 01:09:16 +09:00
30a0856fe5 selfhost/cmd/wcc/cgenstmt+test: emit slice-form alloc let-init shortcut
Cstage's cmd/w6c/cgen.c:6363-6411 special-cases `let s: []T =
alloc([], n)!;` to inline rt_alloc + null-check + exit(1) + slice
header build, avoiding a generic call-then-store path. Wwstage's
cglet had no mirror — pre-#31 the path was rejected at check, but
once #31 made the check side accept it, the cgen side would have
silently miscompiled. Mirror added at cgenstmt.ww cglet rhs head,
emitting byte-identical asm.

Element size goes through elemsizeofc so str (16), structs, and
tagged aliases all match cstage's lu->sub->size uniformly — the
defensive path matters because check today only allows []u8, but
relaxing that is its own task.

Test exercises the path: writes to s[0] and s[15], reads back. Would
SIGSEGV on a junk header. 994 + 995 byte-identity green.
2026-05-20 00:28:27 +09:00
6f10c832a4 selfhost/cmd/wcc/check+test: reject bare alloc(v) at let-init in wwstage
Cstage's check.c:981-1006/1052-1082 builds a real (*T|nomem) /
([]T|nomem) return type for the alloc builtin; wwstage was returning
nil from exprtype's N_CALL arm (alloc is SK_FN with decl=nil under
seedprimitives), and checkletassign early-returned on nil src,
silently accepting `let p: *T = alloc(v);` without `!`. Stage
asymmetry that #30 papered over until now.

Three coordinated edits in check.ww:
- exprtype N_CALL: synthesize N_TTAGGED{N_TPTR{argt}, nomem} or
  {N_TSLICE{u8}, nomem} for bare alloc (same-module gated, mirrors
  cstage check.c:981-985 / task #23).
- exprtype N_TRYUNW: project the success variant so `let p:*T =
  alloc(v)!;` resolves rhs to *T.
- isassignable: tagged → non-tagged is unconditionally not
  assignable, forcing match/?/!.

950_selfcheck.c rows pin both ptr and slice forms.
2026-05-20 00:03:43 +09:00
dd27ce3339 lib/strings+test: re-port index str-arm to dual-iterator rune walk
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.
2026-05-19 23:11:01 +09:00
18fe1a7a31 lib/strings+test: 0-arg trim strips ASCII whitespace per Hare
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'.
2026-05-19 22:52:23 +09:00
f827429215 lib/strings+test: compare returns int, not i32
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.
2026-05-19 22:39:05 +09:00
031f5f9ec8 lib/strings+test: split sub byte-wise vs rune-wise per Hare
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).
2026-05-19 22:25:06 +09:00
65db360b91 selfhost/cmd/wcc/cgen: size amalloc slots to struct, not sizeof(str)@16
Nine sites used hardcoded byte counts sized for str=16. With str's
in-memory size invariant about to grow under #1, the next-pointer or
field write would land past the slot and corrupt the next bump
allocation — selfhost/CLAUDE.md flags this exact pattern. Over-alloc
by 8B is harmless under the bump allocator, so bumping the constants
is correct at str=16 too.

Sites: fnret, enumtype, modent (×4), ffi, strlit, enummember slot
sizes; loopendbuf, loopcontbuf, yieldbuf LOOP_MAX strides.

Latent bug found by str-size-hang-debug worker via PC trace on a
str=24 probe: fnretlookup spun forever because the frnext write
fell into the string heap, forming a cycle. Fix verified at str=16
(130/130 + 994/995) and probed at str=24 (994 still green; further
graduation work tracked by #1).
2026-05-19 22:09:47 +09:00
61705fb39e cmd+rt+selfhost+test: graduate alloc to (*T | nomem) / ([]T | nomem)
Per Hare convention, alloc is a typed builtin that returns a tagged
union carrying nomem as the OOM variant. Callers spell their policy:
`alloc(T)!` aborts on OOM (the old behavior), `alloc(T)?` propagates
when the enclosing fn already returns nomem.

cstage: check builds TY_TAGGED{*T | nomem} (or {[]T | nomem}); cgen
emits AX=tag, DX=ptr per the general tagged-return ABI (the (*T|!void)
nullable-ptr fold gated in ea76ee4 keeps this clean). wwstage cgalloc
mirrors. rt/alloc.s zeroes AX on syscall error so the builtin's null
check sees a clean 0 instead of mmap's -errno leaking through as a
poisoned pointer.

Migration: 3 `!` sites in test/wcc/700_e2e.c, 1 `!` site in
rt/ensure.ww (preserves the pre-existing sizeof bug tracked by #27),
1 `?` site in selfhost/test/tagged_ptr_ret.ww (allocbox exercises
real `?` propagation against a (*T | nomem) return).

130/130 tests green, 994_w6c_ww + 995_self_rebuild stage byte-identity
preserved. Follow-ups #31 (wwstage checkletassign leniency), #32
(wwstage slice-form gap), #33 (tagged_ptr_ret.ww make-test wiring).
2026-05-19 20:25:14 +09:00
d27411d833 cmd+selfhost+test: predeclare nomem in universe scope
Per Hare convention, `nomem` is a language-level error type — no
import required, in scope alongside void/done/rune/str. ref/hare uses
it bare at errors/string.ha:14, types/c/strings.ha:89, net/uri/parse.ha:17
with no `use`. Precondition for graduating the `alloc` builtin to
`(*T | nomem)` returns.

cstage: ty_nomem is NAMED{under=ty_void, iserror=1}, installed by
typesinit and surfaced via lookup_builtin. wwstage seeds the same
shape in both check.ww (scope) and cgen.ww (aliases) — separate
tables, both consulted; without the cgen seed wwstage drops the
zero-init for `let e: nomem;` locals and breaks byte-identity.

Tests: tagged_ptr_ret.ww and trypromote.ww drop their local
`type nomem = !void;` aliases. 990_selfhost.c adds a regression that
a value named `nomem` does not collide with the predeclared type.
2026-05-19 19:50:38 +09:00
ea76ee4aa3 cmd/wcc/check+test: don't fold (*T | !void) into nullable-ptr ABI
resolve_type for N_TTAGGED was peeling NAMED aliases to TY_VOID before
deciding the union is a nullable pointer, which caught (*T | nomem)
(nomem = !void) and routed it through cstage's ptr-in-AX shortcut.
wwstage's isnullabletype is purely AST-keyed on bare `void`, so any
alias or error-tagged void naturally fell through to the general
AX=tag, DX=word0 ABI. Rule 10 says align richer DOWN: gate the cstage
classifier on iserror==0 so only the literal (*T | void) shape still
folds to nullable-ptr. The literal void case stays intact for
700_e2e:642/661/1129.

Smoke test selfhost/test/tagged_ptr_ret.ww exercises (*u8 | nomem)
across both arms; cstage and wwstage now emit byte-identical asm
modulo the pre-existing #20 fmt.formatfield divergence.
2026-05-19 19:10:21 +09:00
3fe968c8a0 cmd+selfhost+test: gate alloc builtin behind same-module fn alloc
Mirrors the existing abort/assert gates in cstage check.c (strict
same-module lookup rather than scope_lookup_prefer, since lib/os.alloc
under a `use os;` import must not suppress the bare-alloc builtin in
client code). cgen.c shadows the resolution: only fire the rt_alloc
path when the typer left N_CALL.lhs->type == ty_err. wwstage gets a
new samemodfn helper for the matching gate.

Test fixtures: package-main repair for the 3 alloc rows in 700_e2e.c
that the parser was inheriting curmod="os" from the concat'd os.ww;
new shadow-test row asserts a same-module `fn alloc(n: i64) i64`
beats the builtin in cgen.
2026-05-19 18:51:07 +09:00
58e6d349a2 cmd/w6c/cgen+test: skip dead TRYPROP propret on same-shape ?
Per CLAUDE.md rule 10, align cstage down to wwstage — when every
variant in a `?` propagation maps to itself, the remap loop emits
zero JMPs and the propret label is dead. Lazy-allocate it so the
label-counter ID is only consumed when at least one JMP fires.

Smoke test selfhost/test/trypromote.ww exercises same-shape
(i64|nomem)→(i64|nomem) propagation; cstage and wwstage now emit
byte-identical asm for the TRYPROP region.
2026-05-19 17:45:13 +09:00
2b46abe7d1 lib/encoding/utf8+test: add strerror per Hare 2026-05-19 16:50:02 +09:00
8683202a4c lib/bytes+test: port ltrim/rtrim/trim from Hare (u8... variadic) 2026-05-19 16:19:12 +09:00
e5c1d6baa4 lib/strings+test: port lpad/rpad from Hare 2026-05-19 16:01:25 +09:00
3176d83d37 lib/strings+test: port split family from Hare 2026-05-19 15:40:33 +09:00
b0da6167b8 lib/strings+test: port tokenize family (Hare cross-module re-export) 2026-05-19 15:22:45 +09:00
a1d9f36d11 selfhost+cstage+test: graduate alias-chain unwrap to transitive (#22)
Single-peel TY_NAMED.under bottoms out at the inner alias when
chain length is 2+, surfaces in two stages with different
mechanisms: cstage's gates inline `if (t->kind == TY_NAMED)
t = t->under` at every callsite (cgreturn, cglet sizing, cgexpr
N_DOT, cgassign N_DOT, cg_sret_retsize) — graduated to a
while-loop via new type_chase_named helper across 11 sites.
wwstage routes all field-walks through structlookup, which
registers only direct struct definitions (not aliases) — missing
the alias-recurse fallback. New structlookupchain helper mirrors
slotsize's N_TARRAY arm precedent; sretretsize + 4 cgenexpr.ww
sites route through it. Splitting would either land cstage
without unblocking wwstage's strings.tokenize wrapper shape
(rule 10 byte-id regression) or land wwstage without cstage
gate parity (breaking 995 self-rebuild). 756 sentinel exercises
4 rows × cstage RC + wwstage RC + byte-id = 12 fixtures; pre-fix
rows 2 + 4 (slice-fields single alias, i32 double alias) fail
on both RC and byte-id. The ~67 cstage / ~26 wwstage candidate
sibling sites are #17-style structural-close follow-up; this
commit fixes the immediate strings.tokenize-wrapper blockers.
2026-05-19 15:09:57 +09:00
7e1b681701 lib/bytes+test: port split family from Hare 2026-05-19 13:50:54 +09:00
d5e8d699d1 selfhost: graduate wwstage &N_DOT[N_INDEX] to cstage canonical lean form (#21)
Latent #21 has two surface shapes — register polarity in cgun
TK_AMP N_INDEX's complex-base arm, and indexbaseesz's
over-broad .ptr pseudo-field gate — that share a single semantic
path: &N_DOT[N_INDEX] where the inner N_DOT cannot be peeled
into a plain ident base. Polarity-A (cgenexpr.ww) lifted to
cstage's three-line shape; stride-B (cgenutil.ww) narrowed so
the .ptr arm only fires on actual str/slice inners and falls
through to the generic struct-field arm for struct N_TNAME
bases. The fixes compose at the same call site (esz from
indexbaseesz, then the IMULQ-or-elide gate, then complex-base
emit), so splitting them into two commits would leave a
half-fixed intermediate — neither half stands alone as a
bisect-clean closure. Sentinel 755_amp_dot_idx exercises both
shapes across 4 stride classes (slice-elem 24, struct-elem 16,
u8 stride-1 elide, i64 stride-8); pre-fix 5/12 fail, post-fix
12/12 ok. Latent silent miscompile in lib/memio + lib/bufio's
.ptr[i] shape also unmasked.
2026-05-19 13:43:45 +09:00
f0b8c25b29 selfhost+cstage+test: graduate *[]T indexing to slice-element type (#20)
Cstage and wwstage share the latent: check.c's N_INDEX bespoke
TY_PTR-over-TY_SLICE clause peeled the slice in `*[]T[i]` and
returned the element of the element, while wwstage's elemsizeof
had no N_TSLICE arm for the post-N_TPTR-peel elem and fell to
the 8B catch-all. Splitting leaves one stage broken on the
exact `*[]T[i]` shape the new 754 sentinel asserts byte-identical
between stages (rule 11). The companion 24B per-element copy
emit is a separate codegen wedge already pinned inline at
cmd/w6c/cgen.c:6518; out-of-scope here and noted in the fixture
header.
2026-05-19 12:30:36 +09:00
02ada29624 lib/strings+test: port join from Hare str... variadic (#19) 2026-05-19 12:22:16 +09:00
3a85db0f3f lib/bytes+test: port tokenize family from Hare (#18) 2026-05-19 11:41:36 +09:00
006df414aa selfhost+test: route convenience-wrapper N_DOT probes via fnretlookupmod (#17)
Structural close of the #4-trio convenience-wrapper audit. Session-6's
#4-trio + #11/#16 graduated individual lookup helpers (fnret/fnparams/
enum/struct/def) to same-module-first via *mod variants. The close
didn't enumerate every cgcall-context callsite — convenience wrappers
that take a *node callee and probe its return shape via bare-leaf
fnretlookup stripped the N_DOT module hint, same wedge shape as #16
(callee_variadic_param, d9b0c90) through a different family of
consumers.

Eight LATENT sites in selfhost/cmd/wcc fixed (each mirrors #34's
nodeisslice two-arm route — N_IDENT uses cmod=c.curmod, N_DOT uses
cmod=callee.lhs.str, terminal call routes through fnretlookupmod):

- cgenstmt.ww  cgreturn forwardtagged probe
- cgenstmt.ww  cgmlet tuple-return shape probe
- cgenexpr.ww  cgdot fn-rvalue probe (mod.fn LEAQ)
- cgenexpr.ww  cgtryprop succisstr probe
- cgenexpr.ww  cgtryunw  succisstr probe
- cgenutil.ww  callsretsize (sret arg-prep)
- cgenutil.ww  inferletcalltype (let x = f()? tnode)
- cgenutil.ww  rhstaggedabicall N_CALL branch

cstage carries no sister bug: cmd/w6c/cgen.c reads every callee
return shape from the typed n->lhs->type per TY_FN sig. Mirror of
#4d/#28/#31/#34/#16 cstage no-sister notes.

753_convwrap_audit: table-driven sentinel exercising cgmlet's tuple-
shape probe. alpha exports foo() (i64, str); beta exports foo()
(i64, i64); main calls beta.foo() — source order puts alpha LAST so
alpha.foo prepends to head of c.fnrets, pre-fix bare walk picks
alpha's str-branch dispatch for beta's call. Post-fix routes to
beta.foo via fnretlookupmod. Asserts MOVQ\\tCX, absent in main.run
TEXT (no str.len store; would fire pre-fix). Remaining 7 sites
covered structurally by shape-mirror — single wedge shape, single
exercise.

make test 127/127; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.
2026-05-19 10:44:16 +09:00
23fccee4a0 lib/bytes+test: graduate contains to Hare (u8|[]u8)... variadic (#10)
contains(s, needle: (u8|[]u8)) -> contains(s: []u8, needles:
(u8|[]u8)...) bool per ref/hare/bytes/contains.ha:6.

Body: for-loop over needles.len; inner match (needles[i]) with u8/
[]u8 arms each forwarding to index(s, ...) with early return true
on the i32-match arm. 0-arg returns false per Hare spec. Mirrors
sister #9 strings.contains body shape modulo element type.

Sister of #9 (7c5463c). Element shape (u8|[]u8) — slice payload +
scalar u8 — structurally distinct from (str|rune). Was flagged as
potential new-latent surface; verified clean by 967_bytes_run +
cross-module 750_mklabel_modscoped[bytes_strings_contains] +
995_self_rebuild byte-id. No cgen wedge fired — #15 frame growth +
#12 sum-tag forward + #16 fnparamslookupmod close it on the slice-
payload variant too.

contains_cases adds 5 variadic rows (signalled 1700+i): 0-arg false,
1-arg slice hit, 1-arg u8 hit, 3-arg mixed middle-hit, 3-arg all-miss.

Module-header non-variadic divergence note removed.

make test 126/126; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.
2026-05-19 05:19:21 +09:00
7c5463c7d9 lib/strings+test: graduate contains to Hare (str|rune)... variadic (#9)
contains(haystack, needle: (str|rune)) -> contains(haystack: str,
needles: (str|rune)...) bool per ref/hare/strings/contains.ha:9.

Body: for-loop over needles.len; inner match (needles[i]) dispatches
byteindex(haystack, s|r); early return true on hit; fall-through
return false. Hare's match-yield + if (matched) return true collapses
naturally given byteindex's (i32|void) shape (Hare uses bytes::contains
-> bool). 0-arg returns false per Hare spec.

Unblocked by: #8 (5ed6293 varargseq per cgcall), #12 (cbf1042 sum-tag
forward), #15 (5609d04 frame-strategy), #16 (d9b0c90 callee_variadic_
param N_DOT). All four landed this session; #9 exercises every
unblocker. The cross-module same-leaf collision with bytes.contains
(non-variadic) is closed by #16's fnparamslookupmod re-routing —
existing 750 sentinel row bytes_strings_contains continues to pass.

contains_cases adds 6 variadic rows (signalled 1600+i): 0-arg,
1-arg str, 1-arg rune, 3-arg mixed (middle hits), 3-arg all-miss,
multibyte mixed (heart-é via 0xE9u32: rune per single-byte lexrune
precedent at hasprefix_cases:115).

Module-header divergence note for contains removed; lib/bytes
whitespace-default note retained for future bytes graduation.

make test 126/126; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.
2026-05-19 05:08:57 +09:00
d9b0c90fbc selfhost+test: route callee_variadic_param N_DOT via fnparamslookupmod (#16)
Latent silent miscompile surfaced by worker-strcontains3 attempting
strings.contains tagged-variadic graduation: wwstage cgcall's
callee_variadic_param helper (cgenutil.ww:60-70) consumed the N_DOT
callee's leaf via callee.str but routed bare fnparamslookup — bypassed
the module hint at callee.lhs.str. When two modules export same-leaf
fns with differing variadic shapes (e.g. strings.contains(str|rune)...
+ bytes.contains scalar (u8|[]u8)), the bare walk returned the wrong
fn's params for arg-prep while the CALL targeted the correct
module-qualified symbol — ABI mismatch.

Direct sister of #34 (049ebc1) which graduated fnret's N_DOT arm
through fnretlookupmod. #4d's commit body (862715d) explicitly
deferred callee_variadic_param's *mod re-routing pending "future
stdlib port introducing a tagged-vs-scalar or variadic-vs-non-variadic
same-leaf N_DOT collision shape." This is that surfacing.

cgenutil.ww: split callee_variadic_param on callee.kind. N_IDENT stays
on bare fnparamslookup (same-module-first post-#4d). N_DOT routes
through fnparamslookupmod(c, callee.str, callee.lhs.str), pattern-
identical to cgcall's N_DOT branch at cgenexpr.ww:2922-2935.

Cstage cmd/w6c/cgen.c:4279-4302 reads callee params via typed AST
(n->lhs->type + cu->params) — module-aware natively, no sister
change needed (mirrors #4d/#28/#31/#34 cstage no-sister notes).

752_modparam_callee: table-driven 3 rows x 2 stages = 6 fixtures.
cross_module_same_leaf_variadic_vs_scalar (the wedge),
same_module_same_leaf (no-regress), bare_leaf_no_collision (control).

#17 filed for the wider convenience-wrapper audit (enumerate all
wwstage cgen* helpers that take *node and do bare-leaf lookups; sweep
for N_DOT-arm omissions). This commit is narrow to callee_variadic_param.

make test 126/126; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.
2026-05-19 04:56:56 +09:00
77e62e5ed2 lib/strings+test: add index/rindex Hare sum-type dispatch (#11)
index(haystack: str, needle: (str|rune)) (i32|void) per
ref/hare/strings/index.ha:10. rindex symmetric per :22.

Returns RUNE-index (not byte-index) per Hare contract. str-arm reuses
byteindex/rbyteindex for the anchor byte offset, then walks iter
forward counting runes until position(&it) >= bo. rune-arm forward-
iterates with next(), counts rune positions.

rindex_rune divergence from ref/hare/strings/index.ha:45: Hare's
rindex_rune walks i = len(s) - 1 by 1 per step (byte-len-1 minus
decrement count) — neither pure-byte nor pure-rune for multibyte
input, contradicts its own docstring's rune-wise claim. ww honors
the docstring contract: forward iter + last-match-index. Cited
inline at strings.ww.

Unblocked by #13 (d2c64bc) — pre-#13 the test rows
"strings.index" + "bytes.index" would collide on
*.index_match_next_1 labels in combined.s.

index_cases + rindex_cases per-row inline-match (sister convention of
byteindex_str/rune_cases at stringstest.ww:147-231; sum-type-needle
single-struct table awkward). Bisect via signalled = 1400+i / 1500+i.
Rune-vs-byte pin rows: "こんにちは"+"ちは" → 3 (byte 9),
"またあったね"+"た" rindex → 4 (byte 12). Void miss + 4-byte rune
multibyte coverage.

make test 125/125; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.
2026-05-19 04:12:42 +09:00
5ed6293330 selfhost+test: bump wwstage varargseq per cgcall (#8)
Latent surface from #15: cgcall variadic-gather block read seq from
n.uval, which post-#15 is always 0 because scanlocals (which used to
stamp it during pre-pass) was deleted. Every variadic callsite in a
fn aliased to @vararg_d_0 / @vararg_sl_0. When two callsites in one
fn had differing arities, the second hit #15's first-use+fail-loud
guard ("localadd: @-prefix slot grew within fn") — correctly, since
the slot was being asked to grow mid-fn.

Fix: read seq from c.varargseq + bump in cgcall's gather branch.
Mirrors cstage's mklabel("vararg_d/sl") natural seq bump.
cgeninit zeroes c.varargseq per-fn (existing), so the counter is
correctly per-fn scoped.

cgen.ww varargseq comment refreshed — replaces stale "bumped only at
emit time" misclaim with the post-#15 per-call shape + the #15
grow-on-pin discipline that surfaced the wedge.

751_vararg_seq_percall: table-driven 3 rows x 2 stages = 6 fixtures.
mixed_arity_two_calls (the wedge), same_arity_two_calls (no-regress),
three_arity_drift (1/2/3 mints @vararg_d_0/1/2).

make test 125/125; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.

Surfaced by worker-strcontains2 attempting strings.contains tagged-
variadic graduation — mixed-arity spec test rows triggered the wedge.
Unblocks #9 + #10 (strings/bytes.contains).
2026-05-19 04:04:41 +09:00
d2c64bc962 selfhost+cstage+test: module-scope mklabel labels (#13)
Latent silent miscompile: cstage + wwstage mklabel emitted
<fn>_<prefix>_<seq> with no module qualification, so two top-level
fns sharing a leaf across modules (e.g. bytes.index + strings.index)
emitted colliding labels into the same combined .s. Last assembler
symbol-definition won; JNE/JMP rel32 resolved to the wrong fn's body.

Repro (HEAD pre-fix): two_modules_same_leaf row in 750 — mod1.locate
+ mod2.locate sharing match-over-(u8|[]u8)+for shape. mod1.locate's
JMP misresolved into mod2's body, exit 10. Post-fix: exit 0.

Latent already at HEAD: bytes.contains_match_next_1 +
strings.contains_match_next_1 collide today but the corpus had no
forwarding path that surfaced it.

cmd/w6c/cgen.c + selfhost/cmd/wcc/cgen.ww mklabel: prepend
<module>. when c->cur_mod / c.curmod non-NULL/non-empty. Plan-9
convention extension: TEXT directive already uses <module>.<fnname>
(lex.c:18 a_isidcont accepts '.'); mklabel now mirrors that for
local labels. Both stages symmetric per rule 10. Fragment input
(no `package`) collapses to pre-fix shape — no cross-unit risk.

750_mklabel_modscoped: table-driven 3 rows x 2 stages = 6 sub-cases
(two_modules_same_leaf, bytes_strings_contains, same_module_same_leaf
non-regression). All required substrings asserted via grep + runtime
rc check.

make test 124/124; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.
@-prefix slot keys (cg_tagbase, cg_tagscr, @retscr) are orthogonal
(local_alloc keys, not mklabel emissions).
2026-05-19 03:39:42 +09:00
cbf10427df selfhost+test: graduate wwstage sum-typed N_INDEX call-arg to tagged ABI (#12)
pushargsrev's widening detection was N_IDENT-only — N_INDEX of a
sum-typed slice element fell through to the scalar widening branch,
which hardcoded the param's first-variant tag (MOVQ $1, AX) and
pushed AX as a single scalar word. Callees that match-dispatched
on the runtime tag always ran the static-guess arm on garbage.

cstage knew the arg's type via check.c so its widen[] flag stayed
off and the natural-push tagged-arg arm pushed CX/DX/AX (high → low)
high → low. wwstage now mirrors via two narrow arms in pushargsrev:
the aistagged guard treats N_INDEX-of-sum-typed-element matching
the param slot as already-tagged, and the natural-push fallthrough
emits PUSHQ CX / DX / AX for the same shape. Both arms gate on
istaggedtype(indexvaluetnode(arg)) so literal- and ident-source
sum args stay on their existing paths.

Sentinel 749_sumtype_forward table-drives the three forward shapes
(N_INDEX, N_IDENT, literal) and asserts per-stage runtime plus a
byte-id window over the callsite asm.

Combined.ww regen for wwdump_ww and w6c_ww follows the cgen source
change; smoke.combined.ww unaffected.

Tests: 123/123 pass; bootstrap fixed point holds (ww2==ww3==ww4).
2026-05-19 03:09:34 +09:00
c305b04cb5 lib/strings+test: graduate trim/ltrim/rtrim to Hare rune... variadic
ltrim(input, exclude: rune) -> ltrim(input, trim: rune...) per
ref/hare/strings/trim.ha:11. rtrim mirrors via riter/prev per :32.
trim thin wrapper ltrim(rtrim(input, trim...), trim...) per :54.
Unblocked by #15 (5609d04) — multi-rune iter+match-prev composition
in non-leaf callees now produces byte-identical asm + correct runtime
on both stages.

Subset: 0-arg call (len(trim)==0) returns input unchanged. lib/bytes
whitespace-default future commit will graduate strings.trim's
whitespace... fast path.

ltrim_cases / rtrim_cases / trim_cases table-driven via parallel runes
+ inputs + argo/argn + want arrays + loop. Hare ref/hare/strings/trim.ha:75
vectors covered: bracket-pair, "mississippi" multi-rune, "yellowwooddoor"
multi-rune, Sentimentalized long form, plus 0-arg cases and 4-byte rune
"abacadabra"/"ahi" coverage. Bisect via signalled = 1100/1200/1300 + i.

make test 122/122; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.
2026-05-19 02:32:17 +09:00
5609d0456f selfhost+cstage+test: graduate frame growth to first-use+fail-loud (#15)
Subsumes #36. Drop wwstage scanlocals pre-pass; both stages converge on
first-use+fail-loud frame growth, rule-10 polarity DOWN to leaner side.
#36's surfaces (frame-total divergence on match-arm case-let; sibling
offset divergence in variadic+iter+match-prev compositions) close
naturally — running-max c.frame includes every first-use binding.

selfhost/cmd/wcc: add atlocals persistent @-prefix registry surviving
cgblock save/restore; add cgoutbuf/cgoutmode/cgout_enable/disable/flush
for deferred prologue (emit body to buffer, finalise c.frame, then
TEXT/SUBQ + flush); localadd @-prefix dedups against atlocals +
fail-louds on size-grow (rule 7 — no silent truncate); cgreturn-tagged
routes through @retscr (was colliding with @tagscr on arg-widen sizes);
variadic gather esz uses raw primsize (rune->4) not slotsize (rune->8)
— matches cstage and fixes the #36 sibling runtime miscompile in
non-leaf variadic+iter+match-prev callees.

cmd/w6c/cgen.c: drop the over-allocation hack ("for byte-id with
wwstage scanlocals reservation") since wwstage no longer over-reserves;
add fail-loud on @sretscr size-grow; @tagscr sites pass actual slot_sz
instead of stale c.tagscrsz.

748_size_strategy_convergence: table-driven 4 rows x 2 stages
(tag_variadic_runearm, trim_iter_match_prev, variadic_gather_rune_stride,
leaf_baseline). Each exercises a #36 surface shape; 8/8 ok.

Net -1565 lines. Sister latents filed as cosmetic (cs/ws frame size
drift on multiple-variadic-call fns): labelseq drift + varargseq
stuck at 0 — both bootstrap-byte-id safe (ww2==ww3==ww4 holds since
both ww2 and ww3 are wwstage outputs).

make test 122/122; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.
2026-05-19 02:13:58 +09:00
7a278c1a2d selfhost+cstage+test: graduate deflookup mod-qualified same-module-first (#11)
cstage Sdef walk #2 N_DOT branch used c->cur_mod where n->lhs->str is
the correct module hint. Sister of #4c wwstage graduation; same shape
as the TY_FN branch which already uses mafn(c, n->str, n->lhs->str).

cmd/w6c/cgen.c: add sdef_mod_match_hint(s, hint); walk #2 routes hint
first then head-pick fallback, matching #4a/#28/#31/#34 *mod variant
pattern. selfhost: add deflookuprhsmod(c, name, mod); cgdot N_DOT
mod-qualified str-def value-load routes through it. Rule-10 symmetric
stages: both stages now share the lhs.str polarity (was: both used
cur_mod / cur-module hint).

747_def_modqual_modshadow: table-driven sentinel — gamma calls
alpha.MSG with beta.MSG (same-leaf-name) at head of c.defs/sdefs.
want_imm "$38," (alpha strlit len), bad_imm "$27," (beta strlit len),
plus cs-vs-ws byte-id. Reverting cstage walk #2 to head-pick → fails
$38 on cstage + diverges cs-vs-ws; reverting wwstage cgdot to plain
deflookuprhs → fails $38 on wwstage.

make test 121/121; ww2==ww3==ww4 byte-id holds.
2026-05-19 00:58:28 +09:00
c9cf25be28 selfhost: regen combined.ww for 5466ea0 strings.concat variadic
Frozen-artifact catch-up. 5466ea0 modified lib/strings/strings.ww and
lib/strings/stringstest.ww but did not regenerate the three bundled
.combined.ww files. Bisect of the frozen-fallback build path was
broken at 5466ea0..HEAD.
2026-05-19 00:56:30 +09:00
5466ea048d lib/strings+test: graduate concat to Hare str... variadic
concat(a, b: str) -> concat(strs: str...) per ref/hare/strings/concat.ha:5.
Drop nomem return per project no-alloc-error idiom (os.alloc aborts).
Unblocked by #16 variadic-pack store fix (3bd9b1d).

concat_cases rewritten to table-driven: flat pool + argo/argn parallel
arrays + slice-spread call. 9 rows cover Hare concat.ha:18 vectors
(0/1/2/3-arg, multibyte) plus empty-mid/first/last/2-empty edges.
Bisect via signalled = 200 + i.

trim/contains variadic held on task #36 — surfaced by worker-variadic
pre-flight: iter + match prev composition in non-leaf callees still
hits scanlocals offset divergence. Resolves via #15 size-strategy.

make test 121/121; ww2==ww3==ww4 byte-id holds.
2026-05-19 00:40:33 +09:00
d985622cb1 selfhost+test: strlit-inline str-def value-load shape (#12)
Class A silent miscompile. wwstage cgenexpr.ww cgident's bare-ident
deflookup→true branch and cgdot's module-qualified leaf branch
emitted `MOVQ <mod>.<name>(SB), AX` for a `def MSG: str = "..."`
value reference — a load from a SB symbol that emit_data never
writes. Str defs are not laid out at SB; they live as interned
strlits the .ptr/.len fold (post-#4c) and value-load consume.
Cstage already strlit-inlines via Sdef walks #1 (case N_IDENT
non-local) and #2 (case N_DOT untyped-lhs); wwstage now matches
the (LEAQ _S_<n>(SB), MOVQ $<len>, BX) emit shape per rule 10.

Surfaced by reviewer-def during #4c R3 while attempting option (B)
for the cstage Sdef walks #1/#2 prefer-pass — both walks'
cs-vs-ws byte-id sentinel rows could not pass while wwstage
emitted the bogus DATAW shape. Filed as #12 and deferred until
the wwstage emit shape was fixed. Unblocks #11 + #13 (cstage
prefer-pass graduations).

Latent: no in-tree corpus referenced a str def as a value (only
as .ptr/.len via cgdot field-fold) prior to lib/strings c3 —
same corpus-coverage-blind shape as the #4a-#4e graduations.

746_strdef_inline pins both sites with 2 rows: bare ident +
mod-qualified. Each row asserts `LEAQ _S_` + `MOVQ $<strlit_len>,`
inside the caller TEXT before RET, anti-checks the pre-fix
`<mod>.<name>(SB)` symbol-load, and cs-vs-ws byte-id per row.

120/120 ok. ww2 == ww3 == ww4 byte-id holds.
2026-05-18 23:56:58 +09:00