config/lang: implement type lookup
This commit is contained in:
parent
25a2fbc902
commit
ec3b5f3886
|
@ -18,7 +18,7 @@ type Pos struct {
|
||||||
Column, Line int // Column/Line number, starting at 1
|
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)
|
return fmt.Sprintf("%d:%d", p.Line, p.Column)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,8 @@ import (
|
||||||
// Engine is the execution engine for this language. It should be configured
|
// Engine is the execution engine for this language. It should be configured
|
||||||
// prior to running Execute.
|
// prior to running Execute.
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
// VarMap and FuncMap are the mappings of identifiers to functions
|
// GlobalScope is the global scope of execution for this engine.
|
||||||
// and variable values.
|
GlobalScope *Scope
|
||||||
VarMap map[string]Variable
|
|
||||||
FuncMap map[string]Function
|
|
||||||
|
|
||||||
// SemanticChecks is a list of additional semantic checks that will be run
|
// SemanticChecks is a list of additional semantic checks that will be run
|
||||||
// on the tree prior to executing it. The type checker, identifier checker,
|
// 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.
|
// semantic check on an AST tree. This will be called with the root node.
|
||||||
type SemanticChecker func(ast.Node) error
|
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
|
// Execute executes the given ast.Node and returns its final value, its
|
||||||
// type, and an error if one exists.
|
// type, and an error if one exists.
|
||||||
func (e *Engine) Execute(root ast.Node) (interface{}, ast.Type, error) {
|
func (e *Engine) Execute(root ast.Node) (interface{}, ast.Type, error) {
|
||||||
v := &executeVisitor{Engine: e}
|
v := &executeVisitor{Scope: e.GlobalScope}
|
||||||
return v.Visit(root)
|
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
|
// a program. Note at this point it is assumed that the types check out
|
||||||
// and the identifiers exist.
|
// and the identifiers exist.
|
||||||
type executeVisitor struct {
|
type executeVisitor struct {
|
||||||
Engine *Engine
|
Scope *Scope
|
||||||
|
|
||||||
stack []*ast.LiteralNode
|
stack []*ast.LiteralNode
|
||||||
err error
|
err error
|
||||||
|
@ -104,7 +86,7 @@ func (v *executeVisitor) visit(raw ast.Node) {
|
||||||
|
|
||||||
func (v *executeVisitor) visitCall(n *ast.Call) {
|
func (v *executeVisitor) visitCall(n *ast.Call) {
|
||||||
// Look up the function in the map
|
// Look up the function in the map
|
||||||
function, ok := v.Engine.FuncMap[n.Func]
|
function, ok := v.Scope.FuncMap[n.Func]
|
||||||
if !ok {
|
if !ok {
|
||||||
v.err = fmt.Errorf("unknown function called: %s", n.Func)
|
v.err = fmt.Errorf("unknown function called: %s", n.Func)
|
||||||
return
|
return
|
||||||
|
@ -156,7 +138,7 @@ func (v *executeVisitor) visitLiteral(n *ast.LiteralNode) {
|
||||||
|
|
||||||
func (v *executeVisitor) visitVariableAccess(n *ast.VariableAccess) {
|
func (v *executeVisitor) visitVariableAccess(n *ast.VariableAccess) {
|
||||||
// Look up the variable in the map
|
// Look up the variable in the map
|
||||||
variable, ok := v.Engine.VarMap[n.Name]
|
variable, ok := v.Scope.VarMap[n.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
v.err = fmt.Errorf("unknown variable accessed: %s", n.Name)
|
v.err = fmt.Errorf("unknown variable accessed: %s", n.Name)
|
||||||
return
|
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]
|
x, v.stack = v.stack[len(v.stack)-1], v.stack[:len(v.stack)-1]
|
||||||
return x
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -10,14 +10,14 @@ import (
|
||||||
func TestEngineExecute(t *testing.T) {
|
func TestEngineExecute(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
Input string
|
Input string
|
||||||
Engine *Engine
|
Scope *Scope
|
||||||
Error bool
|
Error bool
|
||||||
Result interface{}
|
Result interface{}
|
||||||
ResultType ast.Type
|
ResultType ast.Type
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"foo",
|
"foo",
|
||||||
&Engine{},
|
nil,
|
||||||
false,
|
false,
|
||||||
"foo",
|
"foo",
|
||||||
ast.TypeString,
|
ast.TypeString,
|
||||||
|
@ -25,7 +25,7 @@ func TestEngineExecute(t *testing.T) {
|
||||||
|
|
||||||
{
|
{
|
||||||
"foo ${bar}",
|
"foo ${bar}",
|
||||||
&Engine{
|
&Scope{
|
||||||
VarMap: map[string]Variable{
|
VarMap: map[string]Variable{
|
||||||
"bar": Variable{
|
"bar": Variable{
|
||||||
Value: "baz",
|
Value: "baz",
|
||||||
|
@ -40,7 +40,7 @@ func TestEngineExecute(t *testing.T) {
|
||||||
|
|
||||||
{
|
{
|
||||||
"foo ${rand()}",
|
"foo ${rand()}",
|
||||||
&Engine{
|
&Scope{
|
||||||
FuncMap: map[string]Function{
|
FuncMap: map[string]Function{
|
||||||
"rand": Function{
|
"rand": Function{
|
||||||
ReturnType: ast.TypeString,
|
ReturnType: ast.TypeString,
|
||||||
|
@ -62,7 +62,8 @@ func TestEngineExecute(t *testing.T) {
|
||||||
t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input)
|
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 {
|
if (err != nil) != tc.Error {
|
||||||
t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input)
|
t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
|
@ -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,
|
||||||
|
}
|
Loading…
Reference in New Issue