From 4ae8cae9e70863635796e78cc6d6bf3d823a6140 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Jan 2015 15:26:54 -0800 Subject: [PATCH] config/lang: execution --- config/lang/ast/ast.go | 17 +++- config/lang/ast/call.go | 8 ++ config/lang/ast/concat.go | 8 ++ config/lang/ast/literal.go | 4 + config/lang/ast/type_string.go | 7 +- config/lang/ast/variable_access.go | 4 + config/lang/engine.go | 148 +++++++++++++++++++++++++++++ config/lang/engine_test.go | 59 ++++++++++++ 8 files changed, 248 insertions(+), 7 deletions(-) create mode 100644 config/lang/engine.go create mode 100644 config/lang/engine_test.go diff --git a/config/lang/ast/ast.go b/config/lang/ast/ast.go index c9ae1b882..45c3ddc77 100644 --- a/config/lang/ast/ast.go +++ b/config/lang/ast/ast.go @@ -1,7 +1,18 @@ package ast // Node is the interface that all AST nodes must implement. -type Node interface{} +type Node interface { + // Accept is called to dispatch to the visitors. + Accept(Visitor) +} + +// Visitors are just implementations of this function. +// +// Note that this isn't a true implementation of the visitor pattern, which +// generally requires proper type dispatch on the function. However, +// implementing this basic visitor pattern style is still very useful even +// if you have to type switch. +type Visitor func(Node) //go:generate stringer -type=Type @@ -9,6 +20,6 @@ type Node interface{} type Type uint const ( - TypeInvalid Type = 1 << iota - TypeString + TypeInvalid Type = 0 + TypeString = 1 << iota ) diff --git a/config/lang/ast/call.go b/config/lang/ast/call.go index 35fc03f50..6f2854799 100644 --- a/config/lang/ast/call.go +++ b/config/lang/ast/call.go @@ -5,3 +5,11 @@ type Call struct { Func string Args []Node } + +func (n *Call) Accept(v Visitor) { + for _, a := range n.Args { + a.Accept(v) + } + + v(n) +} diff --git a/config/lang/ast/concat.go b/config/lang/ast/concat.go index c88e3de5a..77f7181ab 100644 --- a/config/lang/ast/concat.go +++ b/config/lang/ast/concat.go @@ -10,6 +10,14 @@ type Concat struct { Exprs []Node } +func (n *Concat) Accept(v Visitor) { + for _, n := range n.Exprs { + n.Accept(v) + } + + v(n) +} + func (n *Concat) GoString() string { return fmt.Sprintf("*%#v", *n) } diff --git a/config/lang/ast/literal.go b/config/lang/ast/literal.go index 3477a5dbd..8b7e2b88c 100644 --- a/config/lang/ast/literal.go +++ b/config/lang/ast/literal.go @@ -11,6 +11,10 @@ type LiteralNode struct { Type Type } +func (n *LiteralNode) Accept(v Visitor) { + v(n) +} + func (n *LiteralNode) GoString() string { return fmt.Sprintf("*%#v", *n) } diff --git a/config/lang/ast/type_string.go b/config/lang/ast/type_string.go index 59c336f5a..55e2af017 100644 --- a/config/lang/ast/type_string.go +++ b/config/lang/ast/type_string.go @@ -4,14 +4,13 @@ package ast import "fmt" -const _Type_name = "TypeInvalidTypeString" +const _Type_name = "TypeInvalid" -var _Type_index = [...]uint8{0, 11, 21} +var _Type_index = [...]uint8{0, 11} func (i Type) String() string { - i -= 1 if i+1 >= Type(len(_Type_index)) { - return fmt.Sprintf("Type(%d)", i+1) + return fmt.Sprintf("Type(%d)", i) } return _Type_name[_Type_index[i]:_Type_index[i+1]] } diff --git a/config/lang/ast/variable_access.go b/config/lang/ast/variable_access.go index 5d63dcba1..987df97d2 100644 --- a/config/lang/ast/variable_access.go +++ b/config/lang/ast/variable_access.go @@ -9,6 +9,10 @@ type VariableAccess struct { Name string } +func (n *VariableAccess) Accept(v Visitor) { + v(n) +} + func (n *VariableAccess) GoString() string { return fmt.Sprintf("*%#v", *n) } diff --git a/config/lang/engine.go b/config/lang/engine.go new file mode 100644 index 000000000..0fd8f7eac --- /dev/null +++ b/config/lang/engine.go @@ -0,0 +1,148 @@ +package lang + +import ( + "bytes" + "fmt" + "sync" + + "github.com/hashicorp/terraform/config/lang/ast" +) + +// Engine is the execution engine for this language. It should be configured +// prior to running Execute. +type Engine struct { + // VarMap and FuncMap are the mappings of identifiers to functions + // and variable values. + VarMap map[string]Variable + FuncMap map[string]Function + + // SemanticChecks is a list of additional semantic checks that will be run + // on the tree prior to executing it. The type checker, identifier checker, + // etc. will be run before these. + SemanticChecks []SemanticChecker +} + +// SemanticChecker is the type that must be implemented to do a +// semantic check on an AST tree. This will be called with the root node. +type SemanticChecker func(ast.Node) error + +// Variable is a variable value for execution given as input to the engine. +// It records the value of a variables along with their type. +type Variable struct { + Value interface{} + Type ast.Type +} + +// Function defines a function that can be executed by the engine. +// The type checker will validate that the proper types will be called +// to the callback. +type Function struct { + Name string + ArgTypes []ast.Type + Callback func([]interface{}) (interface{}, ast.Type, error) +} + +// Execute executes the given ast.Node and returns its final value, its +// type, and an error if one exists. +func (e *Engine) Execute(root ast.Node) (interface{}, ast.Type, error) { + v := &executeVisitor{Engine: e} + return v.Visit(root) +} + +// executeVisitor is the visitor used to do the actual execution of +// a program. Note at this point it is assumed that the types check out +// and the identifiers exist. +type executeVisitor struct { + Engine *Engine + + stack []*ast.LiteralNode + err error + lock sync.Mutex +} + +func (v *executeVisitor) Visit(root ast.Node) (interface{}, ast.Type, error) { + v.lock.Lock() + defer v.lock.Unlock() + + // Run the actual visitor pattern + root.Accept(v.visit) + + // Get our result and clear out everything else + var result *ast.LiteralNode + if len(v.stack) > 0 { + result = v.stack[len(v.stack)-1] + } else { + result = new(ast.LiteralNode) + } + resultErr := v.err + + // Clear everything else so we aren't just dangling + v.stack = nil + v.err = nil + + return result.Value, result.Type, resultErr +} + +func (v *executeVisitor) visit(raw ast.Node) { + if v.err != nil { + return + } + + switch n := raw.(type) { + case *ast.Concat: + v.visitConcat(n) + case *ast.LiteralNode: + v.visitLiteral(n) + case *ast.VariableAccess: + v.visitVariableAccess(n) + default: + v.err = fmt.Errorf("unknown node: %#v", raw) + } +} + +func (v *executeVisitor) visitConcat(n *ast.Concat) { + // The expressions should all be on the stack in reverse + // order. So pop them off, reverse their order, and concatenate. + nodes := make([]*ast.LiteralNode, 0, len(n.Exprs)) + for range n.Exprs { + nodes = append(nodes, v.stackPop()) + } + + var buf bytes.Buffer + for i := len(nodes) - 1; i >= 0; i-- { + buf.WriteString(nodes[i].Value.(string)) + } + + v.stackPush(&ast.LiteralNode{ + Value: buf.String(), + Type: ast.TypeString, + }) +} + +func (v *executeVisitor) visitLiteral(n *ast.LiteralNode) { + v.stack = append(v.stack, n) +} + +func (v *executeVisitor) visitVariableAccess(n *ast.VariableAccess) { + // Look up the variable in the map + variable, ok := v.Engine.VarMap[n.Name] + if !ok { + v.err = fmt.Errorf("unknown variable accessed: %s", n.Name) + return + } + + v.stack = append(v.stack, &ast.LiteralNode{ + Value: variable.Value, + Type: variable.Type, + }) +} + +func (v *executeVisitor) stackPush(n *ast.LiteralNode) { + v.stack = append(v.stack, n) +} + +func (v *executeVisitor) stackPop() *ast.LiteralNode { + var x *ast.LiteralNode + x, v.stack = v.stack[len(v.stack)-1], v.stack[:len(v.stack)-1] + return x +} diff --git a/config/lang/engine_test.go b/config/lang/engine_test.go new file mode 100644 index 000000000..489758a6e --- /dev/null +++ b/config/lang/engine_test.go @@ -0,0 +1,59 @@ +package lang + +import ( + "reflect" + "testing" + + "github.com/hashicorp/terraform/config/lang/ast" +) + +func TestEngineExecute(t *testing.T) { + cases := []struct { + Input string + Engine *Engine + Error bool + Result interface{} + ResultType ast.Type + }{ + { + "foo", + &Engine{}, + false, + "foo", + ast.TypeString, + }, + + { + "foo ${bar}", + &Engine{ + VarMap: map[string]Variable{ + "bar": Variable{ + Value: "baz", + Type: ast.TypeString, + }, + }, + }, + false, + "foo baz", + ast.TypeString, + }, + } + + for _, tc := range cases { + node, err := Parse(tc.Input) + if err != nil { + t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) + } + + out, outType, err := tc.Engine.Execute(node) + if (err != nil) != tc.Error { + t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) + } + if outType != tc.ResultType { + t.Fatalf("Bad: %s\n\nInput: %s", outType, tc.Input) + } + if !reflect.DeepEqual(out, tc.Result) { + t.Fatalf("Bad: %#v\n\nInput: %s", out, tc.Input) + } + } +}