5f33292496
This change just moves the assertion/failure functions from the pkg/util package to pkg/util/contract, so things read a bit nicer (i.e., `contract.Assert(x)` versus `util.Assert(x)`).
970 lines
33 KiB
Go
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/contract"
|
|
)
|
|
|
|
// 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) {
|
|
contract.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) {
|
|
contract.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) {
|
|
contract.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) {
|
|
contract.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) {
|
|
contract.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 {
|
|
contract.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() {
|
|
contract.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]
|
|
contract.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)
|
|
contract.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.
|
|
contract.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)
|
|
}
|
|
contract.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 != "" {
|
|
contract.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) {
|
|
contract.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.
|
|
contract.Assert(svc.BoundType.BoundPropertyValues != nil)
|
|
contract.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
|
|
}
|
|
}
|
|
contract.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 {
|
|
contract.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() {
|
|
contract.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 {
|
|
contract.Assert(decors.KeyType != nil)
|
|
contract.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:
|
|
contract.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.
|
|
contract.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
|
|
contract.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 {
|
|
contract.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.
|
|
contract.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
|
|
}
|