w6c+selfhost: cross-module same-leaf type disambiguation via Sym.mod

This commit is contained in:
2026-05-15 10:26:20 +09:00
parent e349536f62
commit 66d6408cbe
15 changed files with 614 additions and 60 deletions

View File

@@ -0,0 +1,119 @@
/*
* 696_modtype_leaf_collision — same-leaf-name cross-module type
* disambiguation. Two modules each export `type stream` with
* intentionally distinct field sets; the consumer pins both via
* `mod1.stream` / `mod2.stream`.
*
* Pre-fix (before sym.mod tagging + scope_lookup_in_module): the
* second `type stream` registration in pass-1 failed with
* "duplicate type stream", or when both did survive they collapsed
* to whichever sym was first in the bucket chain. The negative case
* (`b: mod1.stream; return b.c;`) used to silently bind through the
* wrong type and the bogus field read would either crash at run
* time or return the wrong byte. Post-fix it must surface as a
* compile-time "no field 'c' in stream" error.
*
* Fixtures live in test/wcc/data/modcollision/. mod1/mod1.ww and
* mod2/mod2.ww are deliberately *new* source files so the test
* doesn't lean on bufio's `bstream` workaround (still in place
* until #21 lands).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.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;
}
static int
run_pos(const char *driver, const char *fixdir, const char *tag)
{
char cmd[2048];
/* cd into the fixture dir so the driver's source-dir-first import
* search resolves `use mod1; use mod2;`. */
snprintf(cmd, sizeof cmd,
"cd %s && %s build pos.ww >/dev/null 2>&1", fixdir, driver);
if (runwait(cmd) != 0) {
fprintf(stderr, "modtype_leaf[%s]: pos.ww build failed\n", tag);
return 1;
}
char bin[2048];
snprintf(bin, sizeof bin, "%s/pos", fixdir);
int got = runwait(bin);
unlink(bin);
/* 3 + 5 (mod1.stream) + 7 + 11 (mod2.stream) = 26. */
if (got != 26) {
fprintf(stderr,
"modtype_leaf[%s]: pos.ww exit=%d want=26\n", tag, got);
return 1;
}
return 0;
}
static int
run_neg(const char *driver, const char *fixdir, const char *tag)
{
char cmd[2048];
/* Build must fail with a field-resolution error: the `c` field
* lives on mod2.stream, not mod1.stream. If pre-fix sym-binding
* smashed mod1.stream's identity into mod2.stream's slot, this
* would silently succeed. */
snprintf(cmd, sizeof cmd,
"cd %s && %s build neg.ww >/dev/null 2>&1", fixdir, driver);
int rc = runwait(cmd);
char bin[2048];
snprintf(bin, sizeof bin, "%s/neg", fixdir);
unlink(bin);
if (rc == 0) {
fprintf(stderr,
"modtype_leaf[%s]: neg.ww built but should have errored\n",
tag);
return 1;
}
return 0;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[1024];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cdrv[1024];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
char fixdir[1024];
if (getcwd(fixdir, sizeof fixdir) == NULL) return 1;
size_t cwd_n = strlen(fixdir);
const char *rel = "/test/wcc/data/modcollision";
if (cwd_n + strlen(rel) + 1 >= sizeof fixdir) return 1;
memcpy(fixdir + cwd_n, rel, strlen(rel) + 1);
int fail = 0;
fail += run_pos(cdrv, fixdir, "cstage");
fail += run_neg(cdrv, fixdir, "cstage");
if (fail) {
fprintf(stderr,
"modtype_leaf_collision: %d case(s) failed\n", fail);
return 1;
}
printf("modtype_leaf_collision: 2/2 ok\n");
return 0;
}

View File

@@ -0,0 +1,8 @@
// modcollision/mod1 — exports `type stream` with mod1-shaped fields.
// Paired with mod2/mod2.ww to exercise same-leaf-name cross-module
// type disambiguation. Test driver: 696_modtype_leaf_collision.c.
export type stream = struct {
a: i32,
b: i32,
};

View File

@@ -0,0 +1,12 @@
// modcollision/mod2 — exports `type stream` with mod2-shaped fields.
// Paired with mod1/mod1.ww to exercise same-leaf-name cross-module
// type disambiguation. Test driver: 696_modtype_leaf_collision.c.
//
// Fields are intentionally named distinctly from mod1's (c/d vs a/b)
// so the negative test (`b: mod1.stream` accessed via mod2-only field
// 'c') surfaces as a compile-time field-resolution error.
export type stream = struct {
c: i32,
d: i32,
};

View File

@@ -0,0 +1,19 @@
// Negative case: declare `b: mod1.stream` then access a mod2-only
// field. With the mod-tagged scope lookup `b` resolves to mod1.stream
// (fields a, b), so the field access `b.c` must be a compile-time
// error rather than silently binding to mod2.stream and succeeding.
//
// No cross-module call is made: a missing mod1.make would itself
// trigger a link-time error that could mask the field-resolution
// error this test is actually pinning. `let b: mod1.stream;` is
// enough — we read b.c before initializing it, which is fine because
// the field-resolution error fires at check, long before any reach
// analysis or codegen runs.
use mod1;
use mod2;
fn main() i32 = {
let b: mod1.stream;
return b.c; // mod1.stream has no `c`; expect a compile error.
};

View File

@@ -0,0 +1,18 @@
// Positive case: two modules each export `type stream`. The consumer
// imports both and disambiguates via the module qualifier. mod1.stream
// has fields (a, b); mod2.stream has fields (c, d). The exit code
// encodes a sum of all four fields, so any cross-binding would either
// fail to compile or return the wrong value.
use mod1;
use mod2;
fn main() i32 = {
let s1: mod1.stream;
s1.a = 3: i32;
s1.b = 5: i32;
let s2: mod2.stream;
s2.c = 7: i32;
s2.d = 11: i32;
return s1.a + s1.b + s2.c + s2.d;
};