test: 995_self_rebuild — wwstage rebuilds itself byte-identical

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.
This commit is contained in:
2026-05-11 11:41:12 +09:00
parent af8836cc8e
commit 37bffa5284
6 changed files with 7108 additions and 18 deletions

View File

@@ -2633,9 +2633,31 @@ fn parsestmt(p: *parser) *node = {
expect_tok(p, TK_SEMI, "expected ';' after continue");
return newnode(p.a, N_CONTINUE, pf, pl, pc);
};
// expression statement
// expression statement, or tuple-destructure multi-assign:
// a, b = expr;
// Mirrors cmd/wwc/parse.c:1015-1031. We parse the first lvalue
// with parseexpr (matches the C side); subsequent lvalues go
// through parsebin(parseunary, 1) so the `=` stays for us to
// consume — parseexpr would absorb it.
let e: *node = parseexpr(p);
if (p.cur_kind == TK_COMMA) {
let m: *node = newnode(p.a, N_MASSIGN, pf, pl, pc);
let head: *node = e;
let tail: *node = e;
for (p.cur_kind == TK_COMMA) {
advance(p);
let lv: *node = parsebin(p, parseunary(p), 1);
tail.next = lv;
tail = lv;
};
expect_tok(p, TK_ASSIGN, "expected '=' after multi-assign lvalues");
m.rhs = parseexpr(p);
m.list = head;
expect_tok(p, TK_SEMI, "expected ';' after multi-assign");
return m;
};
let n: *node = newnode(p.a, N_EXPRSTMT, pf, pl, pc);
n.lhs = parseexpr(p);
n.lhs = e;
expect_tok(p, TK_SEMI, "expected ';' after expression statement");
return n;
};
@@ -4346,15 +4368,14 @@ fn node_isstr(c: *cgen, n: *node) bool = {
if (streq(fld, "len")) { return false; };
if (streq(fld, "cap")) { return false; };
if (base != nil) {
let sname: str;
sname.ptr = nil; sname.len = 0;
if (base.kind == N_IDENT) {
let bn: str = base.str;
let lc: *local = local_find_node(c, bn);
let lc: *local = local_find_node(c, base.str);
if (lc != nil) {
let tn: *node = lc.tnode;
let lkind: i32 = -1;
if (tn != nil) { lkind = tn.kind; };
let sname: str;
sname.ptr = nil; sname.len = 0;
if (lkind == N_TNAME) { sname = tn.str; };
if (lkind == N_TPTR) {
let inner: *node = tn.lhs;
@@ -4362,18 +4383,27 @@ fn node_isstr(c: *cgen, n: *node) bool = {
if (inner.kind == N_TNAME) { sname = inner.str; };
};
};
if (sname.len > 0) {
let si: *struct_info = struct_lookup(c, sname);
if (si != nil) {
let fi: *field_info = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fld)) {
return is_str_type(c, fi.tnode);
};
fi = fi.finext;
};
};
};
// Chained dot (`p.foo.bar`): use dot_inner_struct_ptr
// to resolve the inner chain to the *struct it lands
// on, then look up `fld` in that struct.
if (base.kind == N_DOT) {
let inner_t: *node = dot_inner_struct_ptr(c, base);
if (inner_t != nil) {
if (inner_t.kind == N_TNAME) { sname = inner_t.str; };
};
};
if (sname.len > 0) {
let si: *struct_info = struct_lookup(c, sname);
if (si != nil) {
let fi: *field_info = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fld)) {
return is_str_type(c, fi.tnode);
};
fi = fi.finext;
};
};
};
@@ -4404,6 +4434,36 @@ fn type_node_isunsigned(t: *node) bool = {
return false;
};
// type_is_8byte_primitive — does this type take exactly one 8-byte
// slot (pointer / fn-ptr / 64-bit int / chan / scalar primitive
// padded up to 8) rather than a wider aggregate? Used by N_LET
// zero-init to mirror C cgen's "only zero if sz == 8 at the type
// level" rule. Strings (16), slices (24), tagged unions (>=16),
// tuples (16), structs (varies), arrays — all fall through to
// false here even when their *slot* rounds up to 8.
fn type_is_8byte_primitive(c: *cgen, t: *node) bool = {
if (t == nil) { return false; };
let k: i32 = t.kind;
if (k == N_TPTR) { return true; };
if (k == N_TFN) { return true; };
if (k == N_TCHAN) { return true; };
if (k == N_TSLICE) { return false; };
if (k == N_TARRAY) { return false; };
if (k == N_TTUPLE) { return false; };
if (k == N_TTAGGED){ return false; };
if (k == N_TNAME) {
let nm: str = t.str;
if (streq(nm, "str")) { return false; };
// Struct alias: not a primitive even if the slot is 8B.
if (struct_lookup(c, nm) != nil) { return false; };
// Primitive (i8/u8/.../i64/u64/bool/rune/f32/f64/int/...).
// All of these get slot-padded to 8 and zero-init in C.
if (prim_size(nm) > 0) { return true; };
return false;
};
return false;
};
// type_name_issigned — true for i8/i16/i32/i64/int/rune.
fn type_name_issigned(nm: str) bool = {
if (streq(nm, "i8")) { return true; };
@@ -4506,6 +4566,12 @@ fn index_base_esz(c: *cgen, base: *node) i32 = {
return 8;
};
if (ft.kind == N_TSLICE) { return elem_size_of(ft); };
// str-typed field: indexing yields one byte
// (`n.s[i]` where .s is str — matches C cgen's
// MOVZBQ for byte indexing).
if (ft.kind == N_TNAME) {
if (streq(ft.str, "str")) { return 1; };
};
return 8;
};
fi = fi.finext;
@@ -4655,6 +4721,32 @@ fn node_isunsigned(c: *cgen, n: *node) bool = {
return node_isunsigned(c, n.rhs);
};
if (k == N_UN) { return node_isunsigned(c, n.lhs); };
// N_INDEX: `p[i]` is unsigned iff p's element type is unsigned.
// Walks the base local's declared type and pulls the element
// out — *u8 → u8, [N]u32 → u32, []u64 → u64. Without this the
// compare-codegen for `p[i] >= 48u8` falls back to signed JGE
// instead of JAE, diverging from C 6c on byte indexing.
if (k == N_INDEX) {
let base: *node = n.lhs;
if (base != nil) {
if (base.kind == N_IDENT) {
let lc: *local = local_find_node(c, base.str);
if (lc != nil) {
let tn: *node = lc.tnode;
if (tn != nil) {
let elem: *node = nil;
if (tn.kind == N_TPTR) { elem = tn.lhs; };
if (tn.kind == N_TARRAY) { elem = tn.lhs; };
if (tn.kind == N_TSLICE) { elem = tn.lhs; };
if (elem != nil) {
return type_node_isunsigned(elem);
};
};
};
};
};
return false;
};
return false;
};
@@ -6165,6 +6257,20 @@ fn cgstmt(c: *cgen, n: *node) void = {
emit_off((off + 16): i64);
emit_line("(BP)\n");
};
} else {
// Bare `let x: T;` with no initializer. C cgen
// (cmd/6c/cgen.c:2181-2183) zero-inits only when
// the underlying type's natural size is 8 — pointers,
// i64/u64, function pointers, ints. Structs/arrays/
// slices/strings/tagged/tuples are left for per-field
// writes. ww's slot_size pads struct slots up to 8,
// so we can't just check sz == 8: walk the type AST
// directly to make the same call.
if (type_is_8byte_primitive(c, n.lhs)) {
emit_line("\tMOVQ\t$0, ");
emit_off(off: i64);
emit_line("(BP)\n");
};
};
c.last_was_return = 0;
return;
@@ -6221,6 +6327,42 @@ fn cgstmt(c: *cgen, n: *node) void = {
return;
};
// Tuple-destructure assign: `a, b = call();`. The call's tuple
// return lands in (AX, DX); push DX to free it, store AX into
// the first lvalue, then pop DX into the second. Mirrors
// cmd/6c/cgen.c:2424-2440. Lvalues beyond two are dropped (same
// as C — no fixture uses >2 today).
if (k == N_MASSIGN) {
if (n.rhs != nil) { cgexpr(c, n.rhs); };
emit_line("\tPUSHQ\tDX\n");
let l0: *node = n.list;
let l1: *node = nil;
if (l0 != nil) { l1 = l0.next; };
if (l0 != nil) {
if (l0.kind == N_IDENT) {
let off: i32 = local_find(c, l0.str);
if (off != 0) {
emit_line("\tMOVQ\tAX, ");
emit_off(off: i64);
emit_line("(BP)\n");
};
};
};
emit_line("\tPOPQ\tDX\n");
if (l1 != nil) {
if (l1.kind == N_IDENT) {
let off: i32 = local_find(c, l1.str);
if (off != 0) {
emit_line("\tMOVQ\tDX, ");
emit_off(off: i64);
emit_line("(BP)\n");
};
};
};
c.last_was_return = 0;
return;
};
if (k == N_BREAK) {
if (c.loop_top > 0) {
let lbl: str = c.loop_end_buf[c.loop_top - 1];