874 lines
30 KiB
C
874 lines
30 KiB
C
/*
|
|
* 990_selfhost — phase-10 marker test. Three probes:
|
|
*
|
|
* 1. The selfhost stubs (mem.ww, err.ww) must parse, typecheck and
|
|
* codegen via C-side `w6c`.
|
|
*
|
|
* 2. selfhost/test/smoke.ww — an actual ww program exercising the
|
|
* patterns the real port will use (bump arena, error union,
|
|
* struct-of-fn-pointer dispatch, byte scanner, strconv) — must
|
|
* build through `ww build` and exit 42 when run. Any non-42 exit
|
|
* names which probe broke (1..N).
|
|
*
|
|
* 3. `wwdump` is deterministic: running it twice on the same input
|
|
* must produce byte-identical output. This is the diff anchor for
|
|
* self-host. When the ww-side wwdump lands (task 3), the same
|
|
* bytes will have to come out of the ww frontend.
|
|
*
|
|
* The ceiling — `cmp ww2 ww3` — is gated on the full frontend port.
|
|
*/
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <sys/stat.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(const char *path, char **outbuf, size_t *outlen)
|
|
{
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) return -1;
|
|
fseek(f, 0, SEEK_END);
|
|
long n = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
if (n < 0) { fclose(f); return -1; }
|
|
char *b = malloc((size_t)n + 1);
|
|
if (!b) { fclose(f); return -1; }
|
|
if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; }
|
|
b[n] = '\0';
|
|
fclose(f);
|
|
*outbuf = b;
|
|
*outlen = (size_t)n;
|
|
return 0;
|
|
}
|
|
|
|
/* Probe 1: each file must compile through `w6c`. */
|
|
static int
|
|
probe_codegen(const char *bin)
|
|
{
|
|
static const char *files[] = {
|
|
"selfhost/cmd/wcc/mem.ww",
|
|
"selfhost/cmd/wcc/err.ww",
|
|
NULL,
|
|
};
|
|
char cwd[1024];
|
|
if (getcwd(cwd, sizeof cwd) == NULL) return -1;
|
|
int fail = 0;
|
|
for (int i = 0; files[i]; i++) {
|
|
char cmd[2048];
|
|
snprintf(cmd, sizeof cmd,
|
|
"%s/w6c %s/%s > /dev/null 2>&1", bin, cwd, files[i]);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "codegen FAIL: %s\n", files[i]);
|
|
fail++;
|
|
}
|
|
}
|
|
return fail ? -1 : 0;
|
|
}
|
|
|
|
/* Probe 2: end-to-end build+run of selfhost/test/smoke.ww. Exit 42 = ok. */
|
|
static int
|
|
probe_smoke(const char *bin)
|
|
{
|
|
char cwd[1024];
|
|
if (getcwd(cwd, sizeof cwd) == NULL) return -1;
|
|
char tmpdir[64];
|
|
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwsh_%d", getpid());
|
|
mkdir(tmpdir, 0755);
|
|
char cmd[2048];
|
|
snprintf(cmd, sizeof cmd,
|
|
"cd %s && %s/ww build %s/selfhost/test/smoke.ww >/dev/null 2>&1",
|
|
tmpdir, bin, cwd);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "smoke FAIL: ww build did not succeed\n");
|
|
return -1;
|
|
}
|
|
char outbin[256];
|
|
snprintf(outbin, sizeof outbin, "%s/smoke", tmpdir);
|
|
int rc = runwait(outbin);
|
|
unlink(outbin);
|
|
char tmp[1024];
|
|
snprintf(tmp, sizeof tmp, "%s/smoke.s", tmpdir); unlink(tmp);
|
|
snprintf(tmp, sizeof tmp, "%s/smoke.o", tmpdir); unlink(tmp);
|
|
snprintf(tmp, sizeof tmp, "%s/smoke.combined.ww", tmpdir); unlink(tmp);
|
|
rmdir(tmpdir);
|
|
if (rc != 42) {
|
|
fprintf(stderr, "smoke FAIL: exit=%d (want 42; the number "
|
|
"names which probe in selfhost/test/smoke.ww broke)\n", rc);
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Probe 3a: ww-side wwdump_ww must produce byte-identical output to
|
|
* the C-side wwdump on every fixture. The token-stream diff (-t) is
|
|
* the strongest signal: the lex.ww port has converged when this is
|
|
* empty across realistic source. The AST diff (-a) is a partial
|
|
* signal — the ww-side parser is currently a stub that handles only
|
|
* `use` clauses, so we exercise it on a stripped fixture. As parse.ww
|
|
* grows, more files will be added to the -a list. */
|
|
static int
|
|
probe_dump_diff_mode(const char *bin, const char *flag, const char **inputs)
|
|
{
|
|
char cwd[1024];
|
|
if (getcwd(cwd, sizeof cwd) == NULL) return -1;
|
|
int fail = 0;
|
|
for (int i = 0; inputs[i]; i++) {
|
|
char a[256], b[256], cmd[2048];
|
|
snprintf(a, sizeof a, "/tmp/wwd_diff_%d_c", getpid());
|
|
snprintf(b, sizeof b, "/tmp/wwd_diff_%d_w", getpid());
|
|
snprintf(cmd, sizeof cmd, "%s/wwdump %s %s/%s > %s 2>/dev/null",
|
|
bin, flag, cwd, inputs[i], a);
|
|
runwait(cmd);
|
|
snprintf(cmd, sizeof cmd, "%s/wwdump_ww %s %s/%s > %s 2>/dev/null",
|
|
bin, flag, cwd, inputs[i], b);
|
|
runwait(cmd);
|
|
char *ba = NULL, *bb = NULL;
|
|
size_t na = 0, nb = 0;
|
|
int rc1 = slurp(a, &ba, &na);
|
|
int rc2 = slurp(b, &bb, &nb);
|
|
if (rc1 < 0 || rc2 < 0) {
|
|
fprintf(stderr, "diff FAIL (%s): cannot read dumps for %s\n",
|
|
flag, inputs[i]);
|
|
fail++;
|
|
} else if (na != nb || memcmp(ba, bb, na) != 0) {
|
|
fprintf(stderr, "diff FAIL (%s): %s — C %zu bytes vs ww %zu bytes\n",
|
|
flag, inputs[i], na, nb);
|
|
fail++;
|
|
}
|
|
free(ba); free(bb);
|
|
unlink(a); unlink(b);
|
|
}
|
|
return fail ? -1 : 0;
|
|
}
|
|
|
|
static int
|
|
probe_dump_diff(const char *bin)
|
|
{
|
|
const char *tok_inputs[] = {
|
|
"selfhost/cmd/wcc/mem.ww",
|
|
"selfhost/cmd/wcc/err.ww",
|
|
"lib/ww/lex/lex.ww",
|
|
"lib/ww/lex/tok.ww",
|
|
"lib/ww/ast.ww",
|
|
"lib/ww/parse/parse.ww", "lib/ww/parse/expr.ww", "lib/ww/parse/stmt.ww", "lib/ww/parse/decl.ww",
|
|
"selfhost/cmd/wwdump/main.ww",
|
|
"selfhost/test/smoke.ww",
|
|
NULL,
|
|
};
|
|
/* AST diff coverage. The ww-side parser handles use/def/type/let/fn
|
|
* (with bodies), expressions with full Pratt precedence, statements
|
|
* (block/let/return/if/for/defer/break/continue), match arms,
|
|
* tagged unions, struct literals, attributes, and tuples. We diff
|
|
* against the C parser on every selfhost file plus the stdlib. */
|
|
const char *ast_inputs[] = {
|
|
"selfhost/test/uses.ww",
|
|
"selfhost/cmd/wcc/err.ww",
|
|
"selfhost/cmd/wcc/mem.ww",
|
|
"lib/ww/lex/lex.ww",
|
|
"lib/ww/lex/tok.ww",
|
|
"lib/ww/ast.ww",
|
|
"lib/ww/parse/parse.ww", "lib/ww/parse/expr.ww", "lib/ww/parse/stmt.ww", "lib/ww/parse/decl.ww",
|
|
"lib/ww/typ.ww",
|
|
"lib/ww/sym.ww",
|
|
"selfhost/cmd/wcc/check.ww",
|
|
"selfhost/cmd/wcc/cgen.ww",
|
|
"selfhost/cmd/wwdump/main.ww",
|
|
"selfhost/test/smoke.ww",
|
|
"lib/strconv/strconv.ww",
|
|
"lib/os/os.ww",
|
|
"lib/ascii/ascii.ww",
|
|
"lib/fmt/fmt.ww",
|
|
"lib/strings/strings.ww",
|
|
"lib/bytes/bytes.ww",
|
|
"lib/io/stream.ww",
|
|
"lib/errors/errors.ww",
|
|
"lib/bufio/bufio.ww",
|
|
"lib/types/types.ww",
|
|
"lib/sort/sort.ww",
|
|
"lib/path/path.ww",
|
|
"lib/net/net.ww",
|
|
"lib/encoding/utf8/utf8.ww",
|
|
"lib/encoding/hex/hex.ww",
|
|
"lib/hash/fnv/fnv.ww",
|
|
"lib/time/time.ww",
|
|
"lib/c/libc/libc.ww",
|
|
NULL,
|
|
};
|
|
int fail = 0;
|
|
if (probe_dump_diff_mode(bin, "-t", tok_inputs) != 0) fail++;
|
|
if (probe_dump_diff_mode(bin, "-a", ast_inputs) != 0) fail++;
|
|
return fail ? -1 : 0;
|
|
}
|
|
|
|
/* Probe 5: ww-side cgen (wwdump_ww -c) emits Plan 9 amd64 asm. For
|
|
* each fixture we (a) diff the ww asm against C-side w6c and (b)
|
|
* assemble + link + run the ww output and compare exit codes. Both
|
|
* must succeed. The cgen port handles literals, +/-/star/slash,
|
|
* compound assignment, idents, calls, if/else, for loops, and
|
|
* comparisons; fixtures stay within that surface. */
|
|
struct cprog { const char *src; int want_exit; };
|
|
|
|
static int
|
|
probe_ww_compile(const char *bin)
|
|
{
|
|
static const struct cprog progs[] = {
|
|
{ "fn main() i32 = { return 42; };", 42 },
|
|
{ "fn add(a: i32, b: i32) i32 = { return a + b; };\n"
|
|
"fn main() i32 = { return add(7, 35); };", 42 },
|
|
{ "fn sum(n: i32) i32 = {\n"
|
|
" let s: i32 = 0;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < n) { s += i; i += 1; };\n"
|
|
" return s;\n"
|
|
"};\n"
|
|
"fn main() i32 = { return sum(10); };", 45 },
|
|
{ "fn check(x: i32) i32 = {\n"
|
|
" if (x > 0) { return 1; };\n"
|
|
" if (x < 0) { return 100; };\n"
|
|
" return 0;\n"
|
|
"};\n"
|
|
"fn main() i32 = { return check(7); };", 1 },
|
|
{ "fn fact(n: i32) i32 = {\n"
|
|
" if (n <= 1) { return 1; };\n"
|
|
" return n * fact(n - 1);\n"
|
|
"};\n"
|
|
"fn main() i32 = { return fact(5); };", 120 },
|
|
/* hello-world via syscall: exercises @symbol FFI, string
|
|
* literal interning, DATA emission, str struct slot (16B),
|
|
* str pseudo-fields (.ptr/.len), N_CAST. write(1,"hi\n",3)
|
|
* returns 3 — we cast that down to the exit code. */
|
|
{ "@symbol(\"rt_syscall\") fn rt_syscall(num: i64, a: i64, b: i64, c: i64) i64;\n"
|
|
"fn main() i32 = {\n"
|
|
" let s: str = \"hi\\n\";\n"
|
|
" let r: i64 = rt_syscall(1, 1, s.ptr: i64, s.len: i64);\n"
|
|
" return r: i32;\n"
|
|
"};", 3 },
|
|
/* struct fields (local + via *struct), &local, struct-name
|
|
* registry: distance squared returns 25 = 3*3 + 4*4. */
|
|
{ "type point = struct { x: i64, y: i64 };\n"
|
|
"fn distance_sq(p: *point) i64 = { return p.x * p.x + p.y * p.y; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let p: point;\n"
|
|
" p.x = 3i64;\n"
|
|
" p.y = 4i64;\n"
|
|
" return distance_sq(&p): i32;\n"
|
|
"};", 25 },
|
|
/* array indexing — element-size scaling for u8 arrays.
|
|
* Sets buf[0..3] to 7,8,9,10 then sums them: 34. */
|
|
{ "fn main() i32 = {\n"
|
|
" let buf: [4]u8;\n"
|
|
" buf[0] = 7u8;\n"
|
|
" buf[1] = 8u8;\n"
|
|
" buf[2] = 9u8;\n"
|
|
" buf[3] = 10u8;\n"
|
|
" let s: i32 = 0;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < 4) { s += buf[i]: i32; i += 1; };\n"
|
|
" return s;\n"
|
|
"};", 34 },
|
|
/* struct literal init — `let p: point = point { x=..., y=...};`.
|
|
* Each field stored at its struct offset in the slot. */
|
|
{ "type point = struct { x: i64, y: i64 };\n"
|
|
"fn main() i32 = {\n"
|
|
" let p: point = point { x = 7i64, y = 35i64 };\n"
|
|
" return (p.x + p.y): i32;\n"
|
|
"};", 42 },
|
|
/* fn-returning-str — exercises the SysV (AX, DX) → (AX, BX)
|
|
* shuffle so str values flow through cgen as the canonical
|
|
* pair. write(\"hello world\\n\") → exit 12 (length). */
|
|
{ "@symbol(\"rt_syscall\") fn rt_syscall(num: i64, a: i64, b: i64, c: i64) i64;\n"
|
|
"fn greeting() str = { return \"hello world\\n\"; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let g: str = greeting();\n"
|
|
" rt_syscall(1, 1, g.ptr: i64, g.len: i64);\n"
|
|
" return g.len: i32;\n"
|
|
"};", 12 },
|
|
/* fn-pointer field call — `w.emit(b)` where w is a struct
|
|
* with an emit: fn(b: u8) i32 field. Loads the field value
|
|
* into AX and CALLs through it. The polymorphism shape ww
|
|
* uses instead of interfaces. */
|
|
{ "type writer = struct { emit: fn(b: u8) i32 };\n"
|
|
"fn count_emit(b: u8) i32 = { return b: i32 + 1; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let w: writer = writer { emit = count_emit };\n"
|
|
" return w.emit(41u8);\n"
|
|
"};", 42 },
|
|
/* Match expression — tagged union dispatch. classify(ok)=42,
|
|
* classify(err)=-1, sum 41. Exercises tagged init, tagged-arg
|
|
* push (3 regs), match arm tag-compare + payload bind. */
|
|
{ "fn classify(r: (i32 | str)) i32 = {\n"
|
|
" match (r) {\n"
|
|
" case let v: i32 => return v;\n"
|
|
" case let e: str => return -1;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let ok: (i32 | str) = 42;\n"
|
|
" let err: (i32 | str) = \"fail\";\n"
|
|
" return classify(ok) + classify(err);\n"
|
|
"};", 41 },
|
|
/* Compound subtract via *struct field — regression for
|
|
* the bug that made wwdump_ww segfault on its own source.
|
|
* c.x = 5; dec; dec; dec → expect 2. Pre-fix, both C and
|
|
* ww emitted SUBQ in the wrong direction (computing rhs-old
|
|
* instead of old-rhs) and this returned -4 (= 252 as u8). */
|
|
{ "type ctx = struct { x: i32 };\n"
|
|
"fn dec(c: *ctx) void = { c.x -= 1; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let c: ctx;\n"
|
|
" c.x = 5;\n"
|
|
" dec(&c); dec(&c); dec(&c);\n"
|
|
" return c.x;\n"
|
|
"};", 2 },
|
|
/* Uninit `[8]u8` local — raw size 8B, C cgen zero-inits via
|
|
* `MOVQ $0, off(BP)`. Pre-fix the wwstage left the slot
|
|
* holding stack junk and `buf[0]` returned non-zero. */
|
|
{ "fn main() i32 = {\n"
|
|
" let buf: [8]u8;\n"
|
|
" return buf[0]: i32;\n"
|
|
"};", 0 },
|
|
/* [N]i32 indexed write + read: MOVL store, MOVSXD load.
|
|
* Pre-fix the wwstage emitted MOVQ in both directions —
|
|
* overwrites adjacent slots and loads the wrong width.
|
|
* arr[0..3] = -7, 3, 11, sum = 7. */
|
|
{ "fn main() i32 = {\n"
|
|
" let arr: [3]i32;\n"
|
|
" arr[0] = 0 - 7;\n"
|
|
" arr[1] = 3;\n"
|
|
" arr[2] = 11;\n"
|
|
" return arr[0] + arr[1] + arr[2];\n"
|
|
"};", 7 },
|
|
/* Hare-style for-range over a slice: `for (let b .. s)`.
|
|
* Allocates `.rgi`/`.rgl` scratch slots, walks i=0..s.len
|
|
* loading s.ptr[i] into the binding. esz=1 here so the
|
|
* load is MOVZBQ. Sum 10+20+30+40 = 100. */
|
|
{ "use os;\n"
|
|
"fn main() i32 = {\n"
|
|
" let s: []u8;\n"
|
|
" s.ptr = nil; s.len = 0; s.cap = 0;\n"
|
|
" append(s, 10u8, 20u8, 30u8, 40u8);\n"
|
|
" let total: i32 = 0;\n"
|
|
" for (let b .. s) { total += b: i32; };\n"
|
|
" return total;\n"
|
|
"};", 100 },
|
|
/* for-range with tuple destructure on `(i64, i64)`. Element
|
|
* size is 16 (sum of raw param sizes); each binding loads
|
|
* via BX+foff. Buf has (1,10) (2,20); sum = 33. */
|
|
{ "fn main() i32 = {\n"
|
|
" let buf: [4]i64;\n"
|
|
" buf[0] = 1i64; buf[1] = 10i64; buf[2] = 2i64; buf[3] = 20i64;\n"
|
|
" let s: [](i64, i64);\n"
|
|
" s.ptr = buf.ptr: *(i64, i64);\n"
|
|
" s.len = 2; s.cap = 2;\n"
|
|
" let total: i64 = 0i64;\n"
|
|
" for (let (k, v) .. s) { total += k + v; };\n"
|
|
" return total: i32;\n"
|
|
"};", 33 },
|
|
/* append(s, v, ...) builtin — Hare's rt::ensure model.
|
|
* Each value: PUSHQ AX, ADDQ $1 to s.len, LEAQ s/MOVQ esz
|
|
* args for rt_ensure, then write into the freshly-grown
|
|
* slot. Returns s.len = 3 after appending three u8s. */
|
|
{ "use os;\n"
|
|
"fn main() i32 = {\n"
|
|
" let s: []u8;\n"
|
|
" s.ptr = nil; s.len = 0; s.cap = 0;\n"
|
|
" append(s, 65u8, 66u8, 67u8);\n"
|
|
" return s.len: i32;\n"
|
|
"};", 3 },
|
|
/* append(s, items...) spread variant — wraps the per-value
|
|
* body in a counted loop over items.len. Combined with
|
|
* single-value appends in the same fn. dst ends up with
|
|
* [1, 10, 20, 30] — sum = 61. */
|
|
{ "use os;\n"
|
|
"fn main() i32 = {\n"
|
|
" let src: []i64;\n"
|
|
" src.ptr = nil; src.len = 0; src.cap = 0;\n"
|
|
" append(src, 10i64, 20i64, 30i64);\n"
|
|
" let dst: []i64;\n"
|
|
" dst.ptr = nil; dst.len = 0; dst.cap = 0;\n"
|
|
" append(dst, 1i64);\n"
|
|
" append(dst, src...);\n"
|
|
" let total: i64 = 0i64;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < dst.len) { total += dst[i]; i += 1; };\n"
|
|
" return total: i32;\n"
|
|
"};", 61 },
|
|
/* switch with multi-expr cases + default. Lowers to a chain
|
|
* of CMPQ + JE; the scrutinee lands in a fresh 8B local slot
|
|
* (".sw_N") so case bodies can spill SP without losing it.
|
|
* classify(2)=10, classify(7)=70, classify(99)=0, sum 80. */
|
|
{ "fn classify(x: i32) i32 = {\n"
|
|
" switch (x) {\n"
|
|
" case 1, 2, 3: return 10;\n"
|
|
" case 7: return 70;\n"
|
|
" case: return 0;\n"
|
|
" };\n"
|
|
" return -1;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" return classify(2) + classify(7) + classify(99);\n"
|
|
"};", 80 },
|
|
/* Chained N_DOT: o.ptr_to_inner.val (read field through a
|
|
* pointer-to-struct field). Pre-fix the cgen fell through
|
|
* silently and returned the inner pointer instead of the
|
|
* dereferenced field. Surfaced porting w6l/pass.ww. */
|
|
{ "@symbol(\"rt_alloc\") fn rt_alloc(n: u64) *void;\n"
|
|
"type inner = struct { val: u64 };\n"
|
|
"type outer = struct { p: *inner };\n"
|
|
"fn main() i32 = {\n"
|
|
" let in_: *inner = rt_alloc(8u64): *inner;\n"
|
|
" in_.val = 42u64;\n"
|
|
" let o: *outer = rt_alloc(8u64): *outer;\n"
|
|
" o.p = in_;\n"
|
|
" return o.p.val: i32;\n"
|
|
"};", 42 },
|
|
{ NULL, 0 },
|
|
};
|
|
char rt[1024];
|
|
snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin);
|
|
int fail = 0;
|
|
for (int i = 0; progs[i].src; i++) {
|
|
char src[64], wws[64], cs[64], obj[64], exe[64], cmd[2048];
|
|
snprintf(src, sizeof src, "/tmp/wwc_%d_%d.ww", getpid(), i);
|
|
snprintf(wws, sizeof wws, "/tmp/wwc_%d_%d_w.s", getpid(), i);
|
|
snprintf(cs, sizeof cs, "/tmp/wwc_%d_%d_c.s", getpid(), i);
|
|
snprintf(obj, sizeof obj, "/tmp/wwc_%d_%d.o", getpid(), i);
|
|
snprintf(exe, sizeof exe, "/tmp/wwc_%d_%d", getpid(), i);
|
|
|
|
FILE *f = fopen(src, "wb");
|
|
if (!f) { fail++; continue; }
|
|
fputs(progs[i].src, f);
|
|
fclose(f);
|
|
|
|
snprintf(cmd, sizeof cmd, "%s/wwdump_ww -c %s > %s 2>/dev/null",
|
|
bin, src, wws);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "ww-compile prog %d: wwdump_ww -c errored\n", i);
|
|
fail++; goto cleanup;
|
|
}
|
|
snprintf(cmd, sizeof cmd, "%s/w6c %s > %s 2>/dev/null", bin, src, cs);
|
|
runwait(cmd);
|
|
|
|
char *bw = NULL, *bc = NULL;
|
|
size_t nw = 0, nc = 0;
|
|
if (slurp(wws, &bw, &nw) >= 0 && slurp(cs, &bc, &nc) >= 0) {
|
|
if (nw != nc || memcmp(bw, bc, nw) != 0) {
|
|
fprintf(stderr, "ww-compile prog %d: asm differs (C %zu, ww %zu)\n",
|
|
i, nc, nw);
|
|
fail++;
|
|
}
|
|
}
|
|
free(bw); free(bc);
|
|
|
|
snprintf(cmd, sizeof cmd, "%s/w6a -o %s %s 2>/dev/null", bin, obj, wws);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "ww-compile prog %d: w6a failed\n", i);
|
|
fail++; goto cleanup;
|
|
}
|
|
snprintf(cmd, sizeof cmd, "%s/w6l -o %s %s %s 2>/dev/null",
|
|
bin, exe, obj, rt);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "ww-compile prog %d: w6l failed\n", i);
|
|
fail++; goto cleanup;
|
|
}
|
|
int rc = runwait(exe);
|
|
if (rc != progs[i].want_exit) {
|
|
fprintf(stderr, "ww-compile prog %d: exit=%d, want %d\n",
|
|
i, rc, progs[i].want_exit);
|
|
fail++;
|
|
}
|
|
cleanup:
|
|
unlink(src); unlink(wws); unlink(cs); unlink(obj); unlink(exe);
|
|
}
|
|
return fail ? -1 : 0;
|
|
}
|
|
|
|
/* Probe 4: ww-side checker (wwdump_ww -r) runs name resolution on
|
|
* each "complete" selfhost fixture and reports zero unresolved. The
|
|
* complete set is the ones whose identifiers are all locally defined
|
|
* — they don't import other selfhost modules. (Files that DO import
|
|
* still resolve most names but have some unresolved type/sym refs;
|
|
* those are exercised by the broader -t and -a diff probes.) */
|
|
static int
|
|
probe_resolve(const char *bin)
|
|
{
|
|
char cwd[1024];
|
|
if (getcwd(cwd, sizeof cwd) == NULL) return -1;
|
|
const char *complete[] = {
|
|
"selfhost/cmd/wcc/err.ww",
|
|
"selfhost/cmd/wcc/mem.ww",
|
|
"lib/ww/lex/tok.ww",
|
|
"selfhost/test/smoke.ww",
|
|
NULL,
|
|
};
|
|
int fail = 0;
|
|
for (int i = 0; complete[i]; i++) {
|
|
char a[256], cmd[2048];
|
|
snprintf(a, sizeof a, "/tmp/wwd_r_%d", getpid());
|
|
snprintf(cmd, sizeof cmd, "%s/wwdump_ww -r %s/%s > %s 2>/dev/null",
|
|
bin, cwd, complete[i], a);
|
|
int rc = runwait(cmd);
|
|
if (rc != 0) {
|
|
fprintf(stderr, "resolve FAIL: %s exited %d (unresolved names)\n",
|
|
complete[i], rc);
|
|
fail++;
|
|
}
|
|
unlink(a);
|
|
}
|
|
return fail ? -1 : 0;
|
|
}
|
|
|
|
/* Probe 3: wwdump must be deterministic across two runs. */
|
|
static int
|
|
probe_dump_stable(const char *bin)
|
|
{
|
|
char cwd[1024];
|
|
if (getcwd(cwd, sizeof cwd) == NULL) return -1;
|
|
const char *inputs[] = {
|
|
"selfhost/cmd/wcc/mem.ww",
|
|
"selfhost/cmd/wcc/err.ww",
|
|
"selfhost/test/smoke.ww",
|
|
NULL,
|
|
};
|
|
const char *modes[] = { "-t", "-a", NULL };
|
|
int fail = 0;
|
|
for (int i = 0; inputs[i]; i++) {
|
|
for (int m = 0; modes[m]; m++) {
|
|
char a[256], b[256];
|
|
snprintf(a, sizeof a, "/tmp/wwd_%d_a", getpid());
|
|
snprintf(b, sizeof b, "/tmp/wwd_%d_b", getpid());
|
|
char cmd[2048];
|
|
snprintf(cmd, sizeof cmd, "%s/wwdump %s %s/%s > %s 2>/dev/null",
|
|
bin, modes[m], cwd, inputs[i], a);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "wwdump FAIL: %s %s\n", modes[m], inputs[i]);
|
|
unlink(a);
|
|
fail++;
|
|
continue;
|
|
}
|
|
snprintf(cmd, sizeof cmd, "%s/wwdump %s %s/%s > %s 2>/dev/null",
|
|
bin, modes[m], cwd, inputs[i], b);
|
|
runwait(cmd);
|
|
char *ba = NULL, *bb = NULL;
|
|
size_t na = 0, nb = 0;
|
|
if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) {
|
|
fprintf(stderr, "wwdump FAIL: cannot read dumps\n");
|
|
free(ba); free(bb);
|
|
unlink(a); unlink(b);
|
|
fail++;
|
|
continue;
|
|
}
|
|
if (na == 0) {
|
|
fprintf(stderr, "wwdump FAIL: empty dump for %s %s\n",
|
|
modes[m], inputs[i]);
|
|
fail++;
|
|
} else if (na != nb || memcmp(ba, bb, na) != 0) {
|
|
fprintf(stderr, "wwdump FAIL: nondeterministic %s %s\n",
|
|
modes[m], inputs[i]);
|
|
fail++;
|
|
}
|
|
free(ba); free(bb);
|
|
unlink(a); unlink(b);
|
|
}
|
|
}
|
|
return fail ? -1 : 0;
|
|
}
|
|
|
|
/* Probe 9: full bootstrap fixed-point. ww1 → ww2 → ww3 with
|
|
* cmp ww2.s == ww3.s and cmp ww2 == ww3 (byte-identical binaries).
|
|
* This is the textbook self-host gate: the compiler must reach a
|
|
* fixed point under iterated self-compilation. */
|
|
static int
|
|
probe_bootstrap_fixed_point(const char *bin)
|
|
{
|
|
char cwd[1024];
|
|
if (getcwd(cwd, sizeof cwd) == NULL) return -1;
|
|
char tmpdir[64];
|
|
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwfp_%d", getpid());
|
|
mkdir(tmpdir, 0755);
|
|
char ww2s[512], ww2o[512], ww2e[512];
|
|
char ww3s[512], ww3o[512], ww3e[512];
|
|
char rt[1024], cmd[4096];
|
|
snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin);
|
|
snprintf(ww2s, sizeof ww2s, "%s/ww2.s", tmpdir);
|
|
snprintf(ww2o, sizeof ww2o, "%s/ww2.o", tmpdir);
|
|
snprintf(ww2e, sizeof ww2e, "%s/ww2", tmpdir);
|
|
snprintf(ww3s, sizeof ww3s, "%s/ww3.s", tmpdir);
|
|
snprintf(ww3o, sizeof ww3o, "%s/ww3.o", tmpdir);
|
|
snprintf(ww3e, sizeof ww3e, "%s/ww3", tmpdir);
|
|
int fail = 0;
|
|
/* ww1 -> ww2 */
|
|
snprintf(cmd, sizeof cmd,
|
|
"%s/wwdump_ww -c %s/selfhost/cmd/wwdump/main.combined.ww > %s 2>/dev/null",
|
|
bin, cwd, ww2s);
|
|
if (runwait(cmd) != 0) { fprintf(stderr, "fp FAIL: ww1 self-compile\n"); fail++; goto cleanup; }
|
|
snprintf(cmd, sizeof cmd, "%s/w6a -o %s %s 2>/dev/null", bin, ww2o, ww2s);
|
|
if (runwait(cmd) != 0) { fprintf(stderr, "fp FAIL: w6a ww2.s\n"); fail++; goto cleanup; }
|
|
snprintf(cmd, sizeof cmd, "%s/w6l -o %s %s %s 2>/dev/null", bin, ww2e, ww2o, rt);
|
|
if (runwait(cmd) != 0) { fprintf(stderr, "fp FAIL: w6l ww2\n"); fail++; goto cleanup; }
|
|
/* ww2 -> ww3 */
|
|
snprintf(cmd, sizeof cmd,
|
|
"%s -c %s/selfhost/cmd/wwdump/main.combined.ww > %s 2>/dev/null",
|
|
ww2e, cwd, ww3s);
|
|
if (runwait(cmd) != 0) { fprintf(stderr, "fp FAIL: ww2 self-compile\n"); fail++; goto cleanup; }
|
|
snprintf(cmd, sizeof cmd, "%s/w6a -o %s %s 2>/dev/null", bin, ww3o, ww3s);
|
|
if (runwait(cmd) != 0) { fprintf(stderr, "fp FAIL: w6a ww3.s\n"); fail++; goto cleanup; }
|
|
snprintf(cmd, sizeof cmd, "%s/w6l -o %s %s %s 2>/dev/null", bin, ww3e, ww3o, rt);
|
|
if (runwait(cmd) != 0) { fprintf(stderr, "fp FAIL: w6l ww3\n"); fail++; goto cleanup; }
|
|
/* The fixed-point check: ww2.s == ww3.s, ww2 == ww3. */
|
|
snprintf(cmd, sizeof cmd, "cmp -s %s %s", ww2s, ww3s);
|
|
if (runwait(cmd) != 0) { fprintf(stderr, "fp FAIL: ww2.s != ww3.s (no fixed point)\n"); fail++; }
|
|
snprintf(cmd, sizeof cmd, "cmp -s %s %s", ww2e, ww3e);
|
|
if (runwait(cmd) != 0) { fprintf(stderr, "fp FAIL: ww2 != ww3 binaries\n"); fail++; }
|
|
cleanup:
|
|
snprintf(cmd, sizeof cmd, "rm -rf %s", tmpdir);
|
|
runwait(cmd);
|
|
return fail ? -1 : 0;
|
|
}
|
|
|
|
/* Probe 8: ww1 → ww2 ladder. wwdump_ww (= ww1, built by C tools)
|
|
* compiles its own concatenated source. The output assembles + links
|
|
* via w6a/w6l into ww2 (a fresh ww-built compiler binary). Confirms
|
|
* the full ww-cgen pipeline is functionally correct end-to-end on
|
|
* the entire frontend (lex+parse+check+cgen+ast+typ+sym+mem+err+
|
|
* driver). Pre-fix-set this segfaulted at 5919 lines of asm; post-
|
|
* fix-set it produces 43K+ lines that assemble/link cleanly. */
|
|
static int
|
|
probe_ww1_to_ww2(const char *bin)
|
|
{
|
|
char cwd[1024];
|
|
if (getcwd(cwd, sizeof cwd) == NULL) return -1;
|
|
char tmpdir[64];
|
|
snprintf(tmpdir, sizeof tmpdir, "/tmp/ww12_%d", getpid());
|
|
mkdir(tmpdir, 0755);
|
|
char asm_p[512], obj_p[512], exe_p[512], cmd[4096];
|
|
snprintf(asm_p, sizeof asm_p, "%s/ww2.s", tmpdir);
|
|
snprintf(obj_p, sizeof obj_p, "%s/ww2.o", tmpdir);
|
|
snprintf(exe_p, sizeof exe_p, "%s/ww2", tmpdir);
|
|
int fail = 0;
|
|
snprintf(cmd, sizeof cmd,
|
|
"%s/wwdump_ww -c %s/selfhost/cmd/wwdump/main.combined.ww > %s 2>/dev/null",
|
|
bin, cwd, asm_p);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "ww1->ww2 FAIL: wwdump_ww segfaulted on its own source\n");
|
|
fail++;
|
|
goto cleanup;
|
|
}
|
|
snprintf(cmd, sizeof cmd, "%s/w6a -o %s %s 2>/dev/null", bin, obj_p, asm_p);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "ww1->ww2 FAIL: w6a errored on ww1 output\n");
|
|
fail++;
|
|
goto cleanup;
|
|
}
|
|
char rt[1024];
|
|
snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin);
|
|
snprintf(cmd, sizeof cmd, "%s/w6l -o %s %s %s 2>/dev/null",
|
|
bin, exe_p, obj_p, rt);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "ww1->ww2 FAIL: w6l errored\n");
|
|
fail++;
|
|
goto cleanup;
|
|
}
|
|
struct stat st;
|
|
if (stat(exe_p, &st) != 0 || !(st.st_mode & 0111)) {
|
|
fprintf(stderr, "ww1->ww2 FAIL: ww2 binary not executable\n");
|
|
fail++;
|
|
}
|
|
cleanup:
|
|
snprintf(cmd, sizeof cmd, "rm -rf %s", tmpdir);
|
|
runwait(cmd);
|
|
return fail ? -1 : 0;
|
|
}
|
|
|
|
/* Probe 7: end-to-end via wwdump_ww. Use `ww build` to produce the
|
|
* concatenated source as a side effect, then drive ww-cgen → w6a →
|
|
* w6l → run, comparing the binary's exit code to the fixture's
|
|
* declared expectation. This is the bootstrap-grade signal: it
|
|
* doesn't require ww-cgen output to be byte-identical to C-cgen,
|
|
* only to be functionally correct. The byte-match probe above is
|
|
* useful while it lasts but layout choices (slot allocation order,
|
|
* spill placement) eventually diverge between cgens; correctness is
|
|
* the test that scales. */
|
|
static int
|
|
probe_ww_links(const char *bin)
|
|
{
|
|
const char *fixtures[][2] = {
|
|
{ "selfhost/test/sym_link.ww", "42" }, /* sym/typ/ast/mem */
|
|
{ NULL, NULL },
|
|
};
|
|
char cwd[1024];
|
|
if (getcwd(cwd, sizeof cwd) == NULL) return -1;
|
|
int fail = 0;
|
|
for (int i = 0; fixtures[i][0]; i++) {
|
|
const char *fix = fixtures[i][0];
|
|
int want = atoi(fixtures[i][1]);
|
|
char tmpdir[64];
|
|
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwlnk_%d_%d", getpid(), i);
|
|
mkdir(tmpdir, 0755);
|
|
const char *base = strrchr(fix, '/');
|
|
base = base ? base + 1 : fix;
|
|
char stem[256];
|
|
snprintf(stem, sizeof stem, "%s/%s", tmpdir, base);
|
|
char *dot = strrchr(stem, '.');
|
|
if (dot) *dot = '\0';
|
|
char tmpsrc[512];
|
|
snprintf(tmpsrc, sizeof tmpsrc, "%s.ww", stem);
|
|
char cmd[4096];
|
|
snprintf(cmd, sizeof cmd, "cp %s/%s %s", cwd, fix, tmpsrc);
|
|
runwait(cmd);
|
|
/* ww build to get the .combined.ww as a side effect. */
|
|
snprintf(cmd, sizeof cmd,
|
|
"cd %s && %s/ww build -I %s/lib/ww -I %s/lib/ww/lex -I %s/lib/ww/parse -I %s/selfhost/cmd/wcc %s >/dev/null 2>&1",
|
|
tmpdir, bin, cwd, cwd, cwd, cwd, tmpsrc);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "ww-links FAIL: ww build %s\n", fix);
|
|
fail++;
|
|
goto cleanup;
|
|
}
|
|
/* Now compile the combined source via the ww frontend. */
|
|
char combinedp[512], wsp[512], wop[512], wexep[512];
|
|
snprintf(combinedp, sizeof combinedp, "%s.combined.ww", stem);
|
|
snprintf(wsp, sizeof wsp, "%s_w.s", stem);
|
|
snprintf(wop, sizeof wop, "%s_w.o", stem);
|
|
snprintf(wexep, sizeof wexep, "%s_w", stem);
|
|
snprintf(cmd, sizeof cmd,
|
|
"%s/wwdump_ww -c %s > %s 2>/dev/null", bin, combinedp, wsp);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "ww-links FAIL: wwdump_ww -c %s\n", fix);
|
|
fail++;
|
|
goto cleanup;
|
|
}
|
|
snprintf(cmd, sizeof cmd, "%s/w6a -o %s %s 2>/dev/null",
|
|
bin, wop, wsp);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "ww-links FAIL: w6a %s\n", fix);
|
|
fail++;
|
|
goto cleanup;
|
|
}
|
|
char rt[1024];
|
|
snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin);
|
|
snprintf(cmd, sizeof cmd, "%s/w6l -o %s %s %s 2>/dev/null",
|
|
bin, wexep, wop, rt);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "ww-links FAIL: w6l %s\n", fix);
|
|
fail++;
|
|
goto cleanup;
|
|
}
|
|
int rc = runwait(wexep);
|
|
if (rc != want) {
|
|
fprintf(stderr, "ww-links FAIL: %s exit=%d want=%d\n",
|
|
fix, rc, want);
|
|
fail++;
|
|
}
|
|
cleanup:
|
|
/* Sweep tmpdir contents, then remove. ww build also drops a
|
|
* C-built binary at <stem> — clean it too. */
|
|
snprintf(cmd, sizeof cmd, "rm -rf %s", tmpdir);
|
|
runwait(cmd);
|
|
}
|
|
return fail ? -1 : 0;
|
|
}
|
|
|
|
/* Probe 6: ww-cgen byte-matches C-cgen on every selfhost file that
|
|
* raw w6c can compile in isolation (no driver concatenation). The list
|
|
* is the convergence set: when this stays empty, the cmp ww2 ww3
|
|
* ceiling becomes feasible. */
|
|
static int
|
|
probe_cgen_match(const char *bin)
|
|
{
|
|
const char *files[] = {
|
|
"selfhost/cmd/wcc/mem.ww",
|
|
"selfhost/cmd/wcc/err.ww",
|
|
"lib/ww/lex/tok.ww",
|
|
"selfhost/test/smoke.ww",
|
|
NULL,
|
|
};
|
|
char cwd[1024];
|
|
if (getcwd(cwd, sizeof cwd) == NULL) return -1;
|
|
int fail = 0;
|
|
for (int i = 0; files[i]; i++) {
|
|
char a[256], b[256], cmd[2048];
|
|
snprintf(a, sizeof a, "/tmp/wwd_cm_%d_c", getpid());
|
|
snprintf(b, sizeof b, "/tmp/wwd_cm_%d_w", getpid());
|
|
snprintf(cmd, sizeof cmd, "%s/w6c %s/%s > %s 2>/dev/null",
|
|
bin, cwd, files[i], a);
|
|
runwait(cmd);
|
|
snprintf(cmd, sizeof cmd, "%s/wwdump_ww -c %s/%s > %s 2>/dev/null",
|
|
bin, cwd, files[i], b);
|
|
runwait(cmd);
|
|
char *bc = NULL, *bw = NULL;
|
|
size_t nc = 0, nw = 0;
|
|
int rc1 = slurp(a, &bc, &nc);
|
|
int rc2 = slurp(b, &bw, &nw);
|
|
if (rc1 < 0 || rc2 < 0) {
|
|
fprintf(stderr, "cgen-match FAIL: read %s\n", files[i]);
|
|
fail++;
|
|
} else if (nc != nw || memcmp(bc, bw, nc) != 0) {
|
|
fprintf(stderr, "cgen-match FAIL: %s — C %zu vs ww %zu bytes\n",
|
|
files[i], nc, nw);
|
|
fail++;
|
|
}
|
|
free(bc); free(bw);
|
|
unlink(a); unlink(b);
|
|
}
|
|
return fail ? -1 : 0;
|
|
}
|
|
|
|
int
|
|
main(void)
|
|
{
|
|
const char *bin = absbin();
|
|
if (!bin) return 1;
|
|
|
|
int fail = 0;
|
|
if (probe_codegen(bin) != 0) fail++;
|
|
if (probe_smoke(bin) != 0) fail++;
|
|
if (probe_dump_stable(bin) != 0) fail++;
|
|
if (probe_dump_diff(bin) != 0) fail++;
|
|
if (probe_resolve(bin) != 0) fail++;
|
|
if (probe_ww_compile(bin) != 0) fail++;
|
|
if (probe_cgen_match(bin) != 0) fail++;
|
|
if (probe_ww_links(bin) != 0) fail++;
|
|
if (probe_ww1_to_ww2(bin) != 0) fail++;
|
|
if (probe_bootstrap_fixed_point(bin) != 0) fail++;
|
|
|
|
if (fail) {
|
|
fprintf(stderr, "selfhost: %d probe(s) failed\n", fail);
|
|
return 1;
|
|
}
|
|
printf("selfhost: codegen ok; smoke=42; wwdump stable; "
|
|
"ww lexer matches C on 8 fixtures (-t); "
|
|
"ww parser matches C on 32 fixtures (-a); "
|
|
"ww checker resolves 4 self-contained fixtures (-r); "
|
|
"ww cgen compiles + runs 13 ww programs (incl. compound -= regression); "
|
|
"ww cgen byte-matches C on 4 selfhost fixtures (smoke/mem/err/tok); "
|
|
"ww cgen builds runnable binaries from sym/typ/ast/mem stack; "
|
|
"ww1 -> ww2 -> ww3 fixed-point reached: ww2.s == ww3.s, ww2 == ww3 byte-identical\n");
|
|
return 0;
|
|
}
|