check: reject invalid enum decls in wwstage (catB-2)

The wwstage checker silently accepted enums with a non-integer storage type, duplicate members, or a non-constant member value; cstage already rejects all three (cmd/wcc/check.c:1000-1042). Add validateenummembers, a pure read-only diagnostic dispatched once per enum decl from resolvewalk's N_TENUM arm (check.ww:791, beside stampenumvals -- not the per-query recompute arms), mirroring the catB-7/14 validatestructfields pattern. Storage gate uses typeisint on the resolved tinfo (the exact type_isint mirror: chases TY_NAMED.under and TY_ENUM.sub, so an int-alias storage is accepted; raw-AST isinttypeast would not). Duplicate members: O(n^2) name walk. Unfoldable values reuse enumvalfold with until=member (forward-only). Emits via cerr + c.errs, no mutation, so valid-program codegen is unchanged and cstage==wwstage byte-id holds.

wwstage's value-fold message is intentionally generic where cstage's eval_enum_value gives per-reason text (enumvalfold returns a bool, not a reason); both reject. Documented at the site, filed as follow-up task #10.

Test: new table-driven both-stage reject test 850_enum_reject (non-int storage / duplicate member / forward-ref rows + a distinct-member control whose backward-ref value builds and runs). Full make test: 452 green incl. 990-997 byte-id.
This commit is contained in:
2026-06-19 15:01:08 +09:00
parent c024c09bc6
commit 7589b1bf0b
3 changed files with 263 additions and 1 deletions

View File

@@ -788,7 +788,7 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
};
};
if (k == syntax.nkind.N_TENUM) { stampenumvals(c, n); };
if (k == syntax.nkind.N_TENUM) { stampenumvals(c, n); validateenummembers(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
@@ -1894,6 +1894,72 @@ fn validatestructfields(c: *checker, n: *syntax.node) void = {
};
};
// validateenummembers — reject the three invalid enum-decl shapes
// cstage rejects at cmd/wcc/check.c:1000-1042 (the N_TENUM arm of
// resolve_type): a non-integer storage type, a duplicate member name,
// and an unfoldable (non-constant) member value-expr. Pure diagnostic:
// it mutates no n.type_ / member value / checker state beyond bumping
// c.errs, so valid programs (integer storage, distinct members,
// foldable values) emit nothing and their stamping/codegen is untouched
// — the byte-id safety condition. Fires once per enum decl from
// resolvewalk's eager type-decl dispatch, sibling to validatestructfields
// (rule-10 symmetric with cstage's once-per-resolve_type), not from the
// per-query size/align arms.
fn validateenummembers(c: *checker, n: *syntax.node) void = {
// storage type: cstage resolves n->lhs then gates type_isint
// (check.c:1004-1009); default storage is i32, always integer.
if (n.lhs != nil) {
let s: *syntax.tinfo = tinfofornode(c, n.lhs);
if (!syntax.typeisint(s)) {
cerr(n.lhs.file);
cerr(": error: enum storage type must be integer\n");
c.errs += 1;
};
};
let m: *syntax.node = n.list;
for (m != nil) {
if (m.kind == syntax.nkind.N_TENUMMEMBER && m.str.len != 0) {
// duplicate member: cstage compares each member against
// the earlier ones (check.c:1024-1032).
let e: *syntax.node = n.list;
for (e != m) {
if (e.kind == syntax.nkind.N_TENUMMEMBER
&& e.str.len != 0
&& syntax.streq(e.str, m.str)) {
cerr(m.file);
cerr(": error: duplicate enum member '");
cerr(m.str);
cerr("'\n");
c.errs += 1;
break;
};
e = e.next;
};
};
// unfoldable value: cstage delegates to eval_enum_value
// (check.c:1020), which emits a SHAPE-SPECIFIC reason —
// "enum value: unknown identifier 'B'" for a forward sibling
// ref (check.c:316), "...division by zero" (327), "...unsupported
// binary/unary op" (330/343), else the generic constant-expr
// message (349). enumvalfold returns a bare bool (no reason), so
// wwstage collapses all of these to the generic "enum value must
// be a constant integer expression". Both stages REJECT (errs
// counted, build gated) → functionally symmetric, no asm impact;
// the message-specificity gap is the lone divergence, retained
// and tracked as task #10 (rule-7: documented, not silent).
// `until = m` enforces harec's forward-only sibling-ref discipline.
if (m.lhs != nil) {
let v: u64 = 0u64;
if (!enumvalfold(n, m, m.lhs, &v)) {
cerr(m.file);
cerr(": error: enum value must be a constant integer expression\n");
c.errs += 1;
};
};
m = m.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