409 lines
13 KiB
Go
409 lines
13 KiB
Go
|
package cty
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"math"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
// Range returns an object that offers partial information about the range
|
||
|
// of the receiver.
|
||
|
//
|
||
|
// This is most relevant for unknown values, because it gives access to any
|
||
|
// optional additional constraints on the final value (specified by the source
|
||
|
// of the value using "refinements") beyond what we can assume from the value's
|
||
|
// type.
|
||
|
//
|
||
|
// Calling Range for a known value is a little strange, but it's supported by
|
||
|
// returning a [ValueRange] object that describes the exact value as closely
|
||
|
// as possible. Typically a caller should work directly with the exact value
|
||
|
// in that case, but some purposes might only need the level of detail
|
||
|
// offered by ranges and so can share code between both known and unknown
|
||
|
// values.
|
||
|
func (v Value) Range() ValueRange {
|
||
|
// For an unknown value we just use its own refinements.
|
||
|
if unk, isUnk := v.v.(*unknownType); isUnk {
|
||
|
refinement := unk.refinement
|
||
|
if refinement == nil {
|
||
|
// We'll generate an unconstrained refinement, just to
|
||
|
// simplify the code in ValueRange methods which can
|
||
|
// therefore assume that there's always a refinement.
|
||
|
refinement = &refinementNullable{isNull: tristateUnknown}
|
||
|
}
|
||
|
return ValueRange{v.Type(), refinement}
|
||
|
}
|
||
|
|
||
|
if v.IsNull() {
|
||
|
// If we know a value is null then we'll just report that,
|
||
|
// since no other refinements make sense for a definitely-null value.
|
||
|
return ValueRange{
|
||
|
v.Type(),
|
||
|
&refinementNullable{isNull: tristateTrue},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// For a known value we construct synthetic refinements that match
|
||
|
// the value, just as a convenience for callers that want to share
|
||
|
// codepaths between both known and unknown values.
|
||
|
ty := v.Type()
|
||
|
var synth unknownValRefinement
|
||
|
switch {
|
||
|
case ty == String:
|
||
|
synth = &refinementString{
|
||
|
prefix: v.AsString(),
|
||
|
}
|
||
|
case ty == Number:
|
||
|
synth = &refinementNumber{
|
||
|
min: v,
|
||
|
max: v,
|
||
|
minInc: true,
|
||
|
maxInc: true,
|
||
|
}
|
||
|
case ty.IsCollectionType():
|
||
|
if lenVal := v.Length(); lenVal.IsKnown() {
|
||
|
l, _ := lenVal.AsBigFloat().Int64()
|
||
|
synth = &refinementCollection{
|
||
|
minLen: int(l),
|
||
|
maxLen: int(l),
|
||
|
}
|
||
|
} else {
|
||
|
synth = &refinementCollection{
|
||
|
minLen: 0,
|
||
|
maxLen: math.MaxInt,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
default:
|
||
|
// If we don't have anything else to say then we can at least
|
||
|
// guarantee that the value isn't null.
|
||
|
synth = &refinementNullable{}
|
||
|
}
|
||
|
|
||
|
// If we get down here then the value is definitely not null
|
||
|
synth.setNull(tristateFalse)
|
||
|
|
||
|
return ValueRange{ty, synth}
|
||
|
}
|
||
|
|
||
|
// ValueRange offers partial information about the range of a value.
|
||
|
//
|
||
|
// This is primarily interesting for unknown values, because it provides access
|
||
|
// to any additional known constraints (specified using "refinements") on the
|
||
|
// range of the value beyond what is represented by the value's type.
|
||
|
type ValueRange struct {
|
||
|
ty Type
|
||
|
raw unknownValRefinement
|
||
|
}
|
||
|
|
||
|
// TypeConstraint returns a type constraint describing the value's type as
|
||
|
// precisely as possible with the available information.
|
||
|
func (r ValueRange) TypeConstraint() Type {
|
||
|
return r.ty
|
||
|
}
|
||
|
|
||
|
// CouldBeNull returns true unless the value being described is definitely
|
||
|
// known to represent a non-null value.
|
||
|
func (r ValueRange) CouldBeNull() bool {
|
||
|
if r.raw == nil {
|
||
|
// A totally-unconstrained unknown value could be null
|
||
|
return true
|
||
|
}
|
||
|
return r.raw.null() != tristateFalse
|
||
|
}
|
||
|
|
||
|
// DefinitelyNotNull returns true if there are no null values in the range.
|
||
|
func (r ValueRange) DefinitelyNotNull() bool {
|
||
|
if r.raw == nil {
|
||
|
// A totally-unconstrained unknown value could be null
|
||
|
return false
|
||
|
}
|
||
|
return r.raw.null() == tristateFalse
|
||
|
}
|
||
|
|
||
|
// NumberLowerBound returns information about the lower bound of the range of
|
||
|
// a number value, or panics if the value is definitely not a number.
|
||
|
//
|
||
|
// If the value is nullable then the result represents the range of the number
|
||
|
// only if it turns out not to be null.
|
||
|
//
|
||
|
// The resulting value might itself be an unknown number if there is no
|
||
|
// known lower bound. In that case the "inclusive" flag is meaningless.
|
||
|
func (r ValueRange) NumberLowerBound() (min Value, inclusive bool) {
|
||
|
if r.ty == DynamicPseudoType {
|
||
|
// We don't even know if this is a number yet.
|
||
|
return UnknownVal(Number), false
|
||
|
}
|
||
|
if r.ty != Number {
|
||
|
panic(fmt.Sprintf("NumberLowerBound for %#v", r.ty))
|
||
|
}
|
||
|
if rfn, ok := r.raw.(*refinementNumber); ok && rfn.min != NilVal {
|
||
|
if !rfn.min.IsKnown() {
|
||
|
return NegativeInfinity, true
|
||
|
}
|
||
|
return rfn.min, rfn.minInc
|
||
|
}
|
||
|
return NegativeInfinity, false
|
||
|
}
|
||
|
|
||
|
// NumberUpperBound returns information about the upper bound of the range of
|
||
|
// a number value, or panics if the value is definitely not a number.
|
||
|
//
|
||
|
// If the value is nullable then the result represents the range of the number
|
||
|
// only if it turns out not to be null.
|
||
|
//
|
||
|
// The resulting value might itself be an unknown number if there is no
|
||
|
// known upper bound. In that case the "inclusive" flag is meaningless.
|
||
|
func (r ValueRange) NumberUpperBound() (max Value, inclusive bool) {
|
||
|
if r.ty == DynamicPseudoType {
|
||
|
// We don't even know if this is a number yet.
|
||
|
return UnknownVal(Number), false
|
||
|
}
|
||
|
if r.ty != Number {
|
||
|
panic(fmt.Sprintf("NumberUpperBound for %#v", r.ty))
|
||
|
}
|
||
|
if rfn, ok := r.raw.(*refinementNumber); ok && rfn.max != NilVal {
|
||
|
if !rfn.max.IsKnown() {
|
||
|
return PositiveInfinity, true
|
||
|
}
|
||
|
return rfn.max, rfn.maxInc
|
||
|
}
|
||
|
return PositiveInfinity, false
|
||
|
}
|
||
|
|
||
|
// StringPrefix returns a string that is guaranteed to be the prefix of
|
||
|
// the string value being described, or panics if the value is definitely not
|
||
|
// a string.
|
||
|
//
|
||
|
// If the value is nullable then the result represents the prefix of the string
|
||
|
// only if it turns out to not be null.
|
||
|
//
|
||
|
// If the resulting value is zero-length then the value could potentially be
|
||
|
// a string but it has no known prefix.
|
||
|
//
|
||
|
// cty.String values always contain normalized UTF-8 sequences; the result is
|
||
|
// also guaranteed to be a normalized UTF-8 sequence so the result also
|
||
|
// represents the exact bytes of the string value's prefix.
|
||
|
func (r ValueRange) StringPrefix() string {
|
||
|
if r.ty == DynamicPseudoType {
|
||
|
// We don't even know if this is a string yet.
|
||
|
return ""
|
||
|
}
|
||
|
if r.ty != String {
|
||
|
panic(fmt.Sprintf("StringPrefix for %#v", r.ty))
|
||
|
}
|
||
|
if rfn, ok := r.raw.(*refinementString); ok {
|
||
|
return rfn.prefix
|
||
|
}
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
// LengthLowerBound returns information about the lower bound of the length of
|
||
|
// a collection-typed value, or panics if the value is definitely not a
|
||
|
// collection.
|
||
|
//
|
||
|
// If the value is nullable then the result represents the range of the length
|
||
|
// only if the value turns out not to be null.
|
||
|
func (r ValueRange) LengthLowerBound() int {
|
||
|
if r.ty == DynamicPseudoType {
|
||
|
// We don't even know if this is a collection yet.
|
||
|
return 0
|
||
|
}
|
||
|
if !r.ty.IsCollectionType() {
|
||
|
panic(fmt.Sprintf("LengthLowerBound for %#v", r.ty))
|
||
|
}
|
||
|
if rfn, ok := r.raw.(*refinementCollection); ok {
|
||
|
return rfn.minLen
|
||
|
}
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
// LengthUpperBound returns information about the upper bound of the length of
|
||
|
// a collection-typed value, or panics if the value is definitely not a
|
||
|
// collection.
|
||
|
//
|
||
|
// If the value is nullable then the result represents the range of the length
|
||
|
// only if the value turns out not to be null.
|
||
|
//
|
||
|
// The resulting value might itself be an unknown number if there is no
|
||
|
// known upper bound. In that case the "inclusive" flag is meaningless.
|
||
|
func (r ValueRange) LengthUpperBound() int {
|
||
|
if r.ty == DynamicPseudoType {
|
||
|
// We don't even know if this is a collection yet.
|
||
|
return math.MaxInt
|
||
|
}
|
||
|
if !r.ty.IsCollectionType() {
|
||
|
panic(fmt.Sprintf("LengthUpperBound for %#v", r.ty))
|
||
|
}
|
||
|
if rfn, ok := r.raw.(*refinementCollection); ok {
|
||
|
return rfn.maxLen
|
||
|
}
|
||
|
return math.MaxInt
|
||
|
}
|
||
|
|
||
|
// Includes determines whether the given value is in the receiving range.
|
||
|
//
|
||
|
// It can return only three possible values:
|
||
|
// - [cty.True] if the range definitely includes the value
|
||
|
// - [cty.False] if the range definitely does not include the value
|
||
|
// - An unknown value of [cty.Bool] if there isn't enough information to decide.
|
||
|
//
|
||
|
// This function is not fully comprehensive: it may return an unknown value
|
||
|
// in some cases where a definitive value could be computed in principle, and
|
||
|
// those same situations may begin returning known values in later releases as
|
||
|
// the rules are refined to be more complete. Currently the rules focus mainly
|
||
|
// on answering [cty.False], because disproving membership tends to be more
|
||
|
// useful than proving membership.
|
||
|
func (r ValueRange) Includes(v Value) Value {
|
||
|
unknownResult := UnknownVal(Bool).RefineNotNull()
|
||
|
|
||
|
if r.raw.null() == tristateTrue {
|
||
|
if v.IsNull() {
|
||
|
return True
|
||
|
} else {
|
||
|
return False
|
||
|
}
|
||
|
}
|
||
|
if r.raw.null() == tristateFalse {
|
||
|
if v.IsNull() {
|
||
|
return False
|
||
|
}
|
||
|
// A definitely-not-null value could potentially match
|
||
|
// but we won't know until we do some more checks below.
|
||
|
}
|
||
|
// If our range includes both null and non-null values and the value is
|
||
|
// null then it's definitely in range.
|
||
|
if v.IsNull() {
|
||
|
return True
|
||
|
}
|
||
|
if len(v.Type().TestConformance(r.TypeConstraint())) != 0 {
|
||
|
// If the value doesn't conform to the type constraint then it's
|
||
|
// definitely not in the range.
|
||
|
return False
|
||
|
}
|
||
|
if v.Type() == DynamicPseudoType {
|
||
|
// If it's an unknown value of an unknown type then there's no
|
||
|
// further tests we can make.
|
||
|
return unknownResult
|
||
|
}
|
||
|
|
||
|
switch r.raw.(type) {
|
||
|
case *refinementString:
|
||
|
if v.IsKnown() {
|
||
|
prefix := r.StringPrefix()
|
||
|
got := v.AsString()
|
||
|
|
||
|
if !strings.HasPrefix(got, prefix) {
|
||
|
return False
|
||
|
}
|
||
|
}
|
||
|
case *refinementCollection:
|
||
|
lenVal := v.Length()
|
||
|
minLen := NumberIntVal(int64(r.LengthLowerBound()))
|
||
|
maxLen := NumberIntVal(int64(r.LengthUpperBound()))
|
||
|
if minOk := lenVal.GreaterThanOrEqualTo(minLen); minOk.IsKnown() && minOk.False() {
|
||
|
return False
|
||
|
}
|
||
|
if maxOk := lenVal.LessThanOrEqualTo(maxLen); maxOk.IsKnown() && maxOk.False() {
|
||
|
return False
|
||
|
}
|
||
|
case *refinementNumber:
|
||
|
minVal, minInc := r.NumberLowerBound()
|
||
|
maxVal, maxInc := r.NumberUpperBound()
|
||
|
var minOk, maxOk Value
|
||
|
if minInc {
|
||
|
minOk = v.GreaterThanOrEqualTo(minVal)
|
||
|
} else {
|
||
|
minOk = v.GreaterThan(minVal)
|
||
|
}
|
||
|
if maxInc {
|
||
|
maxOk = v.LessThanOrEqualTo(maxVal)
|
||
|
} else {
|
||
|
maxOk = v.LessThan(maxVal)
|
||
|
}
|
||
|
if minOk.IsKnown() && minOk.False() {
|
||
|
return False
|
||
|
}
|
||
|
if maxOk.IsKnown() && maxOk.False() {
|
||
|
return False
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If we fall out here then we don't have enough information to decide.
|
||
|
return unknownResult
|
||
|
}
|
||
|
|
||
|
// numericRangeArithmetic is a helper we use to calculate derived numeric ranges
|
||
|
// for arithmetic on refined numeric values.
|
||
|
//
|
||
|
// op must be a monotone operation. numericRangeArithmetic adapts that operation
|
||
|
// into the equivalent interval arithmetic operation.
|
||
|
//
|
||
|
// The result is a superset of the range of the given operation against the
|
||
|
// given input ranges, if it's possible to calculate that without encountering
|
||
|
// an invalid operation. Currently the result is inexact due to ignoring
|
||
|
// the inclusiveness of the input bounds and just always returning inclusive
|
||
|
// bounds.
|
||
|
func numericRangeArithmetic(op func(a, b Value) Value, a, b ValueRange) func(*RefinementBuilder) *RefinementBuilder {
|
||
|
wrapOp := func(a, b Value) (ret Value) {
|
||
|
// Our functions have various panicking edge cases involving incompatible
|
||
|
// uses of infinities. To keep things simple here we'll catch those
|
||
|
// and just return an unconstrained number.
|
||
|
defer func() {
|
||
|
if v := recover(); v != nil {
|
||
|
ret = UnknownVal(Number)
|
||
|
}
|
||
|
}()
|
||
|
return op(a, b)
|
||
|
}
|
||
|
|
||
|
return func(builder *RefinementBuilder) *RefinementBuilder {
|
||
|
aMin, _ := a.NumberLowerBound()
|
||
|
aMax, _ := a.NumberUpperBound()
|
||
|
bMin, _ := b.NumberLowerBound()
|
||
|
bMax, _ := b.NumberUpperBound()
|
||
|
|
||
|
v1 := wrapOp(aMin, bMin)
|
||
|
v2 := wrapOp(aMin, bMax)
|
||
|
v3 := wrapOp(aMax, bMin)
|
||
|
v4 := wrapOp(aMax, bMax)
|
||
|
|
||
|
newMin := mostNumberValue(Value.LessThan, v1, v2, v3, v4)
|
||
|
newMax := mostNumberValue(Value.GreaterThan, v1, v2, v3, v4)
|
||
|
|
||
|
if isInf := newMin.Equals(NegativeInfinity); isInf.IsKnown() && isInf.False() {
|
||
|
builder = builder.NumberRangeLowerBound(newMin, true)
|
||
|
}
|
||
|
if isInf := newMax.Equals(PositiveInfinity); isInf.IsKnown() && isInf.False() {
|
||
|
builder = builder.NumberRangeUpperBound(newMax, true)
|
||
|
}
|
||
|
return builder
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func mostNumberValue(op func(i, j Value) Value, v1 Value, vN ...Value) Value {
|
||
|
r := v1
|
||
|
for _, v := range vN {
|
||
|
more := op(v, r)
|
||
|
if !more.IsKnown() {
|
||
|
return UnknownVal(Number)
|
||
|
}
|
||
|
if more.True() {
|
||
|
r = v
|
||
|
}
|
||
|
}
|
||
|
return r
|
||
|
}
|
||
|
|
||
|
// definitelyNotNull is a convenient helper for the common situation of checking
|
||
|
// whether a value could possibly be null.
|
||
|
//
|
||
|
// Returns true if the given value is either a known value that isn't null
|
||
|
// or an unknown value that has been refined to exclude null values from its
|
||
|
// range.
|
||
|
func definitelyNotNull(v Value) bool {
|
||
|
if v.IsKnown() {
|
||
|
return !v.IsNull()
|
||
|
}
|
||
|
return v.Range().DefinitelyNotNull()
|
||
|
}
|