go/types, types2: change and enforce lifecycle of Named.fromRHS and Named.underlying fields

A type definition or alias declaration consists of a type name (LHS)
which is bound to a type expression (RHS) by the declaration.

This CL consistently uses the fromRHS fields of Named and Alias types
to represent that RHS type expression, and sets Named.underlying and
Alias.actual only once those types have been computed.

Currently, Named types use Named.underlying for some of this
functionality, which makes the code difficult to understand. Operations
which used Named.underlying now use Named.fromRHS.

For example, in:

  type A = B
  type B = int

A.fromRHS is B (Alias) and B.fromRHS is int (Basic).

Meanwhile, in:

  type A B
  type B int

A.underlying is B (Named) and B.underlying is int (Basic) initially.

Note that despite A.underlying pointing to B, B is not the underlying
type of A (it is int). At some point during type checking, A walks
through the chain A.underlying -> B.underlying -> int and sets
A.underlying to int.

While this approach works, it introduces some problems:

  1. Whether A.underlying refers to the underlying type (int) or not
     (B) depends on when the field is accessed.
  2. There is no convenient mechanism to check if the underlying type
     of B has been deduced. One can check if B.underlying is a named
     type, but since B.underlying is already B's underlying type (int),
     it's still ambiguous.

Operations derived from Named.underlying share similar problems. For
example, Named.expandUnderlying() (which substitutes type arguments)
returns an instantiated named type whose Named.underlying also may or
may not refer to its underlying type.

With this change, Named.underlying is nil as long as it is unknown, and
non-nil and not a named type once it is known. Additional assertions are
added to enforce that:

  1. Named.underlying is not set until Named has been resolved.
  2. Named is not resolved until Named.fromRHS is populated, unless it
     is given explicit permission. This permission is briefly given
     while type-checking declarations of named types to account for
     cycles of alias types represented as TypeNames. It is also given to
     named types created through NewNamed for backward compatibility.
     This permission is revoked when SetUnderlying is called.

Accessors of Named.underlying are responsible for first resolving
the named type, unless they are in a context where they know the
type to already be resolved.

This change also exposed a bug in validType wherein the underlying
type for struct types containing invalid types did not have their
underlying type set to invalid (see #75194). This bug was exploited by a
test in x/tools, which has been disabled for Go 1.26 (via CL 700395).

Other minor adjustments are made for instantiated and loaded types.
Instantiated types have no RHS as they are not declared, and loaded
types set their RHS to the underlying from export data directly.

Minor simplifications are also made throughout.

Fixes #75194

Change-Id: I72644d7329c996eb1e67514063fe51c3ae06c38d
Reviewed-on: https://go-review.googlesource.com/c/go/+/695977
Auto-Submit: Mark Freeman <markfreeman@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Robert Griesemer <gri@google.com>
This commit is contained in:
Mark Freeman 2025-08-20 17:30:26 -04:00 committed by Gopher Robot
parent 41f5659347
commit 1099436f1b
21 changed files with 428 additions and 412 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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},

View file

@ -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))
}

View file

@ -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()
}

View file

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

View file

@ -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)
}
}

View file

@ -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),

View file

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

View file

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

View file

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

View file

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

View file

@ -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},

View file

@ -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))
}

View file

@ -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()
}

View file

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

View file

@ -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)
}
}

View file

@ -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),

View file

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