diff --git a/Makefile b/Makefile index 0672122e..74ec50a2 100644 --- a/Makefile +++ b/Makefile @@ -284,6 +284,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_def_modqual_modshadow \ $(BIN)/test_size_strategy_convergence \ $(BIN)/test_sumtype_forward \ + $(BIN)/test_mklabel_modscoped \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -713,6 +714,12 @@ $(BIN)/test_sumtype_forward: test/wcc/749_sumtype_forward.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_mklabel_modscoped: test/wcc/750_mklabel_modscoped.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(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/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index ee5211e2..d833e962 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1028,8 +1028,12 @@ abranch(const char *s) static char * mklabel(Cg *c, const char *prefix) { - return aprintf(c->a, "%s_%s_%d", c->fnname ? c->fnname : "_", prefix, - c->labelseq++); + /* Module-qualified to avoid cross-module same-leaf collisions + * (task #13). w6a accepts '.' in label-cont (lex.c:18). */ + return aprintf(c->a, "%s%s%s_%s_%d", + c->cur_mod ? c->cur_mod : "", + c->cur_mod ? "." : "", + c->fnname ? c->fnname : "_", prefix, c->labelseq++); } static void diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index f718be70..e2d063e8 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -19382,13 +19382,23 @@ fn emitoff(v: i64) void = { if (v != 0i64) { emitint(v); }; }; -// mklabel — fresh label "__". Returns an -// arena-owned str. Mirrors C cgen's mklabel so diffs match. +// mklabel — fresh label ".__" (bare +// "_..." when curmod is empty). Returns an arena-owned str. +// Mirrors C cgen's mklabel so diffs match. Module-qualified to +// avoid cross-module same-leaf collisions (task #13); w6a accepts +// '.' in label-cont (lex.c:18). fn mklabel(c: *cgen, prefix: str) str = { let buf: [128]u8; let i: i32 = 0; - let fname: str = c.fnname; + let mname: str = c.curmod; let j: i32 = 0; + for (j < mname.len) { + buf[i] = mname[j]; + i += 1; j += 1; + }; + if (mname.len > 0) { buf[i] = 46u8; i += 1; }; // '.' + let fname: str = c.fnname; + j = 0; for (j < fname.len) { buf[i] = fname[j]; i += 1; j += 1; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 27e76497..8d3ccd3b 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -768,13 +768,23 @@ fn emitoff(v: i64) void = { if (v != 0i64) { emitint(v); }; }; -// mklabel — fresh label "__". Returns an -// arena-owned str. Mirrors C cgen's mklabel so diffs match. +// mklabel — fresh label ".__" (bare +// "_..." when curmod is empty). Returns an arena-owned str. +// Mirrors C cgen's mklabel so diffs match. Module-qualified to +// avoid cross-module same-leaf collisions (task #13); w6a accepts +// '.' in label-cont (lex.c:18). fn mklabel(c: *cgen, prefix: str) str = { let buf: [128]u8; let i: i32 = 0; - let fname: str = c.fnname; + let mname: str = c.curmod; let j: i32 = 0; + for (j < mname.len) { + buf[i] = mname[j]; + i += 1; j += 1; + }; + if (mname.len > 0) { buf[i] = 46u8; i += 1; }; // '.' + let fname: str = c.fnname; + j = 0; for (j < fname.len) { buf[i] = fname[j]; i += 1; j += 1; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 05d05cc0..f656e404 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -19382,13 +19382,23 @@ fn emitoff(v: i64) void = { if (v != 0i64) { emitint(v); }; }; -// mklabel — fresh label "__". Returns an -// arena-owned str. Mirrors C cgen's mklabel so diffs match. +// mklabel — fresh label ".__" (bare +// "_..." when curmod is empty). Returns an arena-owned str. +// Mirrors C cgen's mklabel so diffs match. Module-qualified to +// avoid cross-module same-leaf collisions (task #13); w6a accepts +// '.' in label-cont (lex.c:18). fn mklabel(c: *cgen, prefix: str) str = { let buf: [128]u8; let i: i32 = 0; - let fname: str = c.fnname; + let mname: str = c.curmod; let j: i32 = 0; + for (j < mname.len) { + buf[i] = mname[j]; + i += 1; j += 1; + }; + if (mname.len > 0) { buf[i] = 46u8; i += 1; }; // '.' + let fname: str = c.fnname; + j = 0; for (j < fname.len) { buf[i] = fname[j]; i += 1; j += 1; diff --git a/test/wcc/750_mklabel_modscoped.c b/test/wcc/750_mklabel_modscoped.c new file mode 100644 index 00000000..4b2ff92d --- /dev/null +++ b/test/wcc/750_mklabel_modscoped.c @@ -0,0 +1,328 @@ +/* + * 750_mklabel_modscoped — sentinel for task #13. mklabel pre-fix + * formatted labels as `__` (just the leaf). Two + * top-level fns in different modules sharing a leaf (e.g. `bytes.index` + * + `strings.index`) emitted colliding labels into the same combined + * .s; w6a's a_intern collapsed the names and the LAST definition's + * `s->addr` won — earlier fns' JNE/JMP rel32 resolved into the later + * fn's body. Silent miscompile in cstage; wwstage's mklabel had the + * same shape so the symmetric path was equally exposed (the wedge + * just happened to fire under cstage's combined link first). + * + * Fix (rule 10): `.__` mirrors the + * existing TEXT-directive convention (`TEXT bytes.index,$N`). w6a + * already accepts `.` in label-cont (lex.c:18) so the format is a + * valid Plan-9 symbol. + * + * row | what it pins + * ----------------------------|-------------------------------- + * two_modules_same_leaf | original wedge. mod1.locate / + * | mod2.locate both contain `match` + * | so both stamp `_match_next_1`. + * | Pre-fix run aborts on mis-jump + * | (exit=10); post-fix exit=0 and + * | mod1.locate_match_next_1 + + * | mod2.locate_match_next_1 BOTH + * | appear in the .s. + * bytes_strings_contains | the latent that flips live once + * | task #11 lands strings.index. + * | Force both bytes.contains and + * | strings.contains into the link; + * | confirm post-fix bytes.contains_* + * | and strings.contains_* labels + * | are distinct. + * same_module_same_leaf | non-regression: two fns in the + * | same module with the same `match` + * | shape still get unique labels via + * | the per-fn `labelseq` counter. + */ +#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; + int want_exit; + int n_files; + const char *files[8]; /* {path, src, path, src, ...} */ + const char *entry; /* file to `ww build` (relative) */ + int n_required; + const char *required[6]; /* substrings every emitted .s must contain */ +}; + +static const struct row rows[] = { + /* Original wedge shape. mod1.locate and mod2.locate both run a + * `match (needle) { case u8 / case []u8 }` over a `for` loop — + * both stamp `_match_next_1` and `_loop_2` etc. Pre-fix the + * second TEXT's labels overwrite the first's s->addr; mod1.locate + * misjumps and returns the wrong byte offset. Post-fix the + * module-qualified labels coexist. */ + { "two_modules_same_leaf", 0, 3, + { "mod1/mod1.ww", + "package mod1;\n" + "export fn locate(s: []u8, needle: (u8 | []u8)) (i32 | void) = {\n" + " match (needle) {\n" + " case let c: u8 => {\n" + " let i: i32 = 0;\n" + " for (i < s.len) {\n" + " if (s[i] == c) { return i; };\n" + " i += 1;\n" + " };\n" + " return;\n" + " };\n" + " case let sub: []u8 => { return; };\n" + " };\n" + " return;\n" + "};\n", + "mod2/mod2.ww", + "package mod2;\n" + "export fn locate(s: []u8, needle: (u8 | []u8)) (i32 | void) = {\n" + " match (needle) {\n" + " case let c: u8 => {\n" + " let i: i32 = 0;\n" + " for (i < s.len) {\n" + " if (s[i] == c) { return i + 100; };\n" + " i += 1;\n" + " };\n" + " return;\n" + " };\n" + " case let sub: []u8 => { return; };\n" + " };\n" + " return;\n" + "};\n", + "main.ww", + "package main;\n" + "import mod1;\n" + "import mod2;\n" + "export fn main() i32 = {\n" + " let arr: [3]u8 = [65u8, 66u8, 67u8];\n" + " let s: []u8 = arr[0:3];\n" + " let n: u8 = 66u8;\n" + " let needle: (u8 | []u8) = n;\n" + " let a: (i32 | void) = mod1.locate(s, needle);\n" + " let b: (i32 | void) = mod2.locate(s, needle);\n" + " let av: i32 = match (a) { case let v: i32 => yield v; " + "case void => yield -1; };\n" + " let bv: i32 = match (b) { case let v: i32 => yield v; " + "case void => yield -1; };\n" + " if (av != 1) { return 10; };\n" + " if (bv != 101) { return 20; };\n" + " return 0;\n" + "};\n" }, + "main.ww", 2, + { "mod1.locate_match_next_1", "mod2.locate_match_next_1" } }, + + /* The pre-located bytes.contains / strings.contains latent. Both + * libs already export `contains`; force both into the link so the + * pair coexists in main.s. */ + { "bytes_strings_contains", 0, 1, + { "main.ww", + "package main;\n" + "import bytes;\n" + "import strings;\n" + "export fn main() i32 = {\n" + " let s: str = \"abc\";\n" + " let b: []u8 = strings.toutf8(s);\n" + " let n: (u8 | []u8) = 98u8;\n" + " if (!bytes.contains(b, n)) { return 11; };\n" + " if (!strings.contains(s, 'b')) { return 12; };\n" + " return 0;\n" + "};\n" }, + "main.ww", 2, + { "bytes.contains_", "strings.contains_" } }, + + /* Non-regression: same module, two fns with the same `match` + * shape. Per-fn labelseq still gives unique labels — both fns' + * labels carry the same `pkg.fn_` prefix but differ in the seq + * suffix. */ + { "same_module_same_leaf", 0, 1, + { "main.ww", + "package main;\n" + "fn a(needle: (u8 | []u8)) i32 = {\n" + " match (needle) {\n" + " case let c: u8 => return 1;\n" + " case let s: []u8 => return 2;\n" + " };\n" + " return 0;\n" + "};\n" + "fn b(needle: (u8 | []u8)) i32 = {\n" + " match (needle) {\n" + " case let c: u8 => return 3;\n" + " case let s: []u8 => return 4;\n" + " };\n" + " return 0;\n" + "};\n" + "export fn main() i32 = {\n" + " let x: (u8 | []u8) = 7u8;\n" + " if (a(x) != 1) { return 30; };\n" + " if (b(x) != 3) { return 31; };\n" + " return 0;\n" + "};\n" }, + "main.ww", 2, + { "main.a_match_next_1", "main.b_match_next_1" } }, +}; + +/* Write `src` to `/`, mkdir'ing intermediate dirs. */ +static int +write_one(const char *dir, const char *rel, const char *src) +{ + char path[512]; + snprintf(path, sizeof path, "%s/%s", dir, rel); + /* mkdir parent if rel has a slash. */ + char *slash = strchr(path + strlen(dir) + 1, '/'); + if (slash) { + *slash = '\0'; + mkdir(path, 0755); + *slash = '/'; + } + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(src, f); + fclose(f); + return 0; +} + +static char * +slurp(const char *path, size_t *outsz) +{ + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return NULL; } + char *buf = malloc((size_t)n + 1); + if (!buf) { fclose(f); return NULL; } + size_t got = fread(buf, 1, (size_t)n, f); + fclose(f); + buf[got] = '\0'; + if (outsz) *outsz = got; + return buf; +} + +static int +run_row(const char *driver, const struct row *r, int idx, const char *tag) +{ + char tmpdir[64]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/mklabel_%s_%d_%d", + tag, getpid(), idx); + mkdir(tmpdir, 0755); + int fail = 0; + + for (int k = 0; k < r->n_files; k++) { + if (write_one(tmpdir, r->files[2 * k], + r->files[2 * k + 1]) != 0) { + fprintf(stderr, + "mklabel_modscoped[%s][%s]: write %s failed\n", + tag, r->label, r->files[2 * k]); + return 1; + } + } + + char cmd[2048]; + snprintf(cmd, sizeof cmd, + "cd %s && %s build %s >/dev/null 2>&1", tmpdir, driver, r->entry); + if (runwait(cmd) != 0) { + fprintf(stderr, + "mklabel_modscoped[%s][%s]: build failed\n", + tag, r->label); + fail++; + goto cleanup; + } + + /* Runtime check. */ + char entry_bin[512]; + snprintf(entry_bin, sizeof entry_bin, "%s/%s", tmpdir, r->entry); + char *dot = strrchr(entry_bin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(entry_bin); + if (got != r->want_exit) { + fprintf(stderr, + "mklabel_modscoped[%s][%s]: rc=%d want=%d\n", + tag, r->label, got, r->want_exit); + fail++; + } + + /* Required-substring check on the emitted .s. */ + char entry_s[512]; + snprintf(entry_s, sizeof entry_s, "%s/%s", tmpdir, r->entry); + char *d2 = strrchr(entry_s, '.'); + if (d2 && strcmp(d2, ".ww") == 0) { strcpy(d2, ".s"); } + size_t sz = 0; + char *asm_buf = slurp(entry_s, &sz); + if (!asm_buf) { + fprintf(stderr, + "mklabel_modscoped[%s][%s]: cannot read %s\n", + tag, r->label, entry_s); + fail++; + } else { + for (int k = 0; k < r->n_required; k++) { + if (strstr(asm_buf, r->required[k]) == NULL) { + fprintf(stderr, + "mklabel_modscoped[%s][%s]: missing label " + "substring \"%s\" in %s\n", + tag, r->label, r->required[k], entry_s); + fail++; + } + } + free(asm_buf); + } + +cleanup: + /* Best-effort cleanup. Leaving artifacts is fine on failure. */ + snprintf(cmd, sizeof cmd, "rm -rf %s", tmpdir); + (void)runwait(cmd); + return fail; +} + +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 cdrv[640], wdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + int have_ww = (access(wdrv, X_OK) == 0); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + total++; + fail += run_row(cdrv, &rows[i], i, "cs"); + if (have_ww) { + total++; + fail += run_row(wdrv, &rows[i], i, "ws"); + } + } + + if (fail) { + fprintf(stderr, + "mklabel_modscoped: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("mklabel_modscoped: %d/%d ok\n", total, total); + return 0; +}