From c4273974dea4e51fa4666ccf7d27a849dd3d0b2a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 14 Jan 2015 20:13:35 -0800 Subject: [PATCH 1/7] config/lang/ast: introduce Type --- config/lang/ast/ast.go | 11 ++++- config/lang/ast/call.go | 9 ++++ config/lang/ast/call_test.go | 36 ++++++++++++++ config/lang/ast/concat.go | 4 ++ config/lang/ast/concat_test.go | 16 ++++++ config/lang/ast/literal.go | 6 ++- config/lang/ast/literal_test.go | 16 ++++++ config/lang/ast/scope.go | 65 +++++++++++++++++++++++++ config/lang/ast/scope_test.go | 39 +++++++++++++++ config/lang/ast/stack.go | 25 ++++++++++ config/lang/ast/stack_test.go | 46 +++++++++++++++++ config/lang/ast/variable_access.go | 9 ++++ config/lang/ast/variable_access_test.go | 36 ++++++++++++++ config/lang/engine.go | 32 ------------ 14 files changed, 316 insertions(+), 34 deletions(-) create mode 100644 config/lang/ast/call_test.go create mode 100644 config/lang/ast/concat_test.go create mode 100644 config/lang/ast/literal_test.go create mode 100644 config/lang/ast/scope.go create mode 100644 config/lang/ast/scope_test.go create mode 100644 config/lang/ast/stack.go create mode 100644 config/lang/ast/stack_test.go create mode 100644 config/lang/ast/variable_access_test.go diff --git a/config/lang/ast/ast.go b/config/lang/ast/ast.go index dfe863f99..0d2465b90 100644 --- a/config/lang/ast/ast.go +++ b/config/lang/ast/ast.go @@ -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 @@ -23,6 +26,12 @@ func (p Pos) String() string { return fmt.Sprintf("%d:%d", p.Line, p.Column) } +// EvalContext is the context given for evaluation. +type EvalContext struct { + Scope Scope + Stack Stack +} + // Visitors are just implementations of this function. // // The function must return the Node to replace this node with. "nil" is @@ -40,7 +49,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 ( diff --git a/config/lang/ast/call.go b/config/lang/ast/call.go index bbb632b7b..ace1147a6 100644 --- a/config/lang/ast/call.go +++ b/config/lang/ast/call.go @@ -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 +} diff --git a/config/lang/ast/call_test.go b/config/lang/ast/call_test.go new file mode 100644 index 000000000..ef63888d2 --- /dev/null +++ b/config/lang/ast/call_test.go @@ -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") + } +} diff --git a/config/lang/ast/concat.go b/config/lang/ast/concat.go index 871b0f44a..0246a3bc1 100644 --- a/config/lang/ast/concat.go +++ b/config/lang/ast/concat.go @@ -36,3 +36,7 @@ func (n *Concat) String() string { return b.String() } + +func (n *Concat) Type(Scope) (Type, error) { + return TypeString, nil +} diff --git a/config/lang/ast/concat_test.go b/config/lang/ast/concat_test.go new file mode 100644 index 000000000..65fa67601 --- /dev/null +++ b/config/lang/ast/concat_test.go @@ -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) + } +} diff --git a/config/lang/ast/literal.go b/config/lang/ast/literal.go index b314fcc21..9da3ff3a3 100644 --- a/config/lang/ast/literal.go +++ b/config/lang/ast/literal.go @@ -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 } @@ -27,3 +27,7 @@ func (n *LiteralNode) GoString() string { func (n *LiteralNode) String() string { return fmt.Sprintf("Literal(%s, %v)", n.Type, n.Value) } + +func (n *LiteralNode) Type(Scope) (Type, error) { + return n.Typex, nil +} diff --git a/config/lang/ast/literal_test.go b/config/lang/ast/literal_test.go new file mode 100644 index 000000000..2759d7722 --- /dev/null +++ b/config/lang/ast/literal_test.go @@ -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) + } +} diff --git a/config/lang/ast/scope.go b/config/lang/ast/scope.go new file mode 100644 index 000000000..77c3ee79a --- /dev/null +++ b/config/lang/ast/scope.go @@ -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 +} diff --git a/config/lang/ast/scope_test.go b/config/lang/ast/scope_test.go new file mode 100644 index 000000000..b1484a1fd --- /dev/null +++ b/config/lang/ast/scope_test.go @@ -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") + } +} diff --git a/config/lang/ast/stack.go b/config/lang/ast/stack.go new file mode 100644 index 000000000..bd2bc1578 --- /dev/null +++ b/config/lang/ast/stack.go @@ -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 +} diff --git a/config/lang/ast/stack_test.go b/config/lang/ast/stack_test.go new file mode 100644 index 000000000..95a9d9255 --- /dev/null +++ b/config/lang/ast/stack_test.go @@ -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()) + } +} diff --git a/config/lang/ast/variable_access.go b/config/lang/ast/variable_access.go index 148094a6a..4c1362d75 100644 --- a/config/lang/ast/variable_access.go +++ b/config/lang/ast/variable_access.go @@ -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 +} diff --git a/config/lang/ast/variable_access_test.go b/config/lang/ast/variable_access_test.go new file mode 100644 index 000000000..1880bc514 --- /dev/null +++ b/config/lang/ast/variable_access_test.go @@ -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") + } +} diff --git a/config/lang/engine.go b/config/lang/engine.go index b18db0f39..9bcdb5563 100644 --- a/config/lang/engine.go +++ b/config/lang/engine.go @@ -227,38 +227,6 @@ type Scope struct { 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) { From c96b3b9ddc4a06b5baf0d992ee4fa3284cf396aa Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 14 Jan 2015 20:37:16 -0800 Subject: [PATCH 2/7] config/lang/ast: Eval --- config/lang/ast/ast.go | 8 +++++++- config/lang/ast/call.go | 19 +++++++++++++++++++ config/lang/ast/call_test.go | 21 +++++++++++++++++++++ config/lang/ast/concat.go | 14 ++++++++++++++ config/lang/ast/concat_test.go | 18 ++++++++++++++++++ config/lang/ast/literal.go | 4 ++++ config/lang/ast/literal_test.go | 13 +++++++++++++ config/lang/ast/variable_access.go | 9 +++++++++ config/lang/ast/variable_access_test.go | 17 +++++++++++++++++ 9 files changed, 122 insertions(+), 1 deletion(-) diff --git a/config/lang/ast/ast.go b/config/lang/ast/ast.go index 0d2465b90..7f88beaa3 100644 --- a/config/lang/ast/ast.go +++ b/config/lang/ast/ast.go @@ -15,6 +15,10 @@ type Node interface { // Type returns the type of this node for the given context. Type(Scope) (Type, error) + + // Eval evaluates this node, returning its final value. The type + // of the final value will match the result of Type with the same scope. + Eval(*EvalContext) (interface{}, error) } // Pos is the starting position of an AST node @@ -27,9 +31,11 @@ func (p Pos) String() string { } // EvalContext is the context given for evaluation. +// +// It is simple for now with just a Scope but we use a struct in case we +// plan on adding fields in the future. type EvalContext struct { Scope Scope - Stack Stack } // Visitors are just implementations of this function. diff --git a/config/lang/ast/call.go b/config/lang/ast/call.go index ace1147a6..fda7d3e62 100644 --- a/config/lang/ast/call.go +++ b/config/lang/ast/call.go @@ -41,3 +41,22 @@ func (n *Call) Type(s Scope) (Type, error) { return f.ReturnType, nil } + +func (n *Call) Eval(ctx *EvalContext) (interface{}, error) { + f, ok := ctx.Scope.LookupFunc(n.Func) + if !ok { + return TypeInvalid, fmt.Errorf("unknown function: %s", n.Func) + } + + args := make([]interface{}, len(n.Args)) + for i, arg := range n.Args { + result, err := arg.Eval(ctx) + if err != nil { + return nil, err + } + + args[i] = result + } + + return f.Callback(args) +} diff --git a/config/lang/ast/call_test.go b/config/lang/ast/call_test.go index ef63888d2..b08a67d4b 100644 --- a/config/lang/ast/call_test.go +++ b/config/lang/ast/call_test.go @@ -34,3 +34,24 @@ func TestCallType_invalid(t *testing.T) { t.Fatal("should error") } } + +func TestCallEval(t *testing.T) { + c := &Call{Func: "foo"} + scope := &BasicScope{ + FuncMap: map[string]Function{ + "foo": Function{ + Callback: func([]interface{}) (interface{}, error) { + return "42", nil + }, + }, + }, + } + + actual, err := c.Eval(&EvalContext{Scope: scope}) + if err != nil { + t.Fatalf("err: %s", err) + } + if actual != "42" { + t.Fatalf("bad: %s", actual) + } +} diff --git a/config/lang/ast/concat.go b/config/lang/ast/concat.go index 0246a3bc1..03d406d6b 100644 --- a/config/lang/ast/concat.go +++ b/config/lang/ast/concat.go @@ -40,3 +40,17 @@ func (n *Concat) String() string { func (n *Concat) Type(Scope) (Type, error) { return TypeString, nil } + +func (n *Concat) Eval(ctx *EvalContext) (interface{}, error) { + var b bytes.Buffer + for _, expr := range n.Exprs { + result, err := expr.Eval(ctx) + if err != nil { + return nil, err + } + + b.WriteString(result.(string)) + } + + return b.String(), nil +} diff --git a/config/lang/ast/concat_test.go b/config/lang/ast/concat_test.go index 65fa67601..111092270 100644 --- a/config/lang/ast/concat_test.go +++ b/config/lang/ast/concat_test.go @@ -14,3 +14,21 @@ func TestConcatType(t *testing.T) { t.Fatalf("bad: %s", actual) } } + +func TestConcatEval(t *testing.T) { + c := &Concat{ + Exprs: []Node{ + &LiteralNode{Value: "foo"}, + &LiteralNode{Value: "bar"}, + }, + } + scope := &BasicScope{} + + actual, err := c.Eval(&EvalContext{Scope: scope}) + if err != nil { + t.Fatalf("err: %s", err) + } + if actual != "foobar" { + t.Fatalf("bad: %s", actual) + } +} diff --git a/config/lang/ast/literal.go b/config/lang/ast/literal.go index 9da3ff3a3..da1985023 100644 --- a/config/lang/ast/literal.go +++ b/config/lang/ast/literal.go @@ -31,3 +31,7 @@ func (n *LiteralNode) String() string { func (n *LiteralNode) Type(Scope) (Type, error) { return n.Typex, nil } + +func (n *LiteralNode) Eval(*EvalContext) (interface{}, error) { + return n.Value, nil +} diff --git a/config/lang/ast/literal_test.go b/config/lang/ast/literal_test.go index 2759d7722..51eedaf94 100644 --- a/config/lang/ast/literal_test.go +++ b/config/lang/ast/literal_test.go @@ -14,3 +14,16 @@ func TestLiteralNodeType(t *testing.T) { t.Fatalf("bad: %s", actual) } } + +func TestLiteralNodeEval(t *testing.T) { + c := &LiteralNode{Value: "42", Typex: TypeString} + scope := &BasicScope{} + + actual, err := c.Eval(&EvalContext{Scope: scope}) + if err != nil { + t.Fatalf("err: %s", err) + } + if actual != "42" { + t.Fatalf("bad: %s", actual) + } +} diff --git a/config/lang/ast/variable_access.go b/config/lang/ast/variable_access.go index 4c1362d75..8ed150d87 100644 --- a/config/lang/ast/variable_access.go +++ b/config/lang/ast/variable_access.go @@ -34,3 +34,12 @@ func (n *VariableAccess) Type(s Scope) (Type, error) { return v.Type, nil } + +func (n *VariableAccess) Eval(ctx *EvalContext) (interface{}, error) { + v, ok := ctx.Scope.LookupVar(n.Name) + if !ok { + return TypeInvalid, fmt.Errorf("unknown variable: %s", n.Name) + } + + return v.Value, nil +} diff --git a/config/lang/ast/variable_access_test.go b/config/lang/ast/variable_access_test.go index 1880bc514..4dde6da39 100644 --- a/config/lang/ast/variable_access_test.go +++ b/config/lang/ast/variable_access_test.go @@ -34,3 +34,20 @@ func TestVariableAccessType_invalid(t *testing.T) { t.Fatal("should error") } } + +func TestVariableAccessEval(t *testing.T) { + c := &VariableAccess{Name: "foo"} + scope := &BasicScope{ + VarMap: map[string]Variable{ + "foo": Variable{Value: "42", Type: TypeString}, + }, + } + + actual, err := c.Eval(&EvalContext{Scope: scope}) + if err != nil { + t.Fatalf("err: %s", err) + } + if actual != "42" { + t.Fatalf("bad: %s", actual) + } +} From 57adfe53f67c6a553ee8100e1dff98d17ddb662d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 14 Jan 2015 20:58:46 -0800 Subject: [PATCH 3/7] config/lang: use the new AST stuff --- config/lang/builtins.go | 12 ++-- config/lang/check_identifier.go | 2 +- config/lang/check_identifier_test.go | 44 +++++++-------- config/lang/check_types.go | 4 +- config/lang/check_types_test.go | 82 ++++++++++++++-------------- config/lang/engine.go | 51 +++++------------ config/lang/engine_test.go | 32 +++++------ config/lang/lang.y | 10 ++-- config/lang/parse_test.go | 28 +++++----- config/lang/types.go | 53 ------------------ config/lang/types_test.go | 74 ------------------------- config/lang/y.go | 10 ++-- 12 files changed, 125 insertions(+), 277 deletions(-) delete mode 100644 config/lang/types.go delete mode 100644 config/lang/types_test.go diff --git a/config/lang/builtins.go b/config/lang/builtins.go index edeee29f2..ecc9d4a47 100644 --- a/config/lang/builtins.go +++ b/config/lang/builtins.go @@ -8,16 +8,16 @@ import ( // NOTE: All builtins are tested in engine_test.go -func registerBuiltins(scope *Scope) { +func registerBuiltins(scope *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() } -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 +26,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) { diff --git a/config/lang/check_identifier.go b/config/lang/check_identifier.go index 10ee2267d..a50673a2d 100644 --- a/config/lang/check_identifier.go +++ b/config/lang/check_identifier.go @@ -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 diff --git a/config/lang/check_identifier_test.go b/config/lang/check_identifier_test.go index 526128424..1ed52580e 100644 --- a/config/lang/check_identifier_test.go +++ b/config/lang/check_identifier_test.go @@ -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, diff --git a/config/lang/check_types.go b/config/lang/check_types.go index 4491ea496..882c64ca3 100644 --- a/config/lang/check_types.go +++ b/config/lang/check_types.go @@ -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, @@ -140,7 +140,7 @@ func (v *TypeCheck) visitConcat(n *ast.Concat) { } func (v *TypeCheck) visitLiteral(n *ast.LiteralNode) { - v.stackPush(n.Type) + v.stackPush(n.Typex) } func (v *TypeCheck) visitVariableAccess(n *ast.VariableAccess) { diff --git a/config/lang/check_types_test.go b/config/lang/check_types_test.go index 292d9255d..eb108044e 100644 --- a/config/lang/check_types_test.go +++ b/config/lang/check_types_test.go @@ -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, } diff --git a/config/lang/engine.go b/config/lang/engine.go index 9bcdb5563..380f4db1c 100644 --- a/config/lang/engine.go +++ b/config/lang/engine.go @@ -12,7 +12,7 @@ import ( // prior to running Execute. type Engine struct { // GlobalScope is the global scope of execution for this engine. - GlobalScope *Scope + GlobalScope *ast.BasicScope // SemanticChecks is a list of additional semantic checks that will be run // on the tree prior to executing it. The type checker, identifier checker, @@ -61,8 +61,8 @@ func (e *Engine) Execute(root ast.Node) (interface{}, ast.Type, error) { return v.Visit(root) } -func (e *Engine) scope() *Scope { - var scope Scope +func (e *Engine) scope() ast.Scope { + var scope ast.BasicScope if e.GlobalScope != nil { scope = *e.GlobalScope } @@ -75,7 +75,7 @@ func (e *Engine) scope() *Scope { // a program. Note at this point it is assumed that the types check out // and the identifiers exist. type executeVisitor struct { - Scope *Scope + Scope ast.Scope stack EngineStack err error @@ -102,7 +102,12 @@ func (v *executeVisitor) Visit(root ast.Node) (interface{}, ast.Type, error) { v.stack.Reset() v.err = nil - return result.Value, result.Type, resultErr + t, err := result.Type(v.Scope) + if err != nil { + return nil, ast.TypeInvalid, err + } + + return result.Value, t, resultErr } func (v *executeVisitor) visit(raw ast.Node) ast.Node { @@ -151,7 +156,7 @@ func (v *executeVisitor) visitCall(n *ast.Call) { // Push the result v.stack.Push(&ast.LiteralNode{ Value: result, - Type: function.ReturnType, + Typex: function.ReturnType, }) } @@ -170,7 +175,7 @@ func (v *executeVisitor) visitConcat(n *ast.Concat) { v.stack.Push(&ast.LiteralNode{ Value: buf.String(), - Type: ast.TypeString, + Typex: ast.TypeString, }) } @@ -188,7 +193,7 @@ func (v *executeVisitor) visitVariableAccess(n *ast.VariableAccess) { v.stack.Push(&ast.LiteralNode{ Value: variable.Value, - Type: variable.Type, + Typex: variable.Type, }) } @@ -218,33 +223,3 @@ func (s *EngineStack) Pop() *ast.LiteralNode { 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 -} - -// 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 -} diff --git a/config/lang/engine_test.go b/config/lang/engine_test.go index 488d56be7..a1e5aaba2 100644 --- a/config/lang/engine_test.go +++ b/config/lang/engine_test.go @@ -11,7 +11,7 @@ import ( func TestEngineExecute(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) { diff --git a/config/lang/lang.y b/config/lang/lang.y index fffe53089..4ee47fb54 100644 --- a/config/lang/lang.y +++ b/config/lang/lang.y @@ -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, } } diff --git a/config/lang/parse_test.go b/config/lang/parse_test.go index a176da7f9..3d158d667 100644 --- a/config/lang/parse_test.go +++ b/config/lang/parse_test.go @@ -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{ diff --git a/config/lang/types.go b/config/lang/types.go deleted file mode 100644 index 8adfcd05e..000000000 --- a/config/lang/types.go +++ /dev/null @@ -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 -} diff --git a/config/lang/types_test.go b/config/lang/types_test.go deleted file mode 100644 index 74513e24a..000000000 --- a/config/lang/types_test.go +++ /dev/null @@ -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 } diff --git a/config/lang/y.go b/config/lang/y.go index 6d3a456a6..de1ce7501 100644 --- a/config/lang/y.go +++ b/config/lang/y.go @@ -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, } } From 4302dbaf2a1c55e75871e4daf25bef679c2cdd7f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 14 Jan 2015 21:18:22 -0800 Subject: [PATCH 4/7] config/lang: make TypeCheck implementable by other nodes --- config/lang/check_types.go | 153 +++++++++++++++++++++++-------------- 1 file changed, 97 insertions(+), 56 deletions(-) diff --git a/config/lang/check_types.go b/config/lang/check_types.go index 882c64ca3..0165eadfc 100644 --- a/config/lang/check_types.go +++ b/config/lang/check_types.go @@ -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.Typex) +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 } From 8d2c60a8af49bd22e3786503e3f079b619a59490 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 14 Jan 2015 21:48:20 -0800 Subject: [PATCH 5/7] config/lang: eval --- config/lang/builtins.go | 6 +- config/lang/engine.go | 225 ------------------- config/lang/eval.go | 213 ++++++++++++++++++ config/lang/{engine_test.go => eval_test.go} | 5 +- 4 files changed, 220 insertions(+), 229 deletions(-) delete mode 100644 config/lang/engine.go create mode 100644 config/lang/eval.go rename config/lang/{engine_test.go => eval_test.go} (95%) diff --git a/config/lang/builtins.go b/config/lang/builtins.go index ecc9d4a47..e8513119f 100644 --- a/config/lang/builtins.go +++ b/config/lang/builtins.go @@ -8,12 +8,16 @@ import ( // NOTE: All builtins are tested in engine_test.go -func registerBuiltins(scope *ast.BasicScope) { +func registerBuiltins(scope *ast.BasicScope) *ast.BasicScope { + if scope == nil { + scope = new(ast.BasicScope) + } if scope.FuncMap == nil { scope.FuncMap = make(map[string]ast.Function) } scope.FuncMap["__builtin_IntToString"] = builtinIntToString() scope.FuncMap["__builtin_StringToInt"] = builtinStringToInt() + return scope } func builtinIntToString() ast.Function { diff --git a/config/lang/engine.go b/config/lang/engine.go deleted file mode 100644 index 380f4db1c..000000000 --- a/config/lang/engine.go +++ /dev/null @@ -1,225 +0,0 @@ -package lang - -import ( - "bytes" - "fmt" - "sync" - - "github.com/hashicorp/terraform/config/lang/ast" -) - -// Engine is the execution engine for this language. It should be configured -// prior to running Execute. -type Engine struct { - // GlobalScope is the global scope of execution for this engine. - GlobalScope *ast.BasicScope - - // SemanticChecks is a list of additional semantic checks that will be run - // on the tree prior to executing it. The type checker, identifier checker, - // etc. will be run before these. - SemanticChecks []SemanticChecker -} - -// SemanticChecker is the type that must be implemented to do a -// semantic check on an AST tree. This will be called with the root node. -type SemanticChecker func(ast.Node) error - -// Execute executes the given ast.Node and returns its final value, its -// type, and an error if one exists. -func (e *Engine) Execute(root ast.Node) (interface{}, ast.Type, error) { - // Copy the scope so we can add our builtins - scope := e.scope() - implicitMap := map[ast.Type]map[ast.Type]string{ - ast.TypeInt: { - ast.TypeString: "__builtin_IntToString", - }, - ast.TypeString: { - ast.TypeInt: "__builtin_StringToInt", - }, - } - - // Build our own semantic checks that we always run - tv := &TypeCheck{Scope: scope, Implicit: implicitMap} - ic := &IdentifierCheck{Scope: scope} - - // Build up the semantic checks for execution - checks := make( - []SemanticChecker, len(e.SemanticChecks), len(e.SemanticChecks)+2) - copy(checks, e.SemanticChecks) - checks = append(checks, ic.Visit) - checks = append(checks, tv.Visit) - - // Run the semantic checks - for _, check := range checks { - if err := check(root); err != nil { - return nil, ast.TypeInvalid, err - } - } - - // Execute - v := &executeVisitor{Scope: scope} - return v.Visit(root) -} - -func (e *Engine) scope() ast.Scope { - var scope ast.BasicScope - if e.GlobalScope != nil { - scope = *e.GlobalScope - } - - registerBuiltins(&scope) - return &scope -} - -// executeVisitor is the visitor used to do the actual execution of -// a program. Note at this point it is assumed that the types check out -// and the identifiers exist. -type executeVisitor struct { - Scope ast.Scope - - stack EngineStack - err error - lock sync.Mutex -} - -func (v *executeVisitor) Visit(root ast.Node) (interface{}, ast.Type, error) { - v.lock.Lock() - defer v.lock.Unlock() - - // Run the actual visitor pattern - root.Accept(v.visit) - - // Get our result and clear out everything else - var result *ast.LiteralNode - if v.stack.Len() > 0 { - result = v.stack.Pop() - } else { - result = new(ast.LiteralNode) - } - resultErr := v.err - - // Clear everything else so we aren't just dangling - v.stack.Reset() - v.err = nil - - t, err := result.Type(v.Scope) - if err != nil { - return nil, ast.TypeInvalid, err - } - - return result.Value, t, resultErr -} - -func (v *executeVisitor) visit(raw ast.Node) ast.Node { - if v.err != nil { - return raw - } - - switch n := raw.(type) { - case *ast.Call: - v.visitCall(n) - case *ast.Concat: - v.visitConcat(n) - case *ast.LiteralNode: - v.visitLiteral(n) - case *ast.VariableAccess: - v.visitVariableAccess(n) - default: - v.err = fmt.Errorf("unknown node: %#v", raw) - } - - return raw -} - -func (v *executeVisitor) visitCall(n *ast.Call) { - // Look up the function in the map - function, ok := v.Scope.LookupFunc(n.Func) - if !ok { - v.err = fmt.Errorf("unknown function called: %s", n.Func) - return - } - - // The arguments are on the stack in reverse order, so pop them off. - args := make([]interface{}, len(n.Args)) - for i, _ := range n.Args { - node := v.stack.Pop() - args[len(n.Args)-1-i] = node.Value - } - - // Call the function - result, err := function.Callback(args) - if err != nil { - v.err = fmt.Errorf("%s: %s", n.Func, err) - return - } - - // Push the result - v.stack.Push(&ast.LiteralNode{ - Value: result, - Typex: function.ReturnType, - }) -} - -func (v *executeVisitor) visitConcat(n *ast.Concat) { - // The expressions should all be on the stack in reverse - // order. So pop them off, reverse their order, and concatenate. - nodes := make([]*ast.LiteralNode, 0, len(n.Exprs)) - for range n.Exprs { - nodes = append(nodes, v.stack.Pop()) - } - - var buf bytes.Buffer - for i := len(nodes) - 1; i >= 0; i-- { - buf.WriteString(nodes[i].Value.(string)) - } - - v.stack.Push(&ast.LiteralNode{ - Value: buf.String(), - Typex: ast.TypeString, - }) -} - -func (v *executeVisitor) visitLiteral(n *ast.LiteralNode) { - v.stack.Push(n) -} - -func (v *executeVisitor) visitVariableAccess(n *ast.VariableAccess) { - // Look up the variable in the map - variable, ok := v.Scope.LookupVar(n.Name) - if !ok { - v.err = fmt.Errorf("unknown variable accessed: %s", n.Name) - return - } - - v.stack.Push(&ast.LiteralNode{ - Value: variable.Value, - Typex: variable.Type, - }) -} - -// EngineStack is a stack of ast.LiteralNodes that the Engine keeps track -// of during execution. This is currently backed by a dumb slice, but can be -// replaced with a better data structure at some point in the future if this -// turns out to require optimization. -type EngineStack struct { - stack []*ast.LiteralNode -} - -func (s *EngineStack) Len() int { - return len(s.stack) -} - -func (s *EngineStack) Push(n *ast.LiteralNode) { - s.stack = append(s.stack, n) -} - -func (s *EngineStack) Pop() *ast.LiteralNode { - x := s.stack[len(s.stack)-1] - s.stack[len(s.stack)-1] = nil - s.stack = s.stack[:len(s.stack)-1] - return x -} - -func (s *EngineStack) Reset() { - s.stack = nil -} diff --git a/config/lang/eval.go b/config/lang/eval.go new file mode 100644 index 000000000..9b0a9839b --- /dev/null +++ b/config/lang/eval.go @@ -0,0 +1,213 @@ +package lang + +import ( + "bytes" + "fmt" + "sync" + + "github.com/hashicorp/terraform/config/lang/ast" +) + +// EvalConfig is the configuration for evaluating. +type EvalConfig struct { + // GlobalScope is the global scope of execution for evaluation. + GlobalScope *ast.BasicScope + + // SemanticChecks is a list of additional semantic checks that will be run + // on the tree prior to evaluating it. The type checker, identifier checker, + // etc. will be run before these automatically. + SemanticChecks []SemanticChecker +} + +// SemanticChecker is the type that must be implemented to do a +// semantic check on an AST tree. This will be called with the root node. +type SemanticChecker func(ast.Node) error + +// Eval evaluates the given AST tree and returns its output value, the type +// of the output, and any error that occurred. +func Eval(root ast.Node, config *EvalConfig) (interface{}, ast.Type, error) { + // Copy the scope so we can add our builtins + scope := registerBuiltins(config.GlobalScope) + implicitMap := map[ast.Type]map[ast.Type]string{ + ast.TypeInt: { + ast.TypeString: "__builtin_IntToString", + }, + ast.TypeString: { + ast.TypeInt: "__builtin_StringToInt", + }, + } + + // Build our own semantic checks that we always run + tv := &TypeCheck{Scope: scope, Implicit: implicitMap} + ic := &IdentifierCheck{Scope: scope} + + // Build up the semantic checks for execution + checks := make( + []SemanticChecker, + len(config.SemanticChecks), len(config.SemanticChecks)+2) + copy(checks, config.SemanticChecks) + checks = append(checks, ic.Visit) + checks = append(checks, tv.Visit) + + // Run the semantic checks + for _, check := range checks { + if err := check(root); err != nil { + return nil, ast.TypeInvalid, err + } + } + + // Execute + v := &evalVisitor{Scope: scope} + return v.Visit(root) +} + +// EvalNode is the interface that must be implemented by any ast.Node +// to support evaluation. This will be called in visitor pattern order. +// The result of each call to Eval is automatically pushed onto the +// stack as a LiteralNode. Pop elements off the stack to get child +// values. +type EvalNode interface { + Eval(ast.Scope, *ast.Stack) (interface{}, ast.Type, error) +} + +type evalVisitor struct { + Scope ast.Scope + Stack ast.Stack + + err error + lock sync.Mutex +} + +func (v *evalVisitor) Visit(root ast.Node) (interface{}, ast.Type, error) { + // Run the actual visitor pattern + root.Accept(v.visit) + + // Get our result and clear out everything else + var result *ast.LiteralNode + if v.Stack.Len() > 0 { + result = v.Stack.Pop().(*ast.LiteralNode) + } else { + result = new(ast.LiteralNode) + } + resultErr := v.err + + // Clear everything else so we aren't just dangling + v.Stack.Reset() + v.err = nil + + t, err := result.Type(v.Scope) + if err != nil { + return nil, ast.TypeInvalid, err + } + + return result.Value, t, resultErr +} + +func (v *evalVisitor) visit(raw ast.Node) ast.Node { + if v.err != nil { + return raw + } + + en, err := evalNode(raw) + if err != nil { + v.err = err + return raw + } + + out, outType, err := en.Eval(v.Scope, &v.Stack) + if err != nil { + v.err = err + return raw + } + + v.Stack.Push(&ast.LiteralNode{ + Value: out, + Typex: outType, + }) + return raw +} + +// evalNode is a private function that returns an EvalNode for built-in +// types as well as any other EvalNode implementations. +func evalNode(raw ast.Node) (EvalNode, error) { + switch n := raw.(type) { + case *ast.Call: + return &evalCall{n}, nil + case *ast.Concat: + return &evalConcat{n}, nil + case *ast.LiteralNode: + return &evalLiteralNode{n}, nil + case *ast.VariableAccess: + return &evalVariableAccess{n}, nil + default: + en, ok := n.(EvalNode) + if !ok { + return nil, fmt.Errorf("node doesn't support evaluation: %#v", raw) + } + + return en, nil + } +} + +type evalCall struct{ *ast.Call } + +func (v *evalCall) Eval(s ast.Scope, stack *ast.Stack) (interface{}, ast.Type, error) { + // Look up the function in the map + function, ok := s.LookupFunc(v.Func) + if !ok { + return nil, ast.TypeInvalid, fmt.Errorf( + "unknown function called: %s", v.Func) + } + + // The arguments are on the stack in reverse order, so pop them off. + args := make([]interface{}, len(v.Args)) + for i, _ := range v.Args { + node := stack.Pop().(*ast.LiteralNode) + args[len(v.Args)-1-i] = node.Value + } + + // Call the function + result, err := function.Callback(args) + if err != nil { + return nil, ast.TypeInvalid, fmt.Errorf("%s: %s", v.Func, err) + } + + return result, function.ReturnType, nil +} + +type evalConcat struct{ *ast.Concat } + +func (v *evalConcat) Eval(s ast.Scope, stack *ast.Stack) (interface{}, ast.Type, error) { + // The expressions should all be on the stack in reverse + // order. So pop them off, reverse their order, and concatenate. + nodes := make([]*ast.LiteralNode, 0, len(v.Exprs)) + for range v.Exprs { + nodes = append(nodes, stack.Pop().(*ast.LiteralNode)) + } + + var buf bytes.Buffer + for i := len(nodes) - 1; i >= 0; i-- { + buf.WriteString(nodes[i].Value.(string)) + } + + return buf.String(), ast.TypeString, nil +} + +type evalLiteralNode struct{ *ast.LiteralNode } + +func (v *evalLiteralNode) Eval(ast.Scope, *ast.Stack) (interface{}, ast.Type, error) { + return v.Value, v.Typex, nil +} + +type evalVariableAccess struct{ *ast.VariableAccess } + +func (v *evalVariableAccess) Eval(scope ast.Scope, _ *ast.Stack) (interface{}, ast.Type, error) { + // Look up the variable in the map + variable, ok := scope.LookupVar(v.Name) + if !ok { + return nil, ast.TypeInvalid, fmt.Errorf( + "unknown variable accessed: %s", v.Name) + } + + return variable.Value, variable.Type, nil +} diff --git a/config/lang/engine_test.go b/config/lang/eval_test.go similarity index 95% rename from config/lang/engine_test.go rename to config/lang/eval_test.go index a1e5aaba2..bf66be79f 100644 --- a/config/lang/engine_test.go +++ b/config/lang/eval_test.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform/config/lang/ast" ) -func TestEngineExecute(t *testing.T) { +func TestEval(t *testing.T) { cases := []struct { Input string Scope *ast.BasicScope @@ -121,8 +121,7 @@ func TestEngineExecute(t *testing.T) { t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) } - engine := &Engine{GlobalScope: tc.Scope} - out, outType, err := engine.Execute(node) + out, outType, err := Eval(node, &EvalConfig{GlobalScope: tc.Scope}) if (err != nil) != tc.Error { t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) } From 61ee63d842350fdb876683cb21c1fd92d9ad8444 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 14 Jan 2015 21:49:39 -0800 Subject: [PATCH 6/7] config/lang/ast: remove unused Eval --- config/lang/ast/ast.go | 12 ------------ config/lang/ast/call.go | 19 ------------------- config/lang/ast/call_test.go | 21 --------------------- config/lang/ast/concat.go | 14 -------------- config/lang/ast/concat_test.go | 18 ------------------ config/lang/ast/literal.go | 4 ---- config/lang/ast/literal_test.go | 13 ------------- config/lang/ast/variable_access.go | 9 --------- config/lang/ast/variable_access_test.go | 17 ----------------- 9 files changed, 127 deletions(-) diff --git a/config/lang/ast/ast.go b/config/lang/ast/ast.go index 7f88beaa3..fc6c966b0 100644 --- a/config/lang/ast/ast.go +++ b/config/lang/ast/ast.go @@ -15,10 +15,6 @@ type Node interface { // Type returns the type of this node for the given context. Type(Scope) (Type, error) - - // Eval evaluates this node, returning its final value. The type - // of the final value will match the result of Type with the same scope. - Eval(*EvalContext) (interface{}, error) } // Pos is the starting position of an AST node @@ -30,14 +26,6 @@ func (p Pos) String() string { return fmt.Sprintf("%d:%d", p.Line, p.Column) } -// EvalContext is the context given for evaluation. -// -// It is simple for now with just a Scope but we use a struct in case we -// plan on adding fields in the future. -type EvalContext struct { - Scope Scope -} - // Visitors are just implementations of this function. // // The function must return the Node to replace this node with. "nil" is diff --git a/config/lang/ast/call.go b/config/lang/ast/call.go index fda7d3e62..ace1147a6 100644 --- a/config/lang/ast/call.go +++ b/config/lang/ast/call.go @@ -41,22 +41,3 @@ func (n *Call) Type(s Scope) (Type, error) { return f.ReturnType, nil } - -func (n *Call) Eval(ctx *EvalContext) (interface{}, error) { - f, ok := ctx.Scope.LookupFunc(n.Func) - if !ok { - return TypeInvalid, fmt.Errorf("unknown function: %s", n.Func) - } - - args := make([]interface{}, len(n.Args)) - for i, arg := range n.Args { - result, err := arg.Eval(ctx) - if err != nil { - return nil, err - } - - args[i] = result - } - - return f.Callback(args) -} diff --git a/config/lang/ast/call_test.go b/config/lang/ast/call_test.go index b08a67d4b..ef63888d2 100644 --- a/config/lang/ast/call_test.go +++ b/config/lang/ast/call_test.go @@ -34,24 +34,3 @@ func TestCallType_invalid(t *testing.T) { t.Fatal("should error") } } - -func TestCallEval(t *testing.T) { - c := &Call{Func: "foo"} - scope := &BasicScope{ - FuncMap: map[string]Function{ - "foo": Function{ - Callback: func([]interface{}) (interface{}, error) { - return "42", nil - }, - }, - }, - } - - actual, err := c.Eval(&EvalContext{Scope: scope}) - if err != nil { - t.Fatalf("err: %s", err) - } - if actual != "42" { - t.Fatalf("bad: %s", actual) - } -} diff --git a/config/lang/ast/concat.go b/config/lang/ast/concat.go index 03d406d6b..0246a3bc1 100644 --- a/config/lang/ast/concat.go +++ b/config/lang/ast/concat.go @@ -40,17 +40,3 @@ func (n *Concat) String() string { func (n *Concat) Type(Scope) (Type, error) { return TypeString, nil } - -func (n *Concat) Eval(ctx *EvalContext) (interface{}, error) { - var b bytes.Buffer - for _, expr := range n.Exprs { - result, err := expr.Eval(ctx) - if err != nil { - return nil, err - } - - b.WriteString(result.(string)) - } - - return b.String(), nil -} diff --git a/config/lang/ast/concat_test.go b/config/lang/ast/concat_test.go index 111092270..65fa67601 100644 --- a/config/lang/ast/concat_test.go +++ b/config/lang/ast/concat_test.go @@ -14,21 +14,3 @@ func TestConcatType(t *testing.T) { t.Fatalf("bad: %s", actual) } } - -func TestConcatEval(t *testing.T) { - c := &Concat{ - Exprs: []Node{ - &LiteralNode{Value: "foo"}, - &LiteralNode{Value: "bar"}, - }, - } - scope := &BasicScope{} - - actual, err := c.Eval(&EvalContext{Scope: scope}) - if err != nil { - t.Fatalf("err: %s", err) - } - if actual != "foobar" { - t.Fatalf("bad: %s", actual) - } -} diff --git a/config/lang/ast/literal.go b/config/lang/ast/literal.go index da1985023..9da3ff3a3 100644 --- a/config/lang/ast/literal.go +++ b/config/lang/ast/literal.go @@ -31,7 +31,3 @@ func (n *LiteralNode) String() string { func (n *LiteralNode) Type(Scope) (Type, error) { return n.Typex, nil } - -func (n *LiteralNode) Eval(*EvalContext) (interface{}, error) { - return n.Value, nil -} diff --git a/config/lang/ast/literal_test.go b/config/lang/ast/literal_test.go index 51eedaf94..2759d7722 100644 --- a/config/lang/ast/literal_test.go +++ b/config/lang/ast/literal_test.go @@ -14,16 +14,3 @@ func TestLiteralNodeType(t *testing.T) { t.Fatalf("bad: %s", actual) } } - -func TestLiteralNodeEval(t *testing.T) { - c := &LiteralNode{Value: "42", Typex: TypeString} - scope := &BasicScope{} - - actual, err := c.Eval(&EvalContext{Scope: scope}) - if err != nil { - t.Fatalf("err: %s", err) - } - if actual != "42" { - t.Fatalf("bad: %s", actual) - } -} diff --git a/config/lang/ast/variable_access.go b/config/lang/ast/variable_access.go index 8ed150d87..4c1362d75 100644 --- a/config/lang/ast/variable_access.go +++ b/config/lang/ast/variable_access.go @@ -34,12 +34,3 @@ func (n *VariableAccess) Type(s Scope) (Type, error) { return v.Type, nil } - -func (n *VariableAccess) Eval(ctx *EvalContext) (interface{}, error) { - v, ok := ctx.Scope.LookupVar(n.Name) - if !ok { - return TypeInvalid, fmt.Errorf("unknown variable: %s", n.Name) - } - - return v.Value, nil -} diff --git a/config/lang/ast/variable_access_test.go b/config/lang/ast/variable_access_test.go index 4dde6da39..1880bc514 100644 --- a/config/lang/ast/variable_access_test.go +++ b/config/lang/ast/variable_access_test.go @@ -34,20 +34,3 @@ func TestVariableAccessType_invalid(t *testing.T) { t.Fatal("should error") } } - -func TestVariableAccessEval(t *testing.T) { - c := &VariableAccess{Name: "foo"} - scope := &BasicScope{ - VarMap: map[string]Variable{ - "foo": Variable{Value: "42", Type: TypeString}, - }, - } - - actual, err := c.Eval(&EvalContext{Scope: scope}) - if err != nil { - t.Fatalf("err: %s", err) - } - if actual != "42" { - t.Fatalf("bad: %s", actual) - } -} From 2abeb2d9ace1ac61c7b1e76265e1bfc568e55258 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 14 Jan 2015 22:01:42 -0800 Subject: [PATCH 7/7] config: use new API --- config/config.go | 7 ++++--- config/interpolate.go | 9 +++++---- config/interpolate_funcs.go | 25 ++++++++++++------------- config/interpolate_funcs_test.go | 9 ++++----- config/lang/ast/literal.go | 2 +- config/lang/check_identifier.go | 2 +- config/lang/eval.go | 3 +++ config/raw_config.go | 16 ++++++++-------- config/raw_config_test.go | 21 ++++++++++----------- helper/diff/diff_test.go | 5 ++--- helper/schema/schema_test.go | 9 ++++----- terraform/context.go | 27 +++++++++++++-------------- 12 files changed, 67 insertions(+), 68 deletions(-) diff --git a/config/config.go b/config/config.go index 7163e2829..c67b58a45 100644 --- a/config/config.go +++ b/config/config.go @@ -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 } diff --git a/config/interpolate.go b/config/interpolate.go index e6a709d94..b13cdb405 100644 --- a/config/interpolate.go +++ b/config/interpolate.go @@ -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 diff --git a/config/interpolate_funcs.go b/config/interpolate_funcs.go index 825913588..c529adb8d 100644 --- a/config/interpolate_funcs.go +++ b/config/interpolate_funcs.go @@ -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) { diff --git a/config/interpolate_funcs_test.go b/config/interpolate_funcs_test.go index 68fce1098..1a4a59057 100644 --- a/config/interpolate_funcs_test.go +++ b/config/interpolate_funcs_test.go @@ -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) } diff --git a/config/lang/ast/literal.go b/config/lang/ast/literal.go index 9da3ff3a3..1714ff026 100644 --- a/config/lang/ast/literal.go +++ b/config/lang/ast/literal.go @@ -25,7 +25,7 @@ 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) { diff --git a/config/lang/check_identifier.go b/config/lang/check_identifier.go index a50673a2d..b835e8e4e 100644 --- a/config/lang/check_identifier.go +++ b/config/lang/check_identifier.go @@ -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 diff --git a/config/lang/eval.go b/config/lang/eval.go index 9b0a9839b..cc4d756df 100644 --- a/config/lang/eval.go +++ b/config/lang/eval.go @@ -27,6 +27,9 @@ type SemanticChecker func(ast.Node) error // 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: { diff --git a/config/raw_config.go b/config/raw_config.go index 1fed79dfe..753e7f1b8 100644 --- a/config/raw_config.go +++ b/config/raw_config.go @@ -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, }, diff --git a/config/raw_config_test.go b/config/raw_config_test.go index 726c6c955..49e13a45a 100644 --- a/config/raw_config_test.go +++ b/config/raw_config_test.go @@ -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, }, diff --git a/helper/diff/diff_test.go b/helper/diff/diff_test.go index 30d0aebfd..8afeddb14 100644 --- a/helper/diff/diff_test.go +++ b/helper/diff/diff_test.go @@ -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 { diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go index 8129d5184..8e1a6c98e 100644 --- a/helper/schema/schema_test.go +++ b/helper/schema/schema_test.go @@ -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 { diff --git a/terraform/context.go b/terraform/context.go index 36db6c466..50ad2a2bc 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -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, }