Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 151 additions & 1 deletion interpreter/interpretable.go
Original file line number Diff line number Diff line change
Expand Up @@ -799,7 +799,7 @@ func (fold *evalFold) Eval(ctx Activation) ref.Val {
f := newFolder(fold, ctx)
defer releaseFolder(f)

foldRange := fold.iterRange.Eval(ctx)
foldRange := fold.iterRange.Eval(f.iterRangeActivation())
if types.IsUnknownOrError(foldRange) {
return foldRange
}
Expand Down Expand Up @@ -1309,6 +1309,7 @@ type folder struct {
accuVal ref.Val
iterVar1Val any
iterVar2Val any
iterIndex int64

// bookkeeping flags to modify Activation and fold behaviors.
initialized bool
Expand All @@ -1321,6 +1322,9 @@ func (f *folder) foldIterable(iterable traits.Iterable) ref.Val {
it := iterable.Iterator()
for it.HasNext() == types.True {
f.iterVar1Val = it.Next()
if unk, found := f.maybeUnknownIteratorValue(); found {
f.iterVar1Val = unk
}

cond := f.cond.Eval(f)
condBool, ok := cond.(types.Bool)
Expand All @@ -1335,6 +1339,7 @@ func (f *folder) foldIterable(iterable traits.Iterable) ref.Val {
f.interrupted = true
return f.evalResult()
}
f.iterIndex++
}
return f.evalResult()
}
Expand Down Expand Up @@ -1429,6 +1434,150 @@ func (f *folder) AsPartialActivation() (PartialActivation, bool) {
return nil, false
}

func (f *folder) maybeUnknownIteratorValue() (ref.Val, bool) {
partial, isPartial := AsPartialActivation(f.activation)
if !isPartial {
return nil, false
}
attr, namespaced, ok := f.iterRangeAttribute()
if !ok {
return nil, false
}
rangeQuals := namespaced.Qualifiers()
for _, pat := range partial.UnknownAttributePatterns() {
if !matchesAnyVariable(pat, namespaced.CandidateVariableNames()) {
continue
}
qualPats := pat.QualifierPatterns()
if len(qualPats) <= len(rangeQuals) {
continue
}
if !qualifierPatternsMatch(qualPats, rangeQuals) {
continue
}
if !qualifierPatternMatchesInt(qualPats[len(rangeQuals)], f.iterIndex) {
continue
}
return types.NewUnknown(attr.ID(), attributeTrailFromPattern(pat, f.iterIndex, len(rangeQuals))), true
}
return nil, false
}

func (f *folder) iterRangeActivation() Activation {
partial, isPartial := AsPartialActivation(f.activation)
if !isPartial {
return f.activation
}
_, namespaced, ok := f.iterRangeAttribute()
if !ok {
return f.activation
}
rangeQuals := namespaced.Qualifiers()
unknowns := partial.UnknownAttributePatterns()
filtered := make([]*AttributePattern, 0, len(unknowns))
for _, pat := range unknowns {
if matchesAnyVariable(pat, namespaced.CandidateVariableNames()) &&
len(pat.QualifierPatterns()) > len(rangeQuals) &&
qualifierPatternsMatch(pat.QualifierPatterns(), rangeQuals) {
continue
}
filtered = append(filtered, pat)
}
if len(filtered) == len(unknowns) {
return f.activation
}
return &partActivation{Activation: partial, unknowns: filtered}
}

func (f *folder) iterRangeAttribute() (InterpretableAttribute, NamespacedAttribute, bool) {
attr, ok := f.iterRange.(InterpretableAttribute)
if !ok {
return nil, nil, false
}
namespaced, ok := attr.Attr().(NamespacedAttribute)
if !ok {
return nil, nil, false
}
return attr, namespaced, true
}

func matchesAnyVariable(pat *AttributePattern, variables []string) bool {
for _, variable := range variables {
if pat.VariableMatches(variable) {
return true
}
}
return false
}

func qualifierPatternsMatch(patterns []*AttributeQualifierPattern, qualifiers []Qualifier) bool {
if len(patterns) < len(qualifiers) {
return false
}
for i, qual := range qualifiers {
if !patterns[i].Matches(qual) {
return false
}
}
return true
}

func qualifierPatternMatchesInt(pattern *AttributeQualifierPattern, value int64) bool {
if pattern.wildcard {
return true
}
switch v := pattern.value.(type) {
case int:
return int64(v) == value
case int32:
return int64(v) == value
case int64:
return v == value
case uint:
return uint64(v) == uint64(value)
case uint32:
return uint64(v) == uint64(value)
case uint64:
return v == uint64(value)
}
return false
}

func attributeTrailFromPattern(pat *AttributePattern, iterIndex int64, iterIndexOffset int) *types.AttributeTrail {
attr := types.NewAttributeTrail(pat.variable)
for i, qualPat := range pat.QualifierPatterns() {
if i == iterIndexOffset {
types.QualifyAttribute[int64](attr, iterIndex)
continue
}
if qualPat.wildcard {
types.QualifyAttribute[string](attr, "*")
continue
}
switch v := qualPat.value.(type) {
case bool:
types.QualifyAttribute[bool](attr, v)
case int:
types.QualifyAttribute[int64](attr, int64(v))
case int32:
types.QualifyAttribute[int64](attr, int64(v))
case int64:
types.QualifyAttribute[int64](attr, v)
case string:
types.QualifyAttribute[string](attr, v)
case uint:
types.QualifyAttribute[uint64](attr, uint64(v))
case uint32:
types.QualifyAttribute[uint64](attr, uint64(v))
case uint64:
types.QualifyAttribute[uint64](attr, v)
default:
types.QualifyAttribute[string](attr, fmt.Sprintf("%v", v))
}
}
return attr
}

// evalResult computes the final result of the fold after all entries have been folded and accumulated.
func (f *folder) evalResult() ref.Val {
f.computeResult = true
Expand Down Expand Up @@ -1456,6 +1605,7 @@ func (f *folder) reset() {
f.accuVal = nil
f.iterVar1Val = nil
f.iterVar2Val = nil
f.iterIndex = 0

f.initialized = false
f.mutableValue = false
Expand Down
44 changes: 44 additions & 0 deletions interpreter/interpreter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2090,6 +2090,50 @@ func TestInterpreter_MissingIdentInSelect(t *testing.T) {
}
}

func TestInterpreter_ComprehensionTracksUnknownAttribute(t *testing.T) {
tc := testCase{
name: "comprehension_tracks_unknown_attribute",
expr: `items.exists(x, x.field == 'test')`,
vars: []*decls.VariableDecl{
decls.NewVariable("items", types.NewListType(types.NewMapType(types.StringType, types.StringType))),
},
attrs: NewPartialAttributeFactory(testContainer(""), types.DefaultTypeAdapter, types.NewEmptyRegistry()),
in: newTestPartialActivation(t, map[string]any{
"items": []map[string]string{
{"field": "other"},
{"field": "nope"},
{"field": "unknown"},
},
}, NewAttributePattern("items").QualInt(2).QualString("field")),
}
prg, vars, err := program(t, &tc)
if err != nil {
t.Fatalf("program() failed: %v", err)
}

got := prg.Eval(vars)
unk, ok := got.(*types.Unknown)
if !ok {
t.Fatalf("Eval() got %v, wanted unknown", got)
}
want := types.QualifyAttribute[string](
types.QualifyAttribute[int64](types.NewAttributeTrail("items"), 2),
"field",
)
for _, id := range unk.IDs() {
trails, found := unk.GetAttributeTrails(id)
if !found {
continue
}
for _, trail := range trails {
if trail.Equal(want) {
return
}
}
}
t.Fatalf("Eval() got unknown %v, wanted attribute trail %v", unk, want)
}

func TestInterpreter_TypeConversionOpt(t *testing.T) {
tests := []struct {
in string
Expand Down