From cbf10427df71c4a349c1f3af4cc50cb8a874e67b Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 19 May 2026 03:09:34 +0900 Subject: [PATCH] selfhost+test: graduate wwstage sum-typed N_INDEX call-arg to tagged ABI (#12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pushargsrev's widening detection was N_IDENT-only — N_INDEX of a sum-typed slice element fell through to the scalar widening branch, which hardcoded the param's first-variant tag (MOVQ $1, AX) and pushed AX as a single scalar word. Callees that match-dispatched on the runtime tag always ran the static-guess arm on garbage. cstage knew the arg's type via check.c so its widen[] flag stayed off and the natural-push tagged-arg arm pushed CX/DX/AX (high → low) high → low. wwstage now mirrors via two narrow arms in pushargsrev: the aistagged guard treats N_INDEX-of-sum-typed-element matching the param slot as already-tagged, and the natural-push fallthrough emits PUSHQ CX / DX / AX for the same shape. Both arms gate on istaggedtype(indexvaluetnode(arg)) so literal- and ident-source sum args stay on their existing paths. Sentinel 749_sumtype_forward table-drives the three forward shapes (N_INDEX, N_IDENT, literal) and asserts per-stage runtime plus a byte-id window over the callsite asm. Combined.ww regen for wwdump_ww and w6c_ww follows the cgen source change; smoke.combined.ww unaffected. Tests: 123/123 pass; bootstrap fixed point holds (ww2==ww3==ww4). --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 31 +++ selfhost/cmd/wcc/cgenutil.ww | 31 +++ selfhost/cmd/wwdump/main.combined.ww | 31 +++ test/wcc/749_sumtype_forward.c | 327 +++++++++++++++++++++++++++ 5 files changed, 427 insertions(+) create mode 100644 test/wcc/749_sumtype_forward.c diff --git a/Makefile b/Makefile index f168bf2c..0672122e 100644 --- a/Makefile +++ b/Makefile @@ -283,6 +283,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_strdef_inline \ $(BIN)/test_def_modqual_modshadow \ $(BIN)/test_size_strategy_convergence \ + $(BIN)/test_sumtype_forward \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -706,6 +707,12 @@ $(BIN)/test_size_strategy_convergence: test/wcc/748_size_strategy_convergence.c $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_sumtype_forward: test/wcc/749_sumtype_forward.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_match_4arm_cross_module_run: test/wcc/929_match_4arm_cross_module_run.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index f4d8fd21..f718be70 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -7292,6 +7292,21 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { if (taggedcallslot(c, arg) == slotsize(c, ptype)) { aistagged = true; }; + // #12: N_INDEX of a sum-typed slice element — + // cgindex emits the same AX/DX/CX/R8 tagged ABI. + // Without this gate the widening scalar branch + // hardcodes the param's first-variant tag and + // the callee reads a fixed arm on garbage. + if (arg.kind == nkind.N_INDEX) { + let etn: *node = indexvaluetnode(c, arg); + if (etn != nil) { + if (istaggedtype(c, etn)) { + if (slotsize(c, etn) == slotsize(c, ptype)) { + aistagged = true; + }; + }; + }; + }; if (!aistagged) { widensz = slotsize(c, ptype); let tagged: *node = resolvetagged(c, ptype); @@ -7591,6 +7606,22 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { emitline("\tPUSHQ\tAX\n"); return rest + tcs / 8; }; + // #12: N_INDEX of a sum-typed slice element. cgindex above left + // the tagged-CALL ABI in AX/DX/CX/R8; the bare PUSHQ AX below + // would only carry the tag word and drop the payload. + if (arg.kind == nkind.N_INDEX) { + let etn: *node = indexvaluetnode(c, arg); + if (etn != nil) { + if (istaggedtype(c, etn)) { + let isz: i32 = slotsize(c, etn); + if (isz > 24) { emitline("\tPUSHQ\tR8\n"); }; + if (isz > 16) { emitline("\tPUSHQ\tCX\n"); }; + if (isz > 8) { emitline("\tPUSHQ\tDX\n"); }; + emitline("\tPUSHQ\tAX\n"); + return rest + isz / 8; + }; + }; + }; emitline("\tPUSHQ\tAX\n"); return rest + 1; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 3b0a431f..b545e4bb 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -152,6 +152,21 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { if (taggedcallslot(c, arg) == slotsize(c, ptype)) { aistagged = true; }; + // #12: N_INDEX of a sum-typed slice element — + // cgindex emits the same AX/DX/CX/R8 tagged ABI. + // Without this gate the widening scalar branch + // hardcodes the param's first-variant tag and + // the callee reads a fixed arm on garbage. + if (arg.kind == nkind.N_INDEX) { + let etn: *node = indexvaluetnode(c, arg); + if (etn != nil) { + if (istaggedtype(c, etn)) { + if (slotsize(c, etn) == slotsize(c, ptype)) { + aistagged = true; + }; + }; + }; + }; if (!aistagged) { widensz = slotsize(c, ptype); let tagged: *node = resolvetagged(c, ptype); @@ -451,6 +466,22 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { emitline("\tPUSHQ\tAX\n"); return rest + tcs / 8; }; + // #12: N_INDEX of a sum-typed slice element. cgindex above left + // the tagged-CALL ABI in AX/DX/CX/R8; the bare PUSHQ AX below + // would only carry the tag word and drop the payload. + if (arg.kind == nkind.N_INDEX) { + let etn: *node = indexvaluetnode(c, arg); + if (etn != nil) { + if (istaggedtype(c, etn)) { + let isz: i32 = slotsize(c, etn); + if (isz > 24) { emitline("\tPUSHQ\tR8\n"); }; + if (isz > 16) { emitline("\tPUSHQ\tCX\n"); }; + if (isz > 8) { emitline("\tPUSHQ\tDX\n"); }; + emitline("\tPUSHQ\tAX\n"); + return rest + isz / 8; + }; + }; + }; emitline("\tPUSHQ\tAX\n"); return rest + 1; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 9281e328..05d05cc0 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -7292,6 +7292,21 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { if (taggedcallslot(c, arg) == slotsize(c, ptype)) { aistagged = true; }; + // #12: N_INDEX of a sum-typed slice element — + // cgindex emits the same AX/DX/CX/R8 tagged ABI. + // Without this gate the widening scalar branch + // hardcodes the param's first-variant tag and + // the callee reads a fixed arm on garbage. + if (arg.kind == nkind.N_INDEX) { + let etn: *node = indexvaluetnode(c, arg); + if (etn != nil) { + if (istaggedtype(c, etn)) { + if (slotsize(c, etn) == slotsize(c, ptype)) { + aistagged = true; + }; + }; + }; + }; if (!aistagged) { widensz = slotsize(c, ptype); let tagged: *node = resolvetagged(c, ptype); @@ -7591,6 +7606,22 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { emitline("\tPUSHQ\tAX\n"); return rest + tcs / 8; }; + // #12: N_INDEX of a sum-typed slice element. cgindex above left + // the tagged-CALL ABI in AX/DX/CX/R8; the bare PUSHQ AX below + // would only carry the tag word and drop the payload. + if (arg.kind == nkind.N_INDEX) { + let etn: *node = indexvaluetnode(c, arg); + if (etn != nil) { + if (istaggedtype(c, etn)) { + let isz: i32 = slotsize(c, etn); + if (isz > 24) { emitline("\tPUSHQ\tR8\n"); }; + if (isz > 16) { emitline("\tPUSHQ\tCX\n"); }; + if (isz > 8) { emitline("\tPUSHQ\tDX\n"); }; + emitline("\tPUSHQ\tAX\n"); + return rest + isz / 8; + }; + }; + }; emitline("\tPUSHQ\tAX\n"); return rest + 1; }; diff --git a/test/wcc/749_sumtype_forward.c b/test/wcc/749_sumtype_forward.c new file mode 100644 index 00000000..64e4b77e --- /dev/null +++ b/test/wcc/749_sumtype_forward.c @@ -0,0 +1,327 @@ +/* + * 749_sumtype_forward — sentinel for #12. + * + * Pre-fix wwstage hardcoded the param's first-variant tag when + * forwarding a sum-typed variadic element to a scalar (T|U) param. + * cgindex emitted the tagged-CALL ABI (AX=tag, DX=word0, CX=word1[, + * R8=word2]) for `needles[i]` correctly, then pushargsrev's widening + * detection — keyed only on N_IDENT-source — fell into the scalar + * widening branch: XORQ DX,DX / PUSHQ DX / PUSHQ AX / MOVQ $1,AX / + * PUSHQ AX. The hardcoded `$1` made the callee always run the rune + * arm; payload was the slice's first ptr word reinterpreted as a rune. + * + * cstage knew the arg's static type via check.c (`a->type == p->type`) + * so the widen flag stayed off and the natural-push tagged-arg arm + * pushed CX/DX/AX. + * + * Fix: wwstage pushargsrev now treats `arg.kind == N_INDEX && + * istaggedtype(indexvaluetnode(arg))` as already-tagged (matching + * slot size to the param) and pushes CX/DX/AX in the natural-push + * fallthrough. + * + * Rows assert per-stage runtime AND byte-id at the affected callsite. + * + * row | what it pins + * -------------------------------+-------------------------------- + * forward_index_of_sum_variadic | #12 wedge. Pre-fix wwstage + * | returned 11 (helper saw garbage + * | rune); cstage returned 0. + * | Post-fix both return 0. + * forward_ident_sum | Sibling: forward an ident-source + * | sum local to a sum-typed scalar + * | param. Stays on the N_IDENT + * | aistagged path — non-regression. + * forward_literal_sum | Sibling: forward a literal-source + * | value (str) to a sum-typed scalar + * | param. Stays on the widening + * | path — non-regression. + */ +#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_cs; + int want_ws; +}; + +static const struct row rows[] = { + /* 1. Original #12 wedge. helper(needles[i]) where needles is + * (str|rune)... — N_INDEX of sum-typed slice element forwarded + * to a scalar (str|rune) param. pick("foo","world",'x') hits + * the 'x' rune at i=2, so helper returns true and pick returns + * true. main returns 0 on success. */ + { "forward_index_of_sum_variadic", + "package main;\n" + "fn helper(needle: (str | rune)) bool = {\n" + " match (needle) {\n" + " case let s: str => return s.len > 0;\n" + " case let r: rune => return r == 'x';\n" + " };\n" + " return false;\n" + "};\n" + "fn pick(needles: (str | rune)...) bool = {\n" + " let i: i32 = 0;\n" + " for (i < needles.len) {\n" + " if (helper(needles[i])) { return true; };\n" + " i += 1;\n" + " };\n" + " return false;\n" + "};\n" + "export fn main() i32 = {\n" + " if (!pick(\"foo\", \"world\", 'x')) { return 11; };\n" + " return 0;\n" + "};\n", + 0, 0 }, + + /* 2. Ident-source sum-arg. needle is a (str|rune) local; helper + * call routes through the existing N_IDENT-aistagged path. The + * (str|rune) ident is built from a str literal so the rune arm + * shouldn't fire. */ + { "forward_ident_sum", + "package main;\n" + "fn helper(needle: (str | rune)) bool = {\n" + " match (needle) {\n" + " case let s: str => return s.len == 3;\n" + " case let r: rune => return false;\n" + " };\n" + " return false;\n" + "};\n" + "export fn main() i32 = {\n" + " let needle: (str | rune) = \"foo\";\n" + " if (!helper(needle)) { return 11; };\n" + " return 0;\n" + "};\n", + 0, 0 }, + + /* 3. Literal-source sum-arg. helper(\"foo\") widens the str at + * the call site. Stays on the widening path (param tagged, arg + * not tagged) and pushes a synthesised (tag=0, ptr, len) slot. */ + { "forward_literal_sum", + "package main;\n" + "fn helper(needle: (str | rune)) bool = {\n" + " match (needle) {\n" + " case let s: str => return s.len == 3;\n" + " case let r: rune => return false;\n" + " };\n" + " return false;\n" + "};\n" + "export fn main() i32 = {\n" + " if (!helper(\"foo\")) { return 11; };\n" + " return 0;\n" + "};\n", + 0, 0 }, +}; + +static int +build_with(const char *driver, const char *src_path, const char *tmpdir) +{ + char cmd[1024]; + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", + tmpdir, driver, src_path); + return runwait(cmd); +} + +static int +exec_bin(const char *bin) +{ + return runwait(bin); +} + +/* read_file slurps the .s file produced alongside the driver build, + * returning a malloc'd buffer (NUL-terminated) or NULL on error. */ +static char * +read_file(const char *path, size_t *outsz) +{ + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return NULL; } + char *buf = malloc((size_t)n + 1); + if (!buf) { fclose(f); return NULL; } + size_t got = fread(buf, 1, (size_t)n, f); + fclose(f); + buf[got] = '\0'; + if (outsz) *outsz = got; + return buf; +} + +/* slice_at finds the line beginning with `CALL\tmain.helper(SB)` and + * returns a pointer to the start of the block four lines above (the + * three PUSHQ + the tag MOV in the pre-fix case, the three PUSHQ in + * the post-fix). For our byte-id check we compare a 64-byte window + * starting at the first newline above that anchor. */ +static const char * +helper_callsite(const char *asm_buf) +{ + const char *anchor = strstr(asm_buf, "CALL\tmain.helper(SB)"); + if (!anchor) return NULL; + /* walk back ~12 lines so the slice straddles the args set-up. */ + const char *p = anchor; + int back = 12; + while (back > 0 && p > asm_buf) { + p--; + if (*p == '\n') back--; + } + return p; +} + +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], wdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + int have_ww = (access(wdrv, X_OK) == 0); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + const struct row *r = &rows[i]; + + char src[64], tmpdir[64]; + snprintf(src, sizeof src, "/tmp/sumtype_fwd_%d_%d.ww", + getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/sumtype_fwd_%d_d_%d", + getpid(), i); + FILE *f = fopen(src, "wb"); + if (!f) { fail++; total++; continue; } + fputs(r->src, f); + fclose(f); + mkdir(tmpdir, 0755); + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[128], outasm[160]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + /* .s lands alongside the .ww source per `ww build`. */ + snprintf(outasm, sizeof outasm, "%s", src); + char *dot2 = strrchr(outasm, '.'); + if (dot2 && strcmp(dot2, ".ww") == 0) { + strcpy(dot2, ".s"); + } + + char *cs_asm = NULL; + size_t cs_n = 0; + + /* Runtime parity (cstage). */ + total++; + if (build_with(cdrv, src, tmpdir) != 0) { + fprintf(stderr, + "sumtype_forward[cs][%s]: build failed\n", + r->label); + fail++; + } else { + int got = exec_bin(outbin); + if (got != r->want_cs) { + fprintf(stderr, + "sumtype_forward[cs][%s]: rc=%d want=%d\n", + r->label, got, r->want_cs); + fail++; + } + cs_asm = read_file(outasm, &cs_n); + } + unlink(outbin); + + /* Runtime parity (wwstage). */ + char *ws_asm = NULL; + size_t ws_n = 0; + if (have_ww) { + total++; + if (build_with(wdrv, src, tmpdir) != 0) { + fprintf(stderr, + "sumtype_forward[ws][%s]: build failed\n", + r->label); + fail++; + } else { + int got = exec_bin(outbin); + if (got != r->want_ws) { + fprintf(stderr, + "sumtype_forward[ws][%s]: rc=%d want=%d\n", + r->label, got, r->want_ws); + fail++; + } + ws_asm = read_file(outasm, &ws_n); + } + unlink(outbin); + } + + /* Byte-id of the helper(needles[i]) callsite window. Only + * the first row has a main.helper call; rows 2/3 also have + * one but a simpler shape — match them all for symmetry. */ + if (cs_asm && ws_asm) { + total++; + const char *cs_site = helper_callsite(cs_asm); + const char *ws_site = helper_callsite(ws_asm); + if (!cs_site || !ws_site) { + fprintf(stderr, + "sumtype_forward[bid][%s]: no callsite anchor\n", + r->label); + fail++; + } else if (strncmp(cs_site, ws_site, 96) != 0) { + fprintf(stderr, + "sumtype_forward[bid][%s]: callsite asm diverged\n", + r->label); + fprintf(stderr, " cs: %.96s\n", cs_site); + fprintf(stderr, " ws: %.96s\n", ws_site); + fail++; + } + } + free(cs_asm); + free(ws_asm); + + char extra[160]; + unlink(outasm); + snprintf(extra, sizeof extra, "%s", src); + char *d3 = strrchr(extra, '.'); + if (d3 && strcmp(d3, ".ww") == 0) { + strcpy(d3, ".o"); + unlink(extra); + } + snprintf(extra, sizeof extra, "%s", src); + d3 = strrchr(extra, '.'); + if (d3 && strcmp(d3, ".ww") == 0) { + strcpy(d3, ".combined.ww"); + unlink(extra); + } + unlink(src); + rmdir(tmpdir); + } + + if (fail) { + fprintf(stderr, + "sumtype_forward: %d/%d rows failed\n", fail, total); + return 1; + } + printf("sumtype_forward: %d/%d ok\n", total, total); + return 0; +}