Files
ww/test/wcc/771_widen_transitive.c
Hojun-Cho 4d44242363 wcc: reject transitive nested-tagged widen at type_assignable (#199 α)
cgen has no wrapped-slot layout — the tagged-union slot is universally
[tag:8B][payload:up_to_24B], single level. The recursive walk admitted
let r: (size|io.eof|io.error) = u for u: io.underread (transitively
in io.error.params); cg_tag_for_variant + taggedvariantindext don't
recurse, returned -1, defaulted to tag=0, and the slot read back as
variant 0 = size at runtime.

Restores SSoT inside the checker pair: is / as / match variant
lookup is already non-recursive (#198 sibling), and the LET-init /
return / assign arms now agree. Aligns DOWN to the leaner side
(rule-10 stage symmetry). ww-stricter than Hare; harec keeps the
drill at ref/harec/src/types.c:702-739 (#199b is the deferred
wrapped-slot layout port).

Pre-flight audit (drew mandate): zero transitive-widen sites in
lib/ + selfhost/ + cmd/ + examples/. No wrapper-tagged variant
(io.error, strconv.error, fmt.field) is used as a variant of a
wider union anywhere in bootstrap. Mechanical fix.

Escape hatch for callers: spread (...wrapper) inlines the wrapper's
flat variants into the parent set at parse time. Wwstage's gate
additionally preserves the recursive drill on op == TK_ELLIPSIS
because wwstage stays AST-keyed (cstage flattens at resolve_type).

771_widen_transitive: 5 rows (reject_transitive_widen,
spread_alt_widen, direct_flat_variant, branched_callee_widen,
wrapper_typed_widen). Row 2 is CS-only — wwstage's is / match on
spread-expanded variants is open-bug #190/#198.
2026-05-29 03:04:18 +09:00

315 lines
9.7 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 771_widen_transitive — project #199 (α): cstage type_assignable +
* wwstage isassignable must REJECT transitive nested-tagged widen.
* Cgen has no wrapped-slot layout (#199b deferred future-work); the
* pre-α recursive `type_assignable(p->type, src)` walk admitted
* `let r: (size|io.eof|io.error) = u` for u: io.underread, then cgen
* silently miscompiled (cg_tag_for_variant returned -1 → tag=0 → r
* reads as variant 0 = size at runtime).
*
* Fix (cmd/wcc/type.c:308 + selfhost/cmd/wcc/check.ww:3063): the
* tagged-non-tagged arm now matches direct variants ONLY. NAMED-tagged
* wrapper variants (vu.kind == TY_TAGGED) compare nominally (type_eq /
* typeeqast); other variants keep the leaf recursion for primitive
* compat / untyped resolution. wwstage additionally preserves the
* spread-recursion path: `...wrapper` variants (op == TK_ELLIPSIS) drill
* through because the AST list hasn't pre-flattened (cstage flattens at
* resolve_type, wwstage stays AST-keyed).
*
* Restores SSoT inside the checker pair: `is` / `as` / `match` arm
* lookup is already non-recursive (#198 sibling), and the LET-init /
* return / assign arms now agree. Aligns DOWN to the leaner side
* (rule-10 stage symmetry). ww-stricter than Hare; harec keeps the
* drill at ref/harec/src/types.c:702-739 (#199b — wrapped-slot layout
* port deferred per drew).
*
* Coverage (5 rows):
* 1. reject_transitive_widen — `let r: (size|wrapper) = u` with
* u: underread, wrapper = !(underread | nomem). BOTH stages
* MUST reject at the checker. The repro.
* 2. spread_alt_widen — `let r: (size|...wrapper) = u;`. The
* `...` spread inlines wrapper's variants into the parent flat
* set, so underread becomes a DIRECT variant; widen succeeds.
* CS-only runtime: wwstage's `is`/`match` cross-spread variant
* lookup is open-bug #190/#198 (wwstage walks original AST list,
* doesn't see expanded variants); byte-id still gates the asm.
* 3. direct_flat_variant — `let r: (size|underread|nomem) = u`
* with the underlying variants flat in the parent (no wrapper).
* Regression gate: direct widen unchanged by the fix.
* 4. branched_callee_widen — fn returns (size|underread|nomem),
* runtime picks between two flat-variant returns. Verifies the
* cg_widen_tagged_store scalar branch's tag synthesis is
* unchanged for direct widens.
* 5. wrapper_typed_widen — `let r: wrapper = u` with u:
* underread (DIRECT variant of wrapper). Regression gate: the
* single-level wrapper widen is the supported escape hatch and
* MUST continue working.
*
* Per-row gates: cstage compile + runtime, wwstage compile + runtime,
* cs.s == ww.s byte-identical (rule-10). Row 1 uses
* expected_exit = -2 to indicate "build must fail" — the driver
* invocation is expected to return non-zero from the checker.
*/
#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
#define EXPECT_REJECT (-2)
struct row {
const char *label;
const char *src;
int expected_exit; /* EXPECT_REJECT means build must fail */
int stage_mask;
int byte_id; /* run cs.s == ww.s diff (works rows only) */
};
static const struct row rows[] = {
{ "reject_transitive_widen",
"package main;\n"
"type underread = !i32;\n"
"type nomem = !void;\n"
"type wrapper = !(underread | nomem);\n"
"export fn main() i32 = {\n"
" let u: underread;\n"
" let r: (size | wrapper) = u;\n"
" return 0;\n"
"};\n",
EXPECT_REJECT,
STAGE_CS | STAGE_WW, 0 },
{ "spread_alt_widen",
"package main;\n"
"type underread = !i32;\n"
"type nomem = !void;\n"
"type wrapper = !(underread | nomem);\n"
"export fn main() i32 = {\n"
" let u: underread;\n"
" let r: (size | ...wrapper) = u;\n"
" if (r is underread) { return 70; };\n"
" return 99;\n"
"};\n",
70,
STAGE_CS, 0 },
{ "direct_flat_variant",
"package main;\n"
"type underread = !i32;\n"
"type nomem = !void;\n"
"export fn main() i32 = {\n"
" let u: underread;\n"
" let r: (size | underread | nomem) = u;\n"
" if (r is underread) { return 71; };\n"
" return 99;\n"
"};\n",
71,
STAGE_CS | STAGE_WW, 1 },
{ "branched_callee_widen",
"package main;\n"
"type underread = !i32;\n"
"type nomem = !void;\n"
"fn pick(b: bool) (size | underread | nomem) = {\n"
" if (b) { let u: underread; return u; };\n"
" return 42: size;\n"
"};\n"
"export fn main() i32 = {\n"
" let r: (size | underread | nomem) = pick(true);\n"
" if (r is underread) { return 72; };\n"
" return 99;\n"
"};\n",
72,
STAGE_CS | STAGE_WW, 1 },
{ "wrapper_typed_widen",
"package main;\n"
"type underread = !i32;\n"
"type nomem = !void;\n"
"type wrapper = !(underread | nomem);\n"
"export fn main() i32 = {\n"
" let u: underread;\n"
" let r: wrapper = u;\n"
" if (r is underread) { return 73; };\n"
" return 99;\n"
"};\n",
73,
STAGE_CS | STAGE_WW, 1 },
};
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/wt_%d_d_%d", getpid(), seq);
snprintf(src, sizeof src, "%s/main771.ww", tmpdir);
snprintf(base, sizeof base, "main771");
mkdir(tmpdir, 0755);
if (write_source(src, r->src) != 0) {
cleanup_tmp(tmpdir, base);
return -1;
}
int rc;
int br = build_via_driver(driver, tmpdir, src);
if (r->expected_exit == EXPECT_REJECT) {
/* Build must fail with a non-zero exit (checker error). */
rc = (br != 0) ? r->expected_exit : 0;
} else if (br == 0) {
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
rc = runwait(outbin);
} else {
rc = -1;
}
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/wt_%d_c_%d", getpid(), seq);
snprintf(tdw, sizeof tdw, "/tmp/wt_%d_w_%d", getpid(), seq);
snprintf(base, sizeof base, "main771");
mkdir(tdc, 0755);
mkdir(tdw, 0755);
snprintf(src, sizeof src, "%s/main771.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.s", tdc, base);
snprintf(src, sizeof src, "%s/main771.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.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,
"widen_transitive[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,
"widen_transitive[wwstage run][%s]: exit=%d want=%d\n",
rows[i].label, got, rows[i].expected_exit);
fail++;
}
if (rows[i].byte_id) {
total++;
if (asm_byte_identical(cdrv, wdrv, &rows[i], seq++) != 0) {
fprintf(stderr,
"widen_transitive[byte-id][%s]: cstage vs wwstage asm differs\n",
rows[i].label);
fail++;
}
}
}
}
if (!wwpresent)
fprintf(stderr, "widen_transitive: skip wwstage (no %s)\n", wdrv);
if (fail) {
fprintf(stderr, "widen_transitive: %d/%d fixtures failed\n",
fail, total);
return 1;
}
printf("widen_transitive: %d/%d ok\n", total, total);
return 0;
}