[dev.typeparams] go/types: merge instance and Named to eliminate sanitization

Storing temporary syntactic information using an *instance type forces
us to be careful not to leak references to *instance in the checker
output. This is complex and error prone, as types are written in many
places during type checking.

Instead, temporarily pin the necessary syntactic information directly to
the Named type during the type checking pass. This allows us to avoid
having to sanitize references.

This includes a couple of small, unrelated changes that were made in the
process of debugging:
 - eliminate the expandf indirection: it is no longer necessary
 - include type parameters when printing objects

For #46151

Change-Id: I767e35b289f2fea512a168997af0f861cd242175
Reviewed-on: https://go-review.googlesource.com/c/go/+/335929
Trust: Robert Findley <rfindley@google.com>
Trust: Robert Griesemer <gri@golang.org>
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Robert Griesemer <gri@golang.org>
This commit is contained in:
Rob Findley 2021-07-19 13:11:50 -04:00 committed by Robert Findley
parent 8e9109e95a
commit 61f69d2559
15 changed files with 96 additions and 314 deletions

View file

@ -273,10 +273,6 @@ func (check *Checker) checkFiles(files []*ast.File) (err error) {
check.recordUntyped() check.recordUntyped()
if check.Info != nil {
sanitizeInfo(check.Info)
}
check.pkg.complete = true check.pkg.complete = true
// no longer needed - release memory // no longer needed - release memory

View file

@ -317,6 +317,7 @@ func (check *Checker) validType(typ Type, path []Object) typeInfo {
} }
case *Named: case *Named:
t.complete()
// don't touch the type if it is from a different package or the Universe scope // don't touch the type if it is from a different package or the Universe scope
// (doing so would lead to a race condition - was issue #35049) // (doing so would lead to a race condition - was issue #35049)
if t.obj.pkg != check.pkg { if t.obj.pkg != check.pkg {
@ -349,9 +350,6 @@ func (check *Checker) validType(typ Type, path []Object) typeInfo {
panic("internal error: cycle start not found") panic("internal error: cycle start not found")
} }
return t.info return t.info
case *instance:
return check.validType(t.expand(), path)
} }
return valid return valid
@ -607,6 +605,7 @@ func (check *Checker) typeDecl(obj *TypeName, tdecl *ast.TypeSpec, def *Named) {
// determine underlying type of named // determine underlying type of named
named.fromRHS = check.definedType(tdecl.Type, named) named.fromRHS = check.definedType(tdecl.Type, named)
assert(named.fromRHS != nil)
// The underlying type of named may be itself a named type that is // The underlying type of named may be itself a named type that is
// incomplete: // incomplete:
@ -685,7 +684,8 @@ func (check *Checker) boundType(e ast.Expr) Type {
bound := check.typ(e) bound := check.typ(e)
check.later(func() { check.later(func() {
if _, ok := under(bound).(*Interface); !ok && bound != Typ[Invalid] { u := under(bound)
if _, ok := u.(*Interface); !ok && u != Typ[Invalid] {
check.errorf(e, _Todo, "%s is not an interface", bound) check.errorf(e, _Todo, "%s is not an interface", bound)
} }
}) })

View file

@ -337,9 +337,6 @@ func (w *tpWalker) isParameterized(typ Type) (res bool) {
// t must be one of w.tparams // t must be one of w.tparams
return t.index < len(w.tparams) && w.tparams[t.index].typ == t return t.index < len(w.tparams) && w.tparams[t.index].typ == t
case *instance:
return w.isParameterizedList(t.targs)
default: default:
unreachable() unreachable()
} }

View file

@ -4,56 +4,39 @@
package types package types
// TODO(rfindley): move this code to named.go.
import "go/token" import "go/token"
// An instance represents an instantiated generic type syntactically // instance holds a Checker along with syntactic information
// (without expanding the instantiation). Type instances appear only // information, for use in lazy instantiation.
// during type-checking and are replaced by their fully instantiated
// (expanded) types before the end of type-checking.
type instance struct { type instance struct {
check *Checker // for lazy instantiation check *Checker
pos token.Pos // position of type instantiation; for error reporting only pos token.Pos // position of type instantiation; for error reporting only
base *Named // parameterized type to be instantiated
targs []Type // type arguments
posList []token.Pos // position of each targ; for error reporting only posList []token.Pos // position of each targ; for error reporting only
verify bool // if set, constraint satisfaction is verified verify bool // if set, constraint satisfaction is verified
value Type // base[targs...] after instantiation or Typ[Invalid]; nil if not yet set
} }
// expand returns the instantiated (= expanded) type of t. // complete ensures that the underlying type of n is instantiated.
// The result is either an instantiated *Named type, or // The underlying type will be Typ[Invalid] if there was an error.
// Typ[Invalid] if there was an error. // TODO(rfindley): expand would be a better name for this method, but conflicts
func (t *instance) expand() Type { // with the existing concept of lazy expansion. Need to reconcile this.
v := t.value func (n *Named) complete() {
if v == nil { if n.instance != nil && len(n.targs) > 0 && n.underlying == nil {
v = t.check.Instantiate(t.pos, t.base, t.targs, t.posList, t.verify) check := n.instance.check
if v == nil { inst := check.instantiate(n.instance.pos, n.orig.underlying, n.tparams, n.targs, n.instance.posList, n.instance.verify)
v = Typ[Invalid] n.underlying = inst
} n.fromRHS = inst
t.value = v n.methods = n.orig.methods
} }
// After instantiation we must have an invalid or a *Named type.
if debug && v != Typ[Invalid] {
_ = v.(*Named)
}
return v
} }
// expand expands a type instance into its instantiated // expand expands a type instance into its instantiated
// type and leaves all other types alone. expand does // type and leaves all other types alone. expand does
// not recurse. // not recurse.
func expand(typ Type) Type { func expand(typ Type) Type {
if t, _ := typ.(*instance); t != nil { if t, _ := typ.(*Named); t != nil {
return t.expand() t.complete()
} }
return typ return typ
} }
// expandf is set to expand.
// Call expandf when calling expand causes compile-time cycle error.
var expandf func(Type) Type
func init() { expandf = expand }
func (t *instance) Underlying() Type { return t }
func (t *instance) String() string { return TypeString(t, nil) }

View file

@ -25,29 +25,6 @@ import (
// Any methods attached to a *Named are simply copied; they are not // Any methods attached to a *Named are simply copied; they are not
// instantiated. // instantiated.
func (check *Checker) Instantiate(pos token.Pos, typ Type, targs []Type, posList []token.Pos, verify bool) (res Type) { func (check *Checker) Instantiate(pos token.Pos, typ Type, targs []Type, posList []token.Pos, verify bool) (res Type) {
if verify && check == nil {
panic("cannot have nil receiver if verify is set")
}
if check != nil && trace {
check.trace(pos, "-- instantiating %s with %s", typ, typeListString(targs))
check.indent++
defer func() {
check.indent--
var under Type
if res != nil {
// Calling under() here may lead to endless instantiations.
// Test case: type T[P any] T[P]
// TODO(gri) investigate if that's a bug or to be expected.
under = res.Underlying()
}
check.trace(pos, "=> %s (under = %s)", res, under)
}()
}
assert(len(posList) <= len(targs))
// TODO(gri) What is better here: work with TypeParams, or work with TypeNames?
var tparams []*TypeName var tparams []*TypeName
switch t := typ.(type) { switch t := typ.(type) {
case *Named: case *Named:
@ -77,6 +54,10 @@ func (check *Checker) Instantiate(pos token.Pos, typ Type, targs []Type, posList
panic(fmt.Sprintf("%v: cannot instantiate %v", pos, typ)) panic(fmt.Sprintf("%v: cannot instantiate %v", pos, typ))
} }
return check.instantiate(pos, typ, tparams, targs, posList, verify)
}
func (check *Checker) instantiate(pos token.Pos, typ Type, tparams []*TypeName, targs []Type, posList []token.Pos, verify bool) (res Type) {
// the number of supplied types must match the number of type parameters // the number of supplied types must match the number of type parameters
if len(targs) != len(tparams) { if len(targs) != len(tparams) {
// TODO(gri) provide better error message // TODO(gri) provide better error message
@ -86,6 +67,29 @@ func (check *Checker) Instantiate(pos token.Pos, typ Type, targs []Type, posList
} }
panic(fmt.Sprintf("%v: got %d arguments but %d type parameters", pos, len(targs), len(tparams))) panic(fmt.Sprintf("%v: got %d arguments but %d type parameters", pos, len(targs), len(tparams)))
} }
if verify && check == nil {
panic("cannot have nil receiver if verify is set")
}
if check != nil && trace {
check.trace(pos, "-- instantiating %s with %s", typ, typeListString(targs))
check.indent++
defer func() {
check.indent--
var under Type
if res != nil {
// Calling under() here may lead to endless instantiations.
// Test case: type T[P any] T[P]
// TODO(gri) investigate if that's a bug or to be expected.
under = res.Underlying()
}
check.trace(pos, "=> %s (under = %s)", res, under)
}()
}
assert(len(posList) <= len(targs))
// TODO(gri) What is better here: work with TypeParams, or work with TypeNames?
if len(tparams) == 0 { if len(tparams) == 0 {
return typ // nothing to do (minor optimization) return typ // nothing to do (minor optimization)
@ -120,15 +124,26 @@ func (check *Checker) InstantiateLazy(pos token.Pos, typ Type, targs []Type, pos
if base == nil { if base == nil {
panic(fmt.Sprintf("%v: cannot instantiate %v", pos, typ)) panic(fmt.Sprintf("%v: cannot instantiate %v", pos, typ))
} }
h := instantiatedHash(base, targs)
if check != nil {
if named := check.typMap[h]; named != nil {
return named
}
}
return &instance{ tname := NewTypeName(pos, base.obj.pkg, base.obj.name, nil)
named := check.newNamed(tname, base, nil, base.tparams, base.methods) // methods are instantiated lazily
named.targs = targs
named.instance = &instance{
check: check, check: check,
pos: pos, pos: pos,
base: base,
targs: targs,
posList: posList, posList: posList,
verify: verify, verify: verify,
} }
if check != nil {
check.typMap[h] = named
}
return named
} }
// satisfies reports whether the type argument targ satisfies the constraint of type parameter // satisfies reports whether the type argument targ satisfies the constraint of type parameter

View file

@ -10,7 +10,7 @@ import "sync"
// A Named represents a named (defined) type. // A Named represents a named (defined) type.
type Named struct { type Named struct {
check *Checker // for Named.under implementation; nilled once under has been called instance *instance // syntactic information for lazy instantiation
info typeInfo // for cycle detection info typeInfo // for cycle detection
obj *TypeName // corresponding declared object obj *TypeName // corresponding declared object
orig *Named // original, uninstantiated type orig *Named // original, uninstantiated type
@ -65,7 +65,13 @@ func (t *Named) expand() *Named {
// newNamed is like NewNamed but with a *Checker receiver and additional orig argument. // newNamed is like NewNamed but with a *Checker receiver and additional orig argument.
func (check *Checker) newNamed(obj *TypeName, orig *Named, underlying Type, tparams []*TypeName, methods []*Func) *Named { func (check *Checker) newNamed(obj *TypeName, orig *Named, underlying Type, tparams []*TypeName, methods []*Func) *Named {
typ := &Named{check: check, obj: obj, orig: orig, fromRHS: underlying, underlying: underlying, tparams: tparams, methods: methods} var inst *instance
if check != nil {
inst = &instance{
check: check,
}
}
typ := &Named{instance: inst, obj: obj, orig: orig, fromRHS: underlying, underlying: underlying, tparams: tparams, methods: methods}
if typ.orig == nil { if typ.orig == nil {
typ.orig = typ typ.orig = typ
} }
@ -83,10 +89,10 @@ func (check *Checker) newNamed(obj *TypeName, orig *Named, underlying Type, tpar
if check != nil { if check != nil {
check.later(func() { check.later(func() {
switch typ.under().(type) { switch typ.under().(type) {
case *Named, *instance: case *Named:
panic("internal error: unexpanded underlying type") panic("internal error: unexpanded underlying type")
} }
typ.check = nil typ.instance = nil
}) })
} }
return typ return typ
@ -153,6 +159,8 @@ func (t *Named) String() string { return TypeString(t, nil) }
// is detected, the result is Typ[Invalid]. If a cycle is detected and // is detected, the result is Typ[Invalid]. If a cycle is detected and
// n0.check != nil, the cycle is reported. // n0.check != nil, the cycle is reported.
func (n0 *Named) under() Type { func (n0 *Named) under() Type {
n0.complete()
u := n0.Underlying() u := n0.Underlying()
if u == Typ[Invalid] { if u == Typ[Invalid] {
@ -168,17 +176,17 @@ func (n0 *Named) under() Type {
default: default:
// common case // common case
return u return u
case *Named, *instance: case *Named:
// handled below // handled below
} }
if n0.check == nil { if n0.instance == nil || n0.instance.check == nil {
panic("internal error: Named.check == nil but type is incomplete") panic("internal error: Named.check == nil but type is incomplete")
} }
// Invariant: after this point n0 as well as any named types in its // Invariant: after this point n0 as well as any named types in its
// underlying chain should be set up when this function exits. // underlying chain should be set up when this function exits.
check := n0.check check := n0.instance.check
// If we can't expand u at this point, it is invalid. // If we can't expand u at this point, it is invalid.
n := asNamed(u) n := asNamed(u)
@ -199,12 +207,8 @@ func (n0 *Named) under() Type {
var n1 *Named var n1 *Named
switch u1 := u.(type) { switch u1 := u.(type) {
case *Named: case *Named:
u1.complete()
n1 = u1 n1 = u1
case *instance:
n1, _ = u1.expand().(*Named)
if n1 == nil {
u = Typ[Invalid]
}
} }
if n1 == nil { if n1 == nil {
break // end of chain break // end of chain

View file

@ -429,6 +429,9 @@ func writeObject(buf *bytes.Buffer, obj Object, qf Qualifier) {
if _, ok := typ.(*Basic); ok { if _, ok := typ.(*Basic); ok {
return return
} }
if named, _ := typ.(*Named); named != nil && len(named.tparams) > 0 {
writeTParamList(buf, named.tparams, qf, nil)
}
if tname.IsAlias() { if tname.IsAlias() {
buf.WriteString(" =") buf.WriteString(" =")
} else { } else {

View file

@ -10,7 +10,7 @@ package types
// isNamed may be called with types that are not fully set up. // isNamed may be called with types that are not fully set up.
func isNamed(typ Type) bool { func isNamed(typ Type) bool {
switch typ.(type) { switch typ.(type) {
case *Basic, *Named, *TypeParam, *instance: case *Basic, *Named, *TypeParam:
return true return true
} }
return false return false
@ -159,8 +159,8 @@ func (p *ifacePair) identical(q *ifacePair) bool {
// For changes to this code the corresponding changes should be made to unifier.nify. // For changes to this code the corresponding changes should be made to unifier.nify.
func identical(x, y Type, cmpTags bool, p *ifacePair) bool { func identical(x, y Type, cmpTags bool, p *ifacePair) bool {
// types must be expanded for comparison // types must be expanded for comparison
x = expandf(x) x = expand(x)
y = expandf(y) y = expand(y)
if x == y { if x == y {
return true return true

View file

@ -1,206 +0,0 @@
// Copyright 2020 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 types
// sanitizeInfo walks the types contained in info to ensure that all instances
// are expanded.
//
// This includes some objects that may be shared across concurrent
// type-checking passes (such as those in the universe scope), so we are
// careful here not to write types that are already sanitized. This avoids a
// data race as any shared types should already be sanitized.
func sanitizeInfo(info *Info) {
var s sanitizer = make(map[Type]Type)
// Note: Some map entries are not references.
// If modified, they must be assigned back.
for e, tv := range info.Types {
if typ := s.typ(tv.Type); typ != tv.Type {
tv.Type = typ
info.Types[e] = tv
}
}
inferred := info.Inferred
for e, inf := range inferred {
changed := false
for i, targ := range inf.TArgs {
if typ := s.typ(targ); typ != targ {
inf.TArgs[i] = typ
changed = true
}
}
if typ := s.typ(inf.Sig); typ != inf.Sig {
inf.Sig = typ.(*Signature)
changed = true
}
if changed {
inferred[e] = inf
}
}
for _, obj := range info.Defs {
if obj != nil {
if typ := s.typ(obj.Type()); typ != obj.Type() {
obj.setType(typ)
}
}
}
for _, obj := range info.Uses {
if obj != nil {
if typ := s.typ(obj.Type()); typ != obj.Type() {
obj.setType(typ)
}
}
}
// TODO(gri) sanitize as needed
// - info.Implicits
// - info.Selections
// - info.Scopes
// - info.InitOrder
}
type sanitizer map[Type]Type
func (s sanitizer) typ(typ Type) Type {
if typ == nil {
return nil
}
if t, found := s[typ]; found {
return t
}
s[typ] = typ
switch t := typ.(type) {
case *Basic, *top:
// nothing to do
case *Array:
if elem := s.typ(t.elem); elem != t.elem {
t.elem = elem
}
case *Slice:
if elem := s.typ(t.elem); elem != t.elem {
t.elem = elem
}
case *Struct:
s.varList(t.fields)
case *Pointer:
if base := s.typ(t.base); base != t.base {
t.base = base
}
case *Tuple:
s.tuple(t)
case *Signature:
s.var_(t.recv)
s.tuple(t.params)
s.tuple(t.results)
case *Union:
s.typeList(t.types)
case *Interface:
s.funcList(t.methods)
s.typeList(t.embeddeds)
// TODO(gri) do we need to sanitize type sets?
tset := t.typeSet()
s.funcList(tset.methods)
if types := s.typ(tset.types); types != tset.types {
tset.types = types
}
case *Map:
if key := s.typ(t.key); key != t.key {
t.key = key
}
if elem := s.typ(t.elem); elem != t.elem {
t.elem = elem
}
case *Chan:
if elem := s.typ(t.elem); elem != t.elem {
t.elem = elem
}
case *Named:
if debug && t.check != nil {
panic("internal error: Named.check != nil")
}
t.expand()
if orig := s.typ(t.fromRHS); orig != t.fromRHS {
t.fromRHS = orig
}
if under := s.typ(t.underlying); under != t.underlying {
t.underlying = under
}
s.typeList(t.targs)
s.funcList(t.methods)
case *TypeParam:
if bound := s.typ(t.bound); bound != t.bound {
t.bound = bound
}
case *instance:
typ = t.expand()
s[t] = typ
default:
panic("unimplemented")
}
return typ
}
func (s sanitizer) var_(v *Var) {
if v != nil {
if typ := s.typ(v.typ); typ != v.typ {
v.typ = typ
}
}
}
func (s sanitizer) varList(list []*Var) {
for _, v := range list {
s.var_(v)
}
}
func (s sanitizer) tuple(t *Tuple) {
if t != nil {
s.varList(t.vars)
}
}
func (s sanitizer) func_(f *Func) {
if f != nil {
if typ := s.typ(f.typ); typ != f.typ {
f.typ = typ
}
}
}
func (s sanitizer) funcList(list []*Func) {
for _, f := range list {
s.func_(f)
}
}
func (s sanitizer) typeList(list []Type) {
for i, t := range list {
if typ := s.typ(t); typ != t {
list[i] = typ
}
}
}

View file

@ -32,7 +32,6 @@ func TestSizeof(t *testing.T) {
{Chan{}, 12, 24}, {Chan{}, 12, 24},
{Named{}, 84, 160}, {Named{}, 84, 160},
{TypeParam{}, 28, 48}, {TypeParam{}, 28, 48},
{instance{}, 48, 96},
{top{}, 0, 0}, {top{}, 0, 0},
// Objects // Objects

View file

@ -29,12 +29,7 @@ func makeSubstMap(tpars []*TypeName, targs []Type) *substMap {
assert(len(tpars) == len(targs)) assert(len(tpars) == len(targs))
proj := make(map[*TypeParam]Type, len(tpars)) proj := make(map[*TypeParam]Type, len(tpars))
for i, tpar := range tpars { for i, tpar := range tpars {
// We must expand type arguments otherwise *instance proj[tpar.typ.(*TypeParam)] = targs[i]
// types end up as components in composite types.
// TODO(gri) explain why this causes problems, if it does
targ := expand(targs[i]) // possibly nil
targs[i] = targ
proj[tpar.typ.(*TypeParam)] = targ
} }
return &substMap{targs, proj} return &substMap{targs, proj}
} }
@ -86,6 +81,7 @@ func (check *Checker) subst(pos token.Pos, typ Type, smap *substMap) Type {
// for recursive types (example: type T[P any] *T[P]). // for recursive types (example: type T[P any] *T[P]).
subst.typMap = make(map[string]*Named) subst.typMap = make(map[string]*Named)
} }
return subst.typ(typ) return subst.typ(typ)
} }
@ -248,10 +244,13 @@ func (subst *subster) typ(typ Type) Type {
named := subst.check.newNamed(tname, t, t.Underlying(), t.TParams(), t.methods) // method signatures are updated lazily named := subst.check.newNamed(tname, t, t.Underlying(), t.TParams(), t.methods) // method signatures are updated lazily
named.targs = newTargs named.targs = newTargs
subst.typMap[h] = named subst.typMap[h] = named
t.complete() // must happen after typMap update to avoid infinite recursion
// do the substitution // do the substitution
dump(">>> subst %s with %s (new: %s)", t.underlying, subst.smap, newTargs) dump(">>> subst %s with %s (new: %s)", t.underlying, subst.smap, newTargs)
named.underlying = subst.typOrNil(t.Underlying()) named.underlying = subst.typOrNil(t.Underlying())
dump(">>> underlying: %v", named.underlying)
assert(named.underlying != nil)
named.fromRHS = named.underlying // for cycle detection (Checker.validType) named.fromRHS = named.underlying // for cycle detection (Checker.validType)
return named return named
@ -259,10 +258,6 @@ func (subst *subster) typ(typ Type) Type {
case *TypeParam: case *TypeParam:
return subst.smap.lookup(t) return subst.smap.lookup(t)
case *instance:
// TODO(gri) can we avoid the expansion here and just substitute the type parameters?
return subst.typ(t.expand())
default: default:
panic("unimplemented") panic("unimplemented")
} }

View file

@ -81,8 +81,10 @@ func (u T2[U]) Add1() U {
return u.s + 1 return u.s + 1
} }
// TODO(rfindley): we should probably report an error here as well, not
// just when the type is first instantiated.
func NewT2[U any]() T2[U /* ERROR U has no type constraints */ ] { func NewT2[U any]() T2[U /* ERROR U has no type constraints */ ] {
return T2[U /* ERROR U has no type constraints */ ]{} return T2[U]{}
} }
func _() { func _() {

View file

@ -24,7 +24,8 @@ type TypeParam struct {
id uint64 // unique id, for debugging only id uint64 // unique id, for debugging only
obj *TypeName // corresponding type name obj *TypeName // corresponding type name
index int // type parameter index in source order, starting at 0 index int // type parameter index in source order, starting at 0
bound Type // *Named or *Interface; underlying type is always *Interface // TODO(rfindley): this could also be Typ[Invalid]. Verify that this is handled correctly.
bound Type // *Named or *Interface; underlying type is always *Interface
} }
// NewTypeParam returns a new TypeParam. bound can be nil (and set later). // NewTypeParam returns a new TypeParam. bound can be nil (and set later).

View file

@ -295,13 +295,6 @@ func writeType(buf *bytes.Buffer, typ Type, qf Qualifier, visited []Type) {
} }
buf.WriteString(s + subscript(t.id)) buf.WriteString(s + subscript(t.id))
case *instance:
buf.WriteByte(instanceMarker) // indicate "non-evaluated" syntactic instance
writeTypeName(buf, t.base.obj, qf)
buf.WriteByte('[')
writeTypeList(buf, t.targs, qf, visited)
buf.WriteByte(']')
case *top: case *top:
buf.WriteString("") buf.WriteString("")

View file

@ -436,7 +436,7 @@ func (check *Checker) instantiatedType(x ast.Expr, targsx []ast.Expr, def *Named
// make sure we check instantiation works at least once // make sure we check instantiation works at least once
// and that the resulting type is valid // and that the resulting type is valid
check.later(func() { check.later(func() {
t := typ.(*instance).expand() t := expand(typ)
check.validType(t, nil) check.validType(t, nil)
}) })