go/types: support blank identifiers for unused receiver type parameters

Change-Id: I5d7d359d7453f7ed949a7cab7b85419f8f4ee2a9
This commit is contained in:
Robert Griesemer 2020-02-20 14:11:36 -08:00
parent 08e2d11638
commit f7bc9d382f
5 changed files with 202 additions and 16 deletions

View File

@ -10,6 +10,8 @@ TODO
- improve error messages!
- use []*TypeParam for tparams in subst? (unclear)
- should we use nil instead of &emptyInterface for no type bounds (as an optimization)?
- TBD: in prose, should we use "generic" or "parameterized" (try to be consistent)
- don't apply receiver type identifier substitution in place (typexpr.go)
----------------------------------------------------------------------------------------------------
KNOWN ISSUES
@ -18,8 +20,18 @@ KNOWN ISSUES
- cannot handle mutually recursive parameterized interfaces using themselves as type bounds
- contract instantiation requires the type arguments to be type parameters from the type of function
type parameter list or enclosing contract
- leaving away unused receiver type parameters leads to an error; e.g.: "type S(type T) struct{}; func (S) _()"
- using _ for an unused receiver type parameter leads to an error and crash; e.g.: "type T(type P) int; func (_ T(_)) m()"
- A type parameter that is constrained by multiple contracts will not get the correct type bound.
We may disallow this for now.
----------------------------------------------------------------------------------------------------
OBSERVATIONS
- Because we permit parenthesized types anywhere for consistency, also in parameter lists (mea culpa),
we have parsing ambiguities when using instantiated types in parameter lists w/o argument names.
We could disallow the use of parentheses at the top level of type literals and then we might not have
this problem. This is not a backward-compatible change but perhaps worthwhile investigating. Specifically,
will this always work (look specifically at channel types where we need parentheses for disambiguation
and possibly function types). File a proposal?
----------------------------------------------------------------------------------------------------
OPEN QUESTIONS
@ -97,3 +109,7 @@ DESIGN/IMPLEMENTATION
- 2/19/2020: Permit parentheses around embedded contracts for symmetry with embedding in structs
and interfaces.
- 2/20/2020: Receiver type parameters must always be provided in the receiver parameter list of
a method, even if they are not used by the method. Since the receiver acts like an implicit
declaration of those type parameters, they may be blank, as with any other declaration.

View File

@ -117,6 +117,7 @@ var tests = [][]string{
// Go 2 prototype examples
{"examples/contracts.go2"},
{"examples/functions.go2"},
{"examples/methods.go2"},
{"examples/types.go2"},
}

View File

@ -0,0 +1,111 @@
// Copyright 2019 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.
// This file shows some examples of methods on type-parameterized types.
package p
// Parameterized types may have methods.
type T1(type A) struct{ a A }
// When declaring a method for a parameterized type, the "instantiated"
// receiver type acts as an implicit declaration of the type parameters
// for the receiver type. In the example below, method m1 on type T1 has
// the receiver type T1(A) which declares the type parameter A for use
// with this method. That is, within the method m1, A stands for the
// actual type argument provided to an instantiated T1.
func (t T1(A)) m1() A { return t.a }
// For instance, if T1 is instantiated with the type int, the type
// parameter A in m1 assumes that type (int) as well and we can write
// code like this:
var x T1(int)
var _ int = x.m1()
// Because the type parameter provided to a parameterized receiver type
// is declared through that receiver declaration, it must be an identifier.
// It cannot possibly be some other type because the receiver type is not
// instantiated with concrete types, it is standing for the parameterized
// receiver type.
func (t T1([ /* ERROR must be an identifier */ ]int)) m2() {}
// Note that using what looks like a predeclared identifier, say int,
// as type parameter in this situation is deceptive and considered bad
// style. In m3 below, int is the name of the local receiver type parameter
// and it shadows the predeclared identifier int which then cannot be used
// anymore as expected.
func (t T1(int)) m3() { var _ int = 42 /* ERROR cannot convert 42 .* to int */ }
// The names of the type parameters used in a parameterized receiver
// type don't have to match the type parameter names in the the declaration
// of the type used for the receiver. In our example, even though T1 is
// declared with type parameter named A, methods using that receiver type
// are free to use their own name for that type parameter. That is, the
// name of type parameters is always local to the declaration where they
// are introduced. In our example we can write a method m2 and use the
// name X instead of A for the type parameter w/o any difference.
func (t T1(X)) m4() X { return t.a }
// If the receiver type is parameterized, type parameters must always be
// provided: this simply follows from the general rule that a parameterized
// type must be instantiated before it can be used. A method receiver
// declaration using a parameterized receiver type is no exception. It is
// simply that such receiver type expressions perform two tasks simultaneously:
// they declare the (local) type parameters and then use them to instantiate
// the receiver type. Forgetting to provide a type parameter leads to an error.
func (t T1 /* ERROR generic type .* without instantiation */ ) m5() {}
// However, sometimes we don't need the type parameter, and thus it is
// inconvenient to have to choose a name. Since the receiver type expression
// serves as a declaration for its type parameters, we are free to choose the
// blank identifier:
func (t T1(_)) m6() {}
// Naturally, these rules apply to any number of type parameters on the receiver
// type. Here are some more complex examples.
type T2(type A, B, C) struct {
a A
b B
c C
}
// Naming of the type parameters is local and has no semantic impact:
func (t T2(A, B, C)) m1() (A, B, C) { return t.a, t.b, t.c }
func (t T2(C, B, A)) m2() (C, B, A) { return t.a, t.b, t.c }
func (t T2(X, Y, Z)) m3() (X, Y, Z) { return t.a, t.b, t.c }
// Type parameters may be left blank if they are not needed:
func (t T2(A, _, C)) m4() (A, C) { return t.a, t.c }
func (t T2(_, _, X)) m5() X { return t.c }
func (t T2(_, _, _)) m6() {}
// As usual, blank names may be used for any object which we don't care about
// using later. For instance, we may write an unnamed method with a receiver
// that cannot be accessed:
func (_ T2(_, _, _)) _() int { return 42 }
// Because a receiver parameter list is simply a parameter list, we can
// leave the receiver argument away for non-parameterized receiver types.
type T0 struct{}
func (T0) m() {}
// This doesn't work for parameterized receiver types because there is
// a syntactic ambiguity: what looks like a parameterized reciver type
// T1(A) is parsed as a receiver argument named T1 followed by a
// parenthesized receiver type (A): (T1 (A)).
func (T1(A /* ERROR undeclared name: A */ )) _() {}
// The work-around is to either use blank identifier for the receiver
// argument or to parenthesize the receiver type:
func (_ T1(A)) _(a A) {}
func ((T1(A))) _(a A) {}
// Consequently, this problem appears in parameter lists in general:
func _(T1(A /* ERROR undeclared name: A */ )) {}
// And the workaround is the same as for receivers:
func _(_ T1(int)) {}
func _((T1(int))) {}
func _(type A)(_ T1(A)) {}
func _(type A)((T1(A))) {}

View File

@ -4,14 +4,7 @@
package p
/*
type T(type E) struct {
f *T(E)
}
type T(type a, b, c) struct {}
var _ T(int)
*/
func f(type T)(...T) T
var _ int = f() /* ERROR cannot infer T */
//func (_ T(_, T(_, _, _), _)) m() {}
func (_ T(_, int, _)) m() {}

View File

@ -163,6 +163,51 @@ func (check *Checker) genericType(e ast.Expr, reportErr bool) Type {
return typ
}
// isubsts returns an x with identifiers substituted per the substitution map smap.
// isubsts only handles the case of (valid) method receiver type expressions correctly.
func isubst(x ast.Expr, smap map[*ast.Ident]*ast.Ident) ast.Expr {
switch n := x.(type) {
case *ast.Ident:
if alt := smap[n]; alt != nil {
return alt
}
case *ast.StarExpr:
X := isubst(n.X, smap)
if X != n.X {
new := *n
n.X = X
return &new
}
case *ast.CallExpr:
var args []ast.Expr
for i, arg := range n.Args {
Arg := isubst(arg, smap)
if Arg != arg {
if args == nil {
args = make([]ast.Expr, len(n.Args))
copy(args, n.Args)
}
args[i] = Arg
}
}
if args != nil {
new := *n
new.Args = args
return &new
}
case *ast.ParenExpr:
X := isubst(n.X, smap)
if X != n.X {
return X // no need to recreate the parentheses
}
default:
// Any more complex receiver type expression is invalid.
// It's fine to ignore those here since we will report
// an error later.
}
return x
}
// funcType type-checks a function or method type.
func (check *Checker) funcType(sig *Signature, recvPar *ast.FieldList, ftyp *ast.FuncType) {
check.openScope(ftyp, "function")
@ -173,12 +218,32 @@ func (check *Checker) funcType(sig *Signature, recvPar *ast.FieldList, ftyp *ast
if recvPar != nil && len(recvPar.List) > 0 {
// collect parameterized receiver type parameters, if any
// - a receiver type parameter is like any other type parameter, except that it is passed implicitly (via the receiver)
// - the receiver specification acts as local declaration for its type parameters (which may be blank _)
// - if the receiver type is parameterized but we don't need the parameters, we permit leaving them away
// (TODO(gri) this is not working because the code doesn't allow an uninstantiated parameterized recv type)
// - a receiver type parameter is like any other type parameter, except that it is passed implicitly
// - the receiver specification acts as local declaration for its type parameters, which may be blank
_, rname, rparams := check.unpackRecv(recvPar.List[0].Type, true)
if len(rparams) > 0 {
// The receiver type parameters may not be used by the method, in which case they may be
// blank identifiers. But blank identifiers don't get declared and regular type-checking
// of the instantiated parameterized receiver type expression fails.
// Identify blank type parameters and substitute each with a unique new identifier named
// "!n" (where n is an increasing index) and which cannot conflict with any user-defined
// name; do the substitution both in the rparams and the receiver parameter list.
var smap map[*ast.Ident]*ast.Ident // substitution map from "_" to "!n" identifiers
for i, p := range rparams {
if p.Name == "_" {
new := *p
new.Name = fmt.Sprintf("%d_", i)
rparams[i] = &new
if smap == nil {
smap = make(map[*ast.Ident]*ast.Ident)
}
smap[p] = &new
}
}
if smap != nil {
// TODO(gri) should not do this in place
recvPar.List[0].Type = isubst(recvPar.List[0].Type, smap)
}
sig.rparams = check.declareTypeParams(nil, rparams)
// determine receiver type to get its type parameters
// and the respective type parameter bounds