Skip to content

Commit

Permalink
hclsyntax: ConditionalExpr can refine its unknown results
Browse files Browse the repository at this point in the history
When ConditionalExpr has an unknown predicate it can still often infer
some refinement to the range of its result by noticing characteristics
that the two results have in common.

In all cases we can test if either result could be null and return a
definitely-not-null unknown value if not.

For two known numbers we can constrain the range to be between those two
numbers. This is primarily aimed at the common case where the two possible
results are zero and one, which significantly constrains the range.

For two known collections of the same kind we can constrain the length
to be between the two collection lengths.

In these last two cases we can also sometimes collapse the unknown into
a known value if the range gets reduced enough. For example, if choosing
between two collections of the same length we might return a known
collection of that length containing unknown elements, rather than an
unknown collection.
  • Loading branch information
apparentlymart committed May 31, 2023
1 parent e0058a2 commit 628da05
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 1 deletion.
54 changes: 53 additions & 1 deletion hclsyntax/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,59 @@ func (e *ConditionalExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostic
return cty.UnknownVal(resultType), diags
}
if !condResult.IsKnown() {
return cty.UnknownVal(resultType), diags
// We might be able to offer a refined range for the result based on
// the two possible outcomes.
if trueResult.Type() == cty.Number && falseResult.Type() == cty.Number {
// This case deals with the common case of (predicate ? 1 : 0) and
// significantly decreases the range of the result in that case.
if !(trueResult.IsNull() || falseResult.IsNull()) {
if gt := trueResult.GreaterThan(falseResult); gt.IsKnown() {
b := cty.UnknownVal(cty.Number).Refine()
if gt.True() {
b = b.
NumberRangeLowerBound(falseResult, true).
NumberRangeUpperBound(trueResult, true)
} else {
b = b.
NumberRangeLowerBound(trueResult, true).
NumberRangeUpperBound(falseResult, true)
}
b = b.NotNull() // If neither of the results is null then the result can't be either
return b.NewValue().WithSameMarks(condResult).WithSameMarks(trueResult).WithSameMarks(falseResult), diags
}
}
}
if trueResult.Type().IsCollectionType() && falseResult.Type().IsCollectionType() {
if trueResult.Type().Equals(falseResult.Type()) {
if !(trueResult.IsNull() || falseResult.IsNull()) {
trueLen := trueResult.Length()
falseLen := falseResult.Length()
if gt := trueLen.GreaterThan(falseLen); gt.IsKnown() {
b := cty.UnknownVal(resultType).Refine()
trueLen, _ := trueLen.AsBigFloat().Int64()
falseLen, _ := falseLen.AsBigFloat().Int64()
if gt.True() {
b = b.
CollectionLengthLowerBound(int(falseLen)).
CollectionLengthUpperBound(int(trueLen))
} else {
b = b.
CollectionLengthLowerBound(int(trueLen)).
CollectionLengthUpperBound(int(falseLen))
}
b = b.NotNull() // If neither of the results is null then the result can't be either
return b.NewValue().WithSameMarks(condResult).WithSameMarks(trueResult).WithSameMarks(falseResult), diags
}
}
}
}
trueRng := trueResult.Range()
falseRng := falseResult.Range()
ret := cty.UnknownVal(resultType)
if trueRng.DefinitelyNotNull() && falseRng.DefinitelyNotNull() {
ret = ret.RefineNotNull()
}
return ret.WithSameMarks(condResult).WithSameMarks(trueResult).WithSameMarks(falseResult), diags
}
condResult, err := convert.Convert(condResult, cty.Bool)
if err != nil {
Expand Down
79 changes: 79 additions & 0 deletions hclsyntax/expression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1840,6 +1840,85 @@ EOT
cty.DynamicVal,
0,
},
{
`unknown ? 1 : 0`,
&hcl.EvalContext{
Variables: map[string]cty.Value{
"unknown": cty.UnknownVal(cty.Bool),
},
},
cty.UnknownVal(cty.Number).Refine().
NotNull().
NumberRangeLowerBound(cty.Zero, true).
NumberRangeUpperBound(cty.NumberIntVal(1), true).
NewValue(),
0,
},
{
`unknown ? 0 : 1`,
&hcl.EvalContext{
Variables: map[string]cty.Value{
"unknown": cty.UnknownVal(cty.Bool),
},
},
cty.UnknownVal(cty.Number).Refine().
NotNull().
NumberRangeLowerBound(cty.Zero, true).
NumberRangeUpperBound(cty.NumberIntVal(1), true).
NewValue(),
0,
},
{
`unknown ? a : b`,
&hcl.EvalContext{
Variables: map[string]cty.Value{
"unknown": cty.UnknownVal(cty.Bool),
"a": cty.UnknownVal(cty.Bool).RefineNotNull(),
"b": cty.UnknownVal(cty.Bool).RefineNotNull(),
},
},
cty.UnknownVal(cty.Bool).RefineNotNull(),
0,
},
{
`unknown ? a : b`,
&hcl.EvalContext{
Variables: map[string]cty.Value{
"unknown": cty.UnknownVal(cty.Bool),
"a": cty.ListValEmpty(cty.String),
"b": cty.ListValEmpty(cty.String),
},
},
cty.ListValEmpty(cty.String), // deduced through refinements
0,
},
{
`unknown ? a : b`,
&hcl.EvalContext{
Variables: map[string]cty.Value{
"unknown": cty.UnknownVal(cty.Bool),
"a": cty.ListValEmpty(cty.String),
"b": cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}),
},
},
cty.UnknownVal(cty.List(cty.String)).Refine().
NotNull().
CollectionLengthUpperBound(1).
NewValue(),
0,
},
{
`unknown ? a : b`,
&hcl.EvalContext{
Variables: map[string]cty.Value{
"unknown": cty.UnknownVal(cty.Bool),
"a": cty.ListVal([]cty.Value{cty.StringVal("hello")}),
"b": cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}),
},
},
cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}), // deduced through refinements
0,
},
{ // marked conditional
`var.foo ? 1 : 0`,
&hcl.EvalContext{
Expand Down

0 comments on commit 628da05

Please sign in to comment.