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()
}
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]
}
}
}
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)
// - 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
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.
// 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
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
// 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
}
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
// 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
}
}
// 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
}
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
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.
// 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) }
// 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.
-//
-// This is necessary because the underlying type of named may be itself a
-// named type that is incomplete:
-//
-// 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()
-
- // 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 n0.check == nil {
- panic("Named.check == nil but type is incomplete")
- }
-
- // 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) // 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) {
+// under returns the (possibly expanded) underlying type of n.
+//
+// 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.
+//
+// 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 n.underlying != nil {
+ return n.underlying
+ }
+
+ var rhs Type = n
+ var u Type
+
+ seen := make(map[*Named]int)
+ var path []Object
+
+ 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
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
- targs := n.inst.targs
- 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)
- }
+ assert(orig.state() >= resolved)
+ assert(orig.fromRHS != nil)
+
+ targs := n.inst.targs
+ tpars := orig.tparams
- 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
// 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
}
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 {
{Interface{}, 40, 80},
{Map{}, 16, 32},
{Chan{}, 12, 24},
- {Named{}, 60, 112},
+ {Named{}, 64, 120},
{TypeParam{}, 28, 48},
{term{}, 12, 24},
case *Basic:
assert(t == Typ[Invalid])
case *Named:
- t.underlying = typ
+ t.fromRHS = typ
default:
panic(fmt.Sprintf("unexpected type %T", t))
}
// 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()
}
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))
{
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)
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)
}
{
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)
}
}
// 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 {
// 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),
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()
}
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]
}
}
}
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)
// - 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
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.
// 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
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
// 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
}
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
// 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
}
}
// 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
}
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
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.
// 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) }
// 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.
-//
-// This is necessary because the underlying type of named may be itself a
-// named type that is incomplete:
-//
-// 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()
-
- // 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 n0.check == nil {
- panic("Named.check == nil but type is incomplete")
- }
-
- // 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) // 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) {
+// under returns the (possibly expanded) underlying type of n.
+//
+// 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.
+//
+// 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 n.underlying != nil {
+ return n.underlying
+ }
+
+ var rhs Type = n
+ var u Type
+
+ seen := make(map[*Named]int)
+ var path []Object
+
+ 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
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
- targs := n.inst.targs
- 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)
- }
+ assert(orig.state() >= resolved)
+ assert(orig.fromRHS != nil)
+
+ targs := n.inst.targs
+ tpars := orig.tparams
- 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
// 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
}
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 {
{Interface{}, 40, 80},
{Map{}, 16, 32},
{Chan{}, 12, 24},
- {Named{}, 60, 112},
+ {Named{}, 64, 120},
{TypeParam{}, 28, 48},
{term{}, 12, 24},
case *Basic:
assert(t == Typ[Invalid])
case *Named:
- t.underlying = typ
+ t.fromRHS = typ
default:
panic(fmt.Sprintf("unexpected type %T", t))
}
// 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()
}
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))
{
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)
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)
}
{
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)
}
}
// 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 {
// 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),
--- /dev/null
+// 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
+}