/* * 848_xmod_qualstructlit_run — task #76. Pins that a qualified struct * literal `pkg.Type{...}` (the type named through an imported module) * constructs end-to-end, on BOTH stages, byte-identically (rule-10). * * THE BUG (both stages, pre-fix a PARSE reject): `pkg.Type` parses to * two different node shapes by position. In DECL position parsetype * collapses the dotted path into ONE N_TNAME ("pkg.Type") and the * checker resolves it via the strrchr-leaf path. In LITERAL position * the dotted path folds into an N_DOT chain; the struct-literal handoff * handed that N_DOT to the checker, whose resolve_type has N_IDENT and * N_TNAME arms but no N_DOT arm — "expected type expression". So a * qualified struct literal was unparseable, blocking wcc's own use of * `syntax.tok{...}`-style construction across the syntax->wcc boundary * (#75). * * THE FIX (#76, PARSER root, zero new checker arms): at the struct-lit * handoff, if the typeref is an N_DOT chain rooted at a bare ident with * all-identifier components, FLATTEN it into one N_TNAME whose str joins * the chain in SOURCE order ("pkg.Type") — byte-identical to parsetype's * dotted collapse — so the existing N_TNAME resolver arm handles it. The * two parse positions then converge on one canonical qualified-type * representation. cstage flattens in parseprimary (peek-driven dotted * fold); the ww parser has no token-peek and folds dotted chains in * parsepostfix, so its flatten lives there (faithful adaptation, same * canonical N_TNAME output). Bare `Foo{}` keeps the N_IDENT fast-path. * * scenario | shape | exit | byte-id * ----------+---------------------------------------------+------+-------- * fields | several `pkg.point{x=..,y=..}` with | 37 | cs==ww * | differing field values; each field | | * | verified, single derived exit | | * ret_assign| qualified lit in a `return` and an `=` | 27 | cs==ww * | reassignment position (`return pkg.point | | * | {...}` / `a = pkg.point{...}`) | | * nested | nested qualified lit | 18 | cs==ww * | `pkg.rect{lo=pkg.point{...},hi=...}` | | * * A trailing op on a struct-literal temporary — `pkg.point{...}.x` — is * rejected by cgen the same way the BARE `point{...}.x` form is (a * pre-existing literal-temporary lowering gap, #77, orthogonal to #76), * so it can't ride a runtime scenario. But that trailing op is exactly * what the in-loop-`continue` requirement (the parser must stay in the * postfix loop so `.x` wraps the literal, not terminate at the `}`) bites * on. The ast_proof scenarios cover it WITHOUT cgen: dump the parse AST * (`wwdump -a`, parse-only) on BOTH stages and assert (1) the dumps are * byte-identical (rule-10), (2) the node shape is dot-over-structlit-over- * tname (proving `continue` kept the loop live), and (3) a multi-level * path `a.b.C` joins SOURCE order in the flattened tname (req-c). * * GATE POLARITY: must stay GREEN. Pre-fix the driver build fails (parse * reject). Red means a qualified struct literal stopped parsing again, * or the two stages' .s diverged (rule-10 break). * * Builds its own 2-module fixtures in a private mktemp dir, so all * intermediates (main.combined.ww, the linked binary, the .s probes) * land OFF the source tree (CLAUDE.md rule 14). */ #include #include #include #include #include #include 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 slurp_eq(const char *a, const char *b) { FILE *fa = fopen(a, "rb"); FILE *fb = fopen(b, "rb"); if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } int rc = 0; for (;;) { int ca = fgetc(fa); int cb = fgetc(fb); if (ca != cb) { rc = -1; break; } if (ca == EOF) break; } fclose(fa); fclose(fb); return rc; } /* slurp a (small) text file into buf; returns bytes read, -1 on error. */ static long slurp(const char *path, char *buf, long cap) { FILE *f = fopen(path, "rb"); if (!f) return -1; long n = (long)fread(buf, 1, (size_t)cap - 1, f); int truncated = !feof(f); fclose(f); if (n < 0 || truncated) return -1; buf[n] = '\0'; return n; } /* substrings must occur in the given order (proves tree nesting without * coupling to astprint's exact indentation). */ static int ordered(const char *hay, const char *const *needles) { const char *p = hay; for (int i = 0; needles[i]; i++) { const char *q = strstr(p, needles[i]); if (!q) return -1; p = q + 1; } return 0; } /* ast_proof: req-(a)/(c). Dump the parse AST of a TRAILING-op qualified * struct literal on both stages (wwdump -a, parse-only — sidesteps the #77 * cgen gap), assert byte-id (rule-10) and the dot-over-structlit-over-tname * ordering. Single-file fixtures: astprint resolves no names, so the * `pkg`/`a.b.C` paths need not name a real import. */ struct ast_case { const char *label; const char *src; const char *const *order; /* NULL-terminated ordered substrings */ }; static int run_ast_proof(const char *wwdump, const char *wwdump_ww, const struct ast_case *ac) { char dir[] = "/tmp/ww848a_XXXXXX"; if (mkdtemp(dir) == NULL) { fprintf(stderr, "848-ast[%s]: mkdtemp failed\n", ac->label); return -1; } char src[1024], cs[1024], ww[1024], cmd[4096]; int rc = 0; snprintf(src, sizeof src, "%s/a.ww", dir); FILE *f = fopen(src, "wb"); if (!f) { fprintf(stderr, "848-ast[%s]: write\n", ac->label); rc = -1; goto done; } fputs(ac->src, f); fclose(f); snprintf(cs, sizeof cs, "%s/cs.ast", dir); snprintf(ww, sizeof ww, "%s/ww.ast", dir); snprintf(cmd, sizeof cmd, "%s -a %s > %s 2>/dev/null", wwdump, src, cs); if (runwait(cmd) != 0) { fprintf(stderr, "848-ast[%s]: cstage wwdump -a failed\n", ac->label); rc = -1; goto done; } snprintf(cmd, sizeof cmd, "%s -a %s > %s 2>/dev/null", wwdump_ww, src, ww); if (runwait(cmd) != 0) { fprintf(stderr, "848-ast[%s]: wwstage wwdump_ww -a failed\n", ac->label); rc = -1; goto done; } if (slurp_eq(cs, ww) != 0) { fprintf(stderr, "848-ast[%s]: cs/ww AST dumps DIFFER (rule-10)\n", ac->label); rc = -1; } char buf[65536]; if (slurp(cs, buf, (long)sizeof buf) < 0) { fprintf(stderr, "848-ast[%s]: slurp dump\n", ac->label); rc = -1; goto done; } if (ordered(buf, ac->order) != 0) { fprintf(stderr, "848-ast[%s]: AST node shape wrong (trailing op " "did not wrap the qualified struct literal — req-a/c)\n", ac->label); rc = -1; } done: snprintf(cmd, sizeof cmd, "rm -rf %s", dir); (void)runwait(cmd); return rc; } /* trailing `.x` must wrap the qualified literal: dot -> structlit -> tname. */ static const char *const trailing_order[] = { "(dot \"x\"", "(structlit", "(tname \"pkg.point\"", NULL }; /* a multi-level `a.b.C` path flattens to a source-order tname. */ static const char *const multilevel_order[] = { "(dot \"x\"", "(structlit", "(tname \"a.b.C\"", NULL }; static const struct ast_case ast_cases[] = { { "trailing", "package main;\n" "export fn f() i64 = {\n" " let v: i64 = pkg.point { x = 1i64, y = 2i64 }.x;\n" " return v;\n" "};\n", trailing_order }, { "multilevel", "package main;\n" "export fn g() i64 = {\n" " let v: i64 = a.b.C { x = 1i64 }.x;\n" " return v;\n" "};\n", multilevel_order }, }; struct file { const char *name; const char *src; }; struct scenario { const char *label; const struct file *files; /* name==NULL terminates */ int want_exit; }; /* shared package: two struct types, the second nesting the first. */ #define PKG_WW \ { "pkg.ww", \ "package pkg;\n" \ "export type point = struct { x: i64, y: i64 };\n" \ "export type rect = struct { lo: point, hi: point };\n" } /* ---- fields: several qualified lits, differing values, verify each -- */ static const struct file fields_files[] = { PKG_WW, { "main.ww", "package main;\n" "import pkg;\n" "export fn main() i32 = {\n" " let a: pkg.point = pkg.point { x = 3i64, y = 4i64 };\n" " let b: pkg.point = pkg.point { x = 10i64, y = 20i64 };\n" " if (a.x != 3i64) { return 1; };\n" " if (a.y != 4i64) { return 2; };\n" " if (b.x != 10i64) { return 3; };\n" " if (b.y != 20i64) { return 4; };\n" " return (a.x + a.y + b.x + b.y): i32;\n" /* 3+4+10+20 = 37 */ "};\n" }, { NULL, NULL } }; /* ---- ret_assign: a qualified struct literal in `return` position (the * callee builds `pkg.point{...}` and returns it) and in `=` reassignment * position (`a = pkg.point{...}`). Both are cgen-supported aggregate * stores, so this pins the new parser arm firing outside let-init too. */ static const struct file ret_assign_files[] = { PKG_WW, { "main.ww", "package main;\n" "import pkg;\n" "fn mk(vx: i64, vy: i64) pkg.point = {\n" " return pkg.point { x = vx, y = vy };\n" "};\n" "export fn main() i32 = {\n" " let a: pkg.point = mk(8i64, 2i64);\n" " a = pkg.point { x = 15i64, y = 5i64 };\n" " if (a.x != 15i64) { return 1; };\n" " if (a.y != 5i64) { return 2; };\n" " let b: pkg.point = mk(3i64, 4i64);\n" " if (b.x != 3i64) { return 3; };\n" " return (a.x + a.y + b.x + b.y): i32;\n" /* 15+5+3+4 = 27 */ "};\n" }, { NULL, NULL } }; /* ---- nested: a qualified lit whose fields are themselves qualified * lits (`pkg.rect{ lo = pkg.point{...}, hi = pkg.point{...} }`). */ static const struct file nested_files[] = { PKG_WW, { "main.ww", "package main;\n" "import pkg;\n" "export fn main() i32 = {\n" " let r: pkg.rect = pkg.rect {\n" " lo = pkg.point { x = 1i64, y = 2i64 },\n" " hi = pkg.point { x = 7i64, y = 5i64 }\n" " };\n" " if (r.lo.x != 1i64) { return 1; };\n" " if (r.hi.y != 5i64) { return 2; };\n" " return ((r.hi.x - r.lo.x) * (r.hi.y - r.lo.y)): i32;\n" /* 6*3=18 */ "};\n" }, { NULL, NULL } }; static const struct scenario scenarios[] = { { "fields", fields_files, 37 }, { "ret_assign", ret_assign_files, 27 }, { "nested", nested_files, 18 }, }; static int run_scenario(const char *bin, const char *cdrv, const char *wdrv, const struct scenario *sc) { (void)bin; char dir[] = "/tmp/ww848_XXXXXX"; if (mkdtemp(dir) == NULL) { fprintf(stderr, "848[%s]: mkdtemp failed\n", sc->label); return -1; } char path[1024], cmd[4096]; int rc = 0; for (int i = 0; sc->files[i].name; i++) { snprintf(path, sizeof path, "%s/%s", dir, sc->files[i].name); FILE *f = fopen(path, "wb"); if (!f) { fprintf(stderr, "848[%s]: write %s\n", sc->label, sc->files[i].name); rc = -1; goto done; } fputs(sc->files[i].src, f); fclose(f); } /* #94 sep layout: cstage --sep build + run pins parse + runtime field * values; pin WW_PKGCACHE under the scratch dir. */ snprintf(cmd, sizeof cmd, "cd %s && WW_PKGCACHE=%s/pkgc_c %s build --sep -I %s -o %s/main %s/main.ww", dir, dir, cdrv, dir, dir, dir); if (runwait(cmd) != 0) { fprintf(stderr, "848[%s]: cstage build failed " "(qualified struct literal parse reject?)\n", sc->label); rc = -1; goto done; } snprintf(path, sizeof path, "%s/main", dir); int got = runwait(path); if (got != sc->want_exit) { fprintf(stderr, "848[%s]: cstage exit %d, want %d\n", sc->label, got, sc->want_exit); rc = -1; } /* rule-10 discriminator: the wwstage driver's --sep build emits * per-package w6c_ww asm that must be byte-identical to cstage's. The * root + each imported pkg compile to separate .sepwork/.s; * concat (sorted glob, identical set both stages) for the compare. */ char cs_s[1024], ws_s[1024]; snprintf(cmd, sizeof cmd, "cd %s && WW_PKGCACHE=%s/pkgc_w %s build --sep -I %s -o %s/mainww %s/main.ww", dir, dir, wdrv, dir, dir, dir); if (runwait(cmd) != 0) { fprintf(stderr, "848[%s]: ww_ww build failed " "(qualified struct literal parse reject?)\n", sc->label); rc = -1; goto done; } snprintf(cs_s, sizeof cs_s, "%s/all_cs.s", dir); snprintf(ws_s, sizeof ws_s, "%s/all_ww.s", dir); snprintf(cmd, sizeof cmd, "cat %s/main.sepwork/*.s > %s 2>/dev/null", dir, cs_s); if (system(cmd)) {} snprintf(cmd, sizeof cmd, "cat %s/mainww.sepwork/*.s > %s 2>/dev/null", dir, ws_s); if (system(cmd)) {} if (slurp_eq(cs_s, ws_s) != 0) { fprintf(stderr, "848[%s]: cs.s/ww.s DIFFER (rule-10 byte-id " "violation)\n", sc->label); rc = -1; } done: snprintf(cmd, sizeof cmd, "rm -rf %s", dir); (void)runwait(cmd); return rc; } int main(void) { const char *bin = getenv("BIN"); if (!bin) bin = "out/bin"; char absbin[2048]; 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[2100], wdrv[2100], wwdump[2100], wwdump_ww[2100]; snprintf(cdrv, sizeof cdrv, "%s/ww", bin); snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); snprintf(wwdump, sizeof wwdump, "%s/wwdump", bin); snprintf(wwdump_ww, sizeof wwdump_ww, "%s/wwdump_ww", bin); if (access(wdrv, X_OK) != 0 || access(wwdump_ww, X_OK) != 0) { fprintf(stderr, "848: ww_ww/wwdump_ww missing — cannot run the " "cs==ww byte-id gate (the whole point of this test)\n"); return 1; } int n = (int)(sizeof scenarios / sizeof scenarios[0]); int na = (int)(sizeof ast_cases / sizeof ast_cases[0]); int fail = 0; for (int i = 0; i < n; i++) { if (run_scenario(bin, cdrv, wdrv, &scenarios[i]) != 0) fail++; } for (int i = 0; i < na; i++) { if (run_ast_proof(wwdump, wwdump_ww, &ast_cases[i]) != 0) fail++; } if (fail) { fprintf(stderr, "848_xmod_qualstructlit: %d/%d checks failed\n", fail, n + na); return 1; } printf("848_xmod_qualstructlit: %d/%d ok\n", n + na, n + na); return 0; }