pulumi/pkg/compiler/binder.go
joeduffy 8fa1fd9082 Add some targeting tests
This adds some tests around cloud targeting, in addition to enabling builds
to use in-memory Mufiles (mostly to make testing simpler, but this is a
generally useful capability to have when hosting the compiler API).
2016-11-17 13:08:20 -08:00

249 lines
7.3 KiB
Go

// Copyright 2016 Marapongo, Inc. All rights reserved.
package compiler
import (
"github.com/golang/glog"
"github.com/marapongo/mu/pkg/ast"
"github.com/marapongo/mu/pkg/compiler/core"
"github.com/marapongo/mu/pkg/diag"
"github.com/marapongo/mu/pkg/errors"
)
// Binder annotates an existing parse tree with semantic information.
type Binder interface {
core.Phase
// Bind takes the parse tree, binds all names inside of it, mutating it in place.
Bind(doc *diag.Document, 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()
// And now populate that symbol table with all known predefined Stack types before returning it.
for nm, stack := range PredefStackTypes {
sym := NewStackSymbol(nm, stack)
if ok := b.RegisterSymbol(sym); !ok {
glog.Fatalf("Unexpected Symbol collision when registering predef Stack type %v\n", nm)
}
}
return b
}
type binder struct {
c Compiler
scope *scope
}
func (b *binder) Diag() diag.Sink {
return b.c.Diag()
}
func (b *binder) Bind(doc *diag.Document, stack *ast.Stack) {
glog.Infof("Binding Mu Stack: %v", stack.Name)
if glog.V(2) {
defer func() {
glog.V(2).Infof("Binding 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()
defer b.PopScope()
// The binding logic is split into two-phases, due to the possibility of intra-stack references between elements.
phase1 := &binderPhase1{b: b, doc: doc}
phase2 := &binderPhase2{b: b, doc: doc}
// Now walk the trees. We use an InOrderVisitor to do this in the right order, handling determinism, etc. for us.
v1 := core.NewInOrderVisitor(phase1, nil)
v1.VisitStack(doc, stack)
if b.Diag().Errors() == 0 {
v2 := core.NewInOrderVisitor(phase2, nil)
v2.VisitStack(doc, stack)
}
}
// LookupStack binds a name to a Stack type.
func (b *binder) LookupStack(nm ast.Name) (*Symbol, *ast.Stack) {
if b.scope == nil {
glog.Fatalf("Unexpected empty binding scope during LookupStack")
}
return b.scope.LookupStack(nm)
}
// LookupSymbol binds a name to any kind of Symbol.
func (b *binder) LookupSymbol(nm ast.Name) *Symbol {
if b.scope == nil {
glog.Fatalf("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 {
if b.scope == nil {
glog.Fatalf("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() {
if b.scope == nil {
glog.Fatalf("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
}
// LookupStack binds a name to a Stack type.
func (s *scope) LookupStack(nm ast.Name) (*Symbol, *ast.Stack) {
sym := s.LookupSymbol(nm)
if sym != nil && sym.Kind == SymKindStack {
return sym, sym.Real.(*ast.Stack)
}
// TODO: we probably need to issue an error for this condition (wrong expected symbol type).
return nil, nil
}
// LookupSymbol binds a name to any kind of Symbol.
func (s *scope) LookupSymbol(nm ast.Name) *Symbol {
for s != nil {
if sym, exists := s.symtbl[nm]; exists {
return sym
}
s = s.parent
}
return nil
}
// 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 binderPhase1 struct {
core.Visitor
b *binder
doc *diag.Document
}
func (p *binderPhase1) Diag() diag.Sink {
return p.b.Diag()
}
func (p *binderPhase1) VisitMetadata(doc *diag.Document, kind string, meta *ast.Metadata) {
}
func (p *binderPhase1) VisitStack(doc *diag.Document, stack *ast.Stack) {
}
func (p *binderPhase1) VisitParameter(doc *diag.Document, name string, param *ast.Parameter) {
}
func (p *binderPhase1) VisitDependency(doc *diag.Document, name ast.Name, dep *ast.Dependency) {
}
func (p *binderPhase1) VisitServices(doc *diag.Document, svcs *ast.Services) {
}
func (p *binderPhase1) VisitService(doc *diag.Document, 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."
if svc.Type == "" {
svc.Type = svc.Name
svc.Name = ast.NamePart(svc.Name)
}
// Next, note that service definitions can "refer" to other service definitions within the same file. Any
// unqualified name is interpreted as such. As a result, we must add this service to the symbol table even before
// doing any subsequent binding of its type, etc. This simplifies the 2nd phase of binding which can rely on this
// fact, making its logic far simpler.
sym := NewServiceSymbol(svc.Name, svc)
if !p.b.RegisterSymbol(sym) {
p.Diag().Errorf(errors.SymbolAlreadyExists.WithDocument(p.doc), sym.Name)
}
}
func (p *binderPhase1) VisitTarget(doc *diag.Document, name string, target *ast.Target) {
}
type binderPhase2 struct {
core.Visitor
b *binder
doc *diag.Document
}
func (p *binderPhase2) Diag() diag.Sink {
return p.b.Diag()
}
func (p *binderPhase2) VisitMetadata(doc *diag.Document, kind string, meta *ast.Metadata) {
}
func (p *binderPhase2) VisitStack(doc *diag.Document, stack *ast.Stack) {
}
func (p *binderPhase2) VisitParameter(doc *diag.Document, name string, param *ast.Parameter) {
}
func (p *binderPhase2) VisitDependency(doc *diag.Document, name ast.Name, dep *ast.Dependency) {
}
func (p *binderPhase2) VisitServices(doc *diag.Document, svcs *ast.Services) {
}
func (p *binderPhase2) VisitService(doc *diag.Document, 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.
if svc.Type == "" {
glog.Fatalf("Expected all Services to have types in binding phase2; %v is missing one", svc.Name)
}
ty, _ := p.b.LookupStack(svc.Type)
if ty == nil {
p.Diag().Errorf(errors.TypeNotFound.WithDocument(p.doc), svc.Type)
}
}
func (p *binderPhase2) VisitTarget(doc *diag.Document, name string, target *ast.Target) {
}