From d985622cb1c2312ddc66c0f505d84b117b02c305 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 18 May 2026 23:56:58 +0900 Subject: [PATCH] selfhost+test: strlit-inline str-def value-load shape (#12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Class A silent miscompile. wwstage cgenexpr.ww cgident's bare-ident deflookup→true branch and cgdot's module-qualified leaf branch emitted `MOVQ .(SB), AX` for a `def MSG: str = "..."` value reference — a load from a SB symbol that emit_data never writes. Str defs are not laid out at SB; they live as interned strlits the .ptr/.len fold (post-#4c) and value-load consume. Cstage already strlit-inlines via Sdef walks #1 (case N_IDENT non-local) and #2 (case N_DOT untyped-lhs); wwstage now matches the (LEAQ _S_(SB), MOVQ $, BX) emit shape per rule 10. Surfaced by reviewer-def during #4c R3 while attempting option (B) for the cstage Sdef walks #1/#2 prefer-pass — both walks' cs-vs-ws byte-id sentinel rows could not pass while wwstage emitted the bogus DATAW shape. Filed as #12 and deferred until the wwstage emit shape was fixed. Unblocks #11 + #13 (cstage prefer-pass graduations). Latent: no in-tree corpus referenced a str def as a value (only as .ptr/.len via cgdot field-fold) prior to lib/strings c3 — same corpus-coverage-blind shape as the #4a-#4e graduations. 746_strdef_inline pins both sites with 2 rows: bare ident + mod-qualified. Each row asserts `LEAQ _S_` + `MOVQ $,` inside the caller TEXT before RET, anti-checks the pre-fix `.(SB)` symbol-load, and cs-vs-ws byte-id per row. 120/120 ok. ww2 == ww3 == ww4 byte-id holds. --- Makefile | 5 + selfhost/cmd/w6c/main.combined.ww | 38 +++++ selfhost/cmd/wcc/cgenexpr.ww | 38 +++++ selfhost/cmd/wwdump/main.combined.ww | 38 +++++ test/wcc/746_strdef_inline.c | 235 +++++++++++++++++++++++++++ 5 files changed, 354 insertions(+) create mode 100644 test/wcc/746_strdef_inline.c diff --git a/Makefile b/Makefile index 7e8fb66f..e8c49678 100644 --- a/Makefile +++ b/Makefile @@ -280,6 +280,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_fnparams_bare_leaf_shadow \ $(BIN)/test_fnret_bare_leaf_shadow \ $(BIN)/test_fnret34_modshadow \ + $(BIN)/test_strdef_inline \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -689,6 +690,10 @@ $(BIN)/test_fnret34_modshadow: test/wcc/745_fnret34_modshadow.c \ $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_strdef_inline: test/wcc/746_strdef_inline.c \ + $(BIN)/w6c $(BIN)/w6c_ww | $(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 114654ed..e80cde94 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -11467,7 +11467,26 @@ fn cgident(c: *cgen, n: *node) void = { return; }; // Top-level `def` constant — load from its DATA symbol. + // Str defs (rhs N_STRLIT) aren't laid out at a SB symbol; the + // MOVQ symname(SB) fallback below would emit a bogus reference + // (e.g. `alpha.MSG(SB)`, never DATAW-defined). Strlit-inline + // the (LEAQ ptr, MOVQ $len) pair instead, mirroring cstage + // Sdef walk #1 N_IDENT bare-load (cmd/w6c/cgen.c). Filed #12. if (deflookup(c, nm)) { + let drhs: *node = deflookuprhs(c, nm); + if (drhs != nil) { + if (drhs.kind == nkind.N_STRLIT) { + let bytes: str = drhs.str; + let lab: str = internstrlit(c, bytes); + emitline("\tLEAQ\t"); + os.write(1, lab.ptr, lab.len: u64); + emitline("(SB), AX\n"); + emitline("\tMOVQ\t$"); + emitint(bytes.len: i64); + emitline(", BX\n"); + return; + }; + }; emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); @@ -12636,6 +12655,25 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("(SB), AX\n"); return; }; + // `mod.MSG` where MSG is `def MSG: str = "..."` — + // strlit-inline matches cstage Sdef walk #2 in + // cmd/w6c/cgen.c N_DOT mod-qualified. Without this + // the MOVQ leaf(SB) fallback emits a bogus ref + // (`alpha.MSG(SB)`, never DATAW-defined). Filed #12. + let drhs: *node = deflookuprhs(c, fld); + if (drhs != nil) { + if (drhs.kind == nkind.N_STRLIT) { + let bytes: str = drhs.str; + let lab: str = internstrlit(c, bytes); + emitline("\tLEAQ\t"); + os.write(1, lab.ptr, lab.len: u64); + emitline("(SB), AX\n"); + emitline("\tMOVQ\t$"); + emitint(bytes.len: i64); + emitline(", BX\n"); + return; + }; + }; let mqop: str = localloadop(c, letvartnode(c, fld)); if (streq(mqop, "MOVQ")) { emitline("\tMOVQ\t"); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 7b66fea6..8345577d 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -552,7 +552,26 @@ fn cgident(c: *cgen, n: *node) void = { return; }; // Top-level `def` constant — load from its DATA symbol. + // Str defs (rhs N_STRLIT) aren't laid out at a SB symbol; the + // MOVQ symname(SB) fallback below would emit a bogus reference + // (e.g. `alpha.MSG(SB)`, never DATAW-defined). Strlit-inline + // the (LEAQ ptr, MOVQ $len) pair instead, mirroring cstage + // Sdef walk #1 N_IDENT bare-load (cmd/w6c/cgen.c). Filed #12. if (deflookup(c, nm)) { + let drhs: *node = deflookuprhs(c, nm); + if (drhs != nil) { + if (drhs.kind == nkind.N_STRLIT) { + let bytes: str = drhs.str; + let lab: str = internstrlit(c, bytes); + emitline("\tLEAQ\t"); + os.write(1, lab.ptr, lab.len: u64); + emitline("(SB), AX\n"); + emitline("\tMOVQ\t$"); + emitint(bytes.len: i64); + emitline(", BX\n"); + return; + }; + }; emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); @@ -1721,6 +1740,25 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("(SB), AX\n"); return; }; + // `mod.MSG` where MSG is `def MSG: str = "..."` — + // strlit-inline matches cstage Sdef walk #2 in + // cmd/w6c/cgen.c N_DOT mod-qualified. Without this + // the MOVQ leaf(SB) fallback emits a bogus ref + // (`alpha.MSG(SB)`, never DATAW-defined). Filed #12. + let drhs: *node = deflookuprhs(c, fld); + if (drhs != nil) { + if (drhs.kind == nkind.N_STRLIT) { + let bytes: str = drhs.str; + let lab: str = internstrlit(c, bytes); + emitline("\tLEAQ\t"); + os.write(1, lab.ptr, lab.len: u64); + emitline("(SB), AX\n"); + emitline("\tMOVQ\t$"); + emitint(bytes.len: i64); + emitline(", BX\n"); + return; + }; + }; let mqop: str = localloadop(c, letvartnode(c, fld)); if (streq(mqop, "MOVQ")) { emitline("\tMOVQ\t"); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index fd06c60a..371c487b 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -11467,7 +11467,26 @@ fn cgident(c: *cgen, n: *node) void = { return; }; // Top-level `def` constant — load from its DATA symbol. + // Str defs (rhs N_STRLIT) aren't laid out at a SB symbol; the + // MOVQ symname(SB) fallback below would emit a bogus reference + // (e.g. `alpha.MSG(SB)`, never DATAW-defined). Strlit-inline + // the (LEAQ ptr, MOVQ $len) pair instead, mirroring cstage + // Sdef walk #1 N_IDENT bare-load (cmd/w6c/cgen.c). Filed #12. if (deflookup(c, nm)) { + let drhs: *node = deflookuprhs(c, nm); + if (drhs != nil) { + if (drhs.kind == nkind.N_STRLIT) { + let bytes: str = drhs.str; + let lab: str = internstrlit(c, bytes); + emitline("\tLEAQ\t"); + os.write(1, lab.ptr, lab.len: u64); + emitline("(SB), AX\n"); + emitline("\tMOVQ\t$"); + emitint(bytes.len: i64); + emitline(", BX\n"); + return; + }; + }; emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); @@ -12636,6 +12655,25 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("(SB), AX\n"); return; }; + // `mod.MSG` where MSG is `def MSG: str = "..."` — + // strlit-inline matches cstage Sdef walk #2 in + // cmd/w6c/cgen.c N_DOT mod-qualified. Without this + // the MOVQ leaf(SB) fallback emits a bogus ref + // (`alpha.MSG(SB)`, never DATAW-defined). Filed #12. + let drhs: *node = deflookuprhs(c, fld); + if (drhs != nil) { + if (drhs.kind == nkind.N_STRLIT) { + let bytes: str = drhs.str; + let lab: str = internstrlit(c, bytes); + emitline("\tLEAQ\t"); + os.write(1, lab.ptr, lab.len: u64); + emitline("(SB), AX\n"); + emitline("\tMOVQ\t$"); + emitint(bytes.len: i64); + emitline(", BX\n"); + return; + }; + }; let mqop: str = localloadop(c, letvartnode(c, fld)); if (streq(mqop, "MOVQ")) { emitline("\tMOVQ\t"); diff --git a/test/wcc/746_strdef_inline.c b/test/wcc/746_strdef_inline.c new file mode 100644 index 00000000..ecb2f79b --- /dev/null +++ b/test/wcc/746_strdef_inline.c @@ -0,0 +1,235 @@ +/* + * 746_strdef_inline — sentinel for wwstage's str-def value-load shape + * (selfhost/cmd/wcc/cgenexpr.ww cgident bare-leaf + cgdot mod-qualified + * paths). Pins both reference shapes to emit the strlit-inline pair + * (LEAQ _S_(SB), AX; MOVQ $, BX) rather than the bogus DATAW + * symbol-load fallback (`MOVQ .(SB), AX` — the symbol is + * never defined because str defs aren't laid out at SB, they live as + * interned strlits the .ptr/.len fold and value-load consume). + * + * Pre-fix wwstage cgenexpr.ww:553 (bare ident → deflookup true branch) + * emitted `MOVQ .(SB), AX` — a load from a SB symbol + * that emit_data never writes. Same shape latent on the mod-qualified + * arm (cgenexpr.ww:1729 cgdot module-qualified leaf branch): bare- + * symbol fallback was reached even when the leaf was an Sdef-backed + * str. Both stages already had .ptr/.len field-fold via deflookuprhs + * (#4c); only the value-reference shape was broken. Surfaced by + * reviewer-def during #4c R3 — blocked cstage Sdef walks #1/#2 + * prefer-pass sentinels (#11, #13) from shipping because their cs-vs- + * ws byte-id rows could not pass while the wwstage emit shape was + * mismatched. + * + * Cstage emits the strlit-inline pair via Sdef walks #1 (N_IDENT bare + * load, cmd/w6c/cgen.c case N_IDENT non-local) and #2 (N_DOT mod- + * qualified, case N_DOT untyped-lhs). Both walks have already been in + * place; this commit aligns wwstage UP to match per rule 10. + * + * Class A silent miscompile — every bare/qualified `MSG` value + * reference where `def MSG: str = "..."` would have loaded garbage + * from a never-defined SB symbol at runtime (the linker would have + * rejected the asm, but in test isolation the symbol resolves to 0). + * Latent: no in-tree corpus referenced an str def as a value (only as + * `.ptr`/`.len` via cgdot field-fold) prior to lib/strings c3. + * + * Pin: 2 rows. Row 1 (bare ident, single module) sentinel-flips the + * cgenexpr.ww:555 deflookup-strlit-inline branch on wwstage — revert + * the branch and row 1 fails (`MOVQ\talpha.MSG(SB)` appears instead + * of `LEAQ\t_S_`). Row 2 (mod-qualified, two modules) sentinel-flips + * the cgenexpr.ww:1730 cgdot deflookup-strlit-inline branch — revert + * and row 2 fails the same way (`MOVQ\talpha.MSG(SB)` appears in + * beta.b). Asserts the strlit-inline pair lands inside the right + * TEXT sym (LEAQ\t_S_ + MOVQ\t$,) with the bad_movq + * anti-check on each stage plus cs-vs-ws byte-id per row. + */ +#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; + const char *textsym; /* TEXT sym containing the load */ + const char *want_lea; /* must appear: strlit address load */ + const char *want_len; /* must appear: strlit length immediate */ + const char *bad_movq; /* must NOT appear: bogus SB symbol load */ +}; + +/* Strlit length 39 (not aliased to common frame/offset immediates + * 0/8/16/24/32/40/48). Single source-order def per row so + * the choice of Sdef entry is unambiguous regardless of head-walk vs + * prefer-pass — this commit fixes the EMIT SHAPE, not the lookup + * ordering (see #11, #13 for the lookup-side prefer-pass graduations + * that #12 unblocks). */ +static const struct row rows[] = { + { "bare_ident_strdef", + "package alpha;\n" + "def MSG: str = \"strdef_inline_pin_aaaaaaaaaaaaaaa_43chr\";\n" + "export fn afn() str = { return MSG; };\n" + "export fn main() i32 = { return 0; };\n", + "TEXT alpha.afn", "LEAQ\t_S_", "MOVQ\t$39,", "alpha.MSG(SB)" }, + { "mod_qualified_strdef", + "package alpha;\n" + "def MSG: str = \"strdef_inline_pin_aaaaaaaaaaaaaaa_43chr\";\n" + "package beta;\n" + "import alpha;\n" + "export fn bfn() str = { return alpha.MSG; };\n" + "export fn main() i32 = { return 0; };\n", + "TEXT beta.bfn", "LEAQ\t_S_", "MOVQ\t$39,", "alpha.MSG(SB)" }, +}; + +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; +} + +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/sdi_%d_%d.ww", getpid(), i); + snprintf(out_s, cap, "/tmp/sdi_%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; +} + +/* Inside the named TEXT sym, before its first RET, want_lea AND + * want_len MUST appear and bad_movq MUST NOT. bad_movq flags pre-fix + * cgenexpr.ww:555 / :1730 falling through to the bogus DATAW symbol- + * load. */ +static int +check_emit(const char *spath, const struct row *r, const char *stage) +{ + char buf[1 << 14]; + if (slurp(spath, buf, sizeof buf) < 0) { + fprintf(stderr, "row[%s][%s]: cannot read %s\n", + r->label, stage, spath); + return -1; + } + const char *fn = strstr(buf, r->textsym); + if (!fn) { + fprintf(stderr, "row[%s][%s]: no %s in %s\n", + r->label, stage, r->textsym, spath); + return -1; + } + const char *ret = strstr(fn, "\tRET"); + if (!ret) { + fprintf(stderr, "row[%s][%s]: no RET inside %s\n", + r->label, stage, r->textsym); + return -1; + } + const char *lea = strstr(fn, r->want_lea); + if (!lea || lea >= ret) { + fprintf(stderr, + "row[%s][%s]: want_lea %s missing inside %s\n", + r->label, stage, r->want_lea, r->textsym); + return -1; + } + const char *len = strstr(fn, r->want_len); + if (!len || len >= ret) { + fprintf(stderr, + "row[%s][%s]: want_len %s missing inside %s\n", + r->label, stage, r->want_len, r->textsym); + return -1; + } + const char *bad = strstr(fn, r->bad_movq); + if (bad && bad < ret) { + fprintf(stderr, + "row[%s][%s]: bad_movq %s present inside %s — bogus SB load\n", + r->label, stage, r->bad_movq, r->textsym); + return -1; + } + return 0; +} + +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]; + + if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { + fprintf(stderr, + "strdef_inline[cstage][%s]: w6c failed\n", + rows[i].label); + fail++; total++; continue; + } + total++; + if (check_emit(cs_path, &rows[i], "cstage") != 0) fail++; + + if (!have_ww) { unlink(cs_path); continue; } + + if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { + fprintf(stderr, + "strdef_inline[wwstage][%s]: w6c_ww failed\n", + rows[i].label); + fail++; total++; + unlink(cs_path); continue; + } + total++; + if (check_emit(ws_path, &rows[i], "wwstage") != 0) fail++; + + total++; + char cmd[512]; + snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); + if (runwait(cmd) != 0) { + fprintf(stderr, + "strdef_inline[%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + + unlink(cs_path); unlink(ws_path); + } + + if (fail) { + fprintf(stderr, + "strdef_inline: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("strdef_inline: %d/%d ok\n", total, total); + return 0; +}