Files
ww/test/wcc/750_mklabel_modscoped.c
Hojun-Cho a95a7a316b test/wcc: carrier ownership repair and driver-contract adaptation
Every surviving carrier now owns its artifacts: checked mkdir/mkdtemp/
fopen acquisition, one all-exit cleanup funnel per carrier, ENOENT-
tolerant checked unlinks, exact-path deletion (rm -rf only for an
owned pid-keyed dir or a .sepwork beneath one), and cleanup failure
fails a passing carrier without overwriting its diagnostic. In the
same pass the carriers adapt to the driver contract this branch lands:
--sep and WW_PKGCACHE are gone, -S and the /tmp/ww_run_<pid> scratch
contract are asserted, and rows whose runtime or reject coverage moved
to test/wcc/data fixtures or test/lang @test owners are trimmed to the
byte/artifact/diagnostic observations only they can make.

Repair and adaptation ride together because most files interleave both
in the same hunks; splitting would manufacture intermediate carrier
states that never existed and cannot run against either driver.
2026-08-07 23:21:04 +09:00

400 lines
13 KiB
C

/*
* 750_mklabel_modscoped — sentinel for task #13. mklabel pre-fix
* formatted labels as `<fnname>_<prefix>_<seq>` (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): `<module>.<fnname>_<prefix>_<seq>` 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.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;
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 `<dir>/<rel>`, mkdir'ing intermediate dirs. */
static int
write_one(const char *dir, const char *rel, const char *src,
int *parent_owned)
{
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';
if (mkdir(path, 0755) != 0) return -1;
*parent_owned = 1;
*slash = '/';
}
FILE *f = fopen(path, "wb");
if (!f) return -1;
int failed = fputs(src, f) == EOF;
if (fclose(f) != 0) failed = 1;
return failed ? -1 : 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[96];
snprintf(tmpdir, sizeof tmpdir, "/tmp/mklabel_%s_%d_%d",
tag, getpid(), idx);
if (mkdir(tmpdir, 0755) != 0) {
fprintf(stderr,
"mklabel_modscoped[%s][%s]: cannot acquire temp directory: %s\n",
tag, r->label, strerror(errno));
return 1;
}
int fail = 0;
int parent_owned[4] = { 0 };
char entry_bin[512], stem[512], alls[600], cmd[2048];
snprintf(entry_bin, sizeof entry_bin, "%s/%s", tmpdir, r->entry);
char *dot = strrchr(entry_bin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
snprintf(stem, sizeof stem, "%s/%s", tmpdir, r->entry);
char *d2 = strrchr(stem, '.');
if (d2 && strcmp(d2, ".ww") == 0) *d2 = '\0';
snprintf(alls, sizeof alls, "%s/all.s", tmpdir);
for (int k = 0; k < r->n_files; k++) {
if (write_one(tmpdir, r->files[2 * k],
r->files[2 * k + 1], &parent_owned[k]) != 0) {
fprintf(stderr,
"mklabel_modscoped[%s][%s]: write %s failed\n",
tag, r->label, r->files[2 * k]);
fail++;
goto cleanup;
}
}
/* #93 sep layout: emit asm to <entry-stem>.sepwork/<pkg>.s; pin
* all outputs stay under tmpdir. */
snprintf(cmd, sizeof cmd,
"cd %s && b='%s'; %s build "
"-o \"${b%%.ww}\" \"$b\" >/dev/null 2>&1",
tmpdir, r->entry, driver);
if (runwait(cmd) != 0) {
fprintf(stderr,
"mklabel_modscoped[%s][%s]: build failed\n",
tag, r->label);
fail++;
goto cleanup;
}
/* Runtime check. */
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. #93: root + every
* imported pkg now compile to separate <stem>.sepwork/<pkg>.s; the
* cross-module label substrings (mod1.x / strings.x) live in the
* per-package files, so concat ALL of them (sorted glob) — a single
* __root.s read would FALSE-FAIL the multi-package rows. */
char catcmd[1200];
snprintf(catcmd, sizeof catcmd, "cat %s.sepwork/*.s > %s 2>/dev/null",
stem, alls);
(void)runwait(catcmd);
size_t sz = 0;
char *asm_buf = slurp(alls, &sz);
if (!asm_buf) {
fprintf(stderr,
"mklabel_modscoped[%s][%s]: cannot read %s\n",
tag, r->label, alls);
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], alls);
fail++;
}
}
free(asm_buf);
}
cleanup: ;
/* The retained build scratch is the only recursively removed path. */
int cleanup_fail = 0;
char sepwork[600];
snprintf(sepwork, sizeof sepwork, "%s.sepwork", stem);
snprintf(cmd, sizeof cmd, "rm -rf -- '%s'", sepwork);
if (runwait(cmd) != 0) {
fprintf(stderr,
"mklabel_modscoped[%s][%s]: remove %s failed\n",
tag, r->label, sepwork);
cleanup_fail = 1;
}
const char *outputs[] = { alls, entry_bin };
for (size_t k = 0; k < sizeof outputs / sizeof outputs[0]; k++) {
if (unlink(outputs[k]) != 0 && errno != ENOENT) {
fprintf(stderr,
"mklabel_modscoped[%s][%s]: unlink %s: %s\n",
tag, r->label, outputs[k], strerror(errno));
cleanup_fail = 1;
}
}
for (int k = 0; k < r->n_files; k++) {
char path[512];
snprintf(path, sizeof path, "%s/%s", tmpdir, r->files[2 * k]);
char *slash = strchr(path + strlen(tmpdir) + 1, '/');
if (slash && !parent_owned[k]) continue;
if (unlink(path) != 0 && errno != ENOENT) {
fprintf(stderr,
"mklabel_modscoped[%s][%s]: unlink %s: %s\n",
tag, r->label, path, strerror(errno));
cleanup_fail = 1;
}
}
for (int k = r->n_files - 1; k >= 0; k--) {
char path[512];
snprintf(path, sizeof path, "%s/%s", tmpdir, r->files[2 * k]);
char *slash = strrchr(path, '/');
if (parent_owned[k] && slash && slash > path + strlen(tmpdir)) {
*slash = '\0';
if (rmdir(path) != 0 && errno != ENOENT) {
fprintf(stderr,
"mklabel_modscoped[%s][%s]: rmdir %s: %s\n",
tag, r->label, path, strerror(errno));
cleanup_fail = 1;
}
}
}
if (rmdir(tmpdir) != 0) {
fprintf(stderr,
"mklabel_modscoped[%s][%s]: rmdir %s: %s\n",
tag, r->label, tmpdir, strerror(errno));
cleanup_fail = 1;
}
if (cleanup_fail) fail++;
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;
}