w6c+w6c_ww: box [N]tagged array-literal elements via the tagged-store path (fix #12)

A [N]tagged-union array-literal element fell through the is_agg
multi-word-copy path (STRUCT/ARRAY/TUPLE/str/slice only) to the scalar
1-word store: the raw value landed in word 0 (the tag slot) with no tag
written and no payload boxed, so a later match found no variant. Both
stages under-copied identically, so the copy-depth bug was byte-id-blind
— a stride-only fix would still store 1 word and pass the gate green on
both-wrong.

Route each tagged element through cg_widen_tagged_store / the N_LET "BP"
tagged-store wrapper — the same choke-point let-init, vararg gather and
struct-field stores already use — so boxing, tag-remap and zero-pad-to-
slot come for free. esz now comes from the stamped slot size (rule-13);
the wwstage narrow override only covered widths 1/2/4, leaving a 16/24B
tagged element on the wrong 8-byte sentinel stride. rule-7 loud-stops
the unwired `[N]tagged=[x...]` repeat-fill (the widen call consumes the
node and trashes AX).

test/wcc/685: table-driven runtime readback (106/42/13) + a build-fail
row for the repeat-fill loud-stop, both stages.
This commit is contained in:
2026-06-03 20:32:22 +09:00
parent 3f04ea15ef
commit 63142770de
6 changed files with 475 additions and 3 deletions

View File

@@ -0,0 +1,382 @@
/*
* 685_arr_tagged_elem — cstage and wwstage agree, byte-for-byte and at
* runtime, that a `[N]tagged` array-LITERAL init boxes the FULL tag+
* payload slot of every element (task #12, the #270 aggregate-element-
* store family's tagged-union arm; sibling of #20's str/slice arm in
* test 683).
*
* The bug: cgen's N_ARRLIT per-element store excluded TY_TAGGED from the
* is_agg predicate, so a tagged element fell through to the scalar
* 1-word MOVQ — it stored the raw scalar (99) into WORD0 (the TAG slot),
* with NO tag word and NO payload placement. A subsequent `match` found
* no variant (tag was the payload value, not a variant index) and the
* arms silently produced 0. wwstage was worse: esz stayed the 8 sentinel
* for a 16B tagged element (the #8-class stride trap), so element i+1
* overran element i's tail. The under-copy DEPTH (1 word vs full slot)
* is byte-id-blind — both stages stored 1 word, so the asm-byte-id gate
* passed green on both-wrong; only the runtime readback below catches it.
*
* The fix (BOTH stages, converged byte-identical): route each tagged
* element through the SAME cg_widen_tagged_store / cgwidentaggedstore
* choke-point every other tagged store uses (let-init, vararg gather,
* struct-field) — NOT a new is_agg branch (is_agg word-copies/fatals and
* never boxes the tag). cstage adds is_tagged_el = esubu->kind ==
* TY_TAGGED; wwstage adds istaggedel and sizes esz from the stamped
* tinfo (esubti.size, rule-13) so the 16B/24B stride is correct.
*
* row | shape | want
* ----------------+-------------------------------------+-----------
* lit_sum106 | [2]cell=[99i64,7i64] (cell= | 106 (ken
* | (i64|bool)); match+sum both. | oracle)
* lit_first | same; match t[0] only. | 99
* lit_second | same; match t[1] only. | 7
* bool_variant | [2]cell=[5i64, true]; match t[1]. | 1 pins the
* | The bool arm only fires if the tag | tag word
* | word was written (not payload-in- | was placed
* | word0). Pre-fix: 0 (no tag). |
* var_sum | [2]cell=[a,b] (a=40i64,b=2i64 | 42 variable-
* | idents); match+sum. Repro B. | source
* stride3 | [3]cell=[10i64,20i64,30i64]; match | 30 16B per-
* | t[2]. Exercises the per-element | element
* | slot stride (wwstage 8-sentinel | stride
* | overran into the wrong slot). |
* boxed_void | [3](i32|void)=[1i32,2i32,void]; | 13 bare-value
* | match: i32 arm sums payload, void | box: tag +
* | arm adds 10. 1+2+10. | payload +
* | | zero-pad
*
* The asm-byte-id rows pin the symmetric widen (cstage == wwstage). The
* under-copy itself is byte-id-blind (see above) — these rows guard the
* stride/op symmetry, the runtime rows guard the correctness.
*/
#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;
}
struct row { const char *label; const char *src; int want; };
static const struct row rows[] = {
{ "lit_sum106",
"package main;\n"
"type cell = (i64 | bool);\n"
"export fn main() i32 = {\n"
"\tlet t: [2]cell = [99i64, 7i64];\n"
"\tlet sum: i64 = 0i64;\n"
"\tlet i: i32 = 0;\n"
"\tfor (i < 2) {\n"
"\t\tmatch (t[i]) {\n"
"\t\tcase let v: i64 => { sum = sum + v; };\n"
"\t\tcase let z: bool => { sum = sum + 1i64; };\n"
"\t\t};\n"
"\t\ti = i + 1;\n"
"\t};\n"
"\treturn sum: i32;\n"
"};\n",
106 },
{ "lit_first",
"package main;\n"
"type cell = (i64 | bool);\n"
"export fn main() i32 = {\n"
"\tlet t: [2]cell = [99i64, 7i64];\n"
"\tmatch (t[0]) {\n"
"\tcase let v: i64 => { return v: i32; };\n"
"\tcase let z: bool => { return -1; };\n"
"\t};\n"
"};\n",
99 },
{ "lit_second",
"package main;\n"
"type cell = (i64 | bool);\n"
"export fn main() i32 = {\n"
"\tlet t: [2]cell = [99i64, 7i64];\n"
"\tmatch (t[1]) {\n"
"\tcase let v: i64 => { return v: i32; };\n"
"\tcase let z: bool => { return -1; };\n"
"\t};\n"
"};\n",
7 },
{ "bool_variant",
"package main;\n"
"type cell = (i64 | bool);\n"
"export fn main() i32 = {\n"
"\tlet t: [2]cell = [5i64, true];\n"
"\tmatch (t[1]) {\n"
"\tcase let v: i64 => { return -1; };\n"
"\tcase let z: bool => { if (z) { return 1; }; return 0; };\n"
"\t};\n"
"};\n",
1 },
{ "var_sum",
"package main;\n"
"type cell = (i64 | bool);\n"
"export fn main() i32 = {\n"
"\tlet a: cell = 40i64;\n"
"\tlet b: cell = 2i64;\n"
"\tlet t: [2]cell = [a, b];\n"
"\tlet sum: i64 = 0i64;\n"
"\tlet i: i32 = 0;\n"
"\tfor (i < 2) {\n"
"\t\tmatch (t[i]) {\n"
"\t\tcase let v: i64 => { sum = sum + v; };\n"
"\t\tcase let z: bool => { sum = sum + 100i64; };\n"
"\t\t};\n"
"\t\ti = i + 1;\n"
"\t};\n"
"\treturn sum: i32;\n"
"};\n",
42 },
{ "stride3",
"package main;\n"
"type cell = (i64 | bool);\n"
"export fn main() i32 = {\n"
"\tlet t: [3]cell = [10i64, 20i64, 30i64];\n"
"\tmatch (t[2]) {\n"
"\tcase let v: i64 => { return v: i32; };\n"
"\tcase let z: bool => { return -1; };\n"
"\t};\n"
"};\n",
30 },
{ "boxed_void",
"package main;\n"
"type ov = (i32 | void);\n"
"export fn main() i32 = {\n"
"\tlet t: [3]ov = [1i32, 2i32, void];\n"
"\tlet sum: i32 = 0;\n"
"\tlet i: i32 = 0;\n"
"\tfor (i < 3) {\n"
"\t\tmatch (t[i]) {\n"
"\t\tcase let v: i32 => { sum = sum + v; };\n"
"\t\tcase void => { sum = sum + 10; };\n"
"\t\t};\n"
"\t\ti = i + 1;\n"
"\t};\n"
"\treturn sum;\n"
"};\n",
13 },
};
/* rule-7 loud-stop: `[N]tagged = [x, ...]` repeat-fill is not wired
* (cg_widen_tagged_store consumes the node and trashes AX, so the
* repeat re-store would write garbage). Both stages must FAIL the
* build loudly, not silently emit a broken array. */
static const char *neg[] = {
"package main;\n"
"type cell = (i64 | bool);\n"
"export fn main() i32 = {\n"
"\tlet t: [3]cell = [99i64...];\n"
"\treturn 0;\n"
"};\n",
};
static int
run_driver(const char *driver, const struct row *r, int i)
{
char src[64], tmpdir[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/atag_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/atag_%d_d_%d", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null",
tmpdir, driver, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: build via %s failed\n",
r->label, driver);
unlink(src); rmdir(tmpdir);
return -1;
}
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
char outbin[128];
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
int got = runwait(outbin);
unlink(src); unlink(outbin); rmdir(tmpdir);
return got;
}
/* build_should_fail — a `[N]tagged=[x,...]` repeat-fill must error on
* `driver` (rule-7 loud-stop); returns 0 when the build correctly
* FAILS, non-zero when it wrongly succeeded. */
static int
build_should_fail(const char *driver, const char *src, int i)
{
char s[64], tmpdir[64], cmd[1024];
snprintf(s, sizeof s, "/tmp/atagn_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/atagn_%d_d_%d", getpid(), i);
FILE *f = fopen(s, "wb");
if (!f) return -1;
fputs(src, f);
fclose(f);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null",
tmpdir, driver, s);
int rc = runwait(cmd);
unlink(s);
const char *base = strrchr(s, '/');
base = base ? base + 1 : s;
char outbin[128];
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
unlink(outbin);
rmdir(tmpdir);
return rc == 0 ? -1 : 0; /* build must NOT succeed */
}
/* asm_byte_identical — generate .s via cstage's w6c and wwstage's
* w6c_ww and diff. The under-copy depth is byte-id-blind (both stages
* stored 1 word pre-fix), so this row guards only the symmetric widen
* stride/op; the runtime rows above are the correctness net. */
static int
asm_byte_identical(const char *bin, const struct row *r, int i)
{
char src[64], cs[64], ws[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/atag_asm_%d_%d.ww", getpid(), i);
snprintf(cs, sizeof cs, "/tmp/atag_asm_%d_%d_c.s", getpid(), i);
snprintf(ws, sizeof ws, "/tmp/atag_asm_%d_%d_w.s", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c errored\n", r->label);
unlink(src);
return -1;
}
snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null",
bin, ws, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label);
unlink(src); unlink(cs);
return -1;
}
FILE *fc = fopen(cs, "rb");
FILE *fw = fopen(ws, "rb");
int rc = 0;
if (!fc || !fw) {
rc = -1;
} else {
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);
if (rc != 0)
fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n",
r->label);
unlink(src); unlink(cs); unlink(ws);
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];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
char wdrv[1024];
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
struct { const char *name; const char *path; int gated_on_existence; }
drivers[] = {
{ "cstage", cdrv, 0 },
{ "wwstage", wdrv, 1 },
{ NULL, NULL, 0 },
};
int n = (int)(sizeof rows / sizeof rows[0]);
int nn = (int)(sizeof neg / sizeof neg[0]);
int total = 0, fail = 0;
for (int d = 0; drivers[d].name; d++) {
if (drivers[d].gated_on_existence
&& access(drivers[d].path, X_OK) != 0) {
fprintf(stderr, "arr_tagged_elem: skip %s (no %s)\n",
drivers[d].name, drivers[d].path);
continue;
}
for (int i = 0; i < n; i++) {
int got = run_driver(drivers[d].path, &rows[i], i);
total++;
if (got != rows[i].want) {
fprintf(stderr,
"arr_tagged_elem[%s][%s]: exit=%d want=%d\n",
drivers[d].name, rows[i].label,
got, rows[i].want);
fail++;
}
}
for (int i = 0; i < nn; i++) {
total++;
if (build_should_fail(drivers[d].path, neg[i],
100 + i) != 0) {
fprintf(stderr,
"arr_tagged_elem[%s][neg%d]: built ok, "
"expected a loud error (rule-7)\n",
drivers[d].name, i);
fail++;
}
}
}
if (access(wdrv, X_OK) == 0) {
for (int i = 0; i < n; i++) {
total++;
if (asm_byte_identical(bin, &rows[i], i) != 0)
fail++;
}
}
if (fail) {
fprintf(stderr,
"arr_tagged_elem: %d/%d fixtures failed\n", fail, total);
return 1;
}
printf("arr_tagged_elem: %d/%d ok\n", total, total);
return 0;
}