selfhost: parser tsuffix plumb + cgen N_UN peel for tagged-store variant index (closes #32)

Typed-int literal assigned into a tagged-union slot (`h.e = 42i64;` where
e: (i32 | i64)) wrote tag = 0 (the i32 slot) instead of tag = 1 (the i64
slot). Cstage was correct: parse.c parseprimary copies tok.tsuffix onto
N_INTLIT, check.c stamps node.type = ty_i64, and cg_widen_tagged_store →
cg_tag_for_variant walks variants matching by structural type_eq —
ty_i64 lands at index 1. Wwstage had two gaps:

1. The parser (lib/ww/parse/expr.ww parseprimary) read p.curuval and
   p.curtext from the current token but never the tsuffix field. Token-
   side capture has been in place since the lexer's `i8/i16/.../u64/f32/
   f64` glue suffix landed (lib/ww/lex/lex.ww sets out.tsuffix); the
   parser side was missed. So an N_INTLIT for `42i64` carried tsuffix=""
   into cgen. Mirror of cmd/wcc/parse.c parseprimary's `n->tsuffix =
   t.tsuffix` line. Same plumb for N_FLOATLIT.

2. Wwstage has no checker stage to stamp N_UN's type from its inner
   expression's type. `-42i64` parses as N_UN(MINUS, N_INTLIT(42,
   tsuffix="i64")) and rhstargetname stopped at N_UN, returning "" and
   falling through to taggedvariantindex's "first non-str variant"
   fallback — which picked tag 0 (i32) for any numeric rhs in an
   (i32|i64) union. Cstage's cunop returns the inner type for
   TK_MINUS / TK_PLUS / TK_TILDE so the N_UN gets ty_i64 stamped
   naturally; wwstage gets the equivalent via an explicit peel in
   rhstargetname, recursing into rhs.lhs for these three ops. The
   recursion also covers nested unary (`- -42i64`), which parseunary
   builds as N_UN over N_UN over N_INTLIT.

The lib/ww/parse change is mirrored in selfhost/cmd/{w6c,wwdump}/
main.combined.ww so the bootstrap snapshot stays consistent with the
working frontend source. parser.curtsuffix is a new str field; refill
copies t.tsuffix into it; parseprimary TK_INT / TK_FLOAT copy it onto
the new node before advance.

Cstage handled both `42i64` and `-42i64` correctly already; no cstage
mirror needed.

Test 694_tagged_store_intlit — eleven rows running on both stages: i64
lit in (i32|i64); i32 lit (existing-working pin); i64 lit in
(i32|i64|str) with the str fallback at tail; u8 lit at head of
(u8|i32|i64); i64 lit at tail of (u8|i32|i64) with a +100 marker so
mis-binding into u8 can't masquerade as success; negative-i64 lit
(N_UN MINUS peel + sign extension through match-arm bind);
unary-plus i64 lit (N_UN PLUS peel); bitwise-not i64 lit (N_UN TILDE
peel; `~0i64 == -1i64`); nested unary `- -42i64` (recursion through
two N_UN levels); direct `let x: ev = 42i64;` (cglet's tagged-init
code path, separate write site from cgassign's field-write);
negative-control str field (pins the existing str-fallback path
through rhstargetname).

Pre-fix run on wwstage: 8/11 rows fail (every typed-i64 case including
all three unary operators, nested unary, and the direct let-init);
cstage 11/11 pass. Post-fix: 22/22 across both stages. make test
41/41. Bootstrap ww2 == ww3 == ww4 byte-identical.
This commit is contained in:
2026-05-15 01:28:52 +09:00
parent bacbf4b845
commit 19fb3a3b3c
7 changed files with 459 additions and 0 deletions

View File

@@ -0,0 +1,356 @@
/*
* 694_tagged_store_intlit — typed-int literal assigned into a tagged-
* union slot must select the variant index matching the literal's
* type suffix, not "first non-str variant". Read-side counterpart is
* 693_dot_tagged_source (covers the AX/DX/CX/R8 ABI on reads); this
* test is the symmetric write-side.
*
* cstage parser stamps N_INTLIT.tsuffix from the lexer (parse.c
* parseprimary TK_INT), then the checker stamps node.type = ty_i64
* for `42i64`. cg_widen_tagged_store calls cg_tag_for_variant(du,
* src->type), which walks variants and matches structurally via
* type_eq(ty_i64, vt) — exact, picking the i64 slot.
*
* wwstage parser dropped tsuffix on the floor (parseprimary in
* lib/ww/parse/expr.ww never copied curtsuffix → node), and there is
* no separate checker stage to re-stamp. cgwidentaggedstore →
* taggedvariantindex → rhstargetname inspected node.tsuffix and
* always saw "", then fell through to "first variant whose
* isstr matches the rhs". For a numeric rhs in (i32 | i64) that's
* always tag 0 (i32). The slot got the i32 tag but stored 8 bytes
* of value, so a match arm on i64 was bypassed entirely and the i32
* arm bound a truncated value.
*
* `-42i64` (N_UN MINUS over N_INTLIT) is an additional gap: even
* after plumbing tsuffix, rhstargetname returned "" for N_UN because
* it didn't peel sign/bitwise-not unary operators. cstage's checker
* stamps N_UN's type from cunop's inner walk so the inner i64
* propagates naturally; wwstage needs an explicit peel to match.
*
* Closed in task #10 by:
* - lib/ww/parse/parse.ww + selfhost/cmd/{w6c,wwdump}/main.combined.ww:
* parser struct gains curtsuffix; refill copies t.tsuffix into it.
* - lib/ww/parse/expr.ww + the combined mirrors: parseprimary TK_INT
* / TK_FLOAT branches copy curtsuffix into the new node before
* advance, mirroring cmd/wcc/parse.c parseprimary.
* - selfhost/cmd/wcc/cgenutil.ww + the combined mirrors: rhstargetname
* peels N_UN of TK_MINUS / TK_PLUS / TK_TILDE and recurses, mirroring
* cstage's cunop returning the inner type for these ops.
*
* Coverage — typed literals across variant orderings ((i32|i64),
* (i32|i64|str) for tail-position str fallback, (u8|i32|i64) for
* head- and tail-position numerics), a negated i64 literal to exercise
* the N_UN peel plus sign extension through the match-arm bind, and a
* direct `let x: (i32|i64) = 42i64;` to fire cglet's tagged-init
* (separate write-site from the field-assign path). One negative
* control pins the str-fallback path so the fix doesn't shadow it.
*/
#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[] = {
/* (i32 | i64) with `42i64` rhs — tag must be 1, value 42. Pre-fix
* wwstage wrote tag = 0 and the i64 arm was bypassed; the i32
* arm bound the low 32 bits and returned that. */
{ "field_i64_in_i32_i64",
"type ev = (i32 | i64);\n"
"type holder = struct { e: ev, mark: i32 };\n"
"fn main() i32 = {\n"
" let h: holder;\n"
" h.mark = 99;\n"
" h.e = 42i64;\n"
" match (h.e) {\n"
" case let v: i64 => { return v: i32; };\n"
" case let z: i32 => { return -1; };\n"
" };\n"
"};\n",
42 },
/* (i32 | i64) with `42i32` rhs — tag must be 0. Pins that the
* fix doesn't flip the existing-working i32 case. */
{ "field_i32_in_i32_i64",
"type ev = (i32 | i64);\n"
"type holder = struct { e: ev, mark: i32 };\n"
"fn main() i32 = {\n"
" let h: holder;\n"
" h.mark = 99;\n"
" h.e = 42i32;\n"
" match (h.e) {\n"
" case let v: i64 => { return -1; };\n"
" case let z: i32 => { return z; };\n"
" };\n"
"};\n",
42 },
/* (i32 | i64 | str) — same i64 lit, but the variant list now has
* a trailing str. Without tsuffix plumbing the fallback also has
* to skip past i32 (visstr == wantstr=false matches i32 first);
* with tsuffix it goes straight to tag 1. */
{ "field_i64_in_i32_i64_str",
"type ev = (i32 | i64 | str);\n"
"type holder = struct { e: ev, mark: i32 };\n"
"fn main() i32 = {\n"
" let h: holder;\n"
" h.mark = 99;\n"
" h.e = 1234567890i64;\n"
" match (h.e) {\n"
" case let v: i64 => { return (v - 1234567848i64): i32; };\n"
" case let z: i32 => { return -1; };\n"
" case let s: str => { return -2; };\n"
" };\n"
"};\n",
42 },
/* (u8 | i32 | i64) — typed lit at head of variant list (u8). Pre-
* fix the fallback picked tag 0 (u8, the first numeric) by accident
* regardless of the rhs suffix; this row would coincidentally pass
* for u8 but mask the bug. We verify u8 binds the right value AND
* the high 56 bits weren't smeared from the 8B store. */
{ "field_u8_in_u8_i32_i64",
"type ev = (u8 | i32 | i64);\n"
"type holder = struct { e: ev, mark: i32 };\n"
"fn main() i32 = {\n"
" let h: holder;\n"
" h.mark = 99;\n"
" h.e = 7u8;\n"
" match (h.e) {\n"
" case let b: u8 => { return b: i32; };\n"
" case let z: i32 => { return -1; };\n"
" case let v: i64 => { return -2; };\n"
" };\n"
"};\n",
7 },
/* (u8 | i32 | i64) with i64 lit — tag must be 2 (last). Pre-fix
* fell through to tag 0 (u8); the u8 arm bound the low byte (42)
* and the test would return 42 for the WRONG reason. We bind in
* the i64 arm and add 100 to distinguish: 42+100 = 142 only if
* the i64 arm fired. The u8 arm returns -1. */
{ "field_i64_in_u8_i32_i64",
"type ev = (u8 | i32 | i64);\n"
"type holder = struct { e: ev, mark: i32 };\n"
"fn main() i32 = {\n"
" let h: holder;\n"
" h.mark = 99;\n"
" h.e = 42i64;\n"
" match (h.e) {\n"
" case let b: u8 => { return -1; };\n"
" case let z: i32 => { return -2; };\n"
" case let v: i64 => { return (v + 100i64): i32; };\n"
" };\n"
"};\n",
142 },
/* Negative-int literal in (i32 | i64). `-42i64` parses as N_UN
* MINUS over N_INTLIT(42, tsuffix="i64"). The N_UN peel in
* rhstargetname routes through to "i64" so the i64 variant is
* selected; the value side must store full 8B so the bind reads
* back -42 (not 0xFFFFFFFFFFFFFFD6 truncated to 0xFFFFFFD6 or
* any other partial-extend nonsense). Returns 42 only if the
* i64 arm fires AND v == -42i64. */
{ "field_negative_i64",
"type ev = (i32 | i64);\n"
"type holder = struct { e: ev, mark: i32 };\n"
"fn main() i32 = {\n"
" let h: holder;\n"
" h.mark = 99;\n"
" h.e = -42i64;\n"
" match (h.e) {\n"
" case let v: i64 => {\n"
" if (v != -42i64) { return 50; };\n"
" return (0i64 - v): i32;\n"
" };\n"
" case let z: i32 => { return -1; };\n"
" };\n"
"};\n",
42 },
/* Unary `+` over a typed-i64 literal. Parses as N_UN(PLUS,
* N_INTLIT(42, "i64")); cstage cunop returns the inner type so
* `+42i64: i64`. Wwstage's rhstargetname peel covers TK_PLUS
* alongside TK_MINUS — pin it. Without the peel rhstargetname
* stops at N_UN and the i32 fallback fires; this row would
* return -1 pre-fix. */
{ "field_plus_unary_i64",
"type ev = (i32 | i64);\n"
"type holder = struct { e: ev, mark: i32 };\n"
"fn main() i32 = {\n"
" let h: holder;\n"
" h.mark = 99;\n"
" h.e = +42i64;\n"
" match (h.e) {\n"
" case let v: i64 => { return v: i32; };\n"
" case let z: i32 => { return -1; };\n"
" };\n"
"};\n",
42 },
/* Unary `~` (bitwise not) over a typed-i64 literal. Parses as
* N_UN(TILDE, N_INTLIT(0, "i64")); cstage cunop returns the
* inner type for TK_TILDE so `~0i64: i64`. Mirrors the peel's
* third op. `~0i64 == -1i64`; we bind in the i64 arm and check
* the value to rule out a half-store. */
{ "field_tilde_unary_i64",
"type ev = (i32 | i64);\n"
"type holder = struct { e: ev, mark: i32 };\n"
"fn main() i32 = {\n"
" let h: holder;\n"
" h.mark = 99;\n"
" h.e = ~0i64;\n"
" match (h.e) {\n"
" case let v: i64 => {\n"
" if (v != -1i64) { return 50; };\n"
" return 42;\n"
" };\n"
" case let z: i32 => { return -1; };\n"
" };\n"
"};\n",
42 },
/* Nested unary `- -42i64`. Parses as N_UN(MINUS, N_UN(MINUS,
* N_INTLIT(42, "i64"))) since parseunary recurses on the
* operand. The peel must recurse too; without recursion the
* outer N_UN's lhs is another N_UN and rhstargetname falls
* through to "". Numerically the value is 42, so a wrong
* variant binding can only be detected via the arm we hit. */
{ "field_nested_unary_i64",
"type ev = (i32 | i64);\n"
"type holder = struct { e: ev, mark: i32 };\n"
"fn main() i32 = {\n"
" let h: holder;\n"
" h.mark = 99;\n"
" h.e = - -42i64;\n"
" match (h.e) {\n"
" case let v: i64 => { return v: i32; };\n"
" case let z: i32 => { return -1; };\n"
" };\n"
"};\n",
42 },
/* Direct `let x: ev = 42i64;` — different write site than the
* struct-field assign (cglet's tagged-init branch in cgenstmt.ww,
* not cgassign's field-write branch in cgenexpr.ww). Both routes
* eventually call cgwidentaggedstore so taggedvariantindex must
* agree; this row pins the let-init path. */
{ "letinit_i64_in_i32_i64",
"type ev = (i32 | i64);\n"
"fn main() i32 = {\n"
" let x: ev = 42i64;\n"
" match (x) {\n"
" case let v: i64 => { return v: i32; };\n"
" case let z: i32 => { return -1; };\n"
" };\n"
"};\n",
42 },
/* Negative control: untagged str field still resolves through the
* isstr fallback. The new N_UN peel in rhstargetname must not
* shadow this path. Reads h.s.len. */
{ "untagged_str_fallback_regression",
"type ev = (i32 | str);\n"
"type holder = struct { e: ev, mark: i32 };\n"
"fn main() i32 = {\n"
" let h: holder;\n"
" h.mark = 99;\n"
" h.e = (\"hello\": ev);\n"
" match (h.e) {\n"
" case let s: str => { return s.len: i32; };\n"
" case let z: i32 => { return -1; };\n"
" };\n"
"};\n",
5 },
};
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/waew_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/waew_%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",
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;
}
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 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, "tagged_store_intlit: 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,
"tagged_store_intlit[%s][%s]: exit=%d want=%d\n",
drivers[d].name, rows[i].label,
got, rows[i].want);
fail++;
}
}
}
if (fail) {
fprintf(stderr,
"tagged_store_intlit: %d/%d fixtures failed\n", fail, total);
return 1;
}
printf("tagged_store_intlit: %d/%d ok\n", total, total);
return 0;
}