pulumi/pkg/compiler/binder.go
joeduffy db80229899 Fix a few type binding mistakes
* Persue the default/optional checking if a property value == nil.

* Use the Interface() function to convert a reflect.Type to its underlying
  interface{} value.  This is required for typechecking to check out.

* Also, unrelated to the above, change type assertions to use nil rather than
  allocating real objects.  Although minimal, this incurs less GC pressure.
2016-12-09 13:12:57 -08:00

970 lines
33 KiB
Go

// Copyright 2016 Marapongo, Inc. All rights reserved.
package compiler
import (
"fmt"
"reflect"
"regexp"
"strings"
"unicode/utf8"
"github.com/golang/glog"
"github.com/marapongo/mu/pkg/ast"
"github.com/marapongo/mu/pkg/ast/conv"
"github.com/marapongo/mu/pkg/compiler/core"
"github.com/marapongo/mu/pkg/diag"
"github.com/marapongo/mu/pkg/errors"
"github.com/marapongo/mu/pkg/util"
)
// Binder annotates an existing parse tree with semantic information.
type Binder interface {
core.Phase
// PrepareStack prepares the AST for binding. It returns a list of all unresolved dependency references. These
// must be bound and supplied to the BindStack function as the deps argument.
PrepareStack(stack *ast.Stack) []ast.Ref
// BindStack takes an AST, and its set of dependencies, and binds all names inside, mutating it in place. It
// returns a full list of all dependency Stacks that this Stack depends upon (which must then be bound).
BindStack(stack *ast.Stack, deprefs ast.DependencyRefs) []*ast.Stack
// ValidateStack runs last, after all transitive dependencies have been bound, to perform last minute validation.
ValidateStack(stack *ast.Stack)
}
func NewBinder(c Compiler) Binder {
// Create a new binder and a new scope with an empty symbol table.
b := &binder{c: c}
b.PushScope()
return b
}
type binder struct {
c Compiler
scope *scope
}
func (b *binder) Diag() diag.Sink {
return b.c.Diag()
}
func (b *binder) PrepareStack(stack *ast.Stack) []ast.Ref {
glog.Infof("Preparing Mu Stack: %v", stack.Name)
if glog.V(2) {
defer glog.V(2).Infof("Preparing Mu Stack %v completed w/ %v warnings and %v errors",
stack.Name, b.Diag().Warnings(), b.Diag().Errors())
}
// Push a new scope for this binding pass.
b.PushScope()
// Now perform a phase1 walk of the tree, preparing it for subsequent binding. This must be done as a
// separate phase because we won't know what to stick into the symbol table until after this first walk.
phase := newBinderPreparePhase(b, stack)
v := core.NewInOrderVisitor(phase, nil)
v.VisitStack(stack)
// Return a set of dependency references that must be loaded before BindStack occurs.
return phase.deps
}
func (b *binder) BindStack(stack *ast.Stack, deprefs ast.DependencyRefs) []*ast.Stack {
glog.Infof("Binding Mu Stack: %v", stack.Name)
if glog.V(2) {
defer glog.V(2).Infof("Binding Mu Stack %v completed w/ %v warnings and %v errors",
stack.Name, b.Diag().Warnings(), b.Diag().Errors())
}
// Now perform a phase2 walk of the tree, completing the binding process. The 1st walk will have given
// us everything we need for a fully populated symbol table, so that type binding will resolve correctly.
phase := newBinderBindPhase(b, stack, deprefs)
v := core.NewInOrderVisitor(phase, nil)
v.VisitStack(stack)
return phase.deps
}
func (b *binder) ValidateStack(stack *ast.Stack) {
glog.Infof("Validating Mu Stack: %v", stack.Name)
if glog.V(2) {
defer glog.V(2).Infof("Validating Mu Stack %v completed w/ %v warnings and %v errors",
stack.Name, b.Diag().Warnings(), b.Diag().Errors())
}
// Restore the original scope after this binding pass.
defer b.PopScope()
// Now perform the final validation of the AST. There's nothing to return, it just may issue errors.
phase := newBinderValidatePhase(b)
v := core.NewInOrderVisitor(phase, nil)
v.VisitStack(stack)
}
// LookupService binds a name to a Service type.
func (b *binder) LookupService(nm ast.Name) (*ast.Service, bool) {
util.AssertM(b.scope != nil, "Unexpected empty binding scope during LookupService")
return b.scope.LookupService(nm)
}
// LookupStack binds a name to a Stack type.
func (b *binder) LookupStack(nm ast.Name) (*ast.Stack, bool) {
util.AssertM(b.scope != nil, "Unexpected empty binding scope during LookupStack")
return b.scope.LookupStack(nm)
}
// LookupUninstStack binds a name to a UninstStack type.
func (b *binder) LookupUninstStack(nm ast.Name) (*ast.UninstStack, bool) {
util.AssertM(b.scope != nil, "Unexpected empty binding scope during LookupUninstStack")
return b.scope.LookupUninstStack(nm)
}
// LookupSchema binds a name to a Schema type.
func (b *binder) LookupSchema(nm ast.Name) (*ast.Schema, bool) {
util.AssertM(b.scope != nil, "Unexpected empty binding scope during LookupSchema")
return b.scope.LookupSchema(nm)
}
// LookupSymbol binds a name to any kind of Symbol.
func (b *binder) LookupSymbol(nm ast.Name) (*Symbol, bool) {
util.AssertM(b.scope != nil, "Unexpected empty binding scope during LookupSymbol")
return b.scope.LookupSymbol(nm)
}
// RegisterSymbol registers a symbol with the given name; if it already exists, the function returns false.
func (b *binder) RegisterSymbol(sym *Symbol) bool {
util.AssertM(b.scope != nil, "Unexpected empty binding scope during RegisterSymbol")
return b.scope.RegisterSymbol(sym)
}
// PushScope creates a new scope with an empty symbol table parented to the existing one.
func (b *binder) PushScope() {
b.scope = &scope{parent: b.scope, symtbl: make(map[ast.Name]*Symbol)}
}
// PopScope replaces the current scope with its parent.
func (b *binder) PopScope() {
util.AssertM(b.scope != nil, "Unexpected empty binding scope during pop")
b.scope = b.scope.parent
}
// scope enables lookups and symbols to obey traditional language scoping rules.
type scope struct {
parent *scope
symtbl map[ast.Name]*Symbol
}
// LookupService binds a name to a Service type.
func (s *scope) LookupService(nm ast.Name) (*ast.Service, bool) {
sym, exists := s.LookupSymbol(nm)
if exists && sym.Kind == SymKindService {
return sym.Real.(*ast.Service), true
}
// TODO: we probably need to issue an error for this condition (wrong expected symbol type).
return nil, false
}
// LookupStack binds a name to a Stack type.
func (s *scope) LookupStack(nm ast.Name) (*ast.Stack, bool) {
sym, exists := s.LookupSymbol(nm)
if exists && sym.Kind == SymKindStack {
return sym.Real.(*ast.Stack), true
}
// TODO: we probably need to issue an error for this condition (wrong expected symbol type).
return nil, false
}
// LookupUninstStack binds a name to a UninstStack type.
func (s *scope) LookupUninstStack(nm ast.Name) (*ast.UninstStack, bool) {
sym, exists := s.LookupSymbol(nm)
if exists && sym.Kind == SymKindUninstStack {
return sym.Real.(*ast.UninstStack), true
}
// TODO: we probably need to issue an error for this condition (wrong expected symbol type).
return nil, false
}
// LookupSchema binds a name to a Schema type.
func (s *scope) LookupSchema(nm ast.Name) (*ast.Schema, bool) {
sym, exists := s.LookupSymbol(nm)
if exists && sym.Kind == SymKindSchema {
return sym.Real.(*ast.Schema), true
}
// TODO: we probably need to issue an error for this condition (wrong expected symbol type).
return nil, false
}
// LookupSymbol binds a name to any kind of Symbol.
func (s *scope) LookupSymbol(nm ast.Name) (*Symbol, bool) {
for s != nil {
if sym, exists := s.symtbl[nm]; exists {
return sym, true
}
s = s.parent
}
return nil, false
}
// RegisterSymbol registers a symbol with the given name; if it already exists, the function returns false.
func (s *scope) RegisterSymbol(sym *Symbol) bool {
nm := sym.Name
if _, exists := s.symtbl[nm]; exists {
// TODO: this won't catch "shadowing" for parent scopes; do we care about this?
return false
}
s.symtbl[nm] = sym
return true
}
type binderPreparePhase struct {
b *binder
top *ast.Stack
deps []ast.Ref
depsm map[ast.Ref]bool
}
var _ core.Visitor = (*binderPreparePhase)(nil) // compile-time assertion that the binder implements core.Visitor.
func newBinderPreparePhase(b *binder, top *ast.Stack) *binderPreparePhase {
return &binderPreparePhase{
b: b,
top: top,
deps: make([]ast.Ref, 0),
depsm: make(map[ast.Ref]bool),
}
}
func (p *binderPreparePhase) Diag() diag.Sink {
return p.b.Diag()
}
func (p *binderPreparePhase) VisitWorkspace(workspace *ast.Workspace) {
}
func (p *binderPreparePhase) VisitCluster(name string, cluster *ast.Cluster) {
}
func (p *binderPreparePhase) VisitDependency(parent *ast.Workspace, ref ast.Ref, dep *ast.Dependency) {
// Workspace dependencies must use legal version specs; validate that this parses now so that we can use it
// later on without needing to worry about additional validation.
_, err := ref.Parse()
if err != nil {
p.Diag().Errorf(errors.ErrorIllegalNameLikeSyntax.At(parent), ref, err)
}
}
func (p *binderPreparePhase) VisitStack(stack *ast.Stack) {
// If the stack has a base type, we must add it as a bound dependency.
if stack.Base != "" {
p.registerDependency(stack, stack.Base)
}
// Stack names are required.
if stack.Name == "" {
p.Diag().Errorf(errors.ErrorMissingStackName.At(stack))
}
// Stack versions must be valid semantic versions (and specifically, not ranges). In other words, we need
// a concrete version number like "1.3.9-beta2" and *not* a range like ">1.3.9".
// TODO: should we require a version number?
if stack.Version != "" {
if err := stack.Version.Check(); err != nil {
p.Diag().Errorf(errors.ErrorIllegalStackVersion.At(stack), stack.Version, err)
}
}
}
func (p *binderPreparePhase) VisitSchemas(parent *ast.Stack, schemas *ast.Schemas) {
}
func (p *binderPreparePhase) VisitSchema(pstack *ast.Stack, parent *ast.Schemas, name ast.Name,
public bool, schema *ast.Schema) {
// If the schema has an unresolved base type, add it as a bound dependency.
if schema.BoundBase != nil && schema.BoundBase.IsUnresolvedRef() {
p.registerDependency(pstack, *schema.BoundBase.Unref)
}
// Add this schema to the symbol table so that this stack can reference it.
sym := NewSchemaSymbol(schema.Name, schema)
if !p.b.RegisterSymbol(sym) {
p.Diag().Errorf(errors.ErrorSymbolAlreadyExists.At(pstack), sym.Name)
}
}
func (p *binderPreparePhase) VisitProperty(parent *ast.Stack, schema *ast.Schema, name string, prop *ast.Property) {
// For properties whose types represent stack types, register them as a dependency.
if prop.BoundType.IsUnresolvedRef() {
p.registerDependency(parent, *prop.BoundType.Unref)
}
}
func (p *binderPreparePhase) VisitServices(parent *ast.Stack, svcs *ast.Services) {
}
func (p *binderPreparePhase) VisitService(pstack *ast.Stack, parent *ast.Services, name ast.Name,
public bool, svc *ast.Service) {
// Each service has a type. There are two forms of specifying a type, and this phase will normalize this to a
// single canonical form to simplify subsequent phases. First, there is a shorthand form:
//
// private:
// acmecorp/db:
// ...
//
// In this example, "acmecorp/db" is the type and the name is shortened to just "db". Second, there is a longhand
// form for people who want more control over the naming of their services:
//
// private:
// customers:
// type: acmecorp/db
// ...
//
// In this example, "acmecorp/db" is still the type, however the name is given the nicer name of "customers."
simplify := false
if svc.Type == "" {
svc.Type = ast.Ref(svc.Name)
simplify = true
}
// Remember this service's type as a stack that must be bound later on.
ty, ok := p.registerDependency(pstack, svc.Type)
if !ok {
return
}
// If we used the simple form, we must now propagate the friendly name over to the service's name.
if simplify {
svc.Name = ty.Name.Simple()
}
// Add this service to the symbol table so that other service definitions can refer to it by name.
sym := NewServiceSymbol(svc.Name, svc)
if !p.b.RegisterSymbol(sym) {
p.Diag().Errorf(errors.ErrorSymbolAlreadyExists.At(pstack), sym.Name)
}
}
// registerDependency adds a dependency that needs to be resolved/bound before phase 2 occurs.
func (p *binderPreparePhase) registerDependency(stack *ast.Stack, ref ast.Ref) (ast.RefParts, bool) {
ty, err := ref.Parse()
if err == nil {
// First see if this resolves to a stack. If it does, it's already in scope; nothing more to do.
nm := ty.Name
if _, exists := p.b.LookupStack(nm); !exists {
// Otherwise, we need to track this as a dependency to resolve. Make sure to canonicalize the key so that
// we don't end up with duplicate semantically equivalent dependency references.
key := ty.Defaults().Ref()
if _, exist := p.depsm[key]; !exist {
// Store these in an array so that the order is deterministic. But use a map to avoid duplicates.
p.deps = append(p.deps, key)
p.depsm[key] = true
}
}
return ty, true
}
p.Diag().Errorf(errors.ErrorIllegalNameLikeSyntax.At(stack), ref, err)
return ty, false
}
type binderBindPhase struct {
b *binder
top *ast.Stack // the top-most stack being bound.
deps []*ast.Stack // a set of dependencies instantiated during this binding phase.
}
var _ core.Visitor = (*binderBindPhase)(nil) // compile-time assertion that the binder implements core.Visitor.
func newBinderBindPhase(b *binder, top *ast.Stack, deprefs ast.DependencyRefs) *binderBindPhase {
p := &binderBindPhase{b: b, top: top}
// Populate the symbol table with this Stack's bound dependencies so that any type lookups are found.
for _, ref := range ast.StableDependencyRefs(deprefs) {
dep := deprefs[ref]
util.Assert(dep.Doc != nil)
nm := refToName(ref)
sym := NewUninstStackSymbol(nm, dep)
if !p.b.RegisterSymbol(sym) {
p.Diag().Errorf(errors.ErrorSymbolAlreadyExists.At(dep.Doc), nm)
}
}
return p
}
func (p *binderBindPhase) Diag() diag.Sink {
return p.b.Diag()
}
func (p *binderBindPhase) VisitWorkspace(workspace *ast.Workspace) {
}
func (p *binderBindPhase) VisitCluster(name string, cluster *ast.Cluster) {
}
func (p *binderBindPhase) VisitDependency(parent *ast.Workspace, ref ast.Ref, dep *ast.Dependency) {
}
func (p *binderBindPhase) VisitStack(stack *ast.Stack) {
// Ensure the name of the base is in scope, and remember the binding information.
if stack.Base != "" {
// TODO[marapongo/mu#7]: we need to plumb construction properties for this stack.
stack.BoundBase = p.ensureStack(stack.Base, nil)
}
// Non-abstract Stacks must declare at least one Service.
if !stack.Intrinsic && !stack.Abstract && len(stack.Services.Public) == 0 && len(stack.Services.Private) == 0 {
p.Diag().Errorf(errors.ErrorNonAbstractStacksMustDefineServices.At(stack))
}
}
func (p *binderBindPhase) VisitSchemas(parent *ast.Stack, schemas *ast.Schemas) {
}
func (p *binderBindPhase) VisitSchema(pstack *ast.Stack, parent *ast.Schemas, name ast.Name,
public bool, schema *ast.Schema) {
// Ensure the base schema is available to us.
if schema.BoundBase != nil && schema.BoundBase.IsUnresolvedRef() {
ref := *schema.BoundBase.Unref
base := p.ensureType(ref)
// Check to ensure that the base is of one of the legal kinds.
if !base.IsPrimitive() && !base.IsSchema() {
p.Diag().Errorf(errors.ErrorSchemaTypeExpected, ref, base)
}
}
// TODO: ensure that schemas with constraints don't have illegal constraints (wrong type; regex won't parse; etc).
}
func (p *binderBindPhase) VisitProperty(parent *ast.Stack, schema *ast.Schema, name string, prop *ast.Property) {
// For properties whose types represent unresolved names, we must bind them to a name now.
if prop.BoundType.IsUnresolvedRef() {
prop.BoundType = p.ensureType(*prop.BoundType.Unref)
util.Assert(prop.BoundType != nil)
}
}
func (p *binderBindPhase) VisitServices(parent *ast.Stack, svcs *ast.Services) {
}
func (p *binderBindPhase) VisitService(pstack *ast.Stack, parent *ast.Services, name ast.Name, public bool,
svc *ast.Service) {
// The service's type has been prepared in phase 1, and must now be bound to a symbol. All shorthand type
// expressions, intra stack references, cycles, and so forth, will have been taken care of by this earlier phase.
util.AssertMF(svc.Type != "",
"Expected all Services to have types in binding phase2; %v is missing one", svc.Name)
svc.BoundType = p.ensureStack(svc.Type, svc.Properties)
// A service cannot instantiate an abstract stack.
if svc.BoundType != nil && svc.BoundType.Abstract {
p.Diag().Errorf(errors.ErrorCannotCreateAbstractStack.At(pstack), svc.Name, svc.BoundType.Name)
}
}
// ensureStack binds a ref to a stack symbol, possibly instantiating it if needed.
func (p *binderBindPhase) ensureStack(ref ast.Ref, props ast.PropertyBag) *ast.Stack {
ty := p.ensureType(ref)
// There are two possibilities. The first is that a type resolves to an *ast.Stack. That's simple, we just fetch
// and return it. The second is that a type resolves to a *diag.Document. That's more complex, as we need to
// actually parse the stack from a document, supplying properties, etc., for template expansion.
if ty.IsStack() {
return ty.Stack
} else if ty.IsUninstStack() {
// We have the dependency's Mufile; now we must "instantiate it", by parsing it and returning the result. Note
// that this will be processed later on in semantic analysis, to ensure semantic problems are caught.
pa := NewParser(p.b.c)
stack := pa.ParseStack(ty.UninstStack.Doc, props)
if !pa.Diag().Success() {
// If we failed to parse the stack, there was something wrong with our dependency information. Bail out.
return nil
}
p.deps = append(p.deps, stack)
return stack
} else {
p.Diag().Errorf(errors.ErrorStackTypeExpected, ref, ty)
return nil
}
}
// ensureStackType looks up a ref, either as a stack, document, or schema symbol, and returns it as-is.
func (p *binderBindPhase) ensureType(ref ast.Ref) *ast.Type {
nm := refToName(ref)
stack, exists := p.b.LookupStack(nm)
if exists {
return ast.NewStackType(stack)
}
stref, exists := p.b.LookupUninstStack(nm)
if exists {
return ast.NewUninstStackType(stref)
}
schema, exists := p.b.LookupSchema(nm)
if exists {
return ast.NewSchemaType(schema)
}
util.FailMF("Expected 1st pass of binding to guarantee type %v exists (%v)", ref, nm)
return nil
}
type binderValidatePhase struct {
b *binder
}
var _ core.Visitor = (*binderValidatePhase)(nil) // compile-time assertion that the binder implements core.Visitor.
func newBinderValidatePhase(b *binder) *binderValidatePhase {
return &binderValidatePhase{b: b}
}
func (p *binderValidatePhase) Diag() diag.Sink {
return p.b.Diag()
}
func (p *binderValidatePhase) VisitWorkspace(workspace *ast.Workspace) {
}
func (p *binderValidatePhase) VisitCluster(name string, cluster *ast.Cluster) {
}
func (p *binderValidatePhase) VisitDependency(parent *ast.Workspace, ref ast.Ref, dep *ast.Dependency) {
}
func (p *binderValidatePhase) VisitStack(stack *ast.Stack) {
if stack.PropertyValues != nil {
// Bind property values.
stack.BoundPropertyValues = p.bindProperties(&stack.Node, stack.Properties, stack.PropertyValues)
}
if stack.Base != "" {
util.Assert(stack.BoundBase != nil)
// TODO[marapongo/mu#7]: validate the properties from this stack on the base.
}
}
func (p *binderValidatePhase) VisitSchemas(parent *ast.Stack, schemas *ast.Schemas) {
}
func (p *binderValidatePhase) VisitSchema(pstack *ast.Stack, parent *ast.Schemas, name ast.Name,
public bool, schema *ast.Schema) {
}
func (p *binderValidatePhase) VisitProperty(parent *ast.Stack, schema *ast.Schema, name string, prop *ast.Property) {
}
func (p *binderValidatePhase) VisitServices(parent *ast.Stack, svcs *ast.Services) {
}
func (p *binderValidatePhase) VisitService(pstack *ast.Stack, parent *ast.Services, name ast.Name, public bool,
svc *ast.Service) {
util.Assert(svc.BoundType != nil)
if svc.BoundType.PropertyValues == nil {
// For some types, there aren't any property values (e.g., built-in types). For those, bind now.
svc.BoundProperties = p.bindProperties(&pstack.Node, svc.BoundType.Properties, svc.Properties)
} else {
// For imported types, we should have property values, which already got bound in an earlier phase.
util.Assert(svc.BoundType.BoundPropertyValues != nil)
util.Assert(len(svc.BoundType.PropertyValues) == len(svc.Properties))
svc.BoundProperties = svc.BoundType.BoundPropertyValues
}
}
// bindProperties typechecks a set of unbounded properties against the target stack, and expands them into a bag
// of bound properties (with AST nodes rather than the naked parsed types).
func (p *binderValidatePhase) bindProperties(node *ast.Node, props ast.Properties,
vals ast.PropertyBag) ast.LiteralPropertyBag {
bound := make(ast.LiteralPropertyBag)
// First, enumerate all known properties on the stack. Ensure all required properties are present, expand default
// values for missing ones where applicable, and check that types are correct, converting them as appropriate.
for _, pname := range ast.StableProperties(props) {
prop := props[pname]
// First see if a value has been supplied by the caller.
val, has := vals[pname]
if !has || val == nil {
if prop.Default != nil {
// If the property has a default value, stick it in and process it normally.
val = prop.Default
} else if prop.Optional {
// If this is an optional property, ok, just skip the remainder of processing.
continue
} else {
// If there's no value, no default, and it isn't optional, issue an error and move on.
p.Diag().Errorf(errors.ErrorMissingRequiredProperty.At(node), pname)
continue
}
}
util.Assert(val != nil)
// Now, value in hand, let's make sure it's the right type.
if lit := p.bindValue(&prop.Node, val, prop.BoundType); lit != nil {
bound[pname] = lit
}
}
for _, pname := range ast.StablePropertyBag(vals) {
if _, ok := props[pname]; !ok {
// TODO: edit distance checking to help with suggesting a fix.
p.Diag().Errorf(errors.ErrorUnrecognizedProperty.At(node), pname)
}
}
return bound
}
// bindValue takes a value and binds it to a type and literal AST node, returning nils if the conversions fails.
func (p *binderValidatePhase) bindValue(node *ast.Node, val interface{}, ty *ast.Type) ast.Literal {
util.Assert(ty != nil)
var lit ast.Literal
if ty.IsDecors() {
lit = p.bindDecorsValue(node, val, ty.Decors)
} else if ty.IsPrimitive() {
lit = p.bindPrimitiveValue(node, val, *ty.Primitive)
} else if ty.IsStack() {
lit = p.bindServiceValue(node, val, ty)
} else if ty.IsSchema() {
lit = p.bindSchemaValue(node, val, ty.Schema)
} else if ty.IsUnresolvedRef() {
util.FailM("Expected all unresolved refs to be gone by this phase in binding")
}
if lit == nil {
// If no successful type binding happened, issue an error.
p.Diag().Errorf(errors.ErrorIncorrectType.At(node), ty, reflect.TypeOf(val))
}
return lit
}
func (p *binderValidatePhase) bindDecorsValue(node *ast.Node, val interface{}, decors *ast.TypeDecors) ast.Literal {
// For decorated types, we need to recurse.
if decors.ElemType != nil {
arr := reflect.ValueOf(val)
if arr.Kind() == reflect.Slice {
len := arr.Len()
lits := make([]ast.Literal, len)
err := false
for i := 0; i < len; i++ {
v := arr.Index(i).Interface()
if lits[i] = p.bindValue(node, v, decors.ElemType); lits[i] == nil {
err = true
}
}
if !err {
return ast.NewArrayLiteral(node, decors.ElemType, lits)
}
} else {
glog.V(7).Infof("Expected array for value %v, got %v", val, arr.Kind())
}
} else {
util.Assert(decors.KeyType != nil)
util.Assert(decors.ValueType != nil)
// TODO: ensure that keytype is something we can actually use as a key (primitive).
m := reflect.ValueOf(val)
if m.Kind() == reflect.Map {
mk := m.MapKeys()
keys := make([]ast.Literal, len(mk))
err := false
for i := 0; i < len(mk); i++ {
k := mk[i].Interface()
if keys[i] = p.bindValue(node, k, decors.KeyType); keys[i] == nil {
glog.V(7).Infof("Error binding map key #%v (%v); expected %v",
i, k, decors.KeyType)
err = true
}
}
vals := make([]ast.Literal, len(mk))
for i := 0; i < len(mk); i++ {
v := m.MapIndex(mk[i])
if vals[i] = p.bindValue(node, v, decors.ValueType); vals[i] == nil {
glog.V(7).Infof("Error binding map value #%v (k=%v v=%v); expected %v",
i, mk[i].Interface(), v, decors.ValueType)
err = true
}
}
if !err {
return ast.NewMapLiteral(node, decors.KeyType, decors.ValueType, keys, vals)
}
} else {
glog.V(7).Infof("Expected map for value %v, got %v", val, m.Kind())
}
}
return nil
}
func (p *binderValidatePhase) bindPrimitiveValue(node *ast.Node, val interface{}, prim ast.PrimitiveType) ast.Literal {
// For primitive types, simply cast the target to the expected type.
switch prim {
case ast.PrimitiveTypeAny:
// Any is easy: just store it as-is.
return ast.NewAnyLiteral(node, val)
case ast.PrimitiveTypeString:
if s, ok := val.(string); ok {
return ast.NewStringLiteral(node, s)
}
return nil
case ast.PrimitiveTypeNumber:
if n, ok := val.(float64); ok {
return ast.NewNumberLiteral(node, n)
}
return nil
case ast.PrimitiveTypeBool:
if b, ok := val.(bool); ok {
return ast.NewBoolLiteral(node, b)
}
return nil
case ast.PrimitiveTypeService:
// Extract the name of the service reference as a string. Then bind it to an actual service in our symbol
// table, and store a strong reference to the result. This lets the backend connect the dots.
return p.bindServiceValue(node, val, nil)
default:
util.FailMF("Unrecognized primitive type: %v", prim)
return nil
}
}
func (p *binderValidatePhase) bindServiceValue(node *ast.Node, val interface{}, expect *ast.Type) ast.Literal {
// Bind the capability ref for this stack type.
if s, ok := val.(string); ok {
if ref := p.bindServiceRef(node, s, expect); ref != nil {
return ast.NewServiceLiteral(node, ref)
}
}
return nil
}
func (p *binderValidatePhase) bindSchemaValue(node *ast.Node, val interface{}, schema *ast.Schema) ast.Literal {
// Bind the custom schema type. This is rather involved, but there are two primary cases:
// 1) A base type exists, plus an optional set of constraints on that base type (if it's a primitive).
// 2) A set of properties exist, meaning an entirely custom object. We must go recursive.
// TODO[marapongo/mu#9]: we may want to support mixing these (e.g., additive properties); for now, we won't.
if schema.BoundBase != nil {
// There is a base type. Bind it as-is, and then apply any additional constraints we have added.
util.Assert(schema.Properties == nil)
lit := p.bindValue(node, val, schema.BoundBase)
if lit != nil {
// The following constraints are valid only on strings:
if schema.Pattern != nil {
if s, ok := conv.ToString(lit); ok {
rex := regexp.MustCompile(*schema.Pattern)
if rex.FindString(s) != s {
p.Diag().Errorf(errors.ErrorSchemaConstraintUnmet, "pattern", schema.Pattern, s)
}
} else {
p.Diag().Errorf(errors.ErrorSchemaConstraintType, "maxLength", ast.PrimitiveTypeString, lit.Type())
}
}
if schema.MaxLength != nil {
if s, ok := conv.ToString(lit); ok {
c := utf8.RuneCountInString(s)
if float64(c) > *schema.MaxLength {
p.Diag().Errorf(errors.ErrorSchemaConstraintUnmet,
"maxLength", fmt.Sprintf("max %v", schema.MaxLength), c)
}
} else {
p.Diag().Errorf(errors.ErrorSchemaConstraintType, "maxLength", ast.PrimitiveTypeString, lit.Type())
}
}
if schema.MinLength != nil {
if s, ok := conv.ToString(lit); ok {
c := utf8.RuneCountInString(s)
if float64(c) < *schema.MinLength {
p.Diag().Errorf(errors.ErrorSchemaConstraintUnmet,
"minLength", fmt.Sprintf("min %v", schema.MinLength), c)
}
} else {
p.Diag().Errorf(errors.ErrorSchemaConstraintType, "minLength", ast.PrimitiveTypeString, lit.Type())
}
}
// The following constraints are valid only on numeric ypes:
if schema.Maximum != nil {
if n, ok := conv.ToNumber(lit); ok {
if n > *schema.Maximum {
p.Diag().Errorf(errors.ErrorSchemaConstraintUnmet,
"maximum", fmt.Sprintf("max %v", schema.Maximum), n)
}
} else {
p.Diag().Errorf(errors.ErrorSchemaConstraintType, "maximum", ast.PrimitiveTypeNumber, lit.Type())
}
}
if schema.Minimum != nil {
if n, ok := conv.ToNumber(lit); ok {
if n < *schema.Minimum {
p.Diag().Errorf(errors.ErrorSchemaConstraintUnmet,
"minimum", fmt.Sprintf("min %v", schema.Minimum), n)
}
} else {
p.Diag().Errorf(errors.ErrorSchemaConstraintType, "minimum", ast.PrimitiveTypeNumber, lit.Type())
}
}
// The following constraints are valid on strings *and* number types.
if len(schema.Enum) > 0 {
if s, ok := conv.ToString(lit); ok {
ok := false
for _, e := range schema.Enum {
if s == e.(string) {
ok = true
break
}
}
if !ok {
p.Diag().Errorf(errors.ErrorSchemaConstraintUnmet,
"enum", fmt.Sprintf("enum %v", schema.Enum), s)
}
} else if n, ok := conv.ToNumber(lit); ok {
ok := false
for _, e := range schema.Enum {
if n == e.(float64) {
ok = true
break
}
}
if !ok {
p.Diag().Errorf(errors.ErrorSchemaConstraintUnmet,
"enum", fmt.Sprintf("enum %v", schema.Enum), n)
}
} else {
p.Diag().Errorf(errors.ErrorSchemaConstraintType,
"enum", ast.PrimitiveTypeString+" or "+ast.PrimitiveTypeNumber, lit.Type())
}
}
}
} else if schema.Properties != nil {
// There are some properties. This is a custom type. Bind the properties as usual.
if props, ok := val.(ast.PropertyBag); ok {
bag := p.bindProperties(node, schema.Properties, props)
return ast.NewSchemaLiteral(node, schema, bag)
}
}
return nil
}
// bindServiceRef binds a string to a service reference, resulting in a ServiceRef. The reference is expected
// to be in the form "<service>[:<selector>]", where <service> is the name of a service that's currently in scope, and
// <selector> is an optional selector of a public service exported from that service.
func (p *binderValidatePhase) bindServiceRef(node *ast.Node, val string, ty *ast.Type) *ast.ServiceRef {
glog.V(5).Infof("Binding capref '%v'", val)
// Peel off the selector, if there is one.
var sels string
if selix := strings.LastIndex(val, ":"); selix != -1 {
sels = val[selix+1:]
val = val[:selix]
}
// Validate and convert the name and selector to names.
var nm ast.Name
if ast.IsName(val) {
nm = ast.AsName(val)
} else {
p.Diag().Errorf(errors.ErrorNotAName.At(node), val)
}
var sel ast.Name
if sels != "" {
if ast.IsName(sels) {
sel = ast.AsName(sels)
} else {
p.Diag().Errorf(errors.ErrorNotAName.At(node), sels)
}
}
// If we have errors at this juncture, bail early, before it just gets worse.
if !p.Diag().Success() {
return nil
}
// Bind the name to a service.
var ref *ast.ServiceRef
if svc, ok := p.b.LookupService(ast.Name(nm)); ok {
svct := svc.BoundType
util.AssertMF(svct != nil, "Expected service '%v' to have a type", svc.Name)
var selsvc *ast.Service
if sel == "" {
// If no selector was specified, just use the service itself as the selsvc.
selsvc = svc
} else if sel == "." {
// A special dot selector can be used to pick the sole public service.
if len(svct.Services.Public) == 0 {
p.Diag().Errorf(errors.ErrorServiceHasNoPublics.At(node), svc.Name, svct.Name)
} else if len(svct.Services.Public) == 1 {
for _, pub := range svct.Services.Public {
selsvc = pub
break
}
} else {
util.Assert(len(svct.Services.Public) > 1)
p.Diag().Errorf(errors.ErrorServiceHasManyPublics.At(node), svc.Name, svct.Name)
}
} else {
// If a selector was specified, ensure that it actually exists.
if entry, ok := svct.Services.Public[sel]; ok {
selsvc = entry
} else {
// The selector wasn't found. Issue an error. If there's a private service by that name,
// say so, for better diagnostics.
if _, has := svct.Services.Private[sel]; has {
p.Diag().Errorf(errors.ErrorServiceSelectorIsPrivate.At(node), sel, svc.Name, svct.Name)
} else {
p.Diag().Errorf(errors.ErrorServiceSelectorNotFound.At(node), sel, svc.Name, svct.Name)
}
}
}
if selsvc != nil {
// If there is an expected type, now ensure that the selected Service is of the right kind.
util.Assert(selsvc.BoundType != nil)
if ty != nil && !subclassOf(selsvc.BoundType, ty) {
p.Diag().Errorf(errors.ErrorIncorrectType.At(node), ty, selsvc.BoundType.Name)
}
ref = &ast.ServiceRef{
Name: nm,
Selector: sel,
Service: svc,
Selected: selsvc,
}
}
} else {
p.Diag().Errorf(errors.ErrorServiceNotFound.At(node), nm)
}
return ref
}
// subclassOf checks that the left type ("typ") is equal to or a subclass of the right type ("or"). The right type is a
// union between *ast.Stack and *ast.UninstStack, so that it can be an uninstantiated type if needed.
func subclassOf(typ *ast.Stack, of *ast.Type) bool {
for typ != nil {
if typ == of.Stack {
// If the type matches the target directly, obviously it's a hit.
return true
} else if of.UninstStack != nil {
// If the type was produced from the same "document" (uninstantiated type), then it's also a hit. Note that
// due to template expansion, we need to walk the document hierarchy to see if there's a match.
doc := typ.Doc
for doc != nil {
if doc == of.UninstStack.Doc {
return true
}
doc = doc.Parent
}
}
// Finally, if neither of those worked, we must see if there's a base class and keep searching.
typ = typ.BoundBase
}
return false
}
// refToName converts a reference to its simple symbolic name.
func refToName(ref ast.Ref) ast.Name {
return ref.MustParse().Name
}