473 lines
13 KiB
Go
473 lines
13 KiB
Go
|
package hil
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"sync"
|
||
|
|
||
|
"github.com/hashicorp/hil/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
|
||
|
|
||
|
// EvaluationResult is a struct returned from the hil.Eval function,
|
||
|
// representing the result of an interpolation. Results are returned in their
|
||
|
// "natural" Go structure rather than in terms of the HIL AST. For the types
|
||
|
// currently implemented, this means that the Value field can be interpreted as
|
||
|
// the following Go types:
|
||
|
// TypeInvalid: undefined
|
||
|
// TypeString: string
|
||
|
// TypeList: []interface{}
|
||
|
// TypeMap: map[string]interface{}
|
||
|
// TypBool: bool
|
||
|
type EvaluationResult struct {
|
||
|
Type EvalType
|
||
|
Value interface{}
|
||
|
}
|
||
|
|
||
|
// InvalidResult is a structure representing the result of a HIL interpolation
|
||
|
// which has invalid syntax, missing variables, or some other type of error.
|
||
|
// The error is described out of band in the accompanying error return value.
|
||
|
var InvalidResult = EvaluationResult{Type: TypeInvalid, Value: nil}
|
||
|
|
||
|
// errExitUnknown is an internal error that when returned means the result
|
||
|
// is an unknown value. We use this for early exit.
|
||
|
var errExitUnknown = errors.New("unknown value")
|
||
|
|
||
|
func Eval(root ast.Node, config *EvalConfig) (EvaluationResult, error) {
|
||
|
output, outputType, err := internalEval(root, config)
|
||
|
if err != nil {
|
||
|
return InvalidResult, err
|
||
|
}
|
||
|
|
||
|
// If the result contains any nested unknowns then the result as a whole
|
||
|
// is unknown, so that callers only have to deal with "entirely known"
|
||
|
// or "entirely unknown" as outcomes.
|
||
|
if ast.IsUnknown(ast.Variable{Type: outputType, Value: output}) {
|
||
|
outputType = ast.TypeUnknown
|
||
|
output = UnknownValue
|
||
|
}
|
||
|
|
||
|
switch outputType {
|
||
|
case ast.TypeList:
|
||
|
val, err := VariableToInterface(ast.Variable{
|
||
|
Type: ast.TypeList,
|
||
|
Value: output,
|
||
|
})
|
||
|
return EvaluationResult{
|
||
|
Type: TypeList,
|
||
|
Value: val,
|
||
|
}, err
|
||
|
case ast.TypeMap:
|
||
|
val, err := VariableToInterface(ast.Variable{
|
||
|
Type: ast.TypeMap,
|
||
|
Value: output,
|
||
|
})
|
||
|
return EvaluationResult{
|
||
|
Type: TypeMap,
|
||
|
Value: val,
|
||
|
}, err
|
||
|
case ast.TypeString:
|
||
|
return EvaluationResult{
|
||
|
Type: TypeString,
|
||
|
Value: output,
|
||
|
}, nil
|
||
|
case ast.TypeBool:
|
||
|
return EvaluationResult{
|
||
|
Type: TypeBool,
|
||
|
Value: output,
|
||
|
}, nil
|
||
|
case ast.TypeUnknown:
|
||
|
return EvaluationResult{
|
||
|
Type: TypeUnknown,
|
||
|
Value: UnknownValue,
|
||
|
}, nil
|
||
|
default:
|
||
|
return InvalidResult, fmt.Errorf("unknown type %s as interpolation output", outputType)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Eval evaluates the given AST tree and returns its output value, the type
|
||
|
// of the output, and any error that occurred.
|
||
|
func internalEval(root ast.Node, config *EvalConfig) (interface{}, ast.Type, error) {
|
||
|
// Copy the scope so we can add our builtins
|
||
|
if config == nil {
|
||
|
config = new(EvalConfig)
|
||
|
}
|
||
|
scope := registerBuiltins(config.GlobalScope)
|
||
|
implicitMap := map[ast.Type]map[ast.Type]string{
|
||
|
ast.TypeFloat: {
|
||
|
ast.TypeInt: "__builtin_FloatToInt",
|
||
|
ast.TypeString: "__builtin_FloatToString",
|
||
|
},
|
||
|
ast.TypeInt: {
|
||
|
ast.TypeFloat: "__builtin_IntToFloat",
|
||
|
ast.TypeString: "__builtin_IntToString",
|
||
|
},
|
||
|
ast.TypeString: {
|
||
|
ast.TypeInt: "__builtin_StringToInt",
|
||
|
ast.TypeFloat: "__builtin_StringToFloat",
|
||
|
ast.TypeBool: "__builtin_StringToBool",
|
||
|
},
|
||
|
ast.TypeBool: {
|
||
|
ast.TypeString: "__builtin_BoolToString",
|
||
|
},
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
if resultErr == errExitUnknown {
|
||
|
// This means the return value is unknown and we used the error
|
||
|
// as an early exit mechanism. Reset since the value on the stack
|
||
|
// should be the unknown value.
|
||
|
resultErr = nil
|
||
|
}
|
||
|
|
||
|
// 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,
|
||
|
})
|
||
|
|
||
|
if outType == ast.TypeUnknown {
|
||
|
// Halt immediately
|
||
|
v.err = errExitUnknown
|
||
|
return raw
|
||
|
}
|
||
|
|
||
|
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.Index:
|
||
|
return &evalIndex{n}, nil
|
||
|
case *ast.Call:
|
||
|
return &evalCall{n}, nil
|
||
|
case *ast.Conditional:
|
||
|
return &evalConditional{n}, nil
|
||
|
case *ast.Output:
|
||
|
return &evalOutput{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)
|
||
|
if node.IsUnknown() {
|
||
|
// If any arguments are unknown then the result is automatically unknown
|
||
|
return UnknownValue, ast.TypeUnknown, nil
|
||
|
}
|
||
|
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 evalConditional struct{ *ast.Conditional }
|
||
|
|
||
|
func (v *evalConditional) Eval(s ast.Scope, stack *ast.Stack) (interface{}, ast.Type, error) {
|
||
|
// On the stack we have literal nodes representing the resulting values
|
||
|
// of the condition, true and false expressions, but they are in reverse
|
||
|
// order.
|
||
|
falseLit := stack.Pop().(*ast.LiteralNode)
|
||
|
trueLit := stack.Pop().(*ast.LiteralNode)
|
||
|
condLit := stack.Pop().(*ast.LiteralNode)
|
||
|
|
||
|
if condLit.IsUnknown() {
|
||
|
// If our conditional is unknown then our result is also unknown
|
||
|
return UnknownValue, ast.TypeUnknown, nil
|
||
|
}
|
||
|
|
||
|
if condLit.Value.(bool) {
|
||
|
return trueLit.Value, trueLit.Typex, nil
|
||
|
} else {
|
||
|
return falseLit.Value, trueLit.Typex, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type evalIndex struct{ *ast.Index }
|
||
|
|
||
|
func (v *evalIndex) Eval(scope ast.Scope, stack *ast.Stack) (interface{}, ast.Type, error) {
|
||
|
key := stack.Pop().(*ast.LiteralNode)
|
||
|
target := stack.Pop().(*ast.LiteralNode)
|
||
|
|
||
|
variableName := v.Index.Target.(*ast.VariableAccess).Name
|
||
|
|
||
|
if key.IsUnknown() {
|
||
|
// If our key is unknown then our result is also unknown
|
||
|
return UnknownValue, ast.TypeUnknown, nil
|
||
|
}
|
||
|
|
||
|
// For target, we'll accept collections containing unknown values but
|
||
|
// we still need to catch when the collection itself is unknown, shallowly.
|
||
|
if target.Typex == ast.TypeUnknown {
|
||
|
return UnknownValue, ast.TypeUnknown, nil
|
||
|
}
|
||
|
|
||
|
switch target.Typex {
|
||
|
case ast.TypeList:
|
||
|
return v.evalListIndex(variableName, target.Value, key.Value)
|
||
|
case ast.TypeMap:
|
||
|
return v.evalMapIndex(variableName, target.Value, key.Value)
|
||
|
default:
|
||
|
return nil, ast.TypeInvalid, fmt.Errorf(
|
||
|
"target %q for indexing must be ast.TypeList or ast.TypeMap, is %s",
|
||
|
variableName, target.Typex)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (v *evalIndex) evalListIndex(variableName string, target interface{}, key interface{}) (interface{}, ast.Type, error) {
|
||
|
// We assume type checking was already done and we can assume that target
|
||
|
// is a list and key is an int
|
||
|
list, ok := target.([]ast.Variable)
|
||
|
if !ok {
|
||
|
return nil, ast.TypeInvalid, fmt.Errorf(
|
||
|
"cannot cast target to []Variable, is: %T", target)
|
||
|
}
|
||
|
|
||
|
keyInt, ok := key.(int)
|
||
|
if !ok {
|
||
|
return nil, ast.TypeInvalid, fmt.Errorf(
|
||
|
"cannot cast key to int, is: %T", key)
|
||
|
}
|
||
|
|
||
|
if len(list) == 0 {
|
||
|
return nil, ast.TypeInvalid, fmt.Errorf("list is empty")
|
||
|
}
|
||
|
|
||
|
if keyInt < 0 || len(list) < keyInt+1 {
|
||
|
return nil, ast.TypeInvalid, fmt.Errorf(
|
||
|
"index %d out of range for list %s (max %d)",
|
||
|
keyInt, variableName, len(list))
|
||
|
}
|
||
|
|
||
|
returnVal := list[keyInt].Value
|
||
|
returnType := list[keyInt].Type
|
||
|
return returnVal, returnType, nil
|
||
|
}
|
||
|
|
||
|
func (v *evalIndex) evalMapIndex(variableName string, target interface{}, key interface{}) (interface{}, ast.Type, error) {
|
||
|
// We assume type checking was already done and we can assume that target
|
||
|
// is a map and key is a string
|
||
|
vmap, ok := target.(map[string]ast.Variable)
|
||
|
if !ok {
|
||
|
return nil, ast.TypeInvalid, fmt.Errorf(
|
||
|
"cannot cast target to map[string]Variable, is: %T", target)
|
||
|
}
|
||
|
|
||
|
keyString, ok := key.(string)
|
||
|
if !ok {
|
||
|
return nil, ast.TypeInvalid, fmt.Errorf(
|
||
|
"cannot cast key to string, is: %T", key)
|
||
|
}
|
||
|
|
||
|
if len(vmap) == 0 {
|
||
|
return nil, ast.TypeInvalid, fmt.Errorf("map is empty")
|
||
|
}
|
||
|
|
||
|
value, ok := vmap[keyString]
|
||
|
if !ok {
|
||
|
return nil, ast.TypeInvalid, fmt.Errorf(
|
||
|
"key %q does not exist in map %s", keyString, variableName)
|
||
|
}
|
||
|
|
||
|
return value.Value, value.Type, nil
|
||
|
}
|
||
|
|
||
|
type evalOutput struct{ *ast.Output }
|
||
|
|
||
|
func (v *evalOutput) 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))
|
||
|
haveUnknown := false
|
||
|
for range v.Exprs {
|
||
|
n := stack.Pop().(*ast.LiteralNode)
|
||
|
nodes = append(nodes, n)
|
||
|
|
||
|
// If we have any unknowns then the whole result is unknown
|
||
|
// (we must deal with this first, because the type checker can
|
||
|
// skip type conversions in the presence of unknowns, and thus
|
||
|
// any of our other nodes may be incorrectly typed.)
|
||
|
if n.IsUnknown() {
|
||
|
haveUnknown = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if haveUnknown {
|
||
|
return UnknownValue, ast.TypeUnknown, nil
|
||
|
}
|
||
|
|
||
|
// Special case the single list and map
|
||
|
if len(nodes) == 1 {
|
||
|
switch t := nodes[0].Typex; t {
|
||
|
case ast.TypeList:
|
||
|
fallthrough
|
||
|
case ast.TypeMap:
|
||
|
fallthrough
|
||
|
case ast.TypeUnknown:
|
||
|
return nodes[0].Value, t, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Otherwise concatenate the strings
|
||
|
var buf bytes.Buffer
|
||
|
for i := len(nodes) - 1; i >= 0; i-- {
|
||
|
if nodes[i].Typex != ast.TypeString {
|
||
|
return nil, ast.TypeInvalid, fmt.Errorf(
|
||
|
"invalid output with %s value at index %d: %#v",
|
||
|
nodes[i].Typex,
|
||
|
i,
|
||
|
nodes[i].Value,
|
||
|
)
|
||
|
}
|
||
|
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
|
||
|
}
|