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:
7
Makefile
7
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 \
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
302
test/wcc/759_check_enum_fold.c
Normal file
302
test/wcc/759_check_enum_fold.c
Normal file
@@ -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 <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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user