Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type system to support list and map element types #42

Closed
wants to merge 7 commits into from
48 changes: 0 additions & 48 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,51 +49,3 @@ var InitPos = Pos{Column: 1, Line: 1}
// implementing this basic visitor pattern style is still very useful even
// if you have to type switch.
type Visitor func(Node) Node

//go:generate stringer -type=Type

// Type is the type of any value.
type Type uint32

const (
TypeInvalid Type = 0
TypeAny Type = 1 << iota
TypeBool
TypeString
TypeInt
TypeFloat
TypeList
TypeMap

// This is a special type used by Terraform to mark "unknown" values.
// It is impossible for this type to be introduced into your HIL programs
// unless you explicitly set a variable to this value. In that case,
// any operation including the variable will return "TypeUnknown" as the
// type.
TypeUnknown
)

func (t Type) Printable() string {
switch t {
case TypeInvalid:
return "invalid type"
case TypeAny:
return "any type"
case TypeBool:
return "type bool"
case TypeString:
return "type string"
case TypeInt:
return "type int"
case TypeFloat:
return "type float"
case TypeList:
return "type list"
case TypeMap:
return "type map"
case TypeUnknown:
return "type unknown"
default:
return "unknown type"
}
}
33 changes: 33 additions & 0 deletions ast/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
)

// Call represents a function call.
//
// The type checker replaces Call nodes with CallTyped nodes in order to retain
// the type information for use in the evaluation phase.
type Call struct {
Func string
Args []Node
Expand Down Expand Up @@ -45,3 +48,33 @@ func (n *Call) Type(s Scope) (Type, error) {
func (n *Call) GoString() string {
return fmt.Sprintf("*%#v", *n)
}

// CallTyped represents a function call *after* type checking.
//
// The type check phase replaces any Call node with a CallTyped node in order to
// capture the type information that was determined so that it can be used during
// a subsequent evaluation.
type CallTyped struct {
// CallTyped embeds the Call it was created from.
Call

// ReturnType is the return type determined for the function during type checking.
// A well-behaved function implementation is bound by the interface contract to return
// a value that conforms to this type.
ReturnType Type
}

func (n *CallTyped) Accept(v Visitor) Node {
// Accept must be re-implemented on CallTyped to make sure we pass the full CallTyped
// value, rather than the embedded Call value that would result were we to inherit
// the implementation from Call.
for i, a := range n.Args {
n.Args[i] = a.Accept(v)
}

return v(n)
}

func (n *CallTyped) Type(s Scope) (Type, error) {
return n.ReturnType, nil
}
4 changes: 2 additions & 2 deletions ast/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (n *Index) Type(s Scope) (Type, error) {
return TypeInvalid, fmt.Errorf("unknown variable accessed: %s", variableAccess.Name)
}

switch variable.Type {
switch variable.Type.(type) {
case TypeList:
return n.typeList(variable, variableAccess.Name)
case TypeMap:
Expand Down Expand Up @@ -65,7 +65,7 @@ func reportTypes(typesFound map[Type]struct{}) string {
stringTypes := make([]string, len(typesFound))
i := 0
for k, _ := range typesFound {
stringTypes[0] = k.String()
stringTypes[0] = k.Printable()
i++
}
return strings.Join(stringTypes, ", ")
Expand Down
16 changes: 8 additions & 8 deletions ast/index_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestIndexTypeMap_empty(t *testing.T) {
scope := &BasicScope{
VarMap: map[string]Variable{
"foo": Variable{
Type: TypeMap,
Type: TypeMap{TypeString},
Value: map[string]Variable{},
},
},
Expand All @@ -44,7 +44,7 @@ func TestIndexTypeMap_string(t *testing.T) {
scope := &BasicScope{
VarMap: map[string]Variable{
"foo": Variable{
Type: TypeMap,
Type: TypeMap{TypeString},
Value: map[string]Variable{
"baz": Variable{
Type: TypeString,
Expand Down Expand Up @@ -80,7 +80,7 @@ func TestIndexTypeMap_int(t *testing.T) {
scope := &BasicScope{
VarMap: map[string]Variable{
"foo": Variable{
Type: TypeMap,
Type: TypeMap{TypeInt},
Value: map[string]Variable{
"baz": Variable{
Type: TypeInt,
Expand Down Expand Up @@ -116,7 +116,7 @@ func TestIndexTypeMap_nonHomogenous(t *testing.T) {
scope := &BasicScope{
VarMap: map[string]Variable{
"foo": Variable{
Type: TypeMap,
Type: TypeMap{TypeAny},
Value: map[string]Variable{
"bar": Variable{
Type: TypeString,
Expand Down Expand Up @@ -149,7 +149,7 @@ func TestIndexTypeList_empty(t *testing.T) {
scope := &BasicScope{
VarMap: map[string]Variable{
"foo": Variable{
Type: TypeList,
Type: TypeList{TypeString},
Value: []Variable{},
},
},
Expand All @@ -176,7 +176,7 @@ func TestIndexTypeList_string(t *testing.T) {
scope := &BasicScope{
VarMap: map[string]Variable{
"foo": Variable{
Type: TypeList,
Type: TypeList{TypeString},
Value: []Variable{
Variable{
Type: TypeString,
Expand Down Expand Up @@ -212,7 +212,7 @@ func TestIndexTypeList_int(t *testing.T) {
scope := &BasicScope{
VarMap: map[string]Variable{
"foo": Variable{
Type: TypeList,
Type: TypeList{TypeInt},
Value: []Variable{
Variable{
Type: TypeInt,
Expand Down Expand Up @@ -248,7 +248,7 @@ func TestIndexTypeList_nonHomogenous(t *testing.T) {
scope := &BasicScope{
VarMap: map[string]Variable{
"foo": Variable{
Type: TypeList,
Type: TypeList{TypeAny},
Value: []Variable{
Variable{
Type: TypeString,
Expand Down
12 changes: 7 additions & 5 deletions ast/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ func (n *Output) Type(s Scope) (Type, error) {
if err != nil {
return TypeInvalid, err
}
switch exprType {
switch exprType.(type) {
case TypeList:
return TypeList, nil
return exprType, nil
case TypeMap:
return TypeMap, nil
return exprType, nil
}
}

Expand All @@ -67,10 +67,12 @@ func (n *Output) Type(s Scope) (Type, error) {
return TypeInvalid, err
}
// We only look for things we know we can't coerce with an implicit conversion func
if exprType == TypeList || exprType == TypeMap {
switch exprType.(type) {
case TypeList, TypeMap:
return TypeInvalid, fmt.Errorf(
"multi-expression HIL outputs may only have string inputs: %d is type %s",
index, exprType)
index, exprType,
)
}
}

Expand Down
12 changes: 6 additions & 6 deletions ast/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func TestOutput_type(t *testing.T) {
Scope: &BasicScope{
VarMap: map[string]Variable{
"testvar": Variable{
Type: TypeList,
Type: TypeList{TypeString},
Value: []Variable{
Variable{
Type: TypeString,
Expand All @@ -57,7 +57,7 @@ func TestOutput_type(t *testing.T) {
},
},
},
ReturnType: TypeList,
ReturnType: TypeList{TypeString},
},
{
Name: "Single map expression",
Expand All @@ -71,7 +71,7 @@ func TestOutput_type(t *testing.T) {
Scope: &BasicScope{
VarMap: map[string]Variable{
"testvar": Variable{
Type: TypeMap,
Type: TypeMap{TypeString},
Value: map[string]Variable{
"key1": Variable{
Type: TypeString,
Expand All @@ -85,7 +85,7 @@ func TestOutput_type(t *testing.T) {
},
},
},
ReturnType: TypeMap,
ReturnType: TypeMap{TypeString},
},
{
Name: "Multiple map expressions",
Expand All @@ -102,7 +102,7 @@ func TestOutput_type(t *testing.T) {
Scope: &BasicScope{
VarMap: map[string]Variable{
"testvar": Variable{
Type: TypeMap,
Type: TypeMap{TypeString},
Value: map[string]Variable{
"key1": Variable{
Type: TypeString,
Expand Down Expand Up @@ -134,7 +134,7 @@ func TestOutput_type(t *testing.T) {
Scope: &BasicScope{
VarMap: map[string]Variable{
"testvar": Variable{
Type: TypeList,
Type: TypeList{TypeString},
Value: []Variable{
Variable{
Type: TypeString,
Expand Down
51 changes: 43 additions & 8 deletions ast/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,60 @@ func (v Variable) String() string {
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
ArgTypes []Type

// Either ReturnType *or* ReturnTypeFunc decide the type of the returned
// value. The Callback MUST return this type. Setting both attributes
// is invalid usage.
ReturnType Type
ReturnTypeFunc ReturnTypeFunc

// 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.
// Either Callback or CallbackTyped are called as the implementation of
// the function. Both recieve a slice interface values of an appropriate
// dynamic type for the call arguments, while CallbackTyped additionally
// recieves the required result type, for easier implementation of
// type-generic functions without duplicating the logic in ReturnTypeFunc.
//
// The argument types are guaranteed by the type checker to match what is
// described by ArgTypes, ReturnTypeFunc and VariadicType.
// The length of the args is strictly == len(ArgTypes) unless Varidiac
// is true, in which case its >= len(ArgTypes).
Callback func([]interface{}) (interface{}, error)
//
// The value returned MUST confirm to the function's return type, whether
// determined by ReturnType or ReturnTypeFunc.
//
// Setting both Callback and CallbackTyped is invalid usage.
Callback func([]interface{}) (interface{}, error)
CallbackTyped func(args []interface{}, returnType Type) (interface{}, error)
}

// ReturnTypeFunc is a function type used to decide the return type of a
// function based on its argument types.
//
// The given argument types are those of the actual *call*, not the types
// declared in ArgTypes and VariadicType. This allows the definition of
// functions that work with TypeList and TypeMap in a generic way for all
// element types, and other similar interesting cases.
//
// Function must either return a concrete Type or an user-oriented error
// that explains why the given combination of argument types are not
// acceptable. If an error is not returned then the Function's Callback
// MUST be able to accept the given argument types without crashing,
// and produce a value of the given return type.
//
// ReturnTypeFunc is called only if the given ArgTypes and VariadicType
// match the given arguments, so it need only check additional
// unusual rules that cannot be expressed as static types. Use TypeAny
// (or TypeList{TypeAny}, etc) in ArgTypes to bypass the simple type
// checking for certain arguments where more complex rules are required.
type ReturnTypeFunc func(argTypes []Type) (Type, error)

// BasicScope is a simple scope that looks up variables and functions
// using a map.
type BasicScope struct {
Expand Down
2 changes: 1 addition & 1 deletion ast/scope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func TestBasicScopeLookupVar(t *testing.T) {
}

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