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
247 changes: 247 additions & 0 deletions cel/cel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4921,3 +4921,250 @@ func TestNativeTypeForAndAlias(t *testing.T) {
t.Errorf("Eval() got %v, wanted true", out)
}
}

func TestTypeParameterInTypeType(t *testing.T) {
t.Run("type_param_in_type_type_int", func(t *testing.T) {
env, err := NewEnv(
Function("cast",
Overload("cast_val_to_type",
[]*Type{DynType, types.NewTypeTypeWithParam(TypeParamType("T"))},
TypeParamType("T"),
),
),
)
if err != nil {
t.Fatalf("NewEnv() failed: %v", err)
}
ast, iss := env.Compile("cast('hello', int)")
if iss.Err() != nil {
t.Fatalf("Compile() failed: %v", iss.Err())
}
if !ast.OutputType().IsExactType(IntType) {
t.Errorf("OutputType() = %v, wanted %v", ast.OutputType(), IntType)
}
})

t.Run("type_param_in_type_type_string", func(t *testing.T) {
env, err := NewEnv(
Function("cast",
Overload("cast_val_to_type",
[]*Type{DynType, types.NewTypeTypeWithParam(TypeParamType("T"))},
TypeParamType("T"),
),
),
)
if err != nil {
t.Fatalf("NewEnv() failed: %v", err)
}
ast, iss := env.Compile("cast(123, string)")
if iss.Err() != nil {
t.Fatalf("Compile() failed: %v", iss.Err())
}
if !ast.OutputType().IsExactType(StringType) {
t.Errorf("OutputType() = %v, wanted %v", ast.OutputType(), StringType)
}
})

t.Run("composite_type_param_in_type_type", func(t *testing.T) {
env, err := NewEnv(
Function("first_elem_type",
Overload("first_elem_type_list",
[]*Type{DynType, types.NewTypeTypeWithParam(ListType(TypeParamType("T")))},
TypeParamType("T"),
),
),
)
if err != nil {
t.Fatalf("NewEnv() failed: %v", err)
}
ast, iss := env.Compile("first_elem_type('data', type([1]))")
if iss.Err() != nil {
t.Fatalf("Compile() failed: %v", iss.Err())
}
if !ast.OutputType().IsExactType(IntType) {
t.Errorf("OutputType() = %v, wanted %v", ast.OutputType(), IntType)
}
})

t.Run("nested_type_param_in_type_type", func(t *testing.T) {
env, err := NewEnv(
Function("unwrap_type",
Overload("unwrap_type_t",
[]*Type{types.NewTypeTypeWithParam(types.NewTypeTypeWithParam(TypeParamType("T")))},
types.NewTypeTypeWithParam(TypeParamType("T")),
),
),
)
if err != nil {
t.Fatalf("NewEnv() failed: %v", err)
}
ast, iss := env.Compile("unwrap_type(type(int))")
if iss.Err() != nil {
t.Fatalf("Compile() failed: %v", iss.Err())
}
wantType := types.NewTypeTypeWithParam(IntType)
if !ast.OutputType().IsExactType(wantType) {
t.Errorf("OutputType() = %v, wanted %v", ast.OutputType(), wantType)
}
})

t.Run("type_map_erasure_comparison", func(t *testing.T) {
env, err := NewEnv()
if err != nil {
t.Fatalf("NewEnv() failed: %v", err)
}
ast, iss := env.Compile("type({}) == map")
if iss.Err() != nil {
t.Fatalf("Compile() failed: %v", iss.Err())
}
if !ast.OutputType().IsExactType(BoolType) {
t.Errorf("OutputType() = %v, wanted bool", ast.OutputType())
}
prg, err := env.Program(ast)
if err != nil {
t.Fatalf("Program() failed: %v", err)
}
out, _, err := prg.Eval(NoVars())
if err != nil {
t.Fatalf("Eval() failed: %v", err)
}
if out != types.True {
t.Errorf("Eval() = %v, wanted true", out)
}
})

t.Run("type_list_erasure_comparison", func(t *testing.T) {
env, err := NewEnv()
if err != nil {
t.Fatalf("NewEnv() failed: %v", err)
}
ast, iss := env.Compile("type([1]) == list")
if iss.Err() != nil {
t.Fatalf("Compile() failed: %v", iss.Err())
}
if !ast.OutputType().IsExactType(BoolType) {
t.Errorf("OutputType() = %v, wanted bool", ast.OutputType())
}
})

t.Run("composite_type_comparisons", func(t *testing.T) {
env, err := NewEnv()
if err != nil {
t.Fatalf("NewEnv() failed: %v", err)
}
ast, iss := env.Compile("list == type([1]) && map == type({1:2u})")
if iss.Err() != nil {
t.Fatalf("Compile() failed: %v", iss.Err())
}
if !ast.OutputType().IsExactType(BoolType) {
t.Errorf("OutputType() = %v, wanted bool", ast.OutputType())
}
})

t.Run("type_comparisons_between_different_types", func(t *testing.T) {
env, err := NewEnv()
if err != nil {
t.Fatalf("NewEnv() failed: %v", err)
}
for _, expr := range []string{
"type(1) == type('a')",
"type(1) != uint",
"type(1) != type(1u)",
"type(1) == type(1u)",
} {
ast, iss := env.Compile(expr)
if iss.Err() != nil {
t.Errorf("Compile(%q) failed: %v", expr, iss.Err())
} else if !ast.OutputType().IsExactType(BoolType) {
t.Errorf("Compile(%q) OutputType() = %v, wanted bool", expr, ast.OutputType())
}
}

// Verify evaluation of equality/inequality between different types
ast, iss := env.Compile("type(1) != type(1u)")
if iss.Err() != nil {
t.Fatalf("Compile() failed: %v", iss.Err())
}
prg, err := env.Program(ast)
if err != nil {
t.Fatalf("Program() failed: %v", err)
}
out, _, err := prg.Eval(NoVars())
if err != nil {
t.Fatalf("Eval() failed: %v", err)
}
if out != types.True {
t.Errorf("Eval() = %v, wanted true", out)
}

ast, iss = env.Compile("type(1) == type(1u)")
if iss.Err() != nil {
t.Fatalf("Compile() failed: %v", iss.Err())
}
prg, err = env.Program(ast)
if err != nil {
t.Fatalf("Program() failed: %v", err)
}
out, _, err = prg.Eval(NoVars())
if err != nil {
t.Fatalf("Eval() failed: %v", err)
}
if out != types.False {
t.Errorf("Eval() = %v, wanted false", out)
}
})

t.Run("type_param_equality_unifies_type_params", func(t *testing.T) {
env, err := NewEnv(
Variable("x", types.NewTypeTypeWithParam(TypeParamType("T"))),
Variable("y", types.NewTypeTypeWithParam(types.NewTypeTypeWithParam(TypeParamType("R")))),
)
if err != nil {
t.Fatalf("NewEnv() failed: %v", err)
}
ast, iss := env.Compile("x == y")
if iss.Err() != nil {
t.Fatalf("Compile() failed: %v", iss.Err())
}
if !ast.OutputType().IsExactType(BoolType) {
t.Errorf("OutputType() = %v, wanted bool", ast.OutputType())
}
})
}

func TestOptionalListTypePermutations(t *testing.T) {
env, err := NewEnv(OptionalTypes())
if err != nil {
t.Fatalf("NewEnv() failed: %v", err)
}
tests := []string{
"[type([]), int, type(optional.none())]",
"[type([]), type(optional.none()), int]",
"[int, type([]), type(optional.none())]",
"[int, type(optional.none()), type([])]",
"[type(optional.none()), type([]), int]",
"[type(optional.none()), int, type([])]",
}
wantListType := ListType(DynType)
for _, expr := range tests {
t.Run(expr, func(t *testing.T) {
ast, iss := env.Compile(expr)
if iss.Err() != nil {
t.Fatalf("Compile(%q) failed: %v", expr, iss.Err())
}
if !ast.OutputType().IsExactType(wantListType) {
t.Errorf("OutputType() = %v, wanted %v", ast.OutputType(), wantListType)
}
elements := ast.NativeRep().Expr().AsList().Elements()
if len(elements) != 3 {
t.Fatalf("len(elements) = %d, wanted 3", len(elements))
}
for i, elem := range elements {
elemType := ast.NativeRep().GetType(elem.ID())
if elemType == nil || elemType == types.ErrorType {
t.Errorf("element %d type = %v, wanted valid type", i, elemType)
}
}
})
}
}
1 change: 1 addition & 0 deletions checker/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ go_test(
"cost_test.go",
"env_test.go",
"format_test.go",
"types_test.go",
],
embed = [
":go_default_library",
Expand Down
48 changes: 46 additions & 2 deletions checker/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,35 @@ func isEqualOrLessSpecific(t1, t2 *types.Type) bool {
return isEqualOrLessSpecific(t1.Parameters()[0], t2.Parameters()[0]) &&
isEqualOrLessSpecific(t1.Parameters()[1], t2.Parameters()[1])
case types.TypeKind:
return true
p1Len, p2Len := len(t1.Parameters()), len(t2.Parameters())
if p1Len > 0 && p2Len > 0 {
if p1Len != p2Len {
return false
}
return isEqualOrLessSpecific(t1.Parameters()[0], t2.Parameters()[0])
}
return p1Len == 0
default:
return t1.IsExactType(t2)
}
}

// hasTypeParam returns true if the type is a type parameter or contains any type parameters directly or transitively.
func hasTypeParam(t *types.Type) bool {
if t == nil {
return false
}
if t.Kind() == types.TypeParamKind {
return true
}
for _, param := range t.Parameters() {
if hasTypeParam(param) {
return true
}
}
return false
}

// / internalIsAssignable returns true if t1 is assignable to t2.
func internalIsAssignable(m *mapping, t1, t2 *types.Type) bool {
// Process type parameters.
Expand Down Expand Up @@ -143,7 +166,28 @@ func internalIsAssignable(m *mapping, t1, t2 *types.Type) bool {
// Struct types.
return t2.IsAssignableType(t1)
case types.TypeKind:
return kind2 == types.TypeKind
if kind2 != types.TypeKind {
return false
}
p1Len, p2Len := len(t1.Parameters()), len(t2.Parameters())
if p1Len == 0 || p2Len == 0 {
return p2Len == 0
}
if p1Len != p2Len {
return false
}
fromType := t1.Parameters()[0]
Comment thread
l46kok marked this conversation as resolved.
toType := t2.Parameters()[0]
// If either type contains a type parameter (e.g., type(T) in foo(data, type(T)) -> T),
// delegate to inner type unification to bind or validate type parameter substitutions.
// Returns true if the inner types structurally match, unify with an unbound type param,
// or conform to an existing binding in 'm'. Returns false on structural/kind mismatches
// (e.g., int vs list(T)), occurs-check cycles, or conflicting type param bindings.
if hasTypeParam(fromType) || hasTypeParam(toType) {
return internalIsAssignable(m, fromType, toType)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there might be some issue with the occurs check with inputs like:

type(map(string, T)), type(T)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be ok, the type substitution check propagates for map parameters. I've added some tests around this just to be sure.

}
// Concrete types are coassignable in CEL (e.g., type(1) == type("a"), type([1]) == list).
return true
case types.OpaqueKind, types.ListKind, types.MapKind:
return t1.Kind() == t2.Kind() && t1.TypeName() == t2.TypeName() &&
internalIsAssignableList(m, t1.Parameters(), t2.Parameters())
Expand Down
Loading
Loading