diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 88e92d15..941c0a35 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -22614,14 +22614,18 @@ fn cgdot(c: *cgen, n: *node) void = { // .cap on a non-ident base (indexed element `t[i].cap`, call, // dot-slice): cgexpr leaves the full {ptr,len,cap} header via // cgslicehdr — shuffle CX→AX. The shuffle fires ONLY for a TYPED - // slice/str base (kind TY_SLICE/TY_STR after NAMED-chase); an untyped - // str literal (`"abc".cap`) leaves only AX=ptr/BX=len and must return - // AX unshuffled. Mirrors cstage cgen.c: the cap-shuffle lives in the - // typed pseudo-field branch (`u->kind == TY_SLICE/TY_STR`), never the - // untyped catch-all. #13 read-fix, sibling of the #20 store. + // slice/str base (kind TY_SLICE/TY_STR after NAMED-chase) that is NOT + // a bare string literal: N_STRLIT's cgexpr loads only AX=ptr/BX=len + // (cgen.ww), never a CX cap, so `"abc".cap` must return AX unshuffled. + // cstage reaches that outcome by typing N_STRLIT as untyped_str + // (cgen.c:8456 catch-all, no cap shuffle); the wwstage checker types + // N_STRLIT as `str` instead (check.ww:2322 vs cstage check.c:1079 — + // divergence filed separately), so the TY_STR kind-gate alone would + // wrongly fire. The N_STRLIT exclusion keeps this byte-identical with + // cstage. #13 read-fix, sibling of the #20 store. if (streq(fld, "cap")) { cgexpr(c, lhs); - if (lhs != nil) { + if (lhs != nil && lhs.kind != nkind.N_STRLIT) { let lu: *tinfo = lhs.type_: *tinfo; for (lu != nil && lu.kind == tykind.TY_NAMED) { lu = lu.under; }; if (lu != nil && (lu.kind == tykind.TY_SLICE diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 117f2fd9..8e752d66 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -2890,14 +2890,18 @@ fn cgdot(c: *cgen, n: *node) void = { // .cap on a non-ident base (indexed element `t[i].cap`, call, // dot-slice): cgexpr leaves the full {ptr,len,cap} header via // cgslicehdr — shuffle CX→AX. The shuffle fires ONLY for a TYPED - // slice/str base (kind TY_SLICE/TY_STR after NAMED-chase); an untyped - // str literal (`"abc".cap`) leaves only AX=ptr/BX=len and must return - // AX unshuffled. Mirrors cstage cgen.c: the cap-shuffle lives in the - // typed pseudo-field branch (`u->kind == TY_SLICE/TY_STR`), never the - // untyped catch-all. #13 read-fix, sibling of the #20 store. + // slice/str base (kind TY_SLICE/TY_STR after NAMED-chase) that is NOT + // a bare string literal: N_STRLIT's cgexpr loads only AX=ptr/BX=len + // (cgen.ww), never a CX cap, so `"abc".cap` must return AX unshuffled. + // cstage reaches that outcome by typing N_STRLIT as untyped_str + // (cgen.c:8456 catch-all, no cap shuffle); the wwstage checker types + // N_STRLIT as `str` instead (check.ww:2322 vs cstage check.c:1079 — + // divergence filed separately), so the TY_STR kind-gate alone would + // wrongly fire. The N_STRLIT exclusion keeps this byte-identical with + // cstage. #13 read-fix, sibling of the #20 store. if (streq(fld, "cap")) { cgexpr(c, lhs); - if (lhs != nil) { + if (lhs != nil && lhs.kind != nkind.N_STRLIT) { let lu: *tinfo = lhs.type_: *tinfo; for (lu != nil && lu.kind == tykind.TY_NAMED) { lu = lu.under; }; if (lu != nil && (lu.kind == tykind.TY_SLICE diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 4f0dc64b..e548d904 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -22614,14 +22614,18 @@ fn cgdot(c: *cgen, n: *node) void = { // .cap on a non-ident base (indexed element `t[i].cap`, call, // dot-slice): cgexpr leaves the full {ptr,len,cap} header via // cgslicehdr — shuffle CX→AX. The shuffle fires ONLY for a TYPED - // slice/str base (kind TY_SLICE/TY_STR after NAMED-chase); an untyped - // str literal (`"abc".cap`) leaves only AX=ptr/BX=len and must return - // AX unshuffled. Mirrors cstage cgen.c: the cap-shuffle lives in the - // typed pseudo-field branch (`u->kind == TY_SLICE/TY_STR`), never the - // untyped catch-all. #13 read-fix, sibling of the #20 store. + // slice/str base (kind TY_SLICE/TY_STR after NAMED-chase) that is NOT + // a bare string literal: N_STRLIT's cgexpr loads only AX=ptr/BX=len + // (cgen.ww), never a CX cap, so `"abc".cap` must return AX unshuffled. + // cstage reaches that outcome by typing N_STRLIT as untyped_str + // (cgen.c:8456 catch-all, no cap shuffle); the wwstage checker types + // N_STRLIT as `str` instead (check.ww:2322 vs cstage check.c:1079 — + // divergence filed separately), so the TY_STR kind-gate alone would + // wrongly fire. The N_STRLIT exclusion keeps this byte-identical with + // cstage. #13 read-fix, sibling of the #20 store. if (streq(fld, "cap")) { cgexpr(c, lhs); - if (lhs != nil) { + if (lhs != nil && lhs.kind != nkind.N_STRLIT) { let lu: *tinfo = lhs.type_: *tinfo; for (lu != nil && lu.kind == tykind.TY_NAMED) { lu = lu.under; }; if (lu != nil && (lu.kind == tykind.TY_SLICE diff --git a/test/wcc/683_arr_strslice_elem.c b/test/wcc/683_arr_strslice_elem.c index b0460f1f..cfee2fed 100644 --- a/test/wcc/683_arr_strslice_elem.c +++ b/test/wcc/683_arr_strslice_elem.c @@ -82,6 +82,13 @@ runwait(const char *cmd) return -1; } +/* want == BYTEID_ONLY: the row's runtime value is non-deterministic (a + * link-time address), so only the cstage/wwstage asm-byte-id is asserted; + * the build must still succeed on both stages. Used for the `"abc".cap` + * symmetry edge (a string-literal .cap is meaningless garbage on both + * stages, but rule 10 still requires byte-identical asm). */ +#define BYTEID_ONLY (-2147483647 - 1) + struct row { const char *label; const char *src; int want; }; static const struct row rows[] = { @@ -177,6 +184,20 @@ static const struct row rows[] = { "};\n", 2 }, + /* #13 symmetry edge: `.cap` of a BARE string literal. The wwstage + * checker types N_STRLIT as `str` (cstage types it untyped_str), so + * the .cap kind-gate would wrongly shuffle CX→AX on wwstage only — + * but a literal's cgexpr never loads a CX cap, and cstage never + * shuffles it, so both must return AX (ptr) unshuffled. Byte-id only: + * the value is a link-time address (non-deterministic). Pre-fix this + * row's asm DIFFERED (wwstage had the stray MOVQ CX, AX). */ + { "str_lit_cap_symmetry", + "package main;\n" + "export fn main() i32 = {\n" + "\treturn \"abc\".cap: i32;\n" + "};\n", + BYTEID_ONLY }, + { "slice_ptr", "package main;\n" "export fn main() i32 = {\n" @@ -349,7 +370,12 @@ main(void) for (int i = 0; i < n; i++) { int got = run_driver(drivers[d].path, &rows[i], i); total++; - if (got != rows[i].want) { + /* BYTEID_ONLY rows assert asm byte-id below; here only + * the build must succeed (run_driver returns -1 on a + * build/run failure). The exit value is ignored. */ + int bad = rows[i].want == BYTEID_ONLY + ? (got == -1) : (got != rows[i].want); + if (bad) { fprintf(stderr, "arr_strslice_elem[%s][%s]: exit=%d want=%d\n", drivers[d].name, rows[i].label,