Skip to content

Commit

Permalink
Type checker awareness of partially-unknown collections
Browse files Browse the repository at this point in the history
Earlier we made the evaluator support collections (lists and maps) that
have a mixture of known and unknown members. However, since the type
checker was not also aware of this it was short-circuiting as before
and skipping type checking of any expression dependent on a value from
a partially-unknown collection.

As well as missing some conversions, which was masked by the evaluator's
own short-circuiting as expected, this also caused us to skip the
conversion of arithmetic to built-in functions, which then caused the
evaluator to fail since it doesn't have any support for direct evaluation
of arithmetic.

This follows the same principle as the evalator changes: there's no longer
a global rule that any partially-unknown value gets flattened to a total
unknown when read from a variable, so each node type separately must deal
with the possibility of unknowns and decide what their behavior will be
in that case.

For most node types the behavior is simple: any unknowns mean that the
result is itself unknown. For index, we optimistically assume that
unknown values will become known values of the correct type on a future
pass and so we let them pass through, which is safe because of our
existing changes to how the evaluator supports unknowns.

This also includes a reorganization of the helper functions
VariableListElementTypesAreHomogenous and
VariableMapValueTypesAreHomogenous to make the different cases easier to
follow in the presence of unknowns.
  • Loading branch information
apparentlymart committed May 1, 2017
1 parent 5a2557c commit 747a6e1
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 38 deletions.
57 changes: 32 additions & 25 deletions ast/variables_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,61 @@ package ast
import "fmt"

func VariableListElementTypesAreHomogenous(variableName string, list []Variable) (Type, error) {
listTypes := make(map[Type]struct{})
if len(list) == 0 {
return TypeInvalid, fmt.Errorf("list %q does not have any elements so cannot determine type.", variableName)
}

elemType := TypeUnknown
for _, v := range list {
// Allow unknown
if v.Type == TypeUnknown {
continue
}

if _, ok := listTypes[v.Type]; ok {
if elemType == TypeUnknown {
elemType = v.Type
continue
}
listTypes[v.Type] = struct{}{}
}

if len(listTypes) != 1 && len(list) != 0 {
return TypeInvalid, fmt.Errorf("list %q does not have homogenous types. found %s", variableName, reportTypes(listTypes))
}
if v.Type != elemType {
return TypeInvalid, fmt.Errorf(
"list %q does not have homogenous types. found %s and then %s",
variableName,
elemType, v.Type,
)
}

if len(list) > 0 {
return list[0].Type, nil
elemType = v.Type
}

return TypeInvalid, fmt.Errorf("list %q does not have any elements so cannot determine type.", variableName)
return elemType, nil
}

func VariableMapValueTypesAreHomogenous(variableName string, vmap map[string]Variable) (Type, error) {
valueTypes := make(map[Type]struct{})
if len(vmap) == 0 {
return TypeInvalid, fmt.Errorf("map %q does not have any elements so cannot determine type.", variableName)
}

elemType := TypeUnknown
for _, v := range vmap {
// Allow unknown
if v.Type == TypeUnknown {
continue
}

if _, ok := valueTypes[v.Type]; ok {
if elemType == TypeUnknown {
elemType = v.Type
continue
}

valueTypes[v.Type] = struct{}{}
}

if len(valueTypes) != 1 && len(vmap) != 0 {
return TypeInvalid, fmt.Errorf("map %q does not have homogenous value types. found %s", variableName, reportTypes(valueTypes))
}
if v.Type != elemType {
return TypeInvalid, fmt.Errorf(
"map %q does not have homogenous types. found %s and then %s",
variableName,
elemType, v.Type,
)
}

// For loop here is an easy way to get a single key, we return immediately.
for _, v := range vmap {
return v.Type, nil
elemType = v.Type
}

// This means the map is empty
return TypeInvalid, fmt.Errorf("map %q does not have any elements so cannot determine type.", variableName)
return elemType, nil
}
57 changes: 44 additions & 13 deletions check_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,6 @@ func (v *TypeCheck) visit(raw ast.Node) ast.Node {
pos.Column, pos.Line, err)
}

if v.StackPeek() == ast.TypeUnknown {
v.err = errExitUnknown
}

return result
}

Expand All @@ -116,6 +112,14 @@ func (tc *typeCheckArithmetic) TypeCheck(v *TypeCheck) (ast.Node, error) {
exprs[len(tc.n.Exprs)-1-i] = v.StackPop()
}

// If any operand is unknown then our result is automatically unknown
for _, ty := range exprs {
if ty == ast.TypeUnknown {
v.StackPush(ast.TypeUnknown)
return tc.n, nil
}
}

switch tc.n.Op {
case ast.ArithmeticOpLogicalAnd, ast.ArithmeticOpLogicalOr:
return tc.checkLogical(v, exprs)
Expand Down Expand Up @@ -333,6 +337,11 @@ func (tc *typeCheckCall) TypeCheck(v *TypeCheck) (ast.Node, error) {
continue
}

if args[i] == ast.TypeUnknown {
v.StackPush(ast.TypeUnknown)
return tc.n, nil
}

if args[i] != expected {
cn := v.ImplicitConversion(args[i], expected, tc.n.Args[i])
if cn != nil {
Expand All @@ -350,6 +359,11 @@ func (tc *typeCheckCall) TypeCheck(v *TypeCheck) (ast.Node, error) {
if function.Variadic && function.VariadicType != ast.TypeAny {
args = args[len(function.ArgTypes):]
for i, t := range args {
if t == ast.TypeUnknown {
v.StackPush(ast.TypeUnknown)
return tc.n, nil
}

if t != function.VariadicType {
realI := i + len(function.ArgTypes)
cn := v.ImplicitConversion(
Expand Down Expand Up @@ -384,6 +398,11 @@ func (tc *typeCheckConditional) TypeCheck(v *TypeCheck) (ast.Node, error) {
trueType := v.StackPop()
condType := v.StackPop()

if condType == ast.TypeUnknown {
v.StackPush(ast.TypeUnknown)
return tc.n, nil
}

if condType != ast.TypeBool {
cn := v.ImplicitConversion(condType, ast.TypeBool, tc.n.CondExpr)
if cn == nil {
Expand Down Expand Up @@ -457,6 +476,13 @@ func (tc *typeCheckOutput) TypeCheck(v *TypeCheck) (ast.Node, error) {
types[len(n.Exprs)-1-i] = v.StackPop()
}

for _, ty := range types {
if ty == ast.TypeUnknown {
v.StackPush(ast.TypeUnknown)
return tc.n, nil
}
}

// If there is only one argument and it is a list, we evaluate to a list
if len(types) == 1 {
switch t := types[0]; t {
Expand All @@ -469,7 +495,14 @@ func (tc *typeCheckOutput) TypeCheck(v *TypeCheck) (ast.Node, error) {
}

// Otherwise, all concat args must be strings, so validate that
resultType := ast.TypeString
for i, t := range types {

if t == ast.TypeUnknown {
resultType = ast.TypeUnknown
continue
}

if t != ast.TypeString {
cn := v.ImplicitConversion(t, ast.TypeString, n.Exprs[i])
if cn != nil {
Expand All @@ -482,8 +515,8 @@ func (tc *typeCheckOutput) TypeCheck(v *TypeCheck) (ast.Node, error) {
}
}

// This always results in type string
v.StackPush(ast.TypeString)
// This always results in type string, unless there are unknowns
v.StackPush(resultType)

return n, nil
}
Expand All @@ -509,13 +542,6 @@ func (tc *typeCheckVariableAccess) TypeCheck(v *TypeCheck) (ast.Node, error) {
"unknown variable accessed: %s", tc.n.Name)
}

// Check if the variable contains any unknown types. If so, then
// mark it as unknown.
if ast.IsUnknown(variable) {
v.StackPush(ast.TypeUnknown)
return tc.n, nil
}

// Add the type to the stack
v.StackPush(variable.Type)

Expand All @@ -530,6 +556,11 @@ func (tc *typeCheckIndex) TypeCheck(v *TypeCheck) (ast.Node, error) {
keyType := v.StackPop()
targetType := v.StackPop()

if keyType == ast.TypeUnknown || targetType == ast.TypeUnknown {
v.StackPush(ast.TypeUnknown)
return tc.n, nil
}

// Ensure we have a VariableAccess as the target
varAccessNode, ok := tc.n.Target.(*ast.VariableAccess)
if !ok {
Expand Down
28 changes: 28 additions & 0 deletions eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,34 @@ func TestEval(t *testing.T) {
UnknownValue,
TypeUnknown,
},
{
// Unknowns can short-circuit bits of our type checking
// AST transform, such as the promotion of arithmetic to
// functions. This test ensures that the evaluator and the
// type checker co-operate to ensure that this doesn't cause
// raw arithmetic nodes to be evaluated (which is not supported).
"${var.alist[0 + var.unknown]}",
&ast.BasicScope{
VarMap: map[string]ast.Variable{
"var.alist": ast.Variable{
Type: ast.TypeList,
Value: []ast.Variable{
ast.Variable{
Type: ast.TypeInt,
Value: 2,
},
},
},
"var.unknown": ast.Variable{
Type: ast.TypeUnknown,
Value: UnknownValue,
},
},
},
false,
UnknownValue,
TypeUnknown,
},
{
"${join(var.alist)}",
&ast.BasicScope{
Expand Down

0 comments on commit 747a6e1

Please sign in to comment.