From 3aa1c5ebc60e016cf8693ac63625cc98b930e46e Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Tue, 21 Jul 2026 15:56:52 +0200 Subject: [PATCH] Specialize higher-arity overload evaluation Add fixed evaluators and operations for three-, four-, and five-argument overloads. Retain generic dispatch, error and unknown ordering, receiver fallback, cost, and decorator behavior for unsupported paths. --- .../interpreter/OverloadDispatchBench.java | 482 +++++++++++ .../projectnessie/cel/extension/Guards.java | 67 ++ .../projectnessie/cel/extension/MathLib.java | 124 ++- .../cel/extension/StringsLib.java | 6 + .../cel/interpreter/Interpretable.java | 334 +++++++ .../cel/interpreter/InterpretablePlanner.java | 81 ++ .../cel/interpreter/functions/Overload.java | 101 ++- .../interpreter/functions/QuaternaryOp.java | 24 + .../cel/interpreter/functions/QuinaryOp.java | 24 + .../cel/interpreter/functions/TernaryOp.java | 24 + .../cel/interpreter/OverloadDispatchTest.java | 819 ++++++++++++++++++ 11 files changed, 2070 insertions(+), 16 deletions(-) create mode 100644 core/src/jmh/java/org/projectnessie/cel/interpreter/OverloadDispatchBench.java create mode 100644 core/src/main/java/org/projectnessie/cel/interpreter/functions/QuaternaryOp.java create mode 100644 core/src/main/java/org/projectnessie/cel/interpreter/functions/QuinaryOp.java create mode 100644 core/src/main/java/org/projectnessie/cel/interpreter/functions/TernaryOp.java create mode 100644 core/src/test/java/org/projectnessie/cel/interpreter/OverloadDispatchTest.java diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/OverloadDispatchBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/OverloadDispatchBench.java new file mode 100644 index 000000000..19f3600ef --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/OverloadDispatchBench.java @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static org.projectnessie.cel.common.types.BoolT.False; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.TypeT.newObjectTypeValue; +import static org.projectnessie.cel.interpreter.Activation.emptyActivation; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Library; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.common.types.pb.ProtoTypeRegistry; +import org.projectnessie.cel.common.types.ref.BaseVal; +import org.projectnessie.cel.common.types.ref.Type; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Receiver; +import org.projectnessie.cel.common.types.traits.Trait; +import org.projectnessie.cel.extension.MathLib; +import org.projectnessie.cel.extension.StringsLib; +import org.projectnessie.cel.interpreter.Interpretable.EvalBinary; +import org.projectnessie.cel.interpreter.Interpretable.EvalConst; +import org.projectnessie.cel.interpreter.Interpretable.EvalQuaternary; +import org.projectnessie.cel.interpreter.Interpretable.EvalQuinary; +import org.projectnessie.cel.interpreter.Interpretable.EvalReceiverVarArgs; +import org.projectnessie.cel.interpreter.Interpretable.EvalTernary; +import org.projectnessie.cel.interpreter.Interpretable.EvalUnary; +import org.projectnessie.cel.interpreter.Interpretable.EvalVarArgs; +import org.projectnessie.cel.interpreter.Interpretable.EvalZeroArity; +import org.projectnessie.cel.interpreter.functions.BinaryOp; +import org.projectnessie.cel.interpreter.functions.FunctionOp; +import org.projectnessie.cel.interpreter.functions.QuaternaryOp; +import org.projectnessie.cel.interpreter.functions.QuinaryOp; +import org.projectnessie.cel.interpreter.functions.TernaryOp; +import org.projectnessie.cel.interpreter.functions.UnaryOp; + +@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Fork(3) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class OverloadDispatchBench { + + private static final String FUNCTION = "benchmark"; + private static final String OVERLOAD = "benchmark_overload"; + private static final Type RECEIVER_TYPE = + newObjectTypeValue("benchmark_receiver", Trait.ReceiverType); + + @State(Scope.Thread) + public static class DispatchState { + Activation activation; + + FunctionOp zeroOp; + UnaryOp unaryOp; + BinaryOp binaryOp; + TernaryOp ternaryOp; + QuaternaryOp quaternaryOp; + QuinaryOp quinaryOp; + FunctionOp functionOp; + + Val unaryArg; + Val binaryLhs; + Val binaryRhs; + Val[] emptyArgs; + Val[] arity3Args; + Val[] arity4Args; + Val[] arity5Args; + + BenchmarkReceiver receiver; + Val[] receiverTail0; + Val[] receiverTail1; + Val[] receiverTail2; + Val[] receiverTail3; + Val[] receiverTail4; + + Interpretable evalZero; + Interpretable evalUnary; + Interpretable evalBinary; + Interpretable evalVarArgs3; + Interpretable evalTernary3; + Interpretable evalVarArgs4; + Interpretable evalQuaternary4; + Interpretable evalVarArgs5; + Interpretable evalQuinary5; + Interpretable evalReceiverTail0; + Interpretable evalReceiverTail1; + Interpretable evalReceiverTail2; + Interpretable evalReceiverTail3; + Interpretable evalReceiverTail4; + Interpretable evalBoundGenericTraitSuccess3; + Interpretable evalBoundGenericTraitFallback3; + Program stringsIndexOfOffset; + Program stringsReplaceN; + Program mathGreatest3; + Program mathGreatest4; + Program mathGreatest5; + + @Setup + public void init() { + activation = emptyActivation(); + + zeroOp = values -> True; + unaryOp = value -> value; + binaryOp = (lhs, rhs) -> rhs; + ternaryOp = (first, second, third) -> third; + quaternaryOp = (first, second, third, fourth) -> fourth; + quinaryOp = (first, second, third, fourth, fifth) -> fifth; + functionOp = values -> values[values.length - 1]; + + unaryArg = intOf(1); + binaryLhs = intOf(2); + binaryRhs = intOf(3); + emptyArgs = new Val[0]; + arity3Args = new Val[] {intOf(4), intOf(5), intOf(6)}; + arity4Args = new Val[] {intOf(7), intOf(8), intOf(9), intOf(10)}; + arity5Args = new Val[] {intOf(11), intOf(12), intOf(13), intOf(14), intOf(15)}; + + receiver = new BenchmarkReceiver(); + receiverTail0 = new Val[0]; + receiverTail1 = new Val[] {intOf(11)}; + receiverTail2 = new Val[] {intOf(12), intOf(13)}; + receiverTail3 = new Val[] {intOf(14), intOf(15), intOf(16)}; + receiverTail4 = new Val[] {intOf(17), intOf(18), intOf(19), intOf(20)}; + + evalZero = new EvalZeroArity(1, FUNCTION, OVERLOAD, zeroOp); + evalUnary = new EvalUnary(2, FUNCTION, OVERLOAD, constant(2, unaryArg), null, unaryOp); + evalBinary = + new EvalBinary( + 3, + FUNCTION, + OVERLOAD, + constant(3, binaryLhs), + constant(4, binaryRhs), + null, + binaryOp); + evalVarArgs3 = + new EvalVarArgs(5, FUNCTION, OVERLOAD, constants(5, arity3Args), null, functionOp); + evalTernary3 = + new EvalTernary( + 6, + FUNCTION, + OVERLOAD, + constant(8, arity3Args[0]), + constant(9, arity3Args[1]), + constant(10, arity3Args[2]), + null, + ternaryOp); + evalVarArgs4 = + new EvalVarArgs(7, FUNCTION, OVERLOAD, constants(11, arity4Args), null, functionOp); + evalQuaternary4 = + new EvalQuaternary( + 8, + FUNCTION, + OVERLOAD, + constant(15, arity4Args[0]), + constant(16, arity4Args[1]), + constant(17, arity4Args[2]), + constant(18, arity4Args[3]), + null, + quaternaryOp); + evalVarArgs5 = + new EvalVarArgs(9, FUNCTION, OVERLOAD, constants(19, arity5Args), null, functionOp); + evalQuinary5 = + new EvalQuinary( + 10, + FUNCTION, + OVERLOAD, + constant(24, arity5Args[0]), + constant(25, arity5Args[1]), + constant(26, arity5Args[2]), + constant(27, arity5Args[3]), + constant(28, arity5Args[4]), + null, + quinaryOp); + + evalReceiverTail0 = new EvalUnary(7, FUNCTION, OVERLOAD, constant(12, receiver), null, null); + evalReceiverTail1 = + new EvalBinary( + 8, + FUNCTION, + OVERLOAD, + constant(13, receiver), + constant(14, receiverTail1[0]), + null, + null); + evalReceiverTail2 = + new EvalReceiverVarArgs(9, FUNCTION, OVERLOAD, receiverArgs(15, receiver, receiverTail2)); + evalReceiverTail3 = + new EvalReceiverVarArgs( + 10, FUNCTION, OVERLOAD, receiverArgs(18, receiver, receiverTail3)); + evalReceiverTail4 = + new EvalReceiverVarArgs( + 11, FUNCTION, OVERLOAD, receiverArgs(22, receiver, receiverTail4)); + + var receiverArgs3 = receiverArgs(27, receiver, receiverTail2); + evalBoundGenericTraitSuccess3 = + new EvalVarArgs(12, FUNCTION, OVERLOAD, receiverArgs3, Trait.ReceiverType, functionOp); + evalBoundGenericTraitFallback3 = + new EvalVarArgs(13, FUNCTION, OVERLOAD, receiverArgs3, Trait.AdderType, functionOp); + + var env = + Env.newCustomEnv( + ProtoTypeRegistry.newRegistry(), + List.of(Library.StdLib(), StringsLib.strings(), MathLib.math())); + stringsIndexOfOffset = compile(env, "'tacocat'.indexOf('a', 3)"); + stringsReplaceN = compile(env, "'hello hello'.replace('he', 'we', 1)"); + mathGreatest3 = compile(env, "math.greatest(5, 10, 3)"); + mathGreatest4 = compile(env, "math.greatest(5, 10, 3, 8)"); + mathGreatest5 = compile(env, "math.greatest(5, 10, 3, 8, 7)"); + } + + private static EvalConst constant(long id, Val value) { + return new EvalConst(id, value); + } + + private static Interpretable[] constants(long firstId, Val[] values) { + var constants = new Interpretable[values.length]; + for (int i = 0; i < values.length; i++) { + constants[i] = constant(firstId + i, values[i]); + } + return constants; + } + + private static Interpretable[] receiverArgs( + long firstId, BenchmarkReceiver receiver, Val[] tail) { + var values = new Val[tail.length + 1]; + values[0] = receiver; + System.arraycopy(tail, 0, values, 1, tail.length); + return constants(firstId, values); + } + + private static Program compile(Env env, String expression) { + var ast = env.compile(expression); + if (ast.hasIssues()) { + throw new IllegalStateException(ast.getIssues().toString()); + } + return env.program(ast.getAst()); + } + } + + @Benchmark + public Val directFunctionZero(DispatchState state) { + return state.zeroOp.invoke(state.emptyArgs); + } + + @Benchmark + public Val evalZero(DispatchState state) { + return state.evalZero.eval(state.activation); + } + + @Benchmark + public Val directUnary(DispatchState state) { + return state.unaryOp.invoke(state.unaryArg); + } + + @Benchmark + public Val evalUnary(DispatchState state) { + return state.evalUnary.eval(state.activation); + } + + @Benchmark + public Val directBinary(DispatchState state) { + return state.binaryOp.invoke(state.binaryLhs, state.binaryRhs); + } + + @Benchmark + public Val evalBinary(DispatchState state) { + return state.evalBinary.eval(state.activation); + } + + @Benchmark + public Val directFunctionArity3(DispatchState state) { + return state.functionOp.invoke(state.arity3Args); + } + + @Benchmark + public Val directTernary(DispatchState state) { + return state.ternaryOp.invoke(state.arity3Args[0], state.arity3Args[1], state.arity3Args[2]); + } + + @Benchmark + public Val evalVarArgsArity3(DispatchState state) { + return state.evalVarArgs3.eval(state.activation); + } + + @Benchmark + public Val evalTernaryArity3(DispatchState state) { + return state.evalTernary3.eval(state.activation); + } + + @Benchmark + public Val directFunctionArity4(DispatchState state) { + return state.functionOp.invoke(state.arity4Args); + } + + @Benchmark + public Val directQuaternary(DispatchState state) { + return state.quaternaryOp.invoke( + state.arity4Args[0], state.arity4Args[1], state.arity4Args[2], state.arity4Args[3]); + } + + @Benchmark + public Val evalVarArgsArity4(DispatchState state) { + return state.evalVarArgs4.eval(state.activation); + } + + @Benchmark + public Val evalQuaternaryArity4(DispatchState state) { + return state.evalQuaternary4.eval(state.activation); + } + + @Benchmark + public Val directFunctionArity5(DispatchState state) { + return state.functionOp.invoke(state.arity5Args); + } + + @Benchmark + public Val evalVarArgsArity5(DispatchState state) { + return state.evalVarArgs5.eval(state.activation); + } + + @Benchmark + public Val directQuinary(DispatchState state) { + return state.quinaryOp.invoke( + state.arity5Args[0], + state.arity5Args[1], + state.arity5Args[2], + state.arity5Args[3], + state.arity5Args[4]); + } + + @Benchmark + public Val evalQuinaryArity5(DispatchState state) { + return state.evalQuinary5.eval(state.activation); + } + + @Benchmark + public Val directReceiveTail0(DispatchState state) { + return state.receiver.receive(FUNCTION, OVERLOAD, state.receiverTail0); + } + + @Benchmark + public Val evalReceiverTail0(DispatchState state) { + return state.evalReceiverTail0.eval(state.activation); + } + + @Benchmark + public Val directReceiveTail1(DispatchState state) { + return state.receiver.receive(FUNCTION, OVERLOAD, state.receiverTail1); + } + + @Benchmark + public Val evalReceiverTail1(DispatchState state) { + return state.evalReceiverTail1.eval(state.activation); + } + + @Benchmark + public Val directReceiveTail2(DispatchState state) { + return state.receiver.receive(FUNCTION, OVERLOAD, state.receiverTail2); + } + + @Benchmark + public Val evalReceiverTail2(DispatchState state) { + return state.evalReceiverTail2.eval(state.activation); + } + + @Benchmark + public Val directReceiveTail3(DispatchState state) { + return state.receiver.receive(FUNCTION, OVERLOAD, state.receiverTail3); + } + + @Benchmark + public Val evalReceiverTail3(DispatchState state) { + return state.evalReceiverTail3.eval(state.activation); + } + + @Benchmark + public Val directReceiveTail4(DispatchState state) { + return state.receiver.receive(FUNCTION, OVERLOAD, state.receiverTail4); + } + + @Benchmark + public Val evalReceiverTail4(DispatchState state) { + return state.evalReceiverTail4.eval(state.activation); + } + + @Benchmark + public Val evalBoundGenericTraitSuccessArity3(DispatchState state) { + return state.evalBoundGenericTraitSuccess3.eval(state.activation); + } + + @Benchmark + public Val evalBoundGenericTraitFallbackArity3(DispatchState state) { + return state.evalBoundGenericTraitFallback3.eval(state.activation); + } + + @Benchmark + public Val evalStringsIndexOfOffset(DispatchState state) { + return state.stringsIndexOfOffset.eval(state.activation).getVal(); + } + + @Benchmark + public Val evalStringsReplaceN(DispatchState state) { + return state.stringsReplaceN.eval(state.activation).getVal(); + } + + @Benchmark + public Val evalMathGreatest3(DispatchState state) { + return state.mathGreatest3.eval(state.activation).getVal(); + } + + @Benchmark + public Val evalMathGreatest4(DispatchState state) { + return state.mathGreatest4.eval(state.activation).getVal(); + } + + @Benchmark + public Val evalMathGreatest5(DispatchState state) { + return state.mathGreatest5.eval(state.activation).getVal(); + } + + private static final class BenchmarkReceiver extends BaseVal implements Receiver { + @SuppressWarnings("removal") + @Override + public T convertToNative(Class typeDesc) { + throw new UnsupportedOperationException(); + } + + @Override + public Val convertToType(Type typeValue) { + return this; + } + + @Override + public Val equal(Val other) { + return other == this ? True : False; + } + + @Override + public Type type() { + return RECEIVER_TYPE; + } + + @Override + public Object value() { + return "benchmark_receiver"; + } + + @Override + public Val receive(String function, String overload, Val... args) { + return args.length == 0 ? this : args[args.length - 1]; + } + } +} diff --git a/core/src/main/java/org/projectnessie/cel/extension/Guards.java b/core/src/main/java/org/projectnessie/cel/extension/Guards.java index b1c66506b..3102f68b4 100644 --- a/core/src/main/java/org/projectnessie/cel/extension/Guards.java +++ b/core/src/main/java/org/projectnessie/cel/extension/Guards.java @@ -25,6 +25,8 @@ import org.projectnessie.cel.common.types.StringT; import org.projectnessie.cel.interpreter.functions.BinaryOp; import org.projectnessie.cel.interpreter.functions.FunctionOp; +import org.projectnessie.cel.interpreter.functions.QuaternaryOp; +import org.projectnessie.cel.interpreter.functions.TernaryOp; import org.projectnessie.cel.interpreter.functions.UnaryOp; /** function invocation guards for common call signatures within extension functions. */ @@ -57,6 +59,19 @@ public static FunctionOp callInStrIntIntOutStr( }; } + public static TernaryOp callInStrIntIntOutStrTernary( + TriFunction func) { + return (first, second, third) -> { + try { + return StringT.stringOf( + func.apply( + (String) first.value(), getIntValue((IntT) second), getIntValue((IntT) third))); + } catch (RuntimeException e) { + return Err.newErr(e, "%s", e.getMessage()); + } + }; + } + public static BinaryOp callInStrStrOutInt(BiFunction func) { return (lhs, rhs) -> { try { @@ -82,6 +97,18 @@ public static FunctionOp callInStrStrIntOutInt( }; } + public static TernaryOp callInStrStrIntOutIntTernary( + TriFunction func) { + return (first, second, third) -> { + try { + return IntT.intOf( + func.apply((String) first.value(), (String) second.value(), getIntValue((IntT) third))); + } catch (RuntimeException e) { + return Err.newErr(e, "%s", e.getMessage()); + } + }; + } + public static BinaryOp callInStrStrOutStrArr(BiFunction func) { return (lhs, rhs) -> { try { @@ -107,6 +134,18 @@ public static FunctionOp callInStrStrIntOutStrArr( }; } + public static TernaryOp callInStrStrIntOutStrArrTernary( + TriFunction func) { + return (first, second, third) -> { + try { + return ListT.newStringArrayList( + func.apply((String) first.value(), (String) second.value(), getIntValue((IntT) third))); + } catch (RuntimeException e) { + return Err.newErr(e, "%s", e.getMessage()); + } + }; + } + public static FunctionOp callInStrStrStrOutStr(TriFunction func) { return values -> { try { @@ -121,6 +160,18 @@ public static FunctionOp callInStrStrStrOutStr(TriFunction func) { + return (first, second, third) -> { + try { + return StringT.stringOf( + func.apply((String) first.value(), (String) second.value(), (String) third.value())); + } catch (RuntimeException e) { + return Err.newErr(e, "%s", e.getMessage()); + } + }; + } + public static FunctionOp callInStrStrStrIntOutStr( QuadFunction func) { return values -> { @@ -137,6 +188,22 @@ public static FunctionOp callInStrStrStrIntOutStr( }; } + public static QuaternaryOp callInStrStrStrIntOutStrQuaternary( + QuadFunction func) { + return (first, second, third, fourth) -> { + try { + return StringT.stringOf( + func.apply( + (String) first.value(), + (String) second.value(), + (String) third.value(), + getIntValue((IntT) fourth))); + } catch (RuntimeException e) { + return Err.newErr(e, "%s", e.getMessage()); + } + }; + } + public static UnaryOp callInStrOutStr(UnaryOperator func) { return val -> { try { diff --git a/core/src/main/java/org/projectnessie/cel/extension/MathLib.java b/core/src/main/java/org/projectnessie/cel/extension/MathLib.java index d668e109a..427d29647 100644 --- a/core/src/main/java/org/projectnessie/cel/extension/MathLib.java +++ b/core/src/main/java/org/projectnessie/cel/extension/MathLib.java @@ -36,7 +36,11 @@ import org.projectnessie.cel.common.types.UintT; import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.interpreter.functions.FunctionOp; import org.projectnessie.cel.interpreter.functions.Overload; +import org.projectnessie.cel.interpreter.functions.QuaternaryOp; +import org.projectnessie.cel.interpreter.functions.QuinaryOp; +import org.projectnessie.cel.interpreter.functions.TernaryOp; /** MathLib provides CEL helper functions from the standard math extension library. */ public final class MathLib implements Library { @@ -91,10 +95,34 @@ public List getCompileOptions() { public List getProgramOptions() { List overloads = new ArrayList<>(); overloads.add( - Overload.overload(GREATEST, null, MathLib::greatest, MathLib::greatest, MathLib::greatest)); - overloads.add(Overload.overload(LEAST, null, MathLib::least, MathLib::least, MathLib::least)); - addArityOverloads(overloads, GREATEST, MathLib::greatest); - addArityOverloads(overloads, LEAST, MathLib::least); + Overload.overload( + GREATEST, + null, + MathLib::greatest, + MathLib::greatest, + MathLib::greatest, + MathLib::greatest, + MathLib::greatest, + MathLib::greatest)); + overloads.add( + Overload.overload( + LEAST, + null, + MathLib::least, + MathLib::least, + MathLib::least, + MathLib::least, + MathLib::least, + MathLib::least)); + addArityOverloads( + overloads, + GREATEST, + MathLib::greatest, + MathLib::greatest, + MathLib::greatest, + MathLib::greatest); + addArityOverloads( + overloads, LEAST, MathLib::least, MathLib::least, MathLib::least, MathLib::least); overloads.add(Overload.unary(CEIL, MathLib::ceil)); overloads.add(Overload.unary(overloadId(CEIL, 1), MathLib::ceil)); overloads.add(Overload.unary(FLOOR, MathLib::floor)); @@ -164,15 +192,18 @@ private static List dynArgs(int count) { private static void addArityOverloads( List overloads, String function, - org.projectnessie.cel.interpreter.functions.FunctionOp op) { + FunctionOp op, + TernaryOp ternaryOp, + QuaternaryOp quaternaryOp, + QuinaryOp quinaryOp) { overloads.add(Overload.unary(overloadId(function, "int"), op::invoke)); overloads.add(Overload.unary(overloadId(function, "uint"), op::invoke)); overloads.add(Overload.unary(overloadId(function, "double"), op::invoke)); overloads.add( Overload.binary(overloadId(function, 2), (left, right) -> op.invoke(left, right))); - for (int arity = 3; arity <= 5; arity++) { - overloads.add(Overload.function(overloadId(function, arity), op)); - } + overloads.add(Overload.ternary(overloadId(function, 3), ternaryOp)); + overloads.add(Overload.quaternary(overloadId(function, 4), quaternaryOp)); + overloads.add(Overload.quinary(overloadId(function, 5), quinaryOp)); overloads.add(Overload.unary(overloadId(function, "list"), op::invoke)); } @@ -188,10 +219,87 @@ private static Val greatest(Val... values) { return minMax(values, true); } + private static Val greatest(Val first, Val second, Val third) { + return minMax(first, second, third, true); + } + + private static Val greatest(Val first, Val second, Val third, Val fourth) { + return minMax(first, second, third, fourth, true); + } + + private static Val greatest(Val first, Val second, Val third, Val fourth, Val fifth) { + return minMax(first, second, third, fourth, fifth, true); + } + private static Val least(Val... values) { return minMax(values, false); } + private static Val least(Val first, Val second, Val third) { + return minMax(first, second, third, false); + } + + private static Val least(Val first, Val second, Val third, Val fourth) { + return minMax(first, second, third, fourth, false); + } + + private static Val least(Val first, Val second, Val third, Val fourth, Val fifth) { + return minMax(first, second, third, fourth, fifth, false); + } + + private static Val minMax( + Val first, Val second, Val third, Val fourth, Val fifth, boolean greatest) { + Val result = minMax(first, second, third, fourth, greatest); + if (!isNumber(result)) { + return result; + } + if (!isNumber(fifth)) { + return noSuchOverload(); + } + int cmp = compareNumbers(fifth, result); + if ((greatest && cmp > 0) || (!greatest && cmp < 0)) { + result = fifth; + } + return result; + } + + private static Val minMax(Val first, Val second, Val third, Val fourth, boolean greatest) { + Val result = minMax(first, second, third, greatest); + if (!isNumber(result)) { + return result; + } + if (!isNumber(fourth)) { + return noSuchOverload(); + } + int cmp = compareNumbers(fourth, result); + if ((greatest && cmp > 0) || (!greatest && cmp < 0)) { + result = fourth; + } + return result; + } + + private static Val minMax(Val first, Val second, Val third, boolean greatest) { + if (!isNumber(first)) { + return noSuchOverload(); + } + Val result = first; + if (!isNumber(second)) { + return noSuchOverload(); + } + int cmp = compareNumbers(second, result); + if ((greatest && cmp > 0) || (!greatest && cmp < 0)) { + result = second; + } + if (!isNumber(third)) { + return noSuchOverload(); + } + cmp = compareNumbers(third, result); + if ((greatest && cmp > 0) || (!greatest && cmp < 0)) { + result = third; + } + return result; + } + private static Val minMax(Val[] values, boolean greatest) { List candidates = candidates(values); if (candidates.isEmpty()) { diff --git a/core/src/main/java/org/projectnessie/cel/extension/StringsLib.java b/core/src/main/java/org/projectnessie/cel/extension/StringsLib.java index 571ebcd1f..8cc65c47d 100644 --- a/core/src/main/java/org/projectnessie/cel/extension/StringsLib.java +++ b/core/src/main/java/org/projectnessie/cel/extension/StringsLib.java @@ -383,6 +383,7 @@ public List getProgramOptions() { null, null, Guards.callInStrStrOutInt(StringsLib::indexOf), + Guards.callInStrStrIntOutIntTernary(StringsLib::indexOfOffset), values -> values.length == 3 ? Guards.callInStrStrIntOutInt(StringsLib::indexOfOffset).invoke(values) @@ -398,6 +399,7 @@ public List getProgramOptions() { null, null, Guards.callInStrStrOutInt(StringsLib::lastIndexOf), + Guards.callInStrStrIntOutIntTernary(StringsLib::lastIndexOfOffset), values -> values.length == 3 ? Guards.callInStrStrIntOutInt(StringsLib::lastIndexOfOffset).invoke(values) @@ -408,6 +410,8 @@ public List getProgramOptions() { null, null, null, + Guards.callInStrStrStrOutStrTernary(StringsLib::replace), + Guards.callInStrStrStrIntOutStrQuaternary(StringsLib::replaceN), values -> { if (values.length == 3) { return Guards.callInStrStrStrOutStr(StringsLib::replace).invoke(values); @@ -423,6 +427,7 @@ public List getProgramOptions() { null, null, Guards.callInStrStrOutStrArr(StringsLib::split), + Guards.callInStrStrIntOutStrArrTernary(StringsLib::splitN), values -> values.length == 3 ? Guards.callInStrStrIntOutStrArr(StringsLib::splitN).invoke(values) @@ -432,6 +437,7 @@ public List getProgramOptions() { null, null, Guards.callInStrIntOutStr(StringsLib::substr), + Guards.callInStrIntIntOutStrTernary(StringsLib::substrRange), values -> values.length == 3 ? Guards.callInStrIntIntOutStr(StringsLib::substrRange).invoke(values) 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 c01caf666..9a0d534e7 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java @@ -70,6 +70,9 @@ import org.projectnessie.cel.interpreter.InterpretableDecorator.EvalObserver; import org.projectnessie.cel.interpreter.functions.BinaryOp; import org.projectnessie.cel.interpreter.functions.FunctionOp; +import org.projectnessie.cel.interpreter.functions.QuaternaryOp; +import org.projectnessie.cel.interpreter.functions.QuinaryOp; +import org.projectnessie.cel.interpreter.functions.TernaryOp; import org.projectnessie.cel.interpreter.functions.UnaryOp; /** @@ -857,6 +860,337 @@ public String toString() { } } + final class EvalTernary extends AbstractEval implements Coster, InterpretableCall { + private final String function; + private final String overload; + private final Interpretable first; + private final Interpretable second; + private final Interpretable third; + private final Trait trait; + private final TernaryOp impl; + + EvalTernary( + long id, + String function, + String overload, + Interpretable first, + Interpretable second, + Interpretable third, + Trait trait, + TernaryOp impl) { + super(id); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.first = Objects.requireNonNull(first); + this.second = Objects.requireNonNull(second); + this.third = Objects.requireNonNull(third); + this.trait = trait; + this.impl = Objects.requireNonNull(impl); + } + + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val firstVal = first.eval(ctx); + if (isUnknownOrError(firstVal)) { + return firstVal; + } + Val secondVal = second.eval(ctx); + if (isUnknownOrError(secondVal)) { + return secondVal; + } + Val thirdVal = third.eval(ctx); + if (isUnknownOrError(thirdVal)) { + return thirdVal; + } + if (trait == null || firstVal.type().hasTrait(trait)) { + return impl.invoke(firstVal, secondVal, thirdVal); + } + if (firstVal.type().hasTrait(Trait.ReceiverType)) { + return ((Receiver) firstVal).receive(function, overload, secondVal, thirdVal); + } + return noSuchOverload( + firstVal, function, overload, new Val[] {firstVal, secondVal, thirdVal}); + } + + @Override + public Cost cost() { + return estimateCost(first).add(estimateCost(second)).add(estimateCost(third)).add(OneOne); + } + + @Override + public String function() { + return function; + } + + @Override + public String overloadID() { + return overload; + } + + @Override + public Interpretable[] args() { + return new Interpretable[] {first, second, third}; + } + + @Override + public String toString() { + return "EvalTernary{" + + "id=" + + id + + ", first=" + + first + + ", second=" + + second + + ", third=" + + third + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", trait=" + + trait + + ", impl=" + + impl + + '}'; + } + } + + final class EvalQuaternary extends AbstractEval implements Coster, InterpretableCall { + private final String function; + private final String overload; + private final Interpretable first; + private final Interpretable second; + private final Interpretable third; + private final Interpretable fourth; + private final Trait trait; + private final QuaternaryOp impl; + + EvalQuaternary( + long id, + String function, + String overload, + Interpretable first, + Interpretable second, + Interpretable third, + Interpretable fourth, + Trait trait, + QuaternaryOp impl) { + super(id); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.first = Objects.requireNonNull(first); + this.second = Objects.requireNonNull(second); + this.third = Objects.requireNonNull(third); + this.fourth = Objects.requireNonNull(fourth); + this.trait = trait; + this.impl = Objects.requireNonNull(impl); + } + + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val firstVal = first.eval(ctx); + if (isUnknownOrError(firstVal)) { + return firstVal; + } + Val secondVal = second.eval(ctx); + if (isUnknownOrError(secondVal)) { + return secondVal; + } + Val thirdVal = third.eval(ctx); + if (isUnknownOrError(thirdVal)) { + return thirdVal; + } + Val fourthVal = fourth.eval(ctx); + if (isUnknownOrError(fourthVal)) { + return fourthVal; + } + if (trait == null || firstVal.type().hasTrait(trait)) { + return impl.invoke(firstVal, secondVal, thirdVal, fourthVal); + } + if (firstVal.type().hasTrait(Trait.ReceiverType)) { + return ((Receiver) firstVal).receive(function, overload, secondVal, thirdVal, fourthVal); + } + return noSuchOverload( + firstVal, function, overload, new Val[] {firstVal, secondVal, thirdVal, fourthVal}); + } + + @Override + public Cost cost() { + return estimateCost(first) + .add(estimateCost(second)) + .add(estimateCost(third)) + .add(estimateCost(fourth)) + .add(OneOne); + } + + @Override + public String function() { + return function; + } + + @Override + public String overloadID() { + return overload; + } + + @Override + public Interpretable[] args() { + return new Interpretable[] {first, second, third, fourth}; + } + + @Override + public String toString() { + return "EvalQuaternary{" + + "id=" + + id + + ", first=" + + first + + ", second=" + + second + + ", third=" + + third + + ", fourth=" + + fourth + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", trait=" + + trait + + ", impl=" + + impl + + '}'; + } + } + + final class EvalQuinary extends AbstractEval implements Coster, InterpretableCall { + private final String function; + private final String overload; + private final Interpretable first; + private final Interpretable second; + private final Interpretable third; + private final Interpretable fourth; + private final Interpretable fifth; + private final Trait trait; + private final QuinaryOp impl; + + EvalQuinary( + long id, + String function, + String overload, + Interpretable first, + Interpretable second, + Interpretable third, + Interpretable fourth, + Interpretable fifth, + Trait trait, + QuinaryOp impl) { + super(id); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.first = Objects.requireNonNull(first); + this.second = Objects.requireNonNull(second); + this.third = Objects.requireNonNull(third); + this.fourth = Objects.requireNonNull(fourth); + this.fifth = Objects.requireNonNull(fifth); + this.trait = trait; + this.impl = Objects.requireNonNull(impl); + } + + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val firstVal = first.eval(ctx); + if (isUnknownOrError(firstVal)) { + return firstVal; + } + Val secondVal = second.eval(ctx); + if (isUnknownOrError(secondVal)) { + return secondVal; + } + Val thirdVal = third.eval(ctx); + if (isUnknownOrError(thirdVal)) { + return thirdVal; + } + Val fourthVal = fourth.eval(ctx); + if (isUnknownOrError(fourthVal)) { + return fourthVal; + } + Val fifthVal = fifth.eval(ctx); + if (isUnknownOrError(fifthVal)) { + return fifthVal; + } + if (trait == null || firstVal.type().hasTrait(trait)) { + return impl.invoke(firstVal, secondVal, thirdVal, fourthVal, fifthVal); + } + if (firstVal.type().hasTrait(Trait.ReceiverType)) { + return ((Receiver) firstVal) + .receive(function, overload, secondVal, thirdVal, fourthVal, fifthVal); + } + return noSuchOverload( + firstVal, + function, + overload, + new Val[] {firstVal, secondVal, thirdVal, fourthVal, fifthVal}); + } + + @Override + public Cost cost() { + return estimateCost(first) + .add(estimateCost(second)) + .add(estimateCost(third)) + .add(estimateCost(fourth)) + .add(estimateCost(fifth)) + .add(OneOne); + } + + @Override + public String function() { + return function; + } + + @Override + public String overloadID() { + return overload; + } + + @Override + public Interpretable[] args() { + return new Interpretable[] {first, second, third, fourth, fifth}; + } + + @Override + public String toString() { + return "EvalQuinary{" + + "id=" + + id + + ", first=" + + first + + ", second=" + + second + + ", third=" + + third + + ", fourth=" + + fourth + + ", fifth=" + + fifth + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", trait=" + + trait + + ", impl=" + + impl + + '}'; + } + } + final class EvalVarArgs extends AbstractEval implements Coster, InterpretableCall { private final String function; private final String overload; 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 447450059..efe0d4c74 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java @@ -64,7 +64,10 @@ import org.projectnessie.cel.interpreter.Interpretable.EvalObj; import org.projectnessie.cel.interpreter.Interpretable.EvalOptionalOr; import org.projectnessie.cel.interpreter.Interpretable.EvalOr; +import org.projectnessie.cel.interpreter.Interpretable.EvalQuaternary; +import org.projectnessie.cel.interpreter.Interpretable.EvalQuinary; import org.projectnessie.cel.interpreter.Interpretable.EvalReceiverVarArgs; +import org.projectnessie.cel.interpreter.Interpretable.EvalTernary; import org.projectnessie.cel.interpreter.Interpretable.EvalTestOnly; import org.projectnessie.cel.interpreter.Interpretable.EvalUnary; import org.projectnessie.cel.interpreter.Interpretable.EvalVarArgs; @@ -74,6 +77,9 @@ import org.projectnessie.cel.interpreter.functions.BinaryOp; import org.projectnessie.cel.interpreter.functions.FunctionOp; import org.projectnessie.cel.interpreter.functions.Overload; +import org.projectnessie.cel.interpreter.functions.QuaternaryOp; +import org.projectnessie.cel.interpreter.functions.QuinaryOp; +import org.projectnessie.cel.interpreter.functions.TernaryOp; import org.projectnessie.cel.interpreter.functions.UnaryOp; /** interpretablePlanner creates an Interpretable evaluation plan from a proto Expr value. */ @@ -415,6 +421,10 @@ Interpretable planCall(Expr expr) { case 0 -> planCallZero(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef); case 1 -> planCallUnary(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, args); case 2 -> planCallBinary(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, args); + case 3 -> planCallTernary(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, args); + case 4 -> + planCallQuaternary(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, args); + case 5 -> planCallQuinary(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, args); default -> planCallVarArgs(expr, resolvedFunc.fnName, resolvedFunc.overloadId, fnDef, args); }; } @@ -458,6 +468,77 @@ static Interpretable planCallBinary( return new EvalBinary(expr.getId(), function, overload, args[0], args[1], trait, fn); } + /** planCallTernary generates a ternary or variable argument callable Interpretable. */ + Interpretable planCallTernary( + Expr expr, String function, String overload, Overload impl, Interpretable... args) { + if (impl == null) { + return new EvalReceiverVarArgs(expr.getId(), function, overload, args); + } + if (impl.ternary != null) { + TernaryOp fn = impl.ternary; + return new EvalTernary( + expr.getId(), function, overload, args[0], args[1], args[2], impl.operandTrait, fn); + } + if (impl.function != null) { + return new EvalVarArgs( + expr.getId(), function, overload, args, impl.operandTrait, impl.function); + } + throw new IllegalStateException(String.format("no such overload: %s(...)", function)); + } + + /** planCallQuaternary generates a quaternary or variable argument callable Interpretable. */ + Interpretable planCallQuaternary( + Expr expr, String function, String overload, Overload impl, Interpretable... args) { + if (impl == null) { + return new EvalReceiverVarArgs(expr.getId(), function, overload, args); + } + if (impl.quaternary != null) { + QuaternaryOp fn = impl.quaternary; + return new EvalQuaternary( + expr.getId(), + function, + overload, + args[0], + args[1], + args[2], + args[3], + impl.operandTrait, + fn); + } + if (impl.function != null) { + return new EvalVarArgs( + expr.getId(), function, overload, args, impl.operandTrait, impl.function); + } + throw new IllegalStateException(String.format("no such overload: %s(...)", function)); + } + + /** planCallQuinary generates a quinary or variable argument callable Interpretable. */ + Interpretable planCallQuinary( + Expr expr, String function, String overload, Overload impl, Interpretable... args) { + if (impl == null) { + return new EvalReceiverVarArgs(expr.getId(), function, overload, args); + } + if (impl.quinary != null) { + QuinaryOp fn = impl.quinary; + return new EvalQuinary( + expr.getId(), + function, + overload, + args[0], + args[1], + args[2], + args[3], + args[4], + impl.operandTrait, + fn); + } + if (impl.function != null) { + return new EvalVarArgs( + expr.getId(), function, overload, args, impl.operandTrait, impl.function); + } + throw new IllegalStateException(String.format("no such overload: %s(...)", function)); + } + /** planCallVarArgs generates a variable argument callable Interpretable. */ static Interpretable planCallVarArgs( Expr expr, String function, String overload, Overload impl, Interpretable... args) { diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/functions/Overload.java b/core/src/main/java/org/projectnessie/cel/interpreter/functions/Overload.java index 92b6596e3..0f2e3326d 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/functions/Overload.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/functions/Overload.java @@ -52,8 +52,8 @@ /** * Overload defines a named overload of a function, indicating an operand trait which must be - * present on the first argument to the overload as well as one of either a unary, binary, or - * function implementation. + * present on the first argument to the overload as well as one or more fixed-arity or generic + * function implementations. * *

The majority of operators within the expression language are unary or binary and the * specializations simplify the call contract for implementers of types with operator overloads. Any @@ -75,6 +75,15 @@ public final class Overload { /** Binary defines the overload with a BinaryOp implementation. May be nil. */ public final BinaryOp binary; + /** Ternary defines the overload with a TernaryOp implementation. May be nil. */ + public final TernaryOp ternary; + + /** Quaternary defines the overload with a QuaternaryOp implementation. May be nil. */ + public final QuaternaryOp quaternary; + + /** Quinary defines the overload with a QuinaryOp implementation. May be nil. */ + public final QuinaryOp quinary; + /** Function defines the overload with a FunctionOp implementation. May be nil. */ public final FunctionOp function; @@ -91,7 +100,7 @@ public static Overload unary(Operator operator, Trait trait, UnaryOp op) { } public static Overload unary(String operator, Trait trait, UnaryOp op) { - return new Overload(operator, trait, op, null, null); + return new Overload(operator, trait, op, null, null, null, null, null); } public static Overload binary(Operator operator, BinaryOp op) { @@ -107,7 +116,31 @@ public static Overload binary(Operator operator, Trait trait, BinaryOp op) { } public static Overload binary(String operator, Trait trait, BinaryOp op) { - return new Overload(operator, trait, null, op, null); + return new Overload(operator, trait, null, op, null, null, null, null); + } + + public static Overload ternary(String operator, TernaryOp op) { + return ternary(operator, null, op); + } + + public static Overload ternary(String operator, Trait trait, TernaryOp op) { + return new Overload(operator, trait, null, null, op, null, null, null); + } + + public static Overload quaternary(String operator, QuaternaryOp op) { + return quaternary(operator, null, op); + } + + public static Overload quaternary(String operator, Trait trait, QuaternaryOp op) { + return new Overload(operator, trait, null, null, null, op, null, null); + } + + public static Overload quinary(String operator, QuinaryOp op) { + return quinary(operator, null, op); + } + + public static Overload quinary(String operator, Trait trait, QuinaryOp op) { + return new Overload(operator, trait, null, null, null, null, op, null); } public static Overload function(String operator, FunctionOp op) { @@ -115,20 +148,63 @@ public static Overload function(String operator, FunctionOp op) { } public static Overload function(String operator, Trait trait, FunctionOp op) { - return new Overload(operator, trait, null, null, op); + return new Overload(operator, trait, null, null, null, null, null, op); } public static Overload overload( String operator, Trait trait, UnaryOp unary, BinaryOp binary, FunctionOp function) { - return new Overload(operator, trait, unary, binary, function); + return new Overload(operator, trait, unary, binary, null, null, null, function); + } + + public static Overload overload( + String operator, + Trait trait, + UnaryOp unary, + BinaryOp binary, + TernaryOp ternary, + FunctionOp function) { + return new Overload(operator, trait, unary, binary, ternary, null, null, function); + } + + public static Overload overload( + String operator, + Trait trait, + UnaryOp unary, + BinaryOp binary, + TernaryOp ternary, + QuaternaryOp quaternary, + FunctionOp function) { + return new Overload(operator, trait, unary, binary, ternary, quaternary, null, function); + } + + public static Overload overload( + String operator, + Trait trait, + UnaryOp unary, + BinaryOp binary, + TernaryOp ternary, + QuaternaryOp quaternary, + QuinaryOp quinary, + FunctionOp function) { + return new Overload(operator, trait, unary, binary, ternary, quaternary, quinary, function); } private Overload( - String operator, Trait operandTrait, UnaryOp unary, BinaryOp binary, FunctionOp function) { + String operator, + Trait operandTrait, + UnaryOp unary, + BinaryOp binary, + TernaryOp ternary, + QuaternaryOp quaternary, + QuinaryOp quinary, + FunctionOp function) { this.operator = operator; this.operandTrait = operandTrait; this.unary = unary; this.binary = binary; + this.ternary = ternary; + this.quaternary = quaternary; + this.quinary = quinary; this.function = function; } @@ -143,7 +219,16 @@ public String toString() { if (binary != null) { sb.append(", binary"); } - if (binary != null) { + if (ternary != null) { + sb.append(", ternary"); + } + if (quaternary != null) { + sb.append(", quaternary"); + } + if (quinary != null) { + sb.append(", quinary"); + } + if (function != null) { sb.append(", function"); } sb.append('}'); diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/functions/QuaternaryOp.java b/core/src/main/java/org/projectnessie/cel/interpreter/functions/QuaternaryOp.java new file mode 100644 index 000000000..d7793d35c --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/functions/QuaternaryOp.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter.functions; + +import org.projectnessie.cel.common.types.ref.Val; + +/** QuaternaryOp is a function that takes four values and produces an output. */ +@FunctionalInterface +public interface QuaternaryOp { + Val invoke(Val first, Val second, Val third, Val fourth); +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/functions/QuinaryOp.java b/core/src/main/java/org/projectnessie/cel/interpreter/functions/QuinaryOp.java new file mode 100644 index 000000000..c06cb22bf --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/functions/QuinaryOp.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter.functions; + +import org.projectnessie.cel.common.types.ref.Val; + +/** QuinaryOp is a function that takes five values and produces an output. */ +@FunctionalInterface +public interface QuinaryOp { + Val invoke(Val first, Val second, Val third, Val fourth, Val fifth); +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/functions/TernaryOp.java b/core/src/main/java/org/projectnessie/cel/interpreter/functions/TernaryOp.java new file mode 100644 index 000000000..d5d33ec9b --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/functions/TernaryOp.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter.functions; + +import org.projectnessie.cel.common.types.ref.Val; + +/** TernaryOp is a function that takes three values and produces an output. */ +@FunctionalInterface +public interface TernaryOp { + Val invoke(Val first, Val second, Val third); +} diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/OverloadDispatchTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/OverloadDispatchTest.java new file mode 100644 index 000000000..d10921a4b --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/interpreter/OverloadDispatchTest.java @@ -0,0 +1,819 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.interpreter; + +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.ProgramOption.functions; +import static org.projectnessie.cel.Util.mapOf; +import static org.projectnessie.cel.checker.Decls.Int; +import static org.projectnessie.cel.checker.Decls.newFunction; +import static org.projectnessie.cel.checker.Decls.newOverload; +import static org.projectnessie.cel.checker.Decls.newVar; +import static org.projectnessie.cel.common.containers.Container.defaultContainer; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.Types.boolOf; +import static org.projectnessie.cel.common.types.UnknownT.unknownOf; +import static org.projectnessie.cel.common.types.pb.ProtoTypeRegistry.newRegistry; +import static org.projectnessie.cel.interpreter.Activation.emptyActivation; +import static org.projectnessie.cel.interpreter.AttributeFactory.newAttributeFactory; +import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; +import static org.projectnessie.cel.interpreter.Coster.costOf; +import static org.projectnessie.cel.interpreter.Dispatcher.newDispatcher; +import static org.projectnessie.cel.interpreter.EvalState.newEvalState; +import static org.projectnessie.cel.interpreter.Interpreter.newInterpreter; +import static org.projectnessie.cel.interpreter.Interpreter.trackState; + +import com.google.api.expr.v1alpha1.Constant; +import com.google.api.expr.v1alpha1.Expr; +import com.google.api.expr.v1alpha1.Reference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.common.Source; +import org.projectnessie.cel.common.types.Err; +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.Receiver; +import org.projectnessie.cel.common.types.traits.Trait; +import org.projectnessie.cel.interpreter.Interpretable.EvalBinary; +import org.projectnessie.cel.interpreter.Interpretable.EvalQuaternary; +import org.projectnessie.cel.interpreter.Interpretable.EvalQuinary; +import org.projectnessie.cel.interpreter.Interpretable.EvalReceiverVarArgs; +import org.projectnessie.cel.interpreter.Interpretable.EvalTernary; +import org.projectnessie.cel.interpreter.Interpretable.EvalUnary; +import org.projectnessie.cel.interpreter.Interpretable.EvalVarArgs; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableCall; +import org.projectnessie.cel.interpreter.functions.Overload; +import org.projectnessie.cel.parser.Parser; +import org.projectnessie.cel.parser.Parser.ParseResult; + +@SuppressWarnings("removal") +class OverloadDispatchTest { + + private static final long CALL_ID = 100; + + @TestFactory + Stream checkedExactOverloadIdsWinAtEveryCurrentArity() { + return IntStream.rangeClosed(0, 5) + .mapToObj( + arity -> + DynamicTest.dynamicTest( + "arity " + arity, + () -> { + String function = "checked_exact_" + arity; + String overload = function + "_overload"; + Dispatcher dispatcher = newDispatcher(); + dispatcher.add( + operation(function, arity, -1), operation(overload, arity, arity)); + + Interpretable interpretable = + checkedCall(dispatcher, function, arity, reference(overload)); + + assertThat(interpretable.eval(emptyActivation())).isEqualTo(intOf(arity)); + assertCallShape(interpretable, function, overload, arity); + })); + } + + @TestFactory + Stream checkedFunctionNameFallbackHandlesZeroOrMultipleOverloadIds() { + return Stream.of( + DynamicTest.dynamicTest( + "zero overload IDs", () -> assertCheckedNameFallback(Reference.getDefaultInstance())), + DynamicTest.dynamicTest( + "multiple overload IDs", + () -> + assertCheckedNameFallback( + Reference.newBuilder() + .addOverloadId("unused_first") + .addOverloadId("unused_second") + .build()))); + } + + @Test + void uncheckedCallsResolveGlobalAndQualifiedNamesDuringPlanning() { + Dispatcher dispatcher = newDispatcher(); + dispatcher.add( + Overload.unary("global_dispatch", ignored -> intOf(1)), + Overload.unary("test.namespace.qualified", ignored -> intOf(2))); + Interpreter interpreter = interpreter(dispatcher); + + Interpretable global = uncheckedCall(interpreter, "global_dispatch(0)"); + Interpretable qualified = uncheckedCall(interpreter, "test.namespace.qualified(0)"); + + assertThat(global.eval(emptyActivation())).isEqualTo(intOf(1)); + assertBasicCallShape(global, "global_dispatch", "", 1); + assertThat(qualified.eval(emptyActivation())).isEqualTo(intOf(2)); + assertBasicCallShape(qualified, "test.namespace.qualified", "", 1); + } + + @Test + void traitSuccessUsesBoundOperationAndTraitMismatchUsesReceiver() { + RecordingReceiver receiver = new RecordingReceiver(); + AtomicInteger boundCalls = new AtomicInteger(); + Interpretable receiverArg = Interpretable.newConstValue(1, receiver); + + EvalUnary traitSuccess = + new EvalUnary( + CALL_ID, + "dispatch", + "receiver_trait", + receiverArg, + Trait.ReceiverType, + value -> { + boundCalls.incrementAndGet(); + return intOf(10); + }); + EvalUnary traitMismatch = + new EvalUnary( + CALL_ID, + "dispatch", + "adder_trait", + receiverArg, + Trait.AdderType, + value -> { + boundCalls.incrementAndGet(); + return intOf(20); + }); + + assertThat(traitSuccess.eval(emptyActivation())).isEqualTo(intOf(10)); + assertThat(boundCalls).hasValue(1); + assertThat(receiver.invocations).isZero(); + + assertThat(traitMismatch.eval(emptyActivation())).isEqualTo(intOf(0)); + assertThat(boundCalls).hasValue(1); + assertThat(receiver.invocations).isOne(); + assertThat(receiver.function).isEqualTo("dispatch"); + assertThat(receiver.overload).isEqualTo("adder_trait"); + } + + @Test + void ternaryTraitMismatchUsesReceiverWithoutInvokingBoundOperation() { + RecordingReceiver receiver = new RecordingReceiver(); + AtomicInteger boundCalls = new AtomicInteger(); + EvalTernary call = + new EvalTernary( + CALL_ID, + "dispatch", + "adder_trait", + Interpretable.newConstValue(1, receiver), + Interpretable.newConstValue(2, intOf(1)), + Interpretable.newConstValue(3, intOf(2)), + Trait.AdderType, + (first, second, third) -> { + boundCalls.incrementAndGet(); + return intOf(20); + }); + + assertThat(call.eval(emptyActivation())).isEqualTo(intOf(2)); + assertThat(boundCalls).hasValue(0); + assertThat(receiver.invocations).isOne(); + assertThat(receiver.args).extracting(Val::intValue).containsExactly(1L, 2L); + } + + @Test + void ternaryTraitMismatchPreservesGenericNoSuchOverloadMessage() { + Interpretable[] args = { + Interpretable.newConstValue(1, intOf(1)), + Interpretable.newConstValue(2, intOf(2)), + Interpretable.newConstValue(3, intOf(3)) + }; + EvalVarArgs generic = + new EvalVarArgs( + CALL_ID, "dispatch", "receiver_trait", args, Trait.ReceiverType, values -> True); + EvalTernary ternary = + new EvalTernary( + CALL_ID, + "dispatch", + "receiver_trait", + args[0], + args[1], + args[2], + Trait.ReceiverType, + (first, second, third) -> True); + + assertThat(ternary.eval(emptyActivation()).toString()) + .isEqualTo(generic.eval(emptyActivation()).toString()); + } + + @Test + void quaternaryTraitMismatchPreservesGenericNoSuchOverloadMessage() { + Interpretable[] args = { + Interpretable.newConstValue(1, intOf(1)), + Interpretable.newConstValue(2, intOf(2)), + Interpretable.newConstValue(3, intOf(3)), + Interpretable.newConstValue(4, intOf(4)) + }; + EvalVarArgs generic = + new EvalVarArgs( + CALL_ID, "dispatch", "receiver_trait", args, Trait.ReceiverType, values -> True); + EvalQuaternary quaternary = + new EvalQuaternary( + CALL_ID, + "dispatch", + "receiver_trait", + args[0], + args[1], + args[2], + args[3], + Trait.ReceiverType, + (first, second, third, fourth) -> True); + + assertThat(quaternary.eval(emptyActivation()).toString()) + .isEqualTo(generic.eval(emptyActivation()).toString()); + } + + @Test + void quinaryTraitMismatchPreservesGenericNoSuchOverloadMessage() { + Interpretable[] args = { + Interpretable.newConstValue(1, intOf(1)), + Interpretable.newConstValue(2, intOf(2)), + Interpretable.newConstValue(3, intOf(3)), + Interpretable.newConstValue(4, intOf(4)), + Interpretable.newConstValue(5, intOf(5)) + }; + EvalVarArgs generic = + new EvalVarArgs( + CALL_ID, "dispatch", "receiver_trait", args, Trait.ReceiverType, values -> True); + EvalQuinary quinary = + new EvalQuinary( + CALL_ID, + "dispatch", + "receiver_trait", + args[0], + args[1], + args[2], + args[3], + args[4], + Trait.ReceiverType, + (first, second, third, fourth, fifth) -> True); + + assertThat(quinary.eval(emptyActivation()).toString()) + .isEqualTo(generic.eval(emptyActivation()).toString()); + } + + @Test + void nonReceiverTraitMismatchProducesNoSuchOverload() { + AtomicInteger boundCalls = new AtomicInteger(); + EvalUnary call = + new EvalUnary( + CALL_ID, + "dispatch", + "receiver_trait", + Interpretable.newConstValue(1, intOf(1)), + Trait.ReceiverType, + value -> { + boundCalls.incrementAndGet(); + return True; + }); + + Val result = call.eval(emptyActivation()); + + assertThat(result).isInstanceOf(Err.class); + assertThat(result.toString()).contains("no such overload"); + assertThat(boundCalls).hasValue(0); + } + + @TestFactory + Stream receiverFallbackPreservesEveryRelevantTailArity() { + return IntStream.rangeClosed(0, 5) + .mapToObj( + tailArity -> + DynamicTest.dynamicTest( + "tail arity " + tailArity, + () -> { + RecordingReceiver receiver = new RecordingReceiver(); + Interpretable[] args = receiverArgs(receiver, tailArity); + Interpretable call = receiverCall(args); + + assertThat(call.eval(emptyActivation())).isEqualTo(intOf(tailArity)); + assertThat(receiver.invocations).isOne(); + assertThat(receiver.function).isEqualTo("receive"); + assertThat(receiver.overload).isEqualTo("receive_overload"); + assertThat(receiver.args) + .extracting(Val::intValue) + .containsExactlyElementsOf( + IntStream.rangeClosed(1, tailArity) + .mapToLong(i -> i) + .boxed() + .toList()); + assertCallArgumentsAndCost(call, args); + })); + } + + @TestFactory + Stream genericCallsReturnFirstErrorOrUnknownWithoutInvokingTheOperation() { + return Stream.of(newErr("argument failed"), unknownOf(999)) + .flatMap( + terminal -> + IntStream.range(0, 4) + .mapToObj( + position -> + DynamicTest.dynamicTest( + terminal.getClass().getSimpleName() + " at argument " + position, + () -> assertGenericTerminalArgument(terminal, position)))); + } + + @TestFactory + Stream ternaryCallsReturnFirstErrorOrUnknownWithoutInvokingTheOperation() { + return Stream.of(newErr("argument failed"), unknownOf(999)) + .flatMap( + terminal -> + IntStream.range(0, 3) + .mapToObj( + position -> + DynamicTest.dynamicTest( + terminal.getClass().getSimpleName() + " at argument " + position, + () -> assertTernaryTerminalArgument(terminal, position)))); + } + + @TestFactory + Stream fixedAritiesRetainGenericVarArgsFallback() { + return IntStream.rangeClosed(3, 5) + .mapToObj( + arity -> + DynamicTest.dynamicTest( + "arity " + arity, + () -> { + String function = "generic_" + arity; + String overload = function + "_overload"; + Dispatcher dispatcher = newDispatcher(); + dispatcher.add(Overload.function(overload, args -> intOf(args.length))); + + Interpretable call = + checkedCall(dispatcher, function, arity, reference(overload)); + + assertThat(call).isInstanceOf(EvalVarArgs.class); + assertThat(call.eval(emptyActivation())).isEqualTo(intOf(arity)); + })); + } + + @TestFactory + Stream quaternaryCallsReturnFirstErrorOrUnknownWithoutInvokingTheOperation() { + return Stream.of(newErr("argument failed"), unknownOf(999)) + .flatMap( + terminal -> + IntStream.range(0, 4) + .mapToObj( + position -> + DynamicTest.dynamicTest( + terminal.getClass().getSimpleName() + " at argument " + position, + () -> assertQuaternaryTerminalArgument(terminal, position)))); + } + + @TestFactory + Stream quinaryCallsReturnFirstErrorOrUnknownWithoutInvokingTheOperation() { + return Stream.of(newErr("argument failed"), unknownOf(999)) + .flatMap( + terminal -> + IntStream.range(0, 5) + .mapToObj( + position -> + DynamicTest.dynamicTest( + terminal.getClass().getSimpleName() + " at argument " + position, + () -> assertQuinaryTerminalArgument(terminal, position)))); + } + + @Test + void binaryCallEvaluatesBothArgumentsBeforeReturningTheLeftError() { + List evaluationOrder = new ArrayList<>(); + AtomicInteger boundCalls = new AtomicInteger(); + Val error = newErr("left failed"); + EvalBinary call = + new EvalBinary( + CALL_ID, + "binary", + "binary_overload", + recordingArg(0, error, evaluationOrder), + recordingArg(1, intOf(1), evaluationOrder), + null, + (left, right) -> { + boundCalls.incrementAndGet(); + return True; + }); + + assertThat(call.eval(emptyActivation())).isSameAs(error); + assertThat(evaluationOrder).containsExactly(0, 1); + assertThat(boundCalls).hasValue(0); + } + + @Test + void checkedCallWithWrongActivationTypeReturnsCelError() { + String function = "checked_runtime_type"; + String overload = function + "_int"; + var env = + newEnv( + declarations( + newVar("value", Int), + newFunction(function, newOverload(overload, List.of(Int), Int)))); + AstIssuesTuple ast = env.compile(function + "(value)"); + assertThat(ast.hasIssues()).isFalse(); + Program program = + env.program( + ast.getAst(), + functions(Overload.unary(overload, Trait.NegatorType, ignored -> intOf(1)))); + + Val result = program.eval(mapOf("value", "not an int")).getVal(); + + assertThat(result).isInstanceOf(Err.class); + assertThat(result.toString()).contains("no such overload"); + } + + @TestFactory + Stream callShapeCostAndStateTrackingRemainStableAcrossArities() { + return IntStream.rangeClosed(0, 5) + .mapToObj( + arity -> + DynamicTest.dynamicTest( + "arity " + arity, + () -> { + String function = "tracked_" + arity; + String overload = function + "_overload"; + Dispatcher dispatcher = newDispatcher(); + dispatcher.add(operation(overload, arity, arity)); + + Interpretable call = + checkedCall(dispatcher, function, arity, reference(overload)); + assertCallShape(call, function, overload, arity); + assertThat(estimateCost(call)).isEqualTo(costOf(1, 1)); + + EvalState state = newEvalState(); + Interpretable tracked = + checkedCall( + dispatcher, function, arity, reference(overload), trackState(state)); + Val result = tracked.eval(emptyActivation()); + + assertThat(result).isEqualTo(intOf(arity)); + assertThat(state.value(CALL_ID)).isEqualTo(result); + for (int i = 0; i < arity; i++) { + assertThat(state.value(CALL_ID + i + 1)).isEqualTo(intOf(i + 1)); + } + assertThat(estimateCost(tracked)).isEqualTo(costOf(1, 1)); + })); + } + + private static void assertCheckedNameFallback(Reference reference) { + String function = "checked_name_fallback"; + Dispatcher dispatcher = newDispatcher(); + dispatcher.add( + Overload.unary(function, ignored -> intOf(42)), + Overload.unary("unused_first", ignored -> intOf(1)), + Overload.unary("unused_second", ignored -> intOf(2))); + + Interpretable interpretable = checkedCall(dispatcher, function, 1, reference); + + assertThat(interpretable.eval(emptyActivation())).isEqualTo(intOf(42)); + assertCallShape(interpretable, function, "", 1); + } + + private static void assertGenericTerminalArgument(Val terminal, int terminalPosition) { + List evaluationOrder = new ArrayList<>(); + AtomicInteger boundCalls = new AtomicInteger(); + Interpretable[] args = new Interpretable[4]; + for (int i = 0; i < args.length; i++) { + args[i] = recordingArg(i, i == terminalPosition ? terminal : intOf(i), evaluationOrder); + } + EvalVarArgs call = + new EvalVarArgs( + CALL_ID, + "generic", + "generic_overload", + args, + null, + values -> { + boundCalls.incrementAndGet(); + return True; + }); + + assertThat(call.eval(emptyActivation())).isSameAs(terminal); + assertThat(evaluationOrder) + .containsExactlyElementsOf(IntStream.rangeClosed(0, terminalPosition).boxed().toList()); + assertThat(boundCalls).hasValue(0); + } + + private static void assertTernaryTerminalArgument(Val terminal, int terminalPosition) { + List evaluationOrder = new ArrayList<>(); + AtomicInteger boundCalls = new AtomicInteger(); + Interpretable[] args = new Interpretable[3]; + for (int i = 0; i < args.length; i++) { + args[i] = recordingArg(i, i == terminalPosition ? terminal : intOf(i), evaluationOrder); + } + EvalTernary call = + new EvalTernary( + CALL_ID, + "ternary", + "ternary_overload", + args[0], + args[1], + args[2], + null, + (first, second, third) -> { + boundCalls.incrementAndGet(); + return True; + }); + + assertThat(call.eval(emptyActivation())).isSameAs(terminal); + assertThat(evaluationOrder) + .containsExactlyElementsOf(IntStream.rangeClosed(0, terminalPosition).boxed().toList()); + assertThat(boundCalls).hasValue(0); + } + + private static void assertQuaternaryTerminalArgument(Val terminal, int terminalPosition) { + List evaluationOrder = new ArrayList<>(); + AtomicInteger boundCalls = new AtomicInteger(); + Interpretable[] args = new Interpretable[4]; + for (int i = 0; i < args.length; i++) { + args[i] = recordingArg(i, i == terminalPosition ? terminal : intOf(i), evaluationOrder); + } + EvalQuaternary call = + new EvalQuaternary( + CALL_ID, + "quaternary", + "quaternary_overload", + args[0], + args[1], + args[2], + args[3], + null, + (first, second, third, fourth) -> { + boundCalls.incrementAndGet(); + return True; + }); + + assertThat(call.eval(emptyActivation())).isSameAs(terminal); + assertThat(evaluationOrder) + .containsExactlyElementsOf(IntStream.rangeClosed(0, terminalPosition).boxed().toList()); + assertThat(boundCalls).hasValue(0); + } + + private static void assertQuinaryTerminalArgument(Val terminal, int terminalPosition) { + List evaluationOrder = new ArrayList<>(); + AtomicInteger boundCalls = new AtomicInteger(); + Interpretable[] args = new Interpretable[5]; + for (int i = 0; i < args.length; i++) { + args[i] = recordingArg(i, i == terminalPosition ? terminal : intOf(i), evaluationOrder); + } + EvalQuinary call = + new EvalQuinary( + CALL_ID, + "quinary", + "quinary_overload", + args[0], + args[1], + args[2], + args[3], + args[4], + null, + (first, second, third, fourth, fifth) -> { + boundCalls.incrementAndGet(); + return True; + }); + + assertThat(call.eval(emptyActivation())).isSameAs(terminal); + assertThat(evaluationOrder) + .containsExactlyElementsOf(IntStream.rangeClosed(0, terminalPosition).boxed().toList()); + assertThat(boundCalls).hasValue(0); + } + + private static Interpretable receiverCall(Interpretable[] args) { + return switch (args.length) { + case 1 -> new EvalUnary(CALL_ID, "receive", "receive_overload", args[0], null, null); + case 2 -> + new EvalBinary(CALL_ID, "receive", "receive_overload", args[0], args[1], null, null); + default -> new EvalReceiverVarArgs(CALL_ID, "receive", "receive_overload", args); + }; + } + + private static Interpretable[] receiverArgs(RecordingReceiver receiver, int tailArity) { + Interpretable[] args = new Interpretable[tailArity + 1]; + args[0] = Interpretable.newConstValue(1, receiver); + for (int i = 1; i < args.length; i++) { + args[i] = Interpretable.newConstValue(i + 1, intOf(i)); + } + return args; + } + + private static void assertCallArgumentsAndCost( + Interpretable interpretable, Interpretable[] expectedArgs) { + assertThat(interpretable).isInstanceOf(InterpretableCall.class); + InterpretableCall call = (InterpretableCall) interpretable; + assertThat(call.args()).containsExactly(expectedArgs); + assertThat(estimateCost(call)).isEqualTo(costOf(1, 1)); + } + + private static Interpretable recordingArg(int index, Val value, List evaluationOrder) { + return new Interpretable() { + @Override + public long id() { + return index; + } + + @Override + public Val eval(Activation activation) { + evaluationOrder.add(index); + return value; + } + }; + } + + private static void assertCallShape( + Interpretable interpretable, String function, String overload, int arity) { + assertBasicCallShape(interpretable, function, overload, arity); + InterpretableCall call = (InterpretableCall) interpretable; + assertThat(call.args()) + .extracting(Interpretable::id) + .containsExactlyElementsOf( + IntStream.range(0, arity).mapToLong(i -> CALL_ID + i + 1).boxed().toList()); + } + + private static void assertBasicCallShape( + Interpretable interpretable, String function, String overload, int arity) { + assertThat(interpretable).isInstanceOf(InterpretableCall.class); + InterpretableCall call = (InterpretableCall) interpretable; + assertThat(call.function()).isEqualTo(function); + assertThat(call.overloadID()).isEqualTo(overload); + assertThat(call.args()).hasSize(arity); + } + + private static Interpretable checkedCall( + Dispatcher dispatcher, String function, int arity, Reference reference) { + return checkedCall(dispatcher, function, arity, reference, new InterpretableDecorator[0]); + } + + private static Interpretable checkedCall( + Dispatcher dispatcher, + String function, + int arity, + Reference reference, + InterpretableDecorator... decorators) { + return interpreter(dispatcher) + .newInterpretable( + callExpr(function, arity), mapOf(CALL_ID, reference), emptyMap(), decorators); + } + + private static Interpretable uncheckedCall(Interpreter interpreter, String expression) { + ParseResult parsed = Parser.parseAllMacros(Source.newTextSource(expression)); + assertThat(parsed.hasErrors()).withFailMessage(parsed.getErrors()::toDisplayString).isFalse(); + return interpreter.newUncheckedInterpretable(parsed.getExpr()); + } + + private static Interpreter interpreter(Dispatcher dispatcher) { + TypeRegistry registry = newRegistry(); + AttributeFactory attributes = newAttributeFactory(defaultContainer, registry, registry); + return newInterpreter(dispatcher, defaultContainer, registry, registry, attributes); + } + + private static Expr callExpr(String function, int arity) { + Expr.Call.Builder call = Expr.Call.newBuilder().setFunction(function); + for (int i = 0; i < arity; i++) { + call.addArgs( + Expr.newBuilder() + .setId(CALL_ID + i + 1) + .setConstExpr(Constant.newBuilder().setInt64Value(i + 1))); + } + return Expr.newBuilder().setId(CALL_ID).setCallExpr(call).build(); + } + + private static Reference reference(String overload) { + return Reference.newBuilder().addOverloadId(overload).build(); + } + + private static Overload operation(String name, int arity, long result) { + return switch (arity) { + case 0 -> Overload.function(name, args -> intOf(result)); + case 1 -> Overload.unary(name, arg -> intOf(result)); + case 2 -> Overload.binary(name, (left, right) -> intOf(result)); + case 3 -> Overload.ternary(name, (first, second, third) -> intOf(result)); + case 4 -> Overload.quaternary(name, (first, second, third, fourth) -> intOf(result)); + case 5 -> Overload.quinary(name, (first, second, third, fourth, fifth) -> intOf(result)); + default -> Overload.function(name, args -> intOf(result)); + }; + } + + private static final Type RECEIVER_TYPE = + new Type() { + @Override + public boolean hasTrait(Trait trait) { + return trait == Trait.ReceiverType; + } + + @Override + public String typeName() { + return "recording_receiver"; + } + + @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 RecordingReceiver extends BaseVal implements Receiver { + private int invocations; + private String function; + private String overload; + private List args = List.of(); + + @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_TYPE; + } + + @Override + public Object value() { + return this; + } + + @Override + public Val receive(String function, String overload, Val... args) { + invocations++; + this.function = function; + this.overload = overload; + this.args = Arrays.asList(args.clone()); + return intOf(args.length); + } + } +}