The driver flip moves build artifacts from next-to-source <stem>.s to a .sepwork/ scratch dir. 29 Pattern-B gates now build `ww build --sep -o <stem>` and read <stem>.sepwork/__root.s (multi-package gates concat all <stem>.sepwork/*.s, since cross-package labels live in per-package .s). All intermediates redirect to /tmp (WW_PKGCACHE + -o), so the corpus runs parallel-safe with no source-tree pollution. Tests pass now (--sep is live) and survive the flip. 915 additionally retargeted off strconv's PRIVATE left_shift_table (a let, not export) — which separate compilation correctly hides — onto a test-local package that exports its own probe table (#96). The combined path only linked it via a single-unit private leak; encapsulation is now honored under sep. Test-only; all 5 *_ww binaries HOLD. 989_m1mangle_run deferred (blocked by #99, imported-package fn main mangling under sep).
353 lines
12 KiB
C
353 lines
12 KiB
C
/*
|
|
* 769_dot_aliased_ptr — project #191: wwstage cgdot N_DOT on an
|
|
* aliased-pointer receiver. `type vt = struct{...}; type vs = *vt;
|
|
* fn f(s: vs) { s.field }` — wwstage's cgdot read the receiver's
|
|
* tnode kind WITHOUT first peeling N_TNAME alias chains, so lkind
|
|
* stayed N_TNAME (not the underlying N_TPTR), structlookupchain
|
|
* missed (`vs` isn't a struct alias), and the lookup fell through
|
|
* to the SB-global fallback. The emitted asm was
|
|
* MOVQ field(SB), AX
|
|
* which links as an undefined symbol — runtime never reached.
|
|
*
|
|
* Cstage (cmd/w6c/cgen.c:7001-7004) uses type_chase_named to walk
|
|
* TY_NAMED.under to the underlying TY_PTR / TY_STRUCT before the
|
|
* kind-gated arms fire. Mirror: a loop via `aliaslookup(c, name)`
|
|
* at the top of cgdot's lc != nil branch, stopping at struct
|
|
* aliases so the existing direct-struct N_TNAME arm below stays
|
|
* byte-id with pre-fix #22 callers.
|
|
*
|
|
* Fix: selfhost/cmd/wcc/cgenexpr.ww cgdot, after `let tn = lc.tnode;`
|
|
* insert a peel loop before the lkind decision. LOOP not single-
|
|
* peel — Phase-N builds N_TNAME chains (memory
|
|
* project_tinfo_lossy_nominal), so depth-2+ aliases require
|
|
* iteration. Inner peel on the pointee is unnecessary because the
|
|
* existing `structlookupchain(c, inner)` already walks N_TNAME
|
|
* chains via aliaslookup (cgenutil.ww:1266-1276); row 4 below
|
|
* proves the inner-chain depth-3 path stays green without an
|
|
* explicit inner peel.
|
|
*
|
|
* Coverage (4 rows):
|
|
* 1. fn_param_read — `fn f(s: vs) { s.field }`. Single-
|
|
* alias receiver. Pre-fix: link
|
|
* failure on `field(SB)`. Post-fix:
|
|
* deref + offset load. The impl-e1
|
|
* fold-blocker exact shape.
|
|
* 2. let_binding_read — `let s: vs = (&v): vs; s.field`.
|
|
* Single-alias via let-init; the
|
|
* explicit cast routes around the
|
|
* cstage checker assignability gap
|
|
* for chain-depth-1 (see row 3).
|
|
* 3. double_alias_read — `type pvt = *vt; type vs = pvt;`
|
|
* with `let s: vs = (&v): vs`.
|
|
* KEN'S LOOP-PEEL VERIFY GATE:
|
|
* chain depth 2 on the receiver, so
|
|
* a single-peel implementation would
|
|
* stop at N_TNAME("pvt") and miss
|
|
* via structlookupchain (which only
|
|
* bottoms out when aliaslookup
|
|
* returns an N_TNAME that
|
|
* structlookup hits — here the next
|
|
* hop is N_TPTR, breaking its
|
|
* inner-loop guard). The cast in
|
|
* the let-init dodges the cstage
|
|
* checker's "*vt not assignable to
|
|
* vs" through double aliases (see
|
|
* SIBLING below).
|
|
* 4. pointee_alias_chain — `type vti = vt; type vti2 = vti;
|
|
* type vs = *vti2;`. Receiver itself
|
|
* is single-aliased; the chain
|
|
* depth lives inside the pointee.
|
|
* DREW'S INNER-PEEL VERIFY GATE:
|
|
* proves structlookupchain's
|
|
* aliaslookup loop is sufficient
|
|
* without an explicit inner peel.
|
|
* If this row reds, the structural
|
|
* no-inner-peel claim above is
|
|
* wrong and the fix needs an
|
|
* inner-side peel too.
|
|
*
|
|
* Per-row gates: cstage runtime exit, wwstage runtime exit,
|
|
* cs.s == ww.s byte-identical (rule-10 stage symmetry).
|
|
*
|
|
* SIBLING BUGS surfaced during impl — NOT fixed here, filed for
|
|
* separate single-class commits (rule 11 split, rule 7 no
|
|
* workarounds, task spec "If you find sibling bugs in cgdot: file
|
|
* inline"):
|
|
*
|
|
* (A) cgassign N_DOT TK_ASSIGN on aliased-ptr receiver SILENTLY
|
|
* DROPS THE STORE. `s.field = 7` where s: vs = *vt emits
|
|
* nothing — wwstage MOVs the rhs into AX and discards.
|
|
* Cstage emits the correct `MOVQ off(BP), BX; MOVL AX, (BX)`.
|
|
* Same pre-peel pattern as cgdot, at cgenexpr.ww:5160-5168
|
|
* (the `if (lkind == nkind.N_TPTR)` arm of the cgassign
|
|
* base-IDENT branch). Out of scope for #191 (read-side
|
|
* cgdot only); siblings to file as a follow-up class along
|
|
* with cgassign compound (+=, -=, etc.) — same shape, same
|
|
* drop. Repro:
|
|
* type vt = struct { field: i32 };
|
|
* type vs = *vt;
|
|
* fn setit(s: vs) void = { s.field = 7; };
|
|
*
|
|
* (B) Chained N_DOT spine (`p.i.x` where p: vs = *outer, outer
|
|
* has field i: inner, inner has field x) on aliased-ptr
|
|
* receiver also link-fails. The chained spine has dotlhs ==
|
|
* N_DOT (not N_IDENT), so #191's branch doesn't fire; the
|
|
* chained-spine codepath has its own peel gap. Repro:
|
|
* type inner = struct { x: i32 };
|
|
* type outer = struct { i: inner };
|
|
* type vs = *outer;
|
|
* fn callit(p: vs) i32 = { return p.i.x; };
|
|
*
|
|
* (C) Cstage checker rejects single-deep-alias assignability:
|
|
* `let s: vs = &v;` where vs = pvt = *vt errors with
|
|
* `init *vt not assignable to declared vs`. Workaround in
|
|
* row 3: `(&v): vs` explicit cast. The checker walks single
|
|
* aliases but not chains of depth >= 2. Not a wwstage cgen
|
|
* bug; cited as the reason row 3's let-init carries an
|
|
* explicit cast.
|
|
*
|
|
* GATE POLARITY: must stay GREEN. A red here means the cgdot
|
|
* outer-peel regressed (row 1/2), the loop-peel depth handling
|
|
* regressed (row 3), or the inner-chain structlookupchain path
|
|
* drifted (row 4).
|
|
*/
|
|
#include <stdio.h>
|
|
#include <stdlib.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;
|
|
}
|
|
|
|
#define STAGE_CS 1
|
|
#define STAGE_WW 2
|
|
|
|
struct row {
|
|
const char *label;
|
|
const char *src;
|
|
int expected_exit;
|
|
int stage_mask;
|
|
};
|
|
|
|
static const struct row rows[] = {
|
|
{ "fn_param_read",
|
|
"package main;\n"
|
|
"type vt = struct { field: i32 };\n"
|
|
"type vs = *vt;\n"
|
|
"fn callit(s: vs) i32 = { return s.field; };\n"
|
|
"export fn main() i32 = {\n"
|
|
" let v: vt; v.field = 42;\n"
|
|
" return callit(&v);\n"
|
|
"};\n",
|
|
42,
|
|
STAGE_CS | STAGE_WW },
|
|
{ "let_binding_read",
|
|
"package main;\n"
|
|
"type vt = struct { field: i32 };\n"
|
|
"type vs = *vt;\n"
|
|
"export fn main() i32 = {\n"
|
|
" let v: vt; v.field = 42;\n"
|
|
" let s: vs = (&v): vs;\n"
|
|
" return s.field;\n"
|
|
"};\n",
|
|
42,
|
|
STAGE_CS | STAGE_WW },
|
|
{ "double_alias_read",
|
|
"package main;\n"
|
|
"type vt = struct { field: i32 };\n"
|
|
"type pvt = *vt;\n"
|
|
"type vs = pvt;\n"
|
|
"export fn main() i32 = {\n"
|
|
" let v: vt; v.field = 42;\n"
|
|
" let s: vs = (&v): vs;\n"
|
|
" return s.field;\n"
|
|
"};\n",
|
|
42,
|
|
STAGE_CS | STAGE_WW },
|
|
{ "pointee_alias_chain",
|
|
"package main;\n"
|
|
"type vt = struct { field: i32 };\n"
|
|
"type vti = vt;\n"
|
|
"type vti2 = vti;\n"
|
|
"type vs = *vti2;\n"
|
|
"fn callit(s: vs) i32 = { return s.field; };\n"
|
|
"export fn main() i32 = {\n"
|
|
" let v: vti2; v.field = 42;\n"
|
|
" return callit(&v);\n"
|
|
"};\n",
|
|
42,
|
|
STAGE_CS | STAGE_WW },
|
|
};
|
|
|
|
static int
|
|
write_source(const char *src_path, const char *src)
|
|
{
|
|
FILE *f = fopen(src_path, "wb");
|
|
if (!f) return -1;
|
|
fputs(src, f);
|
|
fclose(f);
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
cleanup_tmp(const char *tmpdir, const char *base)
|
|
{
|
|
char p[512];
|
|
snprintf(p, sizeof p, "%s/%s.ww", tmpdir, base); unlink(p);
|
|
snprintf(p, sizeof p, "%s/%s.o", tmpdir, base); unlink(p);
|
|
snprintf(p, sizeof p, "%s/%s.combined.ww", tmpdir, base); unlink(p);
|
|
snprintf(p, sizeof p, "%s/%s", tmpdir, base); unlink(p);
|
|
/* #93: the sep scratch dir + the tmpdir-pinned pkgcache. */
|
|
snprintf(p, sizeof p, "rm -rf %s/%s.sepwork %s/pkgc", tmpdir, base, tmpdir);
|
|
if (system(p)) {} /* best-effort */
|
|
rmdir(tmpdir);
|
|
}
|
|
|
|
static int
|
|
build_via_driver(const char *driver, const char *tmpdir, const char *src)
|
|
{
|
|
char cmd[1024];
|
|
/* #93 sep layout: emit asm to <src-stem>.sepwork/__root.s; pin
|
|
* WW_PKGCACHE under tmpdir so out/.pkgcache is untouched. */
|
|
snprintf(cmd, sizeof cmd,
|
|
"cd %s && b='%s'; WW_PKGCACHE=%s/pkgc timeout 180 %s build --sep "
|
|
"-o \"${b%%.ww}\" \"$b\" 2>/dev/null",
|
|
tmpdir, src, tmpdir, driver);
|
|
return runwait(cmd);
|
|
}
|
|
|
|
static int
|
|
run_row(const char *driver, const struct row *r, int seq)
|
|
{
|
|
char tmpdir[256], src[256], base[64], outbin[512];
|
|
snprintf(tmpdir, sizeof tmpdir, "/tmp/dap_%d_d_%d", getpid(), seq);
|
|
snprintf(src, sizeof src, "%s/main769.ww", tmpdir);
|
|
snprintf(base, sizeof base, "main769");
|
|
mkdir(tmpdir, 0755);
|
|
if (write_source(src, r->src) != 0) {
|
|
cleanup_tmp(tmpdir, base);
|
|
return -1;
|
|
}
|
|
int rc = -1;
|
|
if (build_via_driver(driver, tmpdir, src) == 0) {
|
|
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
|
|
rc = runwait(outbin);
|
|
}
|
|
cleanup_tmp(tmpdir, base);
|
|
return rc;
|
|
}
|
|
|
|
/* asm_byte_identical — diff cstage vs wwstage .s. Parallel trees so
|
|
* ww_ww writing intermediates next to the source doesn't clobber the
|
|
* cstage .s (CLAUDE.md rule 14 phase split). */
|
|
static int
|
|
asm_byte_identical(const char *cdrv, const char *wdrv,
|
|
const struct row *r, int seq)
|
|
{
|
|
char src[256], tdc[256], tdw[256], base[64], cs[512], ws[512];
|
|
snprintf(tdc, sizeof tdc, "/tmp/dap_%d_c_%d", getpid(), seq);
|
|
snprintf(tdw, sizeof tdw, "/tmp/dap_%d_w_%d", getpid(), seq);
|
|
snprintf(base, sizeof base, "main769");
|
|
mkdir(tdc, 0755);
|
|
mkdir(tdw, 0755);
|
|
snprintf(src, sizeof src, "%s/main769.ww", tdc);
|
|
if (write_source(src, r->src) != 0) { cleanup_tmp(tdc, base); cleanup_tmp(tdw, base); return -1; }
|
|
int rc = -1;
|
|
if (build_via_driver(cdrv, tdc, src) != 0) goto out;
|
|
snprintf(cs, sizeof cs, "%s/%s.sepwork/__root.s", tdc, base);
|
|
|
|
snprintf(src, sizeof src, "%s/main769.ww", tdw);
|
|
if (write_source(src, r->src) != 0) goto out;
|
|
if (build_via_driver(wdrv, tdw, src) != 0) goto out;
|
|
snprintf(ws, sizeof ws, "%s/%s.sepwork/__root.s", tdw, base);
|
|
|
|
FILE *fc = fopen(cs, "rb");
|
|
FILE *fw = fopen(ws, "rb");
|
|
if (fc && fw) {
|
|
rc = 0;
|
|
for (;;) {
|
|
int a = fgetc(fc);
|
|
int b = fgetc(fw);
|
|
if (a != b) { rc = -1; break; }
|
|
if (a == EOF) break;
|
|
}
|
|
}
|
|
if (fc) fclose(fc);
|
|
if (fw) fclose(fw);
|
|
out:
|
|
cleanup_tmp(tdc, base);
|
|
cleanup_tmp(tdw, base);
|
|
return rc;
|
|
}
|
|
|
|
int
|
|
main(void)
|
|
{
|
|
const char *bin = getenv("BIN");
|
|
if (!bin) bin = "out/bin";
|
|
char absbin[1024];
|
|
if (bin[0] != '/') {
|
|
char cwd[1024];
|
|
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
|
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
|
|
bin = absbin;
|
|
}
|
|
|
|
char cdrv[1024], wdrv[1024];
|
|
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
|
|
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
|
|
|
|
int n = (int)(sizeof rows / sizeof rows[0]);
|
|
int total = 0, fail = 0;
|
|
int wwpresent = (access(wdrv, X_OK) == 0);
|
|
int seq = 0;
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
if (rows[i].stage_mask & STAGE_CS) {
|
|
total++;
|
|
int got = run_row(cdrv, &rows[i], seq++);
|
|
if (got != rows[i].expected_exit) {
|
|
fprintf(stderr,
|
|
"dot_aliased_ptr[cstage run][%s]: exit=%d want=%d\n",
|
|
rows[i].label, got, rows[i].expected_exit);
|
|
fail++;
|
|
}
|
|
}
|
|
if (wwpresent && (rows[i].stage_mask & STAGE_WW)) {
|
|
total++;
|
|
int got = run_row(wdrv, &rows[i], seq++);
|
|
if (got != rows[i].expected_exit) {
|
|
fprintf(stderr,
|
|
"dot_aliased_ptr[wwstage run][%s]: exit=%d want=%d\n",
|
|
rows[i].label, got, rows[i].expected_exit);
|
|
fail++;
|
|
}
|
|
total++;
|
|
if (asm_byte_identical(cdrv, wdrv, &rows[i], seq++) != 0) {
|
|
fprintf(stderr,
|
|
"dot_aliased_ptr[byte-id][%s]: cstage vs wwstage asm differs\n",
|
|
rows[i].label);
|
|
fail++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!wwpresent)
|
|
fprintf(stderr, "dot_aliased_ptr: skip wwstage (no %s)\n", wdrv);
|
|
|
|
if (fail) {
|
|
fprintf(stderr, "dot_aliased_ptr: %d/%d fixtures failed\n",
|
|
fail, total);
|
|
return 1;
|
|
}
|
|
printf("dot_aliased_ptr: %d/%d ok\n", total, total);
|
|
return 0;
|
|
}
|