diff --git a/Makefile b/Makefile index 3f85023c..db5eb7fc 100644 --- a/Makefile +++ b/Makefile @@ -244,6 +244,8 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_struct_multi_return_scratch \ $(BIN)/test_signed_data_emit \ $(BIN)/test_signed_data_emit_run \ + $(BIN)/test_tagged_call_arg \ + $(BIN)/test_tagged_call_arg_run \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -519,6 +521,16 @@ $(BIN)/test_signed_data_emit_run: test/wcc/923_signed_data_emit_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_tagged_call_arg: test/wcc/720_tagged_call_arg.c \ + $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +$(BIN)/test_tagged_call_arg_run: test/wcc/924_tagged_call_arg_run.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_use_promote_alias: test/wcc/699_use_promote_alias.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index e973cd14..a291841a 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -6528,6 +6528,18 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { aistagged = istaggedtype(c, lc.tnode); }; }; + // #21: a CALL returning a tagged-union must + // skip widening — cgexpr leaves AX=tag, + // DX=word0, CX=word1, R8=word2 per the + // tagged-return ABI; the widening branch would + // treat AX as a concrete payload and silently + // drop DX/CX/R8. Restrict to the matching-slot + // case (mirrors cstage type_eq at + // cmd/w6c/cgen.c:4216-4221); tagged-source + // widening into a wider slot is out of scope. + if (taggedcallslot(c, arg) == slotsize(c, ptype)) { + aistagged = true; + }; if (!aistagged) { widensz = slotsize(c, ptype); let tagged: *node = resolvetagged(c, ptype); @@ -6814,10 +6826,40 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { emitline("\tPUSHQ\tAX\n"); return rest + 2; }; + // #21: CALL returning a tagged-union — the aistagged guard + // above kept us out of the widening path. Push the tagged- + // return ABI registers (AX=tag, DX=word0, CX=word1, R8=word2) + // high → low so the left-to-right POPQ into argregs drains the + // tag first. Mirrors cstage at cmd/w6c/cgen.c:4373-4387. + let tcs: i32 = taggedcallslot(c, arg); + if (tcs > 0) { + if (tcs > 24) { emitline("\tPUSHQ\tR8\n"); }; + if (tcs > 16) { emitline("\tPUSHQ\tCX\n"); }; + if (tcs > 8) { emitline("\tPUSHQ\tDX\n"); }; + emitline("\tPUSHQ\tAX\n"); + return rest + tcs / 8; + }; emitline("\tPUSHQ\tAX\n"); return rest + 1; }; +// taggedcallslot — if `n` is an N_CALL whose callee returns a tagged +// type, returns the slot size in bytes; else 0. Used by pushargsrev's +// aistagged guard and natural-push arm, and by cgcall's pop sizer, to +// route a tagged-return call result through the AX/DX/CX/R8 high→low +// push convention rather than the concrete-variant widening path +// (which drops DX/CX/R8). See task #21. +export fn taggedcallslot(c: *cgen, n: *node) i32 = { + if (n == nil) { return 0; }; + if (n.kind != nkind.N_CALL) { return 0; }; + let callee: *node = n.lhs; + if (callee == nil) { return 0; }; + if (callee.kind != nkind.N_IDENT) { return 0; }; + let rt: *node = fnretlookup(c, callee.str); + if (!istaggedtype(c, rt)) { return 0; }; + return slotsize(c, rt); +}; + fn nodeisslice(c: *cgen, n: *node) bool = { if (n == nil) { return false; }; let k: nkind = n.kind; @@ -12933,6 +12975,12 @@ fn cgcall(c: *cgen, n: *node) void = { let extra: i32 = 0; if (nodeisstr(c, a)) { extra = 1; }; if (nodeisslice(c, a)) { extra = 2; }; + // #21: tagged-CALL arg was pushed AX/DX/CX/R8 high→low + // by pushargsrev; size the per-arg pop to match so the + // next arg's POPQ doesn't land on residual tag/payload + // words and shift intidx out of sync. + let tcs: i32 = taggedcallslot(c, a); + if (tcs > 0) { extra = tcs / 8 - 1; }; let words: i32 = 1 + extra; let w: i32 = 0; for (w < words) { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 48904996..756c1bff 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -3024,6 +3024,12 @@ fn cgcall(c: *cgen, n: *node) void = { let extra: i32 = 0; if (nodeisstr(c, a)) { extra = 1; }; if (nodeisslice(c, a)) { extra = 2; }; + // #21: tagged-CALL arg was pushed AX/DX/CX/R8 high→low + // by pushargsrev; size the per-arg pop to match so the + // next arg's POPQ doesn't land on residual tag/payload + // words and shift intidx out of sync. + let tcs: i32 = taggedcallslot(c, a); + if (tcs > 0) { extra = tcs / 8 - 1; }; let words: i32 = 1 + extra; let w: i32 = 0; for (w < words) { diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 5bd80d72..63751bf5 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -138,6 +138,18 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { aistagged = istaggedtype(c, lc.tnode); }; }; + // #21: a CALL returning a tagged-union must + // skip widening — cgexpr leaves AX=tag, + // DX=word0, CX=word1, R8=word2 per the + // tagged-return ABI; the widening branch would + // treat AX as a concrete payload and silently + // drop DX/CX/R8. Restrict to the matching-slot + // case (mirrors cstage type_eq at + // cmd/w6c/cgen.c:4216-4221); tagged-source + // widening into a wider slot is out of scope. + if (taggedcallslot(c, arg) == slotsize(c, ptype)) { + aistagged = true; + }; if (!aistagged) { widensz = slotsize(c, ptype); let tagged: *node = resolvetagged(c, ptype); @@ -424,10 +436,40 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { emitline("\tPUSHQ\tAX\n"); return rest + 2; }; + // #21: CALL returning a tagged-union — the aistagged guard + // above kept us out of the widening path. Push the tagged- + // return ABI registers (AX=tag, DX=word0, CX=word1, R8=word2) + // high → low so the left-to-right POPQ into argregs drains the + // tag first. Mirrors cstage at cmd/w6c/cgen.c:4373-4387. + let tcs: i32 = taggedcallslot(c, arg); + if (tcs > 0) { + if (tcs > 24) { emitline("\tPUSHQ\tR8\n"); }; + if (tcs > 16) { emitline("\tPUSHQ\tCX\n"); }; + if (tcs > 8) { emitline("\tPUSHQ\tDX\n"); }; + emitline("\tPUSHQ\tAX\n"); + return rest + tcs / 8; + }; emitline("\tPUSHQ\tAX\n"); return rest + 1; }; +// taggedcallslot — if `n` is an N_CALL whose callee returns a tagged +// type, returns the slot size in bytes; else 0. Used by pushargsrev's +// aistagged guard and natural-push arm, and by cgcall's pop sizer, to +// route a tagged-return call result through the AX/DX/CX/R8 high→low +// push convention rather than the concrete-variant widening path +// (which drops DX/CX/R8). See task #21. +export fn taggedcallslot(c: *cgen, n: *node) i32 = { + if (n == nil) { return 0; }; + if (n.kind != nkind.N_CALL) { return 0; }; + let callee: *node = n.lhs; + if (callee == nil) { return 0; }; + if (callee.kind != nkind.N_IDENT) { return 0; }; + let rt: *node = fnretlookup(c, callee.str); + if (!istaggedtype(c, rt)) { return 0; }; + return slotsize(c, rt); +}; + fn nodeisslice(c: *cgen, n: *node) bool = { if (n == nil) { return false; }; let k: nkind = n.kind; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 27ea34c7..1e2ffa46 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -6528,6 +6528,18 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { aistagged = istaggedtype(c, lc.tnode); }; }; + // #21: a CALL returning a tagged-union must + // skip widening — cgexpr leaves AX=tag, + // DX=word0, CX=word1, R8=word2 per the + // tagged-return ABI; the widening branch would + // treat AX as a concrete payload and silently + // drop DX/CX/R8. Restrict to the matching-slot + // case (mirrors cstage type_eq at + // cmd/w6c/cgen.c:4216-4221); tagged-source + // widening into a wider slot is out of scope. + if (taggedcallslot(c, arg) == slotsize(c, ptype)) { + aistagged = true; + }; if (!aistagged) { widensz = slotsize(c, ptype); let tagged: *node = resolvetagged(c, ptype); @@ -6814,10 +6826,40 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { emitline("\tPUSHQ\tAX\n"); return rest + 2; }; + // #21: CALL returning a tagged-union — the aistagged guard + // above kept us out of the widening path. Push the tagged- + // return ABI registers (AX=tag, DX=word0, CX=word1, R8=word2) + // high → low so the left-to-right POPQ into argregs drains the + // tag first. Mirrors cstage at cmd/w6c/cgen.c:4373-4387. + let tcs: i32 = taggedcallslot(c, arg); + if (tcs > 0) { + if (tcs > 24) { emitline("\tPUSHQ\tR8\n"); }; + if (tcs > 16) { emitline("\tPUSHQ\tCX\n"); }; + if (tcs > 8) { emitline("\tPUSHQ\tDX\n"); }; + emitline("\tPUSHQ\tAX\n"); + return rest + tcs / 8; + }; emitline("\tPUSHQ\tAX\n"); return rest + 1; }; +// taggedcallslot — if `n` is an N_CALL whose callee returns a tagged +// type, returns the slot size in bytes; else 0. Used by pushargsrev's +// aistagged guard and natural-push arm, and by cgcall's pop sizer, to +// route a tagged-return call result through the AX/DX/CX/R8 high→low +// push convention rather than the concrete-variant widening path +// (which drops DX/CX/R8). See task #21. +export fn taggedcallslot(c: *cgen, n: *node) i32 = { + if (n == nil) { return 0; }; + if (n.kind != nkind.N_CALL) { return 0; }; + let callee: *node = n.lhs; + if (callee == nil) { return 0; }; + if (callee.kind != nkind.N_IDENT) { return 0; }; + let rt: *node = fnretlookup(c, callee.str); + if (!istaggedtype(c, rt)) { return 0; }; + return slotsize(c, rt); +}; + fn nodeisslice(c: *cgen, n: *node) bool = { if (n == nil) { return false; }; let k: nkind = n.kind; @@ -12933,6 +12975,12 @@ fn cgcall(c: *cgen, n: *node) void = { let extra: i32 = 0; if (nodeisstr(c, a)) { extra = 1; }; if (nodeisslice(c, a)) { extra = 2; }; + // #21: tagged-CALL arg was pushed AX/DX/CX/R8 high→low + // by pushargsrev; size the per-arg pop to match so the + // next arg's POPQ doesn't land on residual tag/payload + // words and shift intidx out of sync. + let tcs: i32 = taggedcallslot(c, a); + if (tcs > 0) { extra = tcs / 8 - 1; }; let words: i32 = 1 + extra; let w: i32 = 0; for (w < words) { diff --git a/test/wcc/720_tagged_call_arg.c b/test/wcc/720_tagged_call_arg.c new file mode 100644 index 00000000..30f73b1d --- /dev/null +++ b/test/wcc/720_tagged_call_arg.c @@ -0,0 +1,255 @@ +/* + * 720_tagged_call_arg — sentinel for #21. Asserts wwstage's caller + * emit sequences `PUSHQ DX` *before* `PUSHQ AX` for the (post-CALL) + * tagged-return ABI when the arg is a fn call returning a 2-word + * tagged union. Pre-fix wwstage emitted + * PUSHQ AX + * MOVQ $0, AX + * PUSHQ AX + * dropping the DX (payload) word and substituting widentag (the + * concrete-variant index) in the second push slot. Post-fix the + * natural-push arm pushes (R8 if >24B), (CX if >16B), DX (if >8B), + * AX — matching cstage at cmd/w6c/cgen.c:4373-4387. + * + * Plus a cstage-vs-wwstage byte-id diff for the !void-free shapes + * (rune-only payload variants) — diffs on shapes that include !void + * variants are gated by task #22 (wwstage skips the prologue + * zero-init of the !void let-decl slot), an orthogonal divergence. + * 924_tagged_call_arg_run pins the runtime behaviour; 995_self_rebuild + * pins the global bootstrap byte-id; this row pins the asm shape so a + * future cgen refactor that re-routes the dispatch can't silently + * regress back to the dropped-DX sequence. + */ +#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 check_byteid; /* 1 iff the row contains no !void variants */ +}; + +static const struct row rows[] = { + /* 4-variant CALL-source. The bug's original repro shape. + * Pre-fix wwstage main emits `PUSHQ AX; MOVQ $0, AX; PUSHQ AX` + * after the CALL — no `PUSHQ DX` anywhere in main. The needle + * search below pins that a PUSHQ DX appears within a window + * after the CALL to yield_rune. byte-id is skipped because the + * !void variants trigger #22. */ + { "4variant_call_source", + "type done = !void; type more = !void; type invalid = !void;\n" + "fn yield_rune() (rune | done | more | invalid) = {\n" + " return 65u32: rune;\n" + "};\n" + "fn dispatch(v: (rune | done | more | invalid)) i32 = {\n" + " match (v) {\n" + " case let r: rune => return r: i32;\n" + " case let d: done => return -1;\n" + " case let m: more => return -2;\n" + " case let e: invalid => return -3;\n" + " };\n" + " return -99;\n" + "};\n" + "export fn main() i32 = {\n" + " if (dispatch(yield_rune()) != 65) { return 11; };\n" + " return 0;\n" + "};\n", + 0 }, + /* 2-variant (*u8 | oserror): pure-scalar variants, no !void — + * #22 doesn't trigger so byte-id between stages is asserted. */ + { "2variant_call_source_ptr", + "type oserror = !i32;\n" + "fn yield_ptr() (*u8 | oserror) = {\n" + " let p: *u8 = nil;\n" + " return p;\n" + "};\n" + "fn dispatch(v: (*u8 | oserror)) i32 = {\n" + " match (v) {\n" + " case let p: *u8 => return 7;\n" + " case let e: oserror => return e: i32;\n" + " };\n" + " return -99;\n" + "};\n" + "export fn main() i32 = {\n" + " if (dispatch(yield_ptr()) != 7) { return 11; };\n" + " return 0;\n" + "};\n", + 1 }, +}; + +static int +slurp(const char *path, char *buf, size_t cap) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + size_t n = fread(buf, 1, cap - 1, f); + fclose(f); + buf[n] = '\0'; + return (int)n; +} + +/* Find the first occurrence of `needle` in `buf` after `start`. + * Returns the offset relative to buf, or -1 if missing. */ +static long +find_after(const char *buf, long start, const char *needle) +{ + const char *p = strstr(buf + start, needle); + if (!p) return -1; + return (long)(p - buf); +} + +/* Assert: after every `CALL\tyield_` site, the *next* PUSHQ + * instruction's operand is DX, not AX, and a subsequent PUSHQ AX + * lands the tag (per the high-→-low convention). The pre-fix + * sequence had PUSHQ AX immediately after the CALL with no + * PUSHQ DX in between. */ +static int +check_pushdx_before_pushax(const char *spath, const struct row *r) +{ + char buf[1 << 16]; + if (slurp(spath, buf, sizeof buf) < 0) return -1; + + long off = 0; + int saw_site = 0; + for (;;) { + long call = find_after(buf, off, "CALL\tyield_"); + if (call < 0) break; + saw_site = 1; + long pdx = find_after(buf, call, "PUSHQ\tDX"); + long pax = find_after(buf, call, "PUSHQ\tAX"); + long nextcall = find_after(buf, call + 1, "CALL\t"); + if (pdx < 0 || (nextcall >= 0 && pdx > nextcall)) { + fprintf(stderr, + "row[%s]: no PUSHQ DX between CALL yield_ and next CALL\n", + r->label); + return -1; + } + if (pax < 0 || pdx > pax) { + fprintf(stderr, + "row[%s]: PUSHQ DX does not precede PUSHQ AX after CALL yield_\n", + r->label); + return -1; + } + off = call + 1; + } + if (!saw_site) { + fprintf(stderr, "row[%s]: no CALL yield_ site found\n", r->label); + return -1; + } + return 0; +} + +static int +emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) +{ + char src[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/tca_asm_%d_%d.ww", getpid(), i); + snprintf(out_s, cap, "/tmp/tca_asm_%d_%d_%s.s", + getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); + int rc = runwait(cmd); + unlink(src); + 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 w6c[640], w6c_ww[640]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + + int have_ww = (access(w6c_ww, X_OK) == 0); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + char cs_path[128], ws_path[128]; + + /* cstage row: presence of PUSHQ DX before PUSHQ AX. */ + if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { + fprintf(stderr, + "tagged_call_arg[cstage][%s]: w6c failed\n", + rows[i].label); + fail++; total++; continue; + } + total++; + if (check_pushdx_before_pushax(cs_path, &rows[i]) != 0) { + fail++; + } + + if (!have_ww) { unlink(cs_path); continue; } + + /* wwstage row: same shape assertion. Pre-fix this is the + * one that fires (cstage was always correct). */ + if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { + fprintf(stderr, + "tagged_call_arg[wwstage][%s]: w6c_ww failed\n", + rows[i].label); + fail++; total++; + unlink(cs_path); + continue; + } + total++; + if (check_pushdx_before_pushax(ws_path, &rows[i]) != 0) { + fail++; + } + + /* Byte-id diff between stages, only for !void-free rows. + * Rows that contain `!void` variants are gated by task #22 + * (wwstage skips the prologue zero-init of the !void slot) + * — that divergence is orthogonal to #21. */ + if (rows[i].check_byteid) { + total++; + char cmd[512]; + snprintf(cmd, sizeof cmd, "cmp -s %s %s", + cs_path, ws_path); + if (runwait(cmd) != 0) { + fprintf(stderr, + "tagged_call_arg[%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + } + + unlink(cs_path); unlink(ws_path); + } + + if (fail) { + fprintf(stderr, + "tagged_call_arg: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("tagged_call_arg: %d/%d ok\n", total, total); + return 0; +} diff --git a/test/wcc/924_tagged_call_arg_run.c b/test/wcc/924_tagged_call_arg_run.c new file mode 100644 index 00000000..2b6b5004 --- /dev/null +++ b/test/wcc/924_tagged_call_arg_run.c @@ -0,0 +1,243 @@ +/* + * 924_tagged_call_arg_run — wwstage caller must not drop the DX/CX/R8 + * payload words when passing a tagged-union arg sourced from a fn + * call that returns the same tagged union (task #21). + * + * Pre-fix the wwstage call-arg-emit dispatch in pushargsrev only + * recognized N_IDENT as an already-tagged source. For arg.kind == + * N_CALL whose callee returns the param's tagged type, the widening + * path treated AX (the returned tag word) as a concrete-variant + * payload, then clobbered AX with widentag for the tag-push slot. + * The DX/CX/R8 value-words from the tagged-return ABI were silently + * dropped — the callee read its second arg-reg (SI / DX / etc.) as + * 0 or as the original tag. + * + * Cstage already had the right shape: a `type_eq(p->type, at)` guard + * at cmd/w6c/cgen.c:4216-4221 sets widen[i]=0, then the natural-push + * branch at 4373-4387 pushes R8/CX/DX/AX high→low. Fix aligns + * wwstage DOWN to that shape (rule 10): + * 1. pushargsrev's aistagged guard (cgenutil.ww) now also fires + * when taggedcallslot(arg) == slotsize(ptype), matching cstage. + * 2. The natural-push fallthrough adds a taggedcallslot arm that + * pushes R8/CX/DX/AX high→low by slot size. + * 3. cgcall's per-arg pop-count uses taggedcallslot too so the + * next arg's POPQ doesn't land on residual tagged words. + * + * Rows pin: + * - 4-variant (rune | done | more | invalid) CALL-source: the + * original utf8 iterator shape from worker-encoding's probe. + * Two-word tagged (16B slot), CALL→CALL composition. + * - IDENT-source guard: a parallel test that was already correct + * pre-fix, pinning that the IDENT fast-path stays byte-identical + * to cstage and isn't perturbed by the fix. + * - 2-variant `(*u8 | oserror)` sanity: narrower 16B slot variant + * to ensure the natural-push arm doesn't regress smaller-slot + * shapes. + * - Multi-arg sequence: tagged-CALL arg followed by a scalar arg, + * pinning that the pop-count fix keeps subsequent args in the + * right register class. + */ +#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[] = { + { "4variant_call_source_rune", + "type done = !void; type more = !void; type invalid = !void;\n" + "fn yield_rune() (rune | done | more | invalid) = {\n" + " return 65u32: rune;\n" + "};\n" + "fn dispatch(v: (rune | done | more | invalid)) i32 = {\n" + " match (v) {\n" + " case let r: rune => return r: i32;\n" + " case let d: done => return -1;\n" + " case let m: more => return -2;\n" + " case let e: invalid => return -3;\n" + " };\n" + " return -99;\n" + "};\n" + "export fn main() i32 = {\n" + " if (dispatch(yield_rune()) != 65) { return 11; };\n" + " return 0;\n" + "};\n", + 0 }, + { "4variant_ident_source_rune", + "type done = !void; type more = !void; type invalid = !void;\n" + "fn dispatch(v: (rune | done | more | invalid)) i32 = {\n" + " match (v) {\n" + " case let r: rune => return r: i32;\n" + " case let d: done => return -1;\n" + " case let m: more => return -2;\n" + " case let e: invalid => return -3;\n" + " };\n" + " return -99;\n" + "};\n" + "export fn main() i32 = {\n" + " let v: (rune | done | more | invalid) = 65u32: rune;\n" + " if (dispatch(v) != 65) { return 11; };\n" + " return 0;\n" + "};\n", + 0 }, + { "2variant_call_source_ptr", + "type oserror = !i32;\n" + "fn yield_ptr() (*u8 | oserror) = {\n" + " let p: *u8 = nil;\n" + " return p;\n" + "};\n" + "fn dispatch(v: (*u8 | oserror)) i32 = {\n" + " match (v) {\n" + " case let p: *u8 => return 7;\n" + " case let e: oserror => return e: i32;\n" + " };\n" + " return -99;\n" + "};\n" + "export fn main() i32 = {\n" + " if (dispatch(yield_ptr()) != 7) { return 11; };\n" + " return 0;\n" + "};\n", + 0 }, + { "2variant_call_source_err", + "type oserror = !i32;\n" + "fn yield_err() (*u8 | oserror) = {\n" + " let e: oserror = 42i32: oserror;\n" + " return e;\n" + "};\n" + "fn dispatch(v: (*u8 | oserror)) i32 = {\n" + " match (v) {\n" + " case let p: *u8 => return 7;\n" + " case let e: oserror => return e: i32;\n" + " };\n" + " return -99;\n" + "};\n" + "export fn main() i32 = {\n" + " if (dispatch(yield_err()) != 42) { return 11; };\n" + " return 0;\n" + "};\n", + 0 }, + /* Multi-arg shape: tagged-CALL followed by a scalar i32 arg. + * Pre-fix the pop-count would be wrong (1 word for arg0 instead + * of 2), leaving arg1's scalar to pick up the residual tag word + * off the stack and routing the real scalar into DX. Post-fix + * the per-arg word count matches the push. */ + { "tagged_call_then_scalar", + "type oserror = !i32;\n" + "fn yield_ptr() (*u8 | oserror) = {\n" + " let p: *u8 = nil;\n" + " return p;\n" + "};\n" + "fn use2(v: (*u8 | oserror), k: i32) i32 = {\n" + " match (v) {\n" + " case let p: *u8 => return k + 1;\n" + " case let e: oserror => return k - 1;\n" + " };\n" + " return -99;\n" + "};\n" + "export fn main() i32 = {\n" + " if (use2(yield_ptr(), 41) != 42) { return 11; };\n" + " return 0;\n" + "};\n", + 0 }, +}; + +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/tca_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/tca_%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[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]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[640]; + 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_call_arg_run: 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_call_arg_run[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + if (fail) { + fprintf(stderr, + "tagged_call_arg_run: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("tagged_call_arg_run: %d/%d ok\n", total, total); + return 0; +}