go/types, types2: instantiate generic alias types

For #46477.

Change-Id: Ifa47d3ff87f67c60fa25654e54194ca8b31ea5a2
Reviewed-on: https://go-review.googlesource.com/c/go/+/567617
Auto-Submit: Robert Griesemer <gri@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Robert Findley <rfindley@google.com>
Reviewed-by: Robert Griesemer <gri@google.com>
This commit is contained in:
Robert Griesemer 2024-02-27 17:12:00 -08:00 committed by Gopher Robot
parent ff2070d939
commit f294ddeb29
16 changed files with 224 additions and 68 deletions

View File

@ -4,7 +4,10 @@
package types2
import "fmt"
import (
"cmd/compile/internal/syntax"
"fmt"
)
// An Alias represents an alias type.
// Whether or not Alias types are created is controlled by the
@ -30,7 +33,10 @@ func NewAlias(obj *TypeName, rhs Type) *Alias {
return alias
}
func (a *Alias) Obj() *TypeName { return a.obj }
// Obj returns the type name for the declaration defining the alias type a.
// For instantiated types, this is same as the type name of the origin type.
func (a *Alias) Obj() *TypeName { return a.orig.obj }
func (a *Alias) String() string { return TypeString(a, nil) }
// Underlying returns the [underlying type] of the alias type a, which is the
@ -125,6 +131,20 @@ func (check *Checker) newAlias(obj *TypeName, rhs Type) *Alias {
return a
}
// newAliasInstance creates a new alias instance for the given origin and type
// arguments, recording pos as the position of its synthetic object (for error
// reporting).
func (check *Checker) newAliasInstance(pos syntax.Pos, orig *Alias, targs []Type, ctxt *Context) *Alias {
assert(len(targs) > 0)
obj := NewTypeName(pos, orig.obj.pkg, orig.obj.name, nil)
rhs := check.subst(pos, orig.fromRHS, makeSubstMap(orig.TypeParams().list(), targs), nil, ctxt)
res := check.newAlias(obj, rhs)
res.orig = orig
res.tparams = orig.tparams
res.targs = newTypeList(targs)
return res
}
func (a *Alias) cleanup() {
// Ensure a.actual is set before types are published,
// so Unalias is a pure "getter", not a "setter".

View File

@ -21,11 +21,13 @@ type genericType interface {
}
// Instantiate instantiates the type orig with the given type arguments targs.
// orig must be a *Named or a *Signature type. If there is no error, the
// resulting Type is an instantiated type of the same kind (either a *Named or
// a *Signature). Methods attached to a *Named type are also instantiated, and
// associated with a new *Func that has the same position as the original
// method, but nil function scope.
// orig must be an *Alias, *Named, or *Signature type. If there is no error,
// the resulting Type is an instantiated type of the same kind (*Alias, *Named
// or *Signature, respectively).
//
// Methods attached to a *Named type are also instantiated, and associated with
// a new *Func that has the same position as the original method, but nil function
// scope.
//
// If ctxt is non-nil, it may be used to de-duplicate the instance against
// previous instances with the same identity. As a special case, generic
@ -35,10 +37,10 @@ type genericType interface {
// not guarantee that identical instances are deduplicated in all cases.
//
// If validate is set, Instantiate verifies that the number of type arguments
// and parameters match, and that the type arguments satisfy their
// corresponding type constraints. If verification fails, the resulting error
// may wrap an *ArgumentError indicating which type argument did not satisfy
// its corresponding type parameter constraint, and why.
// and parameters match, and that the type arguments satisfy their respective
// type constraints. If verification fails, the resulting error may wrap an
// *ArgumentError indicating which type argument did not satisfy its type parameter
// constraint, and why.
//
// If validate is not set, Instantiate does not verify the type argument count
// or whether the type arguments satisfy their constraints. Instantiate is
@ -101,8 +103,9 @@ func (check *Checker) instance(pos syntax.Pos, orig genericType, targs []Type, e
hashes[i] = ctxt.instanceHash(orig, targs)
}
// If local is non-nil, updateContexts return the type recorded in
// local.
// Record the result in all contexts.
// Prefer to re-use existing types from expanding context, if it exists, to reduce
// the memory pinned by the Named type.
updateContexts := func(res Type) Type {
for i := len(ctxts) - 1; i >= 0; i-- {
res = ctxts[i].update(hashes[i], orig, targs, res)
@ -122,6 +125,21 @@ func (check *Checker) instance(pos syntax.Pos, orig genericType, targs []Type, e
case *Named:
res = check.newNamedInstance(pos, orig, targs, expanding) // substituted lazily
case *Alias:
// TODO(gri) is this correct?
assert(expanding == nil) // Alias instances cannot be reached from Named types
tparams := orig.TypeParams()
// TODO(gri) investigate if this is needed (type argument and parameter count seem to be correct here)
if !check.validateTArgLen(pos, orig.String(), tparams.Len(), len(targs)) {
return Typ[Invalid]
}
if tparams.Len() == 0 {
return orig // nothing to do (minor optimization)
}
return check.newAliasInstance(pos, orig, targs, ctxt)
case *Signature:
assert(expanding == nil) // function instances cannot be reached from Named types

View File

@ -137,6 +137,9 @@ func hasEmptyTypeset(t Type) bool {
// TODO(gri) should we include signatures or assert that they are not present?
func isGeneric(t Type) bool {
// A parameterized type is only generic if it doesn't have an instantiation already.
if alias, _ := t.(*Alias); alias != nil && alias.tparams != nil && alias.targs == nil {
return true
}
named := asNamed(t)
return named != nil && named.obj != nil && named.inst == nil && named.TypeParams().Len() > 0
}

View File

@ -96,17 +96,41 @@ func (subst *subster) typ(typ Type) Type {
// nothing to do
case *Alias:
rhs := subst.typ(t.fromRHS)
if rhs != t.fromRHS {
// This branch cannot be reached because the RHS of an alias
// may only contain type parameters of an enclosing function.
// Such function bodies are never "instantiated" and thus
// substitution is not called on locally declared alias types.
// TODO(gri) adjust once parameterized aliases are supported
panic("unreachable for unparameterized aliases")
// return subst.check.newAlias(t.obj, rhs)
// This code follows the code for *Named types closely.
// TODO(gri) try to factor better
orig := t.Origin()
n := orig.TypeParams().Len()
if n == 0 {
return t // type is not parameterized
}
// TODO(gri) do we need this for Alias types?
var newTArgs []Type
if t.TypeArgs().Len() != n {
return Typ[Invalid] // error reported elsewhere
}
// already instantiated
// For each (existing) type argument targ, determine if it needs
// to be substituted; i.e., if it is or contains a type parameter
// that has a type argument for it.
for i, targ := range t.TypeArgs().list() {
new_targ := subst.typ(targ)
if new_targ != targ {
if newTArgs == nil {
newTArgs = make([]Type, n)
copy(newTArgs, t.TypeArgs().list())
}
newTArgs[i] = new_targ
}
}
if newTArgs == nil {
return t // nothing to substitute
}
return subst.check.newAliasInstance(subst.pos, t.orig, newTArgs, subst.ctxt)
case *Array:
elem := subst.typOrNil(t.elem)
if elem != t.elem {

View File

@ -335,6 +335,10 @@ func (w *typeWriter) typ(typ Type) {
case *Alias:
w.typeName(t.obj)
if list := t.targs.list(); len(list) != 0 {
// instantiated type
w.typeList(list)
}
if w.ctxt != nil {
// TODO(gri) do we need to print the alias type name, too?
w.typ(Unalias(t.obj.typ))

View File

@ -453,6 +453,10 @@ func (check *Checker) instantiatedType(x syntax.Expr, xlist []syntax.Expr, def *
}()
}
defer func() {
setDefType(def, res)
}()
var cause string
gtyp := check.genericType(x, &cause)
if cause != "" {
@ -462,21 +466,23 @@ func (check *Checker) instantiatedType(x syntax.Expr, xlist []syntax.Expr, def *
return gtyp // error already reported
}
// evaluate arguments
targs := check.typeList(xlist)
if targs == nil {
return Typ[Invalid]
}
if orig, _ := gtyp.(*Alias); orig != nil {
return check.instance(x.Pos(), orig, targs, nil, check.context())
}
orig := asNamed(gtyp)
if orig == nil {
panic(fmt.Sprintf("%v: cannot instantiate %v", x.Pos(), gtyp))
}
// evaluate arguments
targs := check.typeList(xlist)
if targs == nil {
setDefType(def, Typ[Invalid]) // avoid errors later due to lazy instantiation
return Typ[Invalid]
}
// create the instance
inst := asNamed(check.instance(x.Pos(), orig, targs, nil, check.context()))
setDefType(def, inst)
// orig.tparams may not be set up, so we need to do expansion later.
check.later(func() {

View File

@ -36,7 +36,7 @@ func dddErrPos(call *syntax.CallExpr) *syntax.CallExpr {
return call
}
// argErrPos returns the node (poser) for reportign an invalid argument count.
// argErrPos returns the node (poser) for reporting an invalid argument count.
func argErrPos(call *syntax.CallExpr) *syntax.CallExpr { return call }
// ExprString returns a string representation of x.

View File

@ -7,7 +7,10 @@
package types
import "fmt"
import (
"fmt"
"go/token"
)
// An Alias represents an alias type.
// Whether or not Alias types are created is controlled by the
@ -33,7 +36,10 @@ func NewAlias(obj *TypeName, rhs Type) *Alias {
return alias
}
func (a *Alias) Obj() *TypeName { return a.obj }
// Obj returns the type name for the declaration defining the alias type a.
// For instantiated types, this is same as the type name of the origin type.
func (a *Alias) Obj() *TypeName { return a.orig.obj }
func (a *Alias) String() string { return TypeString(a, nil) }
// Underlying returns the [underlying type] of the alias type a, which is the
@ -128,6 +134,20 @@ func (check *Checker) newAlias(obj *TypeName, rhs Type) *Alias {
return a
}
// newAliasInstance creates a new alias instance for the given origin and type
// arguments, recording pos as the position of its synthetic object (for error
// reporting).
func (check *Checker) newAliasInstance(pos token.Pos, orig *Alias, targs []Type, ctxt *Context) *Alias {
assert(len(targs) > 0)
obj := NewTypeName(pos, orig.obj.pkg, orig.obj.name, nil)
rhs := check.subst(pos, orig.fromRHS, makeSubstMap(orig.TypeParams().list(), targs), nil, ctxt)
res := check.newAlias(obj, rhs)
res.orig = orig
res.tparams = orig.tparams
res.targs = newTypeList(targs)
return res
}
func (a *Alias) cleanup() {
// Ensure a.actual is set before types are published,
// so Unalias is a pure "getter", not a "setter".

View File

@ -100,7 +100,7 @@ func generate(t *testing.T, filename string, write bool) {
type action func(in *ast.File)
var filemap = map[string]action{
"alias.go": nil,
"alias.go": fixTokenPos,
"assignments.go": func(f *ast.File) {
renameImportPath(f, `"cmd/compile/internal/syntax"->"go/ast"`)
renameSelectorExprs(f, "syntax.Name->ast.Ident", "ident.Value->ident.Name", "ast.Pos->token.Pos") // must happen before renaming identifiers

View File

@ -24,11 +24,13 @@ type genericType interface {
}
// Instantiate instantiates the type orig with the given type arguments targs.
// orig must be a *Named or a *Signature type. If there is no error, the
// resulting Type is an instantiated type of the same kind (either a *Named or
// a *Signature). Methods attached to a *Named type are also instantiated, and
// associated with a new *Func that has the same position as the original
// method, but nil function scope.
// orig must be an *Alias, *Named, or *Signature type. If there is no error,
// the resulting Type is an instantiated type of the same kind (*Alias, *Named
// or *Signature, respectively).
//
// Methods attached to a *Named type are also instantiated, and associated with
// a new *Func that has the same position as the original method, but nil function
// scope.
//
// If ctxt is non-nil, it may be used to de-duplicate the instance against
// previous instances with the same identity. As a special case, generic
@ -38,10 +40,10 @@ type genericType interface {
// not guarantee that identical instances are deduplicated in all cases.
//
// If validate is set, Instantiate verifies that the number of type arguments
// and parameters match, and that the type arguments satisfy their
// corresponding type constraints. If verification fails, the resulting error
// may wrap an *ArgumentError indicating which type argument did not satisfy
// its corresponding type parameter constraint, and why.
// and parameters match, and that the type arguments satisfy their respective
// type constraints. If verification fails, the resulting error may wrap an
// *ArgumentError indicating which type argument did not satisfy its type parameter
// constraint, and why.
//
// If validate is not set, Instantiate does not verify the type argument count
// or whether the type arguments satisfy their constraints. Instantiate is
@ -104,8 +106,9 @@ func (check *Checker) instance(pos token.Pos, orig genericType, targs []Type, ex
hashes[i] = ctxt.instanceHash(orig, targs)
}
// If local is non-nil, updateContexts return the type recorded in
// local.
// Record the result in all contexts.
// Prefer to re-use existing types from expanding context, if it exists, to reduce
// the memory pinned by the Named type.
updateContexts := func(res Type) Type {
for i := len(ctxts) - 1; i >= 0; i-- {
res = ctxts[i].update(hashes[i], orig, targs, res)
@ -125,6 +128,21 @@ func (check *Checker) instance(pos token.Pos, orig genericType, targs []Type, ex
case *Named:
res = check.newNamedInstance(pos, orig, targs, expanding) // substituted lazily
case *Alias:
// TODO(gri) is this correct?
assert(expanding == nil) // Alias instances cannot be reached from Named types
tparams := orig.TypeParams()
// TODO(gri) investigate if this is needed (type argument and parameter count seem to be correct here)
if !check.validateTArgLen(pos, orig.String(), tparams.Len(), len(targs)) {
return Typ[Invalid]
}
if tparams.Len() == 0 {
return orig // nothing to do (minor optimization)
}
return check.newAliasInstance(pos, orig, targs, ctxt)
case *Signature:
assert(expanding == nil) // function instances cannot be reached from Named types

View File

@ -140,6 +140,9 @@ func hasEmptyTypeset(t Type) bool {
// TODO(gri) should we include signatures or assert that they are not present?
func isGeneric(t Type) bool {
// A parameterized type is only generic if it doesn't have an instantiation already.
if alias, _ := t.(*Alias); alias != nil && alias.tparams != nil && alias.targs == nil {
return true
}
named := asNamed(t)
return named != nil && named.obj != nil && named.inst == nil && named.TypeParams().Len() > 0
}

View File

@ -99,17 +99,41 @@ func (subst *subster) typ(typ Type) Type {
// nothing to do
case *Alias:
rhs := subst.typ(t.fromRHS)
if rhs != t.fromRHS {
// This branch cannot be reached because the RHS of an alias
// may only contain type parameters of an enclosing function.
// Such function bodies are never "instantiated" and thus
// substitution is not called on locally declared alias types.
// TODO(gri) adjust once parameterized aliases are supported
panic("unreachable for unparameterized aliases")
// return subst.check.newAlias(t.obj, rhs)
// This code follows the code for *Named types closely.
// TODO(gri) try to factor better
orig := t.Origin()
n := orig.TypeParams().Len()
if n == 0 {
return t // type is not parameterized
}
// TODO(gri) do we need this for Alias types?
var newTArgs []Type
if t.TypeArgs().Len() != n {
return Typ[Invalid] // error reported elsewhere
}
// already instantiated
// For each (existing) type argument targ, determine if it needs
// to be substituted; i.e., if it is or contains a type parameter
// that has a type argument for it.
for i, targ := range t.TypeArgs().list() {
new_targ := subst.typ(targ)
if new_targ != targ {
if newTArgs == nil {
newTArgs = make([]Type, n)
copy(newTArgs, t.TypeArgs().list())
}
newTArgs[i] = new_targ
}
}
if newTArgs == nil {
return t // nothing to substitute
}
return subst.check.newAliasInstance(subst.pos, t.orig, newTArgs, subst.ctxt)
case *Array:
elem := subst.typOrNil(t.elem)
if elem != t.elem {

View File

@ -338,6 +338,10 @@ func (w *typeWriter) typ(typ Type) {
case *Alias:
w.typeName(t.obj)
if list := t.targs.list(); len(list) != 0 {
// instantiated type
w.typeList(list)
}
if w.ctxt != nil {
// TODO(gri) do we need to print the alias type name, too?
w.typ(Unalias(t.obj.typ))

View File

@ -443,6 +443,10 @@ func (check *Checker) instantiatedType(ix *typeparams.IndexExpr, def *TypeName)
}()
}
defer func() {
setDefType(def, res)
}()
var cause string
gtyp := check.genericType(ix.X, &cause)
if cause != "" {
@ -452,21 +456,23 @@ func (check *Checker) instantiatedType(ix *typeparams.IndexExpr, def *TypeName)
return gtyp // error already reported
}
// evaluate arguments
targs := check.typeList(ix.Indices)
if targs == nil {
return Typ[Invalid]
}
if orig, _ := gtyp.(*Alias); orig != nil {
return check.instance(ix.Pos(), orig, targs, nil, check.context())
}
orig := asNamed(gtyp)
if orig == nil {
panic(fmt.Sprintf("%v: cannot instantiate %v", ix.Pos(), gtyp))
}
// evaluate arguments
targs := check.typeList(ix.Indices)
if targs == nil {
setDefType(def, Typ[Invalid]) // avoid errors later due to lazy instantiation
return Typ[Invalid]
}
// create the instance
inst := asNamed(check.instance(ix.Pos(), orig, targs, nil, check.context()))
setDefType(def, inst)
// orig.tparams may not be set up, so we need to do expansion later.
check.later(func() {

View File

@ -33,7 +33,7 @@ func hasDots(call *ast.CallExpr) bool { return call.Ellipsis.IsValid() }
// dddErrPos returns the positioner for reporting an invalid ... use in a call.
func dddErrPos(call *ast.CallExpr) positioner { return atPos(call.Ellipsis) }
// argErrPos returns positioner for reportign an invalid argument count.
// argErrPos returns positioner for reporting an invalid argument count.
func argErrPos(call *ast.CallExpr) positioner { return inNode(call, call.Rparen) }
// startPos returns the start position of node n.

View File

@ -28,14 +28,20 @@ type _[P any, Q int] = RHS[P, Q]
type _[P int | float64] = RHS[P, int]
type _[P, Q any] = RHS[P, Q /* ERROR "Q does not satisfy ~int" */]
// ----------------------------------------------------------------------------
// NOTE: The code below does now work yet.
// TODO: Implement this.
// A generic type alias may be used like any other generic type.
type A[P any] = RHS[P, int]
func _(a A /* ERROR "not a generic type" */ [string]) {
func _(a A[string]) {
a.p = "foo"
a.q = 42
}
// A generic alias may refer to another generic alias.
type B[P any] = A[P]
func _(a B[string]) {
a.p = "foo"
a.q = 42
// error messages print the instantiated alias type
a.r /* ERROR "a.r undefined (type B[string] has no field or method r)" */ = 0
}