selfhost/cmd/wcc: extend enum fold to constexpr set (A.6.2.1a)

check.ww's N_DOT enum-fold (L1724-1810) walks the enum body to
resolve each `EnumT.MEMBER` access; the pre-#22 walker only accepted
N_INTLIT for a member's lhs and bailed on every richer shape via
`return nil`. Wwstage compensated at codegen time through cgen.ww's
enumevalmember (cgen.ww:158-227), so program semantics held; the
gap was visible only in check.ww's e.type_ stamp coverage, which
A.6.2.1e's post-checker assertion will land on.

Lift the literal-only branch into an `enumvalfold(body, until, e,
*u64) bool` helper alongside foldtointlit. The accepted set mirrors
cstage cmd/wcc/check.c:185-208 (fold_int_literal) + :210-284
(eval_enum_value) and harec's enum-resolve constexpr eval at
ref/harec/src/check.c:4419-4434: literal leaves
(INTLIT/RUNELIT/TRUE/FALSE/NIL), unary +/-/~, binary +/-/*//%
& | ^ << >>, and N_IDENT sibling backref bounded by `until` per
harec's lnext forward-only-ref discipline
(ref/harec/src/check.c:4436-4438). Both N_DOT call sites (inner
`EnumT.MEMBER`, outer `pkg.EnumT.MEMBER` via base-resolve) delegate
non-literal lhs to enumvalfold instead of bailing.

Closes #7. Lands on the A.6.2.1a slot per PLAN.md / Drew's 5-lite
plan; subsequent A.6.2.1b-d retire the remaining bail paths before
A.6.2.1e enables the assertion.

Add test/wcc/759_check_enum_fold.c — table-driven, modelled on
631_def_neg_global.c. 17 rows cover each new shape (INTLIT,
RUNELIT, sibling backref, unary +/-/~, all ten binops, chained
backref). Exit-code rows pin per-shape fold correctness through
both stages (cgen reads the mutated N_INTLIT, so a wrong fold
leaks into the constant); asm-byte-id rows pin the symmetric-emit
contract between cstage's eval_enum_value and wwstage's
enumvalfold.

`make sizelint` clean. `make test` green 133/133 (132 pre +
new 759).
This commit is contained in:
2026-05-22 00:14:36 +09:00
parent 8e3752f5ea
commit f8aac547b9
5 changed files with 582 additions and 51 deletions

View File

@@ -7886,6 +7886,89 @@ fn foldtointlit(c: *checker, n: *node, v: i64) void = {
n.tsuffix = empty;
};
// enumvalfold — fold an enum member's value expression to a u64
// constant. The Hare-fidelity set: literal leaves, unary +/-/~,
// binary arithmetic (+ - * / %), bitwise (& | ^), shifts (<< >>),
// and sibling backref. Mirrors cstage cmd/wcc/check.c:185-208
// (fold_int_literal) + :210-284 (eval_enum_value); the wider
// constexpr evaluator is at ref/harec/src/eval.c (harec resolves
// each enum member via eval_expr per ref/harec/src/check.c:4419-
// 4434). Wwstage cgen.ww:158-227 (foldintliteral + enumevalmember)
// already ships this set for codegen — check now matches.
//
// `body` is the N_TENUM whose .list is the member chain. `until`
// is the member currently being resolved; sibling lookup walks
// forward from body.list and stops at `until` to enforce harec's
// lnext forward-only-ref discipline (ref/harec/src/check.c:4436-
// 4438). `e` starts as that member's lhs and recurses into its
// children. Returns false on unfoldable shape, unknown sibling,
// or division by zero — callers bail the wrapping N_DOT fold.
//
// Recursion bound: O(N²) worst case on chained sibling backrefs
// (each ident lookup re-walks 0..until). Enum bodies are tiny in
// practice — harec accepts the same shape without memoisation per
// resolve_enum_field's wrap_resolver chain
// (ref/harec/src/check.c:4438) — so the quadratic is harmless.
fn enumvalfold(body: *node, until: *node, e: *node, out: *u64) bool = {
if (e == nil) { return false; };
let k: nkind = e.kind;
if (k == nkind.N_INTLIT) { *out = e.uval; return true; };
if (k == nkind.N_RUNELIT) { *out = e.uval; return true; };
if (k == nkind.N_TRUE) { *out = 1u64; return true; };
if (k == nkind.N_FALSE) { *out = 0u64; return true; };
if (k == nkind.N_NIL) { *out = 0u64; return true; };
if (k == nkind.N_UN) {
let v: u64 = 0u64;
if (!enumvalfold(body, until, e.lhs, &v)) { return false; };
let op: tkind = e.op;
if (op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; };
if (op == tkind.TK_TILDE) { *out = ~v; return true; };
if (op == tkind.TK_PLUS) { *out = v; return true; };
return false;
};
if (k == nkind.N_BIN) {
let a: u64 = 0u64;
let b: u64 = 0u64;
if (!enumvalfold(body, until, e.lhs, &a)) { return false; };
if (!enumvalfold(body, until, e.rhs, &b)) { return false; };
let op: tkind = e.op;
if (op == tkind.TK_PLUS) { *out = a + b; return true; };
if (op == tkind.TK_MINUS) { *out = a - b; return true; };
if (op == tkind.TK_STAR) { *out = a * b; return true; };
if (op == tkind.TK_SLASH) {
if (b == 0u64) { return false; };
*out = a / b; return true;
};
if (op == tkind.TK_PERCENT) {
if (b == 0u64) { return false; };
*out = a % b; return true;
};
if (op == tkind.TK_AMP) { *out = a & b; return true; };
if (op == tkind.TK_PIPE) { *out = a | b; return true; };
if (op == tkind.TK_CARET) { *out = a ^ b; return true; };
if (op == tkind.TK_LSHIFT) { *out = a << b; return true; };
if (op == tkind.TK_RSHIFT) { *out = a >> b; return true; };
return false;
};
if (k == nkind.N_IDENT) {
let prev: u64 = (-1i64): u64;
let m: *node = body.list;
for (m != nil && m != until) {
let val: u64 = 0u64;
if (m.lhs == nil) {
val = prev + 1u64;
} else {
if (!enumvalfold(body, m, m.lhs, &val)) { return false; };
};
prev = val;
if (streq(m.str, e.str)) { *out = val; return true; };
m = m.next;
};
return false;
};
return false;
};
// #61 A.5 helper: per-element slot size when `pt` appears inside a
// tuple. Mirrors cgenutil.ww slotsize TTUPLE — cstage's tuple ABI
// spills each element into its own register / 8B eightbyte, so narrow
@@ -8613,15 +8696,10 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
// :740-832. Struct field + pseudo-field (.len/.cap/.ptr) lands
// in A.6.1.5b. Wwstage has no use_alias (sym.mod disambiguates
// — see installdecl docstring at L195-207); SK_USE alone gates
// case 1. Enum-member fold here is best-effort: cgen.ww's
// enumevalmember covers N_INTLIT + N_IDENT-backref + N_BIN/N_UN,
// but every selfhost+lib enum body uses explicit integer
// literals (audited 2026-05-21), so the check-side eval stays
// N_INTLIT + auto-increment to avoid duplicating cgen's full
// const-folder. A non-literal member's lhs makes the auto-
// increment prev-tracking unsafe (we'd fold subsequent members
// off a wrong base), so we bail rather than guess — cgen's own
// N_DOT enum path still resolves the constant.
// case 1. Enum-member fold delegates non-literal lhs shapes
// (sibling backref, unary, binary, shift) to enumvalfold,
// matching cstage cmd/wcc/check.c:210-284 and harec's enum-
// resolve constexpr set at ref/harec/src/check.c:4419-4434.
let lhsn: *node = e.lhs;
if (lhsn != nil) { if (lhsn.kind == nkind.N_IDENT) {
let ms: *sym = scopelookupprefer(c.cur, c.curmod, lhsn.str);
@@ -8655,11 +8733,9 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
let val: u64 = 0u64;
if (m.lhs == nil) {
val = prev + 1u64;
} else { if (m.lhs.kind == nkind.N_INTLIT) {
val = m.lhs.uval;
} else {
return nil;
}; };
if (!enumvalfold(ub, m, m.lhs, &val)) { return nil; };
};
prev = val;
if (streq(m.str, e.str)) {
foldtointlit(c, e, val: i64);
@@ -8690,11 +8766,9 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
let val: u64 = 0u64;
if (m.lhs == nil) {
val = prev + 1u64;
} else { if (m.lhs.kind == nkind.N_INTLIT) {
val = m.lhs.uval;
} else {
return nil;
}; };
if (!enumvalfold(bu, m, m.lhs, &val)) { return nil; };
};
prev = val;
if (streq(m.str, e.str)) {
foldtointlit(c, e, val: i64);

View File

@@ -922,6 +922,89 @@ fn foldtointlit(c: *checker, n: *node, v: i64) void = {
n.tsuffix = empty;
};
// enumvalfold — fold an enum member's value expression to a u64
// constant. The Hare-fidelity set: literal leaves, unary +/-/~,
// binary arithmetic (+ - * / %), bitwise (& | ^), shifts (<< >>),
// and sibling backref. Mirrors cstage cmd/wcc/check.c:185-208
// (fold_int_literal) + :210-284 (eval_enum_value); the wider
// constexpr evaluator is at ref/harec/src/eval.c (harec resolves
// each enum member via eval_expr per ref/harec/src/check.c:4419-
// 4434). Wwstage cgen.ww:158-227 (foldintliteral + enumevalmember)
// already ships this set for codegen — check now matches.
//
// `body` is the N_TENUM whose .list is the member chain. `until`
// is the member currently being resolved; sibling lookup walks
// forward from body.list and stops at `until` to enforce harec's
// lnext forward-only-ref discipline (ref/harec/src/check.c:4436-
// 4438). `e` starts as that member's lhs and recurses into its
// children. Returns false on unfoldable shape, unknown sibling,
// or division by zero — callers bail the wrapping N_DOT fold.
//
// Recursion bound: O(N²) worst case on chained sibling backrefs
// (each ident lookup re-walks 0..until). Enum bodies are tiny in
// practice — harec accepts the same shape without memoisation per
// resolve_enum_field's wrap_resolver chain
// (ref/harec/src/check.c:4438) — so the quadratic is harmless.
fn enumvalfold(body: *node, until: *node, e: *node, out: *u64) bool = {
if (e == nil) { return false; };
let k: nkind = e.kind;
if (k == nkind.N_INTLIT) { *out = e.uval; return true; };
if (k == nkind.N_RUNELIT) { *out = e.uval; return true; };
if (k == nkind.N_TRUE) { *out = 1u64; return true; };
if (k == nkind.N_FALSE) { *out = 0u64; return true; };
if (k == nkind.N_NIL) { *out = 0u64; return true; };
if (k == nkind.N_UN) {
let v: u64 = 0u64;
if (!enumvalfold(body, until, e.lhs, &v)) { return false; };
let op: tkind = e.op;
if (op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; };
if (op == tkind.TK_TILDE) { *out = ~v; return true; };
if (op == tkind.TK_PLUS) { *out = v; return true; };
return false;
};
if (k == nkind.N_BIN) {
let a: u64 = 0u64;
let b: u64 = 0u64;
if (!enumvalfold(body, until, e.lhs, &a)) { return false; };
if (!enumvalfold(body, until, e.rhs, &b)) { return false; };
let op: tkind = e.op;
if (op == tkind.TK_PLUS) { *out = a + b; return true; };
if (op == tkind.TK_MINUS) { *out = a - b; return true; };
if (op == tkind.TK_STAR) { *out = a * b; return true; };
if (op == tkind.TK_SLASH) {
if (b == 0u64) { return false; };
*out = a / b; return true;
};
if (op == tkind.TK_PERCENT) {
if (b == 0u64) { return false; };
*out = a % b; return true;
};
if (op == tkind.TK_AMP) { *out = a & b; return true; };
if (op == tkind.TK_PIPE) { *out = a | b; return true; };
if (op == tkind.TK_CARET) { *out = a ^ b; return true; };
if (op == tkind.TK_LSHIFT) { *out = a << b; return true; };
if (op == tkind.TK_RSHIFT) { *out = a >> b; return true; };
return false;
};
if (k == nkind.N_IDENT) {
let prev: u64 = (-1i64): u64;
let m: *node = body.list;
for (m != nil && m != until) {
let val: u64 = 0u64;
if (m.lhs == nil) {
val = prev + 1u64;
} else {
if (!enumvalfold(body, m, m.lhs, &val)) { return false; };
};
prev = val;
if (streq(m.str, e.str)) { *out = val; return true; };
m = m.next;
};
return false;
};
return false;
};
// #61 A.5 helper: per-element slot size when `pt` appears inside a
// tuple. Mirrors cgenutil.ww slotsize TTUPLE — cstage's tuple ABI
// spills each element into its own register / 8B eightbyte, so narrow
@@ -1649,15 +1732,10 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
// :740-832. Struct field + pseudo-field (.len/.cap/.ptr) lands
// in A.6.1.5b. Wwstage has no use_alias (sym.mod disambiguates
// — see installdecl docstring at L195-207); SK_USE alone gates
// case 1. Enum-member fold here is best-effort: cgen.ww's
// enumevalmember covers N_INTLIT + N_IDENT-backref + N_BIN/N_UN,
// but every selfhost+lib enum body uses explicit integer
// literals (audited 2026-05-21), so the check-side eval stays
// N_INTLIT + auto-increment to avoid duplicating cgen's full
// const-folder. A non-literal member's lhs makes the auto-
// increment prev-tracking unsafe (we'd fold subsequent members
// off a wrong base), so we bail rather than guess — cgen's own
// N_DOT enum path still resolves the constant.
// case 1. Enum-member fold delegates non-literal lhs shapes
// (sibling backref, unary, binary, shift) to enumvalfold,
// matching cstage cmd/wcc/check.c:210-284 and harec's enum-
// resolve constexpr set at ref/harec/src/check.c:4419-4434.
let lhsn: *node = e.lhs;
if (lhsn != nil) { if (lhsn.kind == nkind.N_IDENT) {
let ms: *sym = scopelookupprefer(c.cur, c.curmod, lhsn.str);
@@ -1691,11 +1769,9 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
let val: u64 = 0u64;
if (m.lhs == nil) {
val = prev + 1u64;
} else { if (m.lhs.kind == nkind.N_INTLIT) {
val = m.lhs.uval;
} else {
return nil;
}; };
if (!enumvalfold(ub, m, m.lhs, &val)) { return nil; };
};
prev = val;
if (streq(m.str, e.str)) {
foldtointlit(c, e, val: i64);
@@ -1726,11 +1802,9 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
let val: u64 = 0u64;
if (m.lhs == nil) {
val = prev + 1u64;
} else { if (m.lhs.kind == nkind.N_INTLIT) {
val = m.lhs.uval;
} else {
return nil;
}; };
if (!enumvalfold(bu, m, m.lhs, &val)) { return nil; };
};
prev = val;
if (streq(m.str, e.str)) {
foldtointlit(c, e, val: i64);

View File

@@ -7886,6 +7886,89 @@ fn foldtointlit(c: *checker, n: *node, v: i64) void = {
n.tsuffix = empty;
};
// enumvalfold — fold an enum member's value expression to a u64
// constant. The Hare-fidelity set: literal leaves, unary +/-/~,
// binary arithmetic (+ - * / %), bitwise (& | ^), shifts (<< >>),
// and sibling backref. Mirrors cstage cmd/wcc/check.c:185-208
// (fold_int_literal) + :210-284 (eval_enum_value); the wider
// constexpr evaluator is at ref/harec/src/eval.c (harec resolves
// each enum member via eval_expr per ref/harec/src/check.c:4419-
// 4434). Wwstage cgen.ww:158-227 (foldintliteral + enumevalmember)
// already ships this set for codegen — check now matches.
//
// `body` is the N_TENUM whose .list is the member chain. `until`
// is the member currently being resolved; sibling lookup walks
// forward from body.list and stops at `until` to enforce harec's
// lnext forward-only-ref discipline (ref/harec/src/check.c:4436-
// 4438). `e` starts as that member's lhs and recurses into its
// children. Returns false on unfoldable shape, unknown sibling,
// or division by zero — callers bail the wrapping N_DOT fold.
//
// Recursion bound: O(N²) worst case on chained sibling backrefs
// (each ident lookup re-walks 0..until). Enum bodies are tiny in
// practice — harec accepts the same shape without memoisation per
// resolve_enum_field's wrap_resolver chain
// (ref/harec/src/check.c:4438) — so the quadratic is harmless.
fn enumvalfold(body: *node, until: *node, e: *node, out: *u64) bool = {
if (e == nil) { return false; };
let k: nkind = e.kind;
if (k == nkind.N_INTLIT) { *out = e.uval; return true; };
if (k == nkind.N_RUNELIT) { *out = e.uval; return true; };
if (k == nkind.N_TRUE) { *out = 1u64; return true; };
if (k == nkind.N_FALSE) { *out = 0u64; return true; };
if (k == nkind.N_NIL) { *out = 0u64; return true; };
if (k == nkind.N_UN) {
let v: u64 = 0u64;
if (!enumvalfold(body, until, e.lhs, &v)) { return false; };
let op: tkind = e.op;
if (op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; };
if (op == tkind.TK_TILDE) { *out = ~v; return true; };
if (op == tkind.TK_PLUS) { *out = v; return true; };
return false;
};
if (k == nkind.N_BIN) {
let a: u64 = 0u64;
let b: u64 = 0u64;
if (!enumvalfold(body, until, e.lhs, &a)) { return false; };
if (!enumvalfold(body, until, e.rhs, &b)) { return false; };
let op: tkind = e.op;
if (op == tkind.TK_PLUS) { *out = a + b; return true; };
if (op == tkind.TK_MINUS) { *out = a - b; return true; };
if (op == tkind.TK_STAR) { *out = a * b; return true; };
if (op == tkind.TK_SLASH) {
if (b == 0u64) { return false; };
*out = a / b; return true;
};
if (op == tkind.TK_PERCENT) {
if (b == 0u64) { return false; };
*out = a % b; return true;
};
if (op == tkind.TK_AMP) { *out = a & b; return true; };
if (op == tkind.TK_PIPE) { *out = a | b; return true; };
if (op == tkind.TK_CARET) { *out = a ^ b; return true; };
if (op == tkind.TK_LSHIFT) { *out = a << b; return true; };
if (op == tkind.TK_RSHIFT) { *out = a >> b; return true; };
return false;
};
if (k == nkind.N_IDENT) {
let prev: u64 = (-1i64): u64;
let m: *node = body.list;
for (m != nil && m != until) {
let val: u64 = 0u64;
if (m.lhs == nil) {
val = prev + 1u64;
} else {
if (!enumvalfold(body, m, m.lhs, &val)) { return false; };
};
prev = val;
if (streq(m.str, e.str)) { *out = val; return true; };
m = m.next;
};
return false;
};
return false;
};
// #61 A.5 helper: per-element slot size when `pt` appears inside a
// tuple. Mirrors cgenutil.ww slotsize TTUPLE — cstage's tuple ABI
// spills each element into its own register / 8B eightbyte, so narrow
@@ -8613,15 +8696,10 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
// :740-832. Struct field + pseudo-field (.len/.cap/.ptr) lands
// in A.6.1.5b. Wwstage has no use_alias (sym.mod disambiguates
// — see installdecl docstring at L195-207); SK_USE alone gates
// case 1. Enum-member fold here is best-effort: cgen.ww's
// enumevalmember covers N_INTLIT + N_IDENT-backref + N_BIN/N_UN,
// but every selfhost+lib enum body uses explicit integer
// literals (audited 2026-05-21), so the check-side eval stays
// N_INTLIT + auto-increment to avoid duplicating cgen's full
// const-folder. A non-literal member's lhs makes the auto-
// increment prev-tracking unsafe (we'd fold subsequent members
// off a wrong base), so we bail rather than guess — cgen's own
// N_DOT enum path still resolves the constant.
// case 1. Enum-member fold delegates non-literal lhs shapes
// (sibling backref, unary, binary, shift) to enumvalfold,
// matching cstage cmd/wcc/check.c:210-284 and harec's enum-
// resolve constexpr set at ref/harec/src/check.c:4419-4434.
let lhsn: *node = e.lhs;
if (lhsn != nil) { if (lhsn.kind == nkind.N_IDENT) {
let ms: *sym = scopelookupprefer(c.cur, c.curmod, lhsn.str);
@@ -8655,11 +8733,9 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
let val: u64 = 0u64;
if (m.lhs == nil) {
val = prev + 1u64;
} else { if (m.lhs.kind == nkind.N_INTLIT) {
val = m.lhs.uval;
} else {
return nil;
}; };
if (!enumvalfold(ub, m, m.lhs, &val)) { return nil; };
};
prev = val;
if (streq(m.str, e.str)) {
foldtointlit(c, e, val: i64);
@@ -8690,11 +8766,9 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
let val: u64 = 0u64;
if (m.lhs == nil) {
val = prev + 1u64;
} else { if (m.lhs.kind == nkind.N_INTLIT) {
val = m.lhs.uval;
} else {
return nil;
}; };
if (!enumvalfold(bu, m, m.lhs, &val)) { return nil; };
};
prev = val;
if (streq(m.str, e.str)) {
foldtointlit(c, e, val: i64);