wcc,lib/ww/syntax: resolve qualified struct-literal pkg.Type{...} (#76)
The parser folded a qualified type pkg.Type into two different node shapes by position: declaration position collapsed it into one N_TNAME (resolved via the strrchr-leaf path), but literal position left an N_DOT chain that the struct-literal typeref handoff had no resolver arm for, so pkg.Type{...} rejected with "expected type expression".
Normalize the literal-position N_DOT chain into the same source-order N_TNAME the declaration path emits, reusing the existing resolver; no new checker arm. cstage flattens at parseprimary struct-lit handoff; wwstage (no token peek) folds dots in parsepostfix and normalizes there, guarding numeric tuple components and staying in the postfix loop so trailing ops still chain. Both stages emit identical N_STRUCTLIT(N_TNAME). Prereq for qualifying wcc syntax refs (#75).
This commit is contained in:
14
Makefile
14
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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
404
test/wcc/848_xmod_qualstructlit_run.c
Normal file
404
test/wcc/848_xmod_qualstructlit_run.c
Normal file
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user