// Copyright 2011 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 types2 import ( "cmd/compile/internal/syntax" "strings" "sync" "sync/atomic" ) // Type-checking Named types is subtle, because they may be recursively // defined, and because their full details may be spread across multiple // declarations (via methods). For this reason they are type-checked lazily, // to avoid information being accessed before it is complete. // // Conceptually, it is helpful to think of named types as having two distinct // sets of information: // - "LHS" information, defining their identity: Obj() and TypeArgs() // - "RHS" information, defining their details: TypeParams(), Underlying(), // and methods. // // In this taxonomy, LHS information is available immediately, but RHS // information is lazy. Specifically, a named type N may be constructed in any // of the following ways: // 1. type-checked from the source // 2. loaded eagerly from export data // 3. loaded lazily from export data (when using unified IR) // 4. instantiated from a generic type // // In cases 1, 3, and 4, it is possible that the underlying type or methods of // N may not be immediately available. // - During type-checking, we allocate N before type-checking its underlying // type or methods, so that we can create recursive references. // - When loading from export data, we may load its methods and underlying // type lazily using a provided load function. // - After instantiating, we lazily expand the underlying type and methods // (note that instances may be created while still in the process of // type-checking the original type declaration). // // In cases 3 and 4 this lazy construction may also occur concurrently, due to // concurrent use of the type checker API (after type checking or importing has // finished). It is critical that we keep track of state, so that Named types // are constructed exactly once and so that we do not access their details too // soon. // // We achieve this by tracking state with an atomic state variable, and // guarding potentially concurrent calculations with a mutex. See [stateMask] // for details. // // GLOSSARY: Here are a few terms used in this file to describe Named types: // - We say that a Named type is "instantiated" if it has been constructed by // instantiating a generic named type with type arguments. // - We say that a Named type is "declared" if it corresponds to a type // declaration in the source. Instantiated named types correspond to a type // instantiation in the source, not a declaration. But their Origin type is // a declared type. // - We say that a Named type is "unpacked" if its RHS information has been // populated, normalizing its representation for use in type-checking // operations and abstracting away how it was created: // - For a Named type constructed from unified IR, this involves invoking // a lazy loader function to extract details from UIR as needed. // - For an instantiated Named type, this involves extracting information // from its 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 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". // // Some invariants to keep in mind: each declared Named type has a single // corresponding object, and that object's type is the (possibly generic) Named // type. Declared Named types are identical if and only if their pointers are // identical. On the other hand, multiple instantiated Named types may be // identical even though their pointers are not identical. One has to use // Identical to compare them. For instantiated named types, their obj is a // synthetic placeholder that records their position of the corresponding // instantiation in the source (if they were constructed during type checking). // // To prevent infinite expansion of named instances that are created outside of // type-checking, instances share a Context with other instances created during // their expansion. Via the pidgeonhole principle, this guarantees that in the // presence of a cycle of named types, expansion will eventually find an // existing instance in the Context and short-circuit the expansion. // // Once an instance is fully expanded, we can nil out this shared Context to unpin // memory, though the Context may still be held by other incomplete instances // in its "lineage". // A Named represents a named (defined) type. // // A declaration such as: // // type S struct { ... } // // creates a defined type whose underlying type is a struct, // and binds this type to the object S, a [TypeName]. // Use [Named.Underlying] to access the underlying type. // Use [Named.Obj] to obtain the object S. // // Before type aliases (Go 1.9), the spec called defined types "named types". 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 // 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] 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 or when mu is held fromRHS Type // the declaration RHS this type is derived from tparams *TypeParamList // type parameters, or nil underlying Type // underlying type, or nil finite bool // whether the type has finite size // methods declared for this type (not the method set of this type) // Signatures are type-checked lazily. // For non-instantiated types, this is a fully populated list of methods. For // instantiated types, methods are individually expanded when they are first // accessed. methods []*Func // loader may be provided to lazily load type parameters, underlying type, methods, and delayed functions loader func(*Named) ([]*TypeParam, Type, []*Func, []func()) } // instance holds information that is only necessary for instantiated named // types. type instance struct { orig *Named // original, uninstantiated type targs *TypeList // type arguments expandedMethods int // number of expanded methods; expandedMethods <= len(orig.methods) ctxt *Context // local Context; set to nil after full expansion } // stateMask represents each state in the lifecycle of a named type. // // Each named type begins in the initial state. A named type may transition to a new state // according to the below diagram: // // initial // lazyLoaded // unpacked // └── hasMethods // └── hasUnder // └── hasFinite // // That is, descent down the tree is mostly linear (initial through unpacked), except upon // reaching the leaves (hasMethods, hasUnder, and hasFinite). A type may occupy any // combination of the leaf states at once (they are independent states). // // To represent this independence, the set of active states is represented with a bit set. State // transitions are monotonic. Once a state bit is set, it remains set. // // The above constraints significantly narrow the possible bit sets for a named type. With bits // set left-to-right, they are: // // 00000 | initial // 10000 | lazyLoaded // 11000 | unpacked, which implies lazyLoaded // 11100 | hasMethods, which implies unpacked (which in turn implies lazyLoaded) // 11010 | hasUnder, which implies unpacked ... // 11001 | hasFinite, which implies unpacked ... // 11110 | both hasMethods and hasUnder which implies unpacked ... // ... | (other combinations of leaf states) // // To read the state of a named type, use [Named.stateHas]; to write, use [Named.setState]. type stateMask uint32 const ( // initially, type parameters, RHS, underlying, and methods might be unavailable lazyLoaded stateMask = 1 << iota // methods are available, but constraints might be unexpanded (for generic types) unpacked // methods might be unexpanded (for instances) hasMethods // methods are all expanded (for instances) hasUnder // underlying type is available hasFinite // size finiteness is available ) // NewNamed returns a new named type for the given type name, underlying type, and associated methods. // If the given type name obj doesn't have a type yet, its type is set to the returned named type. // The underlying type must not be a *Named. func NewNamed(obj *TypeName, underlying Type, methods []*Func) *Named { if asNamed(underlying) != nil { panic("underlying type must not be *Named") } n := (*Checker)(nil).newNamed(obj, underlying, methods) if underlying == nil { n.allowNilRHS = true n.allowNilUnderlying = true } else { n.SetUnderlying(underlying) } return n } // unpack populates the type parameters, methods, and RHS of n. // // For the purposes of unpacking, there are three categories of named types: // 1. Lazy loaded types // 2. Instantiated types // 3. All others // // Note that the above form a partition. // // Lazy loaded types: // Type parameters, methods, and RHS of n become accessible and are fully // expanded. // // Instantiated types: // Type parameters, methods, and RHS of n become accessible, though methods // are lazily populated as needed. // // All others: // Effectively, nothing happens. func (n *Named) unpack() *Named { if n.stateHas(lazyLoaded | unpacked) { // avoid locking below return n } // TODO(rfindley): if n.check is non-nil we can avoid locking here, since // type-checking is not concurrent. Evaluate if this is worth doing. n.mu.Lock() defer n.mu.Unlock() // only atomic for consistency; we are holding the mutex if n.stateHas(lazyLoaded | unpacked) { return n } // underlying comes after unpacking, do not set it defer (func() { assert(!n.stateHas(hasUnder)) })() if n.inst != nil { assert(n.fromRHS == nil) // instantiated types are not declared types assert(n.loader == nil) // cannot import an instantiation orig := n.inst.orig orig.unpack() n.fromRHS = n.expandRHS() n.tparams = orig.tparams if len(orig.methods) == 0 { n.setState(lazyLoaded | unpacked | hasMethods) // nothing further to do n.inst.ctxt = nil } else { n.setState(lazyLoaded | unpacked) } return n } // TODO(mdempsky): Since we're passing n to the loader anyway // (necessary because types2 expects the receiver type for methods // on defined interface types to be the Named rather than the // underlying Interface), maybe it should just handle calling // SetTypeParams, SetUnderlying, and AddMethod instead? Those // 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.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.fromRHS = underlying // for cycle detection n.methods = methods n.setState(lazyLoaded) // avoid deadlock calling delayed functions for _, f := range delayed { f() } } n.setState(lazyLoaded | unpacked | hasMethods) return n } // stateHas atomically determines whether the current state includes any active bit in sm. func (n *Named) stateHas(m stateMask) bool { return stateMask(atomic.LoadUint32(&n.state_))&m != 0 } // setState atomically sets the current state to include each active bit in sm. // Must only be called while holding n.mu. func (n *Named) setState(m stateMask) { atomic.OrUint32(&n.state_, uint32(m)) // verify state transitions if debug { m := stateMask(atomic.LoadUint32(&n.state_)) u := m&unpacked != 0 // unpacked => lazyLoaded if u { assert(m&lazyLoaded != 0) } // hasMethods => unpacked if m&hasMethods != 0 { assert(u) } // hasUnder => unpacked if m&hasUnder != 0 { assert(u) } // hasFinite => unpacked if m&hasFinite != 0 { assert(u) } } } // newNamed is like NewNamed but with a *Checker receiver. 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 } // Ensure that typ is always sanity-checked. if check != nil { check.needsCleanup(typ) } return typ } // newNamedInstance creates a new named instance for the given origin and type // arguments, recording pos as the position of its synthetic object (for error // reporting). // // If set, expanding is the named type instance currently being expanded, that // led to the creation of this instance. func (check *Checker) newNamedInstance(pos syntax.Pos, orig *Named, targs []Type, expanding *Named) *Named { assert(len(targs) > 0) obj := NewTypeName(pos, orig.obj.pkg, orig.obj.name, nil) inst := &instance{orig: orig, targs: newTypeList(targs)} // Only pass the expanding context to the new instance if their packages // match. Since type reference cycles are only possible within a single // package, this is sufficient for the purposes of short-circuiting cycles. // Avoiding passing the context in other cases prevents unnecessary coupling // of types across packages. if expanding != nil && expanding.Obj().pkg == obj.pkg { inst.ctxt = expanding.inst.ctxt } typ := &Named{check: check, obj: obj, inst: inst} obj.typ = typ // Ensure that typ is always sanity-checked. if check != nil { check.needsCleanup(typ) } return typ } 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.Underlying() } n.check = nil } // Obj returns the type name for the declaration defining the named type t. For // instantiated types, this is same as the type name of the origin type. func (t *Named) Obj() *TypeName { if t.inst == nil { return t.obj } return t.inst.orig.obj } // Origin returns the generic type from which the named type t is // instantiated. If t is not an instantiated type, the result is t. func (t *Named) Origin() *Named { if t.inst == nil { return t } return t.inst.orig } // TypeParams returns the type parameters of the named type t, or nil. // The result is non-nil for an (originally) generic type even if it is instantiated. func (t *Named) TypeParams() *TypeParamList { return t.unpack().tparams } // SetTypeParams sets the type parameters of the named type t. // t must not have type arguments. func (t *Named) SetTypeParams(tparams []*TypeParam) { assert(t.inst == nil) t.unpack().tparams = bindTParams(tparams) } // TypeArgs returns the type arguments used to instantiate the named type t. func (t *Named) TypeArgs() *TypeList { if t.inst == nil { return nil } return t.inst.targs } // NumMethods returns the number of explicit methods defined for t. func (t *Named) NumMethods() int { return len(t.Origin().unpack().methods) } // Method returns the i'th method of named type t for 0 <= i < t.NumMethods(). // // For an ordinary or instantiated type t, the receiver base type of this // method is the named type t. For an uninstantiated generic type t, each // method receiver is instantiated with its receiver type parameters. // // Methods are numbered deterministically: given the same list of source files // presented to the type checker, or the same sequence of NewMethod and AddMethod // calls, the mapping from method index to corresponding method remains the same. // But the specific ordering is not specified and must not be relied on as it may // change in the future. func (t *Named) Method(i int) *Func { t.unpack() if t.stateHas(hasMethods) { return t.methods[i] } assert(t.inst != nil) // only instances should have unexpanded methods orig := t.inst.orig t.mu.Lock() defer t.mu.Unlock() if len(t.methods) != len(orig.methods) { assert(len(t.methods) == 0) t.methods = make([]*Func, len(orig.methods)) } if t.methods[i] == nil { assert(t.inst.ctxt != nil) // we should still have a context remaining from the resolution phase t.methods[i] = t.expandMethod(i) t.inst.expandedMethods++ // Check if we've created all methods at this point. If we have, mark the // type as having all of its methods. if t.inst.expandedMethods == len(orig.methods) { t.setState(hasMethods) t.inst.ctxt = nil // no need for a context anymore } } return t.methods[i] } // expandMethod substitutes type arguments in the i'th method for an // instantiated receiver. func (t *Named) expandMethod(i int) *Func { // t.orig.methods is not lazy. origm is the method instantiated with its // receiver type parameters (the "origin" method). origm := t.inst.orig.Method(i) assert(origm != nil) check := t.check // Ensure that the original method is type-checked. if check != nil { check.objDecl(origm) } origSig := origm.typ.(*Signature) rbase, _ := deref(origSig.Recv().Type()) // If rbase is t, then origm is already the instantiated method we're looking // for. In this case, we return origm to preserve the invariant that // traversing Method->Receiver Type->Method should get back to the same // method. // // This occurs if t is instantiated with the receiver type parameters, as in // the use of m in func (r T[_]) m() { r.m() }. if rbase == t { return origm } sig := origSig // We can only substitute if we have a correspondence between type arguments // and type parameters. This check is necessary in the presence of invalid // code. if origSig.RecvTypeParams().Len() == t.inst.targs.Len() { smap := makeSubstMap(origSig.RecvTypeParams().list(), t.inst.targs.list()) var ctxt *Context if check != nil { ctxt = check.context() } sig = check.subst(origm.pos, origSig, smap, t, ctxt).(*Signature) } if sig == origSig { // No substitution occurred, but we still need to create a new signature to // hold the instantiated receiver. copy := *origSig sig = © } var rtyp Type if origm.hasPtrRecv() { rtyp = NewPointer(t) } else { rtyp = t } sig.recv = cloneVar(origSig.recv, rtyp) return cloneFunc(origm, sig) } // SetUnderlying sets the underlying type and marks t as complete. // t must not have type arguments. func (t *Named) SetUnderlying(u Type) { assert(t.inst == nil) if u == nil { panic("underlying type must not be nil") } if asNamed(u) != nil { panic("underlying type must not be *Named") } // be careful to uphold the state invariants t.mu.Lock() defer t.mu.Unlock() t.fromRHS = u t.allowNilRHS = false t.setState(lazyLoaded | unpacked | hasMethods) // TODO(markfreeman): Why hasMethods? t.underlying = u t.allowNilUnderlying = false t.setState(hasUnder) } // AddMethod adds method m unless it is already in the method list. // The method must be in the same package as t, and t must not have // type arguments. func (t *Named) AddMethod(m *Func) { assert(samePkg(t.obj.pkg, m.pkg)) assert(t.inst == nil) t.unpack() if t.methodIndex(m.name, false) < 0 { t.methods = append(t.methods, m) } } // methodIndex returns the index of the method with the given name. // If foldCase is set, capitalization in the name is ignored. // The result is negative if no such method exists. func (t *Named) methodIndex(name string, foldCase bool) int { if name == "_" { return -1 } if foldCase { for i, m := range t.methods { if strings.EqualFold(m.name, name) { return i } } } else { for i, m := range t.methods { if m.name == name { return i } } } return -1 } // rhs returns [Named.fromRHS]. // // In debug mode, it also asserts that n is in an appropriate state. func (n *Named) rhs() Type { if debug { assert(n.stateHas(lazyLoaded | unpacked)) } return n.fromRHS } // Underlying returns the [underlying type] of the named type t, resolving all // forwarding declarations. Underlying types are never Named, TypeParam, or // Alias types. // // [underlying type]: https://go.dev/ref/spec#Underlying_types. func (n *Named) Underlying() Type { n.unpack() // The gccimporter depends on writing a nil underlying via NewNamed and // immediately reading it back. Rather than putting that in Named.under // and complicating things there, we just check for that special case here. if n.rhs() == nil { assert(n.allowNilRHS) if n.allowNilUnderlying { return nil } } if !n.stateHas(hasUnder) { // minor performance optimization n.resolveUnderlying() } return n.underlying } func (t *Named) String() string { return TypeString(t, nil) } // ---------------------------------------------------------------------------- // Implementation // // TODO(rfindley): reorganize the loading and expansion methods under this // heading. // resolveUnderlying computes the underlying type of n. If n already has an // underlying type, nothing happens. // // It does so by following RHS type chains for alias and named types. If any // other type T is found, each named type in the chain has its underlying // type set to T. Aliases are skipped because their underlying type is // not memoized. // // resolveUnderlying assumes that there are no direct cycles; if there were // any, they were broken (by setting the respective types to invalid) during // the directCycles check phase. func (n *Named) resolveUnderlying() { assert(n.stateHas(unpacked)) var seen map[*Named]bool // for debugging only if debug { seen = make(map[*Named]bool) } var path []*Named var u Type for rhs := Type(n); u == nil; { switch t := rhs.(type) { case nil: u = Typ[Invalid] case *Alias: rhs = unalias(t) case *Named: if debug { assert(!seen[t]) seen[t] = true } // don't recalculate the underlying if t.stateHas(hasUnder) { u = t.underlying break } if debug { seen[t] = true } path = append(path, t) t.unpack() assert(t.rhs() != nil || t.allowNilRHS) rhs = t.rhs() default: u = rhs // any type literal or predeclared type works } } for _, t := range path { func() { t.mu.Lock() defer t.mu.Unlock() // Careful, t.underlying has lock-free readers. Since we might be racing // another call to resolveUnderlying, we have to avoid overwriting // t.underlying. Otherwise, the race detector will be tripped. if !t.stateHas(hasUnder) { t.underlying = u t.setState(hasUnder) } }() } } func (n *Named) lookupMethod(pkg *Package, name string, foldCase bool) (int, *Func) { n.unpack() if samePkg(n.obj.pkg, pkg) || isExported(name) || foldCase { // If n is an instance, we may not have yet instantiated all of its methods. // Look up the method index in orig, and only instantiate method at the // matching index (if any). if i := n.Origin().methodIndex(name, foldCase); i >= 0 { // For instances, m.Method(i) will be different from the orig method. return i, n.Method(i) } } return -1, nil } // context returns the type-checker context. func (check *Checker) context() *Context { if check.ctxt == nil { check.ctxt = NewContext() } return check.ctxt } // 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.expandRHS %s", n) check.indent++ defer func() { check.indent-- check.trace(n.obj.pos, "=> %s (rhs = %s)", n, rhs) }() } assert(!n.stateHas(unpacked)) assert(n.inst.orig.stateHas(lazyLoaded | unpacked)) if n.inst.ctxt == nil { n.inst.ctxt = NewContext() } ctxt := n.inst.ctxt orig := n.inst.orig targs := n.inst.targs tpars := orig.tparams if targs.Len() != tpars.Len() { return Typ[Invalid] } h := ctxt.instanceHash(orig, targs.list()) u := ctxt.update(h, orig, targs.list(), n) // block fixed point infinite instantiation assert(n == u) m := makeSubstMap(tpars.list(), targs.list()) if check != nil { ctxt = check.context() } rhs = check.subst(n.obj.pos, orig.rhs(), 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 RHS doesn't use type parameters, it may not have been // substituted; we need to craft a new interface first. if iface == orig.rhs() { 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 // go.dev/issue/61561: We have to complete the interface even without a checker. if check == nil { iface.typeSet() } return iface } } return rhs } // safeUnderlying returns the underlying type of typ without expanding // instances, to avoid infinite recursion. // // TODO(rfindley): eliminate this function or give it a better name. func safeUnderlying(typ Type) Type { if t := asNamed(typ); t != nil { return t.underlying } return typ.Underlying() }