diff --git a/Makefile b/Makefile index 5c9d6990..a216c380 100644 --- a/Makefile +++ b/Makefile @@ -497,6 +497,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_type_value_shadow_run \ $(BIN)/test_xmod_alias_struct_collide_run \ $(BIN)/test_xmod_variant_match \ + $(BIN)/test_xmod_qualstructlit_run \ $(BIN)/test_spread_variant_match \ $(BIN)/test_xmod_ident_prefer \ $(BIN)/test_xmod_valglobal_run \ @@ -1842,6 +1843,19 @@ $(BIN)/test_xmod_variant_match: test/wcc/787_xmod_variant_match.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #76: a qualified struct literal `pkg.Type{...}` (type named through an +# imported module). Pre-fix it failed to parse (the dotted typeref folded +# to an N_DOT chain the checker's resolve_type had no arm for); the fix +# flattens the chain to one N_TNAME at the struct-lit handoff in BOTH +# stages. cstage driver build + run pins parse + runtime field values; +# raw w6c vs w6c_ww on the combined.ww is the rule-10 byte-id gate. +# Builds its own 2-module fixtures in a private mktemp dir. +$(BIN)/test_xmod_qualstructlit_run: test/wcc/848_xmod_qualstructlit_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/w6c_ww $(BIN)/wwdump $(BIN)/wwdump_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # #209: a `match` over a tagged union with a `...inner` SPREAD variant # (e.g. fmt's `field = (...formattable | *mods)`). The wwstage checker # walked the raw AST u.list and false-rejected every flattened member arm diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index 3adb9307..a4ed9fe3 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -427,6 +427,28 @@ parsearglist(Parser *p, Tkind close) return head; } +/* #76: a qualified path `pkg.Type` reaches a struct-literal handoff as an + * N_DOT chain (parseprimary's dotted fold). Flatten it into one N_TNAME + * whose str joins the chain in SOURCE order — byte-identical to parsetype's + * dotted collapse (parse.c: aprintf("%s.%s", ...)) — so resolve_type's + * existing N_TNAME arm resolves it with no new checker arm. */ +static const char * +flattendotstr(Parser *p, Node *n) +{ + if (n->kind == N_IDENT) + return n->str; + return aprintf(p->a, "%s.%s", flattendotstr(p, n->lhs), n->str); +} + +static Node * +flattendot(Parser *p, Node *n) +{ + Node *t = newnode(p->a, N_TNAME, n->pos); + t->str = flattendotstr(p, n); + t->strlen = strlen(t->str); + return t; +} + static Node * parsestructlit(Parser *p, Node *typeref) { @@ -636,8 +658,13 @@ parseprimary(Parser *p) n = mr; } /* struct literal: ident '{' ... '}' (only if ident-shaped) */ - if (p->cur.kind == TK_LBRACE) + if (p->cur.kind == TK_LBRACE) { + /* #76: bare `Foo{}` keeps the N_IDENT fast-path; a + * qualified `pkg.Type{}` (N_DOT chain) flattens first. */ + if (n->kind == N_DOT) + n = flattendot(p, n); return parsestructlit(p, n); + } return n; } default: diff --git a/lib/ww/syntax/expr.ww b/lib/ww/syntax/expr.ww index 09eb31f5..228d4bd7 100644 --- a/lib/ww/syntax/expr.ww +++ b/lib/ww/syntax/expr.ww @@ -243,6 +243,31 @@ fn parsearglist(p: *parser, closekind: tkind, headout: **node) void = { *headout = head; }; +// #76: flatten a qualified `pkg.Type` N_DOT chain into its source-order +// dotted string ("a.b.C"), byte-identical to cstage parsetype's aprintf +// accumulation (cmd/wcc/parse.c) and to flattendotstr in parse.c. +fn flattendotstr(n: *node) str = { + if (n.kind == nkind.N_IDENT) { return n.str; }; + return joindotted(flattendotstr(n.lhs), n.str); +}; + +// #76: an N_DOT chain qualifies as a type path only when it is rooted at +// a bare IDENT and every component is an identifier. cstage folds only +// peek==TK_IDENT dots in parseprimary, so its chain is inherently +// all-ident; ww folds ALL dots (incl tuple `t.0`) in parsepostfix, so we +// guard out numeric-first-byte components here to restore exact symmetry. +fn isqualpath(n: *node) bool = { + if (n.kind != nkind.N_DOT) { return false; }; + let cur: *node = n; + for (cur.kind == nkind.N_DOT) { + if (cur.str.len < 1) { return false; }; + let c0: u8 = cur.str[0]; + if (c0 >= '0') { if (c0 <= '9') { return false; }; }; + cur = cur.lhs; + }; + return cur.kind == nkind.N_IDENT; +}; + fn parsepostfix(p: *parser, lhs: *node) *node = { let cur: *node = lhs; for (true) { @@ -328,6 +353,51 @@ fn parsepostfix(p: *parser, lhs: *node) *node = { cur = n; continue; }; + // #76: qualified struct literal `pkg.Type{...}`. cstage folds + // the dotted chain in parseprimary (peek) and hits `{` there; + // the ww parser has no token-peek and folds dots HERE, so the + // struct-lit handoff lives here too. Fire only on an all-ident + // N_DOT chain (isqualpath); bare `Foo{` stays in parseprimary. + // Build the literal IN-LOOP and `continue` (req a) so trailing + // ops (`.f`, `[i]`, `()`, `as T`) still apply, matching cstage's + // return-to-outer-parsepostfix. The flattened N_TNAME (req c: + // source order) lets the checker's existing N_TNAME arm resolve. + if (p.curkind == tkind.TK_LBRACE) { + if (isqualpath(cur)) { + let tn: *node = newnode(nkind.N_TNAME, pf, pl, pc); + tn.str = flattendotstr(cur); + advance(p); + let s: *node = newnode(nkind.N_STRUCTLIT, pf, pl, pc); + s.lhs = tn; + let head: *node = nil; + let tail: *node = nil; + for (p.curkind != tkind.TK_RBRACE) { + if (p.curkind == tkind.TK_EOF) { break; }; + if (p.curkind == tkind.TK_ELLIPSIS) { + advance(p); + s.op = tkind.TK_ELLIPSIS; + break; + }; + let fpf: str = p.curfile; + let fpl: i32 = p.curline; + let fpc: i32 = p.curcol; + let id: str; + expectident(p, &id); + expecttok(p, tkind.TK_ASSIGN, "expected '=' in struct lit field"); + let v: *node = parseexpr(p); + let f: *node = newnode(nkind.N_FIELD, fpf, fpl, fpc); + f.str = id; + f.lhs = v; + if (head == nil) { head = f; tail = f; } + else { tail.next = f; tail = f; }; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + }; + expecttok(p, tkind.TK_RBRACE, "expected '}' after struct literal"); + s.list = head; + cur = s; + continue; + }; + }; if (p.curkind == tkind.TK_COLON) { if (p.nocast != 0) { return cur; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index d7a5948e..91947b15 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -7249,6 +7249,31 @@ fn parsearglist(p: *parser, closekind: tkind, headout: **node) void = { *headout = head; }; +// #76: flatten a qualified `pkg.Type` N_DOT chain into its source-order +// dotted string ("a.b.C"), byte-identical to cstage parsetype's aprintf +// accumulation (cmd/wcc/parse.c) and to flattendotstr in parse.c. +fn flattendotstr(n: *node) str = { + if (n.kind == nkind.N_IDENT) { return n.str; }; + return joindotted(flattendotstr(n.lhs), n.str); +}; + +// #76: an N_DOT chain qualifies as a type path only when it is rooted at +// a bare IDENT and every component is an identifier. cstage folds only +// peek==TK_IDENT dots in parseprimary, so its chain is inherently +// all-ident; ww folds ALL dots (incl tuple `t.0`) in parsepostfix, so we +// guard out numeric-first-byte components here to restore exact symmetry. +fn isqualpath(n: *node) bool = { + if (n.kind != nkind.N_DOT) { return false; }; + let cur: *node = n; + for (cur.kind == nkind.N_DOT) { + if (cur.str.len < 1) { return false; }; + let c0: u8 = cur.str[0]; + if (c0 >= '0') { if (c0 <= '9') { return false; }; }; + cur = cur.lhs; + }; + return cur.kind == nkind.N_IDENT; +}; + fn parsepostfix(p: *parser, lhs: *node) *node = { let cur: *node = lhs; for (true) { @@ -7334,6 +7359,51 @@ fn parsepostfix(p: *parser, lhs: *node) *node = { cur = n; continue; }; + // #76: qualified struct literal `pkg.Type{...}`. cstage folds + // the dotted chain in parseprimary (peek) and hits `{` there; + // the ww parser has no token-peek and folds dots HERE, so the + // struct-lit handoff lives here too. Fire only on an all-ident + // N_DOT chain (isqualpath); bare `Foo{` stays in parseprimary. + // Build the literal IN-LOOP and `continue` (req a) so trailing + // ops (`.f`, `[i]`, `()`, `as T`) still apply, matching cstage's + // return-to-outer-parsepostfix. The flattened N_TNAME (req c: + // source order) lets the checker's existing N_TNAME arm resolve. + if (p.curkind == tkind.TK_LBRACE) { + if (isqualpath(cur)) { + let tn: *node = newnode(nkind.N_TNAME, pf, pl, pc); + tn.str = flattendotstr(cur); + advance(p); + let s: *node = newnode(nkind.N_STRUCTLIT, pf, pl, pc); + s.lhs = tn; + let head: *node = nil; + let tail: *node = nil; + for (p.curkind != tkind.TK_RBRACE) { + if (p.curkind == tkind.TK_EOF) { break; }; + if (p.curkind == tkind.TK_ELLIPSIS) { + advance(p); + s.op = tkind.TK_ELLIPSIS; + break; + }; + let fpf: str = p.curfile; + let fpl: i32 = p.curline; + let fpc: i32 = p.curcol; + let id: str; + expectident(p, &id); + expecttok(p, tkind.TK_ASSIGN, "expected '=' in struct lit field"); + let v: *node = parseexpr(p); + let f: *node = newnode(nkind.N_FIELD, fpf, fpl, fpc); + f.str = id; + f.lhs = v; + if (head == nil) { head = f; tail = f; } + else { tail.next = f; tail = f; }; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + }; + expecttok(p, tkind.TK_RBRACE, "expected '}' after struct literal"); + s.list = head; + cur = s; + continue; + }; + }; if (p.curkind == tkind.TK_COLON) { if (p.nocast != 0) { return cur; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 80e9e680..c9255e1e 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -7249,6 +7249,31 @@ fn parsearglist(p: *parser, closekind: tkind, headout: **node) void = { *headout = head; }; +// #76: flatten a qualified `pkg.Type` N_DOT chain into its source-order +// dotted string ("a.b.C"), byte-identical to cstage parsetype's aprintf +// accumulation (cmd/wcc/parse.c) and to flattendotstr in parse.c. +fn flattendotstr(n: *node) str = { + if (n.kind == nkind.N_IDENT) { return n.str; }; + return joindotted(flattendotstr(n.lhs), n.str); +}; + +// #76: an N_DOT chain qualifies as a type path only when it is rooted at +// a bare IDENT and every component is an identifier. cstage folds only +// peek==TK_IDENT dots in parseprimary, so its chain is inherently +// all-ident; ww folds ALL dots (incl tuple `t.0`) in parsepostfix, so we +// guard out numeric-first-byte components here to restore exact symmetry. +fn isqualpath(n: *node) bool = { + if (n.kind != nkind.N_DOT) { return false; }; + let cur: *node = n; + for (cur.kind == nkind.N_DOT) { + if (cur.str.len < 1) { return false; }; + let c0: u8 = cur.str[0]; + if (c0 >= '0') { if (c0 <= '9') { return false; }; }; + cur = cur.lhs; + }; + return cur.kind == nkind.N_IDENT; +}; + fn parsepostfix(p: *parser, lhs: *node) *node = { let cur: *node = lhs; for (true) { @@ -7334,6 +7359,51 @@ fn parsepostfix(p: *parser, lhs: *node) *node = { cur = n; continue; }; + // #76: qualified struct literal `pkg.Type{...}`. cstage folds + // the dotted chain in parseprimary (peek) and hits `{` there; + // the ww parser has no token-peek and folds dots HERE, so the + // struct-lit handoff lives here too. Fire only on an all-ident + // N_DOT chain (isqualpath); bare `Foo{` stays in parseprimary. + // Build the literal IN-LOOP and `continue` (req a) so trailing + // ops (`.f`, `[i]`, `()`, `as T`) still apply, matching cstage's + // return-to-outer-parsepostfix. The flattened N_TNAME (req c: + // source order) lets the checker's existing N_TNAME arm resolve. + if (p.curkind == tkind.TK_LBRACE) { + if (isqualpath(cur)) { + let tn: *node = newnode(nkind.N_TNAME, pf, pl, pc); + tn.str = flattendotstr(cur); + advance(p); + let s: *node = newnode(nkind.N_STRUCTLIT, pf, pl, pc); + s.lhs = tn; + let head: *node = nil; + let tail: *node = nil; + for (p.curkind != tkind.TK_RBRACE) { + if (p.curkind == tkind.TK_EOF) { break; }; + if (p.curkind == tkind.TK_ELLIPSIS) { + advance(p); + s.op = tkind.TK_ELLIPSIS; + break; + }; + let fpf: str = p.curfile; + let fpl: i32 = p.curline; + let fpc: i32 = p.curcol; + let id: str; + expectident(p, &id); + expecttok(p, tkind.TK_ASSIGN, "expected '=' in struct lit field"); + let v: *node = parseexpr(p); + let f: *node = newnode(nkind.N_FIELD, fpf, fpl, fpc); + f.str = id; + f.lhs = v; + if (head == nil) { head = f; tail = f; } + else { tail.next = f; tail = f; }; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + }; + expecttok(p, tkind.TK_RBRACE, "expected '}' after struct literal"); + s.list = head; + cur = s; + continue; + }; + }; if (p.curkind == tkind.TK_COLON) { if (p.nocast != 0) { return cur; diff --git a/test/wcc/848_xmod_qualstructlit_run.c b/test/wcc/848_xmod_qualstructlit_run.c new file mode 100644 index 00000000..e8152dda --- /dev/null +++ b/test/wcc/848_xmod_qualstructlit_run.c @@ -0,0 +1,404 @@ +/* + * 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 *w6c, const char *w6c_ww, + const struct scenario *sc) +{ + 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); + } + + char comb[1024]; + snprintf(comb, sizeof comb, "%s/main.combined.ww", dir); + + /* cstage driver build + run: pins parse + runtime field values. */ + snprintf(cmd, sizeof cmd, "cd %s && %s/ww build -I %s %s/main.ww", + dir, bin, 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: raw w6c vs w6c_ww on the driver-produced + * combined.ww must be byte-identical .s. */ + char cs_s[1024], ws_s[1024]; + snprintf(cs_s, sizeof cs_s, "%s/cs.s", dir); + snprintf(ws_s, sizeof ws_s, "%s/ww.s", dir); + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, cs_s, comb); + if (runwait(cmd) != 0) { + fprintf(stderr, "848[%s]: w6c on combined failed\n", sc->label); + rc = -1; goto done; + } + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c_ww, ws_s, comb); + if (runwait(cmd) != 0) { + fprintf(stderr, "848[%s]: w6c_ww on combined failed " + "(qualified struct literal parse reject?)\n", sc->label); + rc = -1; goto done; + } + 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 w6c[2100], w6c_ww[2100], wwdump[2100], wwdump_ww[2100]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + snprintf(wwdump, sizeof wwdump, "%s/wwdump", bin); + snprintf(wwdump_ww, sizeof wwdump_ww, "%s/wwdump_ww", bin); + if (access(w6c_ww, X_OK) != 0 || access(wwdump_ww, X_OK) != 0) { + fprintf(stderr, "848: w6c_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, w6c, w6c_ww, &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; +}