diff --git a/Makefile b/Makefile index 1eab979a..a102ca7b 100644 --- a/Makefile +++ b/Makefile @@ -566,6 +566,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_sepbuild_run \ $(BIN)/test_sepdotpath_run \ $(BIN)/test_seproot_export_run \ + $(BIN)/test_sepstructdef_run \ $(BIN)/test_sepcycle_dup \ $(BIN)/test_separchive_run \ $(BIN)/test_pkgcache_run \ @@ -3134,6 +3135,17 @@ $(BIN)/test_seproot_export_run: test/wcc/989_seproot_export_run.c $(BIN)/ww $(BI $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_sepstructdef_run — sep-build of a dep exporting aggregate-initializer +# defs (N_STRUCTLIT info / N_ARRLIT arr) (#70, BUG-2): the producer must emit a +# value-LESS prototype `export def X: T;` for an aggregate-init def (DATA-global +# per #52), and the def parser must accept that bodyless form. Drives BOTH +# driver stages + reads back the dep `.wwi`, so it needs both driver + both +# compiler + both linker stages + libwwrt for the link. +$(BIN)/test_sepstructdef_run: test/wcc/989_sepstructdef_run.c $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6a_ww \ + $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # 989_sepcycle_dup — M3-tail commit-4 negative gates (#46, #31): a loud # dep-cycle reject (both driver stages, cs==ww stderr) + the #31 # duplicate-symbol reject (both linker stages). Needs both driver stages, diff --git a/cmd/w6c/wwi.c b/cmd/w6c/wwi.c index 339b3aa7..12f1cff2 100644 --- a/cmd/w6c/wwi.c +++ b/cmd/w6c/wwi.c @@ -402,9 +402,24 @@ wwi_decl(FILE *of, Node *d) fputs(d->str, of); fputs(": ", of); wwi_type(of, d->lhs); - fputs(" = ", of); - wwi_expr(of, d->rhs); - fputs(";\n", of); + /* An aggregate initializer (N_STRUCTLIT/N_ARRLIT) is a DATA- + * global (#52): emit a value-LESS prototype `export def X: T;`. + * The defining package's own .o emits the struct/array DATA; the + * importer registers the def by TYPE only (the DATA-suppression + * if(sep_mode && imported)continue at cgen.c is already in place) + * and field/element reads become external LEAQ refs the linker + * fills. Boundary (rule 7): this gives up importer-side const- + * fold of an aggregate def's FIELDS — a no-op, ww never folds + * struct-literal field access, and an aggregate-def field + * demanded in a const-fold context stays a LOUD error (task #71). */ + if (d->rhs && (d->rhs->kind == N_STRUCTLIT + || d->rhs->kind == N_ARRLIT)) { + fputs(";\n", of); + } else { + fputs(" = ", of); + wwi_expr(of, d->rhs); + fputs(";\n", of); + } break; case N_LET: fputs("export let ", of); diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index 5be3b361..3adb9307 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -1284,8 +1284,13 @@ parsedef(Parser *p, int exp) n->str = expectident(p); expect(p, TK_COLON); n->lhs = parsetype(p); - expect(p, TK_ASSIGN); - n->rhs = parseexpr(p); + /* A value-LESS `def X: T;` is an interface prototype (an aggregate- + * init `.wwi` def whose DATA lives in the defining package — BUG-2 / + * task #71), mirroring parsefn's bodyless-prototype arm. rhs stays + * NULL; the checker's fold pass (`if (d->rhs)`) and cgen's emit_defs/ + * emit_lets (`d->rhs == NULL` continue) already expect this shape. */ + if (accept(p, TK_ASSIGN)) + n->rhs = parseexpr(p); expect(p, TK_SEMI); n->export = exp; return n; diff --git a/lib/ww/parse/decl.ww b/lib/ww/parse/decl.ww index 7ac99a1c..a23dbf9e 100644 --- a/lib/ww/parse/decl.ww +++ b/lib/ww/parse/decl.ww @@ -47,8 +47,14 @@ fn parsedef(p: *parser, exported: i32) *node = { n.str = id; expecttok(p, tkind.TK_COLON, "expected ':' in def"); n.lhs = parsetype(p); - expecttok(p, tkind.TK_ASSIGN, "expected '=' in def"); - n.rhs = parseexpr(p); + // A value-LESS `def X: T;` is an interface prototype (an aggregate- + // init `.wwi` def whose DATA lives in the defining package — BUG-2 / + // task #71), mirroring the fn bodyless-prototype arm. rhs stays nil; + // the checker fold pass and cgen emit_defs/emit_lets already guard + // the rhs==nil shape. + if (accepttok(p, tkind.TK_ASSIGN)) { + n.rhs = parseexpr(p); + }; expecttok(p, tkind.TK_SEMI, "expected ';' after def"); n.exported = exported; return n; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index d71e16a4..9b7724d7 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -8146,8 +8146,14 @@ fn parsedef(p: *parser, exported: i32) *node = { n.str = id; expecttok(p, tkind.TK_COLON, "expected ':' in def"); n.lhs = parsetype(p); - expecttok(p, tkind.TK_ASSIGN, "expected '=' in def"); - n.rhs = parseexpr(p); + // A value-LESS `def X: T;` is an interface prototype (an aggregate- + // init `.wwi` def whose DATA lives in the defining package — BUG-2 / + // task #71), mirroring the fn bodyless-prototype arm. rhs stays nil; + // the checker fold pass and cgen emit_defs/emit_lets already guard + // the rhs==nil shape. + if (accepttok(p, tkind.TK_ASSIGN)) { + n.rhs = parseexpr(p); + }; expecttok(p, tkind.TK_SEMI, "expected ';' after def"); n.exported = exported; return n; @@ -45808,9 +45814,23 @@ fn wwidecl(fd: i32, d: *node) void = { wputs(fd, d.str); wputs(fd, ": "); wwitype(fd, d.lhs); - wputs(fd, " = "); - wwiexpr(fd, d.rhs); - wputs(fd, ";\n"); + // An aggregate initializer (N_STRUCTLIT/N_ARRLIT) is a DATA-global + // (#52): emit a value-LESS prototype `export def X: T;`. The + // defining package's own .o emits the struct/array DATA; the + // importer registers the def by TYPE only (DATA-suppression + // already in cgen) and field/element reads become external refs + // the linker fills. Boundary (rule 7): this gives up importer- + // side const-fold of an aggregate def's FIELDS — a no-op, ww + // never folds struct-literal field access, and an aggregate-def + // field demanded in a const-fold context stays a LOUD error (#71). + if (d.rhs != nil && (d.rhs.kind == nkind.N_STRUCTLIT || + d.rhs.kind == nkind.N_ARRLIT)) { + wputs(fd, ";\n"); + } else { + wputs(fd, " = "); + wwiexpr(fd, d.rhs); + wputs(fd, ";\n"); + }; } else { if (d.kind == nkind.N_LET) { wputs(fd, "export let "); wputs(fd, d.str); diff --git a/selfhost/cmd/wcc/wwi.ww b/selfhost/cmd/wcc/wwi.ww index 500130bf..8792fdbb 100644 --- a/selfhost/cmd/wcc/wwi.ww +++ b/selfhost/cmd/wcc/wwi.ww @@ -452,9 +452,23 @@ fn wwidecl(fd: i32, d: *node) void = { wputs(fd, d.str); wputs(fd, ": "); wwitype(fd, d.lhs); - wputs(fd, " = "); - wwiexpr(fd, d.rhs); - wputs(fd, ";\n"); + // An aggregate initializer (N_STRUCTLIT/N_ARRLIT) is a DATA-global + // (#52): emit a value-LESS prototype `export def X: T;`. The + // defining package's own .o emits the struct/array DATA; the + // importer registers the def by TYPE only (DATA-suppression + // already in cgen) and field/element reads become external refs + // the linker fills. Boundary (rule 7): this gives up importer- + // side const-fold of an aggregate def's FIELDS — a no-op, ww + // never folds struct-literal field access, and an aggregate-def + // field demanded in a const-fold context stays a LOUD error (#71). + if (d.rhs != nil && (d.rhs.kind == nkind.N_STRUCTLIT || + d.rhs.kind == nkind.N_ARRLIT)) { + wputs(fd, ";\n"); + } else { + wputs(fd, " = "); + wwiexpr(fd, d.rhs); + wputs(fd, ";\n"); + }; } else { if (d.kind == nkind.N_LET) { wputs(fd, "export let "); wputs(fd, d.str); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 9b442e28..339c157e 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -8146,8 +8146,14 @@ fn parsedef(p: *parser, exported: i32) *node = { n.str = id; expecttok(p, tkind.TK_COLON, "expected ':' in def"); n.lhs = parsetype(p); - expecttok(p, tkind.TK_ASSIGN, "expected '=' in def"); - n.rhs = parseexpr(p); + // A value-LESS `def X: T;` is an interface prototype (an aggregate- + // init `.wwi` def whose DATA lives in the defining package — BUG-2 / + // task #71), mirroring the fn bodyless-prototype arm. rhs stays nil; + // the checker fold pass and cgen emit_defs/emit_lets already guard + // the rhs==nil shape. + if (accepttok(p, tkind.TK_ASSIGN)) { + n.rhs = parseexpr(p); + }; expecttok(p, tkind.TK_SEMI, "expected ';' after def"); n.exported = exported; return n; diff --git a/test/wcc/989_sepstructdef_run.c b/test/wcc/989_sepstructdef_run.c new file mode 100644 index 00000000..018f3cf7 --- /dev/null +++ b/test/wcc/989_sepstructdef_run.c @@ -0,0 +1,251 @@ +/* + * 989_sepstructdef_run — sep-build of a dep package that EXPORTS aggregate- + * initializer defs: `export def info: s = s{a=7,b=11};` (N_STRUCTLIT) and + * `export def arr: [3]i32 = [1,2,3];` (N_ARRLIT) — BUG-2 (#70). + * + * Root cause it guards: the `.wwi` producer's const-expr emitter handled + * scalar/N_BIN/N_UN/N_DOT but NOT N_STRUCTLIT(15)/N_ARRLIT(16). An exported + * aggregate-init def aborted the producer with "unhandled const-expr node + * kind 15", so the dep never produced a `.wwi` and the sep-build failed. + * + * Fix (producer, both stages): an aggregate-init def is a DATA-global (#52), + * so the producer emits a value-LESS prototype `export def X: T;` — the + * defining package's own `.o` emits the struct/array DATA; the importer + * registers the def by TYPE only (cgen's emit_defs/emit_lets already skip a + * NULL-rhs def, no dup DATA) and reads resolve to external refs the linker + * fills. Consuming the prototype also required the def parser (both stages) + * to accept the bodyless form, mirroring the existing fn-prototype arm. + * + * Graph (smallest reproducing shape): root -> { d (one real dep package) }. + * d exports an aggregate STRUCT def (info), an aggregate ARRAY def (arr), + * and three reader fns; root sums them at RUNTIME. + * + * Asserts (all COLD — per-stage WW_PKGCACHE wipes the package cache): + * 1. Build + LINK + run, BOTH stages -> exit EXPECT_EXIT. The link+run is + * the external-DATA-ref proof: the importer's CALL d.geta / d.getelem / + * d.getb and d's own LEAQ d.info/d.arr(SB) refs resolve against the dep + * `.o`. Pre-fix the dep producer aborted ("node kind 15") -> no binary. + * 2. cs==ww (rule 10): per-package `.s`/`.wwi`/`.unit.ww` for {d,__root} + * AND the final binary are byte-identical between the two drivers. + * 3. The dep `.wwi` carries the value-LESS prototypes `export def info: s;` + * and `export def arr: [3]i32;` (the fix's surface), and NOT the old + * value-serialized `export def info: s = ` form (NON-VACUITY: proves the + * producer took the new aggregate arm, not the scalar arm). + * + * Light wwstage-driver test (CLAUDE.md rule 14): all intermediates are + * `-o`-redirected to /tmp, so it is phase-1 parallel-safe. Models + * 989_seproot_export_run.c conventions; 989 prefix per the sep-gate precedent. + */ +#include +#include +#include +#include +#include +#include + +#define EXPECT_EXIT 20 /* info.a(7) + arr[1](2) + info.b(11) */ + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +static const char * +absbin(void) +{ + const char *b = getenv("BIN"); + if (!b) b = "out/bin"; + if (b[0] == '/') return b; + static char buf[2048]; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return NULL; + snprintf(buf, sizeof buf, "%s/%s", cwd, b); + return buf; +} + +static int +slurp(const char *path, char **outbuf, size_t *outlen) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return -1; } + char *b = malloc((size_t)n + 1); + if (!b) { fclose(f); return -1; } + if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } + b[n] = '\0'; + fclose(f); + *outbuf = b; + *outlen = (size_t)n; + return 0; +} + +static int +files_eq(const char *a, const char *b) +{ + char *ba = NULL, *bb = NULL; + size_t na = 0, nb = 0; + if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) { + free(ba); free(bb); + return -1; + } + int eq = (na == nb && memcmp(ba, bb, na) == 0); + free(ba); free(bb); + return eq ? 0 : 1; +} + +/* 0 if `needle` occurs in the file at `path`, 1 if absent, -1 on read err. */ +static int +file_contains(const char *path, const char *needle) +{ + char *b = NULL; + size_t n = 0; + if (slurp(path, &b, &n) < 0) return -1; + int found = (strstr(b, needle) != NULL); + free(b); + return found ? 0 : 1; +} + +static int +write_file(const char *path, const char *body) +{ + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(body, f); + fclose(f); + return 0; +} + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + char td[64], cmd[8192], p[1024]; + int fail = 0; + + snprintf(td, sizeof td, "/tmp/wwsepstructdef_%d", getpid()); + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + mkdir(td, 0755); + + /* lib/d — the dep package exporting aggregate-init defs + reader fns. + * The struct type must be EXPORTED so the prototype names a visible + * type (an exported def over an unexported type is a separate loud + * reject, check_exported_type). */ + snprintf(p, sizeof p, "%s/lib", td); mkdir(p, 0755); + snprintf(p, sizeof p, "%s/lib/d", td); mkdir(p, 0755); + snprintf(p, sizeof p, "%s/lib/d/mod.ww", td); + if (write_file(p, + "package d;\n" + "export type s = struct { a: i32, b: i32 };\n" + "export def info: s = s { a = 7, b = 11 };\n" + "export def arr: [3]i32 = [1, 2, 3];\n" + "export fn geta() i32 = { return info.a; };\n" + "export fn getb() i32 = { return info.b; };\n" + "export fn getelem(i: i32) i32 = { return arr[i]; };\n")) + { fail++; goto out; } + + char rootww[1024]; + snprintf(rootww, sizeof rootww, "%s/root.ww", td); + if (write_file(rootww, + "package main;\n" + "import d;\n" + "fn main() i32 = {\n" + "\treturn d.geta() + d.getelem(1) + d.getb();\n" + "};\n")) + { fail++; goto out; } + + struct { const char *drv, *tag; char prog[1024]; } + stg[] = { { "ww", "cs", {0} }, { "ww_ww", "ww", {0} } }; + + for (int s = 0; s < 2; s++) { + snprintf(stg[s].prog, sizeof stg[s].prog, "%s/prog.%s", td, stg[s].tag); + /* Per-stage fresh WW_PKGCACHE -> every package compiles COLD, so the + * `.s`/`.wwi`/`.unit.ww` this gate inspects are always produced. */ + snprintf(cmd, sizeof cmd, + "WW_PKGCACHE='%s/cache.%s' timeout 240 %s/%s build --sep " + "-I %s/lib -o %s %s >/dev/null 2>&1", + td, stg[s].tag, bin, stg[s].drv, td, stg[s].prog, rootww); + if (runwait(cmd) != 0) { + fprintf(stderr, "sepstructdef FAIL: %s build --sep (pre-fix: the " + "aggregate-init def aborts the producer with \"node kind 15\")\n", + stg[s].drv); + fail++; + continue; + } + int rc = runwait(stg[s].prog); + if (rc != EXPECT_EXIT) { + fprintf(stderr, "sepstructdef FAIL: %s prog exit=%d expected %d " + "(external aggregate-DATA ref did not resolve)\n", + stg[s].drv, rc, EXPECT_EXIT); + fail++; + } + } + + /* cs==ww (rule 10): per-package .s/.wwi/.unit.ww + final binary. */ + const char *pkgs[] = { "d", "__root" }; + for (int i = 0; i < 2; i++) { + const char *suf[] = { ".s", ".wwi", ".unit.ww" }; + for (int k = 0; k < 3; k++) { + /* the root's `.wwi` is intentionally not produced (BUG-1 fix). */ + if (strcmp(pkgs[i], "__root") == 0 && strcmp(suf[k], ".wwi") == 0) + continue; + char a[1024], b[1024]; + snprintf(a, sizeof a, "%s/prog.%s.sepwork/%s%s", + td, stg[0].tag, pkgs[i], suf[k]); + snprintf(b, sizeof b, "%s/prog.%s.sepwork/%s%s", + td, stg[1].tag, pkgs[i], suf[k]); + if (files_eq(a, b) != 0) { + fprintf(stderr, "sepstructdef FAIL: cs!=ww for %s%s (rule 10)\n", + pkgs[i], suf[k]); + fail++; + } + } + } + if (files_eq(stg[0].prog, stg[1].prog) != 0) { + fprintf(stderr, "sepstructdef FAIL: cs exe != ww exe (rule 10)\n"); + fail++; + } + + /* The dep `.wwi` carries the value-LESS aggregate prototypes — and NOT + * the old value-serialized form (NON-VACUITY: the producer took the new + * aggregate arm, not the scalar `= ` arm). */ + { + char wwi[1024]; + snprintf(wwi, sizeof wwi, "%s/prog.cs.sepwork/d.wwi", td); + if (file_contains(wwi, "export def info: s;\n") != 0) { + fprintf(stderr, "sepstructdef FAIL: d.wwi lacks value-less " + "`export def info: s;` (struct-lit def prototype)\n"); + fail++; + } + if (file_contains(wwi, "export def arr: [3]i32;\n") != 0) { + fprintf(stderr, "sepstructdef FAIL: d.wwi lacks value-less " + "`export def arr: [3]i32;` (array-lit def prototype)\n"); + fail++; + } + if (file_contains(wwi, "export def info: s = ") == 0) { + fprintf(stderr, "sepstructdef FAIL: d.wwi serialized the aggregate " + "value (`export def info: s = ...`) -> vacuous gate\n"); + fail++; + } + } + +out: + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + if (fail) { + fprintf(stderr, "sepstructdef: %d check(s) failed\n", fail); + return 1; + } + printf("sepstructdef: exported struct-lit + array-lit defs via " + "build_one_sep — build+link+run (exit %d) + cs==ww per-pkg + binary " + "+ value-less `.wwi` prototypes (both stages)\n", EXPECT_EXIT); + return 0; +}