1818import static com .google .common .base .Preconditions .checkNotNull ;
1919import static com .google .common .collect .ImmutableSet .toImmutableSet ;
2020
21+ import com .google .common .base .Ascii ;
2122import com .google .common .base .Preconditions ;
2223import com .google .common .collect .ImmutableList ;
2324import com .google .common .collect .ImmutableSet ;
3132import dev .cel .common .Operator ;
3233import dev .cel .common .ast .CelExpr ;
3334import dev .cel .common .internal .ComparisonFunctions ;
35+ import dev .cel .common .types .CelType ;
3436import dev .cel .common .types .ListType ;
3537import dev .cel .common .types .SimpleType ;
3638import dev .cel .common .types .TypeParamType ;
39+ import dev .cel .common .values .CelByteString ;
3740import dev .cel .compiler .CelCompilerLibrary ;
3841import dev .cel .parser .CelMacro ;
3942import dev .cel .parser .CelMacroExprFactory ;
5457public 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}
0 commit comments