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

@@ -188,7 +188,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_6c $(BIN)/test_6a $(BIN)/test_6l $(BIN)/test_arch \
$(BIN)/test_e2e $(BIN)/test_ffi $(BIN)/test_dyn $(BIN)/test_stdlib \
$(BIN)/test_selfhost $(BIN)/test_6a_ww $(BIN)/test_6l_ww \
$(BIN)/test_6c_ww $(BIN)/test_ww_ww
$(BIN)/test_6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild
$(BIN)/test_smoke: test/wwc/000_smoke.c $(LIB)/libwwc.a | $(BIN)
$(CC) $(CFLAGS) $(INCS) -o $@ $< -L$(LIB) -lwwc
@@ -247,6 +247,11 @@ $(BIN)/test_ww_ww: test/wwc/993_ww_ww.c $(BIN)/ww $(BIN)/ww_ww \
$(BIN)/6c $(BIN)/6a $(BIN)/6l $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_self_rebuild: test/wwc/995_self_rebuild.c $(BIN)/ww_ww \
$(BIN)/6c_ww $(BIN)/6a_ww $(BIN)/6l_ww $(BIN)/wwdump_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
test: all $(TESTS)
@WW=$(BIN)/ww BIN=$(BIN) sh test/run

View File

@@ -24,6 +24,7 @@ def SYS_OPEN: i64 = 2;
def SYS_CLOSE: i64 = 3;
def SYS_LSEEK: i64 = 8;
def SYS_ACCESS: i64 = 21;
def SYS_DUP2: i64 = 33;
def SYS_GETPID: i64 = 39;
def SYS_FORK: i64 = 57;
def SYS_EXECVE: i64 = 59;
@@ -62,6 +63,14 @@ export fn close(fd: i32) i32 = {
return syscall1(SYS_CLOSE, fd: i64): i32;
};
// dup2(2): make `newfd` refer to the same description as `oldfd`,
// closing `newfd` first if open. Returns `newfd` on success or a
// negative errno. Used by 6c_ww to redirect stdout into an output
// file without changing the cgen emit path.
export fn dup2(oldfd: i32, newfd: i32) i32 = {
return syscall2(SYS_DUP2, oldfd: i64, newfd: i64): i32;
};
// Fallible wrappers. The error variant is a plain str (Plan 9 errstr
// model, see lib/errors); the sum type makes success/failure explicit
// without overloading length-zero.

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,7 @@ def SYS_OPEN: i64 = 2;
def SYS_CLOSE: i64 = 3;
def SYS_LSEEK: i64 = 8;
def SYS_ACCESS: i64 = 21;
def SYS_DUP2: i64 = 33;
def SYS_GETPID: i64 = 39;
def SYS_FORK: i64 = 57;
def SYS_EXECVE: i64 = 59;
@@ -62,6 +63,14 @@ export fn close(fd: i32) i32 = {
return syscall1(SYS_CLOSE, fd: i64): i32;
};
// dup2(2): make `newfd` refer to the same description as `oldfd`,
// closing `newfd` first if open. Returns `newfd` on success or a
// negative errno. Used by 6c_ww to redirect stdout into an output
// file without changing the cgen emit path.
export fn dup2(oldfd: i32, newfd: i32) i32 = {
return syscall2(SYS_DUP2, oldfd: i64, newfd: i64): i32;
};
// Fallible wrappers. The error variant is a plain str (Plan 9 errstr
// model, see lib/errors); the sum type makes success/failure explicit
// without overloading length-zero.

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];

146
test/wwc/995_self_rebuild.c Normal file
View File

@@ -0,0 +1,146 @@
/*
* 995_self_rebuild — the wwstage rebuilds itself.
*
* Drives ww_ww (the ww-side driver, which shells to 6c_ww/6a_ww/6l_ww)
* over each wwstage tool's source and diffs the result byte-for-byte
* against the cstage-built binary in $BIN. A green run means the
* toolchain can recompile itself without touching cc, modulo the
* cold-start binary — which is the v1.0 lock from PLAN.md.
*
* This is stricter than `make bootstrap`: that loop only proves the
* wwdump cgen self-stabilises; this proves every wwstage tool round-
* trips through the wwstage pipeline.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return 1;
}
static const char *
absbin(void)
{
const char *b = getenv("BIN");
if (!b) b = "out/bin";
if (b[0] == '/') return b;
static char buf[2048];
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return NULL;
snprintf(buf, sizeof buf, "%s/%s", cwd, b);
return buf;
}
static int
slurp_eq(const char *a, const char *b)
{
FILE *fa = fopen(a, "rb");
FILE *fb = fopen(b, "rb");
if (!fa || !fb) {
if (fa) fclose(fa);
if (fb) fclose(fb);
return -1;
}
int rc = 0;
for (;;) {
int ca = fgetc(fa);
int cb = fgetc(fb);
if (ca != cb) { rc = -1; break; }
if (ca == EOF) break;
}
fclose(fa); fclose(fb);
return rc;
}
/* Each tool builds via `ww_ww build -I <local> -I selfhost/cmd/wwc src`.
* Some tools have a local module dir (6a, 6l with sibling .ww files);
* wwc-only tools (6c, ww, wwdump) just need the wwc -I. inc_local is
* "" for those.
*/
static int
rebuild_one(const char *bin, const char *cwd, const char *tool,
const char *src_rel, const char *inc_local)
{
char workdir[64];
snprintf(workdir, sizeof workdir, "/tmp/wwsr_%d_%s", getpid(), tool);
char cmd[4096];
snprintf(cmd, sizeof cmd, "rm -rf %s && mkdir -p %s", workdir, workdir);
if (runwait(cmd) != 0) return -1;
if (inc_local && inc_local[0]) {
snprintf(cmd, sizeof cmd,
"cd %s && %s/ww_ww build -I %s/%s -I %s/selfhost/cmd/wwc "
"%s/%s >/dev/null 2>&1",
workdir, bin, cwd, inc_local, cwd, cwd, src_rel);
} else {
snprintf(cmd, sizeof cmd,
"cd %s && %s/ww_ww build -I %s/selfhost/cmd/wwc "
"%s/%s >/dev/null 2>&1",
workdir, bin, cwd, cwd, src_rel);
}
if (runwait(cmd) != 0) {
fprintf(stderr, "self-rebuild FAIL: ww_ww build errored on %s\n", tool);
return -1;
}
char rebuilt[256], canonical[256];
snprintf(rebuilt, sizeof rebuilt, "%s/main", workdir);
snprintf(canonical, sizeof canonical, "%s/%s_ww", bin, tool);
int rc = slurp_eq(rebuilt, canonical);
if (rc != 0) {
fprintf(stderr, "self-rebuild FAIL: %s rebuilt != cstage %s\n",
tool, canonical);
}
/* Leave the driver's intermediates (.s/.o/.combined.ww) next to the
* source — 991/992/994 read those fixtures, and `make wwstage` had
* already produced byte-identical copies anyway. */
snprintf(cmd, sizeof cmd, "rm -rf %s", workdir);
runwait(cmd);
return rc;
}
int
main(void)
{
const char *bin = absbin();
if (!bin) return 1;
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
struct {
const char *tool;
const char *src;
const char *inc_local;
} tools[] = {
{ "6c", "selfhost/cmd/6c/main.ww", "" },
{ "6a", "selfhost/cmd/6a/main.ww", "selfhost/cmd/6a" },
{ "6l", "selfhost/cmd/6l/main.ww", "selfhost/cmd/6l" },
{ "ww", "selfhost/cmd/ww/main.ww", "" },
{ "wwdump", "selfhost/cmd/wwdump/main.ww", "" },
{ NULL, NULL, NULL },
};
int fail = 0, n = 0;
for (int i = 0; tools[i].tool; i++) {
if (rebuild_one(bin, cwd, tools[i].tool, tools[i].src,
tools[i].inc_local) != 0)
fail++;
n++;
}
if (fail) {
fprintf(stderr, "self-rebuild: %d/%d tool(s) diverged\n", fail, n);
return 1;
}
printf("self-rebuild: %d wwstage tool(s) round-trip byte-identical "
"through ww_ww + 6c_ww + 6a_ww + 6l_ww\n", n);
return 0;
}