Skip to content

Commit d88ea56

Browse files
laiyichincopybara-github
authored andcommitted
Fix CEL Java sortBy macro expansion to preserve element type and avoid heterogeneous list literals.
PiperOrigin-RevId: 979138470
1 parent e955f8e commit d88ea56

5 files changed

Lines changed: 211 additions & 61 deletions

File tree

extensions/src/main/java/dev/cel/extensions/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,8 @@ java_library(
274274
"//common/ast",
275275
"//common/internal:comparison_functions",
276276
"//common/types",
277+
"//common/types:type_providers",
278+
"//common/values:cel_byte_string",
277279
"//compiler:compiler_builder",
278280
"//extensions:extension_library",
279281
"//parser:macro",

extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java

Lines changed: 154 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static com.google.common.base.Preconditions.checkNotNull;
1919
import static com.google.common.collect.ImmutableSet.toImmutableSet;
2020

21+
import com.google.common.base.Ascii;
2122
import com.google.common.base.Preconditions;
2223
import com.google.common.collect.ImmutableList;
2324
import com.google.common.collect.ImmutableSet;
@@ -31,9 +32,11 @@
3132
import dev.cel.common.Operator;
3233
import dev.cel.common.ast.CelExpr;
3334
import dev.cel.common.internal.ComparisonFunctions;
35+
import dev.cel.common.types.CelType;
3436
import dev.cel.common.types.ListType;
3537
import dev.cel.common.types.SimpleType;
3638
import dev.cel.common.types.TypeParamType;
39+
import dev.cel.common.values.CelByteString;
3740
import dev.cel.compiler.CelCompilerLibrary;
3841
import dev.cel.parser.CelMacro;
3942
import dev.cel.parser.CelMacroExprFactory;
@@ -54,6 +57,10 @@
5457
public final class CelListsExtensions
5558
implements CelCompilerLibrary, CelInternalRuntimeLibrary, CelExtensionLibrary.FeatureSet {
5659

60+
private static final CelObjectComparator OBJECT_COMPARATOR = new CelObjectComparator();
61+
private static final String UNUSED_ITER_VAR = "#unused";
62+
private static final String SORT_BY_INPUT_VAR = "@__sortBy_input__";
63+
5764
/** Supported functions for Lists extension library. */
5865
@SuppressWarnings({"unchecked"}) // Unchecked: Type-checker guarantees casting safety.
5966
public enum Function {
@@ -131,17 +138,50 @@ public enum Function {
131138
ListType.create(TypeParamType.create("T")))),
132139
CelFunctionBinding.from("list_sort", Collection.class, CelListsExtensions::sort)),
133140
SORT_BY(
134-
CelFunctionDecl.newFunctionDeclaration(
135-
"lists.@sortByAssociatedKeys",
136-
CelOverloadDecl.newGlobalOverload(
137-
"list_sortByAssociatedKeys",
138-
"Sorts a list by a key value. Used by the 'sortBy' macro",
141+
createSortByFunctionDecl(comparableSortKeyTypes()),
142+
createSortByFunctionBindings(comparableSortKeyTypes()));
143+
144+
private static ImmutableList<CelType> comparableSortKeyTypes() {
145+
return ImmutableList.of(
146+
SimpleType.INT,
147+
SimpleType.UINT,
148+
SimpleType.DOUBLE,
149+
SimpleType.BOOL,
150+
SimpleType.STRING,
151+
SimpleType.BYTES,
152+
SimpleType.DURATION,
153+
SimpleType.TIMESTAMP);
154+
}
155+
156+
private static CelFunctionDecl createSortByFunctionDecl(ImmutableList<CelType> keyTypes) {
157+
ImmutableList.Builder<CelOverloadDecl> overloads = ImmutableList.builder();
158+
for (CelType type : keyTypes) {
159+
String typeName = Ascii.toLowerCase(type.kind().name());
160+
overloads.add(
161+
CelOverloadDecl.newMemberOverload(
162+
String.format("list_%s_sortByAssociatedKeys", typeName),
163+
"Sorts a list by an associated list of keys. Used by the 'sortBy' macro",
139164
ListType.create(TypeParamType.create("T")),
140-
ListType.create(TypeParamType.create("T")))),
141-
CelFunctionBinding.from(
142-
"list_sortByAssociatedKeys",
143-
Collection.class,
144-
CelListsExtensions::sortByAssociatedKeys));
165+
ListType.create(TypeParamType.create("T")),
166+
ListType.create(type)));
167+
}
168+
return CelFunctionDecl.newFunctionDeclaration("@sortByAssociatedKeys", overloads.build());
169+
}
170+
171+
private static CelFunctionBinding[] createSortByFunctionBindings(
172+
ImmutableList<CelType> keyTypes) {
173+
return keyTypes.stream()
174+
.map(
175+
type -> {
176+
String typeName = Ascii.toLowerCase(type.kind().name());
177+
return CelFunctionBinding.from(
178+
String.format("list_%s_sortByAssociatedKeys", typeName),
179+
Collection.class,
180+
Collection.class,
181+
CelListsExtensions::sortByAssociatedKeys);
182+
})
183+
.toArray(CelFunctionBinding[]::new);
184+
}
145185

146186
private final CelFunctionDecl functionDecl;
147187
private final ImmutableSet<CelFunctionBinding> functionBindings;
@@ -359,7 +399,15 @@ private static List<Object> reverse(Collection<Object> list) {
359399
}
360400

361401
private static ImmutableList<Object> sort(Collection<Object> objects) {
362-
return ImmutableList.sortedCopyOf(new CelObjectComparator(), objects);
402+
if (objects.isEmpty()) {
403+
return ImmutableList.of();
404+
}
405+
if (objects.size() == 1) {
406+
Object single = objects.iterator().next();
407+
OBJECT_COMPARATOR.compare(single, single);
408+
return ImmutableList.of(single);
409+
}
410+
return ImmutableList.sortedCopyOf(OBJECT_COMPARATOR, objects);
363411
}
364412

365413
private static class CelObjectComparator implements Comparator<Object> {
@@ -372,6 +420,10 @@ public int compare(Object o1, Object o2) {
372420
if (o1 instanceof Number && o2 instanceof Number) {
373421
return ComparisonFunctions.numericCompare((Number) o1, (Number) o2);
374422
}
423+
if (o1 instanceof CelByteString && o2 instanceof CelByteString) {
424+
return CelByteString.unsignedLexicographicalComparator()
425+
.compare((CelByteString) o1, (CelByteString) o2);
426+
}
375427

376428
if (!(o1 instanceof Comparable)) {
377429
throw new IllegalArgumentException("List elements must be comparable");
@@ -383,6 +435,34 @@ public int compare(Object o1, Object o2) {
383435
}
384436
}
385437

438+
/**
439+
* Expands the {@code list.sortBy(var, expr)} receiver macro into a binding expression that sorts
440+
* the target list using keys evaluated by mapping {@code expr} over each element.
441+
*
442+
* <p>For example, given:
443+
*
444+
* <pre>{@code
445+
* myList.sortBy(item, -item.field)
446+
* }</pre>
447+
*
448+
* <p>The macro expands into:
449+
*
450+
* <pre>{@code
451+
* cel.bind(@__sortBy_input__, myList,
452+
* @__sortBy_input__.@sortByAssociatedKeys(
453+
* @__sortBy_input__.map(item, -item.field)
454+
* )
455+
* )
456+
* }</pre>
457+
*
458+
* <p>Where:
459+
*
460+
* <ul>
461+
* <li>{@code @__sortBy_input__.map(item, -item.field)} evaluates the sort key for each element.
462+
* <li>{@code @sortByAssociatedKeys} stably sorts the input list elements based on their
463+
* corresponding sort keys.
464+
* </ul>
465+
*/
386466
private static Optional<CelExpr> sortByMacro(
387467
CelMacroExprFactory exprFactory, CelExpr target, ImmutableList<CelExpr> arguments) {
388468
checkNotNull(exprFactory);
@@ -400,56 +480,86 @@ private static Optional<CelExpr> sortByMacro(
400480
String varName = varIdent.ident().name();
401481
CelExpr sortKeyExpr = checkNotNull(arguments.get(1));
402482

403-
// Compute the key using the second argument of the `sortBy(e, key)` macro.
404-
// Combine the key and the value in a two-element list
405-
CelExpr step = exprFactory.newList(sortKeyExpr, varIdent);
406-
// Wrap the pair in another list in order to be able to use the `list+list` operator
407-
step = exprFactory.newList(step);
408-
// Append the key-value pair to the i
409-
step =
483+
// Build map comprehension: @__sortBy_input__.map(varName, sortKeyExpr)
484+
CelExpr targetIdent = exprFactory.newIdentifier(SORT_BY_INPUT_VAR);
485+
CelExpr mapStep =
410486
exprFactory.newGlobalCall(
411487
Operator.ADD.getFunction(),
412488
exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()),
413-
step);
414-
// Create an intermediate list and populate it with key-value pairs
415-
step =
489+
exprFactory.newList(sortKeyExpr));
490+
CelExpr mapCompr =
416491
exprFactory.fold(
417492
varName,
418-
target,
493+
targetIdent,
419494
exprFactory.getAccumulatorVarName(),
420495
exprFactory.newList(),
421-
exprFactory.newBoolLiteral(true), // Include all elements
422-
step,
496+
exprFactory.newBoolLiteral(true),
497+
mapStep,
423498
exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()));
424-
// Finally, sort the list of key-value pairs and map it to a list of values
425-
step = exprFactory.newGlobalCall(Function.SORT_BY.getFunction(), step);
426499

427-
return Optional.of(step);
500+
// Build call: @__sortBy_input__.@sortByAssociatedKeys(mapCompr)
501+
CelExpr callExpr =
502+
exprFactory.newReceiverCall(
503+
Function.SORT_BY.getFunction(), exprFactory.newIdentifier(SORT_BY_INPUT_VAR), mapCompr);
504+
505+
// Build bind: cel.bind(@__sortBy_input__, target, callExpr)
506+
CelExpr bindExpr =
507+
exprFactory.fold(
508+
UNUSED_ITER_VAR,
509+
exprFactory.newList(),
510+
SORT_BY_INPUT_VAR,
511+
target,
512+
exprFactory.newBoolLiteral(false),
513+
exprFactory.newIdentifier(SORT_BY_INPUT_VAR),
514+
callExpr);
515+
516+
return Optional.of(bindExpr);
428517
}
429518

430-
@SuppressWarnings({"unchecked", "rawtypes"})
519+
/**
520+
* Sorts elements of {@code list} based on the natural order of corresponding elements in {@code
521+
* keys}.
522+
*
523+
* <p>Both {@code list} and {@code keys} must have the exact same size. The sorting is stable
524+
* (i.e., preserves the relative order of elements with equal keys).
525+
*
526+
* @param list The input list to sort
527+
* @param keys The associated keys evaluated for each element in {@code list}
528+
* @return A new {@link ImmutableList} containing the elements of {@code list} sorted by {@code
529+
* keys}
530+
*/
431531
private static ImmutableList<Object> sortByAssociatedKeys(
432-
Collection<List<Object>> keyValuePairs) {
433-
List<Object>[] array = keyValuePairs.toArray(new List[0]);
434-
Arrays.sort(array, new CelObjectByKeyComparator(new CelObjectComparator()));
435-
ImmutableList.Builder<Object> builder = ImmutableList.builderWithExpectedSize(array.length);
436-
for (List<Object> pair : array) {
437-
builder.add(pair.get(1));
532+
Collection<Object> list, Collection<Object> keys) {
533+
checkArgument(
534+
list.size() == keys.size(),
535+
"@sortByAssociatedKeys() expected a list of the same size as the associated keys"
536+
+ " list, but got %s in list and %s in keys",
537+
list.size(),
538+
keys.size());
539+
540+
int listSize = list.size();
541+
if (listSize == 0) {
542+
return ImmutableList.of();
438543
}
439-
return builder.build();
440-
}
441544

442-
private static class CelObjectByKeyComparator implements Comparator<Object> {
443-
private final CelObjectComparator keyComparator;
545+
Object[] listArray = list.toArray();
546+
Object[] keysArray = keys.toArray();
547+
if (listSize == 1) {
548+
OBJECT_COMPARATOR.compare(keysArray[0], keysArray[0]);
549+
return ImmutableList.of(listArray[0]);
550+
}
444551

445-
CelObjectByKeyComparator(CelObjectComparator keyComparator) {
446-
this.keyComparator = keyComparator;
552+
Integer[] indices = new Integer[listSize];
553+
for (int i = 0; i < listSize; i++) {
554+
indices[i] = i;
447555
}
448556

449-
@SuppressWarnings({"unchecked"})
450-
@Override
451-
public int compare(Object o1, Object o2) {
452-
return keyComparator.compare(((List<Object>) o1).get(0), ((List<Object>) o2).get(0));
557+
Arrays.sort(indices, (i1, i2) -> OBJECT_COMPARATOR.compare(keysArray[i1], keysArray[i2]));
558+
559+
ImmutableList.Builder<Object> builder = ImmutableList.builderWithExpectedSize(listSize);
560+
for (int index : indices) {
561+
builder.add(listArray[index]);
453562
}
563+
return builder.build();
454564
}
455565
}

extensions/src/test/java/dev/cel/extensions/BUILD.bazel

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@ java_library(
4242
"//parser:unparser",
4343
"//runtime",
4444
"//runtime:function_binding",
45-
"//runtime:interpreter_util",
4645
"//runtime:lite_runtime",
4746
"//runtime:lite_runtime_factory",
4847
"//runtime:partial_vars",
4948
"//runtime:unknown_attributes",
5049
"//testing:cel_runtime_flavor",
50+
"//validator",
51+
"//validator:validator_builder",
52+
"//validator/validators:homogeneous_literal",
5153
"@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto",
5254
"@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto",
5355
"@cel_spec//proto/cel/expr/conformance/test:simple_java_proto",

extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ public void getAllFunctionNames() {
185185
"distinct",
186186
"reverse",
187187
"sort",
188-
"lists.@sortByAssociatedKeys",
188+
"@sortByAssociatedKeys",
189189
"regex.replace",
190190
"regex.extract",
191191
"regex.extractAll",

0 commit comments

Comments
 (0)