Class A silent miscompile, surfaced by landing strings.slice in Hare's natural delegation form `fromutf8_unsafe(utf8.slice(begin, end))` (ref/hare/strings/iter.ha:75). strings.slice itself returns str, so the inner utf8.slice (cross-module N_DOT) call's cgcall return-ABI fixup hit post-#4e fnretlookup's same-module-first walk and grabbed strings.slice's own str return — emitted a spurious `MOVQ DX, BX` after the cross-module CALL even though utf8.slice returns []u8 (selfhost/cmd/wcc/cgenexpr.ww cgcall return-ABI fixup, line 3249-3261 pre-fix). Every other consumer of cgcall:3249's str-shuffle decision sat on the same bare-leaf table and was silently miscompiling on the same collision shape pre-#34. Sibling: nodeisslice + nodeisstr N_CALL arms in selfhost/cmd/wcc/cgenutil.ww were N_IDENT-only — for a cross- module N_DOT call returning a slice or str, pushargsrev fell through to the natural 1-word PUSHQ AX, dropping the `.len` (and `.cap` for slices) of the return value when consumed as a call arg. strings.slice's body passes utf8.slice's []u8 result to fromutf8_unsafe; pre-fix wwstage pushed 1 word vs cstage's 3, breaking the receiver's slice-3-pop drain. Cstage carries no sister bug: cmd/w6c/cgen.c reads return shape from the typed `n->lhs->type` (TY_FN sig) for both str-shuffle and slice-/str-arg push counts — module-aware via the typed AST, sidestepping any bare-leaf table. Mirror of #4e's cstage-no- sister-bug note. Fix: route cgcall return-ABI fixup + nodeisslice/nodeisstr N_CALL arms through fnretlookupmod with `callee.lhs.str` (N_DOT qualifier) or `c.curmod` (N_IDENT). Mirror of #28 fnparamslookupmod / #31 fnretlookupmod N_DOT re-routing. Remaining bare-leaf fnretlookup consumer sites (~8 sites across cgenexpr/cgenutil/cgenstmt/cgendecl listed in task #34a) stay on the graduated bare-leaf path — none of the present-corpus N_DOT leaf collisions have return-shape divergence at those sites. A future stdlib port introducing a return-shape-divergent same-leaf N_DOT collision will need the *mod re-routing — filed as #34a sibling-latents. Bundled three concerns per rule 11: cgcall fix, nodeisslice/ nodeisstr fix, and strings.slice retire + sentinel. (a) alone leaves strings.slice byte-id breaking on slice-arg push count. (b) alone leaves a phantom MOVQ DX, BX on the inner cross- module CALL. (c) alone fails 995_self_rebuild without (a)+(b). The three cannot land separately bisect-cleanly; the 745 sentinel pins the primary repro (cgcall str-shuffle) which sentinel-flips on a cgcall:3257 revert. 745_fnret34_modshadow pins the fix with 1 row: caller.slice returns str (same leaf as the cross-module callee, divergent return shape); caller.run calls myutf8.slice returning []u8. Asserts CALL myutf8.slice present inside caller.run TEXT + `MOVQ DX, BX` anti-check on each stage plus cs-vs-ws byte-id. strings.slice retired in lib/strings/strings.ww: the deferral block becomes the natural Hare delegation form with two local utf8.decoder reconstructions for the iterator endpoints — ww has no anonymous-embed (parallel to the existing `move` helper). iter_slice_cases mirrors ref/hare/strings/iter.ha:110-127; sidesteps the Hare `let t = s;` iterator-copy via fresh strings.iter() to stay clear of #35's sibling latents. 119/119 ok. ww2 == ww3 == ww4 byte-id holds.
228 lines
6.9 KiB
C
228 lines
6.9 KiB
C
/*
|
|
* 745_fnret34_modshadow — sentinel for task #34 (sub-bug of #4e):
|
|
* wwstage's cgcall return-ABI fixup keyed bare-leaf fnretlookup, not
|
|
* fnretlookupmod. For N_DOT cross-module callees the post-#4e same-
|
|
* module-first walk grabbed the caller-module's same-leaf fn's
|
|
* return type — when that caller-side fn returned `str` and the
|
|
* cross-module callee returned a non-str ([]u8, struct, scalar) the
|
|
* `isstrtype(c, rt)` guard fired against the wrong type and emitted
|
|
* a spurious `MOVQ DX, BX` shuffle after the cross-module CALL
|
|
* (selfhost/cmd/wcc/cgenexpr.ww cgcall return-ABI fixup).
|
|
*
|
|
* Sibling: nodeisslice / nodeisstr N_CALL arms were N_IDENT-only
|
|
* (selfhost/cmd/wcc/cgenutil.ww), so pushargsrev's slice/str word-
|
|
* count fell through to the 1-word natural-push for any cross-
|
|
* module N_DOT call result — dropping `.len` (and `.cap` for
|
|
* slices). Hare's `strings.slice` body
|
|
* (`fromutf8_unsafe(utf8.slice(begin, end))`, ref/hare/strings/
|
|
* iter.ha:75-77) hits both bugs at once because strings.slice
|
|
* itself returns str (str-shuffle phantom on the inner utf8.slice
|
|
* call) AND passes the inner []u8 to a same-module fn (.len/.cap
|
|
* dropped on the push).
|
|
*
|
|
* Cstage carries no sister bug: cmd/w6c/cgen.c reads the return
|
|
* shape from the typed `n->lhs->type` (TY_FN sig), module-aware via
|
|
* the typed AST. Both cgcall's str-shuffle and the slice/str-arg
|
|
* push count come off n->type directly. Mirror of #4e: cstage
|
|
* sidesteps every fnretlookup-keyed bare-leaf table.
|
|
*
|
|
* Pin: row 1 — caller-module exports `fn slice(s: str) str` (same
|
|
* leaf as the cross-module callee, return shape diverges); calls
|
|
* `myutf8.slice(&d)` returning []u8. Pre-fix wwstage emits a phantom
|
|
* `MOVQ DX, BX` after the CALL (and drops the slice 3-word push if
|
|
* the result is consumed as a slice arg). Post-fix the TEXT sym
|
|
* for caller.run carries no `MOVQ DX, BX` between the CALL and the
|
|
* RET. Asserts CALL myutf8.slice present + bad_imm anti-check on
|
|
* each stage plus cs-vs-ws byte-id.
|
|
*/
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <sys/wait.h>
|
|
|
|
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 call */
|
|
const char *want_imm; /* substring that MUST appear */
|
|
const char *bad_imm; /* substring that MUST NOT appear */
|
|
};
|
|
|
|
/* caller.slice (same leaf as myutf8.slice, returns str) lives in the
|
|
* same source so caller's fnret table holds a slice-named fn returning
|
|
* str. caller.run calls the cross-module myutf8.slice (returning []u8).
|
|
* Pre-fix wwstage's cgcall fnretlookup picks caller.slice (same-module-
|
|
* first) and emits a phantom MOVQ DX, BX after the cross-module CALL. */
|
|
static const struct row rows[] = {
|
|
{ "cross_module_same_leaf_str_shuffle",
|
|
"package caller;\n"
|
|
"import myutf8;\n"
|
|
"export fn main() i32 = { return 0; };\n"
|
|
"export fn slice(s: str) str = { return s; };\n"
|
|
"export fn run() i32 = {\n"
|
|
"\tlet d: myutf8.decoder;\n"
|
|
"\td.x = 0;\n"
|
|
"\tlet s: []u8 = myutf8.slice(&d);\n"
|
|
"\treturn s.len;\n"
|
|
"};\n"
|
|
"package myutf8;\n"
|
|
"export type decoder = struct { x: i32 };\n"
|
|
"export fn slice(d: *myutf8.decoder) []u8 = {\n"
|
|
"\tlet r: []u8;\n"
|
|
"\tr.ptr = nil: *u8;\n"
|
|
"\tr.len = d.x;\n"
|
|
"\tr.cap = d.x;\n"
|
|
"\treturn r;\n"
|
|
"};\n",
|
|
"TEXT caller.run", "CALL\tmyutf8.slice", "MOVQ\tDX, BX" },
|
|
};
|
|
|
|
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/fr34_%d_%d.ww", getpid(), i);
|
|
snprintf(out_s, cap, "/tmp/fr34_%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_imm MUST
|
|
* appear and bad_imm MUST NOT. bad_imm flags pre-fix wwstage str-
|
|
* shuffle firing on a non-str cross-module callee. */
|
|
static int
|
|
check_imm(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 *good = strstr(fn, r->want_imm);
|
|
if (!good || good >= ret) {
|
|
fprintf(stderr,
|
|
"row[%s][%s]: want_imm %s missing inside %s\n",
|
|
r->label, stage, r->want_imm, r->textsym);
|
|
return -1;
|
|
}
|
|
const char *bad = strstr(fn, r->bad_imm);
|
|
if (bad && bad < ret) {
|
|
fprintf(stderr,
|
|
"row[%s][%s]: bad_imm %s present inside %s — wrong-module str-shuffle phantom fired\n",
|
|
r->label, stage, r->bad_imm, 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,
|
|
"fnret34_modshadow[cstage][%s]: w6c failed\n",
|
|
rows[i].label);
|
|
fail++; total++; continue;
|
|
}
|
|
total++;
|
|
if (check_imm(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,
|
|
"fnret34_modshadow[wwstage][%s]: w6c_ww failed\n",
|
|
rows[i].label);
|
|
fail++; total++;
|
|
unlink(cs_path); continue;
|
|
}
|
|
total++;
|
|
if (check_imm(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,
|
|
"fnret34_modshadow[%s]: cstage vs wwstage asm differs\n",
|
|
rows[i].label);
|
|
fail++;
|
|
}
|
|
|
|
unlink(cs_path); unlink(ws_path);
|
|
}
|
|
|
|
if (fail) {
|
|
fprintf(stderr,
|
|
"fnret34_modshadow: %d/%d fixtures failed\n",
|
|
fail, total);
|
|
return 1;
|
|
}
|
|
printf("fnret34_modshadow: %d/%d ok\n", total, total);
|
|
return 0;
|
|
}
|