From 19fb3a3b3ccc14eaaec594e2e9a9d2ab2de2483a Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 15 May 2026 01:28:52 +0900 Subject: [PATCH] selfhost: parser tsuffix plumb + cgen N_UN peel for tagged-store variant index (closes #32) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 7 + lib/ww/parse/expr.ww | 8 + lib/ww/parse/parse.ww | 8 + selfhost/cmd/w6c/main.combined.ww | 32 +++ selfhost/cmd/wcc/cgenutil.ww | 16 ++ selfhost/cmd/wwdump/main.combined.ww | 32 +++ test/wcc/694_tagged_store_intlit.c | 356 +++++++++++++++++++++++++++ 7 files changed, 459 insertions(+) create mode 100644 test/wcc/694_tagged_store_intlit.c diff --git a/Makefile b/Makefile index f8dfb5d2..6d0fabc6 100644 --- a/Makefile +++ b/Makefile @@ -222,6 +222,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_dot_str_chained_arg \ $(BIN)/test_dot_slice_arg \ $(BIN)/test_dot_tagged_source \ + $(BIN)/test_tagged_store_intlit \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ $(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \ @@ -333,6 +334,12 @@ $(BIN)/test_dot_tagged_source: test/wcc/693_dot_tagged_source.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_tagged_store_intlit: test/wcc/694_tagged_store_intlit.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_field_signed: test/wcc/660_field_signed.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/lib/ww/parse/expr.ww b/lib/ww/parse/expr.ww index a474a213..3fd33331 100644 --- a/lib/ww/parse/expr.ww +++ b/lib/ww/parse/expr.ww @@ -25,6 +25,13 @@ fn parseprimary(p: *parser) *node = { let n: *node = newnode(p.a, nkind.N_INTLIT, pf, pl, pc); n.uval = p.curuval; n.str = p.curtext; + // Plumb the typed-int suffix (`42i64`, `3u8`) through to + // the node. Cgen's rhstargetname reads tsuffix to pick the + // matching tagged-union variant; without this, typed-int + // rhs of `h.e = 42i64;` falls through to the "first non-str + // variant" fallback and writes tag 0. Mirror of cmd/wcc/ + // parse.c parseprimary TK_INT. + n.tsuffix = p.curtsuffix; advance(p); return n; }; @@ -36,6 +43,7 @@ fn parseprimary(p: *parser) *node = { // don't need a float ABI to materialise the constant. n.uval = p.curuval; n.str = p.curtext; + n.tsuffix = p.curtsuffix; advance(p); return n; }; diff --git a/lib/ww/parse/parse.ww b/lib/ww/parse/parse.ww index 4df0539c..5cfd6a0f 100644 --- a/lib/ww/parse/parse.ww +++ b/lib/ww/parse/parse.ww @@ -31,6 +31,13 @@ type parser = struct { curtext: str, curuval: u64, curfval: f64, + // curtsuffix: typed numeric literal suffix ("i32", "u64", ...) on + // the current TK_INT / TK_FLOAT token, or empty. Parseprimary + // copies this onto the N_INTLIT / N_FLOATLIT node so cgen's + // rhstargetname can map `42i64` to the i64 variant of a tagged + // union without falling back to "first non-str variant" (which + // silently picked tag 0 for typed-int literals; see #10). + curtsuffix: str, }; fn refill(p: *parser) void = { @@ -43,6 +50,7 @@ fn refill(p: *parser) void = { p.curtext = t.text; p.curuval = t.uval; p.curfval = t.fval; + p.curtsuffix = t.tsuffix; }; export fn parserinit(p: *parser, a: *arena, l: *lex) void = { diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 07a7fde0..e9e24faa 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -2703,6 +2703,13 @@ fn parseprimary(p: *parser) *node = { let n: *node = newnode(p.a, nkind.N_INTLIT, pf, pl, pc); n.uval = p.curuval; n.str = p.curtext; + // Plumb the typed-int suffix (`42i64`, `3u8`) through to + // the node. Cgen's rhstargetname reads tsuffix to pick the + // matching tagged-union variant; without this, typed-int + // rhs of `h.e = 42i64;` falls through to the "first non-str + // variant" fallback and writes tag 0. Mirror of cmd/wcc/ + // parse.c parseprimary TK_INT. + n.tsuffix = p.curtsuffix; advance(p); return n; }; @@ -2714,6 +2721,7 @@ fn parseprimary(p: *parser) *node = { // don't need a float ABI to materialise the constant. n.uval = p.curuval; n.str = p.curtext; + n.tsuffix = p.curtsuffix; advance(p); return n; }; @@ -3752,6 +3760,13 @@ type parser = struct { curtext: str, curuval: u64, curfval: f64, + // curtsuffix: typed numeric literal suffix ("i32", "u64", ...) on + // the current TK_INT / TK_FLOAT token, or empty. Parseprimary + // copies this onto the N_INTLIT / N_FLOATLIT node so cgen's + // rhstargetname can map `42i64` to the i64 variant of a tagged + // union without falling back to "first non-str variant" (which + // silently picked tag 0 for typed-int literals; see #10). + curtsuffix: str, }; fn refill(p: *parser) void = { @@ -3764,6 +3779,7 @@ fn refill(p: *parser) void = { p.curtext = t.text; p.curuval = t.uval; p.curfval = t.fval; + p.curtsuffix = t.tsuffix; }; export fn parserinit(p: *parser, a: *arena, l: *lex) void = { @@ -7433,6 +7449,22 @@ fn rhstargetname(c: *cgen, rhs: *node) str = { let nm: str; nm.ptr = nil; nm.len = 0; if (rhs == nil) { return nm; }; + // Unary `-` / `+` / `~` inherit the inner expression's type: + // cstage's checker stamps N_UN's type from cunop's inner walk, + // so `-42i64` is ty_i64 there. Wwstage has no checker stage — + // peel the operator here so a typed-int literal under a sign + // reaches its tsuffix branch below instead of falling into + // taggedvariantindex's "first non-str variant" fallback. Mirror + // of cmd/wcc/check.c cunop TK_MINUS/PLUS/TILDE returning t. + if (rhs.kind == nkind.N_UN) { + let op: tkind = rhs.op; + if (op == tkind.TK_MINUS || op == tkind.TK_PLUS + || op == tkind.TK_TILDE) { + if (rhs.lhs != nil) { + return rhstargetname(c, rhs.lhs); + }; + }; + }; if (rhs.kind == nkind.N_CAST) { let t: *node = rhs.rhs; if (t != nil) { diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 3adbddff..13befef3 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -1863,6 +1863,22 @@ fn rhstargetname(c: *cgen, rhs: *node) str = { let nm: str; nm.ptr = nil; nm.len = 0; if (rhs == nil) { return nm; }; + // Unary `-` / `+` / `~` inherit the inner expression's type: + // cstage's checker stamps N_UN's type from cunop's inner walk, + // so `-42i64` is ty_i64 there. Wwstage has no checker stage — + // peel the operator here so a typed-int literal under a sign + // reaches its tsuffix branch below instead of falling into + // taggedvariantindex's "first non-str variant" fallback. Mirror + // of cmd/wcc/check.c cunop TK_MINUS/PLUS/TILDE returning t. + if (rhs.kind == nkind.N_UN) { + let op: tkind = rhs.op; + if (op == tkind.TK_MINUS || op == tkind.TK_PLUS + || op == tkind.TK_TILDE) { + if (rhs.lhs != nil) { + return rhstargetname(c, rhs.lhs); + }; + }; + }; if (rhs.kind == nkind.N_CAST) { let t: *node = rhs.rhs; if (t != nil) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 40ef4076..90ccb303 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -2703,6 +2703,13 @@ fn parseprimary(p: *parser) *node = { let n: *node = newnode(p.a, nkind.N_INTLIT, pf, pl, pc); n.uval = p.curuval; n.str = p.curtext; + // Plumb the typed-int suffix (`42i64`, `3u8`) through to + // the node. Cgen's rhstargetname reads tsuffix to pick the + // matching tagged-union variant; without this, typed-int + // rhs of `h.e = 42i64;` falls through to the "first non-str + // variant" fallback and writes tag 0. Mirror of cmd/wcc/ + // parse.c parseprimary TK_INT. + n.tsuffix = p.curtsuffix; advance(p); return n; }; @@ -2714,6 +2721,7 @@ fn parseprimary(p: *parser) *node = { // don't need a float ABI to materialise the constant. n.uval = p.curuval; n.str = p.curtext; + n.tsuffix = p.curtsuffix; advance(p); return n; }; @@ -3752,6 +3760,13 @@ type parser = struct { curtext: str, curuval: u64, curfval: f64, + // curtsuffix: typed numeric literal suffix ("i32", "u64", ...) on + // the current TK_INT / TK_FLOAT token, or empty. Parseprimary + // copies this onto the N_INTLIT / N_FLOATLIT node so cgen's + // rhstargetname can map `42i64` to the i64 variant of a tagged + // union without falling back to "first non-str variant" (which + // silently picked tag 0 for typed-int literals; see #10). + curtsuffix: str, }; fn refill(p: *parser) void = { @@ -3764,6 +3779,7 @@ fn refill(p: *parser) void = { p.curtext = t.text; p.curuval = t.uval; p.curfval = t.fval; + p.curtsuffix = t.tsuffix; }; export fn parserinit(p: *parser, a: *arena, l: *lex) void = { @@ -7433,6 +7449,22 @@ fn rhstargetname(c: *cgen, rhs: *node) str = { let nm: str; nm.ptr = nil; nm.len = 0; if (rhs == nil) { return nm; }; + // Unary `-` / `+` / `~` inherit the inner expression's type: + // cstage's checker stamps N_UN's type from cunop's inner walk, + // so `-42i64` is ty_i64 there. Wwstage has no checker stage — + // peel the operator here so a typed-int literal under a sign + // reaches its tsuffix branch below instead of falling into + // taggedvariantindex's "first non-str variant" fallback. Mirror + // of cmd/wcc/check.c cunop TK_MINUS/PLUS/TILDE returning t. + if (rhs.kind == nkind.N_UN) { + let op: tkind = rhs.op; + if (op == tkind.TK_MINUS || op == tkind.TK_PLUS + || op == tkind.TK_TILDE) { + if (rhs.lhs != nil) { + return rhstargetname(c, rhs.lhs); + }; + }; + }; if (rhs.kind == nkind.N_CAST) { let t: *node = rhs.rhs; if (t != nil) { diff --git a/test/wcc/694_tagged_store_intlit.c b/test/wcc/694_tagged_store_intlit.c new file mode 100644 index 00000000..4f8d9543 --- /dev/null +++ b/test/wcc/694_tagged_store_intlit.c @@ -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 +#include +#include +#include +#include +#include + +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; +}