Skip to content

Commit 4945193

Browse files
authored
Merge pull request #633 from hashicorp/jbardin/conditional-length-refinements
hclsyntax: conditional refinements of collections must use Range()
2 parents 63067e8 + bad33d5 commit 4945193

File tree

2 files changed

+203
-49
lines changed

2 files changed

+203
-49
lines changed

hclsyntax/expression.go

Lines changed: 78 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -696,64 +696,95 @@ func (e *ConditionalExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostic
696696
return cty.UnknownVal(resultType), diags
697697
}
698698
if !condResult.IsKnown() {
699+
// we use the unmarked values throughout the unknown branch
700+
_, condResultMarks := condResult.Unmark()
701+
trueResult, trueResultMarks := trueResult.Unmark()
702+
falseResult, falseResultMarks := falseResult.Unmark()
703+
704+
// use a value to merge marks
705+
_, resMarks := cty.DynamicVal.WithMarks(condResultMarks, trueResultMarks, falseResultMarks).Unmark()
706+
707+
trueRange := trueResult.Range()
708+
falseRange := falseResult.Range()
709+
710+
// if both branches are known to be null, then the result must still be null
711+
if trueResult.IsNull() && falseResult.IsNull() {
712+
return cty.NullVal(resultType).WithMarks(resMarks), diags
713+
}
714+
699715
// We might be able to offer a refined range for the result based on
700716
// the two possible outcomes.
701717
if trueResult.Type() == cty.Number && falseResult.Type() == cty.Number {
702-
// This case deals with the common case of (predicate ? 1 : 0) and
703-
// significantly decreases the range of the result in that case.
704-
if !(trueResult.IsNull() || falseResult.IsNull()) {
705-
if gt := trueResult.GreaterThan(falseResult); gt.IsKnown() {
706-
b := cty.UnknownVal(cty.Number).Refine()
707-
if gt.True() {
708-
b = b.
709-
NumberRangeLowerBound(falseResult, true).
710-
NumberRangeUpperBound(trueResult, true)
711-
} else {
712-
b = b.
713-
NumberRangeLowerBound(trueResult, true).
714-
NumberRangeUpperBound(falseResult, true)
715-
}
716-
b = b.NotNull() // If neither of the results is null then the result can't be either
717-
return b.NewValue().WithSameMarks(condResult).WithSameMarks(trueResult).WithSameMarks(falseResult), diags
718+
ref := cty.UnknownVal(cty.Number).Refine()
719+
if trueRange.DefinitelyNotNull() && falseRange.DefinitelyNotNull() {
720+
ref = ref.NotNull()
721+
}
722+
723+
falseLo, falseLoInc := falseRange.NumberLowerBound()
724+
falseHi, falseHiInc := falseRange.NumberUpperBound()
725+
trueLo, trueLoInc := trueRange.NumberLowerBound()
726+
trueHi, trueHiInc := trueRange.NumberUpperBound()
727+
728+
if falseLo.IsKnown() && trueLo.IsKnown() {
729+
lo, loInc := falseLo, falseLoInc
730+
switch {
731+
case trueLo.LessThan(falseLo).True():
732+
lo, loInc = trueLo, trueLoInc
733+
case trueLo.Equals(falseLo).True():
734+
loInc = trueLoInc || falseLoInc
735+
}
736+
737+
ref = ref.NumberRangeLowerBound(lo, loInc)
738+
}
739+
740+
if falseHi.IsKnown() && trueHi.IsKnown() {
741+
hi, hiInc := falseHi, falseHiInc
742+
switch {
743+
case trueHi.GreaterThan(falseHi).True():
744+
hi, hiInc = trueHi, trueHiInc
745+
case trueHi.Equals(falseHi).True():
746+
hiInc = trueHiInc || falseHiInc
718747
}
748+
ref = ref.NumberRangeUpperBound(hi, hiInc)
719749
}
750+
751+
return ref.NewValue().WithMarks(resMarks), diags
720752
}
753+
721754
if trueResult.Type().IsCollectionType() && falseResult.Type().IsCollectionType() {
722755
if trueResult.Type().Equals(falseResult.Type()) {
723-
if !(trueResult.IsNull() || falseResult.IsNull()) {
724-
trueLen := trueResult.Length()
725-
falseLen := falseResult.Length()
726-
if gt := trueLen.GreaterThan(falseLen); gt.IsKnown() {
727-
b := cty.UnknownVal(resultType).Refine()
728-
trueLen, _ := trueLen.AsBigFloat().Int64()
729-
falseLen, _ := falseLen.AsBigFloat().Int64()
730-
if gt.True() {
731-
b = b.
732-
CollectionLengthLowerBound(int(falseLen)).
733-
CollectionLengthUpperBound(int(trueLen))
734-
} else {
735-
b = b.
736-
CollectionLengthLowerBound(int(trueLen)).
737-
CollectionLengthUpperBound(int(falseLen))
738-
}
739-
b = b.NotNull() // If neither of the results is null then the result can't be either
740-
return b.NewValue().WithSameMarks(condResult).WithSameMarks(trueResult).WithSameMarks(falseResult), diags
741-
}
756+
ref := cty.UnknownVal(resultType).Refine()
757+
if trueRange.DefinitelyNotNull() && falseRange.DefinitelyNotNull() {
758+
ref = ref.NotNull()
759+
}
760+
761+
falseLo := falseRange.LengthLowerBound()
762+
falseHi := falseRange.LengthUpperBound()
763+
trueLo := trueRange.LengthLowerBound()
764+
trueHi := trueRange.LengthUpperBound()
765+
766+
lo := falseLo
767+
if trueLo < falseLo {
768+
lo = trueLo
742769
}
770+
771+
hi := falseHi
772+
if trueHi > falseHi {
773+
hi = trueHi
774+
}
775+
776+
ref = ref.CollectionLengthLowerBound(lo).CollectionLengthUpperBound(hi)
777+
return ref.NewValue().WithMarks(resMarks), diags
743778
}
744779
}
745-
_, condResultMarks := condResult.Unmark()
746-
trueResult, trueResultMarks := trueResult.Unmark()
747-
falseResult, falseResultMarks := falseResult.Unmark()
748780

749-
trueRng := trueResult.Range()
750-
falseRng := falseResult.Range()
751781
ret := cty.UnknownVal(resultType)
752-
if trueRng.DefinitelyNotNull() && falseRng.DefinitelyNotNull() {
782+
if trueRange.DefinitelyNotNull() && falseRange.DefinitelyNotNull() {
753783
ret = ret.RefineNotNull()
754784
}
755-
return ret.WithMarks(condResultMarks, trueResultMarks, falseResultMarks), diags
785+
return ret.WithMarks(resMarks), diags
756786
}
787+
757788
condResult, err := convert.Convert(condResult, cty.Bool)
758789
if err != nil {
759790
diags = append(diags, &hcl.Diagnostic{
@@ -1244,9 +1275,9 @@ func (e *ObjectConsKeyExpr) UnwrapExpression() Expression {
12441275

12451276
// ForExpr represents iteration constructs:
12461277
//
1247-
// tuple = [for i, v in list: upper(v) if i > 2]
1248-
// object = {for k, v in map: k => upper(v)}
1249-
// object_of_tuples = {for v in list: v.key: v...}
1278+
// tuple = [for i, v in list: upper(v) if i > 2]
1279+
// object = {for k, v in map: k => upper(v)}
1280+
// object_of_tuples = {for v in list: v.key: v...}
12501281
type ForExpr struct {
12511282
KeyVar string // empty if ignoring the key
12521283
ValVar string
@@ -1742,7 +1773,8 @@ func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
17421773
if ty.IsListType() && sourceVal.Type().IsCollectionType() {
17431774
// We can refine the length of an unknown list result based on
17441775
// the source collection's own length.
1745-
sourceRng := sourceVal.Range()
1776+
sv, _ := sourceVal.Unmark()
1777+
sourceRng := sv.Range()
17461778
ret = ret.Refine().
17471779
CollectionLengthLowerBound(sourceRng.LengthLowerBound()).
17481780
CollectionLengthUpperBound(sourceRng.LengthUpperBound()).

hclsyntax/expression_test.go

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1933,6 +1933,74 @@ EOT
19331933
NewValue(),
19341934
0,
19351935
},
1936+
{
1937+
`unknown ? i : j`,
1938+
&hcl.EvalContext{
1939+
Variables: map[string]cty.Value{
1940+
"unknown": cty.UnknownVal(cty.Bool),
1941+
"i": cty.NullVal(cty.Number),
1942+
"j": cty.NullVal(cty.Number),
1943+
},
1944+
},
1945+
cty.NullVal(cty.Number),
1946+
0,
1947+
},
1948+
{
1949+
`unknown ? im : jm`,
1950+
&hcl.EvalContext{
1951+
Variables: map[string]cty.Value{
1952+
"unknown": cty.UnknownVal(cty.Bool),
1953+
"im": cty.NullVal(cty.Number).Mark("a"),
1954+
"jm": cty.NullVal(cty.Number).Mark("b"),
1955+
},
1956+
},
1957+
cty.NullVal(cty.Number).Mark("a").Mark("b"),
1958+
0,
1959+
},
1960+
{
1961+
`unknown ? im : jm`,
1962+
&hcl.EvalContext{
1963+
Variables: map[string]cty.Value{
1964+
"unknown": cty.UnknownVal(cty.Bool).Mark("a"),
1965+
"im": cty.UnknownVal(cty.Number),
1966+
"jm": cty.UnknownVal(cty.Number).Mark("b"),
1967+
},
1968+
},
1969+
// the empty refinement may eventually be removed, but does nothing here
1970+
cty.UnknownVal(cty.Number).Refine().NewValue().Mark("a").Mark("b"),
1971+
0,
1972+
},
1973+
{
1974+
`unknown ? ix : jx`,
1975+
&hcl.EvalContext{
1976+
Variables: map[string]cty.Value{
1977+
"unknown": cty.UnknownVal(cty.Bool),
1978+
"ix": cty.UnknownVal(cty.Number),
1979+
"jx": cty.UnknownVal(cty.Number),
1980+
},
1981+
},
1982+
// the empty refinement may eventually be removed, but does nothing here
1983+
cty.UnknownVal(cty.Number).Refine().NewValue(),
1984+
0,
1985+
},
1986+
{
1987+
`unknown ? ir : jr`,
1988+
&hcl.EvalContext{
1989+
Variables: map[string]cty.Value{
1990+
"unknown": cty.UnknownVal(cty.Bool),
1991+
"ir": cty.UnknownVal(cty.Number).Refine().
1992+
NumberRangeLowerBound(cty.NumberIntVal(1), false).
1993+
NumberRangeUpperBound(cty.NumberIntVal(3), false).NewValue(),
1994+
"jr": cty.UnknownVal(cty.Number).Refine().
1995+
NumberRangeLowerBound(cty.NumberIntVal(2), true).
1996+
NumberRangeUpperBound(cty.NumberIntVal(4), true).NewValue(),
1997+
},
1998+
},
1999+
cty.UnknownVal(cty.Number).Refine().
2000+
NumberRangeLowerBound(cty.NumberIntVal(1), false).
2001+
NumberRangeUpperBound(cty.NumberIntVal(4), true).NewValue(),
2002+
0,
2003+
},
19362004
{
19372005
`unknown ? a : b`,
19382006
&hcl.EvalContext{
@@ -1946,17 +2014,71 @@ EOT
19462014
0,
19472015
},
19482016
{
1949-
`unknown ? a : b`,
2017+
`unknown ? al : bl`,
19502018
&hcl.EvalContext{
19512019
Variables: map[string]cty.Value{
19522020
"unknown": cty.UnknownVal(cty.Bool),
1953-
"a": cty.ListValEmpty(cty.String),
1954-
"b": cty.ListValEmpty(cty.String),
2021+
"al": cty.ListValEmpty(cty.String),
2022+
"bl": cty.ListValEmpty(cty.String),
19552023
},
19562024
},
19572025
cty.ListValEmpty(cty.String), // deduced through refinements
19582026
0,
19592027
},
2028+
{
2029+
`unknown ? am : bm`,
2030+
&hcl.EvalContext{
2031+
Variables: map[string]cty.Value{
2032+
"unknown": cty.UnknownVal(cty.Bool),
2033+
"am": cty.MapValEmpty(cty.String),
2034+
"bm": cty.MapValEmpty(cty.String).Mark("test"),
2035+
},
2036+
},
2037+
cty.MapValEmpty(cty.String).Mark("test"), // deduced through refinements
2038+
0,
2039+
},
2040+
{
2041+
`unknown ? ar : br`,
2042+
&hcl.EvalContext{
2043+
Variables: map[string]cty.Value{
2044+
"unknown": cty.UnknownVal(cty.Bool),
2045+
"ar": cty.UnknownVal(cty.Set(cty.String)).Refine().
2046+
CollectionLengthLowerBound(1).CollectionLengthUpperBound(3).NewValue(),
2047+
"br": cty.UnknownVal(cty.Set(cty.String)).Refine().
2048+
CollectionLengthLowerBound(2).CollectionLengthUpperBound(4).NewValue(),
2049+
},
2050+
},
2051+
cty.UnknownVal(cty.Set(cty.String)).Refine().CollectionLengthLowerBound(1).CollectionLengthUpperBound(4).NewValue(), // deduced through refinements
2052+
0,
2053+
},
2054+
{
2055+
`unknown ? arn : brn`,
2056+
&hcl.EvalContext{
2057+
Variables: map[string]cty.Value{
2058+
"unknown": cty.UnknownVal(cty.Bool),
2059+
"arn": cty.UnknownVal(cty.Set(cty.String)).Refine().NotNull().
2060+
CollectionLengthLowerBound(1).CollectionLengthUpperBound(2).NewValue(),
2061+
"brn": cty.UnknownVal(cty.Set(cty.String)).Refine().NotNull().
2062+
CollectionLengthLowerBound(3).CollectionLengthUpperBound(4).NewValue(),
2063+
},
2064+
},
2065+
cty.UnknownVal(cty.Set(cty.String)).Refine().NotNull().CollectionLengthLowerBound(1).CollectionLengthUpperBound(4).NewValue(), // deduced through refinements
2066+
0,
2067+
},
2068+
{
2069+
`unknown ? amr : bmr`,
2070+
&hcl.EvalContext{
2071+
Variables: map[string]cty.Value{
2072+
"unknown": cty.UnknownVal(cty.Bool),
2073+
"amr": cty.UnknownVal(cty.Set(cty.String)).Mark("test").Refine().
2074+
CollectionLengthLowerBound(1).CollectionLengthUpperBound(2).NewValue(),
2075+
"bmr": cty.UnknownVal(cty.Set(cty.String)).Mark("test").Refine().
2076+
CollectionLengthLowerBound(3).CollectionLengthUpperBound(4).NewValue(),
2077+
},
2078+
},
2079+
cty.UnknownVal(cty.Set(cty.String)).Refine().CollectionLengthLowerBound(1).CollectionLengthUpperBound(4).NewValue().Mark("test"), // deduced through refinements
2080+
0,
2081+
},
19602082
{
19612083
`unknown ? a : b`,
19622084
&hcl.EvalContext{

0 commit comments

Comments
 (0)