check: reject duplicate struct field name in wwstage (catB-7/14)

The wwstage checker silently accepted a struct with repeated field names; cstage already rejects it (cmd/wcc/check.c:925-947). Add validatestructfields, dispatched once per struct decl from resolvewalk's eager type-decl arm (check.ww:792, sibling to the N_TENUM stampenumvals fire): a pure read-only O(n^2) named-field dup walk that emits "duplicate field 'X'" via cerr + c.errs, with no mutation -- valid-program codegen is unchanged so cstage==wwstage byte-id holds. Named fields only; ww has no struct embedding, so cstage's embed-collision arm is intentionally not ported (separate parser gap, catB-89).

Test: new table-driven both-stage reject test 849_dupfield_reject (adjacent / non-adjacent / different-type dup rows + a distinct-field control that builds and runs). Full make test: 451 green incl. 990-997 byte-id.
This commit is contained in:
2026-06-19 14:41:57 +09:00
parent e9f64a0727
commit c024c09bc6
3 changed files with 229 additions and 0 deletions

View File

@@ -789,6 +789,7 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
};
if (k == syntax.nkind.N_TENUM) { stampenumvals(c, n); };
if (k == syntax.nkind.N_TSTRUCT) { validatestructfields(c, n); };
// #42's size/align/offset fold trigger lived here pre-A.6.0; the
// A.6.0 end-of-fn general dispatch (below) now fires exprtype on
@@ -1859,6 +1860,40 @@ fn stampenumvals(c: *checker, n: *syntax.node) void = {
};
};
// validatestructfields — reject a struct decl carrying two fields with
// the same name. Pure diagnostic: a read-only O(n^2) walk over the named
// field list, no n.type_ / offset / checker-state mutation (the byte-id
// safety condition — valid programs have no dup, so codegen is untouched).
// Cstage twin: cmd/wcc/check.c:943-950 (the regular-named-field arm of
// resolve_type's N_TSTRUCT). The embed-collision arm there (check.c:961-
// 985) is NOT ported — ww has no struct embedding (parser gap catB-89),
// so every N_TFIELD is a regular named field. Fires once per struct decl
// from resolvewalk's eager type-decl dispatch (rule-10 symmetric with
// cstage's once-per-resolve_type), not from the per-query size/align/
// offset recompute arms (use-site = double-fire, drew-dv ruling).
fn validatestructfields(c: *checker, n: *syntax.node) void = {
let f: *syntax.node = n.list;
for (f != nil) {
if (f.kind == syntax.nkind.N_TFIELD && f.str.len != 0) {
let e: *syntax.node = n.list;
for (e != f) {
if (e.kind == syntax.nkind.N_TFIELD
&& e.str.len != 0
&& syntax.streq(e.str, f.str)) {
cerr(f.file);
cerr(": error: duplicate field '");
cerr(f.str);
cerr("'\n");
c.errs += 1;
break;
};
e = e.next;
};
};
f = f.next;
};
};
// stampnilexpr — stamp nil-typed nodes in a constant expr subtree to
// `ti`. lhs/rhs cover the enum constexpr grammar enumvalfold accepts
// (literals, unary, binary, sibling backref); non-nil nodes keep the