From d4338964b306205c4fc17972fc2501fa05d606b5 Mon Sep 17 00:00:00 2001 From: Robert Griesemer Date: Thu, 2 Jan 2020 15:02:15 -0800 Subject: [PATCH] go/types: implement contract embedding Also: - updated README - rebased on master Change-Id: Ieafcdd94460e02964350abc98651d0348d6521f9 --- src/go/types/NOTES | 3 +- src/go/types/README | 5 +- src/go/types/contracts.go | 88 ++++++++++++++++++----------- src/go/types/decl.go | 18 +++++- src/go/types/object.go | 6 -- src/go/types/subst.go | 8 +-- src/go/types/testdata/contracts.go2 | 56 +++++++++++++++--- src/go/types/testdata/linalg.go2 | 7 +-- src/go/types/testdata/tmp.go2 | 25 +++++++- src/go/types/typexpr.go | 11 +++- 10 files changed, 161 insertions(+), 66 deletions(-) diff --git a/src/go/types/NOTES b/src/go/types/NOTES index 3f2758c91a..e08636cb1a 100644 --- a/src/go/types/NOTES +++ b/src/go/types/NOTES @@ -5,7 +5,6 @@ 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) -- implement contract embedding - interface embedding doesn't take care of literal type constraints yet (need an allTypes list, like we have an allMethods list?) - type assertions on/against parameterized types @@ -55,3 +54,5 @@ DESIGN/IMPLEMENTATION the function block. The scope of type parameters starts at the 'type' keyword; the scope of ordinary parameters starts with the (opening '{' of the) function body. Both scopes end with the closing '}' 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. diff --git a/src/go/types/README b/src/go/types/README index 03a745b208..93f15b9f44 100644 --- a/src/go/types/README +++ b/src/go/types/README @@ -74,19 +74,18 @@ contracts are syntactic sugar that may improve readability. KNOWN ISSUES -- gofmt works only partly with parameterized code - importing of packages exporting generic code is not implemented - (and likely won't be implemented in this prototype) + (and won't be implemented in this prototype) - error messages are reasonable but expect them to be significantly better in a real implementation (the subscript numbers on type parameters are there to visually identify different parameters with the same name) -- contract embedding is not implemented yet - embedding of interfaces ignores (the notationally new) type lists in interfaces - various type-specific operations (such as indexing, sending a message, etc.) on expressions of a generic type don't work yet (but are relatively easy to implement, going forward) +- gofmt works only partly with parameterized code See also the NOTES file for a more up-to-date documentation of the current state and issues. diff --git a/src/go/types/contracts.go b/src/go/types/contracts.go index 63458a8f1b..2d83e10225 100644 --- a/src/go/types/contracts.go +++ b/src/go/types/contracts.go @@ -11,18 +11,9 @@ import ( "go/token" ) -type contractType struct{} - -func (contractType) String() string { return "" } -func (contractType) Underlying() Type { panic("unreachable") } - func (check *Checker) contractDecl(obj *Contract, cdecl *ast.ContractSpec) { assert(obj.typ == nil) - // contracts don't have types, but we need to set a type to - // detect recursive declarations and satisfy various assertions - obj.typ = new(contractType) - check.openScope(cdecl, "contract") defer check.closeScope() @@ -47,7 +38,7 @@ func (check *Checker) contractDecl(obj *Contract, cdecl *ast.ContractSpec) { pos := c.Param.Pos() tobj := check.scope.Lookup(c.Param.Name) if tobj == nil { - check.errorf(pos, "%s not declared by contract", c.Param.Name) + check.errorf(pos, "%s is not a type parameter declared by the contract", c.Param.Name) continue } if c.Types == nil { @@ -76,8 +67,7 @@ func (check *Checker) contractDecl(obj *Contract, cdecl *ast.ContractSpec) { } } - tpar := tobj.(*TypeName) - ifaceName := bounds[tpar.typ.(*TypeParam).index] + ifaceName := bounds[tobj.(*TypeName).typ.(*TypeParam).index] iface := ifaceName.underlying.(*Interface) switch nmethods { case 0: @@ -125,42 +115,68 @@ func (check *Checker) contractDecl(obj *Contract, cdecl *ast.ContractSpec) { econtr, _ := c.Types[0].(*ast.CallExpr) if econtr == nil { check.invalidAST(c.Types[0].Pos(), "invalid embedded contract %s", econtr) + continue } - // Handle contract lookup so we don't need to set up a special contract mode + + // Handle contract lookup here so we don't need to set up a special contract mode // for operands just to carry its information through in form of some contract Type. // TODO(gri) this code is also in collectTypeParams (decl.go) - factor out! if ident, ok := unparen(econtr.Fun).(*ast.Ident); ok { if eobj, _ := check.lookup(ident.Name).(*Contract); eobj != nil { - // TODO(gri) must set up contract if not yet done! + // set up contract if not yet done + if eobj.typ == nil { + check.objDecl(eobj, nil) + if eobj.typ == Typ[Invalid] { + continue // don't use contract + } + } // eobj is a valid contract - // TODO(gri) look for contract cycles! // contract arguments must match the embedded contract's parameters - if len(econtr.Args) != len(eobj.TParams) { - check.errorf(c.Types[0].Pos(), "%d type parameters but contract expects %d", len(econtr.Args), len(eobj.TParams)) + n := len(econtr.Args) + if n != len(eobj.TParams) { + check.errorf(c.Types[0].Pos(), "%d type parameters but contract expects %d", n, len(eobj.TParams)) continue } - // contract arguments must be type parameters - // For now, they must be from the enclosing contract. - // TODO(gri) can we allow any type parameter? - targs := make([]Type, len(econtr.Args)) - for i, arg := range econtr.Args { + + // contract arguments must be type parameters from the enclosing contract + var targs []Type + for _, arg := range econtr.Args { targ := check.typ(arg) if parg, _ := targ.(*TypeParam); parg != nil { - // TODO(gri) check that targ is a parameter from the enclosing contract - targs[i] = targ - } else { + // Contract declarations are only permitted at the package level, + // thus the only type parameters visible inside a contract are + // type parameters from the enclosing contract. + // The assertion below cannot fail unless we change the premise + // and permit contract declarations inside parameterized functions. + assert(tparams[parg.index] == parg.obj) + targs = append(targs, targ) + } else if targ != Typ[Invalid] { check.errorf(arg.Pos(), "%s is not a type parameter", arg) } } - // TODO(gri) - implement the steps below - // - for each eobj type parameter, determine its (interface) bound - // - substitute that type parameter with the actual type argument in that interface - // - add the interface as am embedded interface to the bound matching the actual type argument - // - tests! (incl. overlapping methods, etc.) - check.errorf(c.Types[0].Pos(), "%s: contract embedding not yet implemented", c.Types[0]) - continue + if len(targs) < n { + continue // some arguments were incorrect + } + + // instantiate each (embedded) contract bound with contract arguments + assert(n == len(eobj.Bounds)) + ebounds := make([]*Named, n) + for i, ebound := range eobj.Bounds { + ebounds[i] = check.instantiate(econtr.Args[i].Pos(), ebound, targs, nil).(*Named) + } + + // add the instantiated bounds as embedded interfaces to the respective + // embedding (outer) contract bound + for i, ebound := range ebounds { + index := targs[i].(*TypeParam).index + iface := bounds[index].underlying.(*Interface) + iface.embeddeds = append(iface.embeddeds, ebound) + check.posMap[iface] = append(check.posMap[iface], econtr.Pos()) // satisfy completeInterface requirements + } } + continue // success } + check.errorf(c.Types[0].Pos(), "%s is not a contract", c.Types[0]) } } @@ -170,10 +186,18 @@ func (check *Checker) contractDecl(obj *Contract, cdecl *ast.ContractSpec) { check.completeInterface(cdecl.Pos(), bound.underlying.(*Interface)) } + obj.typ = new(contractType) // mark contract as fully set up obj.TParams = tparams obj.Bounds = bounds } +// Contracts don't have types, but we need to set a type to +// detect recursive declarations and satisfy assertions. +type contractType struct{} + +func (contractType) String() string { return "" } +func (contractType) Underlying() Type { panic("unreachable") } + func (check *Checker) collectTypeConstraints(pos token.Pos, list []Type, types []ast.Expr) []Type { for _, texpr := range types { if texpr == nil { diff --git a/src/go/types/decl.go b/src/go/types/decl.go index 54a71834af..341ac0eca8 100644 --- a/src/go/types/decl.go +++ b/src/go/types/decl.go @@ -158,6 +158,12 @@ func (check *Checker) objDecl(obj Object, def *Named) { // initialize a variable with the function. } + case *Contract: + // TODO(gri) is there anything else we need to do here? + if check.cycle(obj) { + obj.typ = Typ[Invalid] + } + default: unreachable() } @@ -249,6 +255,8 @@ func (check *Checker) cycle(obj Object) (isCycle bool) { } case *Func: // ignored for now + case *Contract: + // TODO(gri) what do we need to do here, if anything? default: unreachable() } @@ -631,12 +639,18 @@ func (check *Checker) collectTypeParams(list *ast.FieldList) (tparams []*TypeNam // its information through in form of some contract Type. if ident, ok := unparen(f.Type).(*ast.Ident); ok { if obj, _ := check.lookup(ident.Name).(*Contract); obj != nil { + // set up contract if not yet done + if obj.typ == nil { + check.objDecl(obj, nil) + if obj.typ == Typ[Invalid] { + goto next // don't use contract + } + } if len(f.Names) != len(obj.TParams) { check.errorf(f.Type.Pos(), "%d type parameters but contract expects %d", len(f.Names), len(obj.TParams)) goto next } // obj is a valid contract - // TODO(gri) must set up contract if not yet done! // Use contract's matching type parameter bound and // instantiate it with the actual type parameters // (== targs) present. @@ -645,7 +659,7 @@ func (check *Checker) collectTypeParams(list *ast.FieldList) (tparams []*TypeNam targs[i] = tparam.typ } for i, name := range f.Names { - bound := obj.boundsAt(i) + bound := obj.Bounds[i] setBoundAt(index+i, check.instantiate(name.Pos(), bound, targs, nil)) } goto next diff --git a/src/go/types/object.go b/src/go/types/object.go index 8028982344..fbbe9025eb 100644 --- a/src/go/types/object.go +++ b/src/go/types/object.go @@ -332,12 +332,6 @@ func NewContract(pos token.Pos, pkg *Package, name string) *Contract { return &Contract{object{nil, pos, pkg, name, nil, 0, white, token.NoPos}, nil, nil} } -// boundsAt returns the (named) interface for the respective -// contract type parameter with the given index. -func (c *Contract) boundsAt(index int) *Named { - return c.Bounds[index] -} - // A Label represents a declared label. // Labels don't have a type. type Label struct { diff --git a/src/go/types/subst.go b/src/go/types/subst.go index 369b79e943..ab311bf3a8 100644 --- a/src/go/types/subst.go +++ b/src/go/types/subst.go @@ -228,13 +228,11 @@ func (subst *subster) typ(typ Type) Type { } case *Interface: - // for now ignore embeddeds - // TODO(gri) decide what to do - assert(len(t.embeddeds) == 0) methods, mcopied := subst.funcList(t.methods) + embeddeds, ecopied := subst.typeList(t.embeddeds) types, tcopied := subst.typeList(t.types) - if mcopied || tcopied { - iface := &Interface{methods: methods, types: types} + if mcopied || ecopied || tcopied { + iface := &Interface{methods: methods, embeddeds: embeddeds, types: types} iface.Complete() return iface } diff --git a/src/go/types/testdata/contracts.go2 b/src/go/types/testdata/contracts.go2 index 6b4195fc32..8fee9f8493 100644 --- a/src/go/types/testdata/contracts.go2 +++ b/src/go/types/testdata/contracts.go2 @@ -13,7 +13,7 @@ contract _(A, B, A /* ERROR A redeclared */ ){} // method constraints contract _(A) { A } /* ERROR expected type */ // TODO(gri) investigate this error! looks wrong contract _(A) { A m(); A add(A) int } -contract _(A) { B /* ERROR B not declared by contract */ m() } +contract _(A) { B /* ERROR B is not a type parameter declared by the contract */ m() } contract _(A) { A m(); A m /* ERROR duplicate method */ () } contract _(A) { A m() @@ -45,17 +45,59 @@ contract C(A) { A C /* ERROR use of contract */ } contract _(A) { A struct { f int } } contract _(A, B) { A B } -// embedded contracts -contract E() {} -contract _() { -// E /* ERROR embedding not yet implemented */ -} - // contract use in wrong places const _ E /* ERROR use of contract */ = 0 type _ E /* ERROR use of contract */ var _ E /* ERROR use of contract */ var _ = E /* ERROR use of contract */ +contract E() {} + +// -------------------------------------------------------------------------------------- +// embedded contracts + +contract E0() {} +contract _() { + E0() +} + +contract E1 /* ERROR cycle */ () { + E1() +} + +contract E2 /* ERROR cycle */ (a, b, c) { + E2(a, b) // no further errors here because of incorrect parameter count +} + +contract _(T) { + E3 /* ERROR 0 type parameters */ () + E3(T, int /* ERROR int is not a type parameter */) + E3(T, T) +} + +contract E3(A, B) { + A a() + B b() +} + +// E4 expects the methods T.a and T.b +contract E4(T) { + E3(T, T) +} + +func f(type T E4)() + +func _() { + f(myTa /* ERROR missing method */)() + f(myTab)() +} + +type myTa struct{} + +func (myTa) a() + +type myTab struct{ myTa } + +func (myTab) b() // -------------------------------------------------------------------------------------- // Contract satisfaction diff --git a/src/go/types/testdata/linalg.go2 b/src/go/types/testdata/linalg.go2 index 55c671a8ce..80ca046a48 100644 --- a/src/go/types/testdata/linalg.go2 +++ b/src/go/types/testdata/linalg.go2 @@ -28,12 +28,7 @@ func DotProduct(type T Numeric)(s1, s2 []T) T { // NumericAbs matches numeric types with an Abs method. contract NumericAbs(T) { - // TODO(gri) implement contract embedding - // Numeric(T) - T int; T int8; T int16; T int32; T int64 - T uint; T uint8; T uint16; T uint32; T uint64; T uintptr - T float32; T float64 - T complex64; T complex128 + Numeric(T) T Abs() T } diff --git a/src/go/types/testdata/tmp.go2 b/src/go/types/testdata/tmp.go2 index 5deb0e4605..64170db098 100644 --- a/src/go/types/testdata/tmp.go2 +++ b/src/go/types/testdata/tmp.go2 @@ -11,5 +11,28 @@ contract C1(a, b, c) { } contract C2(a, b) { - C1 /* ERROR contract embedding */ (a, b, a) + C1(a, b, b) } + +func f(type T, _ C2)(x T) + +type X struct{} + +func (X) a() +//func (X) b() +//func (X) c() + +func _() { + var x X + f(X, int)(x) +} + +contract C3(x) { + C4(x) +} + +contract C4(x) { +// C3(x) +} + +contract C(A) { A C /* ERROR use of contract */ } \ No newline at end of file diff --git a/src/go/types/typexpr.go b/src/go/types/typexpr.go index ec576bd639..6fd9d9d634 100644 --- a/src/go/types/typexpr.go +++ b/src/go/types/typexpr.go @@ -37,6 +37,13 @@ func (check *Checker) ident(x *operand, e *ast.Ident, def *Named, wantType bool) } check.recordUse(e, obj) + // If we have a contract, don't bother type-checking it and avoid a + // possible cycle error in favor of the more informative error below. + if obj, _ := obj.(*Contract); obj != nil { + check.errorf(e.Pos(), "use of contract %s not in type parameter declaration", obj.name) + return + } + // Type-check the object. // Only call Checker.objDecl if the object doesn't have a type yet // (in which case we must actually determine it) or the object is a @@ -99,9 +106,7 @@ func (check *Checker) ident(x *operand, e *ast.Ident, def *Named, wantType bool) x.mode = variable case *Contract: - // TODO(gri) need better error message here - check.errorf(e.Pos(), "use of contract %s not in type parameter declaration", obj.name) - return + unreachable() // handled earlier case *Func: check.addDeclDep(obj)