-
Notifications
You must be signed in to change notification settings - Fork 34
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 checker awareness of partially-unknown collections #52
Conversation
When the type checker finds unknowns it will generally skip any AST rewriting it would normally do to insert type conversions, so if the output result contains at least one unknown then it's possible that our results would not all be of type string as we would usually expect. To guard against this, we check first if there are any unknowns and just return early, before we even start building our result buffer.
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.
This fixes the bug introduced by #51. |
@@ -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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the scope of this review I can't claim to know the full impact of this, but this stood out to me as a different behavior. Is this fully understood?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would add, is this actually an error, or should it return TypeUnknown
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the same behavior, just implemented in a different way. It used to build a slice of types and then check if the slice was empty at the end of the function, but now it just takes care of that up-front.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could in principle return TypeUnknown
, but that would be the first instance of an unknown value originating from anywhere other than a variable access, and so may have unintended consequences I'd rather not deal with here.
In the long run I think the correct behavior is for HIL to keep track of the element type of lists and maps as part of the type itself, rather than attempting to infer it from content. This is what #42 was about, but I ended up closing that with the intent of folding it into the more holistic language work we're hoping to do in the not-too-distant future.
This was actually redundant anyway since HIL itself applied a similar rule where any partially-unknown list would be automatically flattened to a single unknown value. However, now we're changing HIL to explicitly permit partially-unknown lists so that we can allow the index operator [...] to succeed when applied to one of the elements that _is_ known. This, in conjunction with hashicorp/hil#51 and hashicorp/hil#52, fixes #3449.
This was actually redundant anyway since HIL itself applied a similar rule where any partially-unknown list would be automatically flattened to a single unknown value. However, now we're changing HIL to explicitly permit partially-unknown lists so that we can allow the index operator [...] to succeed when applied to one of the elements that _is_ known. This, in conjunction with hashicorp/hil#51 and hashicorp/hil#52, fixes #3449.
In #52 we relaxed the rule that any unknown value would immediately cause an early exit, which in turn requires the type checker for each node to specifically deal with unknown values. However, we missed dealing with the check that both types match in a conditional, causing a conditional to fail if either side of it is unknown. This causes errors in Terraform, as described in hashicorp/terraform#14399.
@apparentlymart does this change means you can never work with empty lists ? variable "my_var" {
type = "list"
default = []
}
resource "aws_instance" "main" {
count = "${var.count}"
private_ip = "${length(var.my_var) == 0 ? "" : var.my_var[count.index]}"
} the above is failing in Terraform v0.10.0 with : * module.bob.aws_instance.main: At column 54, line 1: list "var.my_var" does not have any elements so cannot determine type. in:
${length(var.my_var) == 0 ? "" : var.my_var[count.index]} |
@salimane that was not a consequence of this change, but it is true in certain scenarios. Any time the HIL type checker needs to know the element type of a list it requires at least one element to infer it, since the type system isn't strong enough to retain that information as part of the type. (This is a consequence of the fact that Terraform only supported strings at first, and some parts of its design are still not entirely type-aware.) The change in #42 was intended to address that, but it ended up being trickier than first thought so it ended up rolling into a bigger set of language improvements we're currently designing that make broader changes to the architecture of the evaluator to address this and other gotchas by consistently handling types throughout. We'll have more to share on that soon once we have some more concrete plans, but this issue in particular is uncontroversial and will be fixed as part of it! |
@apparentlymart thanks for the context . But is there a workaround for the errors I'm getting above ? |
In that specific scenario it looks like you expect that variable "my_var" {
type = "list"
default = []
}
resource "aws_instance" "main" {
count = "${var.count}"
private_ip = "${var.my_var[count.index]}"
} When If you're count doesn't necessarily match the length of concat(var.my_var, list("")) Unfortunately due to other current HIL limitations (which will also be fixed by the forthcoming changes, thankfully) it's not possible to use the |
@apparentlymart thank you very much for the workaround 🍺 |
@apparentlymart actually the workaround didn't work with terraform v0.10.0, I'm still getting variable "my_var" {
type = "list"
default = []
}
resource "aws_instance" "main" {
count = "${var.count}"
private_ip = "${var.my_var[count.index]}"
} * module.bob.aws_instance.main: At column 18, line 1: list "var.my_var" does not have any elements so cannot determine type. in:
${var.my_var[count.index]} I |
Hmm that's strange, @salimane. I'm afraid I'm not sure why Terraform would be evaluating the expression in the case where In that case, doing something with concatenation of an empty string onto the end of your list is probably the best we can do for a workaround for now. |
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
andVariableMapValueTypesAreHomogenous
to make the different cases easier to follow in the presence of unknowns.This PR also includes a bonus commit to fix incorrect handling of unknowns in the
Output
evaluator, which would result in a crash in certain cases due to this same type checker short-circuit behavior.