Merge pull request #803 from hashicorp/f-type-eval-on-ast
Custom AST types can be type checked and evaluated
This commit is contained in:
commit
d01670f49b
|
@ -359,9 +359,10 @@ func (c *Config) Validate() error {
|
|||
r.RawCount.interpolate(func(root ast.Node) (string, error) {
|
||||
// Execute the node but transform the AST so that it returns
|
||||
// a fixed value of "5" for all interpolations.
|
||||
var engine lang.Engine
|
||||
out, _, err := engine.Execute(lang.FixedValueTransform(
|
||||
root, &ast.LiteralNode{Value: "5", Type: ast.TypeString}))
|
||||
out, _, err := lang.Eval(
|
||||
lang.FixedValueTransform(
|
||||
root, &ast.LiteralNode{Value: "5", Typex: ast.TypeString}),
|
||||
nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
@ -230,23 +230,24 @@ func DetectVariables(root ast.Node) ([]InterpolatedVariable, error) {
|
|||
var resultErr error
|
||||
|
||||
// Visitor callback
|
||||
fn := func(n ast.Node) {
|
||||
fn := func(n ast.Node) ast.Node {
|
||||
if resultErr != nil {
|
||||
return
|
||||
return n
|
||||
}
|
||||
|
||||
vn, ok := n.(*ast.VariableAccess)
|
||||
if !ok {
|
||||
return
|
||||
return n
|
||||
}
|
||||
|
||||
v, err := NewInterpolatedVariable(vn.Name)
|
||||
if err != nil {
|
||||
resultErr = err
|
||||
return
|
||||
return n
|
||||
}
|
||||
|
||||
result = append(result, v)
|
||||
return n
|
||||
}
|
||||
|
||||
// Visitor pattern
|
||||
|
|
|
@ -7,15 +7,14 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/config/lang"
|
||||
"github.com/hashicorp/terraform/config/lang/ast"
|
||||
)
|
||||
|
||||
// Funcs is the mapping of built-in functions for configuration.
|
||||
var Funcs map[string]lang.Function
|
||||
var Funcs map[string]ast.Function
|
||||
|
||||
func init() {
|
||||
Funcs = map[string]lang.Function{
|
||||
Funcs = map[string]ast.Function{
|
||||
"concat": interpolationFuncConcat(),
|
||||
"file": interpolationFuncFile(),
|
||||
"join": interpolationFuncJoin(),
|
||||
|
@ -27,8 +26,8 @@ func init() {
|
|||
// concatenates multiple strings. This isn't actually necessary anymore
|
||||
// since our language supports string concat natively, but for backwards
|
||||
// compat we do this.
|
||||
func interpolationFuncConcat() lang.Function {
|
||||
return lang.Function{
|
||||
func interpolationFuncConcat() ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString},
|
||||
ReturnType: ast.TypeString,
|
||||
Variadic: true,
|
||||
|
@ -46,8 +45,8 @@ func interpolationFuncConcat() lang.Function {
|
|||
|
||||
// interpolationFuncFile implements the "file" function that allows
|
||||
// loading contents from a file.
|
||||
func interpolationFuncFile() lang.Function {
|
||||
return lang.Function{
|
||||
func interpolationFuncFile() ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString},
|
||||
ReturnType: ast.TypeString,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
|
@ -63,8 +62,8 @@ func interpolationFuncFile() lang.Function {
|
|||
|
||||
// interpolationFuncJoin implements the "join" function that allows
|
||||
// multi-variable values to be joined by some character.
|
||||
func interpolationFuncJoin() lang.Function {
|
||||
return lang.Function{
|
||||
func interpolationFuncJoin() ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString},
|
||||
ReturnType: ast.TypeString,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
|
@ -81,8 +80,8 @@ func interpolationFuncJoin() lang.Function {
|
|||
|
||||
// interpolationFuncLookup implements the "lookup" function that allows
|
||||
// dynamic lookups of map types within a Terraform configuration.
|
||||
func interpolationFuncLookup(vs map[string]lang.Variable) lang.Function {
|
||||
return lang.Function{
|
||||
func interpolationFuncLookup(vs map[string]ast.Variable) ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString},
|
||||
ReturnType: ast.TypeString,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
|
@ -107,8 +106,8 @@ func interpolationFuncLookup(vs map[string]lang.Variable) lang.Function {
|
|||
// interpolationFuncElement implements the "element" function that allows
|
||||
// a specific index to be looked up in a multi-variable value. Note that this will
|
||||
// wrap if the index is larger than the number of elements in the multi-variable value.
|
||||
func interpolationFuncElement() lang.Function {
|
||||
return lang.Function{
|
||||
func interpolationFuncElement() ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString, ast.TypeString},
|
||||
ReturnType: ast.TypeString,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
|
|
|
@ -109,8 +109,8 @@ func TestInterpolateFuncJoin(t *testing.T) {
|
|||
|
||||
func TestInterpolateFuncLookup(t *testing.T) {
|
||||
testFunction(t, testFunctionConfig{
|
||||
Vars: map[string]lang.Variable{
|
||||
"var.foo.bar": lang.Variable{
|
||||
Vars: map[string]ast.Variable{
|
||||
"var.foo.bar": ast.Variable{
|
||||
Value: "baz",
|
||||
Type: ast.TypeString,
|
||||
},
|
||||
|
@ -176,7 +176,7 @@ func TestInterpolateFuncElement(t *testing.T) {
|
|||
|
||||
type testFunctionConfig struct {
|
||||
Cases []testFunctionCase
|
||||
Vars map[string]lang.Variable
|
||||
Vars map[string]ast.Variable
|
||||
}
|
||||
|
||||
type testFunctionCase struct {
|
||||
|
@ -192,8 +192,7 @@ func testFunction(t *testing.T, config testFunctionConfig) {
|
|||
t.Fatalf("%d: err: %s", i, err)
|
||||
}
|
||||
|
||||
engine := langEngine(config.Vars)
|
||||
out, _, err := engine.Execute(ast)
|
||||
out, _, err := lang.Eval(ast, langEvalConfig(config.Vars))
|
||||
if (err != nil) != tc.Error {
|
||||
t.Fatalf("%d: err: %s", i, err)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,9 @@ type Node interface {
|
|||
|
||||
// Pos returns the position of this node in some source.
|
||||
Pos() Pos
|
||||
|
||||
// Type returns the type of this node for the given context.
|
||||
Type(Scope) (Type, error)
|
||||
}
|
||||
|
||||
// Pos is the starting position of an AST node
|
||||
|
@ -40,7 +43,7 @@ type Visitor func(Node) Node
|
|||
|
||||
//go:generate stringer -type=Type
|
||||
|
||||
// Type is the type of a literal.
|
||||
// Type is the type of any value.
|
||||
type Type uint32
|
||||
|
||||
const (
|
||||
|
|
|
@ -32,3 +32,12 @@ func (n *Call) String() string {
|
|||
|
||||
return fmt.Sprintf("Call(%s, %s)", n.Func, strings.Join(args, ", "))
|
||||
}
|
||||
|
||||
func (n *Call) Type(s Scope) (Type, error) {
|
||||
f, ok := s.LookupFunc(n.Func)
|
||||
if !ok {
|
||||
return TypeInvalid, fmt.Errorf("unknown function: %s", n.Func)
|
||||
}
|
||||
|
||||
return f.ReturnType, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package ast
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCallType(t *testing.T) {
|
||||
c := &Call{Func: "foo"}
|
||||
scope := &BasicScope{
|
||||
FuncMap: map[string]Function{
|
||||
"foo": Function{ReturnType: TypeString},
|
||||
},
|
||||
}
|
||||
|
||||
actual, err := c.Type(scope)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
if actual != TypeString {
|
||||
t.Fatalf("bad: %s", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallType_invalid(t *testing.T) {
|
||||
c := &Call{Func: "bar"}
|
||||
scope := &BasicScope{
|
||||
FuncMap: map[string]Function{
|
||||
"foo": Function{ReturnType: TypeString},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := c.Type(scope)
|
||||
if err == nil {
|
||||
t.Fatal("should error")
|
||||
}
|
||||
}
|
|
@ -36,3 +36,7 @@ func (n *Concat) String() string {
|
|||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (n *Concat) Type(Scope) (Type, error) {
|
||||
return TypeString, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package ast
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConcatType(t *testing.T) {
|
||||
c := &Concat{}
|
||||
actual, err := c.Type(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
if actual != TypeString {
|
||||
t.Fatalf("bad: %s", actual)
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import (
|
|||
// 42 or 3.14159. Based on the Type, the Value can be safely cast.
|
||||
type LiteralNode struct {
|
||||
Value interface{}
|
||||
Type Type
|
||||
Typex Type
|
||||
Posx Pos
|
||||
}
|
||||
|
||||
|
@ -25,5 +25,9 @@ func (n *LiteralNode) GoString() string {
|
|||
}
|
||||
|
||||
func (n *LiteralNode) String() string {
|
||||
return fmt.Sprintf("Literal(%s, %v)", n.Type, n.Value)
|
||||
return fmt.Sprintf("Literal(%s, %v)", n.Typex, n.Value)
|
||||
}
|
||||
|
||||
func (n *LiteralNode) Type(Scope) (Type, error) {
|
||||
return n.Typex, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package ast
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLiteralNodeType(t *testing.T) {
|
||||
c := &LiteralNode{Typex: TypeString}
|
||||
actual, err := c.Type(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
if actual != TypeString {
|
||||
t.Fatalf("bad: %s", actual)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package ast
|
||||
|
||||
// Scope is the interface used to look up variables and functions while
|
||||
// evaluating. How these functions/variables are defined are up to the caller.
|
||||
type Scope interface {
|
||||
LookupFunc(string) (Function, bool)
|
||||
LookupVar(string) (Variable, bool)
|
||||
}
|
||||
|
||||
// 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 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 {
|
||||
// ArgTypes is the list of types in argument order. These are the
|
||||
// required arguments.
|
||||
//
|
||||
// ReturnType is the type of the returned value. The Callback MUST
|
||||
// return this type.
|
||||
ArgTypes []Type
|
||||
ReturnType Type
|
||||
|
||||
// Variadic, if true, says that this function is variadic, meaning
|
||||
// it takes a variable number of arguments. In this case, the
|
||||
// VariadicType must be set.
|
||||
Variadic bool
|
||||
VariadicType Type
|
||||
|
||||
// Callback is the function called for a function. The argument
|
||||
// types are guaranteed to match the spec above by the type checker.
|
||||
// The length of the args is strictly == len(ArgTypes) unless Varidiac
|
||||
// is true, in which case its >= len(ArgTypes).
|
||||
Callback func([]interface{}) (interface{}, error)
|
||||
}
|
||||
|
||||
// BasicScope is a simple scope that looks up variables and functions
|
||||
// using a map.
|
||||
type BasicScope struct {
|
||||
FuncMap map[string]Function
|
||||
VarMap map[string]Variable
|
||||
}
|
||||
|
||||
func (s *BasicScope) LookupFunc(n string) (Function, bool) {
|
||||
if s == nil {
|
||||
return Function{}, false
|
||||
}
|
||||
|
||||
v, ok := s.FuncMap[n]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (s *BasicScope) LookupVar(n string) (Variable, bool) {
|
||||
if s == nil {
|
||||
return Variable{}, false
|
||||
}
|
||||
|
||||
v, ok := s.VarMap[n]
|
||||
return v, ok
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package ast
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBasicScope_impl(t *testing.T) {
|
||||
var _ Scope = new(BasicScope)
|
||||
}
|
||||
|
||||
func TestBasicScopeLookupFunc(t *testing.T) {
|
||||
scope := &BasicScope{
|
||||
FuncMap: map[string]Function{
|
||||
"foo": Function{},
|
||||
},
|
||||
}
|
||||
|
||||
if _, ok := scope.LookupFunc("bar"); ok {
|
||||
t.Fatal("should not find bar")
|
||||
}
|
||||
if _, ok := scope.LookupFunc("foo"); !ok {
|
||||
t.Fatal("should find foo")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicScopeLookupVar(t *testing.T) {
|
||||
scope := &BasicScope{
|
||||
VarMap: map[string]Variable{
|
||||
"foo": Variable{},
|
||||
},
|
||||
}
|
||||
|
||||
if _, ok := scope.LookupVar("bar"); ok {
|
||||
t.Fatal("should not find bar")
|
||||
}
|
||||
if _, ok := scope.LookupVar("foo"); !ok {
|
||||
t.Fatal("should find foo")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package ast
|
||||
|
||||
// Stack is a stack of Node.
|
||||
type Stack struct {
|
||||
stack []Node
|
||||
}
|
||||
|
||||
func (s *Stack) Len() int {
|
||||
return len(s.stack)
|
||||
}
|
||||
|
||||
func (s *Stack) Push(n Node) {
|
||||
s.stack = append(s.stack, n)
|
||||
}
|
||||
|
||||
func (s *Stack) Pop() Node {
|
||||
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 *Stack) Reset() {
|
||||
s.stack = nil
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package ast
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStack(t *testing.T) {
|
||||
var s Stack
|
||||
if s.Len() != 0 {
|
||||
t.Fatalf("bad: %d", s.Len())
|
||||
}
|
||||
|
||||
n := &LiteralNode{Value: 42}
|
||||
s.Push(n)
|
||||
|
||||
if s.Len() != 1 {
|
||||
t.Fatalf("bad: %d", s.Len())
|
||||
}
|
||||
|
||||
actual := s.Pop()
|
||||
if !reflect.DeepEqual(actual, n) {
|
||||
t.Fatalf("bad: %#v", actual)
|
||||
}
|
||||
|
||||
if s.Len() != 0 {
|
||||
t.Fatalf("bad: %d", s.Len())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStack_reset(t *testing.T) {
|
||||
var s Stack
|
||||
|
||||
n := &LiteralNode{Value: 42}
|
||||
s.Push(n)
|
||||
|
||||
if s.Len() != 1 {
|
||||
t.Fatalf("bad: %d", s.Len())
|
||||
}
|
||||
|
||||
s.Reset()
|
||||
|
||||
if s.Len() != 0 {
|
||||
t.Fatalf("bad: %d", s.Len())
|
||||
}
|
||||
}
|
|
@ -25,3 +25,12 @@ func (n *VariableAccess) GoString() string {
|
|||
func (n *VariableAccess) String() string {
|
||||
return fmt.Sprintf("Variable(%s)", n.Name)
|
||||
}
|
||||
|
||||
func (n *VariableAccess) Type(s Scope) (Type, error) {
|
||||
v, ok := s.LookupVar(n.Name)
|
||||
if !ok {
|
||||
return TypeInvalid, fmt.Errorf("unknown variable: %s", n.Name)
|
||||
}
|
||||
|
||||
return v.Type, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package ast
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVariableAccessType(t *testing.T) {
|
||||
c := &VariableAccess{Name: "foo"}
|
||||
scope := &BasicScope{
|
||||
VarMap: map[string]Variable{
|
||||
"foo": Variable{Type: TypeString},
|
||||
},
|
||||
}
|
||||
|
||||
actual, err := c.Type(scope)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
if actual != TypeString {
|
||||
t.Fatalf("bad: %s", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVariableAccessType_invalid(t *testing.T) {
|
||||
c := &VariableAccess{Name: "bar"}
|
||||
scope := &BasicScope{
|
||||
VarMap: map[string]Variable{
|
||||
"foo": Variable{Type: TypeString},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := c.Type(scope)
|
||||
if err == nil {
|
||||
t.Fatal("should error")
|
||||
}
|
||||
}
|
|
@ -8,16 +8,20 @@ import (
|
|||
|
||||
// NOTE: All builtins are tested in engine_test.go
|
||||
|
||||
func registerBuiltins(scope *Scope) {
|
||||
func registerBuiltins(scope *ast.BasicScope) *ast.BasicScope {
|
||||
if scope == nil {
|
||||
scope = new(ast.BasicScope)
|
||||
}
|
||||
if scope.FuncMap == nil {
|
||||
scope.FuncMap = make(map[string]Function)
|
||||
scope.FuncMap = make(map[string]ast.Function)
|
||||
}
|
||||
scope.FuncMap["__builtin_IntToString"] = builtinIntToString()
|
||||
scope.FuncMap["__builtin_StringToInt"] = builtinStringToInt()
|
||||
return scope
|
||||
}
|
||||
|
||||
func builtinIntToString() Function {
|
||||
return Function{
|
||||
func builtinIntToString() ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeInt},
|
||||
ReturnType: ast.TypeString,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
|
@ -26,8 +30,8 @@ func builtinIntToString() Function {
|
|||
}
|
||||
}
|
||||
|
||||
func builtinStringToInt() Function {
|
||||
return Function{
|
||||
func builtinStringToInt() ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeInt},
|
||||
ReturnType: ast.TypeString,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
// resolve properly and that the right number of arguments are passed
|
||||
// to functions.
|
||||
type IdentifierCheck struct {
|
||||
Scope *Scope
|
||||
Scope ast.Scope
|
||||
|
||||
err error
|
||||
lock sync.Mutex
|
||||
|
@ -40,7 +40,7 @@ func (c *IdentifierCheck) visit(raw ast.Node) ast.Node {
|
|||
case *ast.LiteralNode:
|
||||
// Ignore
|
||||
default:
|
||||
c.createErr(n, fmt.Sprintf("unknown node: %#v", raw))
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// We never do replacement with this visitor
|
||||
|
|
|
@ -9,20 +9,20 @@ import (
|
|||
func TestIdentifierCheck(t *testing.T) {
|
||||
cases := []struct {
|
||||
Input string
|
||||
Scope *Scope
|
||||
Scope ast.Scope
|
||||
Error bool
|
||||
}{
|
||||
{
|
||||
"foo",
|
||||
&Scope{},
|
||||
&ast.BasicScope{},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"foo ${bar} success",
|
||||
&Scope{
|
||||
VarMap: map[string]Variable{
|
||||
"bar": Variable{
|
||||
&ast.BasicScope{
|
||||
VarMap: map[string]ast.Variable{
|
||||
"bar": ast.Variable{
|
||||
Value: "baz",
|
||||
Type: ast.TypeString,
|
||||
},
|
||||
|
@ -33,15 +33,15 @@ func TestIdentifierCheck(t *testing.T) {
|
|||
|
||||
{
|
||||
"foo ${bar}",
|
||||
&Scope{},
|
||||
&ast.BasicScope{},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
"foo ${rand()} success",
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"rand": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"rand": ast.Function{
|
||||
ReturnType: ast.TypeString,
|
||||
Callback: func([]interface{}) (interface{}, error) {
|
||||
return "42", nil
|
||||
|
@ -54,15 +54,15 @@ func TestIdentifierCheck(t *testing.T) {
|
|||
|
||||
{
|
||||
"foo ${rand()}",
|
||||
&Scope{},
|
||||
&ast.BasicScope{},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
"foo ${rand(42)} ",
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"rand": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"rand": ast.Function{
|
||||
ReturnType: ast.TypeString,
|
||||
Callback: func([]interface{}) (interface{}, error) {
|
||||
return "42", nil
|
||||
|
@ -75,9 +75,9 @@ func TestIdentifierCheck(t *testing.T) {
|
|||
|
||||
{
|
||||
"foo ${rand()} ",
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"rand": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"rand": ast.Function{
|
||||
ReturnType: ast.TypeString,
|
||||
Variadic: true,
|
||||
VariadicType: ast.TypeInt,
|
||||
|
@ -92,9 +92,9 @@ func TestIdentifierCheck(t *testing.T) {
|
|||
|
||||
{
|
||||
"foo ${rand(42)} ",
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"rand": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"rand": ast.Function{
|
||||
ReturnType: ast.TypeString,
|
||||
Variadic: true,
|
||||
VariadicType: ast.TypeInt,
|
||||
|
@ -109,9 +109,9 @@ func TestIdentifierCheck(t *testing.T) {
|
|||
|
||||
{
|
||||
"foo ${rand(\"foo\", 42)} ",
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"rand": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"rand": ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString},
|
||||
ReturnType: ast.TypeString,
|
||||
Variadic: true,
|
||||
|
|
|
@ -16,7 +16,7 @@ import (
|
|||
// this structure but we'd rather do that than duplicate the type checking
|
||||
// logic multiple times.
|
||||
type TypeCheck struct {
|
||||
Scope *Scope
|
||||
Scope ast.Scope
|
||||
|
||||
// Implicit is a map of implicit type conversions that we can do,
|
||||
// and that shouldn't error. The key of the first map is the from type,
|
||||
|
@ -24,9 +24,19 @@ type TypeCheck struct {
|
|||
// value is the function to call (which must be registered in the Scope).
|
||||
Implicit map[ast.Type]map[ast.Type]string
|
||||
|
||||
stack []ast.Type
|
||||
err error
|
||||
lock sync.Mutex
|
||||
// Stack of types. This shouldn't be used directly except by implementations
|
||||
// of TypeCheckNode.
|
||||
Stack []ast.Type
|
||||
|
||||
err error
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// TypeCheckNode is the interface that must be implemented by any
|
||||
// ast.Node that wants to support type-checking. If the type checker
|
||||
// encounters a node that doesn't implement this, it will error.
|
||||
type TypeCheckNode interface {
|
||||
TypeCheck(*TypeCheck) (ast.Node, error)
|
||||
}
|
||||
|
||||
func (v *TypeCheck) Visit(root ast.Node) error {
|
||||
|
@ -42,49 +52,69 @@ func (v *TypeCheck) visit(raw ast.Node) ast.Node {
|
|||
return raw
|
||||
}
|
||||
|
||||
var result ast.Node
|
||||
var err error
|
||||
switch n := raw.(type) {
|
||||
case *ast.Call:
|
||||
v.visitCall(n)
|
||||
tc := &typeCheckCall{n}
|
||||
result, err = tc.TypeCheck(v)
|
||||
case *ast.Concat:
|
||||
v.visitConcat(n)
|
||||
tc := &typeCheckConcat{n}
|
||||
result, err = tc.TypeCheck(v)
|
||||
case *ast.LiteralNode:
|
||||
v.visitLiteral(n)
|
||||
tc := &typeCheckLiteral{n}
|
||||
result, err = tc.TypeCheck(v)
|
||||
case *ast.VariableAccess:
|
||||
v.visitVariableAccess(n)
|
||||
tc := &typeCheckVariableAccess{n}
|
||||
result, err = tc.TypeCheck(v)
|
||||
default:
|
||||
v.createErr(n, fmt.Sprintf("unknown node: %#v", raw))
|
||||
tc, ok := raw.(TypeCheckNode)
|
||||
if !ok {
|
||||
err = fmt.Errorf("unknown node: %#v", raw)
|
||||
break
|
||||
}
|
||||
|
||||
result, err = tc.TypeCheck(v)
|
||||
}
|
||||
|
||||
return raw
|
||||
if err != nil {
|
||||
pos := raw.Pos()
|
||||
v.err = fmt.Errorf("At column %d, line %d: %s",
|
||||
pos.Column, pos.Line, err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (v *TypeCheck) visitCall(n *ast.Call) {
|
||||
type typeCheckCall struct {
|
||||
n *ast.Call
|
||||
}
|
||||
|
||||
func (tc *typeCheckCall) TypeCheck(v *TypeCheck) (ast.Node, error) {
|
||||
// Look up the function in the map
|
||||
function, ok := v.Scope.LookupFunc(n.Func)
|
||||
function, ok := v.Scope.LookupFunc(tc.n.Func)
|
||||
if !ok {
|
||||
v.createErr(n, fmt.Sprintf("unknown function called: %s", n.Func))
|
||||
return
|
||||
return nil, fmt.Errorf("unknown function called: %s", tc.n.Func)
|
||||
}
|
||||
|
||||
// The arguments are on the stack in reverse order, so pop them off.
|
||||
args := make([]ast.Type, len(n.Args))
|
||||
for i, _ := range n.Args {
|
||||
args[len(n.Args)-1-i] = v.stackPop()
|
||||
args := make([]ast.Type, len(tc.n.Args))
|
||||
for i, _ := range tc.n.Args {
|
||||
args[len(tc.n.Args)-1-i] = v.StackPop()
|
||||
}
|
||||
|
||||
// Verify the args
|
||||
for i, expected := range function.ArgTypes {
|
||||
if args[i] != expected {
|
||||
cn := v.implicitConversion(args[i], expected, n.Args[i])
|
||||
cn := v.ImplicitConversion(args[i], expected, tc.n.Args[i])
|
||||
if cn != nil {
|
||||
n.Args[i] = cn
|
||||
tc.n.Args[i] = cn
|
||||
continue
|
||||
}
|
||||
|
||||
v.createErr(n, fmt.Sprintf(
|
||||
return nil, fmt.Errorf(
|
||||
"%s: argument %d should be %s, got %s",
|
||||
n.Func, i+1, expected, args[i]))
|
||||
return
|
||||
tc.n.Func, i+1, expected, args[i])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,75 +124,86 @@ func (v *TypeCheck) visitCall(n *ast.Call) {
|
|||
for i, t := range args {
|
||||
if t != function.VariadicType {
|
||||
realI := i + len(function.ArgTypes)
|
||||
cn := v.implicitConversion(
|
||||
t, function.VariadicType, n.Args[realI])
|
||||
cn := v.ImplicitConversion(
|
||||
t, function.VariadicType, tc.n.Args[realI])
|
||||
if cn != nil {
|
||||
n.Args[realI] = cn
|
||||
tc.n.Args[realI] = cn
|
||||
continue
|
||||
}
|
||||
|
||||
v.createErr(n, fmt.Sprintf(
|
||||
return nil, fmt.Errorf(
|
||||
"%s: argument %d should be %s, got %s",
|
||||
n.Func, realI,
|
||||
function.VariadicType, t))
|
||||
return
|
||||
tc.n.Func, realI,
|
||||
function.VariadicType, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return type
|
||||
v.stackPush(function.ReturnType)
|
||||
v.StackPush(function.ReturnType)
|
||||
|
||||
return tc.n, nil
|
||||
}
|
||||
|
||||
func (v *TypeCheck) visitConcat(n *ast.Concat) {
|
||||
type typeCheckConcat struct {
|
||||
n *ast.Concat
|
||||
}
|
||||
|
||||
func (tc *typeCheckConcat) TypeCheck(v *TypeCheck) (ast.Node, error) {
|
||||
n := tc.n
|
||||
types := make([]ast.Type, len(n.Exprs))
|
||||
for i, _ := range n.Exprs {
|
||||
types[len(n.Exprs)-1-i] = v.stackPop()
|
||||
types[len(n.Exprs)-1-i] = v.StackPop()
|
||||
}
|
||||
|
||||
// All concat args must be strings, so validate that
|
||||
for i, t := range types {
|
||||
if t != ast.TypeString {
|
||||
cn := v.implicitConversion(t, ast.TypeString, n.Exprs[i])
|
||||
cn := v.ImplicitConversion(t, ast.TypeString, n.Exprs[i])
|
||||
if cn != nil {
|
||||
n.Exprs[i] = cn
|
||||
continue
|
||||
}
|
||||
|
||||
v.createErr(n, fmt.Sprintf(
|
||||
"argument %d must be a string", i+1))
|
||||
return
|
||||
return nil, fmt.Errorf(
|
||||
"argument %d must be a string", i+1)
|
||||
}
|
||||
}
|
||||
|
||||
// This always results in type string
|
||||
v.stackPush(ast.TypeString)
|
||||
v.StackPush(ast.TypeString)
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (v *TypeCheck) visitLiteral(n *ast.LiteralNode) {
|
||||
v.stackPush(n.Type)
|
||||
type typeCheckLiteral struct {
|
||||
n *ast.LiteralNode
|
||||
}
|
||||
|
||||
func (v *TypeCheck) visitVariableAccess(n *ast.VariableAccess) {
|
||||
func (tc *typeCheckLiteral) TypeCheck(v *TypeCheck) (ast.Node, error) {
|
||||
v.StackPush(tc.n.Typex)
|
||||
return tc.n, nil
|
||||
}
|
||||
|
||||
type typeCheckVariableAccess struct {
|
||||
n *ast.VariableAccess
|
||||
}
|
||||
|
||||
func (tc *typeCheckVariableAccess) TypeCheck(v *TypeCheck) (ast.Node, error) {
|
||||
// Look up the variable in the map
|
||||
variable, ok := v.Scope.LookupVar(n.Name)
|
||||
variable, ok := v.Scope.LookupVar(tc.n.Name)
|
||||
if !ok {
|
||||
v.createErr(n, fmt.Sprintf(
|
||||
"unknown variable accessed: %s", n.Name))
|
||||
return
|
||||
return nil, fmt.Errorf(
|
||||
"unknown variable accessed: %s", tc.n.Name)
|
||||
}
|
||||
|
||||
// Add the type to the stack
|
||||
v.stackPush(variable.Type)
|
||||
v.StackPush(variable.Type)
|
||||
|
||||
return tc.n, nil
|
||||
}
|
||||
|
||||
func (v *TypeCheck) createErr(n ast.Node, str string) {
|
||||
pos := n.Pos()
|
||||
v.err = fmt.Errorf("At column %d, line %d: %s",
|
||||
pos.Column, pos.Line, str)
|
||||
}
|
||||
|
||||
func (v *TypeCheck) implicitConversion(
|
||||
func (v *TypeCheck) ImplicitConversion(
|
||||
actual ast.Type, expected ast.Type, n ast.Node) ast.Node {
|
||||
if v.Implicit == nil {
|
||||
return nil
|
||||
|
@ -186,16 +227,16 @@ func (v *TypeCheck) implicitConversion(
|
|||
}
|
||||
|
||||
func (v *TypeCheck) reset() {
|
||||
v.stack = nil
|
||||
v.Stack = nil
|
||||
v.err = nil
|
||||
}
|
||||
|
||||
func (v *TypeCheck) stackPush(t ast.Type) {
|
||||
v.stack = append(v.stack, t)
|
||||
func (v *TypeCheck) StackPush(t ast.Type) {
|
||||
v.Stack = append(v.Stack, t)
|
||||
}
|
||||
|
||||
func (v *TypeCheck) stackPop() ast.Type {
|
||||
func (v *TypeCheck) StackPop() ast.Type {
|
||||
var x ast.Type
|
||||
x, v.stack = v.stack[len(v.stack)-1], v.stack[:len(v.stack)-1]
|
||||
x, v.Stack = v.Stack[len(v.Stack)-1], v.Stack[:len(v.Stack)-1]
|
||||
return x
|
||||
}
|
||||
|
|
|
@ -9,20 +9,20 @@ import (
|
|||
func TestTypeCheck(t *testing.T) {
|
||||
cases := []struct {
|
||||
Input string
|
||||
Scope *Scope
|
||||
Scope ast.Scope
|
||||
Error bool
|
||||
}{
|
||||
{
|
||||
"foo",
|
||||
&Scope{},
|
||||
&ast.BasicScope{},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"foo ${bar}",
|
||||
&Scope{
|
||||
VarMap: map[string]Variable{
|
||||
"bar": Variable{
|
||||
&ast.BasicScope{
|
||||
VarMap: map[string]ast.Variable{
|
||||
"bar": ast.Variable{
|
||||
Value: "baz",
|
||||
Type: ast.TypeString,
|
||||
},
|
||||
|
@ -33,9 +33,9 @@ func TestTypeCheck(t *testing.T) {
|
|||
|
||||
{
|
||||
"foo ${rand()}",
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"rand": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"rand": ast.Function{
|
||||
ReturnType: ast.TypeString,
|
||||
Callback: func([]interface{}) (interface{}, error) {
|
||||
return "42", nil
|
||||
|
@ -48,9 +48,9 @@ func TestTypeCheck(t *testing.T) {
|
|||
|
||||
{
|
||||
`foo ${rand("42")}`,
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"rand": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"rand": ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString},
|
||||
ReturnType: ast.TypeString,
|
||||
Callback: func([]interface{}) (interface{}, error) {
|
||||
|
@ -64,9 +64,9 @@ func TestTypeCheck(t *testing.T) {
|
|||
|
||||
{
|
||||
`foo ${rand(42)}`,
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"rand": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"rand": ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString},
|
||||
ReturnType: ast.TypeString,
|
||||
Callback: func([]interface{}) (interface{}, error) {
|
||||
|
@ -80,9 +80,9 @@ func TestTypeCheck(t *testing.T) {
|
|||
|
||||
{
|
||||
`foo ${rand()}`,
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"rand": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"rand": ast.Function{
|
||||
ArgTypes: nil,
|
||||
ReturnType: ast.TypeString,
|
||||
Variadic: true,
|
||||
|
@ -98,9 +98,9 @@ func TestTypeCheck(t *testing.T) {
|
|||
|
||||
{
|
||||
`foo ${rand("42")}`,
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"rand": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"rand": ast.Function{
|
||||
ArgTypes: nil,
|
||||
ReturnType: ast.TypeString,
|
||||
Variadic: true,
|
||||
|
@ -116,9 +116,9 @@ func TestTypeCheck(t *testing.T) {
|
|||
|
||||
{
|
||||
`foo ${rand("42", 42)}`,
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"rand": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"rand": ast.Function{
|
||||
ArgTypes: nil,
|
||||
ReturnType: ast.TypeString,
|
||||
Variadic: true,
|
||||
|
@ -134,9 +134,9 @@ func TestTypeCheck(t *testing.T) {
|
|||
|
||||
{
|
||||
"foo ${bar}",
|
||||
&Scope{
|
||||
VarMap: map[string]Variable{
|
||||
"bar": Variable{
|
||||
&ast.BasicScope{
|
||||
VarMap: map[string]ast.Variable{
|
||||
"bar": ast.Variable{
|
||||
Value: 42,
|
||||
Type: ast.TypeInt,
|
||||
},
|
||||
|
@ -147,9 +147,9 @@ func TestTypeCheck(t *testing.T) {
|
|||
|
||||
{
|
||||
"foo ${rand()}",
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"rand": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"rand": ast.Function{
|
||||
ReturnType: ast.TypeInt,
|
||||
Callback: func([]interface{}) (interface{}, error) {
|
||||
return 42, nil
|
||||
|
@ -184,14 +184,14 @@ func TestTypeCheck_implicit(t *testing.T) {
|
|||
|
||||
cases := []struct {
|
||||
Input string
|
||||
Scope *Scope
|
||||
Scope *ast.BasicScope
|
||||
Error bool
|
||||
}{
|
||||
{
|
||||
"foo ${bar}",
|
||||
&Scope{
|
||||
VarMap: map[string]Variable{
|
||||
"bar": Variable{
|
||||
&ast.BasicScope{
|
||||
VarMap: map[string]ast.Variable{
|
||||
"bar": ast.Variable{
|
||||
Value: 42,
|
||||
Type: ast.TypeInt,
|
||||
},
|
||||
|
@ -202,9 +202,9 @@ func TestTypeCheck_implicit(t *testing.T) {
|
|||
|
||||
{
|
||||
"foo ${foo(42)}",
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"foo": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"foo": ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString},
|
||||
ReturnType: ast.TypeString,
|
||||
},
|
||||
|
@ -215,9 +215,9 @@ func TestTypeCheck_implicit(t *testing.T) {
|
|||
|
||||
{
|
||||
`foo ${foo("42", 42)}`,
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"foo": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"foo": ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeString},
|
||||
Variadic: true,
|
||||
VariadicType: ast.TypeString,
|
||||
|
@ -237,9 +237,9 @@ func TestTypeCheck_implicit(t *testing.T) {
|
|||
|
||||
// Modify the scope to add our conversion functions.
|
||||
if tc.Scope.FuncMap == nil {
|
||||
tc.Scope.FuncMap = make(map[string]Function)
|
||||
tc.Scope.FuncMap = make(map[string]ast.Function)
|
||||
}
|
||||
tc.Scope.FuncMap["intToString"] = Function{
|
||||
tc.Scope.FuncMap["intToString"] = ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeInt},
|
||||
ReturnType: ast.TypeString,
|
||||
}
|
||||
|
|
|
@ -1,282 +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 *Scope
|
||||
|
||||
// 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() *Scope {
|
||||
var scope Scope
|
||||
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 *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
|
||||
|
||||
return result.Value, result.Type, 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,
|
||||
Type: 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(),
|
||||
Type: 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,
|
||||
Type: 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
|
||||
}
|
||||
|
||||
// Scope represents a lookup scope for execution.
|
||||
type Scope struct {
|
||||
// VarMap and FuncMap are the mappings of identifiers to functions
|
||||
// and variable values.
|
||||
VarMap map[string]Variable
|
||||
FuncMap map[string]Function
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// ArgTypes is the list of types in argument order. These are the
|
||||
// required arguments.
|
||||
//
|
||||
// ReturnType is the type of the returned value. The Callback MUST
|
||||
// return this type.
|
||||
ArgTypes []ast.Type
|
||||
ReturnType ast.Type
|
||||
|
||||
// Variadic, if true, says that this function is variadic, meaning
|
||||
// it takes a variable number of arguments. In this case, the
|
||||
// VariadicType must be set.
|
||||
Variadic bool
|
||||
VariadicType ast.Type
|
||||
|
||||
// Callback is the function called for a function. The argument
|
||||
// types are guaranteed to match the spec above by the type checker.
|
||||
// The length of the args is strictly == len(ArgTypes) unless Varidiac
|
||||
// is true, in which case its >= len(ArgTypes).
|
||||
Callback func([]interface{}) (interface{}, error)
|
||||
}
|
||||
|
||||
// LookupFunc will look up a variable by name.
|
||||
// TODO test
|
||||
func (s *Scope) LookupFunc(n string) (Function, bool) {
|
||||
if s == nil {
|
||||
return Function{}, false
|
||||
}
|
||||
|
||||
v, ok := s.FuncMap[n]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// LookupVar will look up a variable by name.
|
||||
// TODO test
|
||||
func (s *Scope) LookupVar(n string) (Variable, bool) {
|
||||
if s == nil {
|
||||
return Variable{}, false
|
||||
}
|
||||
|
||||
v, ok := s.VarMap[n]
|
||||
return v, ok
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
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
|
||||
if config == nil {
|
||||
config = new(EvalConfig)
|
||||
}
|
||||
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,10 +8,10 @@ import (
|
|||
"github.com/hashicorp/terraform/config/lang/ast"
|
||||
)
|
||||
|
||||
func TestEngineExecute(t *testing.T) {
|
||||
func TestEval(t *testing.T) {
|
||||
cases := []struct {
|
||||
Input string
|
||||
Scope *Scope
|
||||
Scope *ast.BasicScope
|
||||
Error bool
|
||||
Result interface{}
|
||||
ResultType ast.Type
|
||||
|
@ -26,9 +26,9 @@ func TestEngineExecute(t *testing.T) {
|
|||
|
||||
{
|
||||
"foo ${bar}",
|
||||
&Scope{
|
||||
VarMap: map[string]Variable{
|
||||
"bar": Variable{
|
||||
&ast.BasicScope{
|
||||
VarMap: map[string]ast.Variable{
|
||||
"bar": ast.Variable{
|
||||
Value: "baz",
|
||||
Type: ast.TypeString,
|
||||
},
|
||||
|
@ -41,9 +41,9 @@ func TestEngineExecute(t *testing.T) {
|
|||
|
||||
{
|
||||
"foo ${rand()}",
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"rand": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"rand": ast.Function{
|
||||
ReturnType: ast.TypeString,
|
||||
Callback: func([]interface{}) (interface{}, error) {
|
||||
return "42", nil
|
||||
|
@ -58,9 +58,9 @@ func TestEngineExecute(t *testing.T) {
|
|||
|
||||
{
|
||||
`foo ${rand("foo", "bar")}`,
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"rand": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"rand": ast.Function{
|
||||
ReturnType: ast.TypeString,
|
||||
Variadic: true,
|
||||
VariadicType: ast.TypeString,
|
||||
|
@ -83,9 +83,9 @@ func TestEngineExecute(t *testing.T) {
|
|||
|
||||
{
|
||||
"foo ${bar}",
|
||||
&Scope{
|
||||
VarMap: map[string]Variable{
|
||||
"bar": Variable{
|
||||
&ast.BasicScope{
|
||||
VarMap: map[string]ast.Variable{
|
||||
"bar": ast.Variable{
|
||||
Value: 42,
|
||||
Type: ast.TypeInt,
|
||||
},
|
||||
|
@ -98,9 +98,9 @@ func TestEngineExecute(t *testing.T) {
|
|||
|
||||
{
|
||||
`foo ${foo("42")}`,
|
||||
&Scope{
|
||||
FuncMap: map[string]Function{
|
||||
"foo": Function{
|
||||
&ast.BasicScope{
|
||||
FuncMap: map[string]ast.Function{
|
||||
"foo": ast.Function{
|
||||
ArgTypes: []ast.Type{ast.TypeInt},
|
||||
ReturnType: ast.TypeString,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
|
@ -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)
|
||||
}
|
|
@ -33,7 +33,7 @@ top:
|
|||
{
|
||||
parserResult = &ast.LiteralNode{
|
||||
Value: "",
|
||||
Type: ast.TypeString,
|
||||
Typex: ast.TypeString,
|
||||
Posx: ast.Pos{Column: 1, Line: 1},
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ top:
|
|||
// it makes for an easy literal check later (to check if a string
|
||||
// has any interpolations).
|
||||
if _, ok := $1.(*ast.Concat); !ok {
|
||||
if n, ok := $1.(*ast.LiteralNode); !ok || n.Type != ast.TypeString {
|
||||
if n, ok := $1.(*ast.LiteralNode); !ok || n.Typex != ast.TypeString {
|
||||
parserResult = &ast.Concat{
|
||||
Exprs: []ast.Node{$1},
|
||||
Posx: $1.Pos(),
|
||||
|
@ -104,7 +104,7 @@ expr:
|
|||
{
|
||||
$$ = &ast.LiteralNode{
|
||||
Value: $1.Value.(int),
|
||||
Type: ast.TypeInt,
|
||||
Typex: ast.TypeInt,
|
||||
Posx: $1.Pos,
|
||||
}
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ expr:
|
|||
{
|
||||
$$ = &ast.LiteralNode{
|
||||
Value: $1.Value.(float64),
|
||||
Type: ast.TypeFloat,
|
||||
Typex: ast.TypeFloat,
|
||||
Posx: $1.Pos,
|
||||
}
|
||||
}
|
||||
|
@ -143,7 +143,7 @@ literal:
|
|||
{
|
||||
$$ = &ast.LiteralNode{
|
||||
Value: $1.Value.(string),
|
||||
Type: ast.TypeString,
|
||||
Typex: ast.TypeString,
|
||||
Posx: $1.Pos,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ func TestParse(t *testing.T) {
|
|||
false,
|
||||
&ast.LiteralNode{
|
||||
Value: "",
|
||||
Type: ast.TypeString,
|
||||
Typex: ast.TypeString,
|
||||
Posx: ast.Pos{Column: 1, Line: 1},
|
||||
},
|
||||
},
|
||||
|
@ -28,7 +28,7 @@ func TestParse(t *testing.T) {
|
|||
false,
|
||||
&ast.LiteralNode{
|
||||
Value: "foo",
|
||||
Type: ast.TypeString,
|
||||
Typex: ast.TypeString,
|
||||
Posx: ast.Pos{Column: 1, Line: 1},
|
||||
},
|
||||
},
|
||||
|
@ -38,7 +38,7 @@ func TestParse(t *testing.T) {
|
|||
false,
|
||||
&ast.LiteralNode{
|
||||
Value: "${var.foo}",
|
||||
Type: ast.TypeString,
|
||||
Typex: ast.TypeString,
|
||||
Posx: ast.Pos{Column: 1, Line: 1},
|
||||
},
|
||||
},
|
||||
|
@ -51,7 +51,7 @@ func TestParse(t *testing.T) {
|
|||
Exprs: []ast.Node{
|
||||
&ast.LiteralNode{
|
||||
Value: "foo ",
|
||||
Type: ast.TypeString,
|
||||
Typex: ast.TypeString,
|
||||
Posx: ast.Pos{Column: 1, Line: 1},
|
||||
},
|
||||
&ast.VariableAccess{
|
||||
|
@ -70,7 +70,7 @@ func TestParse(t *testing.T) {
|
|||
Exprs: []ast.Node{
|
||||
&ast.LiteralNode{
|
||||
Value: "foo ",
|
||||
Type: ast.TypeString,
|
||||
Typex: ast.TypeString,
|
||||
Posx: ast.Pos{Column: 1, Line: 1},
|
||||
},
|
||||
&ast.VariableAccess{
|
||||
|
@ -79,7 +79,7 @@ func TestParse(t *testing.T) {
|
|||
},
|
||||
&ast.LiteralNode{
|
||||
Value: " baz",
|
||||
Type: ast.TypeString,
|
||||
Typex: ast.TypeString,
|
||||
Posx: ast.Pos{Column: 15, Line: 1},
|
||||
},
|
||||
},
|
||||
|
@ -94,12 +94,12 @@ func TestParse(t *testing.T) {
|
|||
Exprs: []ast.Node{
|
||||
&ast.LiteralNode{
|
||||
Value: "foo ",
|
||||
Type: ast.TypeString,
|
||||
Typex: ast.TypeString,
|
||||
Posx: ast.Pos{Column: 1, Line: 1},
|
||||
},
|
||||
&ast.LiteralNode{
|
||||
Value: "bar",
|
||||
Type: ast.TypeString,
|
||||
Typex: ast.TypeString,
|
||||
Posx: ast.Pos{Column: 7, Line: 1},
|
||||
},
|
||||
},
|
||||
|
@ -114,12 +114,12 @@ func TestParse(t *testing.T) {
|
|||
Exprs: []ast.Node{
|
||||
&ast.LiteralNode{
|
||||
Value: "foo ",
|
||||
Type: ast.TypeString,
|
||||
Typex: ast.TypeString,
|
||||
Posx: ast.Pos{Column: 1, Line: 1},
|
||||
},
|
||||
&ast.LiteralNode{
|
||||
Value: 42,
|
||||
Type: ast.TypeInt,
|
||||
Typex: ast.TypeInt,
|
||||
Posx: ast.Pos{Column: 7, Line: 1},
|
||||
},
|
||||
},
|
||||
|
@ -134,12 +134,12 @@ func TestParse(t *testing.T) {
|
|||
Exprs: []ast.Node{
|
||||
&ast.LiteralNode{
|
||||
Value: "foo ",
|
||||
Type: ast.TypeString,
|
||||
Typex: ast.TypeString,
|
||||
Posx: ast.Pos{Column: 1, Line: 1},
|
||||
},
|
||||
&ast.LiteralNode{
|
||||
Value: 3.14159,
|
||||
Type: ast.TypeFloat,
|
||||
Typex: ast.TypeFloat,
|
||||
Posx: ast.Pos{Column: 7, Line: 1},
|
||||
},
|
||||
},
|
||||
|
@ -239,7 +239,7 @@ func TestParse(t *testing.T) {
|
|||
Exprs: []ast.Node{
|
||||
&ast.LiteralNode{
|
||||
Value: "foo ",
|
||||
Type: ast.TypeString,
|
||||
Typex: ast.TypeString,
|
||||
Posx: ast.Pos{Column: 1, Line: 1},
|
||||
},
|
||||
&ast.Concat{
|
||||
|
@ -247,7 +247,7 @@ func TestParse(t *testing.T) {
|
|||
Exprs: []ast.Node{
|
||||
&ast.LiteralNode{
|
||||
Value: "bar ",
|
||||
Type: ast.TypeString,
|
||||
Typex: ast.TypeString,
|
||||
Posx: ast.Pos{Column: 7, Line: 1},
|
||||
},
|
||||
&ast.VariableAccess{
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
package lang
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/config/lang/ast"
|
||||
)
|
||||
|
||||
// LookupType looks up the type of the given node with the given scope.
|
||||
func LookupType(raw ast.Node, scope *Scope) (ast.Type, error) {
|
||||
switch n := raw.(type) {
|
||||
case *ast.LiteralNode:
|
||||
return typedLiteralNode{n}.Type(scope)
|
||||
case *ast.VariableAccess:
|
||||
return typedVariableAccess{n}.Type(scope)
|
||||
default:
|
||||
if t, ok := raw.(TypedNode); ok {
|
||||
return t.Type(scope)
|
||||
}
|
||||
|
||||
return ast.TypeInvalid, fmt.Errorf(
|
||||
"unknown node to get type of: %T", raw)
|
||||
}
|
||||
}
|
||||
|
||||
// TypedNode is an interface that custom AST nodes should implement
|
||||
// if they want to work with LookupType. All the builtin AST nodes have
|
||||
// implementations of this.
|
||||
type TypedNode interface {
|
||||
Type(*Scope) (ast.Type, error)
|
||||
}
|
||||
|
||||
type typedLiteralNode struct {
|
||||
n *ast.LiteralNode
|
||||
}
|
||||
|
||||
func (n typedLiteralNode) Type(s *Scope) (ast.Type, error) {
|
||||
return n.n.Type, nil
|
||||
}
|
||||
|
||||
type typedVariableAccess struct {
|
||||
n *ast.VariableAccess
|
||||
}
|
||||
|
||||
func (n typedVariableAccess) Type(s *Scope) (ast.Type, error) {
|
||||
v, ok := s.LookupVar(n.n.Name)
|
||||
if !ok {
|
||||
return ast.TypeInvalid, fmt.Errorf(
|
||||
"%s: couldn't find variable %s", n.n.Pos(), n.n.Name)
|
||||
}
|
||||
|
||||
return v.Type, nil
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
package lang
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/config/lang/ast"
|
||||
)
|
||||
|
||||
func TestLookupType(t *testing.T) {
|
||||
cases := []struct {
|
||||
Input ast.Node
|
||||
Scope *Scope
|
||||
Output ast.Type
|
||||
Error bool
|
||||
}{
|
||||
{
|
||||
&customUntyped{},
|
||||
nil,
|
||||
ast.TypeInvalid,
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&customTyped{},
|
||||
nil,
|
||||
ast.TypeString,
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
&ast.LiteralNode{
|
||||
Value: 42,
|
||||
Type: ast.TypeInt,
|
||||
},
|
||||
nil,
|
||||
ast.TypeInt,
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
&ast.VariableAccess{
|
||||
Name: "foo",
|
||||
},
|
||||
&Scope{
|
||||
VarMap: map[string]Variable{
|
||||
"foo": Variable{Type: ast.TypeInt},
|
||||
},
|
||||
},
|
||||
ast.TypeInt,
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
actual, err := LookupType(tc.Input, tc.Scope)
|
||||
if (err != nil) != tc.Error {
|
||||
t.Fatalf("bad: %s\n\nInput: %#v", err, tc.Input)
|
||||
}
|
||||
if actual != tc.Output {
|
||||
t.Fatalf("bad: %s\n\nInput: %#v", actual, tc.Input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type customUntyped struct{}
|
||||
|
||||
func (n customUntyped) Accept(ast.Visitor) ast.Node { return n }
|
||||
func (n customUntyped) Pos() (v ast.Pos) { return }
|
||||
|
||||
type customTyped struct{}
|
||||
|
||||
func (n customTyped) Accept(ast.Visitor) ast.Node { return n }
|
||||
func (n customTyped) Pos() (v ast.Pos) { return }
|
||||
func (n customTyped) Type(*Scope) (ast.Type, error) { return ast.TypeString, nil }
|
|
@ -346,7 +346,7 @@ parserdefault:
|
|||
{
|
||||
parserResult = &ast.LiteralNode{
|
||||
Value: "",
|
||||
Type: ast.TypeString,
|
||||
Typex: ast.TypeString,
|
||||
Posx: ast.Pos{Column: 1, Line: 1},
|
||||
}
|
||||
}
|
||||
|
@ -364,7 +364,7 @@ parserdefault:
|
|||
// it makes for an easy literal check later (to check if a string
|
||||
// has any interpolations).
|
||||
if _, ok := parserS[parserpt-0].node.(*ast.Concat); !ok {
|
||||
if n, ok := parserS[parserpt-0].node.(*ast.LiteralNode); !ok || n.Type != ast.TypeString {
|
||||
if n, ok := parserS[parserpt-0].node.(*ast.LiteralNode); !ok || n.Typex != ast.TypeString {
|
||||
parserResult = &ast.Concat{
|
||||
Exprs: []ast.Node{parserS[parserpt-0].node},
|
||||
Posx: parserS[parserpt-0].node.Pos(),
|
||||
|
@ -417,7 +417,7 @@ parserdefault:
|
|||
{
|
||||
parserVAL.node = &ast.LiteralNode{
|
||||
Value: parserS[parserpt-0].token.Value.(int),
|
||||
Type: ast.TypeInt,
|
||||
Typex: ast.TypeInt,
|
||||
Posx: parserS[parserpt-0].token.Pos,
|
||||
}
|
||||
}
|
||||
|
@ -426,7 +426,7 @@ parserdefault:
|
|||
{
|
||||
parserVAL.node = &ast.LiteralNode{
|
||||
Value: parserS[parserpt-0].token.Value.(float64),
|
||||
Type: ast.TypeFloat,
|
||||
Typex: ast.TypeFloat,
|
||||
Posx: parserS[parserpt-0].token.Pos,
|
||||
}
|
||||
}
|
||||
|
@ -460,7 +460,7 @@ parserdefault:
|
|||
{
|
||||
parserVAL.node = &ast.LiteralNode{
|
||||
Value: parserS[parserpt-0].token.Value.(string),
|
||||
Type: ast.TypeString,
|
||||
Typex: ast.TypeString,
|
||||
Posx: parserS[parserpt-0].token.Pos,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,10 +80,10 @@ func (r *RawConfig) Config() map[string]interface{} {
|
|||
// Any prior calls to Interpolate are replaced with this one.
|
||||
//
|
||||
// If a variable key is missing, this will panic.
|
||||
func (r *RawConfig) Interpolate(vs map[string]lang.Variable) error {
|
||||
engine := langEngine(vs)
|
||||
func (r *RawConfig) Interpolate(vs map[string]ast.Variable) error {
|
||||
config := langEvalConfig(vs)
|
||||
return r.interpolate(func(root ast.Node) (string, error) {
|
||||
out, _, err := engine.Execute(root)
|
||||
out, _, err := lang.Eval(root, config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -202,16 +202,16 @@ type gobRawConfig struct {
|
|||
Raw map[string]interface{}
|
||||
}
|
||||
|
||||
// langEngine returns the lang.Engine to use for evaluating configurations.
|
||||
func langEngine(vs map[string]lang.Variable) *lang.Engine {
|
||||
funcMap := make(map[string]lang.Function)
|
||||
// langEvalConfig returns the evaluation configuration we use to execute.
|
||||
func langEvalConfig(vs map[string]ast.Variable) *lang.EvalConfig {
|
||||
funcMap := make(map[string]ast.Function)
|
||||
for k, v := range Funcs {
|
||||
funcMap[k] = v
|
||||
}
|
||||
funcMap["lookup"] = interpolationFuncLookup(vs)
|
||||
|
||||
return &lang.Engine{
|
||||
GlobalScope: &lang.Scope{
|
||||
return &lang.EvalConfig{
|
||||
GlobalScope: &ast.BasicScope{
|
||||
VarMap: vs,
|
||||
FuncMap: funcMap,
|
||||
},
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/config/lang"
|
||||
"github.com/hashicorp/terraform/config/lang/ast"
|
||||
)
|
||||
|
||||
|
@ -43,8 +42,8 @@ func TestRawConfig(t *testing.T) {
|
|||
t.Fatalf("bad: %#v", rc.Config())
|
||||
}
|
||||
|
||||
vars := map[string]lang.Variable{
|
||||
"var.bar": lang.Variable{
|
||||
vars := map[string]ast.Variable{
|
||||
"var.bar": ast.Variable{
|
||||
Value: "baz",
|
||||
Type: ast.TypeString,
|
||||
},
|
||||
|
@ -76,8 +75,8 @@ func TestRawConfig_double(t *testing.T) {
|
|||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
vars := map[string]lang.Variable{
|
||||
"var.bar": lang.Variable{
|
||||
vars := map[string]ast.Variable{
|
||||
"var.bar": ast.Variable{
|
||||
Value: "baz",
|
||||
Type: ast.TypeString,
|
||||
},
|
||||
|
@ -95,8 +94,8 @@ func TestRawConfig_double(t *testing.T) {
|
|||
t.Fatalf("bad: %#v", actual)
|
||||
}
|
||||
|
||||
vars = map[string]lang.Variable{
|
||||
"var.bar": lang.Variable{
|
||||
vars = map[string]ast.Variable{
|
||||
"var.bar": ast.Variable{
|
||||
Value: "what",
|
||||
Type: ast.TypeString,
|
||||
},
|
||||
|
@ -135,8 +134,8 @@ func TestRawConfig_unknown(t *testing.T) {
|
|||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
vars := map[string]lang.Variable{
|
||||
"var.bar": lang.Variable{
|
||||
vars := map[string]ast.Variable{
|
||||
"var.bar": ast.Variable{
|
||||
Value: UnknownVariableValue,
|
||||
Type: ast.TypeString,
|
||||
},
|
||||
|
@ -178,8 +177,8 @@ func TestRawConfigValue(t *testing.T) {
|
|||
t.Fatalf("err: %#v", rc.Value())
|
||||
}
|
||||
|
||||
vars := map[string]lang.Variable{
|
||||
"var.bar": lang.Variable{
|
||||
vars := map[string]ast.Variable{
|
||||
"var.bar": ast.Variable{
|
||||
Value: "baz",
|
||||
Type: ast.TypeString,
|
||||
},
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/config/lang"
|
||||
"github.com/hashicorp/terraform/config/lang/ast"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
@ -23,9 +22,9 @@ func testConfig(
|
|||
}
|
||||
|
||||
if len(vs) > 0 {
|
||||
vars := make(map[string]lang.Variable)
|
||||
vars := make(map[string]ast.Variable)
|
||||
for k, v := range vs {
|
||||
vars[k] = lang.Variable{Value: v, Type: ast.TypeString}
|
||||
vars[k] = ast.Variable{Value: v, Type: ast.TypeString}
|
||||
}
|
||||
|
||||
if err := rc.Interpolate(vars); err != nil {
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/config/lang"
|
||||
"github.com/hashicorp/terraform/config/lang/ast"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
@ -1808,9 +1807,9 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
}
|
||||
|
||||
if len(tc.ConfigVariables) > 0 {
|
||||
vars := make(map[string]lang.Variable)
|
||||
vars := make(map[string]ast.Variable)
|
||||
for k, v := range tc.ConfigVariables {
|
||||
vars[k] = lang.Variable{Value: v, Type: ast.TypeString}
|
||||
vars[k] = ast.Variable{Value: v, Type: ast.TypeString}
|
||||
}
|
||||
|
||||
if err := c.Interpolate(vars); err != nil {
|
||||
|
@ -2585,9 +2584,9 @@ func TestSchemaMap_Validate(t *testing.T) {
|
|||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
if tc.Vars != nil {
|
||||
vars := make(map[string]lang.Variable)
|
||||
vars := make(map[string]ast.Variable)
|
||||
for k, v := range tc.Vars {
|
||||
vars[k] = lang.Variable{Value: v, Type: ast.TypeString}
|
||||
vars[k] = ast.Variable{Value: v, Type: ast.TypeString}
|
||||
}
|
||||
|
||||
if err := c.Interpolate(vars); err != nil {
|
||||
|
|
|
@ -11,7 +11,6 @@ import (
|
|||
"sync/atomic"
|
||||
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/config/lang"
|
||||
"github.com/hashicorp/terraform/config/lang/ast"
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
"github.com/hashicorp/terraform/depgraph"
|
||||
|
@ -1522,9 +1521,9 @@ func (c *walkContext) computeVars(
|
|||
}
|
||||
|
||||
// Copy the default variables
|
||||
vs := make(map[string]lang.Variable)
|
||||
vs := make(map[string]ast.Variable)
|
||||
for k, v := range c.defaultVariables {
|
||||
vs[k] = lang.Variable{
|
||||
vs[k] = ast.Variable{
|
||||
Value: v,
|
||||
Type: ast.TypeString,
|
||||
}
|
||||
|
@ -1537,7 +1536,7 @@ func (c *walkContext) computeVars(
|
|||
switch v.Type {
|
||||
case config.CountValueIndex:
|
||||
if r != nil {
|
||||
vs[n] = lang.Variable{
|
||||
vs[n] = ast.Variable{
|
||||
Value: int(r.CountIndex),
|
||||
Type: ast.TypeInt,
|
||||
}
|
||||
|
@ -1545,7 +1544,7 @@ func (c *walkContext) computeVars(
|
|||
}
|
||||
case *config.ModuleVariable:
|
||||
if c.Operation == walkValidate {
|
||||
vs[n] = lang.Variable{
|
||||
vs[n] = ast.Variable{
|
||||
Value: config.UnknownVariableValue,
|
||||
Type: ast.TypeString,
|
||||
}
|
||||
|
@ -1557,7 +1556,7 @@ func (c *walkContext) computeVars(
|
|||
return err
|
||||
}
|
||||
|
||||
vs[n] = lang.Variable{
|
||||
vs[n] = ast.Variable{
|
||||
Value: value,
|
||||
Type: ast.TypeString,
|
||||
}
|
||||
|
@ -1571,26 +1570,26 @@ func (c *walkContext) computeVars(
|
|||
v.FullKey(), err)
|
||||
}
|
||||
|
||||
vs[n] = lang.Variable{
|
||||
vs[n] = ast.Variable{
|
||||
Value: wd,
|
||||
Type: ast.TypeString,
|
||||
}
|
||||
case config.PathValueModule:
|
||||
if t := c.Context.module.Child(c.Path[1:]); t != nil {
|
||||
vs[n] = lang.Variable{
|
||||
vs[n] = ast.Variable{
|
||||
Value: t.Config().Dir,
|
||||
Type: ast.TypeString,
|
||||
}
|
||||
}
|
||||
case config.PathValueRoot:
|
||||
vs[n] = lang.Variable{
|
||||
vs[n] = ast.Variable{
|
||||
Value: c.Context.module.Config().Dir,
|
||||
Type: ast.TypeString,
|
||||
}
|
||||
}
|
||||
case *config.ResourceVariable:
|
||||
if c.Operation == walkValidate {
|
||||
vs[n] = lang.Variable{
|
||||
vs[n] = ast.Variable{
|
||||
Value: config.UnknownVariableValue,
|
||||
Type: ast.TypeString,
|
||||
}
|
||||
|
@ -1608,14 +1607,14 @@ func (c *walkContext) computeVars(
|
|||
return err
|
||||
}
|
||||
|
||||
vs[n] = lang.Variable{
|
||||
vs[n] = ast.Variable{
|
||||
Value: attr,
|
||||
Type: ast.TypeString,
|
||||
}
|
||||
case *config.UserVariable:
|
||||
val, ok := c.Variables[v.Name]
|
||||
if ok {
|
||||
vs[n] = lang.Variable{
|
||||
vs[n] = ast.Variable{
|
||||
Value: val,
|
||||
Type: ast.TypeString,
|
||||
}
|
||||
|
@ -1623,7 +1622,7 @@ func (c *walkContext) computeVars(
|
|||
}
|
||||
|
||||
if _, ok := vs[n]; !ok && c.Operation == walkValidate {
|
||||
vs[n] = lang.Variable{
|
||||
vs[n] = ast.Variable{
|
||||
Value: config.UnknownVariableValue,
|
||||
Type: ast.TypeString,
|
||||
}
|
||||
|
@ -1634,7 +1633,7 @@ func (c *walkContext) computeVars(
|
|||
// those are map overrides. Include those.
|
||||
for k, val := range c.Variables {
|
||||
if strings.HasPrefix(k, v.Name+".") {
|
||||
vs["var."+k] = lang.Variable{
|
||||
vs["var."+k] = ast.Variable{
|
||||
Value: val,
|
||||
Type: ast.TypeString,
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue