diff --git a/src/go/types/NOTES b/src/go/types/NOTES index e90c97d85e..987d2eca20 100644 --- a/src/go/types/NOTES +++ b/src/go/types/NOTES @@ -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. diff --git a/src/go/types/check_test.go b/src/go/types/check_test.go index 3ed9708e63..eb894aa234 100644 --- a/src/go/types/check_test.go +++ b/src/go/types/check_test.go @@ -117,6 +117,7 @@ var tests = [][]string{ // Go 2 prototype examples {"examples/contracts.go2"}, {"examples/functions.go2"}, + {"examples/methods.go2"}, {"examples/types.go2"}, } diff --git a/src/go/types/examples/methods.go2 b/src/go/types/examples/methods.go2 new file mode 100644 index 0000000000..ab6c562907 --- /dev/null +++ b/src/go/types/examples/methods.go2 @@ -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))) {} diff --git a/src/go/types/testdata/tmp.go2 b/src/go/types/testdata/tmp.go2 index ab871cf2f7..7255712b55 100644 --- a/src/go/types/testdata/tmp.go2 +++ b/src/go/types/testdata/tmp.go2 @@ -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() {} diff --git a/src/go/types/typexpr.go b/src/go/types/typexpr.go index 27eead349f..3b633a203a 100644 --- a/src/go/types/typexpr.go +++ b/src/go/types/typexpr.go @@ -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