diff --git a/checker/src/main/java/dev/cel/checker/Types.java b/checker/src/main/java/dev/cel/checker/Types.java index 4cc502cdf..f9b82ecb7 100644 --- a/checker/src/main/java/dev/cel/checker/Types.java +++ b/checker/src/main/java/dev/cel/checker/Types.java @@ -205,6 +205,19 @@ private static boolean isTypeParam(CelType type) { return type.kind().equals(CelKind.TYPE_PARAM); } + /** Tests whether the {@code type} contains any type params directly or transitively. */ + private static boolean hasTypeParam(CelType type) { + if (isTypeParam(type)) { + return true; + } + for (CelType param : type.parameters()) { + if (hasTypeParam(param)) { + return true; + } + } + return false; + } + /** Returns the more general of two types which are known to unify. */ public static CelType mostGeneral(CelType type1, CelType type2) { return isEqualOrLessSpecific(type1, type2) ? type1 : type2; @@ -332,8 +345,21 @@ private static boolean internalIsAssignable( switch (type1.kind()) { case TYPE: - // A type is a type is a type, any additional parameterization of the type cannot affect - // method resolution or assignability. + if (!(type1 instanceof TypeType) || !(type2 instanceof TypeType)) { + return type2.isAssignableFrom(type1); + } + TypeType fromType = (TypeType) type1; + TypeType toType = (TypeType) type2; + // 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 'subs'. Returns false on structural/kind mismatches + // (e.g., int vs list(T)), occurs-check cycles, or conflicting type param bindings. + + if (hasTypeParam(fromType.type()) || hasTypeParam(toType.type())) { + return internalIsAssignable(subs, fromType.type(), toType.type()); + } + // Concrete types are coassignable in CEL (e.g., type(1) == type("a"), type([1]) == list). return true; case OPAQUE: case LIST: diff --git a/checker/src/test/java/dev/cel/checker/TypesTest.java b/checker/src/test/java/dev/cel/checker/TypesTest.java index 960ebec3f..a8ca2167e 100644 --- a/checker/src/test/java/dev/cel/checker/TypesTest.java +++ b/checker/src/test/java/dev/cel/checker/TypesTest.java @@ -18,10 +18,21 @@ import dev.cel.expr.Type; import dev.cel.expr.Type.PrimitiveType; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOverloadDecl; import dev.cel.common.types.CelKind; import dev.cel.common.types.CelProtoTypes; import dev.cel.common.types.CelType; +import dev.cel.common.types.ListType; +import dev.cel.common.types.MapType; +import dev.cel.common.types.NullableType; +import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; +import dev.cel.common.types.TypeParamType; +import dev.cel.common.types.TypeType; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; import java.util.HashMap; import java.util.Map; import org.junit.Test; @@ -54,6 +65,313 @@ public void isAssignable_usingCustomTypes() { assertThat(Types.isAssignable(subs, customType, intType)).isNull(); } + @Test + public void isAssignable_typeType_concreteTypes_legacyCoassignability() { + Map subs = new HashMap<>(); + CelType intType = TypeType.create(SimpleType.INT); + CelType stringType = TypeType.create(SimpleType.STRING); + + Map result1 = Types.isAssignable(subs, intType, stringType); + Map result2 = Types.isAssignable(subs, stringType, intType); + + // Concrete types are coassignable in CEL (e.g. for equality comparison type(1) == type("a")) + assertThat(result1).isEmpty(); + assertThat(result2).isEmpty(); + } + + @Test + public void isAssignable_typeType_mapContainerErasure() { + Map subs = new HashMap<>(); + CelType mapIntUint = TypeType.create(MapType.create(SimpleType.INT, SimpleType.UINT)); + CelType mapDynDyn = TypeType.create(MapType.create(SimpleType.DYN, SimpleType.DYN)); + + Map result = Types.isAssignable(subs, mapIntUint, mapDynDyn); + + // type({1: 2u}) == map + assertThat(result).isEmpty(); + } + + @Test + public void isAssignable_typeType_listContainerErasure() { + Map subs = new HashMap<>(); + CelType listInt = TypeType.create(ListType.create(SimpleType.INT)); + CelType listDyn = TypeType.create(ListType.create(SimpleType.DYN)); + + Map result = Types.isAssignable(subs, listInt, listDyn); + + // type([1]) == list + assertThat(result).isEmpty(); + } + + @Test + public void isAssignable_typeType_typeParamTarget_bindsConcreteType() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(SimpleType.INT); + CelType toType = TypeType.create(typeParamT); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).containsExactly(typeParamT, SimpleType.INT); + } + + @Test + public void isAssignable_typeType_typeParamSource_bindsConcreteType() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(typeParamT); + CelType toType = TypeType.create(SimpleType.INT); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).containsExactly(typeParamT, SimpleType.INT); + } + + @Test + public void isAssignable_typeType_nestedTypeParam_unifies() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + TypeParamType typeParamR = TypeParamType.create("R"); + CelType fromType = TypeType.create(typeParamT); + CelType toType = TypeType.create(TypeType.create(typeParamR)); + + // type(T) == type(type(R)) + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).containsExactly(typeParamT, TypeType.create(typeParamR)); + } + + @Test + public void isAssignable_typeType_deeplyNestedTypeParam_bindsConcreteType() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(TypeType.create(SimpleType.INT)); + CelType toType = TypeType.create(TypeType.create(typeParamT)); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).containsExactly(typeParamT, SimpleType.INT); + } + + @Test + public void isAssignable_typeType_compositeListTypeParam_bindsConcreteType() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(ListType.create(SimpleType.INT)); + CelType toType = TypeType.create(ListType.create(typeParamT)); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).containsExactly(typeParamT, SimpleType.INT); + } + + @Test + public void isAssignable_typeType_compositeMapTypeParam_bindsConcreteTypes() { + Map subs = new HashMap<>(); + TypeParamType typeParamK = TypeParamType.create("K"); + TypeParamType typeParamV = TypeParamType.create("V"); + CelType fromType = TypeType.create(MapType.create(SimpleType.STRING, SimpleType.INT)); + CelType toType = TypeType.create(MapType.create(typeParamK, typeParamV)); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).containsExactly(typeParamK, SimpleType.STRING, typeParamV, SimpleType.INT); + } + + @Test + public void isAssignable_typeType_nullableTypeParam_unifies() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(NullableType.create(SimpleType.INT)); + CelType toType = TypeType.create(NullableType.create(typeParamT)); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result) + .containsExactly(NullableType.create(typeParamT), NullableType.create(SimpleType.INT)); + } + + @Test + public void isAssignable_typeType_optionalTypeParam_unifies() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(OptionalType.create(SimpleType.INT)); + CelType toType = TypeType.create(OptionalType.create(typeParamT)); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).containsExactly(typeParamT, SimpleType.INT); + } + + @Test + public void isAssignable_typeType_incompatibleTypeParams_returnsNull() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(ListType.create(typeParamT)); + CelType toType = TypeType.create(SimpleType.INT); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).isNull(); + } + + @Test + public void isAssignable_typeType_conflictingBoundTypeParam_returnsNull() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + subs.put(typeParamT, SimpleType.STRING); + CelType fromType = TypeType.create(typeParamT); + CelType toType = TypeType.create(SimpleType.INT); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).isNull(); + } + + @Test + public void isAssignable_typeType_occursCheck_failsOnSelfReference() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + CelType fromType = TypeType.create(typeParamT); + CelType toType = TypeType.create(TypeType.create(typeParamT)); + + // Occurs check: T = type(T) is cyclic and must fail + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).isNull(); + } + + @Test + public void isAssignable_typeType_occursCheck_failsOnTransitiveCycle() { + Map subs = new HashMap<>(); + TypeParamType typeParamT = TypeParamType.create("T"); + TypeParamType typeParamR = TypeParamType.create("R"); + subs.put(typeParamT, TypeType.create(typeParamR)); + // Trying to assign type(R) to type(T) would produce R = type(R) transitively through T + CelType fromType = TypeType.create(typeParamR); + CelType toType = TypeType.create(TypeType.create(typeParamT)); + + Map result = Types.isAssignable(subs, fromType, toType); + + assertThat(result).isNull(); + } + + @Test + public void compiler_typeParamInTypeType_resolvesReturnTypeInt() throws Exception { + TypeParamType typeParamT = TypeParamType.create("T"); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "cast", + CelOverloadDecl.newGlobalOverload( + "cast_t", typeParamT, SimpleType.DYN, TypeType.create(typeParamT)))) + .build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("cast('hello', int)").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.INT); + } + + @Test + public void compiler_typeParamInTypeType_resolvesReturnTypeString() throws Exception { + TypeParamType typeParamT = TypeParamType.create("T"); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "cast", + CelOverloadDecl.newGlobalOverload( + "cast_t", typeParamT, SimpleType.DYN, TypeType.create(typeParamT)))) + .build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("cast(123, string)").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.STRING); + } + + @Test + public void compiler_typeParamInCompositeTypeType_resolvesReturnType() throws Exception { + TypeParamType typeParamT = TypeParamType.create("T"); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addFunctionDeclarations( + CelFunctionDecl.newFunctionDeclaration( + "first_elem_type", + CelOverloadDecl.newGlobalOverload( + "first_elem_type_overload", + typeParamT, + SimpleType.DYN, + TypeType.create(ListType.create(typeParamT))))) + .build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("first_elem_type('data', type([1]))").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.INT); + } + + @Test + public void compiler_typeComparison_mapType_succeeds() throws Exception { + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("type({}) == map").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + + @Test + public void compiler_typeComparison_compositeTypes_succeeds() throws Exception { + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + + CelAbstractSyntaxTree ast = + celCompiler.compile("list == type([1]) && map == type({1:2u})").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + + @Test + public void compiler_typeComparison_differentTypesEqual_succeeds() throws Exception { + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("type(1) == type('a')").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + + @Test + public void compiler_typeComparison_differentTypesNotEqual_succeeds() throws Exception { + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("type(1) != uint").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + + @Test + public void compiler_typeComparison_type1NotEqualsType1u_succeeds() throws Exception { + CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + + CelAbstractSyntaxTree ast = celCompiler.compile("type(1) != type(1u)").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + + @Test + public void compiler_typeParamEquality_unifiesTypeParams() throws Exception { + TypeParamType typeParamT = TypeParamType.create("T"); + TypeParamType typeParamR = TypeParamType.create("R"); + CelCompiler celCompiler = + CelCompilerFactory.standardCelCompilerBuilder() + .addVar("x", TypeType.create(typeParamT)) + .addVar("y", TypeType.create(TypeType.create(typeParamR))) + .build(); + + // type(T) == type(type(R)) + CelAbstractSyntaxTree ast = celCompiler.compile("x == y").getAst(); + + assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL); + } + private static final class CustomCelType extends CelType { @Override