diff --git a/Makefile b/Makefile index a1e0d740..77a87e74 100644 --- a/Makefile +++ b/Makefile @@ -369,6 +369,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_xmod_valglobal_run \ $(BIN)/test_xmod_valglobal_dot_run \ $(BIN)/test_len_strglobal_run \ + $(BIN)/test_litstr_pseudo_run \ $(BIN)/test_tuple_sret_callee \ $(BIN)/test_tuple_sret_receive_run \ $(BIN)/test_struct_tuple_field_slot \ @@ -875,6 +876,15 @@ $(BIN)/test_len_strglobal_run: test/wcc/797_len_strglobal_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #14: a string LITERAL `.len` is TY_UNTYPED_STR, so it misses the typed +# slice/str pseudo-field gate and fell to the N_DOT base-eval fallback — +# cstage returned AX=.ptr instead of shuffling BX=.len into AX (wwstage +# already did). Runtime + cs==ww byte-id; `.ptr` unchanged (control row). +$(BIN)/test_litstr_pseudo_run: test/wcc/801_litstr_pseudo_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # #10 Fold A (wide tuple-return / sret, CALLEE side): an over-cap tuple # return (> 4 GP or > 2 SSE eightbytes) now compiles via sret instead of # loud-stopping at the SEND. Compile + cs==ww byte-id only — the receive diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 1c42fcb5..e00cc589 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -8433,8 +8433,15 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX)); break; } - /* fall through to base evaluation; result placeholder */ + /* Non-ident / untyped-str base pseudo-field: e.g. `"abc".len` + * / `"abc".ptr`. A string literal is TY_UNTYPED_STR, not + * TY_STR, so it misses the typed slice/str gate above and + * lands here. cgexpr leaves (AX=ptr, BX=len); `.ptr` keeps AX, + * `.len` shuffles BX→AX. Mirrors wwstage cgdot's catch-all + * (selfhost/cmd/wcc/cgenexpr.ww). #14. */ cgexpr(c, n->lhs, locals); + if (lenfld) + ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); dot_done: break; } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index d8b00378..8bab1485 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -22616,10 +22616,11 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; // Non-ident base pseudo-field: e.g. `"abc".ptr` / `"abc".len`. - // Evaluate the str-producing expression — that leaves - // (AX=ptr, BX=len). Then `.ptr` returns AX as is; `.len` - // shuffles BX→AX. Mirrors what C cgen does (it just evaluates - // the literal and picks the half it wants). + // A string literal is TY_UNTYPED_STR, so it misses the typed + // slice/str gate above and lands here. Evaluate the str-producing + // expression — that leaves (AX=ptr, BX=len). Then `.ptr` returns + // AX as is; `.len` shuffles BX→AX. cstage cgen.c was aligned UP + // to this shuffle in #14 (it had returned the ptr for `.len`). if (streq(fld, "ptr")) { cgexpr(c, lhs); return; }; if (streq(fld, "len")) { cgexpr(c, lhs); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 69e2a35a..ddf2e7f3 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -2880,10 +2880,11 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; // Non-ident base pseudo-field: e.g. `"abc".ptr` / `"abc".len`. - // Evaluate the str-producing expression — that leaves - // (AX=ptr, BX=len). Then `.ptr` returns AX as is; `.len` - // shuffles BX→AX. Mirrors what C cgen does (it just evaluates - // the literal and picks the half it wants). + // A string literal is TY_UNTYPED_STR, so it misses the typed + // slice/str gate above and lands here. Evaluate the str-producing + // expression — that leaves (AX=ptr, BX=len). Then `.ptr` returns + // AX as is; `.len` shuffles BX→AX. cstage cgen.c was aligned UP + // to this shuffle in #14 (it had returned the ptr for `.len`). if (streq(fld, "ptr")) { cgexpr(c, lhs); return; }; if (streq(fld, "len")) { cgexpr(c, lhs); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index f64d886d..597d0a5a 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -22616,10 +22616,11 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; // Non-ident base pseudo-field: e.g. `"abc".ptr` / `"abc".len`. - // Evaluate the str-producing expression — that leaves - // (AX=ptr, BX=len). Then `.ptr` returns AX as is; `.len` - // shuffles BX→AX. Mirrors what C cgen does (it just evaluates - // the literal and picks the half it wants). + // A string literal is TY_UNTYPED_STR, so it misses the typed + // slice/str gate above and lands here. Evaluate the str-producing + // expression — that leaves (AX=ptr, BX=len). Then `.ptr` returns + // AX as is; `.len` shuffles BX→AX. cstage cgen.c was aligned UP + // to this shuffle in #14 (it had returned the ptr for `.len`). if (streq(fld, "ptr")) { cgexpr(c, lhs); return; }; if (streq(fld, "len")) { cgexpr(c, lhs); diff --git a/test/wcc/801_litstr_pseudo_run.c b/test/wcc/801_litstr_pseudo_run.c new file mode 100644 index 00000000..6c424cc4 --- /dev/null +++ b/test/wcc/801_litstr_pseudo_run.c @@ -0,0 +1,185 @@ +/* + * 801_litstr_pseudo_run — BUG #14. Runtime + cs==ww byte-id net for the + * string-LITERAL `.len` / `.ptr` pseudo-field. + * + * THE BUG (cstage wrong, wwstage correct — rule-10 divergence): + * `"hi".len` returned the POINTER, not the length. A string literal is + * TY_UNTYPED_STR (not TY_STR), so it missed the typed slice/str + * pseudo-field gate in cgen.c's N_DOT and fell to the final fallback + * `cgexpr(lhs)`, which leaves AX=.ptr — `.len` then returned AX (the + * ptr) instead of shuffling BX (the len) into AX. wwstage's cgdot + * catch-all (cgenexpr.ww) already did the BX->AX shuffle, so the two + * stages DISAGREED. byte-id was BLIND: no bootstrap source uses + * literal `.len` (devs hardcoded the lengths around it, e.g. + * `os.write(2, "...".ptr, 9u64)`), so the gate never exercised it. + * The str-VARIABLE `.len` path was correct on both (control). + * + * THE FIX (#14): align cstage UP to wwstage — the final N_DOT fallback + * emits `MOVQ BX, AX` for `.len` (cgen.c). `.ptr` already returned AX + * (correct) and is unchanged. + * + * EACH ROW CARRIES BOTH DIMENSIONS (795/796/797 model): + * (a) cstage `ww build` + run, asserting the exit — pins the converged + * asm is runtime-correct (literal lengths reach the program). + * (b) w6c vs w6c_ww `.s` cmp — FAILS if the stages diverge (they did, + * pre-fix: cstage emitted the ptr, wwstage the len). + * + * GATE POLARITY: must stay GREEN. A wrong exit means literal `.len`/`.ptr` + * regressed; a byte-id FAIL means the stages diverged (rule-10). + */ +#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_exit; }; + +static const struct row rows[] = { + /* literal .len: "hello".len == 5. Pre-fix cstage returned the ptr. */ + { "lit_len", + "package main;\n" + "export fn main() i32 = { return \"hello\".len: i32; };\n", 5 }, + /* empty + multi-byte: ""/"héllo\\n" — é is 2 UTF-8 bytes, so the + * literal is 7 bytes (h,é=2,l,l,o,\\n). Guards against a .ptr/.len + * length confusion (a ptr would not be 7). */ + { "lit_len_empty_multibyte", + "package main;\n" + "export fn main() i32 = { if (\"\".len != 0) { return 9; }; " + "return \"héllo\\n\".len: i32; };\n", 7 }, + /* literal .ptr still points at the bytes (deref first byte == 'h'), + * then .len == 5 — proves the fix left .ptr untouched. */ + { "lit_ptr_deref", + "package main;\n" + "export fn main() i32 = { let p: *u8 = \"hello\".ptr; " + "if (*p != 'h') { return 1; }; return \"hello\".len: i32; };\n", 5 }, + /* callee-side .len on a literal passed as a str arg — the real-world + * shape (os.write(1, lit.ptr, lit.len)) that surfaced #14. */ + { "lit_arg_passthrough", + "package main;\n" + "fn slen(s: str) i32 = { return s.len: i32; };\n" + "export fn main() i32 = { if (slen(\"abcdef\") != 6) { return 1; }; " + "return \"abcdef\".len: i32; };\n", 6 }, + { NULL, NULL, 0 } +}; + +static int +slurp_eq(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } + int rc = 0; + for (;;) { + int ca = fgetc(fa); + int cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + 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 w6c[1100], w6c_ww[1100]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + if (access(w6c_ww, X_OK) != 0) { + fprintf(stderr, "litstr_pseudo: w6c_ww missing — cannot run the " + "cs==ww byte-id gate\n"); + return 1; + } + + int n = 0, fail = 0; + for (int i = 0; rows[i].src; i++, n++) { + char src[64]; + snprintf(src, sizeof src, "/tmp/wwlsp_%d_%d.ww", getpid(), i); + FILE *f = fopen(src, "wb"); + if (f == NULL) { fail++; continue; } + fputs(rows[i].src, f); + fclose(f); + + /* (a) cstage build + run in a scratch dir. */ + char tmpdir[64]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/wwlsp_%d_d_%d", getpid(), i); + mkdir(tmpdir, 0755); + + char cmd[2048]; + snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s", + tmpdir, bin, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: cstage build failed\n", + rows[i].label); + fail++; + unlink(src); rmdir(tmpdir); + continue; + } + + char outbin[128]; + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + + int got = runwait(outbin); + if (got != rows[i].want_exit) { + fprintf(stderr, "row[%s]: cstage exit %d, want %d\n", + rows[i].label, got, rows[i].want_exit); + fail++; + } + unlink(outbin); rmdir(tmpdir); + + /* (b) cs==ww byte-id gate. */ + char cs_s[64], ws_s[64]; + snprintf(cs_s, sizeof cs_s, "/tmp/wwlsp_%d_%d_cs.s", getpid(), i); + snprintf(ws_s, sizeof ws_s, "/tmp/wwlsp_%d_%d_ww.s", getpid(), i); + + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, cs_s, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label); + fail++; unlink(src); continue; + } + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", + w6c_ww, ws_s, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww failed\n", rows[i].label); + fail++; unlink(src); unlink(cs_s); continue; + } + if (slurp_eq(cs_s, ws_s) != 0) { + fprintf(stderr, "row[%s]: cstage/wwstage .s DIFFER " + "(rule-10 byte-id violation)\n", rows[i].label); + fail++; + } + unlink(src); unlink(cs_s); unlink(ws_s); + } + + if (fail) { + fprintf(stderr, "%d/%d litstr pseudo-field tests failed\n", fail, n); + return 1; + } + printf("litstr_pseudo: %d/%d ok (cstage run + cs==ww byte-id)\n", n, n); + return 0; +}