Skip to content

Commit 226484d

Browse files
l46kokcopybara-github
authored andcommitted
Fix type unification for type parameters
PiperOrigin-RevId: 979344413
1 parent 2db56dd commit 226484d

2 files changed

Lines changed: 346 additions & 2 deletions

File tree

checker/src/main/java/dev/cel/checker/Types.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,19 @@ private static boolean isTypeParam(CelType type) {
205205
return type.kind().equals(CelKind.TYPE_PARAM);
206206
}
207207

208+
/** Tests whether the {@code type} contains any type params directly or transitively. */
209+
private static boolean hasTypeParam(CelType type) {
210+
if (isTypeParam(type)) {
211+
return true;
212+
}
213+
for (CelType param : type.parameters()) {
214+
if (hasTypeParam(param)) {
215+
return true;
216+
}
217+
}
218+
return false;
219+
}
220+
208221
/** Returns the more general of two types which are known to unify. */
209222
public static CelType mostGeneral(CelType type1, CelType type2) {
210223
return isEqualOrLessSpecific(type1, type2) ? type1 : type2;
@@ -332,8 +345,21 @@ private static boolean internalIsAssignable(
332345

333346
switch (type1.kind()) {
334347
case TYPE:
335-
// A type is a type is a type, any additional parameterization of the type cannot affect
336-
// method resolution or assignability.
348+
if (!(type1 instanceof TypeType) || !(type2 instanceof TypeType)) {
349+
return type2.isAssignableFrom(type1);
350+
}
351+
TypeType fromType = (TypeType) type1;
352+
TypeType toType = (TypeType) type2;
353+
// If either type contains a type parameter (e.g., type(T) in foo(data, type(T)) -> T),
354+
// delegate to inner type unification to bind or validate type parameter substitutions.
355+
// Returns true if the inner types structurally match, unify with an unbound type param,
356+
// or conform to an existing binding in 'subs'. Returns false on structural/kind mismatches
357+
// (e.g., int vs list(T)), occurs-check cycles, or conflicting type param bindings.
358+
359+
if (hasTypeParam(fromType.type()) || hasTypeParam(toType.type())) {
360+
return internalIsAssignable(subs, fromType.type(), toType.type());
361+
}
362+
// Concrete types are coassignable in CEL (e.g., type(1) == type("a"), type([1]) == list).
337363
return true;
338364
case OPAQUE:
339365
case LIST:

checker/src/test/java/dev/cel/checker/TypesTest.java

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,21 @@
1818

1919
import dev.cel.expr.Type;
2020
import dev.cel.expr.Type.PrimitiveType;
21+
import dev.cel.common.CelAbstractSyntaxTree;
22+
import dev.cel.common.CelFunctionDecl;
23+
import dev.cel.common.CelOverloadDecl;
2124
import dev.cel.common.types.CelKind;
2225
import dev.cel.common.types.CelProtoTypes;
2326
import dev.cel.common.types.CelType;
27+
import dev.cel.common.types.ListType;
28+
import dev.cel.common.types.MapType;
29+
import dev.cel.common.types.NullableType;
30+
import dev.cel.common.types.OptionalType;
2431
import dev.cel.common.types.SimpleType;
32+
import dev.cel.common.types.TypeParamType;
33+
import dev.cel.common.types.TypeType;
34+
import dev.cel.compiler.CelCompiler;
35+
import dev.cel.compiler.CelCompilerFactory;
2536
import java.util.HashMap;
2637
import java.util.Map;
2738
import org.junit.Test;
@@ -54,6 +65,313 @@ public void isAssignable_usingCustomTypes() {
5465
assertThat(Types.isAssignable(subs, customType, intType)).isNull();
5566
}
5667

68+
@Test
69+
public void isAssignable_typeType_concreteTypes_legacyCoassignability() {
70+
Map<CelType, CelType> subs = new HashMap<>();
71+
CelType intType = TypeType.create(SimpleType.INT);
72+
CelType stringType = TypeType.create(SimpleType.STRING);
73+
74+
Map<CelType, CelType> result1 = Types.isAssignable(subs, intType, stringType);
75+
Map<CelType, CelType> result2 = Types.isAssignable(subs, stringType, intType);
76+
77+
// Concrete types are coassignable in CEL (e.g. for equality comparison type(1) == type("a"))
78+
assertThat(result1).isEmpty();
79+
assertThat(result2).isEmpty();
80+
}
81+
82+
@Test
83+
public void isAssignable_typeType_mapContainerErasure() {
84+
Map<CelType, CelType> subs = new HashMap<>();
85+
CelType mapIntUint = TypeType.create(MapType.create(SimpleType.INT, SimpleType.UINT));
86+
CelType mapDynDyn = TypeType.create(MapType.create(SimpleType.DYN, SimpleType.DYN));
87+
88+
Map<CelType, CelType> result = Types.isAssignable(subs, mapIntUint, mapDynDyn);
89+
90+
// type({1: 2u}) == map
91+
assertThat(result).isEmpty();
92+
}
93+
94+
@Test
95+
public void isAssignable_typeType_listContainerErasure() {
96+
Map<CelType, CelType> subs = new HashMap<>();
97+
CelType listInt = TypeType.create(ListType.create(SimpleType.INT));
98+
CelType listDyn = TypeType.create(ListType.create(SimpleType.DYN));
99+
100+
Map<CelType, CelType> result = Types.isAssignable(subs, listInt, listDyn);
101+
102+
// type([1]) == list
103+
assertThat(result).isEmpty();
104+
}
105+
106+
@Test
107+
public void isAssignable_typeType_typeParamTarget_bindsConcreteType() {
108+
Map<CelType, CelType> subs = new HashMap<>();
109+
TypeParamType typeParamT = TypeParamType.create("T");
110+
CelType fromType = TypeType.create(SimpleType.INT);
111+
CelType toType = TypeType.create(typeParamT);
112+
113+
Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);
114+
115+
assertThat(result).containsExactly(typeParamT, SimpleType.INT);
116+
}
117+
118+
@Test
119+
public void isAssignable_typeType_typeParamSource_bindsConcreteType() {
120+
Map<CelType, CelType> subs = new HashMap<>();
121+
TypeParamType typeParamT = TypeParamType.create("T");
122+
CelType fromType = TypeType.create(typeParamT);
123+
CelType toType = TypeType.create(SimpleType.INT);
124+
125+
Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);
126+
127+
assertThat(result).containsExactly(typeParamT, SimpleType.INT);
128+
}
129+
130+
@Test
131+
public void isAssignable_typeType_nestedTypeParam_unifies() {
132+
Map<CelType, CelType> subs = new HashMap<>();
133+
TypeParamType typeParamT = TypeParamType.create("T");
134+
TypeParamType typeParamR = TypeParamType.create("R");
135+
CelType fromType = TypeType.create(typeParamT);
136+
CelType toType = TypeType.create(TypeType.create(typeParamR));
137+
138+
// type(T) == type(type(R))
139+
Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);
140+
141+
assertThat(result).containsExactly(typeParamT, TypeType.create(typeParamR));
142+
}
143+
144+
@Test
145+
public void isAssignable_typeType_deeplyNestedTypeParam_bindsConcreteType() {
146+
Map<CelType, CelType> subs = new HashMap<>();
147+
TypeParamType typeParamT = TypeParamType.create("T");
148+
CelType fromType = TypeType.create(TypeType.create(SimpleType.INT));
149+
CelType toType = TypeType.create(TypeType.create(typeParamT));
150+
151+
Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);
152+
153+
assertThat(result).containsExactly(typeParamT, SimpleType.INT);
154+
}
155+
156+
@Test
157+
public void isAssignable_typeType_compositeListTypeParam_bindsConcreteType() {
158+
Map<CelType, CelType> subs = new HashMap<>();
159+
TypeParamType typeParamT = TypeParamType.create("T");
160+
CelType fromType = TypeType.create(ListType.create(SimpleType.INT));
161+
CelType toType = TypeType.create(ListType.create(typeParamT));
162+
163+
Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);
164+
165+
assertThat(result).containsExactly(typeParamT, SimpleType.INT);
166+
}
167+
168+
@Test
169+
public void isAssignable_typeType_compositeMapTypeParam_bindsConcreteTypes() {
170+
Map<CelType, CelType> subs = new HashMap<>();
171+
TypeParamType typeParamK = TypeParamType.create("K");
172+
TypeParamType typeParamV = TypeParamType.create("V");
173+
CelType fromType = TypeType.create(MapType.create(SimpleType.STRING, SimpleType.INT));
174+
CelType toType = TypeType.create(MapType.create(typeParamK, typeParamV));
175+
176+
Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);
177+
178+
assertThat(result).containsExactly(typeParamK, SimpleType.STRING, typeParamV, SimpleType.INT);
179+
}
180+
181+
@Test
182+
public void isAssignable_typeType_nullableTypeParam_unifies() {
183+
Map<CelType, CelType> subs = new HashMap<>();
184+
TypeParamType typeParamT = TypeParamType.create("T");
185+
CelType fromType = TypeType.create(NullableType.create(SimpleType.INT));
186+
CelType toType = TypeType.create(NullableType.create(typeParamT));
187+
188+
Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);
189+
190+
assertThat(result)
191+
.containsExactly(NullableType.create(typeParamT), NullableType.create(SimpleType.INT));
192+
}
193+
194+
@Test
195+
public void isAssignable_typeType_optionalTypeParam_unifies() {
196+
Map<CelType, CelType> subs = new HashMap<>();
197+
TypeParamType typeParamT = TypeParamType.create("T");
198+
CelType fromType = TypeType.create(OptionalType.create(SimpleType.INT));
199+
CelType toType = TypeType.create(OptionalType.create(typeParamT));
200+
201+
Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);
202+
203+
assertThat(result).containsExactly(typeParamT, SimpleType.INT);
204+
}
205+
206+
@Test
207+
public void isAssignable_typeType_incompatibleTypeParams_returnsNull() {
208+
Map<CelType, CelType> subs = new HashMap<>();
209+
TypeParamType typeParamT = TypeParamType.create("T");
210+
CelType fromType = TypeType.create(ListType.create(typeParamT));
211+
CelType toType = TypeType.create(SimpleType.INT);
212+
213+
Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);
214+
215+
assertThat(result).isNull();
216+
}
217+
218+
@Test
219+
public void isAssignable_typeType_conflictingBoundTypeParam_returnsNull() {
220+
Map<CelType, CelType> subs = new HashMap<>();
221+
TypeParamType typeParamT = TypeParamType.create("T");
222+
subs.put(typeParamT, SimpleType.STRING);
223+
CelType fromType = TypeType.create(typeParamT);
224+
CelType toType = TypeType.create(SimpleType.INT);
225+
226+
Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);
227+
228+
assertThat(result).isNull();
229+
}
230+
231+
@Test
232+
public void isAssignable_typeType_occursCheck_failsOnSelfReference() {
233+
Map<CelType, CelType> subs = new HashMap<>();
234+
TypeParamType typeParamT = TypeParamType.create("T");
235+
CelType fromType = TypeType.create(typeParamT);
236+
CelType toType = TypeType.create(TypeType.create(typeParamT));
237+
238+
// Occurs check: T = type(T) is cyclic and must fail
239+
Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);
240+
241+
assertThat(result).isNull();
242+
}
243+
244+
@Test
245+
public void isAssignable_typeType_occursCheck_failsOnTransitiveCycle() {
246+
Map<CelType, CelType> subs = new HashMap<>();
247+
TypeParamType typeParamT = TypeParamType.create("T");
248+
TypeParamType typeParamR = TypeParamType.create("R");
249+
subs.put(typeParamT, TypeType.create(typeParamR));
250+
// Trying to assign type(R) to type(T) would produce R = type(R) transitively through T
251+
CelType fromType = TypeType.create(typeParamR);
252+
CelType toType = TypeType.create(TypeType.create(typeParamT));
253+
254+
Map<CelType, CelType> result = Types.isAssignable(subs, fromType, toType);
255+
256+
assertThat(result).isNull();
257+
}
258+
259+
@Test
260+
public void compiler_typeParamInTypeType_resolvesReturnTypeInt() throws Exception {
261+
TypeParamType typeParamT = TypeParamType.create("T");
262+
CelCompiler celCompiler =
263+
CelCompilerFactory.standardCelCompilerBuilder()
264+
.addFunctionDeclarations(
265+
CelFunctionDecl.newFunctionDeclaration(
266+
"cast",
267+
CelOverloadDecl.newGlobalOverload(
268+
"cast_t", typeParamT, SimpleType.DYN, TypeType.create(typeParamT))))
269+
.build();
270+
271+
CelAbstractSyntaxTree ast = celCompiler.compile("cast('hello', int)").getAst();
272+
273+
assertThat(ast.getResultType()).isEqualTo(SimpleType.INT);
274+
}
275+
276+
@Test
277+
public void compiler_typeParamInTypeType_resolvesReturnTypeString() throws Exception {
278+
TypeParamType typeParamT = TypeParamType.create("T");
279+
CelCompiler celCompiler =
280+
CelCompilerFactory.standardCelCompilerBuilder()
281+
.addFunctionDeclarations(
282+
CelFunctionDecl.newFunctionDeclaration(
283+
"cast",
284+
CelOverloadDecl.newGlobalOverload(
285+
"cast_t", typeParamT, SimpleType.DYN, TypeType.create(typeParamT))))
286+
.build();
287+
288+
CelAbstractSyntaxTree ast = celCompiler.compile("cast(123, string)").getAst();
289+
290+
assertThat(ast.getResultType()).isEqualTo(SimpleType.STRING);
291+
}
292+
293+
@Test
294+
public void compiler_typeParamInCompositeTypeType_resolvesReturnType() throws Exception {
295+
TypeParamType typeParamT = TypeParamType.create("T");
296+
CelCompiler celCompiler =
297+
CelCompilerFactory.standardCelCompilerBuilder()
298+
.addFunctionDeclarations(
299+
CelFunctionDecl.newFunctionDeclaration(
300+
"first_elem_type",
301+
CelOverloadDecl.newGlobalOverload(
302+
"first_elem_type_overload",
303+
typeParamT,
304+
SimpleType.DYN,
305+
TypeType.create(ListType.create(typeParamT)))))
306+
.build();
307+
308+
CelAbstractSyntaxTree ast = celCompiler.compile("first_elem_type('data', type([1]))").getAst();
309+
310+
assertThat(ast.getResultType()).isEqualTo(SimpleType.INT);
311+
}
312+
313+
@Test
314+
public void compiler_typeComparison_mapType_succeeds() throws Exception {
315+
CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build();
316+
317+
CelAbstractSyntaxTree ast = celCompiler.compile("type({}) == map").getAst();
318+
319+
assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL);
320+
}
321+
322+
@Test
323+
public void compiler_typeComparison_compositeTypes_succeeds() throws Exception {
324+
CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build();
325+
326+
CelAbstractSyntaxTree ast =
327+
celCompiler.compile("list == type([1]) && map == type({1:2u})").getAst();
328+
329+
assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL);
330+
}
331+
332+
@Test
333+
public void compiler_typeComparison_differentTypesEqual_succeeds() throws Exception {
334+
CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build();
335+
336+
CelAbstractSyntaxTree ast = celCompiler.compile("type(1) == type('a')").getAst();
337+
338+
assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL);
339+
}
340+
341+
@Test
342+
public void compiler_typeComparison_differentTypesNotEqual_succeeds() throws Exception {
343+
CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build();
344+
345+
CelAbstractSyntaxTree ast = celCompiler.compile("type(1) != uint").getAst();
346+
347+
assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL);
348+
}
349+
350+
@Test
351+
public void compiler_typeComparison_type1NotEqualsType1u_succeeds() throws Exception {
352+
CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build();
353+
354+
CelAbstractSyntaxTree ast = celCompiler.compile("type(1) != type(1u)").getAst();
355+
356+
assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL);
357+
}
358+
359+
@Test
360+
public void compiler_typeParamEquality_unifiesTypeParams() throws Exception {
361+
TypeParamType typeParamT = TypeParamType.create("T");
362+
TypeParamType typeParamR = TypeParamType.create("R");
363+
CelCompiler celCompiler =
364+
CelCompilerFactory.standardCelCompilerBuilder()
365+
.addVar("x", TypeType.create(typeParamT))
366+
.addVar("y", TypeType.create(TypeType.create(typeParamR)))
367+
.build();
368+
369+
// type(T) == type(type(R))
370+
CelAbstractSyntaxTree ast = celCompiler.compile("x == y").getAst();
371+
372+
assertThat(ast.getResultType()).isEqualTo(SimpleType.BOOL);
373+
}
374+
57375
private static final class CustomCelType extends CelType {
58376

59377
@Override

0 commit comments

Comments
 (0)