go/types: more steps towards contract type checking

- moved type parameter collection out of resolver and into decl phase
- collect (meta-) type information (contracts) for type parameters
- first cut at checking contract satisfaction (methods only for now)

Change-Id: I46707969a172423738171aaea9d5282fb4b25a44
This commit is contained in:
Robert Griesemer 2019-08-08 13:18:31 -07:00
parent 590e4ea3f0
commit b87cfd9558
12 changed files with 194 additions and 96 deletions

View File

@ -2763,7 +2763,7 @@ func (p *parser) parseTypeSpec(doc *ast.CommentGroup, _ token.Token, _ int) ast.
lbrack := p.pos
p.next()
if p.tok == token.TYPE {
// parametrized type
// parameterized type
p.next()
p.openScope()
tparams := p.parseTypeParams(p.topScope)
@ -2789,7 +2789,7 @@ func (p *parser) parseTypeSpec(doc *ast.CommentGroup, _ token.Token, _ int) ast.
lparen := p.pos
p.next()
if p.tok == token.TYPE {
// parametrized type
// parameterized type
p.next()
p.openScope()
tparams := p.parseTypeParams(p.topScope)

View File

@ -15,21 +15,15 @@ import (
// won't exclude a contract where we only permit a type. Investigate.
func (check *Checker) contractType(contr *Contract, e *ast.ContractType) {
pos := e.Lbrace
if len(e.TParams) > 0 {
pos = e.TParams[0].Pos()
}
scope := NewScope(check.scope, pos, e.Rbrace, "contract type parameters")
check.scope = scope
check.openScope(e, "contract")
defer check.closeScope()
check.recordScope(e, scope)
// collect type parameters
tparams := make([]*TypeName, len(e.TParams))
for index, name := range e.TParams {
tpar := NewTypeName(name.Pos(), check.pkg, name.Name, nil)
NewTypeParam(tpar, index) // assigns type to tpar as a side-effect
check.declare(scope, name, tpar, scope.pos)
NewTypeParam(tpar, index, nil) // assigns type to tpar as a side-effect
check.declare(check.scope, name, tpar, check.scope.pos)
tparams[index] = tpar
}
@ -50,7 +44,7 @@ func (check *Checker) contractType(contr *Contract, e *ast.ContractType) {
if c.Param != nil {
// If a type name is present, it must be one of the contract's type parameters.
pos := c.Param.Pos()
obj := scope.Lookup(c.Param.Name)
obj := check.scope.Lookup(c.Param.Name)
if obj == nil {
check.errorf(pos, "%s not declared by contract", c.Param.Name)
continue
@ -159,13 +153,20 @@ func (check *Checker) contractType(contr *Contract, e *ast.ContractType) {
ifaces[tpar] = &emptyInterface
} else {
var mset objset
i := 0
for _, m := range iface.methods {
if m0 := mset.insert(m); m0 != nil {
// A method with the same name exists already.
check.errorf(m.Pos(), "method %s already declared", m.name)
check.reportAltDecl(m0)
} else {
// only keep unique methods
// TODO(gri) revisit this code - introduced to fix large rebase
iface.methods[i] = m
i++
}
}
iface.methods = iface.methods[:i]
sort.Sort(byUniqueMethodName(iface.methods))
iface.Complete()
}
@ -233,3 +234,44 @@ func (check *Checker) typeConstraint(typ Type, why *string) bool {
}
return true
}
// satisfyContract reports whether the given type arguments satisfy a contract.
// The contract may be nil, in which case it is always satisfied.
// The number of type arguments must match the number of contract type parameters.
// TODO(gri) missing: good error reporting
func (check *Checker) satisfyContract(contr *Contract, targs []Type) bool {
if contr == nil {
return true
}
assert(len(contr.TParams) == len(targs))
// A contract is represented by a list of interfaces, one for each
// contract type parameter. Each of those interfaces may be parameterized
// with any of the other contract type parameters.
// We need to verify that each type argument implements its respective
// contract interface, but only after substituting any contract type parameters
// in that interface with the respective type arguments.
//
// TODO(gri) This implementation strategy (and implementation) would be
// much more direct if we were to replace contracts simply with (parameterized)
// interfaces. All the existing machinery would simply fall into place.
for i, targ := range targs {
iface := contr.ifaceAt(i)
if iface == nil {
continue // no constraints
}
// If iface is parameterized, we need to replace the type parameters
// with the respective type arguments.
if isParameterized(iface) {
panic("unimplemented")
}
// targ must implement iface
if m, _ := check.missingMethod(targ, iface, true); m != nil {
return false
}
}
return true
}

View File

@ -546,14 +546,14 @@ func (check *Checker) typeDecl(obj *TypeName, tdecl *ast.TypeSpec, def *Named) {
check.validType(obj.typ, nil)
})
if obj.IsParameterized() {
assert(obj.scope != nil)
check.scope = obj.scope // push type parameter scope
}
if tdecl.Assign.IsValid() {
// type alias declaration
if tdecl.TParams != nil {
check.errorf(tdecl.TParams.Pos(), "type alias cannot be parameterized")
// continue but ignore type parameters
}
obj.typ = Typ[Invalid]
obj.typ = check.typ(tdecl.Type)
@ -564,6 +564,11 @@ func (check *Checker) typeDecl(obj *TypeName, tdecl *ast.TypeSpec, def *Named) {
def.setUnderlying(named)
obj.typ = named // make sure recursive type declarations terminate
if tdecl.TParams != nil {
check.openScope(tdecl, "type parameters")
obj.tparams = check.collectTypeParams(tdecl.TParams)
}
// determine underlying type of named
named.orig = check.definedType(tdecl.Type, named)
@ -582,17 +587,46 @@ func (check *Checker) typeDecl(obj *TypeName, tdecl *ast.TypeSpec, def *Named) {
// any forward chain.
named.underlying = check.underlying(named)
}
// this must happen before addMethodDecls - cannot use defer
// TODO(gri) consider refactoring this
if tdecl.TParams != nil {
check.closeScope()
}
// this must happen before addMethodDecls - cannot use defer
// TODO(gri) consider refactoring this
if obj.IsParameterized() {
check.closeScope()
}
check.addMethodDecls(obj)
}
func (check *Checker) collectTypeParams(list *ast.FieldList) (tparams []*TypeName) {
for _, f := range list.List {
var contr *Contract
if f.Type != nil {
typ := check.typ(f.Type)
if typ != Typ[Invalid] {
if contr, _ = typ.Underlying().(*Contract); contr != nil {
if len(f.Names) != len(contr.TParams) {
// TODO(gri) improve error message
check.errorf(f.Type.Pos(), "%d type parameters but contract expects %d", len(f.Names), len(contr.TParams))
contr = nil // cannot use this contract
}
} else {
check.errorf(f.Type.Pos(), "%s is not a contract", typ)
}
}
}
for _, name := range f.Names {
tpar := NewTypeName(name.Pos(), check.pkg, name.Name, nil)
NewTypeParam(tpar, len(tparams), contr) // assigns type to tpar as a side-effect
check.declare(check.scope, name, tpar, check.scope.pos) // TODO(gri) verify scope pos is correct
tparams = append(tparams, tpar)
}
}
return tparams
}
func (check *Checker) addMethodDecls(obj *TypeName) {
// get associated methods
// (Checker.collectObjects only collects methods with non-blank names;
@ -659,18 +693,18 @@ func (check *Checker) funcDecl(obj *Func, decl *declInfo) {
// func declarations cannot use iota
assert(check.iota == nil)
if obj.IsParameterized() {
assert(obj.scope != nil)
check.scope = obj.scope // push type parameter scope
fdecl := decl.fdecl
if fdecl.TParams != nil {
check.openScope(fdecl, "type parameters")
obj.tparams = check.collectTypeParams(fdecl.TParams)
}
sig := new(Signature)
obj.typ = sig // guard against cycles
fdecl := decl.fdecl
check.funcType(sig, fdecl.Recv, fdecl.Type)
sig.tparams = obj.tparams
if obj.IsParameterized() {
if fdecl.TParams != nil {
check.closeScope()
}
@ -795,9 +829,6 @@ func (check *Checker) declStmt(decl ast.Decl) {
case *ast.TypeSpec:
obj := NewTypeName(s.Name.Pos(), pkg, s.Name.Name, nil)
if s.TParams != nil {
obj.scope, obj.tparams = check.collectTypeParams(check.scope, s, s.TParams)
}
// spec: "The scope of a type identifier declared inside a function
// begins at the identifier in the TypeSpec and ends at the end of
// the innermost containing block."

View File

@ -123,12 +123,22 @@ func isParameterized(typ Type) bool {
}
case *Signature:
assert(t.tparams == nil) // TODO(gri) is this correct?
assert(t.recv == nil || !isParameterized(t.recv.typ)) // interface method receiver may not be nil
assert(t.tparams == nil) // TODO(gri) is this correct?
// TODO(gri) Rethink check below: contract interfaces
// have methods where the receiver is a contract type
// parameter, by design.
//assert(t.recv == nil || !isParameterized(t.recv.typ))
return isParameterized(t.params) || isParameterized(t.results)
case *Interface:
panic("unimplemented")
if t.allMethods == nil {
panic("incomplete method")
}
for _, m := range t.allMethods {
if isParameterized(m.typ) {
return true
}
}
case *Map:
return isParameterized(t.key) || isParameterized(t.elem)

View File

@ -213,7 +213,6 @@ func (*Const) isDependency() {} // a constant may be a dependency of an initiali
// A TypeName represents a name for a (defined or alias) type.
type TypeName struct {
object
scope *Scope // type parameter scope; or nil
tparams []*TypeName // type parameters from left to right; or nil
}
@ -225,7 +224,7 @@ type TypeName struct {
// argument for NewNamed, which will set the TypeName's type as a side-
// effect.
func NewTypeName(pos token.Pos, pkg *Package, name string, typ Type) *TypeName {
return &TypeName{object{nil, pos, pkg, name, typ, 0, colorFor(typ), token.NoPos}, nil, nil}
return &TypeName{object{nil, pos, pkg, name, typ, 0, colorFor(typ), token.NoPos}, nil}
}
// IsParameterized reports whether obj is a parametrized type.
@ -312,7 +311,6 @@ func (*Var) isDependency() {} // a variable may be a dependency of an initializa
type Func struct {
object
hasPtrRecv bool // only valid for methods that don't have a type yet
scope *Scope // type parameter scope; or nil
tparams []*TypeName // type parameters from left to right; or nil
}
@ -324,7 +322,7 @@ func NewFunc(pos token.Pos, pkg *Package, name string, sig *Signature) *Func {
if sig != nil {
typ = sig
}
return &Func{object{nil, pos, pkg, name, typ, 0, colorFor(typ), token.NoPos}, false, nil, nil}
return &Func{object{nil, pos, pkg, name, typ, 0, colorFor(typ), token.NoPos}, false, nil}
}
// FullName returns the package- or receiver-type-qualified name of

View File

@ -393,9 +393,6 @@ func (check *Checker) collectObjects() {
case *ast.TypeSpec:
obj := NewTypeName(s.Name.Pos(), pkg, s.Name.Name, nil)
if s.TParams != nil {
obj.scope, obj.tparams = check.collectTypeParams(pkg.scope, s, s.TParams)
}
check.declarePkgObj(s.Name, obj, &declInfo{file: fileScope, tdecl: s})
default:
@ -428,9 +425,6 @@ func (check *Checker) collectObjects() {
check.softErrorf(obj.pos, "missing function body")
}
} else {
if d.TParams != nil {
obj.scope, obj.tparams = check.collectTypeParams(pkg.scope, d, d.TParams)
}
check.declare(pkg.scope, d.Name, obj, token.NoPos)
}
} else {
@ -446,11 +440,12 @@ func (check *Checker) collectObjects() {
// - if the receiver type is parameterized but we don't need the parameters, we permit leaving them away
// - this is a 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
// TODO(gri) move this into decl phase? (like we did for type and func type parameters?)
ptr, recv, tparams := check.unpackRecv(d.Recv.List[0].Type)
if tparams != nil {
obj.scope = NewScope(pkg.scope, d.Pos(), d.End(), "receiver type parameters")
check.recordScope(d, obj.scope)
obj.tparams = check.declareTypeParams(obj.scope, tparams)
scope := NewScope(pkg.scope, d.Pos(), d.End(), "receiver type parameters")
check.recordScope(d, scope)
obj.tparams = check.declareTypeParams(scope, tparams)
}
// (Methods with invalid receiver cannot be associated to a type, and
@ -509,25 +504,11 @@ func (check *Checker) collectObjects() {
}
}
func (check *Checker) collectTypeParams(parent *Scope, node ast.Node, list *ast.FieldList) (scope *Scope, tparams []*TypeName) {
scope = NewScope(parent, node.Pos(), node.End(), "type parameters")
check.recordScope(node, scope)
var names []*ast.Ident
for _, f := range list.List {
for _, name := range f.Names {
names = append(names, name)
}
}
return scope, check.declareTypeParams(scope, names)
}
func (check *Checker) declareTypeParams(scope *Scope, list []*ast.Ident) []*TypeName {
tparams := make([]*TypeName, len(list))
for i, name := range list {
tpar := NewTypeName(name.Pos(), check.pkg, name.Name, nil)
NewTypeParam(tpar, i) // assigns type to tpar as a side-effect
NewTypeParam(tpar, i, nil) // assigns type to tpar as a side-effect
check.declare(scope, name, tpar, scope.pos)
tparams[i] = tpar
}

View File

@ -152,9 +152,9 @@ func (check *Checker) multipleDefaults(list []ast.Stmt) {
}
}
func (check *Checker) openScope(s ast.Stmt, comment string) {
scope := NewScope(check.scope, s.Pos(), s.End(), comment)
check.recordScope(s, scope)
func (check *Checker) openScope(node ast.Node, comment string) {
scope := NewScope(check.scope, node.Pos(), node.End(), comment)
check.recordScope(node, scope)
check.scope = scope
}

View File

@ -59,17 +59,45 @@ contract _() {
// --------------------------------------------------------------------------------------
// Contract implementation
// Type parameter type must be a contract.
type _(type T int /* ERROR not a contract */ ) struct{}
// The number of type parameters must match the number of contract parameters.
contract C0() {}
type _(type A C0 /* ERROR 1 type parameters */ ) struct{}
contract C2(A, B) {
A a()
}
type _(type A C2 /* ERROR 1 type parameters */ ) struct{}
type _(type A, B C2) struct{}
type _(type A, B, C C2 /* ERROR 3 type parameters */ ) struct{}
// Type instantiation must satisfy the contract.
type T1(type _, _ C2) struct{}
type A struct{}
func (A) a() {}
var _ T1 /* ERROR not satisfied */ (int, int)
var _ T1(A, int)
contract Stringer(T) {
T String() string
}
type List(type T Stringer) struct{
data T
link *List(T)
// TODO(gri) cannot handle this yet
//link *List(T)
}
var _ List(MyData)
var _ List(int) // TODO(gri) should get an error here: int doesn't implement the Stringer contract
var _ List /* ERROR not satisfied */ (int)
type MyData string

View File

@ -1,13 +1,10 @@
package p
func f(type T)([]*T) int
// Type instantiation must satisfy the contract.
type Stringer contract(T) {
T String() string
}
var _ = f(nil) // ERROR cannot infer
type List(type T Stringer) struct{}
func g(type T)(T) int
var _ = g(nil) // ERROR cannot infer
func h(type T)(T, T) int
var _ = h(nil /* ERROR cannot convert */, uintptr(1))
var _ List /* ERROR not satisfied */ (int)

View File

@ -17,17 +17,8 @@ type T2(type P) struct {
type List(type P) []P
type A1(type P) = P
type A2(type P) = struct {
f P
g int
}
type A3(type P) = struct {
f P
g myInt // myInt should still be in scope chain
}
// Alias type declarations cannot have parameters.
type A1( /* ERROR cannot be parameterized */ type P) = P /* ERROR undeclared */
// Parametrized type instantiations
@ -42,12 +33,6 @@ type _ T1 /* ERROR got 0 arguments but 1 type parameters */ ()
type _ T1(x /* ERROR not a type */ )
type _ T1 /* ERROR got 2 arguments but 1 type parameters */ (int, float32)
// TODO(gri) parameterized alias types don't work correctly
// var _ A1(int) = x
// var _ A1(float32) = x // ERROR cannot use x .* as float32
// var _ T2(*int) = A2(*int){}
// var _ T2(*int) = A2 /* ERROR cannot use */ (int){}
var _ T2(int) = T2(int){}
var _ List(int) = []int{1, 2, 3}

View File

@ -513,15 +513,26 @@ type Contract struct {
IFaces map[*TypeName]*Interface
}
// ifaceAt returns the interface matching for the respective
// contract type parameter with the given index. If c is nil
// the result is nil.
func (c *Contract) ifaceAt(index int) *Interface {
if c != nil {
return c.IFaces[c.TParams[index]]
}
return nil
}
// A TypeParam represents a type parameter type.
type TypeParam struct {
obj *TypeName
index int
contr *Contract // nil if no contract
}
// NewTypeParam returns a new TypeParam.
func NewTypeParam(obj *TypeName, index int) *TypeParam {
typ := &TypeParam{obj, index}
func NewTypeParam(obj *TypeName, index int, contr *Contract) *TypeParam {
typ := &TypeParam{obj, index, contr}
if obj.typ == nil {
obj.typ = typ
}

View File

@ -457,8 +457,6 @@ func (check *Checker) typeList(list []ast.Expr) []Type {
}
func (check *Checker) parameterizedType(typ *Parameterized, e *ast.CallExpr) bool {
// TODO(gri) This code cannot handle type aliases at the moment.
// Probably need to do the name lookup here.
t := check.typ(e.Fun)
if t == Typ[Invalid] {
return false // error already reported
@ -485,6 +483,23 @@ func (check *Checker) parameterizedType(typ *Parameterized, e *ast.CallExpr) boo
return false
}
// TODO(gri) quick hack - clean this up
// Also, it looks like contract should be part of the parameterized type,
// not its individual type parameters, at least as long as we only permit
// one contract. If we permit multiple contracts C1, C2 as in
//
// type _(type A, B C1, B C2, ...)
//
// the current approach may be the right one. The current approach also
// lends itself more easily to a design where we just use interfaces
// rather than contracts.
assert(len(tname.tparams) > 0)
contr := tname.tparams[0].typ.(*TypeParam).contr
if !check.satisfyContract(contr, args) {
// TODO(gri) need to put in some work for really good error messages here
check.errorf(e.Pos(), "contract for %s is not satisfied", tname)
}
// complete parameterized type
typ.tname = tname
typ.targs = args