Closes the gaps that kept ww_ww from rebuilding the wwstage byte-
for-byte. The pre-existing parser silently produced a broken AST
on `a, b = fn();` (a no-op exprstmt + a single assign that lost
the second tuple slot); the cgen leaned on a handful of cases
that the surface type-walker didn't yet cover, so 6a/6l/ww built
through wwstage drifted by a handful of bytes per file.
Parser:
- Multi-assign in parsestmt mirrors cmd/wwc/parse.c:1015-1031. If
parseexpr is followed by `,`, switch into N_MASSIGN: collect
the chained lvalues with parsebin(parseunary, 1) so they don't
eat the trailing `=`, then absorb `= rhs;` and emit the node.
Cgen:
- N_MASSIGN handler stores AX into l0's slot, pops DX into l1's
slot. Same shape as cmd/6c/cgen.c:2424-2440. Lvalues beyond
two are dropped (C drops them too).
- N_INDEX added to node_isunsigned: `p[i]` where p is *u8/[]u8/
[N]u8 now flags unsigned, so `p[i] >= 48u8` emits JAE instead
of JGE. Bit 6a's parsenum.
- node_isstr's N_DOT branch now handles chained dots via
dot_inner_struct_ptr, so `p.to.asym` (Adr.asym is str) flags
as str and push_args_rev pushes both halves.
- index_base_esz returns 1 for str-typed struct fields. Was
defaulting to 8, so `node.s[i]` scaled by 8 and used MOVQ
instead of MOVZBQ. Bit ww/visit_seen.
- N_LET with no initializer zero-inits the slot when the
underlying type is an 8-byte primitive (pointer, fn-ptr,
i64/u64, ...) — matches cmd/6c/cgen.c:2181. Structs,
strings, slices, etc. are left for per-field writes, even
when their slot rounds up to 8 in scan_locals. New helper
type_is_8byte_primitive walks the type AST to make the
same call C's checker would.
Tests stay 17/17; new self-rebuild test (995) lands in the next
commit and gates on these fixes.
Closes the byte-identity gap between selfhost/cmd/wwc/cgen.ww and
the C cgen, so a ww_ww-built binary matches the cstage-built binary
on the same input. The bootstrap fixed point was already green;
these are the bytes inside that fixed point that diverged from
what cmd/6c emits.
Slot allocation:
- local_add dedups by name (mirror cmd/6c/cgen.c:localoff). Two
`let cp: pos;` in disjoint if-branches share one slot. The
stored tnode is refreshed on each hit so a later `let m: *node`
shadowing an earlier `let m: i32` sees its own type when
emitting `m.next` — without this the N_DOT cgen fell into the
SB-symbol fallback and the linker complained about undefined
`next`.
- scan_locals dedups at frame-size time to keep the prologue SUBQ
in sync. cgfn seeds c.locals with param-name stubs before the
scan so a body's `let <param-name>` reuses the param slot, then
resets c.locals before emission so real offsets get installed.
- local_alloc (no dedup) for N_MCASE bindings: C cgen handles a
match as an expression with by-value `locals`, so two separate
matches each get fresh slots for `v`/`e`.
Per-instruction matching:
- `return;` in a void fn zeros AX (C cgen falls through to
cgexpr_int(c, 0)).
- N_INTLIT prints i64 (signed), not u64. FNV-1a's offset basis
now prints as `$-3750763034362895579`, matching `$%lld`.
- *p = strexpr push order swapped to PUSH AX / PUSH BX → POP CX
/ POP AX (cgen.c:1033-1041).
Feature port from 635818e (the half that the wwdump corpus
actually exercises):
- N_IDENT used as a value with fn type now LEAQs through
ffi_resolve, so `let fp = some_ffi_fn;` emits the C symbol.
- N_CALL on a bare ident checks local_find_node first; a local
fn-pointer dispatches as `cgexpr(callee); CALL AX` instead of
`CALL ident(SB)`.
Tests 990 probe 6 and 994 are byte-identical on mem/err/tok/smoke
+ the four selfhost main.combined.ww files. Bootstrap still
ww2 == ww3.