config/lang: eval
This commit is contained in:
parent
4302dbaf2a
commit
8d2c60a8af
|
@ -8,12 +8,16 @@ import (
|
|||
|
||||
// NOTE: All builtins are tested in engine_test.go
|
||||
|
||||
func registerBuiltins(scope *ast.BasicScope) {
|
||||
func registerBuiltins(scope *ast.BasicScope) *ast.BasicScope {
|
||||
if scope == nil {
|
||||
scope = new(ast.BasicScope)
|
||||
}
|
||||
if scope.FuncMap == nil {
|
||||
scope.FuncMap = make(map[string]ast.Function)
|
||||
}
|
||||
scope.FuncMap["__builtin_IntToString"] = builtinIntToString()
|
||||
scope.FuncMap["__builtin_StringToInt"] = builtinStringToInt()
|
||||
return scope
|
||||
}
|
||||
|
||||
func builtinIntToString() ast.Function {
|
||||
|
|
|
@ -1,225 +0,0 @@
|
|||
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 {
|
||||
// GlobalScope is the global scope of execution for this engine.
|
||||
GlobalScope *ast.BasicScope
|
||||
|
||||
// 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
|
||||
|
||||
// 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) {
|
||||
// Copy the scope so we can add our builtins
|
||||
scope := e.scope()
|
||||
implicitMap := map[ast.Type]map[ast.Type]string{
|
||||
ast.TypeInt: {
|
||||
ast.TypeString: "__builtin_IntToString",
|
||||
},
|
||||
ast.TypeString: {
|
||||
ast.TypeInt: "__builtin_StringToInt",
|
||||
},
|
||||
}
|
||||
|
||||
// Build our own semantic checks that we always run
|
||||
tv := &TypeCheck{Scope: scope, Implicit: implicitMap}
|
||||
ic := &IdentifierCheck{Scope: scope}
|
||||
|
||||
// Build up the semantic checks for execution
|
||||
checks := make(
|
||||
[]SemanticChecker, len(e.SemanticChecks), len(e.SemanticChecks)+2)
|
||||
copy(checks, e.SemanticChecks)
|
||||
checks = append(checks, ic.Visit)
|
||||
checks = append(checks, tv.Visit)
|
||||
|
||||
// Run the semantic checks
|
||||
for _, check := range checks {
|
||||
if err := check(root); err != nil {
|
||||
return nil, ast.TypeInvalid, err
|
||||
}
|
||||
}
|
||||
|
||||
// Execute
|
||||
v := &executeVisitor{Scope: scope}
|
||||
return v.Visit(root)
|
||||
}
|
||||
|
||||
func (e *Engine) scope() ast.Scope {
|
||||
var scope ast.BasicScope
|
||||
if e.GlobalScope != nil {
|
||||
scope = *e.GlobalScope
|
||||
}
|
||||
|
||||
registerBuiltins(&scope)
|
||||
return &scope
|
||||
}
|
||||
|
||||
// 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 {
|
||||
Scope ast.Scope
|
||||
|
||||
stack EngineStack
|
||||
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 v.stack.Len() > 0 {
|
||||
result = v.stack.Pop()
|
||||
} else {
|
||||
result = new(ast.LiteralNode)
|
||||
}
|
||||
resultErr := v.err
|
||||
|
||||
// Clear everything else so we aren't just dangling
|
||||
v.stack.Reset()
|
||||
v.err = nil
|
||||
|
||||
t, err := result.Type(v.Scope)
|
||||
if err != nil {
|
||||
return nil, ast.TypeInvalid, err
|
||||
}
|
||||
|
||||
return result.Value, t, resultErr
|
||||
}
|
||||
|
||||
func (v *executeVisitor) visit(raw ast.Node) ast.Node {
|
||||
if v.err != nil {
|
||||
return raw
|
||||
}
|
||||
|
||||
switch n := raw.(type) {
|
||||
case *ast.Call:
|
||||
v.visitCall(n)
|
||||
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)
|
||||
}
|
||||
|
||||
return raw
|
||||
}
|
||||
|
||||
func (v *executeVisitor) visitCall(n *ast.Call) {
|
||||
// Look up the function in the map
|
||||
function, ok := v.Scope.LookupFunc(n.Func)
|
||||
if !ok {
|
||||
v.err = fmt.Errorf("unknown function called: %s", n.Func)
|
||||
return
|
||||
}
|
||||
|
||||
// The arguments are on the stack in reverse order, so pop them off.
|
||||
args := make([]interface{}, len(n.Args))
|
||||
for i, _ := range n.Args {
|
||||
node := v.stack.Pop()
|
||||
args[len(n.Args)-1-i] = node.Value
|
||||
}
|
||||
|
||||
// Call the function
|
||||
result, err := function.Callback(args)
|
||||
if err != nil {
|
||||
v.err = fmt.Errorf("%s: %s", n.Func, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Push the result
|
||||
v.stack.Push(&ast.LiteralNode{
|
||||
Value: result,
|
||||
Typex: function.ReturnType,
|
||||
})
|
||||
}
|
||||
|
||||
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.stack.Pop())
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
for i := len(nodes) - 1; i >= 0; i-- {
|
||||
buf.WriteString(nodes[i].Value.(string))
|
||||
}
|
||||
|
||||
v.stack.Push(&ast.LiteralNode{
|
||||
Value: buf.String(),
|
||||
Typex: ast.TypeString,
|
||||
})
|
||||
}
|
||||
|
||||
func (v *executeVisitor) visitLiteral(n *ast.LiteralNode) {
|
||||
v.stack.Push(n)
|
||||
}
|
||||
|
||||
func (v *executeVisitor) visitVariableAccess(n *ast.VariableAccess) {
|
||||
// Look up the variable in the map
|
||||
variable, ok := v.Scope.LookupVar(n.Name)
|
||||
if !ok {
|
||||
v.err = fmt.Errorf("unknown variable accessed: %s", n.Name)
|
||||
return
|
||||
}
|
||||
|
||||
v.stack.Push(&ast.LiteralNode{
|
||||
Value: variable.Value,
|
||||
Typex: variable.Type,
|
||||
})
|
||||
}
|
||||
|
||||
// EngineStack is a stack of ast.LiteralNodes that the Engine keeps track
|
||||
// of during execution. This is currently backed by a dumb slice, but can be
|
||||
// replaced with a better data structure at some point in the future if this
|
||||
// turns out to require optimization.
|
||||
type EngineStack struct {
|
||||
stack []*ast.LiteralNode
|
||||
}
|
||||
|
||||
func (s *EngineStack) Len() int {
|
||||
return len(s.stack)
|
||||
}
|
||||
|
||||
func (s *EngineStack) Push(n *ast.LiteralNode) {
|
||||
s.stack = append(s.stack, n)
|
||||
}
|
||||
|
||||
func (s *EngineStack) Pop() *ast.LiteralNode {
|
||||
x := s.stack[len(s.stack)-1]
|
||||
s.stack[len(s.stack)-1] = nil
|
||||
s.stack = s.stack[:len(s.stack)-1]
|
||||
return x
|
||||
}
|
||||
|
||||
func (s *EngineStack) Reset() {
|
||||
s.stack = nil
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
package lang
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/terraform/config/lang/ast"
|
||||
)
|
||||
|
||||
// EvalConfig is the configuration for evaluating.
|
||||
type EvalConfig struct {
|
||||
// GlobalScope is the global scope of execution for evaluation.
|
||||
GlobalScope *ast.BasicScope
|
||||
|
||||
// SemanticChecks is a list of additional semantic checks that will be run
|
||||
// on the tree prior to evaluating it. The type checker, identifier checker,
|
||||
// etc. will be run before these automatically.
|
||||
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
|
||||
|
||||
// Eval evaluates the given AST tree and returns its output value, the type
|
||||
// of the output, and any error that occurred.
|
||||
func Eval(root ast.Node, config *EvalConfig) (interface{}, ast.Type, error) {
|
||||
// Copy the scope so we can add our builtins
|
||||
scope := registerBuiltins(config.GlobalScope)
|
||||
implicitMap := map[ast.Type]map[ast.Type]string{
|
||||
ast.TypeInt: {
|
||||
ast.TypeString: "__builtin_IntToString",
|
||||
},
|
||||
ast.TypeString: {
|
||||
ast.TypeInt: "__builtin_StringToInt",
|
||||
},
|
||||
}
|
||||
|
||||
// Build our own semantic checks that we always run
|
||||
tv := &TypeCheck{Scope: scope, Implicit: implicitMap}
|
||||
ic := &IdentifierCheck{Scope: scope}
|
||||
|
||||
// Build up the semantic checks for execution
|
||||
checks := make(
|
||||
[]SemanticChecker,
|
||||
len(config.SemanticChecks), len(config.SemanticChecks)+2)
|
||||
copy(checks, config.SemanticChecks)
|
||||
checks = append(checks, ic.Visit)
|
||||
checks = append(checks, tv.Visit)
|
||||
|
||||
// Run the semantic checks
|
||||
for _, check := range checks {
|
||||
if err := check(root); err != nil {
|
||||
return nil, ast.TypeInvalid, err
|
||||
}
|
||||
}
|
||||
|
||||
// Execute
|
||||
v := &evalVisitor{Scope: scope}
|
||||
return v.Visit(root)
|
||||
}
|
||||
|
||||
// EvalNode is the interface that must be implemented by any ast.Node
|
||||
// to support evaluation. This will be called in visitor pattern order.
|
||||
// The result of each call to Eval is automatically pushed onto the
|
||||
// stack as a LiteralNode. Pop elements off the stack to get child
|
||||
// values.
|
||||
type EvalNode interface {
|
||||
Eval(ast.Scope, *ast.Stack) (interface{}, ast.Type, error)
|
||||
}
|
||||
|
||||
type evalVisitor struct {
|
||||
Scope ast.Scope
|
||||
Stack ast.Stack
|
||||
|
||||
err error
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (v *evalVisitor) Visit(root ast.Node) (interface{}, ast.Type, error) {
|
||||
// Run the actual visitor pattern
|
||||
root.Accept(v.visit)
|
||||
|
||||
// Get our result and clear out everything else
|
||||
var result *ast.LiteralNode
|
||||
if v.Stack.Len() > 0 {
|
||||
result = v.Stack.Pop().(*ast.LiteralNode)
|
||||
} else {
|
||||
result = new(ast.LiteralNode)
|
||||
}
|
||||
resultErr := v.err
|
||||
|
||||
// Clear everything else so we aren't just dangling
|
||||
v.Stack.Reset()
|
||||
v.err = nil
|
||||
|
||||
t, err := result.Type(v.Scope)
|
||||
if err != nil {
|
||||
return nil, ast.TypeInvalid, err
|
||||
}
|
||||
|
||||
return result.Value, t, resultErr
|
||||
}
|
||||
|
||||
func (v *evalVisitor) visit(raw ast.Node) ast.Node {
|
||||
if v.err != nil {
|
||||
return raw
|
||||
}
|
||||
|
||||
en, err := evalNode(raw)
|
||||
if err != nil {
|
||||
v.err = err
|
||||
return raw
|
||||
}
|
||||
|
||||
out, outType, err := en.Eval(v.Scope, &v.Stack)
|
||||
if err != nil {
|
||||
v.err = err
|
||||
return raw
|
||||
}
|
||||
|
||||
v.Stack.Push(&ast.LiteralNode{
|
||||
Value: out,
|
||||
Typex: outType,
|
||||
})
|
||||
return raw
|
||||
}
|
||||
|
||||
// evalNode is a private function that returns an EvalNode for built-in
|
||||
// types as well as any other EvalNode implementations.
|
||||
func evalNode(raw ast.Node) (EvalNode, error) {
|
||||
switch n := raw.(type) {
|
||||
case *ast.Call:
|
||||
return &evalCall{n}, nil
|
||||
case *ast.Concat:
|
||||
return &evalConcat{n}, nil
|
||||
case *ast.LiteralNode:
|
||||
return &evalLiteralNode{n}, nil
|
||||
case *ast.VariableAccess:
|
||||
return &evalVariableAccess{n}, nil
|
||||
default:
|
||||
en, ok := n.(EvalNode)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("node doesn't support evaluation: %#v", raw)
|
||||
}
|
||||
|
||||
return en, nil
|
||||
}
|
||||
}
|
||||
|
||||
type evalCall struct{ *ast.Call }
|
||||
|
||||
func (v *evalCall) Eval(s ast.Scope, stack *ast.Stack) (interface{}, ast.Type, error) {
|
||||
// Look up the function in the map
|
||||
function, ok := s.LookupFunc(v.Func)
|
||||
if !ok {
|
||||
return nil, ast.TypeInvalid, fmt.Errorf(
|
||||
"unknown function called: %s", v.Func)
|
||||
}
|
||||
|
||||
// The arguments are on the stack in reverse order, so pop them off.
|
||||
args := make([]interface{}, len(v.Args))
|
||||
for i, _ := range v.Args {
|
||||
node := stack.Pop().(*ast.LiteralNode)
|
||||
args[len(v.Args)-1-i] = node.Value
|
||||
}
|
||||
|
||||
// Call the function
|
||||
result, err := function.Callback(args)
|
||||
if err != nil {
|
||||
return nil, ast.TypeInvalid, fmt.Errorf("%s: %s", v.Func, err)
|
||||
}
|
||||
|
||||
return result, function.ReturnType, nil
|
||||
}
|
||||
|
||||
type evalConcat struct{ *ast.Concat }
|
||||
|
||||
func (v *evalConcat) Eval(s ast.Scope, stack *ast.Stack) (interface{}, ast.Type, error) {
|
||||
// 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(v.Exprs))
|
||||
for range v.Exprs {
|
||||
nodes = append(nodes, stack.Pop().(*ast.LiteralNode))
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
for i := len(nodes) - 1; i >= 0; i-- {
|
||||
buf.WriteString(nodes[i].Value.(string))
|
||||
}
|
||||
|
||||
return buf.String(), ast.TypeString, nil
|
||||
}
|
||||
|
||||
type evalLiteralNode struct{ *ast.LiteralNode }
|
||||
|
||||
func (v *evalLiteralNode) Eval(ast.Scope, *ast.Stack) (interface{}, ast.Type, error) {
|
||||
return v.Value, v.Typex, nil
|
||||
}
|
||||
|
||||
type evalVariableAccess struct{ *ast.VariableAccess }
|
||||
|
||||
func (v *evalVariableAccess) Eval(scope ast.Scope, _ *ast.Stack) (interface{}, ast.Type, error) {
|
||||
// Look up the variable in the map
|
||||
variable, ok := scope.LookupVar(v.Name)
|
||||
if !ok {
|
||||
return nil, ast.TypeInvalid, fmt.Errorf(
|
||||
"unknown variable accessed: %s", v.Name)
|
||||
}
|
||||
|
||||
return variable.Value, variable.Type, nil
|
||||
}
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/hashicorp/terraform/config/lang/ast"
|
||||
)
|
||||
|
||||
func TestEngineExecute(t *testing.T) {
|
||||
func TestEval(t *testing.T) {
|
||||
cases := []struct {
|
||||
Input string
|
||||
Scope *ast.BasicScope
|
||||
|
@ -121,8 +121,7 @@ func TestEngineExecute(t *testing.T) {
|
|||
t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input)
|
||||
}
|
||||
|
||||
engine := &Engine{GlobalScope: tc.Scope}
|
||||
out, outType, err := engine.Execute(node)
|
||||
out, outType, err := Eval(node, &EvalConfig{GlobalScope: tc.Scope})
|
||||
if (err != nil) != tc.Error {
|
||||
t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input)
|
||||
}
|
Loading…
Reference in New Issue