From 894d966707d4c3c73f875cd8e7bf4afd4fce6191 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 21 May 2026 18:46:34 +0900 Subject: [PATCH] selfhost/cmd/wcc: fold N_DOT module/enum in exprtype (A.6.1.5a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port cstage cmd/wcc/check.c:740-832 fold cases to exprtype's new N_DOT arm. Two paths land: - Module-qualified ref (`pkg.x`): SK_USE leaf via scopelookupinmodule, type returned from the resolved sym's decl.lhs. Mirrors cstage L749-775. - Enum member fold to N_INTLIT: bare `EnumT.MEMBER` (cstage L780-803) and outer `pkg.EnumT.MEMBER` where the inner N_DOT folded via case 1 (cstage L805-832, including TPTR peel at L808). foldtointlit rewrites kind/uval/str/lhs/rhs; type_ stamps from the enum body. Stamp surface (struct field + pseudo-field .len/.cap/.ptr) lands in A.6.1.5b. #56 mod.fn() parser-rep stays parked. Two documented divergences from cstage: - No use_alias gate — wwstage uses sym.mod disambiguation per installdecl L195-207; SK_USE alone suffices. - Cstage errors on unknown enum member; wwstage falls through to nil under the lenient-check policy at scruttype L656. Check-side enum eval is literal-only (N_INTLIT + nil-auto- increment); cgen.ww's enumevalmember has the full const-folder for cgen-side. Every selfhost+lib enum uses explicit literals today (audited 2026-05-21). Tracked as task #7. Phase 1 A.6 step 5a of ~8 — bisect-clean split per Hare's AST access_identifier vs access_field axis (ref/hare/hare/ast/expr.ha:7-38). Verified 132/132 incl. 995_self_rebuild byte-identity. --- selfhost/cmd/w6c/main.combined.ww | 104 ++++++++++++++++++++++++++- selfhost/cmd/wcc/check.ww | 104 ++++++++++++++++++++++++++- selfhost/cmd/wwdump/main.combined.ww | 104 ++++++++++++++++++++++++++- 3 files changed, 306 insertions(+), 6 deletions(-) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 6e53ff4f..d8d77fce 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -8299,8 +8299,9 @@ fn indexresult(c: *checker, e: *node) *node = { // exprtype — best-effort type-AST inference for an expression // node. Handles literals, identifiers, calls, casts, binary/unary -// ops, indexing; returns nil for shapes we don't statically know -// (struct field access into non-primitive types, etc). +// ops, indexing, module-qualified refs + enum-member folds; returns +// nil for shapes we don't statically know (struct field access into +// non-primitive types, etc). // `hint`: optional declared-type AST passed by the caller (let // target, assign target). nil = "no hint, derive from self". Threaded // for use by A.6.1's STRUCTLIT/ARRLIT arms which can't self-type and @@ -8541,6 +8542,105 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { e.type_ = tinfofornode(c, s.decl.lhs): *void; return s.decl.lhs; }; + if (k == nkind.N_DOT) { + // A.6.1.5a — fold cases only. Mirrors cstage cmd/wcc/check.c + // :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. + let lhsn: *node = e.lhs; + if (lhsn != nil) { if (lhsn.kind == nkind.N_IDENT) { + let ms: *sym = scopelookupprefer(c.cur, c.curmod, lhsn.str); + if (ms != nil) { + // Fold case 1: module-qualified ref. Mirror cstage + // check.c:749-775. cstage returns ty_err on SK_USE + // with missing leaf (extern decl); wwstage falls + // through to outer case — cgen has its own module- + // qualified resolution and the lenient checker policy + // keeps the silent miss documented at scruttype L656. + if (ms.skind == skind.SK_USE) { + let fs: *sym = scopelookupinmodule(c.cur, lhsn.str, e.str); + if (fs != nil) { if (fs.decl != nil) { + let tn: *node = fs.decl.lhs; + if (tn != nil) { + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + }; }; + }; + // Fold case 2 inner: bare `EnumT.MEMBER` where EnumT + // is an SK_TYPE in the flat scope. Mirror cstage + // check.c:780-803. + if (ms.skind == skind.SK_TYPE) { if (ms.decl != nil) { + let body: *node = ms.decl.lhs; + let ub: *node = resolvealias(c, unwrapbang(body)); + if (ub != nil) { if (ub.kind == nkind.N_TENUM) { + let prev: u64 = (-1i64): u64; + let m: *node = ub.list; + for (m != nil) { + 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; + }; }; + prev = val; + if (streq(m.str, e.str)) { + foldtointlit(c, e, val: i64); + e.type_ = tinfofornode(c, body): *void; + return body; + }; + m = m.next; + }; + }; }; + }; }; + }; + }; }; + // Fold case 2 outer: base resolves to enum, e.g. + // `pkg.EnumT.MEMBER` where the inner N_DOT (pkg.EnumT) folded + // via case 1 above to the enum body. Mirror cstage + // check.c:805-832. Peel one TPTR for `(*EnumT).MEMBER` (rare + // but cstage handles it at L808). + let basetn: *node = exprtype(c, lhsn, nil); + if (basetn != nil) { + let bu: *node = resolvealias(c, unwrapbang(basetn)); + if (bu != nil) { if (bu.kind == nkind.N_TPTR) { + bu = resolvealias(c, unwrapbang(bu.lhs)); + }; }; + if (bu != nil) { if (bu.kind == nkind.N_TENUM) { + let prev: u64 = (-1i64): u64; + let m: *node = bu.list; + for (m != nil) { + 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; + }; }; + prev = val; + if (streq(m.str, e.str)) { + foldtointlit(c, e, val: i64); + e.type_ = tinfofornode(c, basetn): *void; + return basetn; + }; + m = m.next; + }; + }; }; + }; + return nil; + }; if (k == nkind.N_TRYPROP) { // success unwrap: the success-variant type of operand's // tagged union. diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index dac8091b..479613ee 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -1367,8 +1367,9 @@ fn indexresult(c: *checker, e: *node) *node = { // exprtype — best-effort type-AST inference for an expression // node. Handles literals, identifiers, calls, casts, binary/unary -// ops, indexing; returns nil for shapes we don't statically know -// (struct field access into non-primitive types, etc). +// ops, indexing, module-qualified refs + enum-member folds; returns +// nil for shapes we don't statically know (struct field access into +// non-primitive types, etc). // `hint`: optional declared-type AST passed by the caller (let // target, assign target). nil = "no hint, derive from self". Threaded // for use by A.6.1's STRUCTLIT/ARRLIT arms which can't self-type and @@ -1609,6 +1610,105 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { e.type_ = tinfofornode(c, s.decl.lhs): *void; return s.decl.lhs; }; + if (k == nkind.N_DOT) { + // A.6.1.5a — fold cases only. Mirrors cstage cmd/wcc/check.c + // :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. + let lhsn: *node = e.lhs; + if (lhsn != nil) { if (lhsn.kind == nkind.N_IDENT) { + let ms: *sym = scopelookupprefer(c.cur, c.curmod, lhsn.str); + if (ms != nil) { + // Fold case 1: module-qualified ref. Mirror cstage + // check.c:749-775. cstage returns ty_err on SK_USE + // with missing leaf (extern decl); wwstage falls + // through to outer case — cgen has its own module- + // qualified resolution and the lenient checker policy + // keeps the silent miss documented at scruttype L656. + if (ms.skind == skind.SK_USE) { + let fs: *sym = scopelookupinmodule(c.cur, lhsn.str, e.str); + if (fs != nil) { if (fs.decl != nil) { + let tn: *node = fs.decl.lhs; + if (tn != nil) { + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + }; }; + }; + // Fold case 2 inner: bare `EnumT.MEMBER` where EnumT + // is an SK_TYPE in the flat scope. Mirror cstage + // check.c:780-803. + if (ms.skind == skind.SK_TYPE) { if (ms.decl != nil) { + let body: *node = ms.decl.lhs; + let ub: *node = resolvealias(c, unwrapbang(body)); + if (ub != nil) { if (ub.kind == nkind.N_TENUM) { + let prev: u64 = (-1i64): u64; + let m: *node = ub.list; + for (m != nil) { + 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; + }; }; + prev = val; + if (streq(m.str, e.str)) { + foldtointlit(c, e, val: i64); + e.type_ = tinfofornode(c, body): *void; + return body; + }; + m = m.next; + }; + }; }; + }; }; + }; + }; }; + // Fold case 2 outer: base resolves to enum, e.g. + // `pkg.EnumT.MEMBER` where the inner N_DOT (pkg.EnumT) folded + // via case 1 above to the enum body. Mirror cstage + // check.c:805-832. Peel one TPTR for `(*EnumT).MEMBER` (rare + // but cstage handles it at L808). + let basetn: *node = exprtype(c, lhsn, nil); + if (basetn != nil) { + let bu: *node = resolvealias(c, unwrapbang(basetn)); + if (bu != nil) { if (bu.kind == nkind.N_TPTR) { + bu = resolvealias(c, unwrapbang(bu.lhs)); + }; }; + if (bu != nil) { if (bu.kind == nkind.N_TENUM) { + let prev: u64 = (-1i64): u64; + let m: *node = bu.list; + for (m != nil) { + 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; + }; }; + prev = val; + if (streq(m.str, e.str)) { + foldtointlit(c, e, val: i64); + e.type_ = tinfofornode(c, basetn): *void; + return basetn; + }; + m = m.next; + }; + }; }; + }; + return nil; + }; if (k == nkind.N_TRYPROP) { // success unwrap: the success-variant type of operand's // tagged union. diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 0ce9f343..bb9cb5db 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -8299,8 +8299,9 @@ fn indexresult(c: *checker, e: *node) *node = { // exprtype — best-effort type-AST inference for an expression // node. Handles literals, identifiers, calls, casts, binary/unary -// ops, indexing; returns nil for shapes we don't statically know -// (struct field access into non-primitive types, etc). +// ops, indexing, module-qualified refs + enum-member folds; returns +// nil for shapes we don't statically know (struct field access into +// non-primitive types, etc). // `hint`: optional declared-type AST passed by the caller (let // target, assign target). nil = "no hint, derive from self". Threaded // for use by A.6.1's STRUCTLIT/ARRLIT arms which can't self-type and @@ -8541,6 +8542,105 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { e.type_ = tinfofornode(c, s.decl.lhs): *void; return s.decl.lhs; }; + if (k == nkind.N_DOT) { + // A.6.1.5a — fold cases only. Mirrors cstage cmd/wcc/check.c + // :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. + let lhsn: *node = e.lhs; + if (lhsn != nil) { if (lhsn.kind == nkind.N_IDENT) { + let ms: *sym = scopelookupprefer(c.cur, c.curmod, lhsn.str); + if (ms != nil) { + // Fold case 1: module-qualified ref. Mirror cstage + // check.c:749-775. cstage returns ty_err on SK_USE + // with missing leaf (extern decl); wwstage falls + // through to outer case — cgen has its own module- + // qualified resolution and the lenient checker policy + // keeps the silent miss documented at scruttype L656. + if (ms.skind == skind.SK_USE) { + let fs: *sym = scopelookupinmodule(c.cur, lhsn.str, e.str); + if (fs != nil) { if (fs.decl != nil) { + let tn: *node = fs.decl.lhs; + if (tn != nil) { + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + }; }; + }; + // Fold case 2 inner: bare `EnumT.MEMBER` where EnumT + // is an SK_TYPE in the flat scope. Mirror cstage + // check.c:780-803. + if (ms.skind == skind.SK_TYPE) { if (ms.decl != nil) { + let body: *node = ms.decl.lhs; + let ub: *node = resolvealias(c, unwrapbang(body)); + if (ub != nil) { if (ub.kind == nkind.N_TENUM) { + let prev: u64 = (-1i64): u64; + let m: *node = ub.list; + for (m != nil) { + 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; + }; }; + prev = val; + if (streq(m.str, e.str)) { + foldtointlit(c, e, val: i64); + e.type_ = tinfofornode(c, body): *void; + return body; + }; + m = m.next; + }; + }; }; + }; }; + }; + }; }; + // Fold case 2 outer: base resolves to enum, e.g. + // `pkg.EnumT.MEMBER` where the inner N_DOT (pkg.EnumT) folded + // via case 1 above to the enum body. Mirror cstage + // check.c:805-832. Peel one TPTR for `(*EnumT).MEMBER` (rare + // but cstage handles it at L808). + let basetn: *node = exprtype(c, lhsn, nil); + if (basetn != nil) { + let bu: *node = resolvealias(c, unwrapbang(basetn)); + if (bu != nil) { if (bu.kind == nkind.N_TPTR) { + bu = resolvealias(c, unwrapbang(bu.lhs)); + }; }; + if (bu != nil) { if (bu.kind == nkind.N_TENUM) { + let prev: u64 = (-1i64): u64; + let m: *node = bu.list; + for (m != nil) { + 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; + }; }; + prev = val; + if (streq(m.str, e.str)) { + foldtointlit(c, e, val: i64); + e.type_ = tinfofornode(c, basetn): *void; + return basetn; + }; + m = m.next; + }; + }; }; + }; + return nil; + }; if (k == nkind.N_TRYPROP) { // success unwrap: the success-variant type of operand's // tagged union.