Skip to content

Commit

Permalink
Merge pull request #3 from hashicorp/f-index-operation
Browse files Browse the repository at this point in the history
Add support for indexing into variables
  • Loading branch information
jen20 committed Feb 16, 2016
2 parents 3eb5226 + 730ac9b commit 0457360
Show file tree
Hide file tree
Showing 17 changed files with 836 additions and 93 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.DS_Store
.idea
*.iml
1 change: 1 addition & 0 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ const (
TypeString
TypeInt
TypeFloat
TypeList
)
68 changes: 68 additions & 0 deletions ast/index.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package ast

import (
"fmt"
"strings"
)

// Index represents an indexing operation into another data structure
type Index struct {
Target Node
Key Node
Posx Pos
}

func (n *Index) Accept(v Visitor) Node {
return v(n)
}

func (n *Index) Pos() Pos {
return n.Posx
}

func (n *Index) String() string {
return fmt.Sprintf("Index(%s, %s)", n.Target, n.Key)
}

func (n *Index) Type(s Scope) (Type, error) {
variableAccess, ok := n.Target.(*VariableAccess)
if !ok {
return TypeInvalid, fmt.Errorf("target is not a variable")
}

variable, ok := s.LookupVar(variableAccess.Name)
if !ok {
return TypeInvalid, fmt.Errorf("unknown variable accessed: %s", variableAccess.Name)
}
if variable.Type != TypeList {
return TypeInvalid, fmt.Errorf("invalid index operation into non-indexable type: %s", variable.Type)
}

list := variable.Value.([]Variable)

// Ensure that the types of the list elements are homogenous
listTypes := make(map[Type]struct{})
for _, v := range list {
if _, ok := listTypes[v.Type]; ok {
continue
}
listTypes[v.Type] = struct{}{}
}

if len(listTypes) != 1 {
typesFound := make([]string, len(listTypes))
i := 0
for k, _ := range listTypes {
typesFound[0] = k.String()
i++
}
types := strings.Join(typesFound, ", ")
return TypeInvalid, fmt.Errorf("list %q does not have homogenous types. found %s", variableAccess.Name, types)
}

return list[0].Type, nil
}

func (n *Index) GoString() string {
return fmt.Sprintf("*%#v", *n)
}
110 changes: 110 additions & 0 deletions ast/index_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package ast

import (
"testing"
)

func TestIndexType_string(t *testing.T) {
i := &Index{
Target: &VariableAccess{Name: "foo"},
Key: &LiteralNode{
Typex: TypeInt,
Value: 1,
},
}

scope := &BasicScope{
VarMap: map[string]Variable{
"foo": Variable{
Type: TypeList,
Value: []Variable{
Variable{
Type: TypeString,
Value: "Hello",
},
Variable{
Type: TypeString,
Value: "World",
},
},
},
},
}

actual, err := i.Type(scope)
if err != nil {
t.Fatalf("err: %s", err)
}
if actual != TypeString {
t.Fatalf("bad: %s", actual)
}
}

func TestIndexType_int(t *testing.T) {
i := &Index{
Target: &VariableAccess{Name: "foo"},
Key: &LiteralNode{
Typex: TypeInt,
Value: 1,
},
}

scope := &BasicScope{
VarMap: map[string]Variable{
"foo": Variable{
Type: TypeList,
Value: []Variable{
Variable{
Type: TypeInt,
Value: 34,
},
Variable{
Type: TypeInt,
Value: 54,
},
},
},
},
}

actual, err := i.Type(scope)
if err != nil {
t.Fatalf("err: %s", err)
}
if actual != TypeInt {
t.Fatalf("bad: %s", actual)
}
}

func TestIndexType_nonHomogenous(t *testing.T) {
i := &Index{
Target: &VariableAccess{Name: "foo"},
Key: &LiteralNode{
Typex: TypeInt,
Value: 1,
},
}

scope := &BasicScope{
VarMap: map[string]Variable{
"foo": Variable{
Type: TypeList,
Value: []Variable{
Variable{
Type: TypeString,
Value: "Hello",
},
Variable{
Type: TypeInt,
Value: 43,
},
},
},
},
}

_, err := i.Type(scope)
if err == nil {
t.Fatalf("expected error")
}
}
6 changes: 6 additions & 0 deletions ast/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ func NewVariable(v interface{}) (result Variable, err error) {
return
}

// String implements Stringer on Variable, displaying the type and value
// of the Variable.
func (v Variable) String() string {
return fmt.Sprintf("{Variable (%s): %+v}", v.Type, v.Value)
}

// 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.
Expand Down
15 changes: 15 additions & 0 deletions ast/scope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,18 @@ func TestBasicScopeLookupVar(t *testing.T) {
t.Fatal("should find foo")
}
}

func TestVariableStringer(t *testing.T) {
expected := "{Variable (TypeInt): 42}"
variable := &Variable{
Type: TypeInt,
Value: 42,
}

actual := variable.String()

if actual != expected {
t.Fatalf("variable string formatting:\nExpected: %s\n Got: %s\n",
expected, actual)
}
}
4 changes: 4 additions & 0 deletions ast/type_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions ast/variable_access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,20 @@ func TestVariableAccessType_invalid(t *testing.T) {
t.Fatal("should error")
}
}

func TestVariableAccessType_list(t *testing.T) {
c := &VariableAccess{Name: "baz"}
scope := &BasicScope{
VarMap: map[string]Variable{
"baz": Variable{Type: TypeList},
},
}

actual, err := c.Type(scope)
if err != nil {
t.Fatalf("err: %s", err)
}
if actual != TypeList {
t.Fatalf("bad: %s", actual)
}
}
52 changes: 52 additions & 0 deletions check_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ func (v *TypeCheck) visit(raw ast.Node) ast.Node {
case *ast.Call:
tc := &typeCheckCall{n}
result, err = tc.TypeCheck(v)
case *ast.Index:
tc := &typeCheckIndex{n}
result, err = tc.TypeCheck(v)
case *ast.Concat:
tc := &typeCheckConcat{n}
result, err = tc.TypeCheck(v)
Expand Down Expand Up @@ -285,6 +288,55 @@ func (tc *typeCheckVariableAccess) TypeCheck(v *TypeCheck) (ast.Node, error) {
return tc.n, nil
}

type typeCheckIndex struct {
n *ast.Index
}

func (tc *typeCheckIndex) TypeCheck(v *TypeCheck) (ast.Node, error) {

value, err := tc.n.Key.Type(v.Scope)
if err != nil {
return nil, err
}
if value != ast.TypeInt {
return nil, fmt.Errorf("key of an index must be an int, was %s", value)
}

// Ensure we have a VariableAccess as the target
varAccessNode, ok := tc.n.Target.(*ast.VariableAccess)
if !ok {
return nil, fmt.Errorf("target of an index must be a VariableAccess node, was %T", tc.n.Target)
}

// Get the variable
variable, ok := v.Scope.LookupVar(varAccessNode.Name)
if !ok {
return nil, fmt.Errorf("unknown variable accessed: %s", varAccessNode.Name)
}
if variable.Type != ast.TypeList {
return nil, fmt.Errorf("invalid index operation into non-indexable type: %s", variable.Type)
}

list := variable.Value.([]ast.Variable)

// Ensure that the types of the list elements are homogenous
listTypes := make(map[ast.Type]struct{})
for _, v := range list {
if _, ok := listTypes[v.Type]; ok {
continue
}
listTypes[v.Type] = struct{}{}
}

if len(listTypes) != 1 {
return nil, fmt.Errorf("list %q does not have homogenous types (%s)", varAccessNode.Name)
}

// This is the type since the list is homogenous in type
v.StackPush(list[0].Type)
return tc.n, nil
}

func (v *TypeCheck) ImplicitConversion(
actual ast.Type, expected ast.Type, n ast.Node) ast.Node {
if v.Implicit == nil {
Expand Down
Loading

0 comments on commit 0457360

Please sign in to comment.