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:
356
test/wcc/694_tagged_store_intlit.c
Normal file
356
test/wcc/694_tagged_store_intlit.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user