From 5b27615315b1dbbfb6b0704e79329431d67f6e51 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Thu, 16 Jul 2026 16:02:21 +0200 Subject: [PATCH 1/3] Specialize macro list folds Recognize map and filter macro fold shapes during planning and build result lists with a mutable evaluation-local buffer. This avoids repeated accumulator list copying while preserving generic fold fallback for non-standard accumulator references. --- .../cel/interpreter/Interpretable.java | 109 +++++++++++++ .../cel/interpreter/InterpretablePlanner.java | 144 ++++++++++++++++++ .../cel/interpreter/InterpreterTest.java | 4 +- 3 files changed, 255 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java b/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java index 5983a792..0588e042 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java @@ -28,8 +28,10 @@ import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; import static org.projectnessie.cel.interpreter.Coster.costOf; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -37,6 +39,7 @@ import org.projectnessie.cel.common.types.Err; import org.projectnessie.cel.common.types.IterableT; import org.projectnessie.cel.common.types.IteratorT; +import org.projectnessie.cel.common.types.ListT; import org.projectnessie.cel.common.types.MapT; import org.projectnessie.cel.common.types.Overloads; import org.projectnessie.cel.common.types.StringT; @@ -49,6 +52,7 @@ import org.projectnessie.cel.common.types.traits.FieldTester; import org.projectnessie.cel.common.types.traits.Negater; import org.projectnessie.cel.common.types.traits.Receiver; +import org.projectnessie.cel.common.types.traits.Sizer; import org.projectnessie.cel.common.types.traits.Trait; import org.projectnessie.cel.interpreter.Activation.VarActivation; import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; @@ -1108,6 +1112,111 @@ public String toString() { } } + final class EvalListFold extends AbstractEval implements Coster { + final String iterVar; + final Interpretable iterRange; + final Interpretable filter; + final Interpretable transform; + private final TypeAdapter adapter; + + EvalListFold( + long id, + String iterVar, + Interpretable iterRange, + Interpretable filter, + Interpretable transform, + TypeAdapter adapter) { + super(id); + this.iterVar = iterVar; + this.iterRange = iterRange; + this.filter = filter; + this.transform = transform; + this.adapter = adapter; + } + + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val foldRange = iterRange.eval(ctx); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return valOrErr( + foldRange, "got '%s', expected iterable type", foldRange.getClass().getName()); + } + + VarActivation iterCtx = new VarActivation(); + iterCtx.parent = ctx; + iterCtx.name = iterVar; + List values = new ArrayList<>(listCapacity(foldRange)); + IteratorT it = ((IterableT) foldRange).iterator(); + while (it.hasNext() == True) { + iterCtx.val = it.next(); + + if (filter != null) { + Val include = filter.eval(iterCtx); + if (include == False) { + continue; + } + if (include != True) { + return noSuchOverload(null, Operator.Conditional.id, include); + } + } + + Val value = transform.eval(iterCtx); + if (isUnknownOrError(value)) { + return value; + } + values.add(value); + } + return ListT.newValArrayList(adapter, values.toArray(new Val[0])); + } + + private int listCapacity(Val foldRange) { + if (foldRange.type().hasTrait(Trait.SizerType)) { + long size = ((Sizer) foldRange).size().intValue(); + if (size > 0 && size <= Integer.MAX_VALUE) { + return (int) size; + } + } + return 0; + } + + @Override + public Cost cost() { + Cost range = estimateCost(iterRange); + Cost result = estimateCost(transform); + if (filter != null) { + result = result.add(estimateCost(filter)); + } + Val foldRange = iterRange.eval(emptyActivation()); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return Cost.Unknown; + } + long rangeCnt = 0L; + IteratorT it = ((IterableT) foldRange).iterator(); + while (it.hasNext() == True) { + it.next(); + rangeCnt++; + } + return range.add(result.multiply(rangeCnt)); + } + + @Override + public String toString() { + return "EvalListFold{" + + "id=" + + id + + ", iterVar='" + + iterVar + + '\'' + + ", iterRange=" + + iterRange + + ", filter=" + + filter + + ", transform=" + + transform + + '}'; + } + } + // Optional Intepretable implementations that specialize, subsume, or extend the core evaluation // plan via decorators. diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java index 910e8de8..818e459a 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java @@ -56,6 +56,7 @@ import org.projectnessie.cel.interpreter.Interpretable.EvalEq; import org.projectnessie.cel.interpreter.Interpretable.EvalFold; import org.projectnessie.cel.interpreter.Interpretable.EvalList; +import org.projectnessie.cel.interpreter.Interpretable.EvalListFold; import org.projectnessie.cel.interpreter.Interpretable.EvalMap; import org.projectnessie.cel.interpreter.Interpretable.EvalNe; import org.projectnessie.cel.interpreter.Interpretable.EvalObj; @@ -591,6 +592,27 @@ Interpretable planCreateObj(Expr expr) { /** planComprehension generates an Interpretable fold operation. */ Interpretable planComprehension(Expr expr) { Comprehension fold = expr.getComprehensionExpr(); + MacroListFold macroListFold = macroListFold(fold); + if (macroListFold != null) { + Interpretable iterRange = plan(fold.getIterRange()); + if (iterRange == null) { + return null; + } + Interpretable filter = null; + if (macroListFold.filter != null) { + filter = plan(macroListFold.filter); + if (filter == null) { + return null; + } + } + Interpretable transform = plan(macroListFold.transform); + if (transform == null) { + return null; + } + return new EvalListFold( + expr.getId(), fold.getIterVar(), iterRange, filter, transform, adapter); + } + Interpretable accu = plan(fold.getAccuInit()); if (accu == null) { return null; @@ -615,6 +637,128 @@ Interpretable planComprehension(Expr expr) { expr.getId(), fold.getAccuVar(), accu, fold.getIterVar(), iterRange, cond, step, result); } + private MacroListFold macroListFold(Comprehension fold) { + if (!isEmptyList(fold.getAccuInit()) + || !isBoolConst(fold.getLoopCondition(), true) + || !isIdent(fold.getResult(), fold.getAccuVar())) { + return null; + } + + Expr step = fold.getLoopStep(); + Expr filter = null; + if (isCall(step, Operator.Conditional.id, 3)) { + Call conditional = step.getCallExpr(); + if (!isIdent(conditional.getArgs(2), fold.getAccuVar())) { + return null; + } + filter = conditional.getArgs(0); + step = conditional.getArgs(1); + } + + Expr transform = appendedValue(fold.getAccuVar(), step); + if (transform == null + || referencesIdent(transform, fold.getAccuVar()) + || (filter != null && referencesIdent(filter, fold.getAccuVar()))) { + return null; + } + return new MacroListFold(filter, transform); + } + + private Expr appendedValue(String accuVar, Expr step) { + if (!isCall(step, Operator.Add.id, 2)) { + return null; + } + Call add = step.getCallExpr(); + if (!isIdent(add.getArgs(0), accuVar)) { + return null; + } + Expr list = add.getArgs(1); + if (list.getExprKindCase() != Expr.ExprKindCase.LIST_EXPR + || list.getListExpr().getElementsCount() != 1) { + return null; + } + return list.getListExpr().getElements(0); + } + + private boolean isCall(Expr expr, String function, int argCount) { + return expr.getExprKindCase() == Expr.ExprKindCase.CALL_EXPR + && expr.getCallExpr().getFunction().equals(function) + && expr.getCallExpr().getArgsCount() == argCount + && !expr.getCallExpr().hasTarget(); + } + + private boolean isIdent(Expr expr, String name) { + return expr.getExprKindCase() == Expr.ExprKindCase.IDENT_EXPR + && expr.getIdentExpr().getName().equals(name); + } + + private boolean isEmptyList(Expr expr) { + return expr.getExprKindCase() == Expr.ExprKindCase.LIST_EXPR + && expr.getListExpr().getElementsCount() == 0; + } + + private boolean isBoolConst(Expr expr, boolean value) { + return expr.getExprKindCase() == Expr.ExprKindCase.CONST_EXPR + && expr.getConstExpr().getConstantKindCase() == Constant.ConstantKindCase.BOOL_VALUE + && expr.getConstExpr().getBoolValue() == value; + } + + private boolean referencesIdent(Expr expr, String name) { + switch (expr.getExprKindCase()) { + case IDENT_EXPR: + return expr.getIdentExpr().getName().equals(name); + case SELECT_EXPR: + return referencesIdent(expr.getSelectExpr().getOperand(), name); + case CALL_EXPR: + Call call = expr.getCallExpr(); + if (call.hasTarget() && referencesIdent(call.getTarget(), name)) { + return true; + } + for (Expr arg : call.getArgsList()) { + if (referencesIdent(arg, name)) { + return true; + } + } + return false; + case LIST_EXPR: + for (Expr elem : expr.getListExpr().getElementsList()) { + if (referencesIdent(elem, name)) { + return true; + } + } + return false; + case STRUCT_EXPR: + for (Entry entry : expr.getStructExpr().getEntriesList()) { + if (referencesIdent(entry.getValue(), name)) { + return true; + } + } + return false; + case COMPREHENSION_EXPR: + Comprehension comprehension = expr.getComprehensionExpr(); + return referencesIdent(comprehension.getIterRange(), name) + || referencesIdent(comprehension.getAccuInit(), name) + || (!comprehension.getIterVar().equals(name) + && referencesIdent(comprehension.getLoopCondition(), name)) + || (!comprehension.getIterVar().equals(name) + && referencesIdent(comprehension.getLoopStep(), name)) + || (!comprehension.getAccuVar().equals(name) + && referencesIdent(comprehension.getResult(), name)); + default: + return false; + } + } + + private final class MacroListFold { + final Expr filter; + final Expr transform; + + private MacroListFold(Expr filter, Expr transform) { + this.filter = filter; + this.transform = transform; + } + } + /** planConst generates a constant valued Interpretable. */ Interpretable planConst(Expr expr) { Val val = constValue(expr.getConstExpr()); diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java index 260f7a47..ca3795e8 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java @@ -809,8 +809,8 @@ static TestCase[] testCases() { .exhaustiveCost(costOf(35, 35)), new TestCase(InterpreterTestCase.macro_map) .expr("[1, 2, 3].map(x, x * 2) == [2, 4, 6]") - .cost(costOf(6, 14)) - .exhaustiveCost(costOf(14, 14)), + .cost(costOf(7, 7)) + .exhaustiveCost(costOf(7, 7)), new TestCase(InterpreterTestCase.matches) .expr( "input.matches('k.*') \n" From 142501f41dad07e815697d1a26fbf203308ca1f3 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Wed, 22 Jul 2026 12:50:15 +0200 Subject: [PATCH 2/3] review / static --- .../cel/interpreter/InterpretablePlanner.java | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java index 818e459a..e2bf4f7d 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java @@ -135,7 +135,7 @@ static InterpretablePlanner newUncheckedPlanner( } /** planner is an implementatio of the interpretablePlanner interface. */ - final class Planner implements InterpretablePlanner { + static final class Planner implements InterpretablePlanner { private final Dispatcher disp; private final TypeProvider provider; private final TypeAdapter adapter; @@ -389,7 +389,7 @@ Interpretable planCall(Expr expr) { } /** planCallZero generates a zero-arity callable Interpretable. */ - Interpretable planCallZero(Expr expr, String function, String overload, Overload impl) { + static Interpretable planCallZero(Expr expr, String function, String overload, Overload impl) { if (impl == null || impl.function == null) { throw new IllegalArgumentException(String.format("no such overload: %s()", function)); } @@ -397,7 +397,7 @@ Interpretable planCallZero(Expr expr, String function, String overload, Overload } /** planCallUnary generates a unary callable Interpretable. */ - Interpretable planCallUnary( + static Interpretable planCallUnary( Expr expr, String function, String overload, Overload impl, Interpretable[] args) { UnaryOp fn = null; Trait trait = null; @@ -412,7 +412,7 @@ Interpretable planCallUnary( } /** planCallBinary generates a binary callable Interpretable. */ - Interpretable planCallBinary( + static Interpretable planCallBinary( Expr expr, String function, String overload, Overload impl, Interpretable... args) { BinaryOp fn = null; Trait trait = null; @@ -428,7 +428,7 @@ Interpretable planCallBinary( } /** planCallVarArgs generates a variable argument callable Interpretable. */ - Interpretable planCallVarArgs( + static Interpretable planCallVarArgs( Expr expr, String function, String overload, Overload impl, Interpretable... args) { FunctionOp fn = null; Trait trait = null; @@ -443,22 +443,22 @@ Interpretable planCallVarArgs( } /** planCallEqual generates an equals (==) Interpretable. */ - Interpretable planCallEqual(Expr expr, Interpretable... args) { + static Interpretable planCallEqual(Expr expr, Interpretable... args) { return new EvalEq(expr.getId(), args[0], args[1]); } /** planCallNotEqual generates a not equals (!=) Interpretable. */ - Interpretable planCallNotEqual(Expr expr, Interpretable... args) { + static Interpretable planCallNotEqual(Expr expr, Interpretable... args) { return new EvalNe(expr.getId(), args[0], args[1]); } /** planCallLogicalAnd generates a logical and (&&) Interpretable. */ - Interpretable planCallLogicalAnd(Expr expr, Interpretable... args) { + static Interpretable planCallLogicalAnd(Expr expr, Interpretable... args) { return new EvalAnd(expr.getId(), args[0], args[1]); } /** planCallLogicalOr generates a logical or (||) Interpretable. */ - Interpretable planCallLogicalOr(Expr expr, Interpretable... args) { + static Interpretable planCallLogicalOr(Expr expr, Interpretable... args) { return new EvalOr(expr.getId(), args[0], args[1]); } @@ -637,7 +637,7 @@ Interpretable planComprehension(Expr expr) { expr.getId(), fold.getAccuVar(), accu, fold.getIterVar(), iterRange, cond, step, result); } - private MacroListFold macroListFold(Comprehension fold) { + private static MacroListFold macroListFold(Comprehension fold) { if (!isEmptyList(fold.getAccuInit()) || !isBoolConst(fold.getLoopCondition(), true) || !isIdent(fold.getResult(), fold.getAccuVar())) { @@ -664,7 +664,7 @@ private MacroListFold macroListFold(Comprehension fold) { return new MacroListFold(filter, transform); } - private Expr appendedValue(String accuVar, Expr step) { + private static Expr appendedValue(String accuVar, Expr step) { if (!isCall(step, Operator.Add.id, 2)) { return null; } @@ -680,30 +680,30 @@ private Expr appendedValue(String accuVar, Expr step) { return list.getListExpr().getElements(0); } - private boolean isCall(Expr expr, String function, int argCount) { + private static boolean isCall(Expr expr, String function, int argCount) { return expr.getExprKindCase() == Expr.ExprKindCase.CALL_EXPR && expr.getCallExpr().getFunction().equals(function) && expr.getCallExpr().getArgsCount() == argCount && !expr.getCallExpr().hasTarget(); } - private boolean isIdent(Expr expr, String name) { + private static boolean isIdent(Expr expr, String name) { return expr.getExprKindCase() == Expr.ExprKindCase.IDENT_EXPR && expr.getIdentExpr().getName().equals(name); } - private boolean isEmptyList(Expr expr) { + private static boolean isEmptyList(Expr expr) { return expr.getExprKindCase() == Expr.ExprKindCase.LIST_EXPR && expr.getListExpr().getElementsCount() == 0; } - private boolean isBoolConst(Expr expr, boolean value) { + private static boolean isBoolConst(Expr expr, boolean value) { return expr.getExprKindCase() == Expr.ExprKindCase.CONST_EXPR && expr.getConstExpr().getConstantKindCase() == Constant.ConstantKindCase.BOOL_VALUE && expr.getConstExpr().getBoolValue() == value; } - private boolean referencesIdent(Expr expr, String name) { + private static boolean referencesIdent(Expr expr, String name) { switch (expr.getExprKindCase()) { case IDENT_EXPR: return expr.getIdentExpr().getName().equals(name); @@ -749,7 +749,7 @@ && referencesIdent(comprehension.getLoopStep(), name)) } } - private final class MacroListFold { + private static final class MacroListFold { final Expr filter; final Expr transform; @@ -760,7 +760,7 @@ private MacroListFold(Expr filter, Expr transform) { } /** planConst generates a constant valued Interpretable. */ - Interpretable planConst(Expr expr) { + static Interpretable planConst(Expr expr) { Val val = constValue(expr.getConstExpr()); if (val == null) { return null; @@ -770,7 +770,7 @@ Interpretable planConst(Expr expr) { /** constValue converts a proto Constant value to a ref.Val. */ @SuppressWarnings("deprecation") - Val constValue(Constant c) { + static Val constValue(Constant c) { switch (c.getConstantKindCase()) { case BOOL_VALUE: return boolOf(c.getBoolValue()); From e9e2fb956377576eabbff83255c9f1b3cb67fc37 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Wed, 22 Jul 2026 12:56:56 +0200 Subject: [PATCH 3/3] fixes --- .../cel/interpreter/Interpretable.java | 59 +++++++++++++++++ .../interpreter/InterpretableDecorator.java | 5 ++ .../cel/interpreter/InterpretablePlanner.java | 2 + .../cel/interpreter/InterpreterTest.java | 66 +++++++++++++++++++ 4 files changed, 132 insertions(+) diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java b/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java index 0588e042..3e332db8 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java @@ -1786,6 +1786,65 @@ public String toString() { } } + /** EvalExhaustiveListFold evaluates every filter and transform without short-circuiting. */ + final class EvalExhaustiveListFold extends AbstractEval implements Coster { + private final EvalListFold fold; + + EvalExhaustiveListFold(EvalListFold fold) { + super(fold.id); + this.fold = fold; + } + + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val foldRange = fold.iterRange.eval(ctx); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return valOrErr( + foldRange, "got '%s', expected iterable type", foldRange.getClass().getName()); + } + + VarActivation iterCtx = new VarActivation(); + iterCtx.parent = ctx; + iterCtx.name = fold.iterVar; + List values = new ArrayList<>(fold.listCapacity(foldRange)); + Val result = null; + IteratorT it = ((IterableT) foldRange).iterator(); + while (it.hasNext() == True) { + iterCtx.val = it.next(); + + Val include = fold.filter != null ? fold.filter.eval(iterCtx) : True; + Val value = fold.transform.eval(iterCtx); + if (include == False) { + continue; + } + if (include != True) { + result = noSuchOverload(null, Operator.Conditional.id, include); + continue; + } + if (result == null) { + if (isUnknownOrError(value)) { + result = value; + } else { + values.add(value); + } + } + } + return result != null + ? result + : ListT.newValArrayList(fold.adapter, values.toArray(new Val[0])); + } + + @Override + public Cost cost() { + return fold.cost(); + } + + @Override + public String toString() { + return "EvalExhaustiveListFold{" + fold + '}'; + } + } + /** evalAttr evaluates an Attribute value. */ final class EvalAttr extends AbstractEval implements InterpretableAttribute, Coster, Qualifier, Attribute { diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretableDecorator.java b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretableDecorator.java index 63d8a45f..eb4f9978 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretableDecorator.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretableDecorator.java @@ -35,9 +35,11 @@ import org.projectnessie.cel.interpreter.Interpretable.EvalExhaustiveAnd; import org.projectnessie.cel.interpreter.Interpretable.EvalExhaustiveConditional; import org.projectnessie.cel.interpreter.Interpretable.EvalExhaustiveFold; +import org.projectnessie.cel.interpreter.Interpretable.EvalExhaustiveListFold; import org.projectnessie.cel.interpreter.Interpretable.EvalExhaustiveOr; import org.projectnessie.cel.interpreter.Interpretable.EvalFold; import org.projectnessie.cel.interpreter.Interpretable.EvalList; +import org.projectnessie.cel.interpreter.Interpretable.EvalListFold; import org.projectnessie.cel.interpreter.Interpretable.EvalMap; import org.projectnessie.cel.interpreter.Interpretable.EvalOr; import org.projectnessie.cel.interpreter.Interpretable.EvalSetMembership; @@ -107,6 +109,9 @@ static InterpretableDecorator decDisableShortcircuits() { expr.step, expr.result); } + if (i instanceof EvalListFold) { + return new EvalExhaustiveListFold((EvalListFold) i); + } if (i instanceof InterpretableAttribute) { InterpretableAttribute expr = (InterpretableAttribute) i; if (expr.attr() instanceof ConditionalAttribute) { diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java index e2bf4f7d..25c522ed 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java @@ -739,8 +739,10 @@ private static boolean referencesIdent(Expr expr, String name) { return referencesIdent(comprehension.getIterRange(), name) || referencesIdent(comprehension.getAccuInit(), name) || (!comprehension.getIterVar().equals(name) + && !comprehension.getAccuVar().equals(name) && referencesIdent(comprehension.getLoopCondition(), name)) || (!comprehension.getIterVar().equals(name) + && !comprehension.getAccuVar().equals(name) && referencesIdent(comprehension.getLoopStep(), name)) || (!comprehension.getAccuVar().equals(name) && referencesIdent(comprehension.getResult(), name)); diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java index ca3795e8..beb18580 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java @@ -16,6 +16,7 @@ package org.projectnessie.cel.interpreter; import static java.lang.String.format; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -83,6 +84,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -111,6 +113,7 @@ import org.projectnessie.cel.interpreter.AttributeFactory.Qualifier; import org.projectnessie.cel.interpreter.AttributesTest.CustAttrFactory; import org.projectnessie.cel.interpreter.Coster.Cost; +import org.projectnessie.cel.interpreter.Interpretable.EvalListFold; import org.projectnessie.cel.interpreter.Interpretable.InterpretableAttribute; import org.projectnessie.cel.interpreter.Interpretable.InterpretableConst; import org.projectnessie.cel.interpreter.functions.Overload; @@ -1400,6 +1403,69 @@ void exhaustiveConditionalExpr() { assertThat(result).isSameAs(True); } + @Test + void exhaustiveListFoldEvaluatesFilteredTransforms() { + AtomicInteger calls = new AtomicInteger(); + Program program = + program( + new TestCase(InterpreterTestCase.macro_map) + .expr("[1, 2, 3].map(x, false, tap(x))") + .unchecked() + .funcs( + Overload.unary( + "tap", + value -> { + calls.incrementAndGet(); + return value; + })), + exhaustiveEval(newEvalState())); + + Val result = program.interpretable.eval(program.activation); + + assertThat(result.equal(DefaultTypeAdapter.Instance.nativeToValue(emptyList()))).isSameAs(True); + assertThat(calls.get()).isEqualTo(3); + } + + @Test + void exhaustiveListFoldContinuesAfterTransformError() { + AtomicInteger calls = new AtomicInteger(); + Program program = + program( + new TestCase(InterpreterTestCase.macro_map) + .expr("[1, 2, 3].map(x, failFirst(x))") + .unchecked() + .funcs( + Overload.unary( + "failFirst", + value -> { + calls.incrementAndGet(); + return value.intValue() == 1 ? Err.newErr("first") : value; + })), + exhaustiveEval(newEvalState())); + + Val result = program.interpretable.eval(program.activation); + + assertThat(result).isInstanceOf(Err.class).hasToString("first"); + assertThat(calls.get()).isEqualTo(3); + } + + @Test + void nestedMacroAccumulatorDoesNotDisableListFoldSpecialization() { + Program program = + program( + new TestCase(InterpreterTestCase.macro_map).expr("[1, 2].map(x, [x].map(y, y + 1))")); + + assertThat(program.interpretable).isInstanceOf(EvalListFold.class); + EvalListFold outer = (EvalListFold) program.interpretable; + assertThat(outer.transform).isInstanceOf(EvalListFold.class); + + Val result = program.interpretable.eval(program.activation); + Val expected = + DefaultTypeAdapter.Instance.nativeToValue( + Arrays.asList(singletonList(2L), singletonList(3L))); + assertThat(result.equal(expected)).isSameAs(True); + } + @Test void exhaustiveLogicalOrEquals() { // a || b == "b"