From 0f0b66ac8629807e37b7a097498572f7a4e4bd14 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Thu, 16 Jul 2026 16:35:33 +0200 Subject: [PATCH 1/2] Specialize receiver varargs fallback Route unresolved varargs calls through a receiver-specific evaluator that avoids building the full argument array before receiver dispatch. Also pass the complete receiver tail to dynamic receivers and cover unchecked receiver varargs calls with multiple arguments. --- .../cel/interpreter/Interpretable.java | 142 +++++++++++++++- .../cel/interpreter/InterpretablePlanner.java | 14 +- .../cel/interpreter/InterpreterTest.java | 154 ++++++++++++++++++ 3 files changed, 302 insertions(+), 8 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 78aa621e..a3af49e0 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java @@ -822,8 +822,7 @@ public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { // Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the // operand (arg0). if (arg0.type().hasTrait(Trait.ReceiverType)) { - return ((Receiver) arg0) - .receive(function, overload, Arrays.copyOfRange(argVals, 1, argVals.length - 1)); + return receiveVarArgs((Receiver) arg0, function, overload, argVals); } return noSuchOverload(arg0, function, overload, argVals); } @@ -1318,6 +1317,145 @@ public String toString() { } } + final class EvalReceiverVarArgs extends AbstractEval implements Coster, InterpretableCall { + private final String function; + private final String overload; + private final Interpretable[] args; + + public EvalReceiverVarArgs(long id, String function, String overload, Interpretable[] args) { + super(id); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.args = Objects.requireNonNull(args); + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val arg0 = args[0].eval(ctx); + if (isUnknownOrError(arg0)) { + return arg0; + } + + switch (args.length) { + case 3: + return evalReceiverTail2(ctx, arg0); + case 4: + return evalReceiverTail3(ctx, arg0); + default: + return evalReceiverTail(ctx, arg0); + } + } + + private Val evalReceiverTail2(org.projectnessie.cel.interpreter.Activation ctx, Val arg0) { + Val arg1 = args[1].eval(ctx); + if (isUnknownOrError(arg1)) { + return arg1; + } + Val arg2 = args[2].eval(ctx); + if (isUnknownOrError(arg2)) { + return arg2; + } + return receiveOrNoSuchOverload(arg0, arg1, arg2); + } + + private Val evalReceiverTail3(org.projectnessie.cel.interpreter.Activation ctx, Val arg0) { + Val arg1 = args[1].eval(ctx); + if (isUnknownOrError(arg1)) { + return arg1; + } + Val arg2 = args[2].eval(ctx); + if (isUnknownOrError(arg2)) { + return arg2; + } + Val arg3 = args[3].eval(ctx); + if (isUnknownOrError(arg3)) { + return arg3; + } + return receiveOrNoSuchOverload(arg0, arg1, arg2, arg3); + } + + private Val evalReceiverTail(org.projectnessie.cel.interpreter.Activation ctx, Val arg0) { + Val[] tailArgs = new Val[args.length - 1]; + for (int i = 1; i < args.length; i++) { + Val argVal = args[i].eval(ctx); + if (isUnknownOrError(argVal)) { + return argVal; + } + tailArgs[i - 1] = argVal; + } + return receiveOrNoSuchOverload(arg0, tailArgs); + } + + private Val receiveOrNoSuchOverload(Val arg0, Val... tailArgs) { + for (Val argVal : tailArgs) { + if (isUnknownOrError(argVal)) { + return argVal; + } + } + if (arg0.type().hasTrait(Trait.ReceiverType)) { + return ((Receiver) arg0).receive(function, overload, tailArgs); + } + return noSuchOverload(arg0, function, overload, tailArgs); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + Cost c = sumOfCost(args); + return c.add(OneOne); // add cost for function + } + + /** Function implements the InterpretableCall interface method. */ + @Override + public String function() { + return function; + } + + /** OverloadID implements the InterpretableCall interface method. */ + @Override + public String overloadID() { + return overload; + } + + /** Args returns the argument to the unary function. */ + @Override + public Interpretable[] args() { + return args; + } + + @Override + public String toString() { + return "EvalReceiverVarArgs{" + + "id=" + + id + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", args=" + + Arrays.toString(args) + + '}'; + } + } + + static Val receiveVarArgs(Receiver receiver, String function, String overload, Val[] argVals) { + switch (argVals.length) { + case 1: + return receiver.receive(function, overload); + case 2: + return receiver.receive(function, overload, argVals[1]); + case 3: + return receiver.receive(function, overload, argVals[1], argVals[2]); + case 4: + return receiver.receive(function, overload, argVals[1], argVals[2], argVals[3]); + default: + return receiver.receive(function, overload, Arrays.copyOfRange(argVals, 1, argVals.length)); + } + } + /** * evalWatch is an Interpretable implementation that wraps the execution of a given expression so * that it may observe the computed value and send it to an observer. 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 6fee2a25..7d9c7416 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java @@ -62,6 +62,7 @@ import org.projectnessie.cel.interpreter.Interpretable.EvalNe; import org.projectnessie.cel.interpreter.Interpretable.EvalObj; import org.projectnessie.cel.interpreter.Interpretable.EvalOr; +import org.projectnessie.cel.interpreter.Interpretable.EvalReceiverVarArgs; import org.projectnessie.cel.interpreter.Interpretable.EvalTestOnly; import org.projectnessie.cel.interpreter.Interpretable.EvalUnary; import org.projectnessie.cel.interpreter.Interpretable.EvalVarArgs; @@ -455,15 +456,16 @@ static Interpretable planCallBinary( /** planCallVarArgs generates a variable argument callable Interpretable. */ static Interpretable planCallVarArgs( Expr expr, String function, String overload, Overload impl, Interpretable... args) { + if (impl == null) { + return new EvalReceiverVarArgs(expr.getId(), function, overload, args); + } FunctionOp fn = null; Trait trait = null; - if (impl != null) { - if (impl.function == null) { - throw new IllegalStateException(String.format("no such overload: %s(...)", function)); - } - fn = impl.function; - trait = impl.operandTrait; + if (impl.function == null) { + throw new IllegalStateException(String.format("no such overload: %s(...)", function)); } + fn = impl.function; + trait = impl.operandTrait; return new EvalVarArgs(expr.getId(), function, overload, args, trait, fn); } 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 9cc589bb..c635afe7 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java @@ -103,10 +103,15 @@ import org.projectnessie.cel.common.types.TimestampT; import org.projectnessie.cel.common.types.UnknownT; import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.BaseVal; +import org.projectnessie.cel.common.types.ref.Type; +import org.projectnessie.cel.common.types.ref.TypeEnum; import org.projectnessie.cel.common.types.ref.TypeRegistry; import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.common.types.traits.Adder; import org.projectnessie.cel.common.types.traits.Negater; +import org.projectnessie.cel.common.types.traits.Receiver; +import org.projectnessie.cel.common.types.traits.Trait; import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifier; import org.projectnessie.cel.interpreter.AttributeFactory.FieldQualifier; import org.projectnessie.cel.interpreter.AttributeFactory.NamespacedAttribute; @@ -122,6 +127,101 @@ class InterpreterTest { + private static final Type RECEIVER_TEST_TYPE = + new Type() { + @Override + public boolean hasTrait(Trait trait) { + return trait == Trait.ReceiverType; + } + + @Override + public String typeName() { + return "receiver_test"; + } + + @Override + public TypeEnum typeEnum() { + return TypeEnum.Object; + } + + @Override + public T convertToNative(Class typeDesc) { + throw new UnsupportedOperationException(); + } + + @Override + public Val convertToType(Type typeValue) { + return this; + } + + @Override + public Val equal(Val other) { + return boolOf(other == this); + } + + @Override + public Type type() { + return this; + } + + @Override + public Object value() { + return typeName(); + } + + @Override + public boolean booleanValue() { + throw new UnsupportedOperationException(); + } + + @Override + public long intValue() { + throw new UnsupportedOperationException(); + } + + @Override + public double doubleValue() { + throw new UnsupportedOperationException(); + } + }; + + private static final class ReceiverTestVal extends BaseVal implements Receiver { + + @Override + public T convertToNative(Class typeDesc) { + throw new UnsupportedOperationException(); + } + + @Override + public Val convertToType(Type typeValue) { + return this; + } + + @Override + public Val equal(Val other) { + return boolOf(other == this); + } + + @Override + public Type type() { + return RECEIVER_TEST_TYPE; + } + + @Override + public Object value() { + return "receiver"; + } + + @Override + public Val receive(String function, String overload, Val... args) { + StringBuilder result = new StringBuilder(function).append(':').append(overload); + for (Val arg : args) { + result.append(':').append(arg.intValue()); + } + return stringOf(result.toString()); + } + } + private static Val base64Encode(Val val) { if (!(val instanceof StringT)) { return noSuchOverload(val, "base64Encode", "", new Val[] {}); @@ -1627,6 +1727,60 @@ void checkedTopLevelIdentSelectPreservesTrackedState() { assertThat(state.value(selectId)).isSameAs(True); } + @Test + void uncheckedReceiverVarargsReceiveAllArguments() { + Source src = newTextSource("target.collect(1, 2)"); + ParseResult parsed = Parser.parseAllMacros(src); + assertThat(parsed.hasErrors()).withFailMessage(parsed.getErrors()::toDisplayString).isFalse(); + + TypeRegistry reg = newRegistry(); + AttributeFactory attrs = newAttributeFactory(Container.defaultContainer, reg, reg); + Interpreter interp = newStandardInterpreter(Container.defaultContainer, reg, reg, attrs); + Interpretable i = interp.newUncheckedInterpretable(parsed.getExpr()); + + Val result = i.eval(newActivation(mapOf("target", new ReceiverTestVal()))); + assertThat(result).isEqualTo(stringOf("collect::1:2")); + } + + @Test + void registeredTraitMismatchReceiverVarargsReceiveAllArguments() { + Program program = + program( + new TestCase(InterpreterTestCase.call_varargs) + .expr("target.collect(1, 2)") + .unchecked() + .funcs(Overload.function("collect", AdderType, args -> stringOf("wrong path"))) + .in("target", new ReceiverTestVal())); + + Val result = program.interpretable.eval(program.activation); + + assertThat(result).isEqualTo(stringOf("collect::1:2")); + } + + @Test + void receiverVarargsStopAfterError() { + AtomicInteger calls = new AtomicInteger(); + Program program = + program( + new TestCase(InterpreterTestCase.call_varargs) + .expr("target.collect(1, fail(), tap(3), 4)") + .unchecked() + .funcs( + Overload.function("fail", args -> Err.newErr("first")), + Overload.unary( + "tap", + value -> { + calls.incrementAndGet(); + return value; + })) + .in("target", new ReceiverTestVal())); + + Val result = program.interpretable.eval(program.activation); + + assertThat(result).isInstanceOf(Err.class).hasToString("first"); + assertThat(calls.get()).isZero(); + } + @Test void equalityWithUnknownOperandStaysUnknown() { assertThat(evalWithUnknownStringX("x == 'foo'")).isInstanceOf(UnknownT.class); From d747f396aa067b73d9c02322130d8009eab0100a Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Thu, 23 Jul 2026 19:24:34 +0200 Subject: [PATCH 2/2] review --- .../org/projectnessie/cel/interpreter/Interpretable.java | 5 ----- 1 file changed, 5 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 a3af49e0..1c8ee0c6 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java @@ -1388,11 +1388,6 @@ private Val evalReceiverTail(org.projectnessie.cel.interpreter.Activation ctx, V } private Val receiveOrNoSuchOverload(Val arg0, Val... tailArgs) { - for (Val argVal : tailArgs) { - if (isUnknownOrError(argVal)) { - return argVal; - } - } if (arg0.type().hasTrait(Trait.ReceiverType)) { return ((Receiver) arg0).receive(function, overload, tailArgs); }