From ec3b5f3886e73f5d87ac0335313f3cc3f3d3ff77 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 12 Jan 2015 09:57:16 -0800 Subject: [PATCH] config/lang: implement type lookup --- config/lang/ast/ast.go | 2 +- config/lang/engine.go | 65 ++++++++++------- config/lang/engine_test.go | 11 +-- config/lang/transform_implicit_types_test.go | 74 ++++++++++++++++++++ config/lang/types.go | 58 +++++++++++++++ 5 files changed, 180 insertions(+), 30 deletions(-) create mode 100644 config/lang/transform_implicit_types_test.go create mode 100644 config/lang/types.go diff --git a/config/lang/ast/ast.go b/config/lang/ast/ast.go index a03070180..2476098e8 100644 --- a/config/lang/ast/ast.go +++ b/config/lang/ast/ast.go @@ -18,7 +18,7 @@ type Pos struct { Column, Line int // Column/Line number, starting at 1 } -func (p *Pos) String() string { +func (p Pos) String() string { return fmt.Sprintf("%d:%d", p.Line, p.Column) } diff --git a/config/lang/engine.go b/config/lang/engine.go index 0b3a6c14d..4f06887d2 100644 --- a/config/lang/engine.go +++ b/config/lang/engine.go @@ -11,10 +11,8 @@ import ( // Engine is the execution engine for this language. It should be configured // prior to running Execute. type Engine struct { - // VarMap and FuncMap are the mappings of identifiers to functions - // and variable values. - VarMap map[string]Variable - FuncMap map[string]Function + // 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, @@ -26,26 +24,10 @@ type Engine struct { // semantic check on an AST tree. This will be called with the root node. type SemanticChecker func(ast.Node) error -// 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 []ast.Type - ReturnType ast.Type - Callback func([]interface{}) (interface{}, 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) { - v := &executeVisitor{Engine: e} + v := &executeVisitor{Scope: e.GlobalScope} return v.Visit(root) } @@ -53,7 +35,7 @@ func (e *Engine) Execute(root ast.Node) (interface{}, ast.Type, error) { // a program. Note at this point it is assumed that the types check out // and the identifiers exist. type executeVisitor struct { - Engine *Engine + Scope *Scope stack []*ast.LiteralNode err error @@ -104,7 +86,7 @@ func (v *executeVisitor) visit(raw ast.Node) { func (v *executeVisitor) visitCall(n *ast.Call) { // Look up the function in the map - function, ok := v.Engine.FuncMap[n.Func] + function, ok := v.Scope.FuncMap[n.Func] if !ok { v.err = fmt.Errorf("unknown function called: %s", n.Func) return @@ -156,7 +138,7 @@ func (v *executeVisitor) visitLiteral(n *ast.LiteralNode) { func (v *executeVisitor) visitVariableAccess(n *ast.VariableAccess) { // Look up the variable in the map - variable, ok := v.Engine.VarMap[n.Name] + variable, ok := v.Scope.VarMap[n.Name] if !ok { v.err = fmt.Errorf("unknown variable accessed: %s", n.Name) return @@ -177,3 +159,38 @@ func (v *executeVisitor) stackPop() *ast.LiteralNode { x, v.stack = v.stack[len(v.stack)-1], v.stack[:len(v.stack)-1] return x } + +// 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 []ast.Type + ReturnType ast.Type + Callback func([]interface{}) (interface{}, error) +} + +// 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 96a1105d5..b65d0475a 100644 --- a/config/lang/engine_test.go +++ b/config/lang/engine_test.go @@ -10,14 +10,14 @@ import ( func TestEngineExecute(t *testing.T) { cases := []struct { Input string - Engine *Engine + Scope *Scope Error bool Result interface{} ResultType ast.Type }{ { "foo", - &Engine{}, + nil, false, "foo", ast.TypeString, @@ -25,7 +25,7 @@ func TestEngineExecute(t *testing.T) { { "foo ${bar}", - &Engine{ + &Scope{ VarMap: map[string]Variable{ "bar": Variable{ Value: "baz", @@ -40,7 +40,7 @@ func TestEngineExecute(t *testing.T) { { "foo ${rand()}", - &Engine{ + &Scope{ FuncMap: map[string]Function{ "rand": Function{ ReturnType: ast.TypeString, @@ -62,7 +62,8 @@ func TestEngineExecute(t *testing.T) { t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) } - out, outType, err := tc.Engine.Execute(node) + engine := &Engine{GlobalScope: tc.Scope} + out, outType, err := engine.Execute(node) if (err != nil) != tc.Error { t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) } diff --git a/config/lang/transform_implicit_types_test.go b/config/lang/transform_implicit_types_test.go new file mode 100644 index 000000000..9eb0fd92f --- /dev/null +++ b/config/lang/transform_implicit_types_test.go @@ -0,0 +1,74 @@ +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) {} +func (n customUntyped) Pos() (v ast.Pos) { return } + +type customTyped struct{} + +func (n customTyped) Accept(ast.Visitor) {} +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/types.go b/config/lang/types.go new file mode 100644 index 000000000..2d6b58431 --- /dev/null +++ b/config/lang/types.go @@ -0,0 +1,58 @@ +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 +} + +var supportedTransforms = map[ast.Type]ast.Type{ + ast.TypeString: ast.TypeInt, + ast.TypeInt: ast.TypeString, +}