/* * 829_slice_str_global_arg — a module-global SLICE or STR passed BY VALUE to * a fn (`let s: str = "..."; take(s)`, `let g: []int = [...]; take(g)`) was * silently miscompiled by wwstage only (task #151, the slice/str twin of the * #150 struct-global arg): * * - cstage (cmd/w6c/cgen.c): node_isslice/node_isstr are TYPE-keyed * (n->type), so a global slice/str ident still pushes all 3 header words * {ptr,len,cap} — slice via its dedicated push arm (BX-base per-word, * cgen.c:9124), str via cgexpr's cgslicehdr (CX-base AX/BX/CX, * cgen.c:1866) then the node_isstr triple push. * - wwstage (selfhost cgenutil.ww pushargsrev): nodeisslice/nodeisstr are * LOCAL-keyed (localfindnode→nil for a global → return false), so a * global slice/str ident fell past every header arm to the scalar * single-PUSHQ default — ONE word (ptr), dropping len+cap. The callee * then read garbage for .len/.cap (silent field-drop). * * The fix (ONE wwstage cgen commit, cstage UNTOUCHED → w6c md5 unchanged): * add a global slice/str arm beside the #150 struct arm in pushargsrev's * `lc == nil` branch, gated `(isletvar || deflookup)`. cstage emits a * DIFFERENT per-type sequence, so each arm is hand-authored to MATCH: * - slice: LEAQ name(SB),BX; MOVQ {16,8,0}(BX),AX; PUSHQ AX (BX-base) * - str: LEAQ name(SB),CX; MOVQ (CX),AX / 8(CX),BX / 16(CX),CX; * PUSHQ CX/BX/AX (cgslicehdr CX-base) * Each `.s` is now byte-IDENTICAL to cstage per type. The LOCAL slice/str * push path (off!=0) is untouched. * * row | shape | want * ---------------+----------------------------------------------+------ * str_glob_len | let s:str="abcd"; take reads x.len | 4 * str_glob_ptr | let s:str="abcd"; take reads x[0] (uses ptr) | 97 * slice_glob_len | let g:[]int=[10..50]; take reads x.len | 5 * slice_glob_idx | let g:[]int=[10..50]; take reads x[2] | 30 * def_str_len | def s:str="abcd"; take reads x.len (#21) | 4 * def_str_ptr | def s:str="abcd"; take reads x[0] (#21) | 97 * def_str_return | return ; take reads x.len (#21) | 5 * ctrl_local_str | LOCAL str arg (off!=0, unchanged, byte-id) | 2 * ctrl_local_sl | LOCAL []int arg (off!=0, unchanged, byte-id) | 8 * * The def_* rows are the #21 residual of this by-value-global family: a * module-level `def s: str` (NO DATA symbol) passed BY VALUE. Pre-fix wwstage's * nodeisstr was LOCAL-keyed for an N_IDENT (def→false), so the def-str fell to * the scalar single-PUSHQ default and dropped len/cap; cstage's node_isstr is * TYPE-keyed (n->type) and pushes all 3 words. A def rides cgident's const-fold * (LEAQ _S_n, MOVQ $len) — NOT the let LEAQ name(SB) path — then the now * type-keyed nodeisstr triple push. def_str_return is a SIBLING guard (a * def-str flowed through a fn return, then pushed via nodeisstr's pre-existing * N_CALL arm) — byte-id-identical pre/post-fix, no #21 teeth of its own. * * The *_glob_* rows are mutation-sane: pre-fix wwstage drops len/cap and the * callee reads garbage; cstage is correct. ctrl_* guard the unchanged local * path. A byte-id row pins cstage==wwstage `.s` for each source. * * NOTE — slice element type is []int, not []u8: a `let g: []u8 = [intlits]` * module-global is rejected by the wwstage checker ("let: not assignable", * the untyped-int→u8 array-lit coercion) while cstage accepts it — a * SEPARATE pre-existing checker divergence (#10/#125 batch, "bare []u8 * cs≠ww"), orthogonal to this caller-side push fix. []int exercises the * identical TY_SLICE push arm. The str rows cover the []u8-header shape * (str IS []u8), including a byte read via x[0]. */ #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[] = { /* str_glob_len — THE regression pin (str header): global str by value, * callee reads .len. Pre-fix wwstage drops len → garbage. */ { "str_glob_len", "package main;\n" "let s: str = \"abcd\";\n" "fn take(x: str) int = { return x.len: int; };\n" "export fn main() i32 = { return take(s): i32; };\n", 4 }, /* str_glob_ptr — global str by value, callee reads x[0] (proves the * ptr word survived the push). 'a' == 97. */ { "str_glob_ptr", "package main;\n" "let s: str = \"abcd\";\n" "fn take(x: str) int = { return x[0]: int; };\n" "export fn main() i32 = { return take(s): i32; };\n", 97 }, /* slice_glob_len — global slice by value, callee reads .len. Pre-fix * wwstage drops len/cap → garbage; the TY_SLICE BX-base arm. */ { "slice_glob_len", "package main;\n" "let g: []int = [10, 20, 30, 40, 50];\n" "fn take(x: []int) int = { return x.len: int; };\n" "export fn main() i32 = { return take(g): i32; };\n", 5 }, /* slice_glob_idx — global slice by value, callee reads x[2] (proves * ptr survived the push). */ { "slice_glob_idx", "package main;\n" "let g: []int = [10, 20, 30, 40, 50];\n" "fn take(x: []int) int = { return x[2]; };\n" "export fn main() i32 = { return take(g): i32; };\n", 30 }, /* def_str_len — THE #21 regression pin: a module-level `def s: str` * passed BY VALUE, callee reads x.len. Pre-fix wwstage drops len/cap * (scalar single-PUSHQ default); cstage type-keyed pushes all 3 words. * A def has NO DATA symbol — it rides cgident's const-fold (LEAQ _S_x, * MOVQ $len BX, $len CX), then the type-keyed str triple push. */ { "def_str_len", "package main;\n" "def s: str = \"abcd\";\n" "fn take(x: str) int = { return x.len: int; };\n" "export fn main() i32 = { return take(s): i32; };\n", 4 }, /* def_str_ptr — def-str by value, callee reads x[0] (proves the ptr * word survived the push). 'a' == 97. */ { "def_str_ptr", "package main;\n" "def s: str = \"abcd\";\n" "fn take(x: str) int = { return x[0]: int; };\n" "export fn main() i32 = { return take(s): i32; };\n", 97 }, /* def_str_return — a def-str flowed through a fn return: give()'s * `return s` leaves the const-folded str header (AX=ptr/BX=len/CX=cap), * and the outer `take(give())` arg rides nodeisstr's pre-existing N_CALL * arm (give returns str) for the 3-word push. A #21 SIBLING, not the * N_IDENT def arm — it is byte-id-IDENTICAL pre- and post-fix (no teeth * of its own), kept as a convergence guard that a def-str surviving a fn * return stays a full str header on both stages. */ { "def_str_return", "package main;\n" "def s: str = \"hello\";\n" "fn give() str = { return s; };\n" "fn take(x: str) int = { return x.len: int; };\n" "export fn main() i32 = { return take(give()): i32; };\n", 5 }, /* ctrl_local_str — a LOCAL str arg (off!=0). cgen unchanged; pins the * local-str push path stays byte-id (no regress). */ { "ctrl_local_str", "package main;\n" "fn take(x: str) int = { return x.len: int; };\n" "export fn main() i32 = {\n" "\tlet s: str = \"ab\";\n" "\treturn take(s): i32;\n" "};\n", 2 }, /* ctrl_local_sl — a LOCAL []int arg (off!=0). cgen unchanged; pins the * local-slice push path stays byte-id (no regress). */ { "ctrl_local_sl", "package main;\n" "fn take(x: []int) int = { return x[1]; };\n" "export fn main() i32 = {\n" "\tlet g: []int = [7, 8, 9];\n" "\treturn take(g): i32;\n" "};\n", 8 }, }; 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/ssg_%d_%d.ww", getpid(), i); snprintf(tmpdir, sizeof tmpdir, "/tmp/ssg_%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 2>/dev/null", 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; } /* asm_byte_identical — w6c vs w6c_ww .s for the same source must match. */ static int asm_byte_identical(const char *bin, const struct row *r, int i) { char src[64], cs[64], ws[64], cmd[1024]; snprintf(src, sizeof src, "/tmp/ssg_asm_%d_%d.ww", getpid(), i); snprintf(cs, sizeof cs, "/tmp/ssg_asm_%d_%d_c.s", getpid(), i); snprintf(ws, sizeof ws, "/tmp/ssg_asm_%d_%d_w.s", getpid(), i); FILE *f = fopen(src, "wb"); if (!f) return -1; fputs(r->src, f); fclose(f); snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: w6c errored\n", r->label); unlink(src); return -1; } snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", bin, ws, src); if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); unlink(src); unlink(cs); return -1; } FILE *fc = fopen(cs, "rb"); FILE *fw = fopen(ws, "rb"); int rc = 0; if (!fc || !fw) { rc = -1; } else { for (;;) { int a = fgetc(fc); int b = fgetc(fw); if (a != b) { rc = -1; break; } if (a == EOF) break; } } if (fc) fclose(fc); if (fw) fclose(fw); if (rc != 0) fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", r->label); unlink(src); unlink(cs); unlink(ws); return rc; } 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, "slice_str_global_arg: 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, "slice_str_global_arg[%s][%s]: exit=%d want=%d\n", drivers[d].name, rows[i].label, got, rows[i].want); fail++; } } } if (access(wdrv, X_OK) == 0) { for (int i = 0; i < n; i++) { total++; if (asm_byte_identical(bin, &rows[i], i) != 0) fail++; } } if (fail) { fprintf(stderr, "slice_str_global_arg: %d/%d fixtures failed\n", fail, total); return 1; } printf("slice_str_global_arg: %d/%d ok\n", total, total); return 0; }