w6c+selfhost: cross-module same-leaf type disambiguation via Sym.mod
This commit is contained in:
119
test/wcc/696_modtype_leaf_collision.c
Normal file
119
test/wcc/696_modtype_leaf_collision.c
Normal 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;
|
||||
}
|
||||
8
test/wcc/data/modcollision/mod1/mod1.ww
Normal file
8
test/wcc/data/modcollision/mod1/mod1.ww
Normal 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,
|
||||
};
|
||||
12
test/wcc/data/modcollision/mod2/mod2.ww
Normal file
12
test/wcc/data/modcollision/mod2/mod2.ww
Normal 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,
|
||||
};
|
||||
19
test/wcc/data/modcollision/neg.ww
Normal file
19
test/wcc/data/modcollision/neg.ww
Normal 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.
|
||||
};
|
||||
18
test/wcc/data/modcollision/pos.ww
Normal file
18
test/wcc/data/modcollision/pos.ww
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user