return
}
- if hasVarSize(x.typ, nil) {
+ if check.hasVarSize(x.typ) {
x.mode = value
if check.recordTypes() {
check.recordBuiltinType(call.Fun, makeSig(Typ[Uintptr], x.typ))
// the part of the struct which is variable-sized. This makes both the rules
// simpler and also permits (or at least doesn't prevent) a compiler from re-
// arranging struct fields if it wanted to.
- if hasVarSize(base, nil) {
+ if check.hasVarSize(base) {
x.mode = value
if check.recordTypes() {
check.recordBuiltinType(call.Fun, makeSig(Typ[Uintptr], obj.Type()))
return
}
- if hasVarSize(x.typ, nil) {
+ if check.hasVarSize(x.typ) {
x.mode = value
if check.recordTypes() {
check.recordBuiltinType(call.Fun, makeSig(Typ[Uintptr], x.typ))
// hasVarSize reports if the size of type t is variable due to type parameters
// or if the type is infinitely-sized due to a cycle for which the type has not
// yet been checked.
-func hasVarSize(t Type, seen map[*Named]bool) (varSized bool) {
- // Cycles are only possible through *Named types.
- // The seen map is used to detect cycles and track
- // the results of previously seen types.
- if named := asNamed(t); named != nil {
- if v, ok := seen[named]; ok {
- return v
- }
- if seen == nil {
- seen = make(map[*Named]bool)
- }
- seen[named] = true // possibly cyclic until proven otherwise
- defer func() {
- seen[named] = varSized // record final determination for named
- }()
- }
+func (check *Checker) hasVarSize(t Type) bool {
+ // Note: We could use Underlying here, but passing through the RHS may yield
+ // better error messages.
+ switch t := Unalias(t).(type) {
+ case *Named:
+ if t.stateHas(hasFinite) {
+ // TODO(mark): Rename t.finite to t.varSize to avoid inversion.
+ return !t.finite
+ }
+
+ if i, ok := check.objPathIdx[t.obj]; ok {
+ cycle := check.objPath[i:]
+ check.cycleError(cycle, firstInSrc(cycle))
+ return true
+ }
+
+ check.push(t.obj)
+ defer check.pop()
+
+ varSize := check.hasVarSize(t.fromRHS)
+
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ // Careful, t.finite has lock-free readers. Since we might be racing
+ // another call to finiteSize, we have to avoid overwriting t.finite.
+ // Otherwise, the race detector will be tripped.
+ if !t.stateHas(hasFinite) {
+ t.finite = !varSize
+ t.setState(hasFinite)
+ }
+
+ return varSize
- switch u := t.Underlying().(type) {
case *Array:
- return hasVarSize(u.elem, seen)
+ // The array length is already computed. If it was a valid length, it
+ // is finite; else, an error was reported in the computation.
+ return check.hasVarSize(t.elem)
+
case *Struct:
- for _, f := range u.fields {
- if hasVarSize(f.typ, seen) {
+ for _, f := range t.fields {
+ if check.hasVarSize(f.typ) {
return true
}
}
- case *Interface:
- return isTypeParam(t)
- case *Named, *Union:
- panic("unreachable")
+
+ case *TypeParam:
+ return true
}
+
return false
}
}
T := x.typ
x.mode = invalid
+ // We cannot convert a value to an incomplete type; make sure it's complete.
+ if !check.isComplete(T) {
+ x.expr = call
+ return conversion
+ }
switch n := len(call.ArgList); n {
case 0:
check.errorf(call, WrongArgCount, "missing argument in conversion to %s", T)
} else {
x.mode = value
}
- x.typ = sig.results.vars[0].typ // unpack tuple
+ typ := sig.results.vars[0].typ // unpack tuple
+ // We cannot return a value of an incomplete type; make sure it's complete.
+ if !check.isComplete(typ) {
+ x.mode = invalid
+ x.expr = call
+ return statement
+ }
+ x.typ = typ
default:
x.mode = value
x.typ = sig.results
goto Error
}
- // Avoid crashing when checking an invalid selector in a method declaration
- // (i.e., where def is not set):
+ // We cannot select on an incomplete type; make sure it's complete.
+ if !check.isComplete(x.typ) {
+ goto Error
+ }
+
+ // Avoid crashing when checking an invalid selector in a method declaration.
//
// type S[T any] struct{}
// type V = S[any]
// expecting a type expression, it is an error.
//
// See go.dev/issue/57522 for more details.
- //
- // TODO(rfindley): We should do better by refusing to check selectors in all cases where
- // x.typ is incomplete.
if wantType {
check.errorf(e.Sel, NotAType, "%s is not a type", syntax.Expr(e))
goto Error
}
+ // Additionally, if x.typ is a pointer type, selecting implicitly dereferences the value, meaning
+ // its base type must also be complete.
+ if p, ok := x.typ.Underlying().(*Pointer); ok && !check.isComplete(p.base) {
+ goto Error
+ }
+
obj, index, indirect = lookupFieldOrMethod(x.typ, x.mode == variable, check.pkg, sel, false)
if obj == nil {
// Don't report another error if the underlying type was invalid (go.dev/issue/49541).
}
}
-// TODO(markfreeman): Can the value cached on Named be used in validType / hasVarSize?
-
-// finiteSize returns whether a type has finite size.
-func (check *Checker) finiteSize(t Type) bool {
- switch t := Unalias(t).(type) {
- case *Named:
- if t.stateHas(hasFinite) {
- return t.finite
- }
-
- if i, ok := check.objPathIdx[t.obj]; ok {
+// isComplete returns whether a type is complete (i.e. up to having an underlying type).
+// Incomplete types will panic if [Type.Underlying] is called on them.
+func (check *Checker) isComplete(t Type) bool {
+ if n, ok := Unalias(t).(*Named); ok {
+ if i, found := check.objPathIdx[n.obj]; found {
cycle := check.objPath[i:]
check.cycleError(cycle, firstInSrc(cycle))
return false
}
- check.push(t.obj)
- defer check.pop()
-
- isFinite := check.finiteSize(t.fromRHS)
-
- t.mu.Lock()
- defer t.mu.Unlock()
- // Careful, t.finite has lock-free readers. Since we might be racing
- // another call to finiteSize, we have to avoid overwriting t.finite.
- // Otherwise, the race detector will be tripped.
- if !t.stateHas(hasFinite) {
- t.finite = isFinite
- t.setState(hasFinite)
- }
-
- return isFinite
- case *Array:
- // The array length is already computed. If it was a valid length, it
- // is finite; else, an error was reported in the computation.
- return check.finiteSize(t.elem)
-
- case *Struct:
- for _, f := range t.fields {
- if !check.finiteSize(f.typ) {
- return false
- }
- }
+ // We must walk through names because we permit certain cycles of names.
+ // Consider:
+ //
+ // type A B
+ // type B [unsafe.Sizeof(A{})]int
+ //
+ // starting at B. At the site of A{}, A has no underlying type, and so a
+ // cycle must be reported.
+ return check.isComplete(n.fromRHS)
}
return true
}
named := check.newNamed(obj, nil, nil)
-
- // TODO: adjust this comment (gotypesalias) as needed if we don't need allowNilRHS anymore.
- // 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.unpack() will panic
- // type N A
- // type A = N // N.fromRHS is not set before N.unpack(), 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()
return
case syntax.Recv:
- if elem := check.chanElem(x, x, true); elem != nil {
+ // We cannot receive a value with an incomplete type; make sure it's complete.
+ if elem := check.chanElem(x, x, true); elem != nil && check.isComplete(elem) {
x.mode = commaok
x.typ = elem
check.hasCallOrRecv = true
check.nonGeneric(T, x)
}
- // Here, x is a value, meaning it has a type. If that type is pending, then we have
- // a cycle. As an example:
- //
- // type T [unsafe.Sizeof(T{})]int
- //
- // has a cycle T->T which is deemed valid (by decl.go), but which is in fact invalid.
- check.pendingType(x)
check.record(x)
return kind
}
}
-// If x has a pending type (i.e. its declaring object is on the object path), pendingType
-// reports an error and invalidates x.mode and x.typ.
-// Otherwise it leaves x alone.
-func (check *Checker) pendingType(x *operand) {
- if x.mode == invalid || x.mode == novalue {
- return
- }
- if !check.finiteSize(x.typ) {
- x.mode = invalid
- x.typ = Typ[Invalid]
- }
-}
-
// exprInternal contains the core of type checking of expressions.
// Must only be called by rawExpr.
// (See rawExpr for an explanation of the parameters.)
if !isValid(T) {
goto Error
}
+ // We cannot assert to an incomplete type; make sure it's complete.
+ if !check.isComplete(T) {
+ goto Error
+ }
check.typeAssertion(e, x, T, false)
x.mode = commaok
x.typ = T
}) {
goto Error
}
+ // We cannot dereference a pointer with an incomplete base type; make sure it's complete.
+ if !check.isComplete(base) {
+ goto Error
+ }
x.mode = variable
x.typ = base
}
return false
}
+ // We cannot index on an incomplete type; make sure it's complete.
+ if !check.isComplete(x.typ) {
+ x.mode = invalid
+ return false
+ }
+ // Additionally, if x.typ is a pointer to an array type, indexing implicitly dereferences the value, meaning
+ // its base type must also be complete.
+ if p, ok := x.typ.Underlying().(*Pointer); ok && !check.isComplete(p.base) {
+ x.mode = invalid
+ return false
+ }
+
// ordinary index expression
valid := false
length := int64(-1) // valid if >= 0
}
}
+ // Note that we don't permit slice expressions where x is a type expression, so we don't check for that here.
+ // However, if x.typ is a pointer to an array type, slicing implicitly dereferences the value, meaning
+ // its base type must also be complete.
+ if p, ok := x.typ.Underlying().(*Pointer); ok && !check.isComplete(p.base) {
+ x.mode = invalid
+ return
+ }
+
valid := false
length := int64(-1) // valid if >= 0
switch u := cu.(type) {
base = typ
}
+ // We cannot create a literal of an incomplete type; make sure it's complete.
+ if !check.isComplete(base) {
+ x.mode = invalid
+ return
+ }
+
switch u, _ := commonUnder(base, nil); utyp := u.(type) {
case *Struct:
if len(e.ElemList) == 0 {
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]
+ allowNilRHS bool // may be true from creation via [NewNamed] until [Named.SetUnderlying]
inst *instance // information for instantiated types; nil otherwise
n := (*Checker)(nil).newNamed(obj, underlying, methods)
if underlying == nil {
n.allowNilRHS = true
- n.allowNilUnderlying = true
} else {
n.SetUnderlying(underlying)
}
t.setState(lazyLoaded | unpacked | hasMethods) // TODO(markfreeman): Why hasMethods?
t.underlying = u
- t.allowNilUnderlying = false
t.setState(hasUnder)
}
// and complicating things there, we just check for that special case here.
if n.rhs() == nil {
assert(n.allowNilRHS)
- if n.allowNilUnderlying {
- return nil
- }
+ return nil
}
if !n.stateHas(hasUnder) { // minor performance optimization
var u Type
for rhs := Type(n); u == nil; {
switch t := rhs.(type) {
- case nil:
- u = Typ[Invalid]
-
case *Alias:
rhs = unalias(t)
path = append(path, t)
t.unpack()
- assert(t.rhs() != nil || t.allowNilRHS)
rhs = t.rhs()
+ assert(rhs != nil)
default:
u = rhs // any type literal or predeclared type works
return
}
- if hasVarSize(x.typ, nil) {
+ if check.hasVarSize(x.typ) {
x.mode = value
if check.recordTypes() {
check.recordBuiltinType(call.Fun, makeSig(Typ[Uintptr], x.typ))
// the part of the struct which is variable-sized. This makes both the rules
// simpler and also permits (or at least doesn't prevent) a compiler from re-
// arranging struct fields if it wanted to.
- if hasVarSize(base, nil) {
+ if check.hasVarSize(base) {
x.mode = value
if check.recordTypes() {
check.recordBuiltinType(call.Fun, makeSig(Typ[Uintptr], obj.Type()))
return
}
- if hasVarSize(x.typ, nil) {
+ if check.hasVarSize(x.typ) {
x.mode = value
if check.recordTypes() {
check.recordBuiltinType(call.Fun, makeSig(Typ[Uintptr], x.typ))
// hasVarSize reports if the size of type t is variable due to type parameters
// or if the type is infinitely-sized due to a cycle for which the type has not
// yet been checked.
-func hasVarSize(t Type, seen map[*Named]bool) (varSized bool) {
- // Cycles are only possible through *Named types.
- // The seen map is used to detect cycles and track
- // the results of previously seen types.
- if named := asNamed(t); named != nil {
- if v, ok := seen[named]; ok {
- return v
- }
- if seen == nil {
- seen = make(map[*Named]bool)
- }
- seen[named] = true // possibly cyclic until proven otherwise
- defer func() {
- seen[named] = varSized // record final determination for named
- }()
- }
+func (check *Checker) hasVarSize(t Type) bool {
+ // Note: We could use Underlying here, but passing through the RHS may yield
+ // better error messages.
+ switch t := Unalias(t).(type) {
+ case *Named:
+ if t.stateHas(hasFinite) {
+ // TODO(mark): Rename t.finite to t.varSize to avoid inversion.
+ return !t.finite
+ }
+
+ if i, ok := check.objPathIdx[t.obj]; ok {
+ cycle := check.objPath[i:]
+ check.cycleError(cycle, firstInSrc(cycle))
+ return true
+ }
+
+ check.push(t.obj)
+ defer check.pop()
+
+ varSize := check.hasVarSize(t.fromRHS)
+
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ // Careful, t.finite has lock-free readers. Since we might be racing
+ // another call to finiteSize, we have to avoid overwriting t.finite.
+ // Otherwise, the race detector will be tripped.
+ if !t.stateHas(hasFinite) {
+ t.finite = !varSize
+ t.setState(hasFinite)
+ }
+
+ return varSize
- switch u := t.Underlying().(type) {
case *Array:
- return hasVarSize(u.elem, seen)
+ // The array length is already computed. If it was a valid length, it
+ // is finite; else, an error was reported in the computation.
+ return check.hasVarSize(t.elem)
+
case *Struct:
- for _, f := range u.fields {
- if hasVarSize(f.typ, seen) {
+ for _, f := range t.fields {
+ if check.hasVarSize(f.typ) {
return true
}
}
- case *Interface:
- return isTypeParam(t)
- case *Named, *Union:
- panic("unreachable")
+
+ case *TypeParam:
+ return true
}
+
return false
}
}
T := x.typ
x.mode = invalid
+ // We cannot convert a value to an incomplete type; make sure it's complete.
+ if !check.isComplete(T) {
+ x.expr = call
+ return conversion
+ }
switch n := len(call.Args); n {
case 0:
check.errorf(inNode(call, call.Rparen), WrongArgCount, "missing argument in conversion to %s", T)
} else {
x.mode = value
}
- x.typ = sig.results.vars[0].typ // unpack tuple
+ typ := sig.results.vars[0].typ // unpack tuple
+ // We cannot return a value of an incomplete type; make sure it's complete.
+ if !check.isComplete(typ) {
+ x.mode = invalid
+ x.expr = call
+ return statement
+ }
+ x.typ = typ
default:
x.mode = value
x.typ = sig.results
goto Error
}
- // Avoid crashing when checking an invalid selector in a method declaration
- // (i.e., where def is not set):
+ // We cannot select on an incomplete type; make sure it's complete.
+ if !check.isComplete(x.typ) {
+ goto Error
+ }
+
+ // Avoid crashing when checking an invalid selector in a method declaration.
//
// type S[T any] struct{}
// type V = S[any]
// expecting a type expression, it is an error.
//
// See go.dev/issue/57522 for more details.
- //
- // TODO(rfindley): We should do better by refusing to check selectors in all cases where
- // x.typ is incomplete.
if wantType {
check.errorf(e.Sel, NotAType, "%s is not a type", ast.Expr(e))
goto Error
}
+ // Additionally, if x.typ is a pointer type, selecting implicitly dereferences the value, meaning
+ // its base type must also be complete.
+ if p, ok := x.typ.Underlying().(*Pointer); ok && !check.isComplete(p.base) {
+ goto Error
+ }
+
obj, index, indirect = lookupFieldOrMethod(x.typ, x.mode == variable, check.pkg, sel, false)
if obj == nil {
// Don't report another error if the underlying type was invalid (go.dev/issue/49541).
}
}
-// TODO(markfreeman): Can the value cached on Named be used in validType / hasVarSize?
-
-// finiteSize returns whether a type has finite size.
-func (check *Checker) finiteSize(t Type) bool {
- switch t := Unalias(t).(type) {
- case *Named:
- if t.stateHas(hasFinite) {
- return t.finite
- }
-
- if i, ok := check.objPathIdx[t.obj]; ok {
+// isComplete returns whether a type is complete (i.e. up to having an underlying type).
+// Incomplete types will panic if [Type.Underlying] is called on them.
+func (check *Checker) isComplete(t Type) bool {
+ if n, ok := Unalias(t).(*Named); ok {
+ if i, found := check.objPathIdx[n.obj]; found {
cycle := check.objPath[i:]
check.cycleError(cycle, firstInSrc(cycle))
return false
}
- check.push(t.obj)
- defer check.pop()
-
- isFinite := check.finiteSize(t.fromRHS)
-
- t.mu.Lock()
- defer t.mu.Unlock()
- // Careful, t.finite has lock-free readers. Since we might be racing
- // another call to finiteSize, we have to avoid overwriting t.finite.
- // Otherwise, the race detector will be tripped.
- if !t.stateHas(hasFinite) {
- t.finite = isFinite
- t.setState(hasFinite)
- }
-
- return isFinite
- case *Array:
- // The array length is already computed. If it was a valid length, it
- // is finite; else, an error was reported in the computation.
- return check.finiteSize(t.elem)
-
- case *Struct:
- for _, f := range t.fields {
- if !check.finiteSize(f.typ) {
- return false
- }
- }
+ // We must walk through names because we permit certain cycles of names.
+ // Consider:
+ //
+ // type A B
+ // type B [unsafe.Sizeof(A{})]int
+ //
+ // starting at B. At the site of A{}, A has no underlying type, and so a
+ // cycle must be reported.
+ return check.isComplete(n.fromRHS)
}
return true
}
named := check.newNamed(obj, nil, nil)
-
- // 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.unpack() will panic
- // type N A
- // type A = N // N.fromRHS is not set before N.unpack(), 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()
return
case token.ARROW:
- if elem := check.chanElem(x, x, true); elem != nil {
+ // We cannot receive a value with an incomplete type; make sure it's complete.
+ if elem := check.chanElem(x, x, true); elem != nil && check.isComplete(elem) {
x.mode = commaok
x.typ = elem
check.hasCallOrRecv = true
check.nonGeneric(T, x)
}
- // Here, x is a value, meaning it has a type. If that type is pending, then we have
- // a cycle. As an example:
- //
- // type T [unsafe.Sizeof(T{})]int
- //
- // has a cycle T->T which is deemed valid (by decl.go), but which is in fact invalid.
- check.pendingType(x)
check.record(x)
return kind
}
}
-// If x has a pending type (i.e. its declaring object is on the object path), pendingType
-// reports an error and invalidates x.mode and x.typ.
-// Otherwise it leaves x alone.
-func (check *Checker) pendingType(x *operand) {
- if x.mode == invalid || x.mode == novalue {
- return
- }
- if !check.finiteSize(x.typ) {
- x.mode = invalid
- x.typ = Typ[Invalid]
- }
-}
-
// exprInternal contains the core of type checking of expressions.
// Must only be called by rawExpr.
// (See rawExpr for an explanation of the parameters.)
if !isValid(T) {
goto Error
}
+ // We cannot assert to an incomplete type; make sure it's complete.
+ if !check.isComplete(T) {
+ goto Error
+ }
check.typeAssertion(e, x, T, false)
x.mode = commaok
x.typ = T
}) {
goto Error
}
+ // We cannot dereference a pointer with an incomplete base type; make sure it's complete.
+ if !check.isComplete(base) {
+ goto Error
+ }
x.mode = variable
x.typ = base
}
return false
}
+ // We cannot index on an incomplete type; make sure it's complete.
+ if !check.isComplete(x.typ) {
+ x.mode = invalid
+ return false
+ }
+ // Additionally, if x.typ is a pointer to an array type, indexing implicitly dereferences the value, meaning
+ // its base type must also be complete.
+ if p, ok := x.typ.Underlying().(*Pointer); ok && !check.isComplete(p.base) {
+ x.mode = invalid
+ return false
+ }
+
// ordinary index expression
valid := false
length := int64(-1) // valid if >= 0
}
}
+ // Note that we don't permit slice expressions where x is a type expression, so we don't check for that here.
+ // However, if x.typ is a pointer to an array type, slicing implicitly dereferences the value, meaning
+ // its base type must also be complete.
+ if p, ok := x.typ.Underlying().(*Pointer); ok && !check.isComplete(p.base) {
+ x.mode = invalid
+ return
+ }
+
valid := false
length := int64(-1) // valid if >= 0
switch u := cu.(type) {
base = typ
}
+ // We cannot create a literal of an incomplete type; make sure it's complete.
+ if !check.isComplete(base) {
+ x.mode = invalid
+ return
+ }
+
switch u, _ := commonUnder(base, nil); utyp := u.(type) {
case *Struct:
if len(e.Elts) == 0 {
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]
+ allowNilRHS bool // may be true from creation via [NewNamed] until [Named.SetUnderlying]
inst *instance // information for instantiated types; nil otherwise
n := (*Checker)(nil).newNamed(obj, underlying, methods)
if underlying == nil {
n.allowNilRHS = true
- n.allowNilUnderlying = true
} else {
n.SetUnderlying(underlying)
}
t.setState(lazyLoaded | unpacked | hasMethods) // TODO(markfreeman): Why hasMethods?
t.underlying = u
- t.allowNilUnderlying = false
t.setState(hasUnder)
}
// and complicating things there, we just check for that special case here.
if n.rhs() == nil {
assert(n.allowNilRHS)
- if n.allowNilUnderlying {
- return nil
- }
+ return nil
}
if !n.stateHas(hasUnder) { // minor performance optimization
var u Type
for rhs := Type(n); u == nil; {
switch t := rhs.(type) {
- case nil:
- u = Typ[Invalid]
-
case *Alias:
rhs = unalias(t)
path = append(path, t)
t.unpack()
- assert(t.rhs() != nil || t.allowNilRHS)
rhs = t.rhs()
+ assert(rhs != nil)
default:
u = rhs // any type literal or predeclared type works
_ = unsafe.Sizeof(struct{ T[P] }{})
}
-const _ = unsafe.Sizeof(T /* ERROR "invalid recursive type" */ [int]{})
+const _ = unsafe /* ERROR "not constant" */ .Sizeof(T /* ERROR "invalid recursive type" */ [int]{})
import "unsafe"
-type A /* ERROR "invalid recursive type" */ [unsafe.Sizeof(S{})]byte
+type A /* ERROR "invalid recursive type" */ [unsafe/* ERROR "must be constant" */.Sizeof(S{})]byte
type S struct {
a A
}
type D C
-type E /* ERROR "invalid recursive type" */ [unsafe.Sizeof(g[F]())]int
+type E /* ERROR "invalid recursive type" */ [unsafe/* ERROR "must be constant" */.Sizeof(g[F]())]int
func g[P any]() P {
panic(0)
}