diff --git a/src/cmd/compile/internal/types2/decl.go b/src/cmd/compile/internal/types2/decl.go index 22fccc15199..70e79129c91 100644 --- a/src/cmd/compile/internal/types2/decl.go +++ b/src/cmd/compile/internal/types2/decl.go @@ -476,7 +476,7 @@ func (check *Checker) isImportedConstraint(typ Type) bool { if named == nil || named.obj.pkg == check.pkg || named.obj.pkg == nil { return false } - u, _ := named.under().(*Interface) + u, _ := named.Underlying().(*Interface) return u != nil && !u.IsMethodSet() } @@ -558,28 +558,33 @@ func (check *Checker) typeDecl(obj *TypeName, tdecl *syntax.TypeDecl, def *TypeN named := check.newNamed(obj, nil, nil) setDefType(def, named) + // The RHS of a named N can be nil if, for example, N is defined as a cycle of aliases with + // gotypesalias=0. Consider: + // + // type D N // N.resolve() will panic + // type N A + // type A = N // N.fromRHS is not set before N.resolve(), since A does not call setDefType + // + // There is likely a better way to detect such cases, but it may not be worth the effort. + // Instead, we briefly permit a nil N.fromRHS while type-checking D. + named.allowNilRHS = true + defer (func() { named.allowNilRHS = false })() + if tdecl.TParamList != nil { check.openScope(tdecl, "type parameters") defer check.closeScope() check.collectTypeParams(&named.tparams, tdecl.TParamList) } - // determine underlying type of named rhs = check.definedType(tdecl.Type, obj) assert(rhs != nil) named.fromRHS = rhs - // If the underlying type was not set while type-checking the right-hand - // side, it is invalid and an error should have been reported elsewhere. - if named.underlying == nil { - named.underlying = Typ[Invalid] - } - // spec: "In a type definition the given type cannot be a type parameter." // (See also go.dev/issue/45639.) if isTypeParam(rhs) { check.error(tdecl.Type, MisplacedTypeParam, "cannot use a type parameter as RHS in type declaration") - named.underlying = Typ[Invalid] + named.fromRHS = Typ[Invalid] } } @@ -721,7 +726,7 @@ func (check *Checker) collectMethods(obj *TypeName) { } func (check *Checker) checkFieldUniqueness(base *Named) { - if t, _ := base.under().(*Struct); t != nil { + if t, _ := base.Underlying().(*Struct); t != nil { var mset objset for i := 0; i < base.NumMethods(); i++ { m := base.Method(i) diff --git a/src/cmd/compile/internal/types2/named.go b/src/cmd/compile/internal/types2/named.go index d02b95e874a..4a28929c513 100644 --- a/src/cmd/compile/internal/types2/named.go +++ b/src/cmd/compile/internal/types2/named.go @@ -62,14 +62,14 @@ import ( // - We say that a Named type is "resolved" if its RHS information has been // loaded or fully type-checked. For Named types constructed from export // data, this may involve invoking a loader function to extract information -// from export data. For instantiated named types this involves reading -// information from their origin. +// from export data. For instantiated Named types this involves reading +// information from their origin and substituting type arguments into a +// "synthetic" RHS; this process is called "expanding" the RHS (see below). // - We say that a Named type is "expanded" if it is an instantiated type and -// type parameters in its underlying type and methods have been substituted -// with the type arguments from the instantiation. A type may be partially -// expanded if some but not all of these details have been substituted. -// Similarly, we refer to these individual details (underlying type or -// method) as being "expanded". +// type parameters in its RHS and methods have been substituted with the type +// arguments from the instantiation. A type may be partially expanded if some +// but not all of these details have been substituted. Similarly, we refer to +// these individual details (RHS or method) as being "expanded". // - When all information is known for a named type, we say it is "complete". // // Some invariants to keep in mind: each declared Named type has a single @@ -107,18 +107,17 @@ type Named struct { check *Checker // non-nil during type-checking; nil otherwise obj *TypeName // corresponding declared object for declared types; see above for instantiated types - // fromRHS holds the type (on RHS of declaration) this *Named type is derived - // from (for cycle reporting). Only used by validType, and therefore does not - // require synchronization. - fromRHS Type + // flags indicating temporary violations of the invariants for fromRHS and underlying + allowNilRHS bool // same as below, as well as briefly during checking of a type declaration + allowNilUnderlying bool // may be true from creation via [NewNamed] until [Named.SetUnderlying] - // information for instantiated types; nil otherwise - inst *instance + underlying Type // underlying type, or nil + inst *instance // information for instantiated types; nil otherwise - mu sync.Mutex // guards all fields below - state_ uint32 // the current state of this type; must only be accessed atomically - underlying Type // possibly a *Named during setup; never a *Named once set up completely - tparams *TypeParamList // type parameters, or nil + mu sync.Mutex // guards all fields below + state_ uint32 // the current state of this type; must only be accessed atomically + fromRHS Type // the declaration RHS this type is derived from + tparams *TypeParamList // type parameters, or nil // methods declared for this type (not the method set of this type) // Signatures are type-checked lazily. @@ -145,7 +144,7 @@ type namedState uint32 // Note: the order of states is relevant const ( - unresolved namedState = iota // tparams, underlying type and methods might be unavailable + unresolved namedState = iota // type parameters, RHS, underlying, and methods might be unavailable resolved // resolve has run; methods might be unexpanded (for instances) loaded // loader has run; constraints might be unexpanded (for generic types) complete // all data is known @@ -158,10 +157,18 @@ func NewNamed(obj *TypeName, underlying Type, methods []*Func) *Named { if asNamed(underlying) != nil { panic("underlying type must not be *Named") } - return (*Checker)(nil).newNamed(obj, underlying, methods) + n := (*Checker)(nil).newNamed(obj, underlying, methods) + if underlying == nil { + n.allowNilRHS = true + n.allowNilUnderlying = true + } else { + n.SetUnderlying(underlying) + } + return n + } -// resolve resolves the type parameters, methods, and underlying type of n. +// resolve resolves the type parameters, methods, and RHS of n. // // For the purposes of resolution, there are three categories of named types: // 1. Instantiated Types @@ -171,18 +178,17 @@ func NewNamed(obj *TypeName, underlying Type, methods []*Func) *Named { // Note that the above form a partition. // // Instantiated types: -// Type parameters, methods, and underlying type of n become accessible, -// though methods are lazily populated as needed. +// Type parameters, methods, and RHS of n become accessible, though methods +// are lazily populated as needed. // // Lazy loaded types: -// Type parameters, methods, and underlying type of n become accessible -// and are fully expanded. +// Type parameters, methods, and RHS of n become accessible and are fully +// expanded. // // All others: -// Effectively, nothing happens. The underlying type of n may still be -// a named type. +// Effectively, nothing happens. func (n *Named) resolve() *Named { - if n.state() > unresolved { // avoid locking below + if n.state() >= resolved { // avoid locking below return n } @@ -191,21 +197,19 @@ func (n *Named) resolve() *Named { n.mu.Lock() defer n.mu.Unlock() - if n.state() > unresolved { + if n.state() >= resolved { return n } if n.inst != nil { - assert(n.underlying == nil) // n is an unresolved instance - assert(n.loader == nil) // instances are created by instantiation, in which case n.loader is nil + assert(n.fromRHS == nil) // instantiated types are not declared types + assert(n.loader == nil) // cannot import an instantiation orig := n.inst.orig orig.resolve() - underlying := n.expandUnderlying() + n.fromRHS = n.expandRHS() n.tparams = orig.tparams - n.underlying = underlying - n.fromRHS = orig.fromRHS // for cycle detection if len(orig.methods) == 0 { n.setState(complete) // nothing further to do @@ -224,25 +228,25 @@ func (n *Named) resolve() *Named { // methods would need to support reentrant calls though. It would // also make the API more future-proof towards further extensions. if n.loader != nil { - assert(n.underlying == nil) - assert(n.TypeArgs().Len() == 0) // instances are created by instantiation, in which case n.loader is nil + assert(n.fromRHS == nil) // not loaded yet + assert(n.inst == nil) // cannot import an instantiation tparams, underlying, methods, delayed := n.loader(n) n.loader = nil n.tparams = bindTParams(tparams) - n.underlying = underlying n.fromRHS = underlying // for cycle detection n.methods = methods - // advance state to avoid deadlock calling delayed functions - n.setState(loaded) + n.setState(loaded) // avoid deadlock calling delayed functions for _, f := range delayed { f() } } + assert(n.fromRHS != nil || n.allowNilRHS) + assert(n.underlying == nil) // underlying comes after resolving n.setState(complete) return n } @@ -259,8 +263,8 @@ func (n *Named) setState(state namedState) { } // newNamed is like NewNamed but with a *Checker receiver. -func (check *Checker) newNamed(obj *TypeName, underlying Type, methods []*Func) *Named { - typ := &Named{check: check, obj: obj, fromRHS: underlying, underlying: underlying, methods: methods} +func (check *Checker) newNamed(obj *TypeName, fromRHS Type, methods []*Func) *Named { + typ := &Named{check: check, obj: obj, fromRHS: fromRHS, methods: methods} if obj.typ == nil { obj.typ = typ } @@ -300,25 +304,13 @@ func (check *Checker) newNamedInstance(pos syntax.Pos, orig *Named, targs []Type return typ } -func (t *Named) cleanup() { - assert(t.inst == nil || t.inst.orig.inst == nil) - // Ensure that every defined type created in the course of type-checking has - // either non-*Named underlying type, or is unexpanded. - // - // This guarantees that we don't leak any types whose underlying type is - // *Named, because any unexpanded instances will lazily compute their - // underlying type by substituting in the underlying type of their origin. - // The origin must have either been imported or type-checked and expanded - // here, and in either case its underlying type will be fully expanded. - switch t.underlying.(type) { - case nil: - if t.TypeArgs().Len() == 0 { - panic("nil underlying") - } - case *Named, *Alias: - t.under() // t.under may add entries to check.cleaners +func (n *Named) cleanup() { + // Instances can have a nil underlying at the end of type checking — they + // will lazily expand it as needed. All other types must have one. + if n.inst == nil { + n.resolve().under() } - t.check = nil + n.check = nil } // Obj returns the type name for the declaration defining the named type t. For @@ -477,10 +469,12 @@ func (t *Named) SetUnderlying(underlying Type) { if asNamed(underlying) != nil { panic("underlying type must not be *Named") } - t.resolve().underlying = underlying - if t.fromRHS == nil { - t.fromRHS = underlying // for cycle detection - } + // Invariant: Presence of underlying type implies it was resolved. + t.fromRHS = underlying + t.allowNilRHS = false + t.resolve() + t.underlying = underlying + t.allowNilUnderlying = false } // AddMethod adds method m unless it is already in the method list. @@ -523,9 +517,20 @@ func (t *Named) methodIndex(name string, foldCase bool) int { // Alias types. // // [underlying type]: https://go.dev/ref/spec#Underlying_types. -func (t *Named) Underlying() Type { - // TODO(gri) Investigate if Unalias can be moved to where underlying is set. - return Unalias(t.resolve().underlying) +func (n *Named) Underlying() Type { + n.resolve() + + // The gccimporter depends on writing a nil underlying via NewNamed and + // immediately reading it back. Rather than putting that in under() and + // complicating things there, we just check for that special case here. + if n.fromRHS == nil { + assert(n.allowNilRHS) + if n.allowNilUnderlying { + return nil + } + } + + return n.under() } func (t *Named) String() string { return TypeString(t, nil) } @@ -536,89 +541,55 @@ func (t *Named) String() string { return TypeString(t, nil) } // TODO(rfindley): reorganize the loading and expansion methods under this // heading. -// under returns the expanded underlying type of n0; possibly by following -// forward chains of named types. If an underlying type is found, resolve -// the chain by setting the underlying type for each defined type in the -// chain before returning it. If no underlying type is found or a cycle -// is detected, the result is Typ[Invalid]. If a cycle is detected and -// n0.check != nil, the cycle is reported. +// under returns the (possibly expanded) underlying type of n. // -// This is necessary because the underlying type of named may be itself a -// named type that is incomplete: +// It does so by following RHS type chains. If a type literal is found, each +// named type in the chain has its underlying set to that type. Aliases are +// skipped because their underlying type is not memoized. // -// type ( -// A B -// B *C -// C A -// ) -// -// The type of C is the (named) type of A which is incomplete, -// and which has as its underlying type the named type B. -func (n0 *Named) under() Type { - u := n0.Underlying() +// This function also checks for instantiated layout cycles, which are +// reachable only in the case where resolve() expanded an instantiated +// type which became self-referencing without indirection. If such a +// cycle is found, the result is Typ[Invalid]; if n.check != nil, the +// cycle is also reported. +func (n *Named) under() Type { + assert(n.state() >= resolved) - // If the underlying type of a defined type is not a defined - // (incl. instance) type, then that is the desired underlying - // type. - var n1 *Named - switch u1 := u.(type) { - case nil: - // After expansion via Underlying(), we should never encounter a nil - // underlying. - panic("nil underlying") - default: - // common case - return u - case *Named: - // handled below - n1 = u1 + if n.underlying != nil { + return n.underlying } - if n0.check == nil { - panic("Named.check == nil but type is incomplete") - } + var rhs Type = n + var u Type - // Invariant: after this point n0 as well as any named types in its - // underlying chain should be set up when this function exits. - check := n0.check - n := n0 + seen := make(map[*Named]int) + var path []Object - seen := make(map[*Named]int) // types that need their underlying type resolved - var path []Object // objects encountered, for cycle reporting - -loop: - for { - seen[n] = len(seen) - path = append(path, n.obj) - n = n1 - if i, ok := seen[n]; ok { - // cycle - check.cycleError(path[i:], firstInSrc(path[i:])) - u = Typ[Invalid] - break - } - u = n.Underlying() - switch u1 := u.(type) { + for u == nil { + switch t := rhs.(type) { case nil: u = Typ[Invalid] - break loop - default: - break loop + case *Alias: + rhs = unalias(t) case *Named: - // Continue collecting *Named types in the chain. - n1 = u1 + if i, ok := seen[t]; ok { + n.check.cycleError(path[i:], firstInSrc(path[i:])) + u = Typ[Invalid] + break + } + seen[t] = len(seen) + path = append(path, t.obj) + t.resolve() + assert(t.fromRHS != nil || t.allowNilRHS) + rhs = t.fromRHS + default: + u = rhs // any type literal works } } - for n := range seen { - // We should never have to update the underlying type of an imported type; - // those underlying types should have been resolved during the import. - // Also, doing so would lead to a race condition (was go.dev/issue/31749). - // Do this check always, not just in debug mode (it's cheap). - if n.obj.pkg != check.pkg { - panic("imported type with unresolved underlying type") - } - n.underlying = u + // go back up the chain + for t := range seen { + t.underlying = u } return u @@ -646,78 +617,108 @@ func (check *Checker) context() *Context { return check.ctxt } -// expandUnderlying substitutes type arguments in the underlying type n.orig, -// returning the result. Returns Typ[Invalid] if there was an error. -func (n *Named) expandUnderlying() Type { +// expandRHS crafts a synthetic RHS for an instantiated type using the RHS of +// its origin type (which must be a generic type). +// +// Suppose that we had: +// +// type T[P any] struct { +// f P +// } +// +// type U T[int] +// +// When we go to U, we observe T[int]. Since T[int] is an instantiation, it has no +// declaration. Here, we craft a synthetic RHS for T[int] as if it were declared, +// somewhat similar to: +// +// type T[int] struct { +// f int +// } +// +// And note that the synthetic RHS here is the same as the underlying for U. Now, +// consider: +// +// type T[_ any] U +// type U int +// type V T[U] +// +// The synthetic RHS for T[U] becomes: +// +// type T[U] U +// +// Whereas the underlying of V is int, not U. +func (n *Named) expandRHS() (rhs Type) { check := n.check if check != nil && check.conf.Trace { - check.trace(n.obj.pos, "-- Named.expandUnderlying %s", n) + check.trace(n.obj.pos, "-- Named.expandRHS %s", n) check.indent++ defer func() { check.indent-- - check.trace(n.obj.pos, "=> %s (tparams = %s, under = %s)", n, n.tparams.list(), n.underlying) + check.trace(n.obj.pos, "=> %s (rhs = %s)", n, rhs) }() } - assert(n.inst.orig.underlying != nil) + assert(n.state() == unresolved) + if n.inst.ctxt == nil { n.inst.ctxt = NewContext() } + ctxt := n.inst.ctxt orig := n.inst.orig + + assert(orig.state() >= resolved) + assert(orig.fromRHS != nil) + targs := n.inst.targs + tpars := orig.tparams - if asNamed(orig.underlying) != nil { - // We should only get a Named underlying type here during type checking - // (for example, in recursive type declarations). - assert(check != nil) - } - - if orig.tparams.Len() != targs.Len() { - // Mismatching arg and tparam length may be checked elsewhere. + if targs.Len() != tpars.Len() { return Typ[Invalid] } - // Ensure that an instance is recorded before substituting, so that we - // resolve n for any recursive references. - h := n.inst.ctxt.instanceHash(orig, targs.list()) - n2 := n.inst.ctxt.update(h, orig, n.TypeArgs().list(), n) - assert(n == n2) + h := ctxt.instanceHash(orig, targs.list()) + u := ctxt.update(h, orig, targs.list(), n) // block fixed point infinite instantiation + assert(n == u) - smap := makeSubstMap(orig.tparams.list(), targs.list()) - var ctxt *Context + m := makeSubstMap(tpars.list(), targs.list()) if check != nil { ctxt = check.context() } - underlying := n.check.subst(n.obj.pos, orig.underlying, smap, n, ctxt) - // If the underlying type of n is an interface, we need to set the receiver of - // its methods accurately -- we set the receiver of interface methods on - // the RHS of a type declaration to the defined type. - if iface, _ := underlying.(*Interface); iface != nil { + + rhs = check.subst(n.obj.pos, orig.fromRHS, m, n, ctxt) + + // TODO(markfreeman): Can we handle this in substitution? + // If the RHS is an interface, we must set the receiver of interface methods + // to the named type. + if iface, _ := rhs.(*Interface); iface != nil { if methods, copied := replaceRecvType(iface.methods, orig, n); copied { - // If the underlying type doesn't actually use type parameters, it's - // possible that it wasn't substituted. In this case we need to create - // a new *Interface before modifying receivers. - if iface == orig.underlying { - old := iface - iface = check.newInterface() - iface.embeddeds = old.embeddeds - assert(old.complete) // otherwise we are copying incomplete data - iface.complete = old.complete - iface.implicit = old.implicit // should be false but be conservative - underlying = iface + // If the RHS doesn't use type parameters, it may not have been + // substituted; we need to craft a new interface first. + if iface == orig.fromRHS { + assert(iface.complete) // otherwise we are copying incomplete data + + crafted := check.newInterface() + crafted.complete = true + crafted.implicit = false + crafted.embeddeds = iface.embeddeds + + iface = crafted } iface.methods = methods iface.tset = nil // recompute type set with new methods - // If check != nil, check.newInterface will have saved the interface for later completion. - if check == nil { // golang/go#61561: all newly created interfaces must be fully evaluated + // go.dev/issue/61561: We have to complete the interface even without a checker. + if check == nil { iface.typeSet() } + + return iface } } - return underlying + return rhs } // safeUnderlying returns the underlying type of typ without expanding diff --git a/src/cmd/compile/internal/types2/object.go b/src/cmd/compile/internal/types2/object.go index 7096c556971..6331259e176 100644 --- a/src/cmd/compile/internal/types2/object.go +++ b/src/cmd/compile/internal/types2/object.go @@ -295,7 +295,8 @@ func NewTypeName(pos syntax.Pos, pkg *Package, name string, typ Type) *TypeName // lazily calls resolve to finish constructing the Named object. func NewTypeNameLazy(pos syntax.Pos, pkg *Package, name string, load func(*Named) ([]*TypeParam, Type, []*Func, []func())) *TypeName { obj := NewTypeName(pos, pkg, name, nil) - NewNamed(obj, nil, nil).loader = load + n := (*Checker)(nil).newNamed(obj, nil, nil) + n.loader = load return obj } diff --git a/src/cmd/compile/internal/types2/signature.go b/src/cmd/compile/internal/types2/signature.go index 1e004eef4ae..ea60254fa68 100644 --- a/src/cmd/compile/internal/types2/signature.go +++ b/src/cmd/compile/internal/types2/signature.go @@ -439,7 +439,7 @@ func (check *Checker) validRecv(pos poser, recv *Var) { break } var cause string - switch u := T.under().(type) { + switch u := T.Underlying().(type) { case *Basic: // unsafe.Pointer is treated like a regular pointer if u.kind == UnsafePointer { diff --git a/src/cmd/compile/internal/types2/sizeof_test.go b/src/cmd/compile/internal/types2/sizeof_test.go index d435c049c5b..13b96209114 100644 --- a/src/cmd/compile/internal/types2/sizeof_test.go +++ b/src/cmd/compile/internal/types2/sizeof_test.go @@ -31,7 +31,7 @@ func TestSizeof(t *testing.T) { {Interface{}, 40, 80}, {Map{}, 16, 32}, {Chan{}, 12, 24}, - {Named{}, 60, 112}, + {Named{}, 64, 120}, {TypeParam{}, 28, 48}, {term{}, 12, 24}, diff --git a/src/cmd/compile/internal/types2/typexpr.go b/src/cmd/compile/internal/types2/typexpr.go index 8accc46751f..b1533de5048 100644 --- a/src/cmd/compile/internal/types2/typexpr.go +++ b/src/cmd/compile/internal/types2/typexpr.go @@ -425,7 +425,7 @@ func setDefType(def *TypeName, typ Type) { case *Basic: assert(t == Typ[Invalid]) case *Named: - t.underlying = typ + t.fromRHS = typ default: panic(fmt.Sprintf("unexpected type %T", t)) } diff --git a/src/cmd/compile/internal/types2/under.go b/src/cmd/compile/internal/types2/under.go index 078ba9ab172..8e9871eacd0 100644 --- a/src/cmd/compile/internal/types2/under.go +++ b/src/cmd/compile/internal/types2/under.go @@ -11,9 +11,7 @@ import "iter" // under must only be called when a type is known // to be fully set up. func under(t Type) Type { - if t := asNamed(t); t != nil { - return t.under() - } + // TODO(markfreeman): Remove this function, it just delegates. return t.Underlying() } diff --git a/src/cmd/compile/internal/types2/unify.go b/src/cmd/compile/internal/types2/unify.go index 9cd3af86071..8e112248c63 100644 --- a/src/cmd/compile/internal/types2/unify.go +++ b/src/cmd/compile/internal/types2/unify.go @@ -339,7 +339,7 @@ func (u *unifier) nify(x, y Type, mode unifyMode, p *ifacePair) (result bool) { if traceInference { u.tracef("%s ≡ under %s", x, ny) } - y = ny.under() + y = ny.Underlying() // Per the spec, a defined type cannot have an underlying type // that is a type parameter. assert(!isTypeParam(y)) diff --git a/src/cmd/compile/internal/types2/universe.go b/src/cmd/compile/internal/types2/universe.go index c66caebd10c..332cd174f97 100644 --- a/src/cmd/compile/internal/types2/universe.go +++ b/src/cmd/compile/internal/types2/universe.go @@ -116,7 +116,7 @@ func defPredeclaredTypes() { { obj := NewTypeName(nopos, nil, "error", nil) obj.setColor(black) - typ := NewNamed(obj, nil, nil) + typ := (*Checker)(nil).newNamed(obj, nil, nil) // error.Error() string recv := newVar(RecvVar, nopos, nil, "", typ) @@ -128,7 +128,8 @@ func defPredeclaredTypes() { ityp := &Interface{methods: []*Func{err}, complete: true} computeInterfaceTypeSet(nil, nopos, ityp) // prevent races due to lazy computation of tset - typ.SetUnderlying(ityp) + typ.fromRHS = ityp + typ.Underlying() def(obj) } @@ -136,12 +137,13 @@ func defPredeclaredTypes() { { obj := NewTypeName(nopos, nil, "comparable", nil) obj.setColor(black) - typ := NewNamed(obj, nil, nil) + typ := (*Checker)(nil).newNamed(obj, nil, nil) // interface{} // marked as comparable ityp := &Interface{complete: true, tset: &_TypeSet{nil, allTermlist, true}} - typ.SetUnderlying(ityp) + typ.fromRHS = ityp + typ.Underlying() def(obj) } } diff --git a/src/cmd/compile/internal/types2/validtype.go b/src/cmd/compile/internal/types2/validtype.go index 32e389a6562..528f5121e39 100644 --- a/src/cmd/compile/internal/types2/validtype.go +++ b/src/cmd/compile/internal/types2/validtype.go @@ -91,13 +91,6 @@ func (check *Checker) validType0(pos syntax.Pos, typ Type, nest, path []*Named) // break // } - // Don't report a 2nd error if we already know the type is invalid - // (e.g., if a cycle was detected earlier, via under). - // Note: ensure that t.orig is fully resolved by calling Underlying(). - if !isValid(t.Underlying()) { - return false - } - // If the current type t is also found in nest, (the memory of) t is // embedded in itself, indicating an invalid recursive type. for _, e := range nest { @@ -125,8 +118,9 @@ func (check *Checker) validType0(pos syntax.Pos, typ Type, nest, path []*Named) // are not yet available to other goroutines). assert(t.obj.pkg == check.pkg) assert(t.Origin().obj.pkg == check.pkg) - t.underlying = Typ[Invalid] - t.Origin().underlying = Typ[Invalid] + + // let t become invalid when it resolves + t.Origin().fromRHS = Typ[Invalid] // Find the starting point of the cycle and report it. // Because each type in nest must also appear in path (see invariant below), diff --git a/src/go/types/decl.go b/src/go/types/decl.go index 13d9d9ce574..6e61dd9a131 100644 --- a/src/go/types/decl.go +++ b/src/go/types/decl.go @@ -551,7 +551,7 @@ func (check *Checker) isImportedConstraint(typ Type) bool { if named == nil || named.obj.pkg == check.pkg || named.obj.pkg == nil { return false } - u, _ := named.under().(*Interface) + u, _ := named.Underlying().(*Interface) return u != nil && !u.IsMethodSet() } @@ -640,28 +640,33 @@ func (check *Checker) typeDecl(obj *TypeName, tdecl *ast.TypeSpec, def *TypeName named := check.newNamed(obj, nil, nil) setDefType(def, named) + // The RHS of a named N can be nil if, for example, N is defined as a cycle of aliases with + // gotypesalias=0. Consider: + // + // type D N // N.resolve() will panic + // type N A + // type A = N // N.fromRHS is not set before N.resolve(), since A does not call setDefType + // + // There is likely a better way to detect such cases, but it may not be worth the effort. + // Instead, we briefly permit a nil N.fromRHS while type-checking D. + named.allowNilRHS = true + defer (func() { named.allowNilRHS = false })() + if tdecl.TypeParams != nil { check.openScope(tdecl, "type parameters") defer check.closeScope() check.collectTypeParams(&named.tparams, tdecl.TypeParams) } - // determine underlying type of named rhs = check.definedType(tdecl.Type, obj) assert(rhs != nil) named.fromRHS = rhs - // If the underlying type was not set while type-checking the right-hand - // side, it is invalid and an error should have been reported elsewhere. - if named.underlying == nil { - named.underlying = Typ[Invalid] - } - // spec: "In a type definition the given type cannot be a type parameter." // (See also go.dev/issue/45639.) if isTypeParam(rhs) { check.error(tdecl.Type, MisplacedTypeParam, "cannot use a type parameter as RHS in type declaration") - named.underlying = Typ[Invalid] + named.fromRHS = Typ[Invalid] } } @@ -814,7 +819,7 @@ func (check *Checker) collectMethods(obj *TypeName) { } func (check *Checker) checkFieldUniqueness(base *Named) { - if t, _ := base.under().(*Struct); t != nil { + if t, _ := base.Underlying().(*Struct); t != nil { var mset objset for i := 0; i < base.NumMethods(); i++ { m := base.Method(i) diff --git a/src/go/types/named.go b/src/go/types/named.go index b2f99ffccb9..d7e4c247963 100644 --- a/src/go/types/named.go +++ b/src/go/types/named.go @@ -65,14 +65,14 @@ import ( // - We say that a Named type is "resolved" if its RHS information has been // loaded or fully type-checked. For Named types constructed from export // data, this may involve invoking a loader function to extract information -// from export data. For instantiated named types this involves reading -// information from their origin. +// from export data. For instantiated Named types this involves reading +// information from their origin and substituting type arguments into a +// "synthetic" RHS; this process is called "expanding" the RHS (see below). // - We say that a Named type is "expanded" if it is an instantiated type and -// type parameters in its underlying type and methods have been substituted -// with the type arguments from the instantiation. A type may be partially -// expanded if some but not all of these details have been substituted. -// Similarly, we refer to these individual details (underlying type or -// method) as being "expanded". +// type parameters in its RHS and methods have been substituted with the type +// arguments from the instantiation. A type may be partially expanded if some +// but not all of these details have been substituted. Similarly, we refer to +// these individual details (RHS or method) as being "expanded". // - When all information is known for a named type, we say it is "complete". // // Some invariants to keep in mind: each declared Named type has a single @@ -110,18 +110,17 @@ type Named struct { check *Checker // non-nil during type-checking; nil otherwise obj *TypeName // corresponding declared object for declared types; see above for instantiated types - // fromRHS holds the type (on RHS of declaration) this *Named type is derived - // from (for cycle reporting). Only used by validType, and therefore does not - // require synchronization. - fromRHS Type + // flags indicating temporary violations of the invariants for fromRHS and underlying + allowNilRHS bool // same as below, as well as briefly during checking of a type declaration + allowNilUnderlying bool // may be true from creation via [NewNamed] until [Named.SetUnderlying] - // information for instantiated types; nil otherwise - inst *instance + underlying Type // underlying type, or nil + inst *instance // information for instantiated types; nil otherwise - mu sync.Mutex // guards all fields below - state_ uint32 // the current state of this type; must only be accessed atomically - underlying Type // possibly a *Named during setup; never a *Named once set up completely - tparams *TypeParamList // type parameters, or nil + mu sync.Mutex // guards all fields below + state_ uint32 // the current state of this type; must only be accessed atomically + fromRHS Type // the declaration RHS this type is derived from + tparams *TypeParamList // type parameters, or nil // methods declared for this type (not the method set of this type) // Signatures are type-checked lazily. @@ -148,7 +147,7 @@ type namedState uint32 // Note: the order of states is relevant const ( - unresolved namedState = iota // tparams, underlying type and methods might be unavailable + unresolved namedState = iota // type parameters, RHS, underlying, and methods might be unavailable resolved // resolve has run; methods might be unexpanded (for instances) loaded // loader has run; constraints might be unexpanded (for generic types) complete // all data is known @@ -161,10 +160,18 @@ func NewNamed(obj *TypeName, underlying Type, methods []*Func) *Named { if asNamed(underlying) != nil { panic("underlying type must not be *Named") } - return (*Checker)(nil).newNamed(obj, underlying, methods) + n := (*Checker)(nil).newNamed(obj, underlying, methods) + if underlying == nil { + n.allowNilRHS = true + n.allowNilUnderlying = true + } else { + n.SetUnderlying(underlying) + } + return n + } -// resolve resolves the type parameters, methods, and underlying type of n. +// resolve resolves the type parameters, methods, and RHS of n. // // For the purposes of resolution, there are three categories of named types: // 1. Instantiated Types @@ -174,18 +181,17 @@ func NewNamed(obj *TypeName, underlying Type, methods []*Func) *Named { // Note that the above form a partition. // // Instantiated types: -// Type parameters, methods, and underlying type of n become accessible, -// though methods are lazily populated as needed. +// Type parameters, methods, and RHS of n become accessible, though methods +// are lazily populated as needed. // // Lazy loaded types: -// Type parameters, methods, and underlying type of n become accessible -// and are fully expanded. +// Type parameters, methods, and RHS of n become accessible and are fully +// expanded. // // All others: -// Effectively, nothing happens. The underlying type of n may still be -// a named type. +// Effectively, nothing happens. func (n *Named) resolve() *Named { - if n.state() > unresolved { // avoid locking below + if n.state() >= resolved { // avoid locking below return n } @@ -194,21 +200,19 @@ func (n *Named) resolve() *Named { n.mu.Lock() defer n.mu.Unlock() - if n.state() > unresolved { + if n.state() >= resolved { return n } if n.inst != nil { - assert(n.underlying == nil) // n is an unresolved instance - assert(n.loader == nil) // instances are created by instantiation, in which case n.loader is nil + assert(n.fromRHS == nil) // instantiated types are not declared types + assert(n.loader == nil) // cannot import an instantiation orig := n.inst.orig orig.resolve() - underlying := n.expandUnderlying() + n.fromRHS = n.expandRHS() n.tparams = orig.tparams - n.underlying = underlying - n.fromRHS = orig.fromRHS // for cycle detection if len(orig.methods) == 0 { n.setState(complete) // nothing further to do @@ -227,25 +231,25 @@ func (n *Named) resolve() *Named { // methods would need to support reentrant calls though. It would // also make the API more future-proof towards further extensions. if n.loader != nil { - assert(n.underlying == nil) - assert(n.TypeArgs().Len() == 0) // instances are created by instantiation, in which case n.loader is nil + assert(n.fromRHS == nil) // not loaded yet + assert(n.inst == nil) // cannot import an instantiation tparams, underlying, methods, delayed := n.loader(n) n.loader = nil n.tparams = bindTParams(tparams) - n.underlying = underlying n.fromRHS = underlying // for cycle detection n.methods = methods - // advance state to avoid deadlock calling delayed functions - n.setState(loaded) + n.setState(loaded) // avoid deadlock calling delayed functions for _, f := range delayed { f() } } + assert(n.fromRHS != nil || n.allowNilRHS) + assert(n.underlying == nil) // underlying comes after resolving n.setState(complete) return n } @@ -262,8 +266,8 @@ func (n *Named) setState(state namedState) { } // newNamed is like NewNamed but with a *Checker receiver. -func (check *Checker) newNamed(obj *TypeName, underlying Type, methods []*Func) *Named { - typ := &Named{check: check, obj: obj, fromRHS: underlying, underlying: underlying, methods: methods} +func (check *Checker) newNamed(obj *TypeName, fromRHS Type, methods []*Func) *Named { + typ := &Named{check: check, obj: obj, fromRHS: fromRHS, methods: methods} if obj.typ == nil { obj.typ = typ } @@ -303,25 +307,13 @@ func (check *Checker) newNamedInstance(pos token.Pos, orig *Named, targs []Type, return typ } -func (t *Named) cleanup() { - assert(t.inst == nil || t.inst.orig.inst == nil) - // Ensure that every defined type created in the course of type-checking has - // either non-*Named underlying type, or is unexpanded. - // - // This guarantees that we don't leak any types whose underlying type is - // *Named, because any unexpanded instances will lazily compute their - // underlying type by substituting in the underlying type of their origin. - // The origin must have either been imported or type-checked and expanded - // here, and in either case its underlying type will be fully expanded. - switch t.underlying.(type) { - case nil: - if t.TypeArgs().Len() == 0 { - panic("nil underlying") - } - case *Named, *Alias: - t.under() // t.under may add entries to check.cleaners +func (n *Named) cleanup() { + // Instances can have a nil underlying at the end of type checking — they + // will lazily expand it as needed. All other types must have one. + if n.inst == nil { + n.resolve().under() } - t.check = nil + n.check = nil } // Obj returns the type name for the declaration defining the named type t. For @@ -480,10 +472,12 @@ func (t *Named) SetUnderlying(underlying Type) { if asNamed(underlying) != nil { panic("underlying type must not be *Named") } - t.resolve().underlying = underlying - if t.fromRHS == nil { - t.fromRHS = underlying // for cycle detection - } + // Invariant: Presence of underlying type implies it was resolved. + t.fromRHS = underlying + t.allowNilRHS = false + t.resolve() + t.underlying = underlying + t.allowNilUnderlying = false } // AddMethod adds method m unless it is already in the method list. @@ -526,9 +520,20 @@ func (t *Named) methodIndex(name string, foldCase bool) int { // Alias types. // // [underlying type]: https://go.dev/ref/spec#Underlying_types. -func (t *Named) Underlying() Type { - // TODO(gri) Investigate if Unalias can be moved to where underlying is set. - return Unalias(t.resolve().underlying) +func (n *Named) Underlying() Type { + n.resolve() + + // The gccimporter depends on writing a nil underlying via NewNamed and + // immediately reading it back. Rather than putting that in under() and + // complicating things there, we just check for that special case here. + if n.fromRHS == nil { + assert(n.allowNilRHS) + if n.allowNilUnderlying { + return nil + } + } + + return n.under() } func (t *Named) String() string { return TypeString(t, nil) } @@ -539,89 +544,55 @@ func (t *Named) String() string { return TypeString(t, nil) } // TODO(rfindley): reorganize the loading and expansion methods under this // heading. -// under returns the expanded underlying type of n0; possibly by following -// forward chains of named types. If an underlying type is found, resolve -// the chain by setting the underlying type for each defined type in the -// chain before returning it. If no underlying type is found or a cycle -// is detected, the result is Typ[Invalid]. If a cycle is detected and -// n0.check != nil, the cycle is reported. +// under returns the (possibly expanded) underlying type of n. // -// This is necessary because the underlying type of named may be itself a -// named type that is incomplete: +// It does so by following RHS type chains. If a type literal is found, each +// named type in the chain has its underlying set to that type. Aliases are +// skipped because their underlying type is not memoized. // -// type ( -// A B -// B *C -// C A -// ) -// -// The type of C is the (named) type of A which is incomplete, -// and which has as its underlying type the named type B. -func (n0 *Named) under() Type { - u := n0.Underlying() +// This function also checks for instantiated layout cycles, which are +// reachable only in the case where resolve() expanded an instantiated +// type which became self-referencing without indirection. If such a +// cycle is found, the result is Typ[Invalid]; if n.check != nil, the +// cycle is also reported. +func (n *Named) under() Type { + assert(n.state() >= resolved) - // If the underlying type of a defined type is not a defined - // (incl. instance) type, then that is the desired underlying - // type. - var n1 *Named - switch u1 := u.(type) { - case nil: - // After expansion via Underlying(), we should never encounter a nil - // underlying. - panic("nil underlying") - default: - // common case - return u - case *Named: - // handled below - n1 = u1 + if n.underlying != nil { + return n.underlying } - if n0.check == nil { - panic("Named.check == nil but type is incomplete") - } + var rhs Type = n + var u Type - // Invariant: after this point n0 as well as any named types in its - // underlying chain should be set up when this function exits. - check := n0.check - n := n0 + seen := make(map[*Named]int) + var path []Object - seen := make(map[*Named]int) // types that need their underlying type resolved - var path []Object // objects encountered, for cycle reporting - -loop: - for { - seen[n] = len(seen) - path = append(path, n.obj) - n = n1 - if i, ok := seen[n]; ok { - // cycle - check.cycleError(path[i:], firstInSrc(path[i:])) - u = Typ[Invalid] - break - } - u = n.Underlying() - switch u1 := u.(type) { + for u == nil { + switch t := rhs.(type) { case nil: u = Typ[Invalid] - break loop - default: - break loop + case *Alias: + rhs = unalias(t) case *Named: - // Continue collecting *Named types in the chain. - n1 = u1 + if i, ok := seen[t]; ok { + n.check.cycleError(path[i:], firstInSrc(path[i:])) + u = Typ[Invalid] + break + } + seen[t] = len(seen) + path = append(path, t.obj) + t.resolve() + assert(t.fromRHS != nil || t.allowNilRHS) + rhs = t.fromRHS + default: + u = rhs // any type literal works } } - for n := range seen { - // We should never have to update the underlying type of an imported type; - // those underlying types should have been resolved during the import. - // Also, doing so would lead to a race condition (was go.dev/issue/31749). - // Do this check always, not just in debug mode (it's cheap). - if n.obj.pkg != check.pkg { - panic("imported type with unresolved underlying type") - } - n.underlying = u + // go back up the chain + for t := range seen { + t.underlying = u } return u @@ -649,78 +620,108 @@ func (check *Checker) context() *Context { return check.ctxt } -// expandUnderlying substitutes type arguments in the underlying type n.orig, -// returning the result. Returns Typ[Invalid] if there was an error. -func (n *Named) expandUnderlying() Type { +// expandRHS crafts a synthetic RHS for an instantiated type using the RHS of +// its origin type (which must be a generic type). +// +// Suppose that we had: +// +// type T[P any] struct { +// f P +// } +// +// type U T[int] +// +// When we go to U, we observe T[int]. Since T[int] is an instantiation, it has no +// declaration. Here, we craft a synthetic RHS for T[int] as if it were declared, +// somewhat similar to: +// +// type T[int] struct { +// f int +// } +// +// And note that the synthetic RHS here is the same as the underlying for U. Now, +// consider: +// +// type T[_ any] U +// type U int +// type V T[U] +// +// The synthetic RHS for T[U] becomes: +// +// type T[U] U +// +// Whereas the underlying of V is int, not U. +func (n *Named) expandRHS() (rhs Type) { check := n.check if check != nil && check.conf._Trace { - check.trace(n.obj.pos, "-- Named.expandUnderlying %s", n) + check.trace(n.obj.pos, "-- Named.expandRHS %s", n) check.indent++ defer func() { check.indent-- - check.trace(n.obj.pos, "=> %s (tparams = %s, under = %s)", n, n.tparams.list(), n.underlying) + check.trace(n.obj.pos, "=> %s (rhs = %s)", n, rhs) }() } - assert(n.inst.orig.underlying != nil) + assert(n.state() == unresolved) + if n.inst.ctxt == nil { n.inst.ctxt = NewContext() } + ctxt := n.inst.ctxt orig := n.inst.orig + + assert(orig.state() >= resolved) + assert(orig.fromRHS != nil) + targs := n.inst.targs + tpars := orig.tparams - if asNamed(orig.underlying) != nil { - // We should only get a Named underlying type here during type checking - // (for example, in recursive type declarations). - assert(check != nil) - } - - if orig.tparams.Len() != targs.Len() { - // Mismatching arg and tparam length may be checked elsewhere. + if targs.Len() != tpars.Len() { return Typ[Invalid] } - // Ensure that an instance is recorded before substituting, so that we - // resolve n for any recursive references. - h := n.inst.ctxt.instanceHash(orig, targs.list()) - n2 := n.inst.ctxt.update(h, orig, n.TypeArgs().list(), n) - assert(n == n2) + h := ctxt.instanceHash(orig, targs.list()) + u := ctxt.update(h, orig, targs.list(), n) // block fixed point infinite instantiation + assert(n == u) - smap := makeSubstMap(orig.tparams.list(), targs.list()) - var ctxt *Context + m := makeSubstMap(tpars.list(), targs.list()) if check != nil { ctxt = check.context() } - underlying := n.check.subst(n.obj.pos, orig.underlying, smap, n, ctxt) - // If the underlying type of n is an interface, we need to set the receiver of - // its methods accurately -- we set the receiver of interface methods on - // the RHS of a type declaration to the defined type. - if iface, _ := underlying.(*Interface); iface != nil { + + rhs = check.subst(n.obj.pos, orig.fromRHS, m, n, ctxt) + + // TODO(markfreeman): Can we handle this in substitution? + // If the RHS is an interface, we must set the receiver of interface methods + // to the named type. + if iface, _ := rhs.(*Interface); iface != nil { if methods, copied := replaceRecvType(iface.methods, orig, n); copied { - // If the underlying type doesn't actually use type parameters, it's - // possible that it wasn't substituted. In this case we need to create - // a new *Interface before modifying receivers. - if iface == orig.underlying { - old := iface - iface = check.newInterface() - iface.embeddeds = old.embeddeds - assert(old.complete) // otherwise we are copying incomplete data - iface.complete = old.complete - iface.implicit = old.implicit // should be false but be conservative - underlying = iface + // If the RHS doesn't use type parameters, it may not have been + // substituted; we need to craft a new interface first. + if iface == orig.fromRHS { + assert(iface.complete) // otherwise we are copying incomplete data + + crafted := check.newInterface() + crafted.complete = true + crafted.implicit = false + crafted.embeddeds = iface.embeddeds + + iface = crafted } iface.methods = methods iface.tset = nil // recompute type set with new methods - // If check != nil, check.newInterface will have saved the interface for later completion. - if check == nil { // golang/go#61561: all newly created interfaces must be fully evaluated + // go.dev/issue/61561: We have to complete the interface even without a checker. + if check == nil { iface.typeSet() } + + return iface } } - return underlying + return rhs } // safeUnderlying returns the underlying type of typ without expanding diff --git a/src/go/types/object.go b/src/go/types/object.go index 823c03c7fd9..305471222c5 100644 --- a/src/go/types/object.go +++ b/src/go/types/object.go @@ -298,7 +298,8 @@ func NewTypeName(pos token.Pos, pkg *Package, name string, typ Type) *TypeName { // lazily calls resolve to finish constructing the Named object. func _NewTypeNameLazy(pos token.Pos, pkg *Package, name string, load func(*Named) ([]*TypeParam, Type, []*Func, []func())) *TypeName { obj := NewTypeName(pos, pkg, name, nil) - NewNamed(obj, nil, nil).loader = load + n := (*Checker)(nil).newNamed(obj, nil, nil) + n.loader = load return obj } diff --git a/src/go/types/signature.go b/src/go/types/signature.go index 972de97b1a1..2f8be54e171 100644 --- a/src/go/types/signature.go +++ b/src/go/types/signature.go @@ -461,7 +461,7 @@ func (check *Checker) validRecv(pos positioner, recv *Var) { break } var cause string - switch u := T.under().(type) { + switch u := T.Underlying().(type) { case *Basic: // unsafe.Pointer is treated like a regular pointer if u.kind == UnsafePointer { diff --git a/src/go/types/sizeof_test.go b/src/go/types/sizeof_test.go index fa07eb10f19..4ff255ffa02 100644 --- a/src/go/types/sizeof_test.go +++ b/src/go/types/sizeof_test.go @@ -30,7 +30,7 @@ func TestSizeof(t *testing.T) { {Interface{}, 40, 80}, {Map{}, 16, 32}, {Chan{}, 12, 24}, - {Named{}, 60, 112}, + {Named{}, 64, 120}, {TypeParam{}, 28, 48}, {term{}, 12, 24}, diff --git a/src/go/types/typexpr.go b/src/go/types/typexpr.go index c040ee2a29c..2e45b7570f3 100644 --- a/src/go/types/typexpr.go +++ b/src/go/types/typexpr.go @@ -421,7 +421,7 @@ func setDefType(def *TypeName, typ Type) { case *Basic: assert(t == Typ[Invalid]) case *Named: - t.underlying = typ + t.fromRHS = typ default: panic(fmt.Sprintf("unexpected type %T", t)) } diff --git a/src/go/types/under.go b/src/go/types/under.go index 43bf0ad07cd..d5e476a79f4 100644 --- a/src/go/types/under.go +++ b/src/go/types/under.go @@ -14,9 +14,7 @@ import "iter" // under must only be called when a type is known // to be fully set up. func under(t Type) Type { - if t := asNamed(t); t != nil { - return t.under() - } + // TODO(markfreeman): Remove this function, it just delegates. return t.Underlying() } diff --git a/src/go/types/unify.go b/src/go/types/unify.go index abcbab433a1..2e3ba512df7 100644 --- a/src/go/types/unify.go +++ b/src/go/types/unify.go @@ -342,7 +342,7 @@ func (u *unifier) nify(x, y Type, mode unifyMode, p *ifacePair) (result bool) { if traceInference { u.tracef("%s ≡ under %s", x, ny) } - y = ny.under() + y = ny.Underlying() // Per the spec, a defined type cannot have an underlying type // that is a type parameter. assert(!isTypeParam(y)) diff --git a/src/go/types/universe.go b/src/go/types/universe.go index 4753111c118..70935dc35ff 100644 --- a/src/go/types/universe.go +++ b/src/go/types/universe.go @@ -119,7 +119,7 @@ func defPredeclaredTypes() { { obj := NewTypeName(nopos, nil, "error", nil) obj.setColor(black) - typ := NewNamed(obj, nil, nil) + typ := (*Checker)(nil).newNamed(obj, nil, nil) // error.Error() string recv := newVar(RecvVar, nopos, nil, "", typ) @@ -131,7 +131,8 @@ func defPredeclaredTypes() { ityp := &Interface{methods: []*Func{err}, complete: true} computeInterfaceTypeSet(nil, nopos, ityp) // prevent races due to lazy computation of tset - typ.SetUnderlying(ityp) + typ.fromRHS = ityp + typ.Underlying() def(obj) } @@ -139,12 +140,13 @@ func defPredeclaredTypes() { { obj := NewTypeName(nopos, nil, "comparable", nil) obj.setColor(black) - typ := NewNamed(obj, nil, nil) + typ := (*Checker)(nil).newNamed(obj, nil, nil) // interface{} // marked as comparable ityp := &Interface{complete: true, tset: &_TypeSet{nil, allTermlist, true}} - typ.SetUnderlying(ityp) + typ.fromRHS = ityp + typ.Underlying() def(obj) } } diff --git a/src/go/types/validtype.go b/src/go/types/validtype.go index f87b82c439e..578dcba1f97 100644 --- a/src/go/types/validtype.go +++ b/src/go/types/validtype.go @@ -94,13 +94,6 @@ func (check *Checker) validType0(pos token.Pos, typ Type, nest, path []*Named) b // break // } - // Don't report a 2nd error if we already know the type is invalid - // (e.g., if a cycle was detected earlier, via under). - // Note: ensure that t.orig is fully resolved by calling Underlying(). - if !isValid(t.Underlying()) { - return false - } - // If the current type t is also found in nest, (the memory of) t is // embedded in itself, indicating an invalid recursive type. for _, e := range nest { @@ -128,8 +121,9 @@ func (check *Checker) validType0(pos token.Pos, typ Type, nest, path []*Named) b // are not yet available to other goroutines). assert(t.obj.pkg == check.pkg) assert(t.Origin().obj.pkg == check.pkg) - t.underlying = Typ[Invalid] - t.Origin().underlying = Typ[Invalid] + + // let t become invalid when it resolves + t.Origin().fromRHS = Typ[Invalid] // Find the starting point of the cycle and report it. // Because each type in nest must also appear in path (see invariant below), diff --git a/src/internal/types/testdata/fixedbugs/issue75194.go b/src/internal/types/testdata/fixedbugs/issue75194.go new file mode 100644 index 00000000000..ec2f9249ec9 --- /dev/null +++ b/src/internal/types/testdata/fixedbugs/issue75194.go @@ -0,0 +1,14 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package p + +type A /* ERROR "invalid recursive type: A refers to itself" */ struct { + a A +} + +type B /* ERROR "invalid recursive type: B refers to itself" */ struct { + a A + b B +}