Files
ww/test/wcc/769_dot_aliased_ptr.c
Hojun-Cho eb6083e54a test: retarget Pattern-B gates to sep .sepwork layout; 915 off private global (M4 E3, #93)
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).
2026-06-18 11:16:56 +09:00

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