config/lang: execution

This commit is contained in:
Mitchell Hashimoto 2015-01-11 15:26:54 -08:00
parent fa7891d182
commit 4ae8cae9e7
8 changed files with 248 additions and 7 deletions

View File

@ -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
)

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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]]
}

View File

@ -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)
}

148
config/lang/engine.go Normal file
View File

@ -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
}

View File

@ -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)
}
}
}