config/lang: start implementing type checking

This commit is contained in:
Mitchell Hashimoto 2015-01-11 23:38:21 -08:00
parent 8f925b93e0
commit d3b1010444
3 changed files with 175 additions and 0 deletions

View File

@ -22,4 +22,5 @@ type Type uint
const (
TypeInvalid Type = 0
TypeString = 1 << iota
TypeInt
)

View File

@ -0,0 +1,100 @@
package lang
import (
"fmt"
"sync"
"github.com/hashicorp/terraform/config/lang/ast"
)
// TypeVisitor implements ast.Visitor for type checking an AST tree.
// It requires some configuration to look up the type of nodes.
type TypeVisitor struct {
VarMap map[string]Variable
FuncMap map[string]Function
stack []ast.Type
err error
lock sync.Mutex
}
func (v *TypeVisitor) Visit(root ast.Node) error {
v.lock.Lock()
defer v.lock.Unlock()
defer v.reset()
root.Accept(v.visit)
return v.err
}
func (v *TypeVisitor) visit(raw ast.Node) {
if v.err != nil {
return
}
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)
}
}
func (v *TypeVisitor) visitCall(n *ast.Call) {
// TODO
v.stackPush(ast.TypeString)
}
func (v *TypeVisitor) visitConcat(n *ast.Concat) {
types := make([]ast.Type, len(n.Exprs))
for i, _ := range n.Exprs {
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 {
v.err = fmt.Errorf("%s: argument %d must be a sting", n, i+1)
return
}
}
// This always results in type string
v.stackPush(ast.TypeString)
}
func (v *TypeVisitor) visitLiteral(n *ast.LiteralNode) {
v.stackPush(n.Type)
}
func (v *TypeVisitor) visitVariableAccess(n *ast.VariableAccess) {
// Look up the variable in the map
variable, ok := v.VarMap[n.Name]
if !ok {
v.err = fmt.Errorf("unknown variable accessed: %s", n.Name)
return
}
// Add the type to the stack
v.stackPush(variable.Type)
}
func (v *TypeVisitor) reset() {
v.stack = nil
v.err = nil
}
func (v *TypeVisitor) stackPush(t ast.Type) {
v.stack = append(v.stack, t)
}
func (v *TypeVisitor) stackPop() ast.Type {
var x ast.Type
x, v.stack = v.stack[len(v.stack)-1], v.stack[:len(v.stack)-1]
return x
}

View File

@ -0,0 +1,74 @@
package lang
import (
"testing"
"github.com/hashicorp/terraform/config/lang/ast"
)
func TestTypeVisitor(t *testing.T) {
cases := []struct {
Input string
Visitor *TypeVisitor
Error bool
}{
{
"foo",
&TypeVisitor{},
false,
},
{
"foo ${bar}",
&TypeVisitor{
VarMap: map[string]Variable{
"bar": Variable{
Value: "baz",
Type: ast.TypeString,
},
},
},
false,
},
{
"foo ${rand()}",
&TypeVisitor{
FuncMap: map[string]Function{
"rand": Function{
ReturnType: ast.TypeString,
Callback: func([]interface{}) (interface{}, error) {
return "42", nil
},
},
},
},
false,
},
{
"foo ${bar}",
&TypeVisitor{
VarMap: map[string]Variable{
"bar": Variable{
Value: 42,
Type: ast.TypeInt,
},
},
},
true,
},
}
for _, tc := range cases {
node, err := Parse(tc.Input)
if err != nil {
t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input)
}
err = tc.Visitor.Visit(node)
if (err != nil) != tc.Error {
t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input)
}
}
}