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:
Mitchell Hashimoto 2015-01-14 22:03:46 -08:00
commit d01670f49b
35 changed files with 804 additions and 649 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,3 +36,7 @@ func (n *Concat) String() string {
return b.String()
}
func (n *Concat) Type(Scope) (Type, error) {
return TypeString, nil
}

View File

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

View File

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

View File

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

65
config/lang/ast/scope.go Normal file
View File

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

View File

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

25
config/lang/ast/stack.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

216
config/lang/eval.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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