Skip to content
Merged
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
30 changes: 28 additions & 2 deletions checker/src/main/java/dev/cel/checker/Types.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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:
Expand Down
318 changes: 318 additions & 0 deletions checker/src/test/java/dev/cel/checker/TypesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,6 +65,313 @@ public void isAssignable_usingCustomTypes() {
assertThat(Types.isAssignable(subs, customType, intType)).isNull();
}

@Test
public void isAssignable_typeType_concreteTypes_legacyCoassignability() {
Map<CelType, CelType> subs = new HashMap<>();
CelType intType = TypeType.create(SimpleType.INT);
CelType stringType = TypeType.create(SimpleType.STRING);

Map<CelType, CelType> result1 = Types.isAssignable(subs, intType, stringType);
Map<CelType, CelType> 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<CelType, CelType> subs = new HashMap<>();
CelType mapIntUint = TypeType.create(MapType.create(SimpleType.INT, SimpleType.UINT));
CelType mapDynDyn = TypeType.create(MapType.create(SimpleType.DYN, SimpleType.DYN));

Map<CelType, CelType> result = Types.isAssignable(subs, mapIntUint, mapDynDyn);

// type({1: 2u}) == map
assertThat(result).isEmpty();
}

@Test
public void isAssignable_typeType_listContainerErasure() {
Map<CelType, CelType> subs = new HashMap<>();
CelType listInt = TypeType.create(ListType.create(SimpleType.INT));
CelType listDyn = TypeType.create(ListType.create(SimpleType.DYN));

Map<CelType, CelType> result = Types.isAssignable(subs, listInt, listDyn);

// type([1]) == list
assertThat(result).isEmpty();
}

@Test
public void isAssignable_typeType_typeParamTarget_bindsConcreteType() {
Map<CelType, CelType> subs = new HashMap<>();
TypeParamType typeParamT = TypeParamType.create("T");
CelType fromType = TypeType.create(SimpleType.INT);
CelType toType = TypeType.create(typeParamT);

Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);

assertThat(result).containsExactly(typeParamT, SimpleType.INT);
}

@Test
public void isAssignable_typeType_typeParamSource_bindsConcreteType() {
Map<CelType, CelType> subs = new HashMap<>();
TypeParamType typeParamT = TypeParamType.create("T");
CelType fromType = TypeType.create(typeParamT);
CelType toType = TypeType.create(SimpleType.INT);

Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);

assertThat(result).containsExactly(typeParamT, SimpleType.INT);
}

@Test
public void isAssignable_typeType_nestedTypeParam_unifies() {
Map<CelType, CelType> 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<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);

assertThat(result).containsExactly(typeParamT, TypeType.create(typeParamR));
}

@Test
public void isAssignable_typeType_deeplyNestedTypeParam_bindsConcreteType() {
Map<CelType, CelType> subs = new HashMap<>();
TypeParamType typeParamT = TypeParamType.create("T");
CelType fromType = TypeType.create(TypeType.create(SimpleType.INT));
CelType toType = TypeType.create(TypeType.create(typeParamT));

Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);

assertThat(result).containsExactly(typeParamT, SimpleType.INT);
}

@Test
public void isAssignable_typeType_compositeListTypeParam_bindsConcreteType() {
Map<CelType, CelType> subs = new HashMap<>();
TypeParamType typeParamT = TypeParamType.create("T");
CelType fromType = TypeType.create(ListType.create(SimpleType.INT));
CelType toType = TypeType.create(ListType.create(typeParamT));

Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);

assertThat(result).containsExactly(typeParamT, SimpleType.INT);
}

@Test
public void isAssignable_typeType_compositeMapTypeParam_bindsConcreteTypes() {
Map<CelType, CelType> 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<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);

assertThat(result).containsExactly(typeParamK, SimpleType.STRING, typeParamV, SimpleType.INT);
}

@Test
public void isAssignable_typeType_nullableTypeParam_unifies() {
Map<CelType, CelType> subs = new HashMap<>();
TypeParamType typeParamT = TypeParamType.create("T");
CelType fromType = TypeType.create(NullableType.create(SimpleType.INT));
CelType toType = TypeType.create(NullableType.create(typeParamT));

Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);

assertThat(result)
.containsExactly(NullableType.create(typeParamT), NullableType.create(SimpleType.INT));
}

@Test
public void isAssignable_typeType_optionalTypeParam_unifies() {
Map<CelType, CelType> subs = new HashMap<>();
TypeParamType typeParamT = TypeParamType.create("T");
CelType fromType = TypeType.create(OptionalType.create(SimpleType.INT));
CelType toType = TypeType.create(OptionalType.create(typeParamT));

Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);

assertThat(result).containsExactly(typeParamT, SimpleType.INT);
}

@Test
public void isAssignable_typeType_incompatibleTypeParams_returnsNull() {
Map<CelType, CelType> subs = new HashMap<>();
TypeParamType typeParamT = TypeParamType.create("T");
CelType fromType = TypeType.create(ListType.create(typeParamT));
CelType toType = TypeType.create(SimpleType.INT);

Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);

assertThat(result).isNull();
}

@Test
public void isAssignable_typeType_conflictingBoundTypeParam_returnsNull() {
Map<CelType, CelType> 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<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);

assertThat(result).isNull();
}

@Test
public void isAssignable_typeType_occursCheck_failsOnSelfReference() {
Map<CelType, CelType> 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<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);

assertThat(result).isNull();
}

@Test
public void isAssignable_typeType_occursCheck_failsOnTransitiveCycle() {
Map<CelType, CelType> 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<CelType, CelType> 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
Expand Down
Loading