From e3a0a7429a8f6a3f3848a784078cd3312823f899 Mon Sep 17 00:00:00 2001 From: Robert Griesemer Date: Mon, 6 Jan 2020 11:30:27 -0800 Subject: [PATCH] go/types: first steps towards parameterized methods This is a generalization of type parameters to methods. Being able to type-check them (even if we don't have an idea how to actually implement such methods) is likely going to inform the structure the type-checker: If the code is organized correctly, the generalization should mostly just work (and consist of the elimination of extra checks). So far, this first step only exposed some minor scoping issues (where does the scope of type parameters start). Change-Id: I8658ea8d1876a0ce9c62c0e9a7e943301e9cc19d --- src/go/types/NOTES | 15 ++++++++ src/go/types/check.go | 11 ++++-- src/go/types/decl.go | 2 +- src/go/types/operand.go | 23 +++++++++++-- src/go/types/resolver.go | 11 ++---- src/go/types/testdata/tmp.go2 | 8 +---- src/go/types/testdata/typeparams.go2 | 51 ++++++++++++++++++++++++++-- src/go/types/typexpr.go | 21 ++++++++---- 8 files changed, 111 insertions(+), 31 deletions(-) diff --git a/src/go/types/NOTES b/src/go/types/NOTES index a75cfb89bf..8fd6ffcc06 100644 --- a/src/go/types/NOTES +++ b/src/go/types/NOTES @@ -7,12 +7,17 @@ TODO - use Underlying() to return a type parameter's bound? investigate! - better error message when declaring a contract local to a function (parser gets out of sync) - if type parameters are repeated in recursive instantiation, they must be the same order (not yet checked) +- debug (and error msg) printing of generic instantiated types needs some work +- improve error messages! - use []*TypeParam for tparams in subst? (unclear) OPEN ISSUES - instantiating a parameterized function type w/o value or result parameters may have unexpected side-effects (we don't make a copy of the signature in some cases) - investigate - using a contract and enumerating type arguments currently leads to an error (e.g. func f(type T C(T)) (x T) ... ) +- leaving away unused receiver type parameters leads to an error; e.g.: "type S(type T) struct{}; func (S) _()" +- allow parenthesized embedded interfaces so we can embed parameterized interfaces and they won't be confused + for methods DESIGN/IMPLEMENTATION - 11/19/2019: For type parameters with interface bounds to work, the scope of all type parameters in @@ -54,3 +59,13 @@ DESIGN/IMPLEMENTATION of the function body (i.e., the end of the function block). - 1/2/2020: Implementation decision: contracts can only be declared at the package level. + +- 1/6/2020: Experimental: First steps towards permitting type parameters in methods as a generalization. + Type-checking problems ooccurring from this are likely to highlight general problematic areas. + First consequence: Scope of type parameters starts at "func" keyword which means that receiver type + name cannot be a type parameter name declared later (or by the receiver type specification). This + seems reasonable and should help avoid confusion which is possible otherwise. + +- 1/6/2020: Embedding a parameterized interface in an interface is not possible because it looks + like a method declaration. We probably need to find a away around this; maybe parenthesize the + embedded interface (which is currently not permitted, syntactically). diff --git a/src/go/types/check.go b/src/go/types/check.go index bac9aee300..77f191b8ec 100644 --- a/src/go/types/check.go +++ b/src/go/types/check.go @@ -17,12 +17,12 @@ import ( // debugging/development support const debug = true // leave on during development -// If Strict is set, the type-checker enforces additional +// If strict is set, the type-checker enforces additional // rules not specified by the Go 1 spec, but which will // catch guaranteed run-time errors if the respective // code is executed. In other words, programs passing in -// Strict mode are Go 1 compliant, but not all Go 1 programs -// will pass in Strict mode. The additional rules are: +// strict mode are Go 1 compliant, but not all Go 1 programs +// will pass in strict mode. The additional rules are: // // - A type assertion x.(T) where T is an interface type // is invalid if any (statically known) method that exists @@ -30,6 +30,11 @@ const debug = true // leave on during development // const strict = false +// If methodTypeParamsOk is set, type parameters are +// permitted in method declarations (in interfaces, too). +// Generalization and experimental feature. +const methodTypeParamsOk = true + // exprInfo stores information about an untyped expression. type exprInfo struct { isLhs bool // expression is lhs operand of a shift with delayed type-check diff --git a/src/go/types/decl.go b/src/go/types/decl.go index 341ac0eca8..a8d8bc0f0c 100644 --- a/src/go/types/decl.go +++ b/src/go/types/decl.go @@ -55,7 +55,7 @@ func pathString(path []Object) string { func (check *Checker) objDecl(obj Object, def *Named) { if check.conf.Trace && obj.Type() == nil { if check.indent == 0 { - fmt.Println() // empty line between top-lebvel objects for readability + fmt.Println() // empty line between top-level objects for readability } check.trace(obj.Pos(), "-- checking %s (%s, objPath = %s)", obj, obj.color(), pathString(check.objPath)) check.indent++ diff --git a/src/go/types/operand.go b/src/go/types/operand.go index 28fd040d08..da398d56f4 100644 --- a/src/go/types/operand.go +++ b/src/go/types/operand.go @@ -261,14 +261,33 @@ func (x *operand) assignableTo(check *Checker, T Type, reason *string) bool { // x's type V and T have identical underlying types // and at least one of V or T is not a named type - //check.dump("Vu = %s, Tu = %s, identical = %v", Vu, Tu, Identical(Vu, Tu)) if check.identical(Vu, Tu) && (!isNamed(V) || !isNamed(T)) { return true } // T is an interface type and x implements T if Ti, ok := Tu.(*Interface); ok { - if m, wrongType := check.missingMethod(V, Ti, true, nil); m != nil /* Implements(V, Ti) */ { + // update targ method signatures + // TODO(gri) This needs documentation and clean up! + update := func(V Type, sig *Signature) *Signature { + V, _ = deref(V) + pos := token.NoPos // TODO(gri) can we do better? + // check.dump(">>> %s: V = %s", pos, V) + var targs []Type + if vnamed, _ := V.(*Named); vnamed != nil { + targs = vnamed.targs + } else { + // nothing to do + return sig + } + // check.dump(">>> %s: targs = %s", pos, targs) + // check.dump(">>> %s: sig.rparams = %s", pos, sig.rparams) + // check.dump(">>> %s: sig before: %s, tparams = %s, targs = %s", pos, sig, sig.rparams, targs) + sig = check.subst(pos, sig, sig.rparams, targs).(*Signature) + // check.dump(">>> %s: sig after : %s", pos, sig) + return sig + } + if m, wrongType := check.missingMethod(V, Ti, true, update); m != nil /* Implements(V, Ti) */ { if reason != nil { if wrongType != nil { if check.identical(m.typ, wrongType.typ) { diff --git a/src/go/types/resolver.go b/src/go/types/resolver.go index 60b9b901a1..9c74fbf70d 100644 --- a/src/go/types/resolver.go +++ b/src/go/types/resolver.go @@ -435,16 +435,9 @@ func (check *Checker) collectObjects() { } else { // method // d.Recv != nil && len(d.Recv.List) > 0 - if d.TParams != nil { - // TODO(gri) should this be done in the parser (and this an invalidAST error)? - check.softErrorf(d.TParams.Pos(), "method must have no type parameters") + if !methodTypeParamsOk && d.TParams != nil { + check.invalidAST(d.TParams.Pos(), "method must have no type parameters") } - // 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 is effectively the declaration of that type parameter - // - if the receiver type is parameterized but we don't need the parameters, we permit leaving them away - // - this is effectively a declaration, and thus a receiver type parameter may be the blank identifier (_) - // - since methods cannot have other type parameters, we store receiver type parameters where function type parameters would be ptr, recv, _ := check.unpackRecv(d.Recv.List[0].Type, false) // (Methods with invalid receiver cannot be associated to a type, and // methods with blank _ names are never found; no need to collect any diff --git a/src/go/types/testdata/tmp.go2 b/src/go/types/testdata/tmp.go2 index 9f577750b4..71ec59b385 100644 --- a/src/go/types/testdata/tmp.go2 +++ b/src/go/types/testdata/tmp.go2 @@ -4,10 +4,4 @@ package p -type B1 interface{type int} - -type T3(type P B1) P - -type B2 interface{type int, string} - -func _(type P B2)(x T3(P /* ERROR missing type string */ )) \ No newline at end of file +type S(type T) struct{}; func (S) _() diff --git a/src/go/types/testdata/typeparams.go2 b/src/go/types/testdata/typeparams.go2 index 3d298ec53f..b069b24f62 100644 --- a/src/go/types/testdata/typeparams.go2 +++ b/src/go/types/testdata/typeparams.go2 @@ -117,7 +117,7 @@ var _ = f8(int, float64)(1, 2.3, 3.4, 4) var _ = f8(int, float64)(0, 0, nil...) // test case for #18268 -// init functions and methods cannot have type parameters +// init functions cannot have type parameters func init() {} func init(/* ERROR func init must have no type parameters */ type)() {} @@ -126,8 +126,9 @@ func init(/* ERROR func init must have no type parameters */ type P)() {} type T struct {} func (T) m1() {} -func (T) m2( /* ERROR method must have no type parameters */ type)() {} -func (T) m3( /* ERROR method must have no type parameters */ type P)() {} +// Experimental: We allow method type parameters. +// func (T) m2( /* ERROR method must have no type parameters */ type)() {} +// func (T) m3( /* ERROR method must have no type parameters */ type P)() {} // type inference across parameterized types @@ -184,3 +185,47 @@ func _(type T) (x T) { m := S1(T).m m(S1(T){x}) } + +// type parameters in methods (generalization) + +type R0 struct{} + +func (R0) _(type T)(x T) +func (R0 /* ERROR invalid receiver */ ) _(type R0)() // scope of type parameters starts at "func" + +type R1(type A, B) struct{} + +func (_ R1(A, B)) m0(A, B) +func (_ R1(A, B)) m1(type T)(A, B, T) T +func (_ R1 /* ERROR not a generic type */ (R1, _)) _() +func (_ R1(A, B)) _(type A /* ERROR redeclared */ )(B) + +func _() { + var r R1(int, string) + r.m1(rune)(42, "foo", 'a') + r.m1(rune)(42, "foo", 1.2 /* ERROR truncated to rune */) + r.m1(42, "foo", 1.2) // using type inferrence + var _ float64 = r.m1(42, "foo", 1.2) +} + +type I1(type A) interface { + m1(A) +} + +var _ I1(int) = r1(int){} + +type r1(type T) struct{} + +func (_ r1(T)) m1(T) + +type I2(type A, B) interface { + m1(A) + m2(A) B +} + +var _ I2(int, float32) = R2(int, float32){} + +type R2(type P, Q) struct{} + +func (_ R2(X, Y)) m1(X) +func (_ R2(X, Y)) m2(X) Y \ No newline at end of file diff --git a/src/go/types/typexpr.go b/src/go/types/typexpr.go index 6b741c9058..58c69a50ba 100644 --- a/src/go/types/typexpr.go +++ b/src/go/types/typexpr.go @@ -148,12 +148,14 @@ func (check *Checker) definedType(e ast.Expr, def *Named) Type { return typ } -// generic is like typ but the type must be an (uninstantiated) generic type. -func (check *Checker) genericType(e ast.Expr) Type { +// genericType is like typ but the type must be an (uninstantiated) generic type. +func (check *Checker) genericType(e ast.Expr, reportErr bool) Type { typ := check.typInternal(e, nil) assert(isTyped(typ)) if typ != Typ[Invalid] && !isGeneric(typ) { - check.errorf(e.Pos(), "%s is not a generic type", typ) + if reportErr { + check.errorf(e.Pos(), "%s is not a generic type", typ) + } typ = Typ[Invalid] } // TODO(gri) what is the correct call below? @@ -170,6 +172,10 @@ func (check *Checker) funcType(sig *Signature, recvPar *ast.FieldList, ftparams defer check.closeScope() 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 _, rname, rparams := check.unpackRecv(recvPar.List[0].Type, true) if len(rparams) > 0 { sig.rparams = check.declareTypeParams(nil, rparams, nil) @@ -178,7 +184,10 @@ func (check *Checker) funcType(sig *Signature, recvPar *ast.FieldList, ftparams var recvTParams []*TypeName if rname != nil { // recv should be a Named type (otherwise an error is reported elsewhere) - if recv, _ := check.genericType(rname).(*Named); recv != nil { + // Also: Don't report an error via genericType since it will be reported + // again when we type-check the signature. + // TODO(gri) maybe the receiver should be marked as invalid instead? + if recv, _ := check.genericType(rname, false).(*Named); recv != nil { recvTParams = recv.tparams } } @@ -210,7 +219,7 @@ func (check *Checker) funcType(sig *Signature, recvPar *ast.FieldList, ftparams } // Value (non-type) parameters' scope starts in the function body. Use a temporary scope for their - // declarations and the squash that scope into the parent scope (and report any redeclarations at + // declarations and then squash that scope into the parent scope (and report any redeclarations at // at that time). scope := NewScope(check.scope, token.NoPos, token.NoPos, "function body (temp. scope)") recvList, _ := check.collectParams(scope, recvPar, false) @@ -327,7 +336,7 @@ func (check *Checker) typInternal(e ast.Expr, def *Named) (T Type) { } case *ast.CallExpr: - typ := check.genericType(e.Fun) // TODO(gri) what about cycles? + typ := check.genericType(e.Fun, true) // TODO(gri) what about cycles? if typ == Typ[Invalid] { return typ // error already reported }