go/types: implement contract embedding

Also:
- updated README
- rebased on master

Change-Id: Ieafcdd94460e02964350abc98651d0348d6521f9
This commit is contained in:
Robert Griesemer 2020-01-02 15:02:15 -08:00
parent b6a8a0a86f
commit d4338964b3
10 changed files with 161 additions and 66 deletions

View File

@ -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.

View File

@ -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.

View File

@ -11,18 +11,9 @@ import (
"go/token"
)
type contractType struct{}
func (contractType) String() string { return "<dummy contract type>" }
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 "<dummy contract type>" }
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 {

View File

@ -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

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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 */ }

View File

@ -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)