diff --git a/cel/cel_test.go b/cel/cel_test.go index 8e74aa54..0aeebc6c 100644 --- a/cel/cel_test.go +++ b/cel/cel_test.go @@ -1438,21 +1438,21 @@ func TestPartialVars(t *testing.T) { interpreter.NewAttributePattern("x"), interpreter.NewAttributePattern("y"), }, - out: types.Unknown{1}, + out: types.NewUnknown(1, types.NewAttributeTrail("x")), }, { in: map[string]any{"x": "10"}, unk: []*interpreter.AttributePattern{ interpreter.NewAttributePattern("y"), }, - out: types.Unknown{4}, + out: types.NewUnknown(4, types.NewAttributeTrail("y")), }, { in: map[string]any{"y": 10}, unk: []*interpreter.AttributePattern{ interpreter.NewAttributePattern("x"), }, - out: types.Unknown{1}, + out: types.NewUnknown(1, types.NewAttributeTrail("x")), }, { in: map[string]any{"x": "10", "y": 10}, @@ -1468,19 +1468,19 @@ func TestPartialVars(t *testing.T) { in: map[string]any{"y": 10}, unk: []*interpreter.AttributePattern{}, out: types.NewErr("no such attribute: x"), - partialOut: types.Unknown{1}, + partialOut: types.NewUnknown(1, types.NewAttributeTrail("x")), }, { in: map[string]any{"x": "10"}, unk: []*interpreter.AttributePattern{}, out: types.NewErr("no such attribute: y"), - partialOut: types.Unknown{4}, + partialOut: types.NewUnknown(4, types.NewAttributeTrail("y")), }, { in: map[string]any{}, unk: []*interpreter.AttributePattern{}, out: types.NewErr("no such attribute: x"), - partialOut: types.Unknown{1}, + partialOut: types.NewUnknown(1, types.NewAttributeTrail("x")), }, } for i, tst := range tests { diff --git a/cel/decls_test.go b/cel/decls_test.go index c33a4d29..f15862fa 100644 --- a/cel/decls_test.go +++ b/cel/decls_test.go @@ -72,11 +72,11 @@ var dispatchTests = []struct { }, { expr: "max(unk, unk)", - out: types.Unknown{42}, + out: types.NewUnknown(42, nil), }, { expr: "max(unk, unk, unk)", - out: types.Unknown{42}, + out: types.NewUnknown(42, nil), }, } @@ -395,11 +395,11 @@ func TestUnaryBinding(t *testing.T) { if err != nil { t.Fatalf("Program() failed: %v", err) } - out, _, err := prg.Eval(map[string]any{"x": types.Unknown{1}}) + out, _, err := prg.Eval(map[string]any{"x": types.NewUnknown(1, nil)}) if err != nil { t.Fatalf("prg.Eval(x=unk) failed: %v", err) } - if !reflect.DeepEqual(out, types.Unknown{1}) { + if !types.NewUnknown(1, nil).Contains(out.(*types.Unknown)) { t.Errorf("prg.Eval(x=unk) returned %v, wanted unknown{1}", out) } } @@ -439,14 +439,14 @@ func TestBinaryBinding(t *testing.T) { if err != nil { t.Fatalf("Program() failed: %v", err) } - out, _, err := prg.Eval(map[string]any{"x": types.Unknown{1}, "y": 1}) + out, _, err := prg.Eval(map[string]any{"x": types.NewUnknown(1, nil), "y": 1}) if err != nil { t.Fatalf("prg.Eval(x=unk) failed: %v", err) } if !reflect.DeepEqual(out, types.IntOne) { t.Errorf("prg.Eval(x=unk, y=1) returned %v, wanted 1", out) } - out, _, err = prg.Eval(map[string]any{"x": 2, "y": types.Unknown{2}}) + out, _, err = prg.Eval(map[string]any{"x": 2, "y": types.NewUnknown(2, nil)}) if err != nil { t.Fatalf("prg.Eval(x=2, y=unk) failed: %v", err) } @@ -790,10 +790,10 @@ func testParse(t testing.TB, env *Env, expr string, want any) { if err != nil { t.Fatalf("env.Program() failed: %v", err) } - out, _, err := prg.Eval(map[string]any{"err": types.NewErr("error argument"), "unk": types.Unknown{42}}) + out, _, err := prg.Eval(map[string]any{"err": types.NewErr("error argument"), "unk": types.NewUnknown(42, nil)}) switch want := want.(type) { - case types.Unknown: - if !reflect.DeepEqual(want, out.(types.Unknown)) { + case *types.Unknown: + if !want.Contains(out.(*types.Unknown)) { t.Errorf("prg.Eval() got %v, wanted %v", out, want) } case ref.Val: @@ -817,10 +817,10 @@ func testCompile(t testing.TB, env *Env, expr string, want any) { if err != nil { t.Fatalf("env.Program() failed: %v", err) } - out, _, err := prg.Eval(map[string]any{"err": types.NewErr("error argument"), "unk": types.Unknown{42}}) + out, _, err := prg.Eval(map[string]any{"err": types.NewErr("error argument"), "unk": types.NewUnknown(42, nil)}) switch want := want.(type) { - case types.Unknown: - if !reflect.DeepEqual(want, out.(types.Unknown)) { + case *types.Unknown: + if !want.Contains(out.(*types.Unknown)) { t.Errorf("prg.Eval() got %v, wanted %v", out, want) } case ref.Val: diff --git a/common/decls/decls.go b/common/decls/decls.go index ab7a093e..0284f8db 100644 --- a/common/decls/decls.go +++ b/common/decls/decls.go @@ -275,17 +275,17 @@ func (f *FunctionDecl) Bindings() ([]*functions.Overload, error) { // to return an unknown set, or to produce a new error for a missing function signature. func MaybeNoSuchOverload(funcName string, args ...ref.Val) ref.Val { argTypes := make([]string, len(args)) - var unk types.Unknown + var unk *types.Unknown = nil for i, arg := range args { if types.IsError(arg) { return arg } if types.IsUnknown(arg) { - unk = append(unk, arg.(types.Unknown)...) + unk = types.MergeUnknowns(arg.(*types.Unknown), unk) } argTypes[i] = arg.Type().TypeName() } - if len(unk) != 0 { + if unk != nil { return unk } signature := strings.Join(argTypes, ", ") diff --git a/common/decls/decls_test.go b/common/decls/decls_test.go index c38ad108..4a017025 100644 --- a/common/decls/decls_test.go +++ b/common/decls/decls_test.go @@ -142,7 +142,7 @@ func TestFunctionVariableArgBindings(t *testing.T) { if !types.IsError(celErr) || !strings.Contains(celErr.(*types.Err).String(), "no such overload") { t.Errorf("binding.Binary(bytes, string) got %v, wanted no such overload", celErr) } - celUnk := binding.Binary(types.Bytes("hi"), types.Unknown{1}) + celUnk := binding.Binary(types.Bytes("hi"), types.NewUnknown(1, types.NewAttributeTrail("x"))) if !types.IsUnknown(celUnk) { t.Errorf("binding.Binary(bytes, unk) got %v, wanted unknown{1}", celUnk) } diff --git a/common/types/unknown.go b/common/types/unknown.go index b8f76641..4b6c8022 100644 --- a/common/types/unknown.go +++ b/common/types/unknown.go @@ -15,45 +15,219 @@ package types import ( + "fmt" + "math" "reflect" + "strings" + "unicode" "github.com/google/cel-go/common/types/ref" ) -// Unknown type implementation which collects expression ids which caused the -// current value to become unknown. -type Unknown []int64 +var ( + unspecifiedAttribute = &AttributeTrail{qualifierPath: []any{}} +) + +// NewAttributeTrail creates a new simple attribute from a variable name. +func NewAttributeTrail(variable string) *AttributeTrail { + if variable == "" { + return unspecifiedAttribute + } + return &AttributeTrail{variable: variable} +} + +// AttributeTrail specifies a variable with an optional qualifier path. An attribute value is expected to +// correspond to an AbsoluteAttribute, meaning a field selection which starts with a top-level variable. +// +// The qualifer path elements adhere to the AttributeQualifier type constraint. +type AttributeTrail struct { + variable string + qualifierPath []any +} + +// Equals returns whether two attribute values have the same variable name and qualifier paths. +func (a *AttributeTrail) Equal(other *AttributeTrail) bool { + if a.Variable() != other.Variable() || len(a.QualifierPath()) != len(other.QualifierPath()) { + return false + } + for i, q := range a.QualifierPath() { + qual := other.QualifierPath()[i] + if !qualifiersEqual(q, qual) { + return false + } + } + return true +} + +func qualifiersEqual(a, b any) bool { + if a == b { + return true + } + switch numA := a.(type) { + case int64: + numB, ok := b.(uint64) + if !ok { + return false + } + return intUintEqual(numA, numB) + case uint64: + numB, ok := b.(int64) + if !ok { + return false + } + return intUintEqual(numB, numA) + default: + return false + } +} + +func intUintEqual(i int64, u uint64) bool { + if i < 0 || u > math.MaxInt64 { + return false + } + return i == int64(u) +} + +// Variable returns the variable name associated with the attribute. +func (a *AttributeTrail) Variable() string { + return a.variable +} + +// QualifierPath returns the optional set of qualifying fields or indices applied to the variable. +func (a *AttributeTrail) QualifierPath() []any { + return a.qualifierPath +} + +// String returns the string representation of the Attribute. +func (a *AttributeTrail) String() string { + if a.variable == "" { + return "" + } + var str strings.Builder + str.WriteString(a.variable) + for _, q := range a.qualifierPath { + switch q := q.(type) { + case bool, int64: + str.WriteString(fmt.Sprintf("[%v]", q)) + case uint64: + str.WriteString(fmt.Sprintf("[%vu]", q)) + case string: + if isIdentifierCharacter(q) { + str.WriteString(fmt.Sprintf(".%v", q)) + } else { + str.WriteString(fmt.Sprintf("[%q]", q)) + } + } + } + return str.String() +} + +func isIdentifierCharacter(str string) bool { + for _, c := range str { + if unicode.IsLetter(c) || unicode.IsDigit(c) || string(c) == "_" { + continue + } + return false + } + return true +} + +// AttributeQualifier constrains the possible types which may be used to qualify an attribute. +type AttributeQualifier interface { + bool | int64 | uint64 | string +} + +// QualifyAttribute qualifies an attribute using a valid AttributeQualifier type. +func QualifyAttribute[T AttributeQualifier](attr *AttributeTrail, qualifier T) *AttributeTrail { + attr.qualifierPath = append(attr.qualifierPath, qualifier) + return attr +} + +// Unknown type which collects expression ids which caused the current value to become unknown. +type Unknown struct { + attributeTrails map[int64][]*AttributeTrail +} + +// NewUnknown creates a new unknown at a given expression id for an attribute. +// +// If the attribute is nil, the attribute value will be the `unspecifiedAttribute`. +func NewUnknown(id int64, attr *AttributeTrail) *Unknown { + if attr == nil { + attr = unspecifiedAttribute + } + return &Unknown{ + attributeTrails: map[int64][]*AttributeTrail{id: {attr}}, + } +} + +// Contains returns true if the input unknown is a subset of the current unknown. +func (u *Unknown) Contains(other *Unknown) bool { + for id, otherTrails := range other.attributeTrails { + trails, found := u.attributeTrails[id] + if !found || len(otherTrails) != len(trails) { + return false + } + for _, ot := range otherTrails { + found := false + for _, t := range trails { + if t.Equal(ot) { + found = true + break + } + } + if !found { + return false + } + } + } + return true +} // ConvertToNative implements ref.Val.ConvertToNative. -func (u Unknown) ConvertToNative(typeDesc reflect.Type) (any, error) { +func (u *Unknown) ConvertToNative(typeDesc reflect.Type) (any, error) { return u.Value(), nil } // ConvertToType is an identity function since unknown values cannot be modified. -func (u Unknown) ConvertToType(typeVal ref.Type) ref.Val { +func (u *Unknown) ConvertToType(typeVal ref.Type) ref.Val { return u } // Equal is an identity function since unknown values cannot be modified. -func (u Unknown) Equal(other ref.Val) ref.Val { +func (u *Unknown) Equal(other ref.Val) ref.Val { return u } +// String implements the Stringer interface +func (u *Unknown) String() string { + var str strings.Builder + for id, attrs := range u.attributeTrails { + if str.Len() != 0 { + str.WriteString(", ") + } + if len(attrs) == 1 { + str.WriteString(fmt.Sprintf("%v (%d)", attrs[0], id)) + } else { + str.WriteString(fmt.Sprintf("%v (%d)", attrs, id)) + } + } + return str.String() +} + // Type implements ref.Val.Type. -func (u Unknown) Type() ref.Type { +func (u *Unknown) Type() ref.Type { return UnknownType } // Value implements ref.Val.Value. -func (u Unknown) Value() any { - return []int64(u) +func (u *Unknown) Value() any { + return u } -// IsUnknown returns whether the element ref.Type or ref.Val is equal to the -// UnknownType singleton. +// IsUnknown returns whether the element ref.Val is in instance of *types.Unknown func IsUnknown(val ref.Val) bool { switch val.(type) { - case Unknown: + case *Unknown: return true default: return false @@ -66,16 +240,51 @@ func IsUnknown(val ref.Val) bool { // If the input `val` is another Unknown, then the result will be the merge of the `val` and the input // `unk`. If the `val` is not unknown, then the result will depend on whether the input `unk` is nil. // If both values are non-nil and unknown, then the return value will be a merge of both unknowns. -func MaybeMergeUnknowns(val ref.Val, unk Unknown) (Unknown, bool) { - src, isUnk := val.(Unknown) +func MaybeMergeUnknowns(val ref.Val, unk *Unknown) (*Unknown, bool) { + src, isUnk := val.(*Unknown) if !isUnk { if unk != nil { return unk, true } - return nil, false + return unk, false } - if unk == nil { - return src, true + return MergeUnknowns(src, unk), true +} + +// MergeUnknowns combines two unknown values into a new unknown value. +func MergeUnknowns(unk1, unk2 *Unknown) *Unknown { + if unk1 == nil { + return unk2 + } + if unk2 == nil { + return unk1 + } + out := &Unknown{ + attributeTrails: make(map[int64][]*AttributeTrail, len(unk1.attributeTrails)+len(unk2.attributeTrails)), + } + for id, ats := range unk1.attributeTrails { + out.attributeTrails[id] = ats + } + for id, ats := range unk2.attributeTrails { + existing, found := out.attributeTrails[id] + if !found { + out.attributeTrails[id] = ats + continue + } + + for _, at := range ats { + found := false + for _, et := range existing { + if at.Equal(et) { + found = true + break + } + } + if !found { + existing = append(existing, at) + } + } + out.attributeTrails[id] = existing } - return append(unk, src...), true + return out } diff --git a/common/types/unknown_test.go b/common/types/unknown_test.go index ef9b89ba..d0d83d6a 100644 --- a/common/types/unknown_test.go +++ b/common/types/unknown_test.go @@ -16,13 +16,15 @@ package types import ( "fmt" + "math" + "strings" "testing" "github.com/google/cel-go/common/types/ref" ) func TestIsUnknown(t *testing.T) { - if IsUnknown(Unknown{}) != true { + if IsUnknown(&Unknown{}) != true { t.Error("IsUnknown(Unknown{}) returned false, wanted true") } if IsUnknown(Bool(true)) != false { @@ -30,35 +32,297 @@ func TestIsUnknown(t *testing.T) { } } +func TestNewAttribute(t *testing.T) { + if NewAttributeTrail("") != unspecifiedAttribute { + t.Error("An empty attribute must be the unspecified attribute") + } + if NewAttributeTrail("v").Equal(unspecifiedAttribute) { + t.Error("A non-empty attribute must not be equal to the unspecified attribute") + } +} + +func TestAttributeEquals(t *testing.T) { + tests := []struct { + a *AttributeTrail + b *AttributeTrail + equal bool + }{ + { + a: unspecifiedAttribute, + b: NewAttributeTrail(""), + equal: true, + }, + { + a: NewAttributeTrail("a"), + b: NewAttributeTrail(""), + equal: false, + }, + { + a: NewAttributeTrail("a"), + b: NewAttributeTrail("a"), + equal: true, + }, + { + a: QualifyAttribute[string](NewAttributeTrail("a"), "b"), + b: NewAttributeTrail("a"), + equal: false, + }, + { + a: QualifyAttribute[string](NewAttributeTrail("a"), "b"), + b: QualifyAttribute[int64](NewAttributeTrail("a"), 1), + equal: false, + }, + { + a: QualifyAttribute[int64](NewAttributeTrail("a"), 1), + b: QualifyAttribute[string](NewAttributeTrail("a"), "1"), + equal: false, + }, + { + a: QualifyAttribute[uint64](NewAttributeTrail("a"), 1), + b: QualifyAttribute[string](NewAttributeTrail("a"), "1"), + equal: false, + }, + { + a: QualifyAttribute[string](NewAttributeTrail("a"), "b"), + b: QualifyAttribute[string](NewAttributeTrail("a"), "b"), + equal: true, + }, + { + a: QualifyAttribute[int64](NewAttributeTrail("a"), 20), + b: QualifyAttribute[uint64](NewAttributeTrail("a"), 20), + equal: true, + }, + { + a: QualifyAttribute[uint64](NewAttributeTrail("a"), 20), + b: QualifyAttribute[int64](NewAttributeTrail("a"), 20), + equal: true, + }, + { + a: QualifyAttribute[uint64](NewAttributeTrail("a"), 21), + b: QualifyAttribute[int64](NewAttributeTrail("a"), 20), + equal: false, + }, + { + a: QualifyAttribute[int64](NewAttributeTrail("a"), 20), + b: QualifyAttribute[uint64](NewAttributeTrail("a"), 21), + equal: false, + }, + { + a: QualifyAttribute[int64](NewAttributeTrail("a"), -1), + b: QualifyAttribute[uint64](NewAttributeTrail("a"), 0), + equal: false, + }, + { + a: QualifyAttribute[int64](NewAttributeTrail("a"), 1), + b: QualifyAttribute[uint64](NewAttributeTrail("a"), math.MaxInt64+1), + equal: false, + }, + } + for i, tst := range tests { + tc := tst + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + out := tc.a.Equal(tc.b) + if out != tc.equal { + t.Errorf("%v.Equal(%v) got %v, wanted %v", tc.a, tc.b, out, tc.equal) + } + }) + } +} + +func TestAttributeString(t *testing.T) { + tests := []struct { + attr *AttributeTrail + out string + }{ + { + attr: unspecifiedAttribute, + out: "", + }, + { + attr: NewAttributeTrail("a"), + out: "a", + }, + { + attr: QualifyAttribute[bool](NewAttributeTrail("a"), false), + out: "a[false]", + }, + { + attr: QualifyAttribute[string](NewAttributeTrail("a"), "b"), + out: "a.b", + }, + { + attr: QualifyAttribute(QualifyAttribute[string](NewAttributeTrail("a"), "b"), "$this"), + out: `a.b["$this"]`, + }, + { + attr: QualifyAttribute[int64](NewAttributeTrail("a"), 12), + out: "a[12]", + }, + { + attr: QualifyAttribute[uint64](NewAttributeTrail("a"), 24), + out: "a[24u]", + }, + } + for i, tst := range tests { + tc := tst + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + out := tc.attr.String() + if out != tc.out { + t.Errorf("%v.String() got %v, wanted %v", tc.attr, out, tc.out) + } + }) + } +} + +func TestUnknownContains(t *testing.T) { + tests := []struct { + unk *Unknown + other *Unknown + out bool + }{ + { + unk: NewUnknown(1, nil), + other: NewUnknown(1, unspecifiedAttribute), + out: true, + }, + { + unk: NewUnknown(3, QualifyAttribute[bool](NewAttributeTrail("a"), true)), + other: NewUnknown(4, QualifyAttribute[string](NewAttributeTrail("a"), "b")), + out: false, + }, + { + unk: NewUnknown(3, QualifyAttribute[string](NewAttributeTrail("a"), "b")), + other: NewUnknown(4, QualifyAttribute[string](NewAttributeTrail("a"), "b")), + out: false, + }, + { + unk: NewUnknown(3, QualifyAttribute[string](NewAttributeTrail("a"), "c")), + other: NewUnknown(3, QualifyAttribute[string](NewAttributeTrail("a"), "b")), + out: false, + }, + { + unk: MergeUnknowns( + NewUnknown(3, QualifyAttribute[bool](NewAttributeTrail("a"), true)), + NewUnknown(4, QualifyAttribute[string](NewAttributeTrail("a"), "b")), + ), + other: NewUnknown(3, QualifyAttribute[bool](NewAttributeTrail("a"), true)), + out: true, + }, + { + unk: NewUnknown(3, QualifyAttribute[bool](NewAttributeTrail("a"), true)), + other: MergeUnknowns( + NewUnknown(3, QualifyAttribute[bool](NewAttributeTrail("a"), true)), + NewUnknown(4, QualifyAttribute[string](NewAttributeTrail("a"), "b")), + ), + out: false, + }, + } + for i, tst := range tests { + tc := tst + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + out := tc.unk.Contains(tc.other) + if out != tc.out { + t.Errorf("%v.Contains(%v) got %v, wanted %v", tc.unk, tc.other, out, tc.out) + } + }) + } +} + +func TestUnknownString(t *testing.T) { + tests := []struct { + unk *Unknown + out any + }{ + { + unk: NewUnknown(1, nil), + out: " (1)", + }, + { + unk: NewUnknown(1, unspecifiedAttribute), + out: " (1)", + }, + { + unk: NewUnknown(2, NewAttributeTrail("a")), + out: "a (2)", + }, + { + unk: NewUnknown(3, QualifyAttribute[bool](NewAttributeTrail("a"), false)), + out: "a[false] (3)", + }, + { + unk: MergeUnknowns( + NewUnknown(3, QualifyAttribute[bool](NewAttributeTrail("a"), true)), + NewUnknown(4, QualifyAttribute[string](NewAttributeTrail("a"), "b")), + ), + out: []string{"a[true] (3)", "a.b (4)"}, + }, + { + // this case might occur in a logical condition where the attributes are equal. + unk: MergeUnknowns( + NewUnknown(3, QualifyAttribute[int64](NewAttributeTrail("a"), 0)), + NewUnknown(3, QualifyAttribute[int64](NewAttributeTrail("a"), 0)), + ), + out: "a[0] (3)", + }, + { + // this case might occur if attribute tracking through comprehensions is supported + unk: MergeUnknowns( + NewUnknown(3, QualifyAttribute[int64](NewAttributeTrail("a"), 0)), + NewUnknown(3, QualifyAttribute[int64](NewAttributeTrail("a"), 1)), + ), + out: "[a[0] a[1]] (3)", + }, + } + for i, tst := range tests { + tc := tst + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + out := tc.unk.String() + switch want := tc.out.(type) { + case string: + if out != want { + t.Errorf("%v.String() got %v, wanted %v", tc.unk, out, want) + } + case []string: + for _, w := range want { + if !strings.Contains(out, w) { + t.Errorf("%v.String() got %v, wanted it to contain %v", tc.unk, out, w) + } + } + } + + }) + } +} + func TestMaybeMergeUnknowns(t *testing.T) { tests := []struct { in ref.Val - unk Unknown - want Unknown + unk *Unknown + want *Unknown isUnk bool }{ + // one of the unknowns is empty { in: String(""), unk: nil, - want: nil, isUnk: false, }, + // both unknowns are empty { in: String(""), - unk: Unknown{}, - want: Unknown{}, + unk: &Unknown{}, + want: &Unknown{}, isUnk: true, }, { - in: Unknown{2}, - unk: Unknown{1}, - want: Unknown{1, 2}, + in: newUnk(t, 2, "x"), + unk: newUnk(t, 1, "y"), + want: MergeUnknowns(newUnk(t, 2, "x"), newUnk(t, 1, "y")), isUnk: true, }, { - in: Unknown{2}, - unk: nil, - want: Unknown{2}, + in: newUnk(t, 2, "x"), + want: newUnk(t, 2, "x"), isUnk: true, }, } @@ -73,3 +337,10 @@ func TestMaybeMergeUnknowns(t *testing.T) { }) } } + +func newUnk(t *testing.T, id int64, varName string) *Unknown { + t.Helper() + attr := NewAttributeTrail(varName) + unk := NewUnknown(id, attr) + return unk +} diff --git a/common/types/util.go b/common/types/util.go index a8e9afa9..71662eee 100644 --- a/common/types/util.go +++ b/common/types/util.go @@ -21,7 +21,7 @@ import ( // IsUnknownOrError returns whether the input element ref.Val is an ErrType or UnknownType. func IsUnknownOrError(val ref.Val) bool { switch val.(type) { - case Unknown, *Err: + case *Unknown, *Err: return true } return false diff --git a/common/types/util_test.go b/common/types/util_test.go index fe9c6fb7..b10b3e84 100644 --- a/common/types/util_test.go +++ b/common/types/util_test.go @@ -18,7 +18,7 @@ import "testing" func BenchmarkIsUnknownOrError(b *testing.B) { err := NewErr("test") - unk := Unknown{} + unk := &Unknown{} for i := 0; i < b.N; i++ { if !(IsUnknownOrError(unk) && IsUnknownOrError(err) && !IsUnknownOrError(IntOne)) { b.Fatal("IsUnknownOrError() provided an incorrect result.") diff --git a/interpreter/attribute_patterns.go b/interpreter/attribute_patterns.go index 1796de70..1fbaaf17 100644 --- a/interpreter/attribute_patterns.go +++ b/interpreter/attribute_patterns.go @@ -15,6 +15,8 @@ package interpreter import ( + "fmt" + "github.com/google/cel-go/common/containers" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" @@ -241,12 +243,15 @@ func (fac *partialAttributeFactory) matchesUnknownPatterns( vars PartialActivation, attrID int64, variableNames []string, - qualifiers []Qualifier) (types.Unknown, error) { + qualifiers []Qualifier) (*types.Unknown, error) { patterns := vars.UnknownAttributePatterns() candidateIndices := map[int]struct{}{} for _, variable := range variableNames { for i, pat := range patterns { if pat.VariableMatches(variable) { + if len(qualifiers) == 0 { + return types.NewUnknown(attrID, types.NewAttributeTrail(variable)), nil + } candidateIndices[i] = struct{}{} } } @@ -255,10 +260,6 @@ func (fac *partialAttributeFactory) matchesUnknownPatterns( if len(candidateIndices) == 0 { return nil, nil } - // Determine whether to return early if there are no qualifiers. - if len(qualifiers) == 0 { - return types.Unknown{attrID}, nil - } // Resolve the attribute qualifiers into a static set. This prevents more dynamic // Attribute resolutions than necessary when there are multiple unknown patterns // that traverse the same Attribute-based qualifier field. @@ -300,7 +301,28 @@ func (fac *partialAttributeFactory) matchesUnknownPatterns( } } if isUnk { - return types.Unknown{matchExprID}, nil + attr := types.NewAttributeTrail(pat.variable) + for i := 0; i < len(qualPats) && i < len(newQuals); i++ { + if qual, ok := newQuals[i].(ConstantQualifier); ok { + switch v := qual.Value().Value().(type) { + case bool: + types.QualifyAttribute[bool](attr, v) + case float64: + types.QualifyAttribute[int64](attr, int64(v)) + case int64: + types.QualifyAttribute[int64](attr, v) + case string: + types.QualifyAttribute[string](attr, v) + case uint64: + types.QualifyAttribute[uint64](attr, v) + default: + types.QualifyAttribute[string](attr, fmt.Sprintf("%v", v)) + } + } else { + types.QualifyAttribute[string](attr, "*") + } + } + return types.NewUnknown(matchExprID, attr), nil } } return nil, nil diff --git a/interpreter/attribute_patterns_test.go b/interpreter/attribute_patterns_test.go index 718385d9..67a93f64 100644 --- a/interpreter/attribute_patterns_test.go +++ b/interpreter/attribute_patterns_test.go @@ -16,7 +16,6 @@ package interpreter import ( "fmt" - "reflect" "testing" "github.com/google/cel-go/common/containers" @@ -206,7 +205,7 @@ func TestAttributePattern_UnknownResolution(t *testing.T) { if err != nil { t.Fatalf("Got error: %s, wanted unknown", err) } - _, isUnk := val.(types.Unknown) + _, isUnk := val.(*types.Unknown) if !isUnk { t.Fatalf("Got value %v, wanted unknown", val) } @@ -252,8 +251,8 @@ func TestAttributePattern_CrossReference(t *testing.T) { if err != nil { t.Fatal(err) } - if !reflect.DeepEqual(val, types.Unknown{2}) { - t.Fatalf("Got %v, wanted unknown attribute id for 'b' (2)", val) + if !types.NewUnknown(2, types.NewAttributeTrail("b")).Contains(val.(*types.Unknown)) { + t.Errorf("Got %v, wanted unknown attribute id for 'b' (2)", val) } // Ensure that a[b], the dynamic index into var 'a' is the unknown value @@ -268,8 +267,8 @@ func TestAttributePattern_CrossReference(t *testing.T) { if err != nil { t.Fatal(err) } - if !reflect.DeepEqual(val, types.Unknown{2}) { - t.Fatalf("Got %v, wanted unknown attribute id for 'b' (2)", val) + if !types.NewUnknown(2, types.NewAttributeTrail("b")).Contains(val.(*types.Unknown)) { + t.Errorf("Got %v, wanted unknown attribute id for 'b' (2)", val) } // Note, that only 'a[0].c' will result in an unknown result since both 'a' and 'b' @@ -282,8 +281,11 @@ func TestAttributePattern_CrossReference(t *testing.T) { if err != nil { t.Fatal(err) } - if !reflect.DeepEqual(val, types.Unknown{2}) { - t.Fatalf("Got %v, wanted unknown attribute id for 'b' (2)", val) + unkAttr := types.NewAttributeTrail("a") + types.QualifyAttribute[int64](unkAttr, 0) + wantUnk := types.NewUnknown(2, unkAttr) + if !wantUnk.Contains(val.(*types.Unknown)) { + t.Errorf("Got %v, wanted unknown attribute id for %v", val, wantUnk) } // Test a positive case that returns a valid value even though the attribugte factory @@ -295,7 +297,7 @@ func TestAttributePattern_CrossReference(t *testing.T) { t.Fatal(err) } if val != int64(1) { - t.Fatalf("Got %v, wanted 1 for a[b]", val) + t.Errorf("Got %v, wanted 1 for a[b]", val) } // Ensure the unknown attribute id moves when the attribute becomes more specific. @@ -310,8 +312,12 @@ func TestAttributePattern_CrossReference(t *testing.T) { if err != nil { t.Fatal(err) } - if !reflect.DeepEqual(val, types.Unknown{3}) { - t.Fatalf("Got %v, wanted unknown attribute id for a[b].c (3)", val) + unkAttr = types.NewAttributeTrail("a") + types.QualifyAttribute[int64](unkAttr, 0) + types.QualifyAttribute[string](unkAttr, "c") + wantUnk = types.NewUnknown(3, unkAttr) + if !wantUnk.Contains(val.(*types.Unknown)) { + t.Errorf("Got %v, wanted unknown attribute id for %v", val, wantUnk) } } diff --git a/interpreter/attributes.go b/interpreter/attributes.go index 4ac56bd1..ca97bdfc 100644 --- a/interpreter/attributes.go +++ b/interpreter/attributes.go @@ -655,7 +655,7 @@ func newQualifier(adapter types.Adapter, id int64, v any, opt bool) (Qualifier, qual = &doubleQualifier{ id: id, value: float64(val), celValue: val, adapter: adapter, optional: opt, } - case types.Unknown: + case *types.Unknown: qual = &unknownQualifier{id: id, value: val} default: if q, ok := v.(Qualifier); ok { @@ -1129,7 +1129,7 @@ func (q *doubleQualifier) Value() ref.Val { // for any value subject to qualification. This is consistent with CEL's unknown handling elsewhere. type unknownQualifier struct { id int64 - value types.Unknown + value *types.Unknown } // ID is an implementation of the Qualifier interface method. @@ -1226,7 +1226,7 @@ func attrQualifyIfPresent(fac AttributeFactory, vars Activation, obj any, qualAt func refQualify(adapter types.Adapter, obj any, idx ref.Val, presenceTest, presenceOnly bool) (ref.Val, bool, error) { celVal := adapter.NativeToValue(obj) switch v := celVal.(type) { - case types.Unknown: + case *types.Unknown: return v, true, nil case *types.Err: return nil, false, v diff --git a/interpreter/attributes_test.go b/interpreter/attributes_test.go index 48dee461..0a08313c 100644 --- a/interpreter/attributes_test.go +++ b/interpreter/attributes_test.go @@ -740,13 +740,12 @@ func TestAttributesConditionalAttrErrorUnknown(t *testing.T) { } // unk ? a : b - condUnk := attrs.ConditionalAttribute(1, NewConstValue(0, types.Unknown{1}), tv, fv) + condUnk := attrs.ConditionalAttribute(1, NewConstValue(0, types.NewUnknown(1, nil)), tv, fv) out, err = condUnk.Resolve(EmptyActivation()) if err != nil { t.Fatal(err) } - unk, ok := out.(types.Unknown) - if !ok || !types.IsUnknown(unk) { + if !types.IsUnknown(out.(ref.Val)) { t.Errorf("Got %v, wanted unknown", out) } } @@ -845,7 +844,7 @@ func TestAttributeMissingMsgUnknownField(t *testing.T) { if err != nil { t.Fatal(err) } - _, isUnk := out.(types.Unknown) + _, isUnk := out.(*types.Unknown) if !isUnk { t.Errorf("got %v, wanted unknown value", out) } @@ -1085,7 +1084,7 @@ func TestAttributeStateTracking(t *testing.T) { }, NewAttributePattern("a").QualString("b"), ), - out: types.Unknown{5}, + out: types.NewUnknown(5, types.QualifyAttribute[string](types.NewAttributeTrail("a"), "b")), }, } for _, test := range tests { @@ -1192,7 +1191,12 @@ type custAttrFactory struct { func (r *custAttrFactory) NewQualifier(objType *types.Type, qualID int64, val any, opt bool) (Qualifier, error) { if objType.Kind() == types.StructKind && objType.TypeName() == "google.expr.proto3.test.TestAllTypes.NestedMessage" { - return &nestedMsgQualifier{id: qualID, field: val.(string)}, nil + switch v := val.(type) { + case string: + return &nestedMsgQualifier{id: qualID, field: v, opt: opt}, nil + case types.String: + return &nestedMsgQualifier{id: qualID, field: string(v), opt: opt}, nil + } } return r.AttributeFactory.NewQualifier(objType, qualID, val, opt) } @@ -1200,12 +1204,17 @@ func (r *custAttrFactory) NewQualifier(objType *types.Type, qualID int64, val an type nestedMsgQualifier struct { id int64 field string + opt bool } func (q *nestedMsgQualifier) ID() int64 { return q.id } +func (q *nestedMsgQualifier) IsOptional() bool { + return q.opt +} + func (q *nestedMsgQualifier) Qualify(vars Activation, obj any) (any, error) { pb := obj.(*proto3pb.TestAllTypes_NestedMessage) return pb.GetBb(), nil @@ -1219,10 +1228,6 @@ func (q *nestedMsgQualifier) QualifyIfPresent(vars Activation, obj any, presence return pb.GetBb(), true, nil } -func (q *nestedMsgQualifier) IsOptional() bool { - return false -} - func addQualifier(t testing.TB, attr Attribute, qual Qualifier) Attribute { t.Helper() _, err := attr.AddQualifier(qual) diff --git a/interpreter/interpretable.go b/interpreter/interpretable.go index 05b3aa03..c4598dfa 100644 --- a/interpreter/interpretable.go +++ b/interpreter/interpretable.go @@ -214,7 +214,7 @@ func (or *evalOr) ID() int64 { // Eval implements the Interpretable interface method. func (or *evalOr) Eval(ctx Activation) ref.Val { var err ref.Val = nil - var unk types.Unknown = nil + var unk *types.Unknown for _, term := range or.terms { val := term.Eval(ctx) boolVal, ok := val.(types.Bool) @@ -256,7 +256,7 @@ func (and *evalAnd) ID() int64 { // Eval implements the Interpretable interface method. func (and *evalAnd) Eval(ctx Activation) ref.Val { var err ref.Val = nil - var unk types.Unknown = nil + var unk *types.Unknown for _, term := range and.terms { val := term.Eval(ctx) boolVal, ok := val.(types.Bool) @@ -861,18 +861,40 @@ type evalWatchAttr struct { // AddQualifier creates a wrapper over the incoming qualifier which observes the qualification // result. func (e *evalWatchAttr) AddQualifier(q Qualifier) (Attribute, error) { - cq, isConst := q.(ConstantQualifier) - if isConst { + switch qual := q.(type) { + // By default, the qualifier is either a constant or an attribute + // There may be some custom cases where the attribute is neither. + case ConstantQualifier: + // Expose a method to test whether the qualifier matches the input pattern. q = &evalWatchConstQual{ - ConstantQualifier: cq, + ConstantQualifier: qual, observer: e.observer, - adapter: e.InterpretableAttribute.Adapter(), + adapter: e.Adapter(), } - } else { + case *evalWatchAttr: + // Unwrap the evalWatchAttr since the observation will be applied during Qualify or + // QualifyIfPresent rather than Eval. + q = &evalWatchAttrQual{ + Attribute: qual.InterpretableAttribute, + observer: e.observer, + adapter: e.Adapter(), + } + case Attribute: + // Expose methods which intercept the qualification prior to being applied as a qualifier. + // Using this interface ensures that the qualifier is converted to a constant value one + // time during attribute pattern matching as the method embeds the Attribute interface + // needed to trip the conversion to a constant. + q = &evalWatchAttrQual{ + Attribute: qual, + observer: e.observer, + adapter: e.Adapter(), + } + default: + // This is likely a custom qualifier type. q = &evalWatchQual{ - Qualifier: q, + Qualifier: qual, observer: e.observer, - adapter: e.InterpretableAttribute.Adapter(), + adapter: e.Adapter(), } } _, err := e.InterpretableAttribute.AddQualifier(q) @@ -930,6 +952,43 @@ func (e *evalWatchConstQual) QualifierValueEquals(value any) bool { return ok && qve.QualifierValueEquals(value) } +// evalWatchAttrQual observes the qualification of an object by a value computed at runtime. +type evalWatchAttrQual struct { + Attribute + observer EvalObserver + adapter ref.TypeAdapter +} + +// Qualify observes the qualification of a object via a value computed at runtime. +func (e *evalWatchAttrQual) Qualify(vars Activation, obj any) (any, error) { + out, err := e.Attribute.Qualify(vars, obj) + var val ref.Val + if err != nil { + val = types.WrapErr(err) + } else { + val = e.adapter.NativeToValue(out) + } + e.observer(e.ID(), e.Attribute, val) + return out, err +} + +// QualifyIfPresent conditionally qualifies the variable and only records a value if one is present. +func (e *evalWatchAttrQual) QualifyIfPresent(vars Activation, obj any, presenceOnly bool) (any, bool, error) { + out, present, err := e.Attribute.QualifyIfPresent(vars, obj, presenceOnly) + var val ref.Val + if err != nil { + val = types.WrapErr(err) + } else if out != nil { + val = e.adapter.NativeToValue(out) + } else if presenceOnly { + val = types.Bool(present) + } + if present || presenceOnly { + e.observer(e.ID(), e.Attribute, val) + } + return out, present, err +} + // evalWatchQual observes the qualification of an object by a value computed at runtime. type evalWatchQual struct { Qualifier @@ -994,7 +1053,7 @@ func (or *evalExhaustiveOr) ID() int64 { // Eval implements the Interpretable interface method. func (or *evalExhaustiveOr) Eval(ctx Activation) ref.Val { var err ref.Val = nil - var unk types.Unknown = nil + var unk *types.Unknown isTrue := false for _, term := range or.terms { val := term.Eval(ctx) @@ -1004,21 +1063,14 @@ func (or *evalExhaustiveOr) Eval(ctx Activation) ref.Val { isTrue = true } if !ok && !isTrue { - if types.IsUnknown(val) { - if unk == nil { - unk = types.Unknown{} - } - unk = append(unk, val.(types.Unknown)...) - continue - } - if types.IsError(val) { - if unk == nil && err == nil { + isUnk := false + unk, isUnk = types.MaybeMergeUnknowns(val, unk) + if !isUnk && err == nil { + if types.IsError(val) { err = val + } else { + err = types.MaybeNoSuchOverloadErr(val) } - continue - } - if unk == nil { - err = types.MaybeNoSuchOverloadErr(val) } } } @@ -1048,7 +1100,7 @@ func (and *evalExhaustiveAnd) ID() int64 { // Eval implements the Interpretable interface method. func (and *evalExhaustiveAnd) Eval(ctx Activation) ref.Val { var err ref.Val = nil - var unk types.Unknown = nil + var unk *types.Unknown isFalse := false for _, term := range and.terms { val := term.Eval(ctx) @@ -1058,21 +1110,14 @@ func (and *evalExhaustiveAnd) Eval(ctx Activation) ref.Val { isFalse = true } if !ok && !isFalse { - if types.IsUnknown(val) { - if unk == nil { - unk = types.Unknown{} - } - unk = append(unk, val.(types.Unknown)...) - continue - } - if types.IsError(val) { - if unk == nil && err == nil { + isUnk := false + unk, isUnk = types.MaybeMergeUnknowns(val, unk) + if !isUnk && err == nil { + if types.IsError(val) { err = val + } else { + err = types.MaybeNoSuchOverloadErr(val) } - continue - } - if unk == nil { - err = types.MaybeNoSuchOverloadErr(val) } } } diff --git a/interpreter/interpreter_test.go b/interpreter/interpreter_test.go index 6119c77e..26e50c2f 100644 --- a/interpreter/interpreter_test.go +++ b/interpreter/interpreter_test.go @@ -59,7 +59,7 @@ type testCase struct { unchecked bool extraOpts []InterpretableDecorator - in map[string]any + in any out any err string progErr string @@ -1215,8 +1215,8 @@ func testData(t testing.TB) []testCase { attrs: &custAttrFactory{ AttributeFactory: NewAttributeFactory( testContainer("google.expr.proto3.test"), - types.DefaultTypeAdapter, - types.NewEmptyRegistry(), + newTestRegistry(t, &proto3pb.TestAllTypes_NestedMessage{}), + newTestRegistry(t, &proto3pb.TestAllTypes_NestedMessage{}), ), }, in: map[string]any{ @@ -1226,6 +1226,29 @@ func testData(t testing.TB) []testCase { }, out: types.True, }, + { + name: "select_custom_pb3_optional_field", + expr: `a.?bb`, + container: "google.expr.proto3.test", + types: []proto.Message{&proto3pb.TestAllTypes_NestedMessage{}}, + vars: []*decls.VariableDecl{ + decls.NewVariable("a", + types.NewObjectType("google.expr.proto3.test.TestAllTypes.NestedMessage")), + }, + attrs: &custAttrFactory{ + AttributeFactory: NewAttributeFactory( + testContainer("google.expr.proto3.test"), + newTestRegistry(t, &proto3pb.TestAllTypes_NestedMessage{}), + newTestRegistry(t, &proto3pb.TestAllTypes_NestedMessage{}), + ), + }, + in: map[string]any{ + "a": &proto3pb.TestAllTypes_NestedMessage{ + Bb: 101, + }, + }, + out: types.OptionalOf(types.Int(101)), + }, { name: "select_relative", expr: `json('{"hi":"world"}').hi == 'world'`, @@ -1391,6 +1414,36 @@ func testData(t testing.TB) []testCase { unchecked: true, err: `no such attribute(s): goog.pkg.mylistundef, pkg.mylistundef`, }, + { + name: "unknown_attribute", + expr: `a[0]`, + vars: []*decls.VariableDecl{ + decls.NewVariable("a", + types.NewMapType(types.IntType, types.BoolType)), + }, + attrs: NewPartialAttributeFactory(testContainer(""), types.DefaultTypeAdapter, types.NewEmptyRegistry()), + in: newTestPartialActivation(t, map[string]any{ + "a": map[int64]any{ + 1: true, + }, + }, NewAttributePattern("a").QualInt(0)), + out: types.NewUnknown(2, types.QualifyAttribute[int64](types.NewAttributeTrail("a"), 0)), + }, + { + name: "unknown_attribute_mixed_qualifier", + expr: `a[dyn(0u)]`, + vars: []*decls.VariableDecl{ + decls.NewVariable("a", + types.NewMapType(types.IntType, types.BoolType)), + }, + attrs: NewPartialAttributeFactory(testContainer(""), types.DefaultTypeAdapter, types.NewEmptyRegistry()), + in: newTestPartialActivation(t, map[string]any{ + "a": map[int64]any{ + 1: true, + }, + }, NewAttributePattern("a").QualInt(0)), + out: types.NewUnknown(2, types.QualifyAttribute[uint64](types.NewAttributeTrail("a"), 0)), + }, } } @@ -1450,7 +1503,7 @@ func TestInterpreter(t *testing.T) { want = tc.out.(ref.Val) } got := prg.Eval(vars) - _, expectUnk := want.(types.Unknown) + _, expectUnk := want.(*types.Unknown) if expectUnk { if !reflect.DeepEqual(got, want) { t.Fatalf("Got %v, wanted %v", got, want) @@ -1486,7 +1539,7 @@ func TestInterpreter(t *testing.T) { } t.Run(mode, func(t *testing.T) { got := prg.Eval(vars) - _, expectUnk := want.(types.Unknown) + _, expectUnk := want.(*types.Unknown) if expectUnk { if !reflect.DeepEqual(got, want) { t.Errorf("Got %v, wanted %v", got, want) @@ -1956,7 +2009,10 @@ func program(ctx testing.TB, tst *testCase, opts ...InterpretableDecorator) (Int // Configure the program input. vars := EmptyActivation() if tst.in != nil { - vars, _ = NewActivation(tst.in) + vars, err = NewActivation(tst.in) + if err != nil { + ctx.Fatalf("NewActivation(%v) failed: %v", tst.in, err) + } } // Adapt the test output, if needed. if tst.out != nil { @@ -2060,6 +2116,15 @@ func newTestRegistry(t testing.TB, msgs ...proto.Message) *types.Registry { return reg } +func newTestPartialActivation(t testing.TB, in any, unknowns ...*AttributePattern) any { + t.Helper() + vars, err := NewPartialActivation(in, unknowns...) + if err != nil { + t.Fatalf("NewPartialActivation(%v) failed: %v", in, err) + } + return vars +} + // newStandardInterpreter builds a Dispatcher and TypeProvider with support for all of the CEL // builtins defined in the language definition. func newStandardInterpreter(t *testing.T, @@ -2108,39 +2173,3 @@ func funcBindings(t testing.TB, funcs ...*decls.FunctionDecl) []*functions.Overl } return bindings } - -func funcExprDecl(t testing.TB, fn *decls.FunctionDecl) *exprpb.Decl { - t.Helper() - d, err := decls.FunctionDeclToExprDecl(fn) - if err != nil { - t.Fatalf("decls.FunctionDeclToExprDecl(%v) failed: %v", fn, err) - } - return d -} - -func funcExprDecls(t testing.TB, funcs ...*decls.FunctionDecl) []*exprpb.Decl { - t.Helper() - d := make([]*exprpb.Decl, 0, len(funcs)) - for _, fn := range funcs { - d = append(d, funcExprDecl(t, fn)) - } - return d -} - -func varExprDecl(t testing.TB, v *decls.VariableDecl) *exprpb.Decl { - t.Helper() - d, err := decls.VariableDeclToExprDecl(v) - if err != nil { - t.Fatalf("decls.VariableDeclToExprDecl(%v) failed: %v", v, err) - } - return d -} - -func varExprDecls(t testing.TB, vars ...*decls.VariableDecl) []*exprpb.Decl { - t.Helper() - d := make([]*exprpb.Decl, 0, len(vars)) - for _, v := range vars { - d = append(d, varExprDecl(t, v)) - } - return d -} diff --git a/interpreter/planner.go b/interpreter/planner.go index 380fd732..757cd080 100644 --- a/interpreter/planner.go +++ b/interpreter/planner.go @@ -473,7 +473,7 @@ func (p *planner) planCallConditional(expr *exprpb.Expr, args []Interpretable) ( func (p *planner) planCallIndex(expr *exprpb.Expr, args []Interpretable, optional bool) (Interpretable, error) { op := args[0] ind := args[1] - opType := p.typeMap[expr.GetCallExpr().GetTarget().GetId()] + opType := p.typeMap[op.ID()] // Establish the attribute reference. var err error diff --git a/server/server.go b/server/server.go index 1a1b7968..de1d97a2 100644 --- a/server/server.go +++ b/server/server.go @@ -202,7 +202,14 @@ func ExprValueToRefValue(adapter types.Adapter, ev *exprpb.ExprValue) (ref.Val, // TODO(jimlarson) make a convention for this. return types.NewErr("XXX add details later"), nil case *exprpb.ExprValue_Unknown: - return types.Unknown(ev.GetUnknown().Exprs), nil + var unk *types.Unknown + for _, id := range ev.GetUnknown().GetExprs() { + if unk == nil { + unk = types.NewUnknown(id, nil) + } + unk = types.MergeUnknowns(types.NewUnknown(id, nil), unk) + } + return unk, nil } return nil, invalidArgument("unknown ExprValue kind") }