From 07b03d6f9ca69d06ef81f45e6a9fbdb773e997ae Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 8 Aug 2026 15:03:45 +0900 Subject: [PATCH] test: port the qualstructlit AST proof to ww; retire 848_xmod_qualstructlit_run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only the ast_proof leg was unowned: wwdump -a / wwdump_ww -a dump byte-id plus the ordered dot-over-structlit-over-tname node shape for a trailing op on a qualified struct literal (inexpressible as a runtime scenario while the #77 literal-temporary cgen gap holds; the multilevel row pins the source-order a.b.C tname flatten). The three build/run scenarios are owned by the r84_qualstruct_* fixtures plus the test-data-byteid blanket — their vestigial want_exit fields were already dead in the carrier. --- Makefile | 2 +- test/misc/qualast_test.ww | 95 ++++++ test/wcc/848_xmod_qualstructlit_run.c | 440 -------------------------- 3 files changed, 96 insertions(+), 441 deletions(-) create mode 100644 test/misc/qualast_test.ww delete mode 100644 test/wcc/848_xmod_qualstructlit_run.c diff --git a/Makefile b/Makefile index bb86909c..18544b3d 100644 --- a/Makefile +++ b/Makefile @@ -409,7 +409,7 @@ OBJECT_WW_TARGETS = $(OBJECT_WW_TESTS:%=wwtest/%) # under test-compiler. MISC_WW_TESTS = test/misc/arrglob_test.ww test/misc/packedwwi_test.ww \ test/misc/reject_test.ww test/misc/heldasym_test.ww \ - test/misc/stampdiag_test.ww + test/misc/stampdiag_test.ww test/misc/qualast_test.ww MISC_WW_TARGETS = $(MISC_WW_TESTS:%=wwtest/%) BOOTSTRAP_WRAPPER_SOURCES = test/wcc/950_selfcheck.c \ test/wcc/991_w6a_ww.c \ diff --git a/test/misc/qualast_test.ww b/test/misc/qualast_test.ww new file mode 100644 index 00000000..31b2edc1 --- /dev/null +++ b/test/misc/qualast_test.ww @@ -0,0 +1,95 @@ +package qualast_test; + +// The ast_proof remainder of the retired native carrier +// test/wcc/848_xmod_qualstructlit_run.c (#76 qualified struct +// literals). The three build/run scenarios are owned by the +// r84_qualstruct_{fields,ret_assign,nested} fixtures plus the +// test-data-byteid blanket; what no fixture can host is the PARSE +// node-shape proof for a TRAILING op on a qualified struct literal — +// `pkg.point{...}.x` trips the pre-existing cgen literal-temporary +// gap (#77), so it can never ride a runtime scenario. +// +// Per row: `wwdump -a` (parse-only) on both stages, rc 0, dumps +// byte-identical (rule 10), then ordered substrings prove +// dot-over-structlit-over-tname — the parser's postfix loop stayed +// live past the `}` so `.x` wrapped the literal, and the dotted path +// flattened into ONE source-order N_TNAME (parsetype's dotted +// collapse, byte-identical across parse positions). astprint resolves +// no names, so `pkg` / `a.b.C` need not name a real import. + +import os; +import os.exec; +import strings; +import testenv; +import time; + +fn fail(label: str, why: str) void = { + let m: str = strings.concat("qualast FAIL: ", label, " -- ", why, + "\n"); + os.write(2, m.ptr, m.len: u64); + assert(false); +}; + +fn tmo() time.duration = { + return (180i64 * (time.second: i64)): time.duration; +}; + +fn dumpstage(td: str, label: str, stage: str, tool: str) str = { + let av: []str = [testenv.driver(tool), "-a", + strings.concat(td, "/a.ww")]; + let co: testenv.commandout; + testenv.runcommand(td, td, stage, av, tmo(), &co); + if (co.termination != exec.termination.EXIT || co.code != 0) { + fail(label, strings.concat(stage, " ", tool, " -a failed")); + }; + return co.stdout; +}; + +// The needles must occur in order (tree nesting without coupling to +// astprint's exact indentation). +fn ordered(hay: str, needles: []str) bool = { + let from: i32 = 0; + let i: i32 = 0; + for (i < needles.len) { + let rest: str = strings.sub(hay, from, hay.len); + let p: i32 = testenv.pos(rest, needles[i]); + if (p < 0) { return false; }; + from = from + p + 1; + i += 1; + }; + return true; +}; + +fn astrow(label: str, src: str, needles: []str) void = { + let td: str = testenv.fresh(); + testenv.writefile(strings.concat(td, "/a.ww"), src); + let cs: str = dumpstage(td, label, "cstage", "wwdump"); + let ws: str = dumpstage(td, label, "wwstage", "wwdump_ww"); + if (!testenv.same(cs, ws)) { + fail(label, "cs/ww AST dumps DIFFER (rule-10)"); + }; + if (!ordered(cs, needles)) { + fail(label, strings.concat("AST node shape wrong (trailing ", + "op did not wrap the qualified struct literal)")); + }; + testenv.clean(td); +}; + +@test fn astproof() void = { + let trailing: []str = ["(dot \"x\"", "(structlit", + "(tname \"pkg.point\""]; + astrow("trailing", strings.concat( + "package main;\n", + "export fn f() i64 = {\n", + "\tlet v: i64 = pkg.point { x = 1i64, y = 2i64 }.x;\n", + "\treturn v;\n", + "};\n"), trailing); + let multilevel: []str = ["(dot \"x\"", "(structlit", + "(tname \"a.b.C\""]; + astrow("multilevel", strings.concat( + "package main;\n", + "export fn g() i64 = {\n", + "\tlet v: i64 = a.b.C { x = 1i64 }.x;\n", + "\treturn v;\n", + "};\n"), multilevel); +}; diff --git a/test/wcc/848_xmod_qualstructlit_run.c b/test/wcc/848_xmod_qualstructlit_run.c deleted file mode 100644 index e9d8790c..00000000 --- a/test/wcc/848_xmod_qualstructlit_run.c +++ /dev/null @@ -1,440 +0,0 @@ -/* - * 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 (the linked binary, the .s probes) land OFF the - * source tree (CLAUDE.md rule 14). - */ -#include -#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; } - int writebad = fputs(ac->src, f) == EOF || ferror(f); - if (fclose(f) != 0) writebad = 1; - if (writebad) { - fprintf(stderr, "848-ast[%s]: write\n", ac->label); - rc = -1; goto done; - } - - 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: - { - int cleanfail = 0; - snprintf(src, sizeof src, "%s/a.ww", dir); - if (unlink(src) != 0 && errno != ENOENT) cleanfail = 1; - snprintf(cs, sizeof cs, "%s/cs.ast", dir); - if (unlink(cs) != 0 && errno != ENOENT) cleanfail = 1; - snprintf(ww, sizeof ww, "%s/ww.ast", dir); - if (unlink(ww) != 0 && errno != ENOENT) cleanfail = 1; - if (rmdir(dir) != 0) cleanfail = 1; - if (cleanfail) { - fprintf(stderr, "848-ast[%s]: workspace cleanup failed\n", - ac->label); - if (rc == 0) rc = -1; - } - } - 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; } - int writebad = fputs(sc->files[i].src, f) == EOF || ferror(f); - if (fclose(f) != 0) writebad = 1; - if (writebad) { - fprintf(stderr, "848[%s]: write %s\n", sc->label, - sc->files[i].name); - rc = -1; goto done; - } - } - - /* Runtime values moved to wwfixtures. These builds intentionally keep - * their caller-owned compiler artifacts for the byte assertion below. */ - snprintf(cmd, sizeof cmd, - "cd %s && %s build -I %s -o %s/main %s/main.ww", - 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; - } - /* rule-10 discriminator: this colocated two-file fixture composes into - * the exact retained __root.s artifact, which must be byte-identical - * across driver stages. */ - char cs_s[1024], ws_s[1024]; - snprintf(cmd, sizeof cmd, - "cd %s && %s build -I %s -o %s/mainww %s/main.ww", - 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/main.sepwork/__root.s", dir); - snprintf(ws_s, sizeof ws_s, "%s/mainww.sepwork/__root.s", dir); - 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: - { - int cleanfail = 0; - for (int i = 0; sc->files[i].name; i++) { - snprintf(path, sizeof path, "%s/%s", dir, - sc->files[i].name); - if (unlink(path) != 0 && errno != ENOENT) cleanfail = 1; - } - const char *files[] = { "main", "mainww", NULL }; - for (int i = 0; files[i]; i++) { - snprintf(path, sizeof path, "%s/%s", dir, files[i]); - if (unlink(path) != 0 && errno != ENOENT) cleanfail = 1; - } - snprintf(path, sizeof path, "%s/main.sepwork", dir); - snprintf(cmd, sizeof cmd, "rm -rf %s", path); - if (runwait(cmd) != 0) cleanfail = 1; - snprintf(path, sizeof path, "%s/mainww.sepwork", dir); - snprintf(cmd, sizeof cmd, "rm -rf %s", path); - if (runwait(cmd) != 0) cleanfail = 1; - if (rmdir(dir) != 0) cleanfail = 1; - if (cleanfail) { - fprintf(stderr, "848[%s]: workspace cleanup failed\n", - sc->label); - if (rc == 0) rc = -1; - } - } - 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; -}