wwstage: spill non-IDENT scrutinee at cgtypeassert (#200)

cgtypeassert kept scrutoff=0 when the scrutinee wasn't an N_IDENT
(direct call result, arr[i], p.field, ?, paren-wrap of any of those),
so the tag-load fell on (BP) — the saved-BP word — and the payload-
load on +8(BP) — the return address. The wwstage repro returned 220
(garbage from RIP) where cstage returned 42 (impl-e1-resume sibling
of #199/#201).

Mirror cstage cmd/w6c/cgen.c:6300-6316 N_TYPEASSERT non-IDENT arm.
Add an `else` branch after the existing N_IDENT path that resolves
the tagged type via matchscrutt, alloc an @asrt_spill slot via
matchspillsz/localalloc, cgexpr the LHS, then spill the AX/DX/CX
tagged-return-ABI words: AX→+0 (tag), DX→+8 (word0), CX→+16
(word1, guarded on spill > 16). Subsequent tag-check + payload load
indexes off the spill like the IDENT path. Helpers reused from
cgmatch (cgenexpr.ww:1422-1460).

cstage's cgtypeassert omits the cgmatch 4-word R8→+24 spill (rule-10
stage symmetry: rather than diverge into a 32B-payload case the test
suite doesn't exercise, mirror cstage exactly and file the cstage
omission inline). Filed inline: cstage cgtypeassert needs the same
R8→+24 path cgmatch already has (drew's design rationale, blocked
by the rule-10 floor today).

772_typeassert_nonident: 7 rows (call_as_size — the repro, call_as_str
— CX→+16 spill + BX post-load, call_as_namedvoid — void-variant
tag-check fires, payload load is a 0-byte no-op, call_as_fnptr —
fn-ptr variant 8B word0, call_as_u8 / call_as_i16 — narrow scalar
round-trip via MOVQ + MOVQ confirms no truncation, branched_call_as
— runtime-chosen tag). Each row gated on cstage runtime + wwstage
runtime + cs.s == ww.s byte-identity.
This commit is contained in:
2026-05-29 03:52:04 +09:00
parent 4d44242363
commit 6ce292b157
5 changed files with 444 additions and 0 deletions

View File

@@ -322,6 +322,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_dot_aliased_ptr \
$(BIN)/test_return_tagged_forward \
$(BIN)/test_widen_transitive \
$(BIN)/test_typeassert_nonident \
$(BIN)/test_use_promote_alias \
$(BIN)/test_field_signed $(BIN)/test_frame_argcount \
$(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \
@@ -660,6 +661,12 @@ $(BIN)/test_widen_transitive: test/wcc/771_widen_transitive.c \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_typeassert_nonident: test/wcc/772_typeassert_nonident.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_arrlit_str_full: test/wcc/711_arrlit_str_full.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \

View File

@@ -18010,6 +18010,41 @@ fn cgtypeassert(c: *cgen, n: *node) void = {
scrutoff = lc.off;
scrutt = resolvetagged(c, lc.tnode);
};
} else {
// Non-ident scrutinee (call result, arr[i], p.field, ?,
// etc.). Mirror cgmatch's spill (cgenexpr.ww:1422-1460)
// and cstage cmd/w6c/cgen.c:6300-6316: alloc an
// `@asrt_spill` slot sized via matchspillsz, evaluate
// the LHS, then copy the AX/DX/CX[/R8] return-ABI words
// into the slot so the tag-check + payload load indexes
// off memory like the IDENT path. Without this, scrutoff
// stayed 0 and the tag read fell on (BP) — the saved-BP
// word — and the payload read on +8(BP) — the return
// address. Bug #200.
scrutt = matchscrutt(c, lhs);
let spillsz: i32 = matchspillsz(c, scrutt);
scrutoff = localalloc(c, "@asrt_spill", spillsz, nil);
cgexpr(c, lhs);
emitline("\tMOVQ\tAX, ");
emitoff(scrutoff: i64);
emitline("(BP)\n");
if (!isnullabletype(scrutt)) {
emitline("\tMOVQ\tDX, ");
emitoff((scrutoff + 8): i64);
emitline("(BP)\n");
// Mirror cstage cmd/w6c/cgen.c:6313-6315: only CX
// → +16 when slot_size > 16. The 4-word case
// (R8 → +24, slot_size > 24) is the cgmatch shape
// (cgenexpr.ww:1454-1458, cmd/w6c/cgen.c:5988-5990)
// but cstage cgtypeassert omits it; preserve the
// asymmetry rather than diverge from rule 10
// byte-id. Filed inline as a cstage twin task.
if (spillsz > 16) {
emitline("\tMOVQ\tCX, ");
emitoff((scrutoff + 16): i64);
emitline("(BP)\n");
};
};
};
};
let want: i32 = cgtagvariantidx(c, scrutt, n.rhs);

View File

@@ -387,6 +387,41 @@ fn cgtypeassert(c: *cgen, n: *node) void = {
scrutoff = lc.off;
scrutt = resolvetagged(c, lc.tnode);
};
} else {
// Non-ident scrutinee (call result, arr[i], p.field, ?,
// etc.). Mirror cgmatch's spill (cgenexpr.ww:1422-1460)
// and cstage cmd/w6c/cgen.c:6300-6316: alloc an
// `@asrt_spill` slot sized via matchspillsz, evaluate
// the LHS, then copy the AX/DX/CX[/R8] return-ABI words
// into the slot so the tag-check + payload load indexes
// off memory like the IDENT path. Without this, scrutoff
// stayed 0 and the tag read fell on (BP) — the saved-BP
// word — and the payload read on +8(BP) — the return
// address. Bug #200.
scrutt = matchscrutt(c, lhs);
let spillsz: i32 = matchspillsz(c, scrutt);
scrutoff = localalloc(c, "@asrt_spill", spillsz, nil);
cgexpr(c, lhs);
emitline("\tMOVQ\tAX, ");
emitoff(scrutoff: i64);
emitline("(BP)\n");
if (!isnullabletype(scrutt)) {
emitline("\tMOVQ\tDX, ");
emitoff((scrutoff + 8): i64);
emitline("(BP)\n");
// Mirror cstage cmd/w6c/cgen.c:6313-6315: only CX
// → +16 when slot_size > 16. The 4-word case
// (R8 → +24, slot_size > 24) is the cgmatch shape
// (cgenexpr.ww:1454-1458, cmd/w6c/cgen.c:5988-5990)
// but cstage cgtypeassert omits it; preserve the
// asymmetry rather than diverge from rule 10
// byte-id. Filed inline as a cstage twin task.
if (spillsz > 16) {
emitline("\tMOVQ\tCX, ");
emitoff((scrutoff + 16): i64);
emitline("(BP)\n");
};
};
};
};
let want: i32 = cgtagvariantidx(c, scrutt, n.rhs);

View File

@@ -18010,6 +18010,41 @@ fn cgtypeassert(c: *cgen, n: *node) void = {
scrutoff = lc.off;
scrutt = resolvetagged(c, lc.tnode);
};
} else {
// Non-ident scrutinee (call result, arr[i], p.field, ?,
// etc.). Mirror cgmatch's spill (cgenexpr.ww:1422-1460)
// and cstage cmd/w6c/cgen.c:6300-6316: alloc an
// `@asrt_spill` slot sized via matchspillsz, evaluate
// the LHS, then copy the AX/DX/CX[/R8] return-ABI words
// into the slot so the tag-check + payload load indexes
// off memory like the IDENT path. Without this, scrutoff
// stayed 0 and the tag read fell on (BP) — the saved-BP
// word — and the payload read on +8(BP) — the return
// address. Bug #200.
scrutt = matchscrutt(c, lhs);
let spillsz: i32 = matchspillsz(c, scrutt);
scrutoff = localalloc(c, "@asrt_spill", spillsz, nil);
cgexpr(c, lhs);
emitline("\tMOVQ\tAX, ");
emitoff(scrutoff: i64);
emitline("(BP)\n");
if (!isnullabletype(scrutt)) {
emitline("\tMOVQ\tDX, ");
emitoff((scrutoff + 8): i64);
emitline("(BP)\n");
// Mirror cstage cmd/w6c/cgen.c:6313-6315: only CX
// → +16 when slot_size > 16. The 4-word case
// (R8 → +24, slot_size > 24) is the cgmatch shape
// (cgenexpr.ww:1454-1458, cmd/w6c/cgen.c:5988-5990)
// but cstage cgtypeassert omits it; preserve the
// asymmetry rather than diverge from rule 10
// byte-id. Filed inline as a cstage twin task.
if (spillsz > 16) {
emitline("\tMOVQ\tCX, ");
emitoff((scrutoff + 16): i64);
emitline("(BP)\n");
};
};
};
};
let want: i32 = cgtagvariantidx(c, scrutt, n.rhs);

View File

@@ -0,0 +1,332 @@
/*
* 772_typeassert_nonident — project #200: wwstage cgtypeassert non-IDENT
* scrutinee bug. The pre-fix `e as T` on a non-N_IDENT scrutinee (direct
* call result, arr[i], p.field, ?, paren-wrap of any of those) left
* scrutoff=0; the tag-load fell on (BP) — the saved-BP word — and the
* payload-load on +8(BP) — the return address — instead of the spilled
* tagged-return ABI words. wwstage produced garbage (exit 220 in the
* repro vs cstage's 42).
*
* Fix (selfhost/cmd/wcc/cgenexpr.ww:368-414 cgtypeassert): mirror cstage
* cmd/w6c/cgen.c:6300-6316 N_TYPEASSERT non-IDENT arm. Add an `else`
* branch after the existing N_IDENT path: resolve the tagged type via
* matchscrutt, alloc an `@asrt_spill` slot via matchspillsz/localalloc,
* cgexpr the LHS, then spill AX→+0 (tag), DX→+8 (word0), CX→+16
* (word1) guarded on spill > 16. Mirrors cgmatch's spill (cgenexpr.ww:
* 1422-1460). The R8→+24 path (cgmatch shape) is intentionally omitted
* to mirror cstage's cgtypeassert exactly (rule-10 byte-id); the cstage
* twin gap is filed inline as a sibling task.
*
* Coverage (7 rows):
* 1. call_as_size — `f() as size`. The repro shape: direct call
* result, basic-size payload. Pre-fix
* wwstage: garbage AX from (BP). Post-fix:
* 42.
* 2. call_as_str — `f() as str`. Non-IDENT, multi-word payload
* (AX=ptr / BX=len). Tests CX→+16 spill +
* the `isstrtype` post-spill BX load.
* 3. call_as_namedvoid — `f() as nvariant` where nvariant is a
* named-void alias. Tag-check fires; the
* payload load is a 0-byte no-op (void
* variant has size 0; no consumer reads AX).
* Drew-add.
* 4. call_as_fnptr — `f() as *fn(i32) i32`. Fn-ptr variant
* (8B word0 = pointer). Round-trips through
* spill→tag-check→MOVQ +8 → AX.
* 5. call_as_u8 — `f() as u8`. Narrow scalar. The callee
* zero/sign-extended the value to 8B on
* return; MOVQ store + MOVQ read round-trips
* it. Ken-add: confirms no narrow-truncation
* on the payload load.
* 6. call_as_i16 — `f() as i16`. Narrow signed scalar. Tests
* negative round-trip through the spill slot
* (callee sign-extended to 8B; MOVQ
* preserves). Ken-add sibling of row 5.
* 7. branched_call_as — callee returns a runtime-chosen variant
* (the success branch); `f(true) as size`.
* Tests that the spill+tag-check path
* doesn't lose the callee's selected tag.
*
* Per-row gates: cstage runtime exit, wwstage runtime exit, cs.s ==
* ww.s byte-identical (rule-10 stage symmetry).
*
* GATE POLARITY: must stay GREEN. A red on rows 1-4 or 7 means the
* spill path is broken (non-IDENT scrutinee returned to the BP/RIP
* read). Rows 5/6 red means narrow-scalar round-trip miscompiled at
* the payload load (would surface if a future SIB regresses MOVQ to a
* narrow load that drops the callee's extension).
*/
#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[] = {
{ "call_as_size",
"package main;\n"
"fn f(b: bool) (size | str) = {\n"
" if (b) { return 42: size; };\n"
" return \"x\";\n"
"};\n"
"export fn main() i32 = {\n"
" let n = f(true) as size;\n"
" return n: i32;\n"
"};\n",
42,
STAGE_CS | STAGE_WW },
{ "call_as_str",
"package main;\n"
"fn f(b: bool) (size | str) = {\n"
" if (b) { return \"AB\"; };\n"
" return 0: size;\n"
"};\n"
"export fn main() i32 = {\n"
" let s = f(true) as str;\n"
" return s.len: i32 + 40;\n"
"};\n",
42,
STAGE_CS | STAGE_WW },
{ "call_as_namedvoid",
"package main;\n"
"type nv = void;\n"
"fn f(b: bool) (size | nv) = {\n"
" if (b) { return void: nv; };\n"
" return 0: size;\n"
"};\n"
"export fn main() i32 = {\n"
" let _ = f(true) as nv;\n"
" return 42;\n"
"};\n",
42,
STAGE_CS | STAGE_WW },
{ "call_as_fnptr",
/* #193: explicit deref required to call a fn-ptr (`(*p)(args)`).
* The probe is wwstage cgtypeassert non-IDENT spill; deref-call
* shape is orthogonal. */
"package main;\n"
"fn g(x: i32) i32 = { return x + 1; };\n"
"fn f(b: bool) (*fn(x: i32) i32 | size) = {\n"
" if (b) { return &g; };\n"
" return 0: size;\n"
"};\n"
"export fn main() i32 = {\n"
" let p = f(true) as *fn(x: i32) i32;\n"
" return (*p)(41);\n"
"};\n",
42,
STAGE_CS | STAGE_WW },
{ "call_as_u8",
"package main;\n"
"fn f(b: bool) (u8 | str) = {\n"
" if (b) { return 42: u8; };\n"
" return \"x\";\n"
"};\n"
"export fn main() i32 = {\n"
" let n = f(true) as u8;\n"
" return n: i32;\n"
"};\n",
42,
STAGE_CS | STAGE_WW },
{ "call_as_i16",
"package main;\n"
"fn f(b: bool) (i16 | str) = {\n"
" if (b) { return -1: i16; };\n"
" return \"x\";\n"
"};\n"
"export fn main() i32 = {\n"
" let n = f(true) as i16;\n"
" if (n: i32 == -1) { return 42; };\n"
" return 1;\n"
"};\n",
42,
STAGE_CS | STAGE_WW },
{ "branched_call_as",
"package main;\n"
"fn f(b: bool) (size | str) = {\n"
" if (b) { return 42: size; };\n"
" return \"x\";\n"
"};\n"
"export fn main() i32 = {\n"
" let pick: bool = true;\n"
" let n = f(pick) as size;\n"
" return n: i32;\n"
"};\n",
42,
STAGE_CS | STAGE_WW },
};
static int
write_source(const char *path, const char *src)
{
FILE *f = fopen(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.s", 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);
rmdir(tmpdir);
}
static int
build_via_driver(const char *driver, const char *tmpdir, const char *src)
{
char cmd[1024];
snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s build %s 2>/dev/null",
tmpdir, driver, src);
return runwait(cmd);
}
static int
run_row(const char *driver, const struct row *r, int seq)
{
char tmpdir[256], src[512], base[64], outbin[768];
snprintf(tmpdir, sizeof tmpdir, "/tmp/tan_%d_d_%d", getpid(), seq);
snprintf(base, sizeof base, "main772");
snprintf(src, sizeof src, "%s/%s.ww", tmpdir, base);
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[512], tdc[256], tdw[256], base[64], cs[512], ws[512];
snprintf(tdc, sizeof tdc, "/tmp/tan_%d_c_%d", getpid(), seq);
snprintf(tdw, sizeof tdw, "/tmp/tan_%d_w_%d", getpid(), seq);
snprintf(base, sizeof base, "main772");
mkdir(tdc, 0755);
mkdir(tdw, 0755);
snprintf(src, sizeof src, "%s/%s.ww", tdc, base);
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.s", tdc, base);
snprintf(src, sizeof src, "%s/%s.ww", tdw, base);
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.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[512];
if (bin[0] != '/') {
char cwd[256];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cdrv[640], wdrv[640];
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,
"typeassert_nonident[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,
"typeassert_nonident[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,
"typeassert_nonident[byte-id][%s]: cstage vs wwstage asm differs\n",
rows[i].label);
fail++;
}
}
}
if (!wwpresent)
fprintf(stderr, "typeassert_nonident: skip wwstage (no %s)\n", wdrv);
if (fail) {
fprintf(stderr, "typeassert_nonident: %d/%d fixtures failed\n",
fail, total);
return 1;
}
printf("typeassert_nonident: %d/%d ok\n", total, total);
return 0;
}