From f8aac547b9c364336f966328314ace86465dc50b Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 22 May 2026 00:14:36 +0900 Subject: [PATCH] selfhost/cmd/wcc: extend enum fold to constexpr set (A.6.2.1a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 108 ++++++++-- selfhost/cmd/wcc/check.ww | 108 ++++++++-- selfhost/cmd/wwdump/main.combined.ww | 108 ++++++++-- test/wcc/759_check_enum_fold.c | 302 +++++++++++++++++++++++++++ 5 files changed, 582 insertions(+), 51 deletions(-) create mode 100644 test/wcc/759_check_enum_fold.c diff --git a/Makefile b/Makefile index 53a94c56..1cb9c9ff 100644 --- a/Makefile +++ b/Makefile @@ -292,6 +292,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ + $(BIN)/test_check_enum_fold \ $(BIN)/test_use_promote_alias \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ @@ -508,6 +509,12 @@ $(BIN)/test_cast_enum_movl: test/wcc/710_cast_enum_movl.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_check_enum_fold: test/wcc/759_check_enum_fold.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_arrlit_str_full: test/wcc/711_arrlit_str_full.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 5cb26325..d7b62c49 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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); diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 118cf94d..4e8e6a73 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -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); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 0ee89acf..522d2841 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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); diff --git a/test/wcc/759_check_enum_fold.c b/test/wcc/759_check_enum_fold.c new file mode 100644 index 00000000..266fa2f8 --- /dev/null +++ b/test/wcc/759_check_enum_fold.c @@ -0,0 +1,302 @@ +/* + * 759_check_enum_fold — table-driven sentinel for #22 / A.6.2.1a. + * + * check-side enum fold (selfhost/cmd/wcc/check.ww enumvalfold) now + * mirrors cstage cmd/wcc/check.c:210-284 eval_enum_value: literals + * (INTLIT/RUNELIT), unary +/-/~, binary arithmetic (+ - * / %), + * bitwise (& | ^), shifts (<< >>), and sibling backref via + * N_IDENT. The N_DOT fold mutates the AST to N_INTLIT in place, so + * a wrong fold value leaks into cgen as the wrong literal and the + * exit code reflects it. Each row is a self-contained ww program + * whose `return EnumT.MEMBER: i32` carries the expected fold. + * + * Pre-#22 the check-side fold bailed on every shape but bare + * N_INTLIT and auto-increment; cgen's enumevalmember + * (selfhost/cmd/wcc/cgen.ww:158-227) absorbed the slack at codegen + * time. Post-#22 check.ww does the fold itself so A.6.2.1e's + * post-checker e.type_ assertion no longer fires on `EnumT.MEMBER` + * references whose body is a backref or arithmetic expression. + * + * Each row runs through the cstage `ww` driver and through `ww_ww` + * when present; the asm-byte-id diff between w6c and w6c_ww pins + * the symmetric-emit contract (CLAUDE.md rule 10) so a future + * drift between the two enumvalfold copies fails loud. + */ +#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; +} + +struct row { const char *label; const char *src; int want; }; + +static const struct row rows[] = { + /* Baseline N_INTLIT — regression: the literal-only path was the + * pre-#22 fold contract and must still resolve. */ + { "intlit", + "type e = enum i32 { A = 7 };\n" + "fn main() i32 = { return e.A: i32; };\n", + 7 }, + + /* N_RUNELIT — rune literals carry their codepoint in n.uval + * exactly like N_INTLIT (cmd/wcc/check.c:191). */ + { "runelit", + "type e = enum i32 { A = 'A' };\n" + "fn main() i32 = { return e.A: i32; };\n", + 65 }, + + /* Bare sibling backref (no op): B = A. Tests the N_IDENT arm + * without a wrapping N_BIN/N_UN. */ + { "sibling_backref", + "type e = enum i32 { A = 7, B = A };\n" + "fn main() i32 = { return e.B: i32; };\n", + 7 }, + + /* N_UN TK_MINUS over literal, fed through a sibling backref. The + * NEG member exercises N_UN; the R member exercises the IDENT + * lookup that must resolve NEG's already-folded value. */ + { "unary_minus", + "type e = enum i32 { NEG = -7, R = NEG + 14 };\n" + "fn main() i32 = { return e.R: i32; };\n", + 7 }, + + /* Nested unary: ~(-8) folds to 7. Tests N_UN recursion (TK_TILDE + * over TK_MINUS over N_INTLIT). */ + { "unary_tilde_nested", + "type e = enum i32 { A = ~(-8) };\n" + "fn main() i32 = { return e.A: i32; };\n", + 7 }, + + /* N_UN TK_PLUS — noop wrapper; completes the unary whitelist. */ + { "unary_plus", + "type e = enum i32 { A = +7 };\n" + "fn main() i32 = { return e.A: i32; };\n", + 7 }, + + /* N_BIN TK_PLUS with sibling backref — the headline RW=R|W + * shape's relative. */ + { "bin_plus", + "type e = enum i32 { A = 3, B = A + 4 };\n" + "fn main() i32 = { return e.B: i32; };\n", + 7 }, + + { "bin_minus", + "type e = enum i32 { A = 10, B = A - 3 };\n" + "fn main() i32 = { return e.B: i32; };\n", + 7 }, + + { "bin_star", + "type e = enum i32 { A = 2, B = A * 4 };\n" + "fn main() i32 = { return e.B: i32; };\n", + 8 }, + + { "bin_slash", + "type e = enum i32 { A = 14, B = A / 2 };\n" + "fn main() i32 = { return e.B: i32; };\n", + 7 }, + + { "bin_percent", + "type e = enum i32 { A = 17, B = A % 10 };\n" + "fn main() i32 = { return e.B: i32; };\n", + 7 }, + + { "bin_amp", + "type e = enum i32 { A = 15, B = A & 7 };\n" + "fn main() i32 = { return e.B: i32; };\n", + 7 }, + + /* N_BIN TK_PIPE — overlaps with 700_e2e's `RW = R | W` row, kept + * here for symmetry with the rest of the binop set. */ + { "bin_pipe", + "type e = enum i32 { A = 4, B = A | 3 };\n" + "fn main() i32 = { return e.B: i32; };\n", + 7 }, + + { "bin_caret", + "type e = enum i32 { A = 5, B = A ^ 2 };\n" + "fn main() i32 = { return e.B: i32; };\n", + 7 }, + + { "bin_lshift", + "type e = enum i32 { A = 1, B = A << 3 };\n" + "fn main() i32 = { return e.B: i32; };\n", + 8 }, + + { "bin_rshift", + "type e = enum i32 { A = 28, B = A >> 2 };\n" + "fn main() i32 = { return e.B: i32; };\n", + 7 }, + + /* Chained sibling backref — pins the O(N²) walker contract. + * Each member's lhs ident lookup re-walks the prefix; if the + * `until` bound were >= instead of > (off-by-one) the lookup + * of D would walk past C and the fold would diverge or loop. */ + { "chained_backref", + "type e = enum i32 { A = 1, B = A + 1, C = B + 1, D = C + 4 };\n" + "fn main() i32 = { return e.D: i32; };\n", + 7 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/cef_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/cef_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return got; +} + +/* asm_byte_identical — diff w6c against w6c_ww for the same source. + * Pins the symmetric-emit contract: a future drift between cstage's + * eval_enum_value and wwstage's enumvalfold would show up here as a + * byte diff even if both stages produce semantically-correct + * constants. */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[64], cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/cef_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/cef_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/cef_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); + unlink(src); unlink(cs); + return -1; + } + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + int rc = 0; + if (!fc || !fw) { + rc = -1; + } else { + for (;;) { + int a = fgetc(fc); + int b = fgetc(fw); + if (a != b) { rc = -1; break; } + if (a == EOF) break; + } + } + if (fc) fclose(fc); + if (fw) fclose(fw); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + unlink(src); unlink(cs); unlink(ws); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + 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 cdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[1024]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated_on_existence; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "check_enum_fold: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "check_enum_fold[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + /* Asm byte-identity diff, only when wwstage is built. */ + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, + "check_enum_fold: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("check_enum_fold: %d/%d ok\n", total, total); + return 0; +}