Drives ww_ww (which already shells to 6c_ww/6a_ww/6l_ww) over each
wwstage tool's source and diffs the resulting binary against the
cstage-built canonical in $BIN. A green run means the toolchain
can recompile itself end-to-end without invoking cc, modulo the
cold-start binary that brings the wwstage into existence.
Stricter than `make bootstrap`: that loop pins wwdump's cgen
self-stabilising; this pins all five wwstage tools (6c, 6a, 6l,
ww, wwdump) round-tripping through the wwstage pipeline.
The .combined.ww refreshes are the expander picking up the
parser/cgen changes from the prior commit. selfhost/cmd/6c/
gains its main.combined.ww for the first time — 995 builds it,
994 reads it.
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.
build_one now invokes 6c_ww / 6a_ww / 6l_ww from $self_dir, not
the C-built binaries that share the directory. After this change
`ww_ww build foo.ww` touches no cstage code at runtime — the
fresh-checkout cstage is still needed to bring the wwstage into
existence, but day-to-day work runs on the ww toolchain end to
end. The C `ww` driver in cmd/ww/ still drives the C 6c/6a/6l.
Test 993 (which used to be trivial — both drivers invoked the
same C tools) now meaningfully compares the cstage pipeline
against the wwstage pipeline on hello + wwdump and confirms
byte-identical exes.
The .combined.ww files for 6a/6l/ww/wwdump and smoke are
regenerated by the ww driver's `expand()` step; their diff is
the lib/os dup2 wrapper and the cgen.ww port from the prior two
commits, propagating into the bootstrap inputs.
selfhost/cmd/6c/main.ww is a thin packaging of the wwc cgen — slurp
a .ww file, run lex+parse+cgen, write Plan 9 amd64 asm to the path
given by -o. The cgen routines in selfhost/cmd/wwc/cgen.ww write
directly to fd 1, so we use dup2 to redirect stdout into the
output file rather than thread an fd through every emit helper.
Adds the SYS_DUP2=33 wrapper in lib/os.
Makefile wires $(BIN)/6c_ww alongside the other wwstage tools and
adds $(BIN)/test_6c_ww to the TESTS list.
test/wwc/994_6c_ww.c diffs 6c_ww byte-for-byte against
`wwdump_ww -c` on five in-source programs plus the four selfhost
main.combined.ww files: same cgen reached through two binaries, so
any divergence is a packaging bug in selfhost/cmd/6c.
We deliberately don't diff against C-side 6c here — 990 probe 5
already covers that on the subset the ww cgen handles today.
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.