selfhost/cmd/wcc: fold N_DOT module/enum in exprtype (A.6.1.5a)

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.
This commit is contained in:
2026-05-21 18:46:34 +09:00
parent d119b058a0
commit 894d966707
3 changed files with 306 additions and 6 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.