selfhost — ww reimplementation of the toolchain (wcc, w6c, w6a, w6l, ww, wwdump). Compiled by the C bootstrap (`../cmd/`); the goal is to eventually compile itself. Identifiers: Plan 9 style. lowercase, words run together (`newbuf`, `tcpsock`, `parsefile`). No snake_case. Syntax and idioms: Hare-shaped. Trailing `;`, `=` after fn/type signatures, `export` for visibility, `match`/`?`/`!` for tagged-union errors. Consult `ref/hare/` for canonical signatures and error-handling patterns before inventing your own. The C bootstrap's cgen has known silent-miscompilation traps. They produce wrong runtime behavior, not compile errors. When porting C → ww here, default to the workarounds: 1. **`amalloc(n)` with n < struct size silently corrupts neighbours.** No error — the bump arena hands out n bytes and field writes overflow into the next record. When introducing or growing a struct, audit every `amalloc(_, n)` call site and over-size (we routinely pass 48 for a 40-byte struct). Symptom: linked-list prepends lose all but the most recent entry. Fixed (no workaround needed): - `def NAME: str = "..."` field access. `.len`/`.ptr` on an Sdef ident now inline the literal length / strlit address rather than reading BP+8. See cmd/w6c/cgen.c N_DOT. - Two-level field write through pointer field: `r.sym.isdyn = 1` where `r.sym: *T` now stores. The chained-N_DOT N_ASSIGN branch evaluates the inner pointer and stores at *(ptr + field.offset). See cmd/w6c/cgen.c N_ASSIGN and selfhost/cmd/wcc/cgenexpr.ww cgassign. - Tuple return `(scalar, str)` (24B). Returns now follow an AX:DX:CX convention: AX = scalar elem, DX = str.ptr, CX = str.len. Receive sites all destructure off the same regs regardless of positional order: let n, s = call(); // ww comma form let (n, s) = call(); // Hare-style paren form let t: (i64, str) = call(); // positional t.0 / t.1.len Wwstage parser + cgen are byte-identical to C cgen on these shapes. See cmd/w6c/cgen.c N_RETURN/N_LET/N_DOT/N_MLET, lib/ww/parse/{stmt, expr}.ww and selfhost/cmd/wcc/{cgenstmt,cgenexpr,cgenutil,cgendecl}.ww. - f64 compound assigns (`acc += d`, also `-= *= /=`) on locals and top-level lets. Both stages now load slot into X1, OP X0 into X1 (ADDSD/SUBSD/MULSD/DIVSD register-register), and store X1 back. See cmd/w6c/cgen.c N_ASSIGN float-IDENT branch and selfhost/cmd/wcc/cgenexpr.ww cgassign float local/global. - Top-level `[N]T` arrays. The cstage cgen now emits a zero-init DATAW slot and accesses go through `LEAQ name(SB)`; previously the array was filtered out by `let_emit_size` and `arr[i]` fell through to `LEAQ (BP), BX` (off-by-frame). `let_isarray` mirrors the selfhost N_TARRAY path in `letemitsize`. - `&arr[i]` (address-of an index). Both stages now compute base + i*esz without a trailing dereference; the previous TK_AMP path pre-evaluated the operand as if it were a value-load. Unblocks Hare's `let s = string { data = &buf, ... }` static-buffer shape. See cmd/w6c/cgen.c N_UN TK_AMP and selfhost/cmd/wcc/cgenexpr.ww cgun TK_AMP. - Tagged-union return ABI is now AX=tag, DX=word0, CX=word1, R8=word2 (was AX/DX/CX, 3 words). Slice-payload variants (`(slice | E)`, slot 32B) round-trip end-to-end. Every receive site (let-init, match scrutinee spill, cgwidentaggedstore for call-source, cgindex tagged-element load, pushargsrev tagged- ident arg) reads the fourth word when slot size > 24. See cmd/w6c/cgen.c N_RETURN / cg_widen_tagged_store and the matching selfhost cgenstmt / cgenutil / cgenexpr branches. - `expr: TaggedAlias` is a widening, not a re-interpret. cgwidentaggedstore peels an `N_CAST` whose destination IS the union itself, so cgexpr's natural register shape (str: AX=ptr, BX=len) is consumed by the str-payload branch instead of being misread as a tagged AX/DX/CX triple. Inner casts to a concrete variant (`7: i32` in `(i64 | i32)`) keep their type so the scalar branch picks the right variant tag. - `[N]Alias` arrays read element size through `slotsize` so an aliased tagged variant (e.g. `[3]fmt.formattable`) takes its full 24B stride per element, not the 8B fallback. selfhost/cmd/wcc/cgenutil.ww slotsize N_TARRAY follows `aliaslookup` on a TNAME element, and `aliaslookup` strips a `pkg.` prefix so cross-module references resolve. - Bare `let x: T;` (no rhs) of a multi-word composite — str (16B), slice (24B), tuple, struct, tagged — now zero-inits the slot. Previously only 8B-primitive slots were zeroed; larger slots read whatever the stack held, so `let empty: str; *into = empty;` copied stack garbage into the caller's slot. `[N]T` arrays still follow the per-index-write contract and stay uninit. See cmd/w6c/cgen.c N_LET (else branch, sz > 8 && !TY_ARRAY) and selfhost/cmd/wcc/cgenstmt.ww cglet no-rhs branch. - Signed-narrow scalar reads sign-extend honestly. `*p: *i32 = v` stores 4 bytes (MOVL — correct for a 4B pointee), and the read side now emits MOVSXD/MOVSWQ/MOVSBQ for any signed-narrow local or let load instead of a raw MOVQ. The natural N_ASSIGN / N_LET store paths happened to leave the slot sign-extended so MOVQ reads accidentally worked; deref-stores were the only path that touched fewer bytes than MOVQ reads, so negative i32 values round-tripped as 4G-rooted positive i64s. Cgen now routes scalar reads through `localloadop(t)`; aliases (`type err = !i32`) resolve the size through TBANG / TNAME / TENUM. See cmd/w6c/cgen.c N_IDENT/N_DOT mod.name catch-alls, cgassign compound RMW for locals + top-level lets, and the matching selfhost cgenexpr.ww cgident / cgassign / module-qualified branches plus cgenutil.ww `localloadop`. Top-level lets and `mod.name` go through a LEAQ + indirect load since w6a has no MOVSXD/MOVSWQ/ MOVSBQ D_EXTERN form. dotchainresolve's out-params are back at their natural i32 width. If a port "should work" but the binary is wrong, suspect these first.