diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 55903380..eebc4f26 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -331,14 +331,10 @@ enumerate_dir_ww(const char *dirpath, char ***out_files) * definer's qualified symbol (#53) equals the consumer's qualified * reference (#40) and the sep `.o`s link. A dep is NEVER bare-embedded. * - * The prepend is the TRANSITIVE closure of a package's deps (lead- - * ratified, superseding rob-c3-spec §1.3 "direct deps"): a dep's public - * interface can name a transitive dep's type (os exposes time.instant), - * so the consuming unit needs the whole closure for name RESOLUTION — - * direct-deps-only does not type-check. This mirrors harec reading the - * transitive `.td` closure. The checker still scopes qualifier lookup to - * each source package's direct N_USE declarations, so carrying those type - * facts does not make a transitive package source-visible. + * Only DIRECT dependency artifacts are prepended. A `.wwi` relocates the + * recursively reachable public foreign type/const facts required by its own + * API, retaining their origin modules without turning them into source + * imports. Full transitive reachability remains a linker concern. */ #define SEP_MAXPKG 256 @@ -749,15 +745,6 @@ sep_topo_visit(struct sepgraph *g, int pi, int *order, int *no, return 0; } -static void -sep_mark_deps(struct sepgraph *g, int pi, char *inset) -{ - for (int k = 0; k < g->pkg[pi].ndeps; k++) { - int di = g->pkg[pi].deps[k]; - if (!inset[di]) { inset[di] = 1; sep_mark_deps(g, di, inset); } - } -} - /* Emit one of pi's own source files into the sep-unit under the * //ww:module-reset primary boundary (so -c emits its decls, imported * ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead of the @@ -839,24 +826,21 @@ sep_emit_body(FILE *out, const char *path, struct ImportSet *visited, return 0; } -/* Compose pi's sep-unit at `unitf`: the transitive-closure `.wwi`s - * (reverse-topo order, each tagged by its dotted path), then pi's own - * body under //ww:module-reset. Directory membership comes only from the - * package node loaded before planning. */ +/* Compose pi's sep-unit at `unitf`: its byte-sorted DIRECT dependency + * `.wwi`s, each tagged by its dotted path, then pi's own body under + * //ww:module-reset. Compiler exports are self-contained for public type + * facts; the linker separately retains the reachable archive closure. */ static int sep_compose_unit(struct sepgraph *g, int pi, const char *scratch, - const int *order, int norder, const char *searchpath, const char *unitf) + const char *searchpath, const char *unitf) { FILE *u = fopen(unitf, "wb"); if (u == NULL) { fprintf(stderr, "ww: cannot open %s\n", unitf); return -1; } - char inset[SEP_MAXPKG] = {0}; - sep_mark_deps(g, pi, inset); - for (int oi = 0; oi < norder; oi++) { - int dj = order[oi]; - if (dj == pi || !inset[dj]) continue; + for (int k = 0; k < g->pkg[pi].ndeps; k++) { + int dj = g->pkg[pi].deps[k]; char wwi[1024]; sep_fname(g, dj, scratch, ".wwi", wwi, sizeof wwi); FILE *wf = fopen(wwi, "rb"); @@ -1043,7 +1027,7 @@ copy_file_atomic(const char *src, const char *dst) static void workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm) { - snprintf(buf, bufsz, "ww workdir fmt 1 mode %s asm %d\n", + snprintf(buf, bufsz, "ww workdir fmt 2 mode %s asm %d\n", is_test ? "test" : "build", emit_asm); } @@ -1222,7 +1206,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *ca = warm ? anew : apath; int needs_export = pi != root || root_package; int needs_archive = pi != root || root_package; - if (sep_compose_unit(g, pi, scratch, order, norder, srcdir, + if (sep_compose_unit(g, pi, scratch, srcdir, cu) < 0) { free(order); return 1; } if (warm && !stale_all && file_equal(unitnew, unitf) && file_is_reg(asmf) diff --git a/docs/build-system.md b/docs/build-system.md index fdfb38cd..84d6f161 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -2802,15 +2802,28 @@ the final component of its import path; two logical identities for one physical directory are rejected rather than compiled twice. Packages compile serially in dependency-first postorder. The compiler emits the -existing deterministic `.wwi` interface for every importable package and only -exported declarations enter that interface. A source qualifier is visible only -when its owning package directly imports it; private members and transitive-only -qualifiers are compiler errors. For compatibility with public signatures that -name deeper types, composed compiler units still carry transitive interface type -facts for internal resolution, but those facts do not create source-visible -bare names, package qualifiers, or value bindings. Replacing that -source-like closure with a self-contained typed export encoding remains part of -the later export-format work, not package-loader semantics. +existing deterministic `.wwi` interface for every importable package. Its +primary section contains that package's byte-sorted direct imports and exported +declarations. The compiler then appends byte-sorted, origin-tagged sections for +only the exported foreign type and constant facts recursively reachable from +the primary public signatures. This makes each direct dependency interface +self-contained for the public type information its consumers need while +retaining the deeper declarations' original package identity. Checked fixed +array dimensions are emitted as numeric type facts, so a public layout never +requires exposing the private constant spelling that produced its length. + +A package compilation unit contains one `.wwi` for each byte-sorted **direct** +import and no separately injected transitive interface. Origin-tagged facts +inside those direct artifacts are compiler data, not source imports: a source +qualifier is visible only when its owning package directly imports it, and +private members, transitive-only qualifiers, bare values, and bare types remain +compiler errors. In `-c` package mode the compiler coalesces repeated exported +type/constant facts with the same origin, kind, and name, preserving one nominal +type identity across diamonds; raw non-package `w6c` retains its existing +duplicate behavior. The source-like `.wwi` syntax remains a transitional export +encoding pending the binary `.wwe` format described above, but the direct-input +ownership boundary is now live in production Cstage and WWstage compilers and +drivers. An ordinary root is linked with the full reachable object closure into the requested executable (legacy WW programs may use a package name other than @@ -2821,7 +2834,10 @@ identity (`ww build -p -I ROOT -o bar.a foo.bar` emits `foo.bar.*` symbols), while a literal directory uses its declared leaf package. Package output requires a directory and `-p` cannot be combined with assembly-only `-S`. Two cold builds with identical inputs are required to produce byte-identical -requested products. +requested products. Compiler intrinsics keep their package-mode runtime ABI +independent of transitive source interfaces (for example, `alloc` lowers to the +runtime allocator without requiring an `rt.wwi` compiler input), while the +linker still receives every reachable package archive plus the runtime archive. ## 12. Candidate architectures and hard-gate decision diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 7b192b4f..374456fc 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -624,16 +624,12 @@ type lflags = struct { // producer (writes this package's `.wwi` via -I). Reverse-topo order // guarantees a package's deps' `.wwi` exist before it compiles. // -// The load-bearing rule (#56, rob-resolved): every dep is tagged by its -// FULL DOTTED import path on prepend (`//ww:module `), so the -// definer's qualified symbol (#53) equals the consumer's qualified -// reference (#40) and the sep `.o`s link. The prepend is the TRANSITIVE -// closure of a package's deps (lead-ratified): a dep's interface can -// name a transitive dep's type, so the consuming unit needs the whole -// closure for resolution. Qualifier lookup remains scoped to the direct -// imports owned by each source package, so those type facts do not expose -// a transitive package. The unit composition is byte-identical to the cstage -// driver (rule 10) so w6c/w6c_ww emit identical `.s`. +// Every direct dep is tagged by its FULL DOTTED import path on prepend +// (`//ww:module `), so the definer's qualified symbol equals the +// consumer's qualified reference and the sep `.o`s link. A dependency's +// compiler-owned `.wwi` carries its reachable public foreign type facts; +// separately prepending transitive interfaces is neither necessary nor +// allowed. The unit composition is byte-identical to the cstage driver. def SEP_MAXPKG: i32 = 256; @@ -670,7 +666,8 @@ fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = { return i; }; if (os.samefile(pathstr(g.pkg[i].entry), pathstr(entry))) { - cerr("ww: one package directory has identities "); + cerr("ww: package directory "); cerr(pathstr(entry)); + cerr(" has identities "); if (g.pkg[i].path[0u64] == 0u8) { cerr("(root)"); } else { cerr(pathstr(g.pkg[i].path)); }; cerr(" and "); @@ -1047,18 +1044,6 @@ fn septopovisit(g: *sepgraph, pi: i32, order: []i32, no: *i32, return 0; }; -fn sepmarkdeps(g: *sepgraph, pi: i32, inset: []u8) void = { - let k: i32 = 0; - for (k < g.pkg[pi].ndeps) { - let di: i32 = g.pkg[pi].deps[k]; - if (inset[di] == 0u8) { - inset[di] = 1u8; - sepmarkdeps(g, di, inset); - }; - k += 1; - }; -}; - // Emit one of pi's own source files into the sep-unit under the // //ww:module-reset primary boundary (so -c emits its decls, imported // ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead); @@ -1165,50 +1150,41 @@ fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8, return 0; }; -// Compose pi's sep-unit at `unitf`: the transitive-closure `.wwi`s -// (reverse-topo order, each tagged by its dotted path), then pi's own -// body under //ww:module-reset. Directory membership comes only from the -// package node loaded before planning. -fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32, - norder: i32, searchpath: *u8, unitf: *u8) i32 = { +// Compose pi's sep-unit at `unitf`: its byte-sorted DIRECT dependency +// `.wwi`s, each tagged by its dotted path, then pi's own body under +// //ww:module-reset. Compiler exports are self-contained for public type +// facts; the linker separately retains the reachable archive closure. +fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, + searchpath: *u8, unitf: *u8) i32 = { let u: i32 = os.open(pathstr(unitf), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 if (u < 0) { cerr("ww: cannot open unit\n"); return -1; }; - let inset: []u8 = alloc([], g.n: u64)!; - inset.len = g.n; - let z: i32 = 0; - for (z < g.n) { inset[z] = 0u8; z += 1; }; - sepmarkdeps(g, pi, inset); - let oi: i32 = 0; - for (oi < norder) { - let dj: i32 = order[oi]; - if (dj != pi) { - if (inset[dj] != 0u8) { - let wwi: *u8 = sepfname(g, dj, scratch, ".wwi"); - let wb: *u8; - let wn: u64; - wb, wn = slurp(wwi); - if (wb == nil) { - cerr("ww: missing wwi\n"); - os.close(u); - return -1; - }; - let dm: str = "//ww:module "; - if (!sepwriteall(u, dm.ptr, dm.len: u64) - || !sepwriteall(u, g.pkg[dj].path, - cstrlen(g.pkg[dj].path)) - || !sepwriteall(u, "\n".ptr, 1u64) - || !sepwriteall(u, wb, wn) - || !sepwriteall(u, "\n".ptr, 1u64)) { - cerr("ww: cannot compose package unit\n"); - os.close(u); - return -1; - }; - }; + let k: i32 = 0; + for (k < g.pkg[pi].ndeps) { + let dj: i32 = g.pkg[pi].deps[k]; + let wwi: *u8 = sepfname(g, dj, scratch, ".wwi"); + let wb: *u8; + let wn: u64; + wb, wn = slurp(wwi); + if (wb == nil) { + cerr("ww: missing wwi\n"); + os.close(u); + return -1; }; - oi += 1; + let dm: str = "//ww:module "; + if (!sepwriteall(u, dm.ptr, dm.len: u64) + || !sepwriteall(u, g.pkg[dj].path, + cstrlen(g.pkg[dj].path)) + || !sepwriteall(u, "\n".ptr, 1u64) + || !sepwriteall(u, wb, wn) + || !sepwriteall(u, "\n".ptr, 1u64)) { + cerr("ww: cannot compose package unit\n"); + os.close(u); + return -1; + }; + k += 1; }; let bv: expctx; bv.out = u; @@ -1429,14 +1405,14 @@ fn copyfileatomic(src: *u8, dst: *u8) i32 = { fn workdirstamptext(istest: i32, emitasm: i32) str = { if (istest != 0) { if (emitasm != 0) { - return "ww workdir fmt 1 mode test asm 1\n"; + return "ww workdir fmt 2 mode test asm 1\n"; }; - return "ww workdir fmt 1 mode test asm 0\n"; + return "ww workdir fmt 2 mode test asm 0\n"; }; if (emitasm != 0) { - return "ww workdir fmt 1 mode build asm 1\n"; + return "ww workdir fmt 2 mode build asm 1\n"; }; - return "ww workdir fmt 1 mode build asm 0\n"; + return "ww workdir fmt 2 mode build asm 0\n"; }; fn stampmatches(path: *u8, want: str) bool = { @@ -1699,7 +1675,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; let needsexport: bool = (pi != root) || rootpackage; let needsarchive: bool = (pi != root) || rootpackage; - if (sepcomposeunit(g, pi, scratch, order, norder, searchpath.ptr, cu) < 0) { + if (sepcomposeunit(g, pi, scratch, searchpath.ptr, cu) < 0) { return 1; }; let fresh: bool = false; diff --git a/test/sep/localbuild_test.ww b/test/sep/localbuild_test.ww index 30f3ecc8..12c97244 100644 --- a/test/sep/localbuild_test.ww +++ b/test/sep/localbuild_test.ww @@ -41,6 +41,17 @@ fn build(dir: str, driver: str, tag: str, target: str, out: str) i32 = { return code(dir, strings.concat("build_", tag), av); }; +fn diagnosticbody(s: str) str = { + let p: i32 = testenv.pos(s, ": error: "); + // Driver-owned graph diagnostics have no source-location prefix. Compare + // those in full; strip only compiler paths/locations from source errors. + if (p < 0) { return s; }; + let begin: i32 = p + 9; + let end: i32 = begin; + for (end < s.len && s[end] != '\n') { end += 1; }; + return strings.sub(s, begin, end); +}; + fn rejectstable(dir: str, label: str, target: str, needle: str) void = { let drivers: []str = ["ww", "ww_ww", "ww", "ww_ww"]; let tags: []str = ["c1", "w1", "c2", "w2"]; @@ -74,6 +85,12 @@ fn rejectstable(dir: str, label: str, target: str, needle: str) void = { if (!testenv.same(errors[1], errors[3])) { fail(label, "WW diagnostic changed across repeated builds"); }; + let cb: str = diagnosticbody(errors[0]); + let wb: str = diagnosticbody(errors[1]); + if (!testenv.same(cb, wb)) { + fail(label, strings.concat("C/WW diagnostic bodies differ: '", cb, + "' vs '", wb, "'")); + }; }; @test fn standalone_and_package_artifact() void = { @@ -136,6 +153,162 @@ fn rejectstable(dir: str, label: str, target: str, needle: str) void = { testenv.clean(td); }; +@test fn self_contained_direct_exports() void = { + let td: str = testenv.fresh(); + let dimensions: str = strings.concat(td, "/dimensions"); + let implementation: str = strings.concat(td, "/implementation"); + let api: str = strings.concat(td, "/api"); + let main: str = strings.concat(td, "/main"); + mkdir(dimensions); mkdir(implementation); mkdir(api); mkdir(main); + let dimensionssrc: str = strings.concat( + "package dimensions;\n", + "export def WIDTH: i32 = 4;\n", + "export def UNUSED_WIDTH: i32 = 8;\n"); + let implsrc: str = strings.concat( + "package implementation;\n", + "import dimensions;\n", + "def PRIVATE_WIDTH: i32 = 3;\n", + "export type Buffer = [PRIVATE_WIDTH]u8;\n", + "export type PublicBuffer = [WIDTH]u8;\n", + "export type CastBuffer = [(WIDTH: u64)]u8;\n", + "export type Detail = struct { code: i64, };\n", + "export type Member = struct { detail: Detail, payload: i64, };\n", + "export type Unused = struct { noise: i32, };\n", + "export def UNUSED: i32 = 99;\n", + "type private_type = struct { secret: i32, };\n", + "def private_value: i32 = 77;\n", + "export fn construct(v: i32) Member = { return Member{detail=Detail{code=(v: i64) + 1i64}, payload=(v: i64) + 2i64}; };\n", + "export fn unrelated() i32 = { return UNUSED; };\n", + "fn hidden() i32 = { return private_value; };\n"); + let apisrc: str = strings.concat( + "package api;\n", + "import implementation;\n", + "export type Member = struct { local: i32, };\n", + "export fn produce(v: i32) implementation.Member = { return implementation.construct(v); };\n", + "export fn score(m: implementation.Member) i32 = { return (m.detail.code + m.payload): i32; };\n", + "export fn width_of(b: Buffer) i32 = { return len(b): i32; };\n", + "export fn public_width_of(b: implementation.PublicBuffer) i32 = { return len(b): i32; };\n", + "export fn cast_width_of(b: implementation.CastBuffer) i32 = { return len(b): i32; };\n"); + let mainsrc: str = strings.concat( + "package main;\n", + "import api;\n", + "fn main() i32 = { let m = api.produce(19); let p: *i64 = alloc(api.score(m): i64)!; return (*p): i32; };\n"); + testenv.writefile(strings.concat(dimensions, "/dimensions.ww"), dimensionssrc); + testenv.writefile(strings.concat(implementation, "/implementation.ww"), + implsrc); + testenv.writefile(strings.concat(api, "/api.ww"), apisrc); + testenv.writefile(strings.concat(main, "/main.ww"), mainsrc); + + let outputs: []str = [strings.concat(td, "/direct-c1"), + strings.concat(td, "/direct-c2"), strings.concat(td, "/direct-w")]; + let drivers: []str = ["ww", "ww", "ww_ww"]; + let tags: []str = ["direct_c1", "direct_c2", "direct_w"]; + let i: i32 = 0; + for (i < outputs.len) { + if (build(td, drivers[i], tags[i], main, outputs[i]) != 0) { + fail("self-contained", strings.concat(drivers[i], " build failed")); + }; + let runav: []str = [outputs[i]]; + if (code(td, strings.concat("run_", tags[i]), runav) != 41) { + fail("self-contained", "implementation archive was not linked"); + }; + i += 1; + }; + if (!testenv.same(testenv.readfile(outputs[0]), testenv.readfile(outputs[1])) + || !testenv.same(testenv.readfile(outputs[0]), testenv.readfile(outputs[2]))) { + fail("self-contained", "clean C/C/WW executable outputs differ"); + }; + + let cwork: str = strings.concat(outputs[0], ".sepwork/"); + let cwork2: str = strings.concat(outputs[1], ".sepwork/"); + let wwork: str = strings.concat(outputs[2], ".sepwork/"); + let apiiface: str = testenv.readfile(strings.concat(cwork, "api.wwi")); + if (!testenv.same(apiiface, testenv.readfile(strings.concat(cwork2, + "api.wwi"))) || !testenv.same(apiiface, testenv.readfile(strings.concat( + wwork, "api.wwi")))) { + fail("self-contained", "C/C/WW api exports differ"); + }; + if (!testenv.has(apiiface, "//ww:module implementation\n") + || !testenv.has(apiiface, "export type Buffer = [3]u8;") + || !testenv.has(apiiface, "export type PublicBuffer = [4]u8;") + || !testenv.has(apiiface, "export type CastBuffer = [4]u8;") + || !testenv.has(apiiface, "export type Detail = struct") + || testenv.occurrences(apiiface, "export type Member = struct") != 2) { + fail("self-contained", "api export lacks the recursive public type facts"); + }; + if (testenv.has(apiiface, "//ww:module dimensions\n") + || testenv.has(apiiface, "WIDTH") + || testenv.has(apiiface, "export def UNUSED_WIDTH") + || testenv.has(apiiface, "PRIVATE_WIDTH") + || testenv.has(apiiface, "export type Unused") + || testenv.has(apiiface, "export def UNUSED") + || testenv.has(apiiface, "export fn construct") + || testenv.has(apiiface, "unrelated") + || testenv.has(apiiface, "private_type") + || testenv.has(apiiface, "private_value") + || testenv.has(apiiface, "hidden")) { + fail("self-contained", "api export leaked unrelated or private declarations"); + }; + let expectedunit: str = strings.concat("//ww:module api\n", apiiface, + "\n//ww:module-reset\n", mainsrc, "\n"); + if (!testenv.same(expectedunit, testenv.readfile(strings.concat(cwork, + "__root.unit.ww"))) || !testenv.same(expectedunit, + testenv.readfile(strings.concat(wwork, "__root.unit.ww")))) { + fail("self-contained", "consumer compiler input was not direct-api-only"); + }; + if (!testenv.exists(strings.concat(cwork, "implementation.a")) + || !testenv.exists(strings.concat(cwork, "dimensions.a"))) { + fail("self-contained", "reachable archives missing from link closure"); + }; + + let packages: []str = [strings.concat(td, "/api-c1.a"), + strings.concat(td, "/api-c2.a"), strings.concat(td, "/api-w.a")]; + i = 0; + for (i < packages.len) { + let pav: []str = [testenv.driver(drivers[i]), "build", "-p", "-I", td, + "-o", packages[i], api]; + if (code(td, strings.concat("package_", tags[i]), pav) != 0) { + fail("self-contained", "ww build -p api failed"); + }; + i += 1; + }; + if (!testenv.same(testenv.readfile(packages[0]), testenv.readfile(packages[1])) + || !testenv.same(testenv.readfile(packages[0]), testenv.readfile(packages[2])) + || !testenv.same(apiiface, testenv.readfile(strings.concat(packages[0], + ".wwi"))) || !testenv.same(apiiface, testenv.readfile(strings.concat( + packages[1], ".wwi"))) || !testenv.same(apiiface, + testenv.readfile(strings.concat(packages[2], ".wwi")))) { + fail("self-contained", "deterministic package artifact/export mismatch"); + }; + + testenv.writefile(strings.concat(td, "/deep-qualifier.ww"), strings.concat( + "package main;\nimport api;\n", + "fn main() i32 = { let m: implementation.Member; return 0; };\n")); + rejectstable(td, "deep-qualifier", strings.concat(td, + "/deep-qualifier.ww"), "unknown type 'implementation.Member'"); + testenv.writefile(strings.concat(td, "/deep-value.ww"), strings.concat( + "package main;\nimport api;\n", + "fn main() i32 = { return construct(1); };\n")); + rejectstable(td, "deep-value", strings.concat(td, "/deep-value.ww"), + "undefined: construct"); + testenv.writefile(strings.concat(td, "/deep-const.ww"), strings.concat( + "package main;\nimport api;\n", + "fn main() i32 = { return WIDTH; };\n")); + rejectstable(td, "deep-const", strings.concat(td, "/deep-const.ww"), + "undefined: WIDTH"); + testenv.writefile(strings.concat(td, "/deep-type.ww"), strings.concat( + "package main;\nimport api;\n", + "fn main() i32 = { let d: Detail; return 0; };\n")); + rejectstable(td, "deep-type", strings.concat(td, "/deep-type.ww"), + "unknown type 'Detail'"); + testenv.writefile(strings.concat(td, "/private-direct.ww"), strings.concat( + "package main;\nimport implementation;\n", + "fn main() i32 = { return implementation.hidden(); };\n")); + rejectstable(td, "private-direct", strings.concat(td, + "/private-direct.ww"), "has no exported declaration 'hidden'"); + testenv.clean(td); +}; + fn writediamond(td: str, reverse: bool) str = { let shared: str = strings.concat(td, "/shared"); let left: str = strings.concat(td, "/left"); @@ -159,13 +332,13 @@ fn writediamond(td: str, reverse: bool) str = { }; testenv.writefile(strings.concat(left, "/left.ww"), strings.concat( "package left;\nimport shared;\nimport shared;\n", - "export fn value() i32 = { return shared.base() + 1; };\n")); + "export fn make() shared.token = { return shared.token{value=shared.base() + 1}; };\n")); testenv.writefile(strings.concat(right, "/right.ww"), strings.concat( "package right;\nimport shared;\n", - "export fn value() i32 = { return shared.base() + 2; };\n")); + "export fn value(t: shared.token) i32 = { return t.value + shared.base() + 2; };\n")); testenv.writefile(strings.concat(main, "/main.ww"), strings.concat( "package main;\nimport right;\nimport left;\n", - "fn main() i32 = { return left.value() + right.value(); };\n")); + "fn main() i32 = { return right.value(left.make()); };\n")); return main; }; @@ -209,9 +382,17 @@ fn writediamond(td: str, reverse: bool) str = { }; let leftiface: str = testenv.readfile(strings.concat(scratch, "left.wwi")); + let rightiface: str = testenv.readfile(strings.concat(scratch, + "right.wwi")); if (testenv.occurrences(leftiface, "import shared;") != 1) { fail("diamond", "duplicate import escaped into export data"); }; + if (testenv.occurrences(leftiface, "export type token") != 1 + || testenv.occurrences(rightiface, "export type token") != 1 + || testenv.occurrences(unit, "export type token") != 2 + || testenv.occurrences(unit, "//ww:module shared\n") != 2) { + fail("diamond", "origin fact closure did not merge deterministically"); + }; i += 1; }; let shuffledout: str = strings.concat(td, "/diamond-c-shuffled");