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:
7
Makefile
7
Makefile
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
6779
selfhost/cmd/6c/main.combined.ww
Normal file
6779
selfhost/cmd/6c/main.combined.ww
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
@@ -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
146
test/wwc/995_self_rebuild.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user