748 lines
23 KiB
Go
748 lines
23 KiB
Go
|
package cty
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"math"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/zclconf/go-cty/cty/ctystrings"
|
||
|
)
|
||
|
|
||
|
// Refine creates a [RefinementBuilder] with which to annotate the reciever
|
||
|
// with zero or more additional refinements that constrain the range of
|
||
|
// the value.
|
||
|
//
|
||
|
// Calling methods on a RefinementBuilder for a known value essentially just
|
||
|
// serves as assertions about the range of that value, leading to panics if
|
||
|
// those assertions don't hold in practice. This is mainly supported just to
|
||
|
// make programs that rely on refinements automatically self-check by using
|
||
|
// the refinement codepath unconditionally on both placeholders and final
|
||
|
// values for those placeholders. It's always a bug to refine the range of
|
||
|
// an unknown value and then later substitute an exact value outside of the
|
||
|
// refined range.
|
||
|
//
|
||
|
// Calling methods on a RefinementBuilder for an unknown value is perhaps
|
||
|
// more useful because the newly-refined value will then be a placeholder for
|
||
|
// a smaller range of values and so it may be possible for other operations
|
||
|
// on the unknown value to return a known result despite the exact value not
|
||
|
// yet being known.
|
||
|
//
|
||
|
// It is never valid to refine [DynamicVal], because that value is a
|
||
|
// placeholder for a value about which we knkow absolutely nothing. A value
|
||
|
// must at least have a known root type before it can support further
|
||
|
// refinement.
|
||
|
func (v Value) Refine() *RefinementBuilder {
|
||
|
v, marks := v.Unmark()
|
||
|
if unk, isUnk := v.v.(*unknownType); isUnk && unk.refinement != nil {
|
||
|
// We're refining a value that's already been refined before, so
|
||
|
// we'll start from a copy of its existing refinements.
|
||
|
wip := unk.refinement.copy()
|
||
|
return &RefinementBuilder{v, marks, wip}
|
||
|
}
|
||
|
|
||
|
ty := v.Type()
|
||
|
var wip unknownValRefinement
|
||
|
switch {
|
||
|
case ty == DynamicPseudoType && !v.IsKnown():
|
||
|
panic("cannot refine an unknown value of an unknown type")
|
||
|
case ty == String:
|
||
|
wip = &refinementString{}
|
||
|
case ty == Number:
|
||
|
wip = &refinementNumber{}
|
||
|
case ty.IsCollectionType():
|
||
|
wip = &refinementCollection{
|
||
|
// A collection can never have a negative length, so we'll
|
||
|
// start with that already constrained.
|
||
|
minLen: 0,
|
||
|
maxLen: math.MaxInt,
|
||
|
}
|
||
|
case ty == Bool || ty.IsObjectType() || ty.IsTupleType() || ty.IsCapsuleType():
|
||
|
// For other known types we'll just track nullability
|
||
|
wip = &refinementNullable{}
|
||
|
case ty == DynamicPseudoType && v.IsNull():
|
||
|
// It's okay in principle to refine a null value of unknown type,
|
||
|
// although all we can refine about it is that it's definitely null and
|
||
|
// so this is pretty pointless and only supported to avoid callers
|
||
|
// always needing to treat this situation as a special case to avoid
|
||
|
// panic.
|
||
|
wip = &refinementNullable{
|
||
|
isNull: tristateTrue,
|
||
|
}
|
||
|
default:
|
||
|
// we leave "wip" as nil for all other types, representing that
|
||
|
// they don't support refinements at all and so any call on the
|
||
|
// RefinementBuilder should fail.
|
||
|
|
||
|
// NOTE: We intentionally don't allow any refinements for
|
||
|
// cty.DynamicVal here, even though it could be nice in principle
|
||
|
// to at least track non-nullness for those, because it's historically
|
||
|
// been valid to directly compare values with cty.DynamicVal using
|
||
|
// the Go "==" operator and recording a refinement for an untyped
|
||
|
// unknown value would break existing code relying on that.
|
||
|
}
|
||
|
|
||
|
return &RefinementBuilder{v, marks, wip}
|
||
|
}
|
||
|
|
||
|
// RefineWith is a variant of Refine which uses callback functions instead of
|
||
|
// the builder pattern.
|
||
|
//
|
||
|
// The result is equivalent to passing the return value of [Value.Refine] to the
|
||
|
// first callback, and then continue passing the builder through any other
|
||
|
// callbacks in turn, and then calling [RefinementBuilder.NewValue] on the
|
||
|
// final result.
|
||
|
//
|
||
|
// The builder pattern approach of [Value.Refine] is more convenient for inline
|
||
|
// annotation of refinements when constructing a value, but this alternative
|
||
|
// approach may be more convenient when applying pre-defined collections of
|
||
|
// refinements, or when refinements are defined separately from the values
|
||
|
// they will apply to.
|
||
|
//
|
||
|
// Each refiner callback should return the same pointer that it was given,
|
||
|
// typically after having mutated it using the [RefinementBuilder] methods.
|
||
|
// It's invalid to return a different builder.
|
||
|
func (v Value) RefineWith(refiners ...func(*RefinementBuilder) *RefinementBuilder) Value {
|
||
|
if len(refiners) == 0 {
|
||
|
return v
|
||
|
}
|
||
|
origBuilder := v.Refine()
|
||
|
builder := origBuilder
|
||
|
for _, refiner := range refiners {
|
||
|
builder = refiner(builder)
|
||
|
if builder != origBuilder {
|
||
|
panic("refiner callback returned a different builder")
|
||
|
}
|
||
|
}
|
||
|
return builder.NewValue()
|
||
|
}
|
||
|
|
||
|
// RefineNotNull is a shorthand for Value.Refine().NotNull().NewValue(), because
|
||
|
// declaring that a unknown value isn't null is by far the most common use of
|
||
|
// refinements.
|
||
|
func (v Value) RefineNotNull() Value {
|
||
|
return v.Refine().NotNull().NewValue()
|
||
|
}
|
||
|
|
||
|
// RefinementBuilder is a supporting type for the [Value.Refine] method,
|
||
|
// using the builder pattern to apply zero or more constraints before
|
||
|
// constructing a new value with all of those constraints applied.
|
||
|
//
|
||
|
// Most of the methods of this type return the same reciever to allow
|
||
|
// for method call chaining. End call chains with a call to
|
||
|
// [RefinementBuilder.NewValue] to obtain the newly-refined value.
|
||
|
type RefinementBuilder struct {
|
||
|
orig Value
|
||
|
marks ValueMarks
|
||
|
wip unknownValRefinement
|
||
|
}
|
||
|
|
||
|
func (b *RefinementBuilder) assertRefineable() {
|
||
|
if b.wip == nil {
|
||
|
panic(fmt.Sprintf("cannot refine a %#v value", b.orig.Type()))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NotNull constrains the value as definitely not being null.
|
||
|
//
|
||
|
// NotNull is valid when refining values of the following types:
|
||
|
// - number, boolean, and string values
|
||
|
// - list, set, or map types of any element type
|
||
|
// - values of object types
|
||
|
// - values of collection types
|
||
|
// - values of capsule types
|
||
|
//
|
||
|
// When refining any other type this function will panic.
|
||
|
//
|
||
|
// In particular note that it is not valid to constrain an untyped value
|
||
|
// -- a value whose type is `cty.DynamicPseudoType` -- as being non-null.
|
||
|
// An unknown value of an unknown type is always completely unconstrained.
|
||
|
func (b *RefinementBuilder) NotNull() *RefinementBuilder {
|
||
|
b.assertRefineable()
|
||
|
|
||
|
if b.orig.IsKnown() && b.orig.IsNull() {
|
||
|
panic("refining null value as non-null")
|
||
|
}
|
||
|
if b.wip.null() == tristateTrue {
|
||
|
panic("refining null value as non-null")
|
||
|
}
|
||
|
|
||
|
b.wip.setNull(tristateFalse)
|
||
|
|
||
|
return b
|
||
|
}
|
||
|
|
||
|
// Null constrains the value as definitely null.
|
||
|
//
|
||
|
// Null is valid for the same types as [RefinementBuilder.NotNull].
|
||
|
// When refining any other type this function will panic.
|
||
|
//
|
||
|
// Explicitly cnstraining a value to be null is strange because that suggests
|
||
|
// that the caller does actually know the value -- there is only one null
|
||
|
// value for each type constraint -- but this is here for symmetry with the
|
||
|
// fact that a [ValueRange] can also represent that a value is definitely null.
|
||
|
func (b *RefinementBuilder) Null() *RefinementBuilder {
|
||
|
b.assertRefineable()
|
||
|
|
||
|
if b.orig.IsKnown() && !b.orig.IsNull() {
|
||
|
panic("refining non-null value as null")
|
||
|
}
|
||
|
if b.wip.null() == tristateFalse {
|
||
|
panic("refining non-null value as null")
|
||
|
}
|
||
|
|
||
|
b.wip.setNull(tristateTrue)
|
||
|
|
||
|
return b
|
||
|
}
|
||
|
|
||
|
// NumericRange constrains the upper and/or lower bounds of a number value,
|
||
|
// or panics if this builder is not refining a number value.
|
||
|
//
|
||
|
// The two given values are interpreted as inclusive bounds and either one
|
||
|
// may be an unknown number if only one of the two bounds is currently known.
|
||
|
// If either of the given values is not a non-null number value then this
|
||
|
// function will panic.
|
||
|
func (b *RefinementBuilder) NumberRangeInclusive(min, max Value) *RefinementBuilder {
|
||
|
return b.NumberRangeLowerBound(min, true).NumberRangeUpperBound(max, true)
|
||
|
}
|
||
|
|
||
|
// NumberRangeLowerBound constraints the lower bound of a number value, or
|
||
|
// panics if this builder is not refining a number value.
|
||
|
func (b *RefinementBuilder) NumberRangeLowerBound(min Value, inclusive bool) *RefinementBuilder {
|
||
|
b.assertRefineable()
|
||
|
|
||
|
wip, ok := b.wip.(*refinementNumber)
|
||
|
if !ok {
|
||
|
panic(fmt.Sprintf("cannot refine numeric bounds for a %#v value", b.orig.Type()))
|
||
|
}
|
||
|
|
||
|
if !min.IsKnown() {
|
||
|
// Nothing to do if the lower bound is unknown.
|
||
|
return b
|
||
|
}
|
||
|
if min.IsNull() {
|
||
|
panic("number range lower bound must not be null")
|
||
|
}
|
||
|
|
||
|
if inclusive {
|
||
|
if gt := min.GreaterThan(b.orig); gt.IsKnown() && gt.True() {
|
||
|
panic(fmt.Sprintf("refining %#v to be >= %#v", b.orig, min))
|
||
|
}
|
||
|
} else {
|
||
|
if gt := min.GreaterThanOrEqualTo(b.orig); gt.IsKnown() && gt.True() {
|
||
|
panic(fmt.Sprintf("refining %#v to be > %#v", b.orig, min))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if wip.min != NilVal {
|
||
|
var ok Value
|
||
|
if inclusive && !wip.minInc {
|
||
|
ok = min.GreaterThan(wip.min)
|
||
|
} else {
|
||
|
ok = min.GreaterThanOrEqualTo(wip.min)
|
||
|
}
|
||
|
if ok.IsKnown() && ok.False() {
|
||
|
return b // Our existing refinement is more constrained
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if min != NegativeInfinity {
|
||
|
wip.min = min
|
||
|
wip.minInc = inclusive
|
||
|
}
|
||
|
|
||
|
wip.assertConsistentBounds()
|
||
|
return b
|
||
|
}
|
||
|
|
||
|
// NumberRangeUpperBound constraints the upper bound of a number value, or
|
||
|
// panics if this builder is not refining a number value.
|
||
|
func (b *RefinementBuilder) NumberRangeUpperBound(max Value, inclusive bool) *RefinementBuilder {
|
||
|
b.assertRefineable()
|
||
|
|
||
|
wip, ok := b.wip.(*refinementNumber)
|
||
|
if !ok {
|
||
|
panic(fmt.Sprintf("cannot refine numeric bounds for a %#v value", b.orig.Type()))
|
||
|
}
|
||
|
|
||
|
if !max.IsKnown() {
|
||
|
// Nothing to do if the upper bound is unknown.
|
||
|
return b
|
||
|
}
|
||
|
if max.IsNull() {
|
||
|
panic("number range upper bound must not be null")
|
||
|
}
|
||
|
|
||
|
if inclusive {
|
||
|
if lt := max.LessThan(b.orig); lt.IsKnown() && lt.True() {
|
||
|
panic(fmt.Sprintf("refining %#v to be <= %#v", b.orig, max))
|
||
|
}
|
||
|
} else {
|
||
|
if lt := max.LessThanOrEqualTo(b.orig); lt.IsKnown() && lt.True() {
|
||
|
panic(fmt.Sprintf("refining %#v to be < %#v", b.orig, max))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if wip.max != NilVal {
|
||
|
var ok Value
|
||
|
if inclusive && !wip.maxInc {
|
||
|
ok = max.LessThan(wip.max)
|
||
|
} else {
|
||
|
ok = max.LessThanOrEqualTo(wip.max)
|
||
|
}
|
||
|
if ok.IsKnown() && ok.False() {
|
||
|
return b // Our existing refinement is more constrained
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if max != PositiveInfinity {
|
||
|
wip.max = max
|
||
|
wip.maxInc = inclusive
|
||
|
}
|
||
|
|
||
|
wip.assertConsistentBounds()
|
||
|
return b
|
||
|
}
|
||
|
|
||
|
// CollectionLengthLowerBound constrains the lower bound of the length of a
|
||
|
// collection value, or panics if this builder is not refining a collection
|
||
|
// value.
|
||
|
func (b *RefinementBuilder) CollectionLengthLowerBound(min int) *RefinementBuilder {
|
||
|
b.assertRefineable()
|
||
|
|
||
|
wip, ok := b.wip.(*refinementCollection)
|
||
|
if !ok {
|
||
|
panic(fmt.Sprintf("cannot refine collection length bounds for a %#v value", b.orig.Type()))
|
||
|
}
|
||
|
|
||
|
minVal := NumberIntVal(int64(min))
|
||
|
if b.orig.IsKnown() {
|
||
|
realLen := b.orig.Length()
|
||
|
if gt := minVal.GreaterThan(realLen); gt.IsKnown() && gt.True() {
|
||
|
panic(fmt.Sprintf("refining collection of length %#v with lower bound %#v", realLen, min))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if wip.minLen > min {
|
||
|
return b // Our existing refinement is more constrained
|
||
|
}
|
||
|
|
||
|
wip.minLen = min
|
||
|
wip.assertConsistentLengthBounds()
|
||
|
|
||
|
return b
|
||
|
}
|
||
|
|
||
|
// CollectionLengthUpperBound constrains the upper bound of the length of a
|
||
|
// collection value, or panics if this builder is not refining a collection
|
||
|
// value.
|
||
|
//
|
||
|
// The upper bound must be a known, non-null number or this function will
|
||
|
// panic.
|
||
|
func (b *RefinementBuilder) CollectionLengthUpperBound(max int) *RefinementBuilder {
|
||
|
b.assertRefineable()
|
||
|
|
||
|
wip, ok := b.wip.(*refinementCollection)
|
||
|
if !ok {
|
||
|
panic(fmt.Sprintf("cannot refine collection length bounds for a %#v value", b.orig.Type()))
|
||
|
}
|
||
|
|
||
|
if b.orig.IsKnown() {
|
||
|
maxVal := NumberIntVal(int64(max))
|
||
|
realLen := b.orig.Length()
|
||
|
if lt := maxVal.LessThan(realLen); lt.IsKnown() && lt.True() {
|
||
|
panic(fmt.Sprintf("refining collection of length %#v with upper bound %#v", realLen, max))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if wip.maxLen < max {
|
||
|
return b // Our existing refinement is more constrained
|
||
|
}
|
||
|
|
||
|
wip.maxLen = max
|
||
|
wip.assertConsistentLengthBounds()
|
||
|
|
||
|
return b
|
||
|
}
|
||
|
|
||
|
// CollectionLength is a shorthand for passing the same length to both
|
||
|
// [CollectionLengthLowerBound] and [CollectionLengthUpperBound].
|
||
|
//
|
||
|
// A collection with a refined length with equal bounds can sometimes collapse
|
||
|
// to a known value. Refining to length zero always produces a known value.
|
||
|
// The behavior for other lengths varies by collection type kind.
|
||
|
//
|
||
|
// If the unknown value is of a set type, it's only valid to use this method
|
||
|
// if the caller knows that there will be the given number of _unique_ values
|
||
|
// in the set. If any values might potentially coalesce together once known,
|
||
|
// use [CollectionLengthUpperBound] instead.
|
||
|
func (b *RefinementBuilder) CollectionLength(length int) *RefinementBuilder {
|
||
|
return b.CollectionLengthLowerBound(length).CollectionLengthUpperBound(length)
|
||
|
}
|
||
|
|
||
|
// StringPrefix constrains the prefix of a string value, or panics if this
|
||
|
// builder is not refining a string value.
|
||
|
//
|
||
|
// The given prefix will be Unicode normalized in the same way that a
|
||
|
// cty.StringVal would be.
|
||
|
//
|
||
|
// Due to Unicode normalization and grapheme cluster rules, appending new
|
||
|
// characters to a string can change the meaning of earlier characters.
|
||
|
// StringPrefix may discard one or more characters from the end of the given
|
||
|
// prefix to avoid that problem.
|
||
|
//
|
||
|
// Although cty cannot check this automatically, applications should avoid
|
||
|
// relying on the discarding of the suffix for correctness. For example, if the
|
||
|
// prefix ends with an emoji base character then StringPrefix will discard it
|
||
|
// in case subsequent characters include emoji modifiers, but it's still
|
||
|
// incorrect for the final string to use an entirely different base character.
|
||
|
//
|
||
|
// Applications which fully control the final result and can guarantee the
|
||
|
// subsequent characters will not combine with the prefix may be able to use
|
||
|
// [RefinementBuilder.StringPrefixFull] instead, after carefully reviewing
|
||
|
// the constraints described in its documentation.
|
||
|
func (b *RefinementBuilder) StringPrefix(prefix string) *RefinementBuilder {
|
||
|
return b.StringPrefixFull(ctystrings.SafeKnownPrefix(prefix))
|
||
|
}
|
||
|
|
||
|
// StringPrefixFull is a variant of StringPrefix that will never shorten the
|
||
|
// given prefix to take into account the possibility of the next character
|
||
|
// combining with the end of the prefix.
|
||
|
//
|
||
|
// Applications which fully control the subsequent characters can use this
|
||
|
// as long as they guarantee that the characters added later cannot possibly
|
||
|
// combine with characters at the end of the prefix to form a single grapheme
|
||
|
// cluster. For example, it would be unsafe to use the full prefix "hello" if
|
||
|
// there is any chance that the final string will add a combining diacritic
|
||
|
// character after the "o", because that would then change the final character.
|
||
|
//
|
||
|
// Use [RefinementBuilder.StringPrefix] instead if an application cannot fully
|
||
|
// control the final result to avoid violating this rule.
|
||
|
func (b *RefinementBuilder) StringPrefixFull(prefix string) *RefinementBuilder {
|
||
|
b.assertRefineable()
|
||
|
|
||
|
wip, ok := b.wip.(*refinementString)
|
||
|
if !ok {
|
||
|
panic(fmt.Sprintf("cannot refine string prefix for a %#v value", b.orig.Type()))
|
||
|
}
|
||
|
|
||
|
// We must apply the same Unicode processing we'd normally use for a
|
||
|
// cty string so that the prefix will be comparable.
|
||
|
prefix = NormalizeString(prefix)
|
||
|
|
||
|
// If we have a known string value then the given prefix must actually
|
||
|
// match it.
|
||
|
if b.orig.IsKnown() && !b.orig.IsNull() {
|
||
|
have := b.orig.AsString()
|
||
|
matchLen := len(have)
|
||
|
if l := len(prefix); l < matchLen {
|
||
|
matchLen = l
|
||
|
}
|
||
|
have = have[:matchLen]
|
||
|
new := prefix[:matchLen]
|
||
|
if have != new {
|
||
|
panic("refined prefix is inconsistent with known value")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If we already have a refined prefix then the overlapping parts of that
|
||
|
// and the new prefix must match.
|
||
|
{
|
||
|
matchLen := len(wip.prefix)
|
||
|
if l := len(prefix); l < matchLen {
|
||
|
matchLen = l
|
||
|
}
|
||
|
|
||
|
have := wip.prefix[:matchLen]
|
||
|
new := prefix[:matchLen]
|
||
|
if have != new {
|
||
|
panic("refined prefix is inconsistent with previous refined prefix")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// We'll only save the new prefix if it's longer than the one we already
|
||
|
// had.
|
||
|
if len(prefix) > len(wip.prefix) {
|
||
|
wip.prefix = prefix
|
||
|
}
|
||
|
|
||
|
return b
|
||
|
}
|
||
|
|
||
|
// NewValue completes the refinement process by constructing a new value
|
||
|
// that is guaranteed to meet all of the previously-specified refinements.
|
||
|
//
|
||
|
// If the original value being refined was known then the result is exactly
|
||
|
// that value, because otherwise the previous refinement calls would have
|
||
|
// panicked reporting the refinements as invalid for the value.
|
||
|
//
|
||
|
// If the original value was unknown then the result is typically also unknown
|
||
|
// but may have additional refinements compared to the original. If the applied
|
||
|
// refinements have reduced the range to a single exact value then the result
|
||
|
// might be that known value.
|
||
|
func (b *RefinementBuilder) NewValue() (ret Value) {
|
||
|
defer func() {
|
||
|
// Regardless of how we return, the new value should have the same
|
||
|
// marks as our original value.
|
||
|
ret = ret.WithMarks(b.marks)
|
||
|
}()
|
||
|
|
||
|
if b.orig.IsKnown() {
|
||
|
return b.orig
|
||
|
}
|
||
|
|
||
|
// We have a few cases where the value has been refined enough that we now
|
||
|
// know exactly what the value is, or at least we can produce a more
|
||
|
// detailed approximation of it.
|
||
|
switch b.wip.null() {
|
||
|
case tristateTrue:
|
||
|
// There is only one null value of each type so this is now known.
|
||
|
return NullVal(b.orig.Type())
|
||
|
case tristateFalse:
|
||
|
// If we know it's definitely not null then we might have enough
|
||
|
// information to construct a known, non-null value.
|
||
|
if rfn, ok := b.wip.(*refinementNumber); ok {
|
||
|
// If both bounds are inclusive and equal then our value can
|
||
|
// only be the same number as the bounds.
|
||
|
if rfn.maxInc && rfn.minInc {
|
||
|
if rfn.min != NilVal && rfn.max != NilVal {
|
||
|
eq := rfn.min.Equals(rfn.max)
|
||
|
if eq.IsKnown() && eq.True() {
|
||
|
return rfn.min
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} else if rfn, ok := b.wip.(*refinementCollection); ok {
|
||
|
// If both of the bounds are equal then we know the length is
|
||
|
// the same number as the bounds.
|
||
|
if rfn.minLen == rfn.maxLen {
|
||
|
knownLen := rfn.minLen
|
||
|
ty := b.orig.Type()
|
||
|
if knownLen == 0 {
|
||
|
// If we know the length is zero then we can construct
|
||
|
// a known value of any collection kind.
|
||
|
switch {
|
||
|
case ty.IsListType():
|
||
|
return ListValEmpty(ty.ElementType())
|
||
|
case ty.IsSetType():
|
||
|
return SetValEmpty(ty.ElementType())
|
||
|
case ty.IsMapType():
|
||
|
return MapValEmpty(ty.ElementType())
|
||
|
}
|
||
|
} else if ty.IsListType() {
|
||
|
// If we know the length of the list then we can
|
||
|
// create a known list with unknown elements instead
|
||
|
// of a wholly-unknown list.
|
||
|
elems := make([]Value, knownLen)
|
||
|
unk := UnknownVal(ty.ElementType())
|
||
|
for i := range elems {
|
||
|
elems[i] = unk
|
||
|
}
|
||
|
return ListVal(elems)
|
||
|
} else if ty.IsSetType() && knownLen == 1 {
|
||
|
// If we know we have a one-element set then we
|
||
|
// know the one element can't possibly coalesce with
|
||
|
// anything else and so we can create a known set with
|
||
|
// an unknown element.
|
||
|
return SetVal([]Value{UnknownVal(ty.ElementType())})
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return Value{
|
||
|
ty: b.orig.ty,
|
||
|
v: &unknownType{refinement: b.wip},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// unknownValRefinment is an interface pretending to be a sum type representing
|
||
|
// the different kinds of unknown value refinements we support for different
|
||
|
// types of value.
|
||
|
type unknownValRefinement interface {
|
||
|
unknownValRefinementSigil()
|
||
|
copy() unknownValRefinement
|
||
|
null() tristateBool
|
||
|
setNull(tristateBool)
|
||
|
rawEqual(other unknownValRefinement) bool
|
||
|
GoString() string
|
||
|
}
|
||
|
|
||
|
type refinementString struct {
|
||
|
refinementNullable
|
||
|
prefix string
|
||
|
}
|
||
|
|
||
|
func (r *refinementString) unknownValRefinementSigil() {}
|
||
|
|
||
|
func (r *refinementString) copy() unknownValRefinement {
|
||
|
ret := *r
|
||
|
// Everything in refinementString is immutable, so a shallow copy is sufficient.
|
||
|
return &ret
|
||
|
}
|
||
|
|
||
|
func (r *refinementString) rawEqual(other unknownValRefinement) bool {
|
||
|
{
|
||
|
other, ok := other.(*refinementString)
|
||
|
if !ok {
|
||
|
return false
|
||
|
}
|
||
|
return (r.refinementNullable.rawEqual(&other.refinementNullable) &&
|
||
|
r.prefix == other.prefix)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (r *refinementString) GoString() string {
|
||
|
var b strings.Builder
|
||
|
b.WriteString(r.refinementNullable.GoString())
|
||
|
if r.prefix != "" {
|
||
|
fmt.Fprintf(&b, ".StringPrefixFull(%q)", r.prefix)
|
||
|
}
|
||
|
return b.String()
|
||
|
}
|
||
|
|
||
|
type refinementNumber struct {
|
||
|
refinementNullable
|
||
|
min, max Value
|
||
|
minInc, maxInc bool
|
||
|
}
|
||
|
|
||
|
func (r *refinementNumber) unknownValRefinementSigil() {}
|
||
|
|
||
|
func (r *refinementNumber) copy() unknownValRefinement {
|
||
|
ret := *r
|
||
|
// Everything in refinementNumber is immutable, so a shallow copy is sufficient.
|
||
|
return &ret
|
||
|
}
|
||
|
|
||
|
func (r *refinementNumber) rawEqual(other unknownValRefinement) bool {
|
||
|
{
|
||
|
other, ok := other.(*refinementNumber)
|
||
|
if !ok {
|
||
|
return false
|
||
|
}
|
||
|
return (r.refinementNullable.rawEqual(&other.refinementNullable) &&
|
||
|
r.min.RawEquals(other.min) &&
|
||
|
r.max.RawEquals(other.max) &&
|
||
|
r.minInc == other.minInc &&
|
||
|
r.maxInc == other.maxInc)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (r *refinementNumber) GoString() string {
|
||
|
var b strings.Builder
|
||
|
b.WriteString(r.refinementNullable.GoString())
|
||
|
if r.min != NilVal && r.min != NegativeInfinity {
|
||
|
fmt.Fprintf(&b, ".NumberLowerBound(%#v, %t)", r.min, r.minInc)
|
||
|
}
|
||
|
if r.max != NilVal && r.max != PositiveInfinity {
|
||
|
fmt.Fprintf(&b, ".NumberUpperBound(%#v, %t)", r.max, r.maxInc)
|
||
|
}
|
||
|
return b.String()
|
||
|
}
|
||
|
|
||
|
func (r *refinementNumber) assertConsistentBounds() {
|
||
|
if r.min == NilVal || r.max == NilVal {
|
||
|
return // If only one bound is constrained then there's nothing to be inconsistent with
|
||
|
}
|
||
|
var ok Value
|
||
|
if r.minInc != r.maxInc {
|
||
|
ok = r.min.LessThan(r.max)
|
||
|
} else {
|
||
|
ok = r.min.LessThanOrEqualTo(r.max)
|
||
|
}
|
||
|
if ok.IsKnown() && ok.False() {
|
||
|
panic(fmt.Sprintf("number lower bound %#v is greater than upper bound %#v", r.min, r.max))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type refinementCollection struct {
|
||
|
refinementNullable
|
||
|
minLen, maxLen int
|
||
|
}
|
||
|
|
||
|
func (r *refinementCollection) unknownValRefinementSigil() {}
|
||
|
|
||
|
func (r *refinementCollection) copy() unknownValRefinement {
|
||
|
ret := *r
|
||
|
// Everything in refinementCollection is immutable, so a shallow copy is sufficient.
|
||
|
return &ret
|
||
|
}
|
||
|
|
||
|
func (r *refinementCollection) rawEqual(other unknownValRefinement) bool {
|
||
|
{
|
||
|
other, ok := other.(*refinementCollection)
|
||
|
if !ok {
|
||
|
return false
|
||
|
}
|
||
|
return (r.refinementNullable.rawEqual(&other.refinementNullable) &&
|
||
|
r.minLen == other.minLen &&
|
||
|
r.maxLen == other.maxLen)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (r *refinementCollection) GoString() string {
|
||
|
var b strings.Builder
|
||
|
b.WriteString(r.refinementNullable.GoString())
|
||
|
if r.minLen != 0 {
|
||
|
fmt.Fprintf(&b, ".CollectionLengthLowerBound(%d)", r.minLen)
|
||
|
}
|
||
|
if r.maxLen != math.MaxInt {
|
||
|
fmt.Fprintf(&b, ".CollectionLengthUpperBound(%d)", r.maxLen)
|
||
|
}
|
||
|
return b.String()
|
||
|
}
|
||
|
|
||
|
func (r *refinementCollection) assertConsistentLengthBounds() {
|
||
|
if r.maxLen < r.minLen {
|
||
|
panic(fmt.Sprintf("collection length upper bound %d is less than lower bound %d", r.maxLen, r.minLen))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type refinementNullable struct {
|
||
|
isNull tristateBool
|
||
|
}
|
||
|
|
||
|
func (r *refinementNullable) unknownValRefinementSigil() {}
|
||
|
|
||
|
func (r *refinementNullable) copy() unknownValRefinement {
|
||
|
ret := *r
|
||
|
// Everything in refinementJustNull is immutable, so a shallow copy is sufficient.
|
||
|
return &ret
|
||
|
}
|
||
|
|
||
|
func (r *refinementNullable) null() tristateBool {
|
||
|
return r.isNull
|
||
|
}
|
||
|
|
||
|
func (r *refinementNullable) setNull(v tristateBool) {
|
||
|
r.isNull = v
|
||
|
}
|
||
|
|
||
|
func (r *refinementNullable) rawEqual(other unknownValRefinement) bool {
|
||
|
{
|
||
|
other, ok := other.(*refinementNullable)
|
||
|
if !ok {
|
||
|
return false
|
||
|
}
|
||
|
return r.isNull == other.isNull
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (r *refinementNullable) GoString() string {
|
||
|
switch r.isNull {
|
||
|
case tristateFalse:
|
||
|
return ".NotNull()"
|
||
|
case tristateTrue:
|
||
|
return ".Null()"
|
||
|
default:
|
||
|
return ""
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type tristateBool rune
|
||
|
|
||
|
const tristateTrue tristateBool = 'T'
|
||
|
const tristateFalse tristateBool = 'F'
|
||
|
const tristateUnknown tristateBool = 0
|