From 6d647a2480a638ff76e6fa468cdbf8cf5bd377f5 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Fri, 16 Oct 2026 19:26:35 +0200 Subject: [PATCH] Prepare and restructure interpreter implementation Introduce allocation-free activation lookup and shared numeric comparison semantics. Extract established planner, dispatcher, activation, and evaluation implementations from public interfaces, update implementation references, and isolate supplier memoization and partial-pattern arrays in activation-owned state. This intentionally breaks source and binary compatibility for concrete interpreter and activation implementation classes previously exposed as public interface members, including `Interpretable.EvalOr`. These classes move to package-private implementations without compatibility shims. The supported evaluator interfaces, factories, and inspection contracts - including `InterpretableConst`, `InterpretableAttribute`, and `InterpretableCall` - remain public. Integrations should use those contracts rather than depend on concrete implementation types. --- .../cel/EvaluatorBaselineBench.java | 197 ++ .../cel/interpreter/FieldAccessBench.java | 2 - .../interpreter/OverloadDispatchBench.java | 9 - .../cel/common/types/DoubleT.java | 24 +- .../projectnessie/cel/common/types/IntT.java | 63 +- .../cel/common/types/NumericComparison.java | 111 + .../projectnessie/cel/common/types/UintT.java | 82 +- .../cel/interpreter/Activation.java | 314 +- .../cel/interpreter/ActivationFunction.java | 52 + .../ActivationImplementations.java | 231 ++ .../cel/interpreter/AttributeFactory.java | 2108 ++++++------ .../cel/interpreter/DefaultDispatcher.java | 62 + .../cel/interpreter/Dispatcher.java | 50 - .../EstablishedAggregateNodes.java | 398 +++ .../cel/interpreter/EstablishedCallNodes.java | 706 +++++ .../EstablishedConstructionNodes.java | 181 ++ .../interpreter/EstablishedControlNodes.java | 207 ++ .../EstablishedDecoratorNodes.java | 464 +++ .../cel/interpreter/EstablishedFoldNodes.java | 516 +++ .../interpreter/EstablishedScalarNodes.java | 479 +++ .../cel/interpreter/ExprInterpreter.java | 75 + .../cel/interpreter/Interpretable.java | 2812 +---------------- .../interpreter/InterpretableDecorator.java | 18 - .../cel/interpreter/InterpretablePlanner.java | 1055 ------- .../cel/interpreter/Interpreter.java | 51 - .../cel/interpreter/Planner.java | 1055 +++++++ .../cel/interpreter/ResolvedValue.java | 35 + .../org/projectnessie/ListContainsTest.java | 8 +- .../common/types/NumericSemanticsTest.java | 84 + .../cel/interpreter/ActivationTest.java | 268 +- .../interpreter/AttributePatternsTest.java | 15 + .../cel/interpreter/InterpreterTest.java | 4 +- .../cel/interpreter/OverloadDispatchTest.java | 7 - .../InterpreterPublicApiCompileTest.java | 100 + .../cel/JacksonEvaluatorBaselineBench.java | 130 + .../types/jackson/Jackson2ScriptHostTest.java | 19 +- .../cel/Jackson3EvaluatorBaselineBench.java | 130 + .../jackson3/Jackson3ScriptHostTest.java | 19 +- .../org/projectnessie/cel/tools/Script.java | 13 + .../cel/tools/ScriptHostTest.java | 28 +- 40 files changed, 6670 insertions(+), 5512 deletions(-) create mode 100644 core/src/jmh/java/org/projectnessie/cel/EvaluatorBaselineBench.java create mode 100644 core/src/main/java/org/projectnessie/cel/common/types/NumericComparison.java create mode 100644 core/src/main/java/org/projectnessie/cel/interpreter/ActivationFunction.java create mode 100644 core/src/main/java/org/projectnessie/cel/interpreter/ActivationImplementations.java create mode 100644 core/src/main/java/org/projectnessie/cel/interpreter/DefaultDispatcher.java create mode 100644 core/src/main/java/org/projectnessie/cel/interpreter/EstablishedAggregateNodes.java create mode 100644 core/src/main/java/org/projectnessie/cel/interpreter/EstablishedCallNodes.java create mode 100644 core/src/main/java/org/projectnessie/cel/interpreter/EstablishedConstructionNodes.java create mode 100644 core/src/main/java/org/projectnessie/cel/interpreter/EstablishedControlNodes.java create mode 100644 core/src/main/java/org/projectnessie/cel/interpreter/EstablishedDecoratorNodes.java create mode 100644 core/src/main/java/org/projectnessie/cel/interpreter/EstablishedFoldNodes.java create mode 100644 core/src/main/java/org/projectnessie/cel/interpreter/EstablishedScalarNodes.java create mode 100644 core/src/main/java/org/projectnessie/cel/interpreter/ExprInterpreter.java create mode 100644 core/src/main/java/org/projectnessie/cel/interpreter/Planner.java create mode 100644 core/src/test/java/org/projectnessie/cel/common/types/NumericSemanticsTest.java create mode 100644 core/src/test/java/org/projectnessie/cel/publicapi/InterpreterPublicApiCompileTest.java create mode 100644 jackson/src/jmh/java/org/projectnessie/cel/JacksonEvaluatorBaselineBench.java create mode 100644 jackson3/src/jmh/java/org/projectnessie/cel/Jackson3EvaluatorBaselineBench.java diff --git a/core/src/jmh/java/org/projectnessie/cel/EvaluatorBaselineBench.java b/core/src/jmh/java/org/projectnessie/cel/EvaluatorBaselineBench.java new file mode 100644 index 000000000..35025ca1b --- /dev/null +++ b/core/src/jmh/java/org/projectnessie/cel/EvaluatorBaselineBench.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2021 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; + +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.EvalOption.OptOptimize; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.interpreter.Activation.newActivation; + +import dev.cel.expr.conformance.proto3.TestAllTypes; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +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.Param; +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.AstIssuesTuple; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.interpreter.Activation; + +/** Attributes evaluator cost at the raw, public-program, and native-result boundaries. */ +@Warmup(iterations = 3, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class EvaluatorBaselineBench { + + @State(Scope.Benchmark) + public static class EvaluationState { + @Param({ + "identityInt", + "addInt", + "chainInt", + "chainDouble", + "chainString", + "shortCircuit", + "mapSelection", + "protoSelection" + }) + public String expression; + + Program program; + Prog internalProgram; + Map variables; + Activation activation; + Class nativeResultType; + Supplier nativeJava; + + @Setup + public void init() { + Env env; + String source; + switch (expression) { + case "identityInt": + env = newEnv(declarations(Decls.newVar("x", Decls.Int))); + source = "x"; + variables = Map.of("x", 41L); + nativeResultType = Long.class; + nativeJava = () -> variables.get("x"); + break; + case "addInt": + env = newEnv(declarations(Decls.newVar("x", Decls.Int))); + source = "x + 1"; + variables = Map.of("x", 41L); + nativeResultType = Long.class; + nativeJava = () -> (long) variables.get("x") + 1L; + break; + case "chainInt": + env = newEnv(declarations(Decls.newVar("x", Decls.Int))); + source = "((x + 1) * 3 - 2) / 2"; + variables = Map.of("x", 41L); + nativeResultType = Long.class; + nativeJava = () -> (((long) variables.get("x") + 1L) * 3L - 2L) / 2L; + break; + case "chainDouble": + env = newEnv(declarations(Decls.newVar("x", Decls.Double))); + source = "((x + 1.25) * 3.0 - 2.0) / 2.0"; + variables = Map.of("x", 41.5d); + nativeResultType = Double.class; + nativeJava = () -> (((double) variables.get("x") + 1.25d) * 3.0d - 2.0d) / 2.0d; + break; + case "chainString": + env = + newEnv( + declarations( + Decls.newVar("prefix", Decls.String), + Decls.newVar("x", Decls.String), + Decls.newVar("suffix", Decls.String))); + source = "prefix + x + suffix"; + variables = Map.of("prefix", "pre-", "x", "value", "suffix", "-post"); + nativeResultType = String.class; + nativeJava = + () -> (String) variables.get("prefix") + variables.get("x") + variables.get("suffix"); + break; + case "shortCircuit": + env = + newEnv( + declarations( + Decls.newVar("enabled", Decls.Bool), Decls.newVar("expensive", Decls.Bool))); + source = "enabled && expensive"; + variables = Map.of("enabled", false, "expensive", true); + nativeResultType = Boolean.class; + nativeJava = + () -> (boolean) variables.get("enabled") && (boolean) variables.get("expensive"); + break; + case "mapSelection": + env = + newEnv( + declarations( + Decls.newVar("attrs", Decls.newMapType(Decls.String, Decls.Int)), + Decls.newVar("key", Decls.String), + Decls.newVar("target", Decls.Int))); + source = "attrs[key] == target"; + variables = Map.of("attrs", Map.of("answer", 42L), "key", "answer", "target", 42L); + nativeResultType = Boolean.class; + nativeJava = + () -> + ((Map) variables.get("attrs")) + .get(variables.get("key")) + .equals(variables.get("target")); + break; + case "protoSelection": + env = + newEnv( + types(TestAllTypes.getDefaultInstance()), + declarations( + Decls.newVar( + "msg", Decls.newObjectType("cel.expr.conformance.proto3.TestAllTypes")), + Decls.newVar("target", Decls.Int))); + source = "msg.single_int64 == target"; + TestAllTypes message = TestAllTypes.newBuilder().setSingleInt64(42L).build(); + variables = Map.of("msg", message, "target", 42L); + nativeResultType = Boolean.class; + nativeJava = () -> message.getSingleInt64() == (long) variables.get("target"); + break; + default: + throw new IllegalArgumentException( + "Unknown evaluator benchmark expression: " + expression); + } + + AstIssuesTuple ast = env.compile(source); + if (ast.hasIssues()) { + throw ast.getIssues().err(); + } + program = env.program(ast.getAst(), evalOptions(OptOptimize)); + internalProgram = (Prog) program; + activation = newActivation(variables); + } + } + + @Benchmark + public Object rawEvaluator(EvaluationState state) { + return state.internalProgram.interpretable.eval(state.activation); + } + + @Benchmark + public Program.EvalResult programEval(EvaluationState state) { + return state.program.eval(state.variables); + } + + @Benchmark + public Object programEvalNative(EvaluationState state) { + return state.internalProgram.e.adapter.valueToNative( + state.program.eval(state.variables).getVal(), state.nativeResultType); + } + + @Benchmark + public Object nativeJava(EvaluationState state) { + return state.nativeJava.get(); + } +} diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/FieldAccessBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/FieldAccessBench.java index c3dee8dcb..440683276 100644 --- a/core/src/jmh/java/org/projectnessie/cel/interpreter/FieldAccessBench.java +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/FieldAccessBench.java @@ -45,8 +45,6 @@ import org.projectnessie.cel.common.types.ref.TypeRegistry; import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.interpreter.AttributeFactory.NamespacedAttribute; -import org.projectnessie.cel.interpreter.Interpretable.EvalAttr; -import org.projectnessie.cel.interpreter.Interpretable.EvalTestOnly; @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS) diff --git a/core/src/jmh/java/org/projectnessie/cel/interpreter/OverloadDispatchBench.java b/core/src/jmh/java/org/projectnessie/cel/interpreter/OverloadDispatchBench.java index 19f3600ef..6eb082a64 100644 --- a/core/src/jmh/java/org/projectnessie/cel/interpreter/OverloadDispatchBench.java +++ b/core/src/jmh/java/org/projectnessie/cel/interpreter/OverloadDispatchBench.java @@ -45,15 +45,6 @@ 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; diff --git a/core/src/main/java/org/projectnessie/cel/common/types/DoubleT.java b/core/src/main/java/org/projectnessie/cel/common/types/DoubleT.java index e720c12e6..87863b84f 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/DoubleT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/DoubleT.java @@ -19,7 +19,6 @@ import static org.projectnessie.cel.common.types.Err.newTypeConversionError; import static org.projectnessie.cel.common.types.Err.noSuchOverload; import static org.projectnessie.cel.common.types.Err.rangeError; -import static org.projectnessie.cel.common.types.IntT.IntZero; import static org.projectnessie.cel.common.types.IntT.intOf; import static org.projectnessie.cel.common.types.IntT.intOfCompare; import static org.projectnessie.cel.common.types.StringT.stringOf; @@ -193,18 +192,11 @@ public Val divide(Val other) { public Val compare(Val other) { switch (other.type().typeEnum()) { case Uint: + return intOfCompare(NumericComparison.compareDoubleUint(d, other.intValue())); case Int: + return intOfCompare(NumericComparison.compareDoubleInt(d, other.intValue())); case Double: - Val converted = other.convertToType(type()); - if (converted.type().typeEnum() == TypeEnum.Err) { - return converted; - } - double od = ((DoubleT) converted).d; - if (d == od) { - // work around for special case of -0.0d == 0.0d (IEEE 754) - return IntZero; - } - return intOfCompare(Double.compare(d, od)); + return intOfCompare(NumericComparison.compareDouble(d, other.doubleValue())); default: return noSuchOverload(this, "compare", other); } @@ -215,15 +207,11 @@ public Val compare(Val other) { public Val equal(Val other) { switch (other.type().typeEnum()) { case Uint: + return boolOf(NumericComparison.equalDoubleUint(d, other.intValue())); case Int: + return boolOf(NumericComparison.equalDoubleInt(d, other.intValue())); case Double: - Val converted = other.convertToType(type()); - if (converted.type().typeEnum() == TypeEnum.Err) { - return converted; - } - double o = ((DoubleT) converted).d; - // TODO: Handle NaNs properly. - return boolOf(d == o); + return boolOf(NumericComparison.equalDouble(d, other.doubleValue())); case Null: case Bool: case Bytes: diff --git a/core/src/main/java/org/projectnessie/cel/common/types/IntT.java b/core/src/main/java/org/projectnessie/cel/common/types/IntT.java index 9608fd491..36dd34918 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/IntT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/IntT.java @@ -16,7 +16,6 @@ package org.projectnessie.cel.common.types; import static org.projectnessie.cel.common.types.BoolT.False; -import static org.projectnessie.cel.common.types.DoubleT.DoubleType; import static org.projectnessie.cel.common.types.DoubleT.doubleOf; import static org.projectnessie.cel.common.types.Err.divideByZero; import static org.projectnessie.cel.common.types.Err.errIntOverflow; @@ -202,60 +201,24 @@ public Val convertToType(Type typeValue) { /** Compare implements traits.Comparer.Compare. */ @Override public Val compare(Val other) { - switch (other.type().typeEnum()) { - case Double: - DoubleT cmp = (DoubleT) convertToType(DoubleType); - return cmp.compare(other); - case Uint: - if (i < 0L) { - // this int is negative, so it MUST be smaller than any uint - return IntNegOne; - } - if (other.intValue() < 0L) { - // the OTHER uint is > Integer.MAX_VALUE, so THIS int MUST be smaller - return IntNegOne; - } - return intOfCompare(Long.compareUnsigned(i, other.intValue())); - case Int: - Val converted = other.convertToType(type()); - if (converted.type().typeEnum() == TypeEnum.Err) { - return converted; - } - return intOfCompare(Long.compare(i, converted.intValue())); - default: - return noSuchOverload(this, "compare", other); - } + return switch (other.type().typeEnum()) { + case Double -> intOfCompare(NumericComparison.compareIntDouble(i, other.doubleValue())); + case Uint -> intOfCompare(NumericComparison.compareIntUint(i, other.intValue())); + case Int -> intOfCompare(NumericComparison.compareInt(i, other.intValue())); + default -> noSuchOverload(this, "compare", other); + }; } /** Equal implements ref.Val.Equal. */ @Override public Val equal(Val other) { - switch (other.type().typeEnum()) { - case Double: - DoubleT cmp = (DoubleT) convertToType(DoubleType); - return cmp.equal(other); - case Uint: - if (other.intValue() < 0L) { - return False; - } - case Int: - Val converted = other.convertToType(type()); - if (converted.type().typeEnum() == TypeEnum.Err) { - return converted; - } - return boolOf(i == converted.intValue()); - case Null: - case Bool: - case Bytes: - case List: - case Map: - case Object: - case String: - case Type: - return False; - default: - return noSuchOverload(this, "equal", other); - } + return switch (other.type().typeEnum()) { + case Double -> boolOf(NumericComparison.equalIntDouble(i, other.doubleValue())); + case Uint -> boolOf(NumericComparison.equalIntUint(i, other.intValue())); + case Int -> boolOf(NumericComparison.equalInt(i, other.intValue())); + case Null, Bool, Bytes, List, Map, Object, String, Type -> False; + default -> noSuchOverload(this, "equal", other); + }; } /** Divide implements traits.Divider.Divide. */ diff --git a/core/src/main/java/org/projectnessie/cel/common/types/NumericComparison.java b/core/src/main/java/org/projectnessie/cel/common/types/NumericComparison.java new file mode 100644 index 000000000..2381e3371 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/common/types/NumericComparison.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2021 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.common.types; + +import java.math.BigInteger; + +/** Pure comparison and equality semantics shared by CEL's built-in numeric values. */ +final class NumericComparison { + private NumericComparison() {} + + static int compareInt(long left, long right) { + return Long.compare(left, right); + } + + static int compareIntUint(long left, long right) { + if (left < 0L || right < 0L) { + return -1; + } + return Long.compare(left, right); + } + + static int compareUintInt(long left, long right) { + return -compareIntUint(right, left); + } + + static int compareUint(long left, long right) { + return Long.compareUnsigned(left, right); + } + + static int compareIntDouble(long left, double right) { + return compareDouble((double) left, right); + } + + static int compareDoubleInt(double left, long right) { + return compareDouble(left, (double) right); + } + + static int compareUintDouble(long left, double right) { + if (right < 0.0d) { + return 1; + } + return compareDouble(unsignedLongToDouble(left), right); + } + + static int compareDoubleUint(double left, long right) { + return compareDouble(left, unsignedLongToDouble(right)); + } + + static int compareDouble(double left, double right) { + if (left == right) { + // CEL treats positive and negative zero as equal. + return 0; + } + return Double.compare(left, right); + } + + static boolean equalInt(long left, long right) { + return left == right; + } + + static boolean equalIntUint(long left, long right) { + return left >= 0L && right >= 0L && left == right; + } + + static boolean equalUintInt(long left, long right) { + return equalIntUint(right, left); + } + + static boolean equalUint(long left, long right) { + return left == right; + } + + static boolean equalIntDouble(long left, double right) { + return (double) left == right; + } + + static boolean equalDoubleInt(double left, long right) { + return left == (double) right; + } + + static boolean equalUintDouble(long left, double right) { + return unsignedLongToDouble(left) == right; + } + + static boolean equalDoubleUint(double left, long right) { + return left == unsignedLongToDouble(right); + } + + static boolean equalDouble(double left, double right) { + return left == right; + } + + private static double unsignedLongToDouble(long value) { + return value >= 0L + ? (double) value + : new BigInteger(Long.toUnsignedString(value)).doubleValue(); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/common/types/UintT.java b/core/src/main/java/org/projectnessie/cel/common/types/UintT.java index 569abb980..8a456e75a 100644 --- a/core/src/main/java/org/projectnessie/cel/common/types/UintT.java +++ b/core/src/main/java/org/projectnessie/cel/common/types/UintT.java @@ -16,7 +16,6 @@ package org.projectnessie.cel.common.types; import static org.projectnessie.cel.common.types.BoolT.False; -import static org.projectnessie.cel.common.types.DoubleT.DoubleType; import static org.projectnessie.cel.common.types.DoubleT.doubleOf; import static org.projectnessie.cel.common.types.Err.divideByZero; import static org.projectnessie.cel.common.types.Err.errUintOverflow; @@ -24,7 +23,6 @@ import static org.projectnessie.cel.common.types.Err.newTypeConversionError; import static org.projectnessie.cel.common.types.Err.noSuchOverload; import static org.projectnessie.cel.common.types.Err.rangeError; -import static org.projectnessie.cel.common.types.IntT.IntOne; import static org.projectnessie.cel.common.types.IntT.intOf; import static org.projectnessie.cel.common.types.IntT.intOfCompare; import static org.projectnessie.cel.common.types.IntT.maxIntJSON; @@ -174,78 +172,24 @@ public Val convertToType(Type typeValue) { /** Compare implements traits.Comparer.Compare. */ @Override public Val compare(Val other) { - switch (other.type().typeEnum()) { - case Int: - if (other.type().typeEnum() == TypeEnum.Err) { - return other; - } - if (other.intValue() < 0L) { - // the other int is < 0, so any uint is greater - return IntOne; - } - if (i < 0L) { - // this uint is > Long.MAX_VALUE, so it MUST be greater than any signed int - return IntOne; - } - return intOfCompare(Long.compareUnsigned(i, other.intValue())); - case Double: - if (other.type().typeEnum() == TypeEnum.Err) { - return other; - } - if (other.doubleValue() < 0d) { - // the other int is < 0, so any uint is greater - return IntOne; - } - DoubleT cmp = (DoubleT) convertToType(DoubleType); - return cmp.compare(other); - case Uint: - Val converted = other.convertToType(type()); - if (converted.type().typeEnum() == TypeEnum.Err) { - return converted; - } - return intOfCompare(Long.compareUnsigned(i, ((UintT) converted).i)); - default: - return noSuchOverload(this, "compare", other); - } + return switch (other.type().typeEnum()) { + case Int -> intOfCompare(NumericComparison.compareUintInt(i, other.intValue())); + case Double -> intOfCompare(NumericComparison.compareUintDouble(i, other.doubleValue())); + case Uint -> intOfCompare(NumericComparison.compareUint(i, other.intValue())); + default -> noSuchOverload(this, "compare", other); + }; } /** Equal implements ref.Val.Equal. */ @Override public Val equal(Val other) { - switch (other.type().typeEnum()) { - case Int: - if (other.type().typeEnum() == TypeEnum.Err) { - return other; - } - if (other.intValue() < 0L) { - // the other int is < 0, so no uint can be equal - return False; - } - if (i < 0L) { - // this uint is > Long.MAX_VALUE, so it CANNOT be equal - return False; - } - return boolOf(i == other.intValue()); - case Double: - return other.equal(this); - case Uint: - Val converted = other.convertToType(type()); - if (converted.type().typeEnum() == TypeEnum.Err) { - return converted; - } - return boolOf(i == converted.intValue()); - case Null: - case Bool: - case Bytes: - case List: - case Map: - case Object: - case String: - case Type: - return False; - default: - return noSuchOverload(this, "equal", other); - } + return switch (other.type().typeEnum()) { + case Int -> boolOf(NumericComparison.equalUintInt(i, other.intValue())); + case Double -> boolOf(NumericComparison.equalUintDouble(i, other.doubleValue())); + case Uint -> boolOf(NumericComparison.equalUint(i, other.intValue())); + case Null, Bool, Bytes, List, Map, Object, String, Type -> False; + default -> noSuchOverload(this, "equal", other); + }; } /** Divide implements traits.Divider.Divide. */ diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/Activation.java b/core/src/main/java/org/projectnessie/cel/interpreter/Activation.java index 5ad8c093a..e190067c4 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Activation.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Activation.java @@ -15,63 +15,104 @@ */ package org.projectnessie.cel.interpreter; -import java.util.Arrays; -import java.util.HashMap; import java.util.Map; import java.util.function.Function; -import java.util.function.Supplier; -import org.projectnessie.cel.common.types.ref.Val; /** - * Activation used to resolve identifiers by name and references by id. + * Supplies variable bindings to a CEL evaluation. * - *

An Activation is the primary mechanism by which a caller supplies input into a CEL program. + *

An activation distinguishes an absent binding ({@link ActivationFunction#ABSENT}) from a + * present CEL null binding (Java {@code null}). Values may be Java host representations or already + * adapted {@link org.projectnessie.cel.common.types.ref.Val} instances; the program's type adapter + * converts host values when they are consumed. + * + *

Activations and their backing objects may be retained for the lifetime of a program or + * evaluation. Callers must not mutate retained maps or bound values while evaluation is in + * progress. Custom implementations and resolver functions may be called concurrently when a program + * is shared and are responsible for their own thread safety. */ -public interface Activation { +public interface Activation extends ActivationFunction { + @SuppressWarnings("removal") + @Override + default Object resolve(String name) { + var resolved = resolveName(name); + if (resolved != null) { + return resolved.present() ? resolved.value() : ABSENT; + } + return ABSENT; + } + /** - * ResolveName returns a value from the activation by qualified name, or false if the name could - * not be found. + * Resolves a name using the legacy wrapper representation. + * + *

Replace with {@link #resolve(String)}. + * + * @param name qualified variable name + * @return the legacy resolved-value wrapper + * @deprecated use {@link #resolve(String)} */ + @SuppressWarnings({"DeprecatedIsStillUsed", "removal"}) + @Deprecated(forRemoval = true) ResolvedValue resolveName(String name); /** - * Parent returns the parent of the current activation, may be nil. If non-nil, the parent will be - * searched during resolve calls. + * Returns this activation's parent, if its implementation exposes one. + * + * @return the parent activation, or {@code null} */ - Activation parent(); + default Activation parent() { + return null; + } /** EmptyActivation returns a variable free activation. */ static Activation emptyActivation() { // This call cannot fail. - return newActivation(new HashMap()); + return newActivation(Map.of()); } /** - * NewActivation returns an activation based on a map-based binding where the map keys are - * expected to be qualified names used with ResolveName calls. + * Creates an activation from a supported binding source. * - *

The input `bindings` may either be of type `Activation` or `map[string]interface{}`. + *

The input {@code bindings} may be an {@link Activation}, a {@link Map}, a {@link Function}, + * or an {@link ActivationFunction}. An activation is returned unchanged. Map keys are qualified + * CEL variable names. An {@code ActivationFunction} uses {@link ActivationFunction#ABSENT} for + * absence and Java {@code null} for a present null. The legacy {@code Function} form treats both + * a Java {@code null} result and a legacy absent wrapper as absence. * - *

Lazy bindings may be supplied within the map-based input in either of the following forms: - - * func() interface{} - func() ref.Val + *

The activation retains a map input without copying it, but does not modify it. The caller + * must not mutate the map while the activation may be resolved. * - *

The output of the lazy binding will overwrite the variable reference in the internal map. + *

A map value may be a no-argument {@link java.util.function.Supplier} for lazy resolution. + * The activation invokes the supplier when the binding is first resolved and memoizes a + * successful result, including {@code null}, in activation-owned state. Concurrent resolutions + * share that result. If the supplier throws, the exception is propagated without memoizing a + * result, so a later resolution retries it. A supplier returned by a supplier is the resolved + * value and is not invoked by the activation. * *

Values which are not represented as ref.Val types on input may be adapted to a ref.Val using * the ref.TypeAdapter configured in the environment. + * + * @param bindings supported binding source + * @return an activation backed by {@code bindings} + * @throws NullPointerException if {@code bindings} is {@code null} + * @throws IllegalArgumentException if {@code bindings} has an unsupported type */ + @SuppressWarnings({"rawtypes", "unchecked", "removal"}) static Activation newActivation(Object bindings) { if (bindings == null) { throw new NullPointerException("bindings must be non-nil"); } - if (bindings instanceof Activation) { - return (Activation) bindings; + if (bindings instanceof Activation activation) { + return activation; + } + if (bindings instanceof Map map) { + return new MapActivation(map); } - if (bindings instanceof Map) { - return new MapActivation((Map) bindings); + if (bindings instanceof Function func) { + bindings = (ActivationFunction) name -> ResolvedValue.mapLegacy(func.apply(name)); } - if (bindings instanceof Function) { - return new FunctionActivation((Function) bindings); + if (bindings instanceof ActivationFunction activationFunction) { + return new FunctionActivation(activationFunction); } throw new IllegalArgumentException( String.format( @@ -79,125 +120,6 @@ static Activation newActivation(Object bindings) { bindings.getClass().getName())); } - /** - * mapActivation which implements Activation and maps of named values. - * - *

Named bindings may lazily supply values by providing a function which accepts no arguments - * and produces an interface value. - */ - final class MapActivation implements Activation { - private final Map bindings; - - MapActivation(Map bindings) { - this.bindings = bindings; - } - - /** Parent implements the Activation interface method. */ - @Override - public Activation parent() { - return null; - } - - /** ResolveName implements the Activation interface method. */ - @Override - public ResolvedValue resolveName(String name) { - if (name.startsWith(".")) { - name = name.substring(1); - } - Object obj = bindings.get(name); - if (obj == null) { - if (!bindings.containsKey(name)) { - return ResolvedValue.ABSENT; - } - return ResolvedValue.NULL_VALUE; - } - - if (obj instanceof Supplier) { - obj = ((Supplier) obj).get(); - bindings.put(name, obj); - } - return ResolvedValue.resolvedValue(obj); - } - - @Override - public String toString() { - return "MapActivation{" + "bindings=" + bindings + '}'; - } - } - - /** functionActivation which implements Activation and a provider of named values. */ - final class FunctionActivation implements Activation { - private final Function provider; - - FunctionActivation(Function provider) { - this.provider = provider; - } - - /** Parent implements the Activation interface method. */ - @Override - public Activation parent() { - return null; - } - - /** ResolveName implements the Activation interface method. */ - @Override - public ResolvedValue resolveName(String name) { - if (name.startsWith(".")) { - name = name.substring(1); - } - Object result = provider.apply(name); - if (result instanceof ResolvedValue) { - return (ResolvedValue) result; - } else if (result == null) { - return ResolvedValue.ABSENT; - } else { - return ResolvedValue.resolvedValue(result); - } - } - - @Override - public String toString() { - return "FunctionActivation{" + "provider=" + provider + '}'; - } - } - - /** - * hierarchicalActivation which implements Activation and contains a parent and child activation. - */ - final class HierarchicalActivation implements Activation { - private final Activation parent; - private final Activation child; - - HierarchicalActivation(Activation parent, Activation child) { - this.parent = parent; - this.child = child; - } - - /** Parent implements the Activation interface method. */ - @Override - public Activation parent() { - return parent; - } - - /** ResolveName implements the Activation interface method. */ - @Override - public ResolvedValue resolveName(String name) { - if (name.startsWith(".")) { - return parent.resolveName(name.substring(1)); - } - ResolvedValue object = child.resolveName(name); - if (object.present()) { - return object; - } - return parent.resolveName(name); - } - - @Override - public String toString() { - return "HierarchicalActivation{" + "parent=" + parent + ", child=" + child + '}'; - } - } - /** * NewHierarchicalActivation takes two activations and produces a new one which prioritizes * resolution in the child first and parent(s) second. @@ -207,11 +129,17 @@ static Activation newHierarchicalActivation(Activation parent, Activation child) } /** - * NewPartialActivation returns an Activation which contains a list of AttributePattern values - * representing field and index operations that should result in a 'types.Unknown' result. + * Creates an activation that marks matching attribute paths as unknown. + * + *

{@code bindings} accepts every source supported by {@link #newActivation(Object)}. The + * pattern array is copied, but each mutable {@link AttributePattern} is retained. Complete + * pattern construction before creating or sharing the activation. Callers receive a cloned array + * from {@link PartialActivation#unknownAttributePatterns()}. * - *

The `bindings` value may be any value type supported by the interpreter.NewActivation call, - * but is typically either an existing Activation or map[string]interface{}. + * @param bindings supported binding source + * @param unknowns non-null attribute patterns that should produce CEL unknown values + * @return a partial activation + * @throws NullPointerException if {@code bindings} or {@code unknowns} is {@code null} */ static PartialActivation newPartialActivation(Object bindings, AttributePattern... unknowns) { Activation a = newActivation(bindings); @@ -222,91 +150,13 @@ static PartialActivation newPartialActivation(Object bindings, AttributePattern. interface PartialActivation extends Activation { /** - * UnknownAttributePaths returns a set of AttributePattern values which match Attribute - * expressions for data accesses whose values are not yet known. + * Returns the attribute patterns whose matching accesses are unknown. + * + *

The returned array is a new shallow copy. Its mutable pattern elements remain shared with + * this activation. + * + * @return a copy of the unknown-pattern array */ AttributePattern[] unknownAttributePatterns(); } - - /** partActivation is the default implementations of the PartialActivation interface. */ - final class PartActivation implements PartialActivation { - private final Activation delegate; - private final AttributePattern[] unknowns; - - PartActivation(Activation delegate, AttributePattern[] unknowns) { - this.delegate = delegate; - this.unknowns = unknowns; - } - - @Override - public Activation parent() { - return delegate.parent(); - } - - @Override - public ResolvedValue resolveName(String name) { - return delegate.resolveName(name); - } - - /** UnknownAttributePatterns implements the PartialActivation interface method. */ - @Override - public AttributePattern[] unknownAttributePatterns() { - return unknowns; - } - - @Override - public String toString() { - return "PartActivation{" - + "delegate=" - + delegate - + ", unknowns=" - + Arrays.toString(unknowns) - + '}'; - } - } - - /** - * varActivation represents a single mutable variable binding. - * - *

This activation type should only be used within folds as the fold loop controls the object - * life-cycle. - */ - final class VarActivation implements Activation { - Activation parent; - String name; - Val val; - - VarActivation() {} - - /** Parent implements the Activation interface method. */ - @Override - public Activation parent() { - return parent; - } - - /** ResolveName implements the Activation interface method. */ - @Override - public ResolvedValue resolveName(String name) { - if (name.startsWith(".")) { - return parent.resolveName(name.substring(1)); - } - if (name.equals(this.name)) { - return ResolvedValue.resolvedValue(val); - } - return parent.resolveName(name); - } - - @Override - public String toString() { - return "VarActivation{" - + "parent=" - + parent - + ", name='" - + name - + '\'' - + ", val=" - + val - + '}'; - } - } } diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/ActivationFunction.java b/core/src/main/java/org/projectnessie/cel/interpreter/ActivationFunction.java new file mode 100644 index 000000000..1c05ee4c4 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/ActivationFunction.java @@ -0,0 +1,52 @@ +/* + * 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; + +/** + * Resolves CEL variable names to caller-supplied Java values. + * + *

Return {@link #ABSENT} by identity when a name has no binding. Java {@code null} is a present + * binding representing CEL null and is therefore distinct from absence. The resolver is retained by + * an activation created with {@link Activation#newActivation(Object)} and may be called + * concurrently when a program is shared; implementations must provide the corresponding thread + * safety and should not mutate evaluation inputs. + */ +@FunctionalInterface +public interface ActivationFunction { + /** + * Identity sentinel returned for an unresolved variable. + * + *

Do not expose this object as an actual variable value. + */ + Object ABSENT = + new Object() { + @Override + public String toString() { + return ""; + } + }; + + /** + * Resolves a value by qualified CEL variable name. + * + *

Return the exact {@link #ABSENT} singleton when unresolved; consumers compare it by + * identity. Java {@code null} means the name is present with a CEL null value. + * + * @param name qualified variable name requested by the evaluator + * @return the resolved Java or CEL value, {@code null} for a present null, or {@link #ABSENT} + */ + Object resolve(String name); +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/ActivationImplementations.java b/core/src/main/java/org/projectnessie/cel/interpreter/ActivationImplementations.java new file mode 100644 index 000000000..179b097c6 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/ActivationImplementations.java @@ -0,0 +1,231 @@ +/* + * 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 java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.Activation.PartialActivation; + +/** + * Activation backed by a map of named values. + * + *

The source map is retained but never mutated and must remain stable while the activation may + * be resolved. Named bindings may lazily supply values through a no-argument {@link Supplier}; + * successful results are memoized in activation-owned state. + */ +final class MapActivation implements Activation { + private final Map bindings; + private Map memoizedSupplierValues; + + MapActivation(Map bindings) { + this.bindings = bindings; + } + + @Override + public Object resolve(String name) { + if (name.startsWith(".")) { + name = name.substring(1); + } + Object obj = bindings.get(name); + if (obj == null) { + if (!bindings.containsKey(name)) { + return ABSENT; + } + return null; + } + + if (obj instanceof Supplier supplier) { + return resolveSupplier(name, supplier); + } + return obj; + } + + private synchronized Object resolveSupplier(String name, Supplier supplier) { + if (memoizedSupplierValues != null && memoizedSupplierValues.containsKey(name)) { + return memoizedSupplierValues.get(name); + } + + Object value = supplier.get(); + if (memoizedSupplierValues == null) { + memoizedSupplierValues = new HashMap<>(); + } + memoizedSupplierValues.put(name, value); + return value; + } + + @SuppressWarnings("removal") + @Override + public ResolvedValue resolveName(String name) { + return ResolvedValue.mapTo(resolve(name)); + } + + @Override + public String toString() { + return "MapActivation{" + "bindings=" + bindings + '}'; + } +} + +/** Activation backed by a provider of named values. */ +final class FunctionActivation implements Activation { + private final ActivationFunction provider; + + FunctionActivation(ActivationFunction provider) { + this.provider = provider; + } + + @Override + public Object resolve(String name) { + if (name.startsWith(".")) { + name = name.substring(1); + } + return provider.resolve(name); + } + + @SuppressWarnings("removal") + @Override + public ResolvedValue resolveName(String name) { + return ResolvedValue.mapTo(resolve(name)); + } + + @Override + public String toString() { + return "FunctionActivation{" + "provider=" + provider + '}'; + } +} + +/** Activation which resolves against its child before its parent. */ +final class HierarchicalActivation implements Activation { + private final Activation parent; + private final Activation child; + + HierarchicalActivation(Activation parent, Activation child) { + this.parent = parent; + this.child = child; + } + + @Override + public Activation parent() { + return parent; + } + + @Override + public Object resolve(String name) { + if (name.startsWith(".")) { + return parent.resolve(name.substring(1)); + } + var resolvedName = child.resolve(name); + if (ABSENT != resolvedName) { + return resolvedName; + } + return parent.resolve(name); + } + + @SuppressWarnings("removal") + @Override + public ResolvedValue resolveName(String name) { + return ResolvedValue.mapTo(resolve(name)); + } + + @Override + public String toString() { + return "HierarchicalActivation{" + "parent=" + parent + ", child=" + child + '}'; + } +} + +/** Default implementation of {@link PartialActivation}. */ +final class PartActivation implements PartialActivation { + private final Activation delegate; + private final AttributePattern[] unknowns; + + PartActivation(Activation delegate, AttributePattern[] unknowns) { + this.delegate = delegate; + this.unknowns = unknowns.clone(); + } + + @Override + public Activation parent() { + return delegate.parent(); + } + + @Override + public Object resolve(String name) { + return delegate.resolve(name); + } + + @SuppressWarnings("removal") + @Override + public ResolvedValue resolveName(String name) { + return ResolvedValue.mapTo(resolve(name)); + } + + @Override + public AttributePattern[] unknownAttributePatterns() { + return unknowns.clone(); + } + + @Override + public String toString() { + return "PartActivation{" + + "delegate=" + + delegate + + ", unknowns=" + + Arrays.toString(unknowns) + + '}'; + } +} + +/** + * A single mutable variable binding. + * + *

This activation type is used only within folds, whose loop controls its lifecycle. + */ +final class VarActivation implements Activation { + Activation parent; + String name; + Val val; + + VarActivation() {} + + @Override + public Activation parent() { + return parent; + } + + @Override + public Object resolve(String name) { + if (name.startsWith(".")) { + return parent.resolve(name.substring(1)); + } + if (name.equals(this.name)) { + return val; + } + return parent.resolve(name); + } + + @SuppressWarnings("removal") + @Override + public ResolvedValue resolveName(String name) { + return ResolvedValue.mapTo(resolve(name)); + } + + @Override + public String toString() { + return "VarActivation{" + "parent=" + parent + ", name='" + name + '\'' + ", val=" + val + '}'; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/AttributeFactory.java b/core/src/main/java/org/projectnessie/cel/interpreter/AttributeFactory.java index 05c2b0ce9..68f5e234c 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/AttributeFactory.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/AttributeFactory.java @@ -33,6 +33,8 @@ import static org.projectnessie.cel.common.types.Types.boolOf; import static org.projectnessie.cel.common.types.UintT.uintOf; import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.interpreter.AttributeFactory.newQualifierStatic; +import static org.projectnessie.cel.interpreter.AttributeFactory.refResolve; import static org.projectnessie.cel.interpreter.Coster.costOf; import com.google.api.expr.v1alpha1.Type; @@ -53,6 +55,12 @@ import org.projectnessie.cel.common.types.traits.Indexer; import org.projectnessie.cel.common.types.traits.Lister; import org.projectnessie.cel.common.types.traits.Mapper; +import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; +import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifier; +import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifierEquator; +import org.projectnessie.cel.interpreter.AttributeFactory.NamespacedAttribute; +import org.projectnessie.cel.interpreter.AttributeFactory.Qualifier; +import org.projectnessie.cel.interpreter.AttributeFactory.ValQualifier; import org.projectnessie.cel.interpreter.AttributePattern.QualifierValueEquator; /** AttributeFactory provides methods creating Attribute and Qualifier values. */ @@ -179,1273 +187,1195 @@ static AttributeFactory newAttributeFactory(Container cont, TypeAdapter a, TypeP return new AttrFactory(cont, a, p); } - final class AttrFactory implements AttributeFactory { - private final Container container; - private final TypeAdapter adapter; - private final TypeProvider provider; - - AttrFactory(Container container, TypeAdapter adapter, TypeProvider provider) { - this.container = container; - this.adapter = adapter; - this.provider = provider; + static Qualifier newQualifierStatic(TypeAdapter adapter, long id, Object v) { + if (v instanceof Attribute) { + return new AttrQualifier(id, (Attribute) v); } - /** - * AbsoluteAttribute refers to a variable value and an optional qualifier path. - * - *

The namespaceNames represent the names the variable could have based on namespace - * resolution rules. - */ - @Override - public NamespacedAttribute absoluteAttribute(long id, String... names) { - return new AbsoluteAttribute(id, names, new ArrayList<>(), adapter, provider, this); - } + Class c = v.getClass(); - /** - * ConditionalAttribute supports the case where an attribute selection may occur on a - * conditional expression, e.g. (cond ? a : b).c - */ - @Override - public AttributeFactory.Attribute conditionalAttribute( - long id, Interpretable expr, AttributeFactory.Attribute t, AttributeFactory.Attribute f) { - return new ConditionalAttribute(id, expr, t, f, adapter, this); + if (v instanceof Val val) { + switch (val.type().typeEnum()) { + case String: + return new StringQualifier(id, (String) val.value(), val, adapter); + case Double: + return new DoubleQualifier(id, (double) val.value(), val, adapter); + case Int: + return new IntQualifier(id, val.intValue(), val, adapter); + case Uint: + return new UintQualifier(id, val.intValue(), val, adapter); + case Bool: + return new BoolQualifier(id, val.booleanValue(), val, adapter); + case Null: + // Not actually a qualifier, but conformance-tests require this, although it's actually an + // error condition. + return new NullQualifier(id, val, adapter); + } } - /** - * MaybeAttribute collects variants of unchecked AbsoluteAttribute values which could either be - * direct variable accesses or some combination of variable access with qualification. - */ - @Override - public Attribute maybeAttribute(long id, String name) { - List attrs = new ArrayList<>(); - attrs.add( - name.startsWith(".") - ? absoluteAttribute(id, name) - : absoluteAttribute(id, container.resolveCandidateNames(name))); - return new MaybeAttribute(id, attrs, adapter, provider, this); + if (c == String.class) { + return new StringQualifier(id, (String) v, stringOf((String) v), adapter); } - - /** RelativeAttribute refers to an expression and an optional qualifier path. */ - @Override - public Attribute relativeAttribute(long id, Interpretable operand) { - return new RelativeAttribute(id, operand, new ArrayList<>(), adapter, this); + if (c == ULong.class) { + long l = ((ULong) v).longValue(); + return new UintQualifier(id, l, uintOf(l), adapter); + } + if ((c == Byte.class) || (c == Short.class) || (c == Integer.class) || (c == Long.class)) { + long i = ((Number) v).longValue(); + return new IntQualifier(id, i, intOf(i), adapter); + } + if (c == Double.class) { + double b = (Double) v; + return new DoubleQualifier(id, b, doubleOf(b), adapter); + } + if (c == Boolean.class) { + boolean b = (Boolean) v; + return new BoolQualifier(id, b, boolOf(b), adapter); } - /** NewQualifier is an implementation of the AttributeFactory interface. */ - @Override - public AttributeFactory.Qualifier newQualifier(Type objType, long qualID, Object val) { - // Before creating a new qualifier check to see if this is a protobuf message field access. - // If so, use the precomputed GetFrom qualification method rather than the standard - // stringQualifier. - if (val instanceof String str) { - if (objType != null && !objType.getMessageType().isEmpty()) { - FieldType ft = provider.findFieldType(objType.getMessageType(), str); - if (ft != null && ft.isSet != null && ft.getFrom != null) { - return new FieldQualifier(qualID, str, ft, adapter); - } - } + throw new IllegalStateException( + String.format("invalid qualifier type: %s", v.getClass().getName())); + } + + /** + * refResolve attempts to convert the value to a CEL value and then uses reflection methods to try + * and resolve the qualifier. + */ + static Val refResolve(TypeAdapter adapter, Val idx, Object obj) { + Val celVal = adapter.nativeToValue(obj); + if (celVal instanceof Mapper mapper) { + Val elem = mapper.find(idx); + if (elem == null) { + return noSuchKey(idx); } - return newQualifierStatic(adapter, qualID, val); + return elem; } - - @Override - public String toString() { - return "AttrFactory{" - + "container=" - + container - + ", adapter=" - + adapter - + ", provider=" - + provider - + '}'; + if (celVal instanceof Lister list && idx instanceof IntT) { + long intIndex = idx.intValue(); + if (intIndex >= Integer.MIN_VALUE && intIndex <= Integer.MAX_VALUE) { + return list.nativeGetAt((int) intIndex); + } + } + if (celVal instanceof Indexer indexer) { + return indexer.get(idx); + } + if (isUnknown(celVal)) { + return celVal; } + // TODO: If the types.Err value contains more than just an error message at some point in the + // future, then it would be reasonable to return error values as ref.Val types rather than + // simple go error types. + throwErrorAsIllegalStateException(celVal); + return noSuchOverload(celVal, "ref-resolve", null); } +} - final class AbsoluteAttribute implements Qualifier, NamespacedAttribute, Coster { - final long id; +/** Default {@link AttributeFactory} implementation. */ +final class AttrFactory implements AttributeFactory { + private final Container container; + private final TypeAdapter adapter; + private final TypeProvider provider; - /** - * namespaceNames represent the names the variable could have based on declared container - * (package) of the expression. - */ - final String[] namespaceNames; - - final List qualifiers; - final TypeAdapter adapter; - final TypeProvider provider; - final AttributeFactory fac; - - AbsoluteAttribute( - long id, - String[] namespaceNames, - List qualifiers, - TypeAdapter adapter, - TypeProvider provider, - AttributeFactory fac) { - this.id = id; - this.namespaceNames = Objects.requireNonNull(namespaceNames); - this.qualifiers = Objects.requireNonNull(qualifiers); - this.adapter = Objects.requireNonNull(adapter); - this.provider = Objects.requireNonNull(provider); - this.fac = Objects.requireNonNull(fac); - } + AttrFactory(Container container, TypeAdapter adapter, TypeProvider provider) { + this.container = container; + this.adapter = adapter; + this.provider = provider; + } - /** ID implements the Attribute interface method. */ - @Override - public long id() { - return id; - } + /** + * AbsoluteAttribute refers to a variable value and an optional qualifier path. + * + *

The namespaceNames represent the names the variable could have based on namespace resolution + * rules. + */ + @Override + public NamespacedAttribute absoluteAttribute(long id, String... names) { + return new AbsoluteAttribute(id, names, new ArrayList<>(), adapter, provider, this); + } + + /** + * ConditionalAttribute supports the case where an attribute selection may occur on a conditional + * expression, e.g. (cond ? a : b).c + */ + @Override + public AttributeFactory.Attribute conditionalAttribute( + long id, Interpretable expr, AttributeFactory.Attribute t, AttributeFactory.Attribute f) { + return new ConditionalAttribute(id, expr, t, f, adapter, this); + } + + /** + * MaybeAttribute collects variants of unchecked AbsoluteAttribute values which could either be + * direct variable accesses or some combination of variable access with qualification. + */ + @Override + public Attribute maybeAttribute(long id, String name) { + List attrs = new ArrayList<>(); + attrs.add( + name.startsWith(".") + ? absoluteAttribute(id, name) + : absoluteAttribute(id, container.resolveCandidateNames(name))); + return new MaybeAttribute(id, attrs, adapter, provider, this); + } + + /** RelativeAttribute refers to an expression and an optional qualifier path. */ + @Override + public Attribute relativeAttribute(long id, Interpretable operand) { + return new RelativeAttribute(id, operand, new ArrayList<>(), adapter, this); + } - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - long min = 0L; - long max = 0L; - for (Qualifier q : qualifiers) { - Cost qc = Cost.estimateCost(q); - min += qc.min; - max += qc.max; + /** NewQualifier is an implementation of the AttributeFactory interface. */ + @Override + public AttributeFactory.Qualifier newQualifier(Type objType, long qualID, Object val) { + // Before creating a new qualifier check to see if this is a protobuf message field access. + // If so, use the precomputed GetFrom qualification method rather than the standard + // stringQualifier. + if (val instanceof String str) { + if (objType != null && !objType.getMessageType().isEmpty()) { + FieldType ft = provider.findFieldType(objType.getMessageType(), str); + if (ft != null && ft.isSet != null && ft.getFrom != null) { + return new FieldQualifier(qualID, str, ft, adapter); + } } - min++; // For object retrieval. - max++; - return costOf(min, max); } + return newQualifierStatic(adapter, qualID, val); + } - /** AddQualifier implements the Attribute interface method. */ - @Override - public Attribute addQualifier(AttributeFactory.Qualifier q) { - qualifiers.add(q); - return this; - } + @Override + public String toString() { + return "AttrFactory{" + + "container=" + + container + + ", adapter=" + + adapter + + ", provider=" + + provider + + '}'; + } +} - /** CandidateVariableNames implements the NamespaceAttribute interface method. */ - @Override - public String[] candidateVariableNames() { - return namespaceNames; - } +/** + * @param namespaceNames namespaceNames represent the names the variable could have based on + * declared container (package) of the expression. + */ +record AbsoluteAttribute( + long id, + String[] namespaceNames, + List qualifiers, + TypeAdapter adapter, + TypeProvider provider, + AttributeFactory fac) + implements Qualifier, NamespacedAttribute, Coster { + AbsoluteAttribute( + long id, + String[] namespaceNames, + List qualifiers, + TypeAdapter adapter, + TypeProvider provider, + AttributeFactory fac) { + this.id = id; + this.namespaceNames = Objects.requireNonNull(namespaceNames); + this.qualifiers = Objects.requireNonNull(qualifiers); + this.adapter = Objects.requireNonNull(adapter); + this.provider = Objects.requireNonNull(provider); + this.fac = Objects.requireNonNull(fac); + } - /** - * Qualifiers returns the list of Qualifier instances associated with the namespaced attribute. - */ - @Override - public List qualifiers() { - return qualifiers; - } + /** ID implements the Attribute interface method. */ + @Override + public long id() { + return id; + } - /** Qualify is an implementation of the Qualifier interface method. */ - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - Object val = resolve(vars); - if (isUnknown(val)) { - return val; - } - Qualifier qual = fac.newQualifier(null, id, val); - return qual.qualify(vars, obj); - } + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + long min = 0L; + long max = 0L; + for (Qualifier q : qualifiers) { + Cost qc = Cost.estimateCost(q); + min += qc.min; + max += qc.max; + } + min++; // For object retrieval. + max++; + return costOf(min, max); + } - /** - * Resolve returns the resolved Attribute value given the Activation, or error if the Attribute - * variable is not found, or if its Qualifiers cannot be applied successfully. - */ - @Override - public Object resolve(org.projectnessie.cel.interpreter.Activation vars) { - return tryResolve(vars); + /** AddQualifier implements the Attribute interface method. */ + @Override + public Attribute addQualifier(Qualifier q) { + qualifiers.add(q); + return this; + } + + /** CandidateVariableNames implements the NamespaceAttribute interface method. */ + @Override + public String[] candidateVariableNames() { + return namespaceNames; + } + + /** + * Qualifiers returns the list of Qualifier instances associated with the namespaced attribute. + */ + @Override + public List qualifiers() { + return qualifiers; + } + + /** Qualify is an implementation of the Qualifier interface method. */ + @Override + public Object qualify(Activation vars, Object obj) { + Object val = resolve(vars); + if (isUnknown(val)) { + return val; } + Qualifier qual = fac.newQualifier(null, id, val); + return qual.qualify(vars, obj); + } - /** - * TryResolve iterates through the namespaced variable names until one is found within the - * Activation or TypeProvider. - * - *

If the variable name cannot be found as an Activation variable or in the TypeProvider as a - * type, then the result is `nil`, `false`, `nil` per the interface requirement. - */ - @Override - public Object tryResolve(org.projectnessie.cel.interpreter.Activation vars) { - Object local = tryResolveCurrentVar(vars); - if (local != null) { - return local; + /** + * Resolve returns the resolved Attribute value given the Activation, or error if the Attribute + * variable is not found, or if its Qualifiers cannot be applied successfully. + */ + @Override + public Object resolve(Activation vars) { + return tryResolve(vars); + } + + /** + * Resolves the first matching namespaced variable or type and applies this attribute's + * qualifiers. + * + * @param vars runtime variables + * @return the resolved value + * @throws RuntimeException if no candidate variable or type exists, or a qualifier cannot be + * applied + */ + @Override + public Object tryResolve(Activation vars) { + Object local = tryResolveCurrentVar(vars); + if (local != null) { + return local; + } + for (String nm : namespaceNames) { + // If the variable is found, process it. Otherwise, wait until the checks to + // determine whether the type is unknown before returning. + Object obj = vars.resolve(nm); + if (Activation.ABSENT != obj) { + return resolveQualifiers(vars, obj); } - for (String nm : namespaceNames) { - // If the variable is found, process it. Otherwise, wait until the checks to - // determine whether the type is unknown before returning. - ResolvedValue obj = vars.resolveName(nm); - if (obj.present()) { - return resolveQualifiers(vars, obj.value()); - } - // Attempt to resolve the qualified type name if the name is not a variable identifier. - Val typ = provider.findIdent(nm); - if (typ != null) { - if (qualifiers.isEmpty()) { - return typ; - } - throw noSuchAttributeException(this); + // Attempt to resolve the qualified type name if the name is not a variable identifier. + Val typ = provider.findIdent(nm); + if (typ != null) { + if (qualifiers.isEmpty()) { + return typ; } + throw noSuchAttributeException(this); } - throw noSuchAttributeException(this); } + throw noSuchAttributeException(this); + } - private Object tryResolveCurrentVar(org.projectnessie.cel.interpreter.Activation vars) { - if (vars instanceof Activation.VarActivation var - && (namespaceNames.length > 1 || !qualifiers.isEmpty())) { - String localName = namespaceNames[namespaceNames.length - 1]; - if (localName.equals(var.name)) { - return resolveQualifiers(vars, var.val); - } + Object tryResolveCurrentVar(Activation vars) { + if (vars instanceof VarActivation var && (namespaceNames.length > 1 || !qualifiers.isEmpty())) { + String localName = namespaceNames[namespaceNames.length - 1]; + if (localName.equals(var.name)) { + return resolveQualifiers(vars, var.val); } - return null; } + return null; + } - private Object resolveQualifiers( - org.projectnessie.cel.interpreter.Activation vars, Object obj) { - Object op = obj; - for (int i = 0; i < qualifiers.size(); i++) { - Qualifier qual = qualifiers.get(i); - Object op2 = qualify(vars, op, qual, i == qualifiers.size() - 1); - if (op2 instanceof Err) { - return op2; - } - if (op2 == null) { - break; - } - op = op2; + private Object resolveQualifiers(Activation vars, Object obj) { + Object op = obj; + for (int i = 0; i < qualifiers.size(); i++) { + Qualifier qual = qualifiers.get(i); + Object op2 = qualify(vars, op, qual, i == qualifiers.size() - 1); + if (op2 instanceof Err) { + return op2; } - return op; - } - - private Object qualify( - org.projectnessie.cel.interpreter.Activation vars, - Object obj, - Qualifier qualifier, - boolean last) { - if (last && qualifier instanceof ValQualifier) { - return ((ValQualifier) qualifier).qualifyToVal(vars, obj); + if (op2 == null) { + break; } - return qualifier.qualify(vars, obj); + op = op2; } + return op; + } - /** String implements the Stringer interface method. */ - @Override - public String toString() { - return "id: " + id + ", names: " + Arrays.toString(namespaceNames); + private Object qualify(Activation vars, Object obj, Qualifier qualifier, boolean last) { + if (last && qualifier instanceof ValQualifier) { + return ((ValQualifier) qualifier).qualifyToVal(vars, obj); } + return qualifier.qualify(vars, obj); } - final class ConditionalAttribute implements Qualifier, Attribute, Coster { - final long id; - final Interpretable expr; - final Attribute truthy; - final Attribute falsy; - final TypeAdapter adapter; - final AttributeFactory fac; - - ConditionalAttribute( - long id, - Interpretable expr, - Attribute truthy, - Attribute falsy, - TypeAdapter adapter, - AttributeFactory fac) { - this.id = id; - this.expr = expr; - this.truthy = truthy; - this.falsy = falsy; - this.adapter = adapter; - this.fac = fac; - } + /** String implements the Stringer interface method. */ + @Override + public String toString() { + return "id: " + id + ", names: " + Arrays.toString(namespaceNames); + } +} - /** ID is an implementation of the Attribute interface method. */ - @Override - public long id() { - return id; - } +record ConditionalAttribute( + long id, + Interpretable expr, + Attribute truthy, + Attribute falsy, + TypeAdapter adapter, + AttributeFactory fac) + implements Qualifier, Attribute, Coster { + + /** ID is an implementation of the Attribute interface method. */ + @Override + public long id() { + return id; + } - /** - * Cost provides the heuristic cost of a ternary operation {@code <expr> ? <t> : - * <f>}. The cost is computed as {@code cost(expr)} plus the min/max costs of evaluating - * either `t` or `f`. - */ - @Override - public Cost cost() { - Cost t = Cost.estimateCost(truthy); - Cost f = Cost.estimateCost(falsy); - Cost e = Cost.estimateCost(expr); - return costOf(e.min + Math.min(t.min, f.min), e.max + Math.max(t.max, f.max)); - } + /** + * Cost provides the heuristic cost of a ternary operation {@code <expr> ? <t> : + * <f>}. The cost is computed as {@code cost(expr)} plus the min/max costs of evaluating + * either `t` or `f`. + */ + @Override + public Cost cost() { + Cost t = Cost.estimateCost(truthy); + Cost f = Cost.estimateCost(falsy); + Cost e = Cost.estimateCost(expr); + return costOf(e.min + Math.min(t.min, f.min), e.max + Math.max(t.max, f.max)); + } - /** - * AddQualifier appends the same qualifier to both sides of the conditional, in effect managing - * the qualification of alternate attributes. - */ - @Override - public Attribute addQualifier(AttributeFactory.Qualifier qual) { - truthy.addQualifier(qual); // just do - falsy.addQualifier(qual); // just do - return this; - } + /** + * AddQualifier appends the same qualifier to both sides of the conditional, in effect managing + * the qualification of alternate attributes. + */ + @Override + public Attribute addQualifier(Qualifier qual) { + truthy.addQualifier(qual); // just do + falsy.addQualifier(qual); // just do + return this; + } - /** Qualify is an implementation of the Qualifier interface method. */ - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - Object val = resolve(vars); - if (isUnknown(val)) { - return val; - } - Qualifier qual = fac.newQualifier(null, id, val); - return qual.qualify(vars, obj); + /** Qualify is an implementation of the Qualifier interface method. */ + @Override + public Object qualify(Activation vars, Object obj) { + Object val = resolve(vars); + if (isUnknown(val)) { + return val; } + Qualifier qual = fac.newQualifier(null, id, val); + return qual.qualify(vars, obj); + } - /** - * Resolve evaluates the condition, and then resolves the truthy or falsy branch accordingly. - */ - @Override - public Object resolve(org.projectnessie.cel.interpreter.Activation vars) { - Val val = expr.eval(vars); - if (val == null) { - throw noSuchAttributeException(this); - } - if (isError(val)) { - throw new ErrException("message: %s", val); - } - if (val == True) { - return truthy.resolve(vars); - } - if (val == False) { - return falsy.resolve(vars); - } - if (isUnknown(val)) { - return val; - } - return maybeNoSuchOverloadErr(val); + /** Resolve evaluates the condition, and then resolves the truthy or falsy branch accordingly. */ + @Override + public Object resolve(Activation vars) { + Val val = expr.eval(vars); + if (val == null) { + throw noSuchAttributeException(this); } - - /** String is an implementation of the Stringer interface method. */ - @Override - public String toString() { - return String.format("id: %d, truthy attribute: %s, falsy attribute: %s", id, truthy, falsy); + if (isError(val)) { + throw new ErrException("message: %s", val); } + if (val == True) { + return truthy.resolve(vars); + } + if (val == False) { + return falsy.resolve(vars); + } + if (isUnknown(val)) { + return val; + } + return maybeNoSuchOverloadErr(val); } - final class MaybeAttribute implements Coster, Attribute, Qualifier { - final long id; - final List attrs; - final TypeAdapter adapter; - final TypeProvider provider; - final AttributeFactory fac; - - MaybeAttribute( - long id, - List attrs, - TypeAdapter adapter, - TypeProvider provider, - AttributeFactory fac) { - this.id = id; - this.attrs = attrs; - this.adapter = adapter; - this.provider = provider; - this.fac = fac; - } + /** String is an implementation of the Stringer interface method. */ + @Override + public String toString() { + return String.format("id: %d, truthy attribute: %s, falsy attribute: %s", id, truthy, falsy); + } +} - /** ID is an implementation of the Attribute interface method. */ - @Override - public long id() { - return id; - } +record MaybeAttribute( + long id, + List attrs, + TypeAdapter adapter, + TypeProvider provider, + AttributeFactory fac) + implements Coster, Attribute, Qualifier { + + /** ID is an implementation of the Attribute interface method. */ + @Override + public long id() { + return id; + } - /** - * Cost implements the Coster interface method. The min cost is computed as the minimal cost - * among all the possible attributes, the max cost ditto. - */ - @Override - public Cost cost() { - long min = Long.MAX_VALUE; - long max = 0L; - for (NamespacedAttribute a : attrs) { - Cost ac = Cost.estimateCost(a); - min = Long.min(min, ac.min); - max = Long.max(max, ac.max); - } - return costOf(min, max); - } + /** + * Cost implements the Coster interface method. The min cost is computed as the minimal cost among + * all the possible attributes, the max cost ditto. + */ + @Override + public Cost cost() { + long min = Long.MAX_VALUE; + long max = 0L; + for (NamespacedAttribute a : attrs) { + Cost ac = Cost.estimateCost(a); + min = Long.min(min, ac.min); + max = Long.max(max, ac.max); + } + return costOf(min, max); + } - /** - * AddQualifier adds a qualifier to each possible attribute variant, and also creates a new - * namespaced variable from the qualified value. - * - *

The algorithm for building the maybe attribute is as follows: - * - *

    - *
  1. Create a maybe attribute from a simple identifier when it occurs in a parsed-only - * expression
    - *
    - * {@code mb = MaybeAttribute(<id>, "a")}
    - *
    - * Initializing the maybe attribute creates an absolute attribute internally which - * includes the possible namespaced names of the attribute. In this example, let's assume - * we are in namespace 'ns', then the maybe is either one of the following variable names: - *
    - *
    - * possible variables names -- ns.a, a - *
  2. Adding a qualifier to the maybe means that the variable name could be a longer - * qualified name, or a field selection on one of the possible variable names produced - * earlier:
    - *
    - * {@code mb.AddQualifier("b")}
    - *
    - * possible variables names -- ns.a.b, a.b
    - * possible field selection -- ns.a['b'], a['b'] - *
- * - * If none of the attributes within the maybe resolves a value, the result is an error. - */ - @Override - public Attribute addQualifier(AttributeFactory.Qualifier qual) { - String str = ""; - boolean isStr = false; - if (qual instanceof ConstantQualifier cq) { - Object cqv = cq.value().value(); - if (cqv instanceof String) { - str = (String) cqv; - isStr = true; - } + /** + * AddQualifier adds a qualifier to each possible attribute variant, and also creates a new + * namespaced variable from the qualified value. + * + *

The algorithm for building the maybe attribute is as follows: + * + *

    + *
  1. Create a maybe attribute from a simple identifier when it occurs in a parsed-only + * expression
    + *
    + * {@code mb = MaybeAttribute(<id>, "a")}
    + *
    + * Initializing the maybe attribute creates an absolute attribute internally which includes + * the possible namespaced names of the attribute. In this example, let's assume we are in + * namespace 'ns', then the maybe is either one of the following variable names:
    + *
    + * possible variables names -- ns.a, a + *
  2. Adding a qualifier to the maybe means that the variable name could be a longer qualified + * name, or a field selection on one of the possible variable names produced earlier:
    + *
    + * {@code mb.AddQualifier("b")}
    + *
    + * possible variables names -- ns.a.b, a.b
    + * possible field selection -- ns.a['b'], a['b'] + *
+ * + * If none of the attributes within the maybe resolves a value, the result is an error. + */ + @Override + public Attribute addQualifier(Qualifier qual) { + String str = ""; + boolean isStr = false; + if (qual instanceof ConstantQualifier cq) { + Object cqv = cq.value().value(); + if (cqv instanceof String) { + str = (String) cqv; + isStr = true; } - String[] augmentedNames = new String[0]; - // First add the qualifier to all existing attributes in the oneof. - for (NamespacedAttribute attr : attrs) { - if (isStr && attr.qualifiers().isEmpty()) { - String[] candidateVars = attr.candidateVariableNames(); - augmentedNames = new String[candidateVars.length]; - for (int i = 0; i < candidateVars.length; i++) { - String name = candidateVars[i]; - augmentedNames[i] = String.format("%s.%s", name, str); - } + } + String[] augmentedNames = new String[0]; + // First add the qualifier to all existing attributes in the oneof. + for (NamespacedAttribute attr : attrs) { + if (isStr && attr.qualifiers().isEmpty()) { + String[] candidateVars = attr.candidateVariableNames(); + augmentedNames = new String[candidateVars.length]; + for (int i = 0; i < candidateVars.length; i++) { + String name = candidateVars[i]; + augmentedNames[i] = String.format("%s.%s", name, str); } - attr.addQualifier(qual); - } - // Next, ensure the most specific variable / type reference is searched first. - if (attrs.isEmpty()) { - attrs.add(fac.absoluteAttribute(qual.id(), augmentedNames)); - } else { - attrs.add(0, fac.absoluteAttribute(qual.id(), augmentedNames)); } - return this; + attr.addQualifier(qual); + } + // Next, ensure the most specific variable / type reference is searched first. + if (attrs.isEmpty()) { + attrs.add(fac.absoluteAttribute(qual.id(), augmentedNames)); + } else { + attrs.add(0, fac.absoluteAttribute(qual.id(), augmentedNames)); } + return this; + } - /** Qualify is an implementation of the Qualifier interface method. */ - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - Object val = resolve(vars); - if (isUnknown(val)) { - return val; - } - Qualifier qual = fac.newQualifier(null, id, val); - return qual.qualify(vars, obj); + /** Qualify is an implementation of the Qualifier interface method. */ + @Override + public Object qualify(Activation vars, Object obj) { + Object val = resolve(vars); + if (isUnknown(val)) { + return val; } + Qualifier qual = fac.newQualifier(null, id, val); + return qual.qualify(vars, obj); + } - /** - * Resolve follows the variable resolution rules to determine whether the attribute is a - * variable or a field selection. - */ - @Override - public Object resolve(org.projectnessie.cel.interpreter.Activation vars) { - for (NamespacedAttribute attr : attrs) { - if (attr instanceof AbsoluteAttribute) { - Object result = ((AbsoluteAttribute) attr).tryResolveCurrentVar(vars); - if (result != null) { - return result; - } - } - } - for (NamespacedAttribute attr : attrs) { - try { - return attr.tryResolve(vars); - } catch (ErrException ignore) { + /** + * Resolve follows the variable resolution rules to determine whether the attribute is a variable + * or a field selection. + */ + @Override + public Object resolve(Activation vars) { + for (NamespacedAttribute attr : attrs) { + if (attr instanceof AbsoluteAttribute) { + Object result = ((AbsoluteAttribute) attr).tryResolveCurrentVar(vars); + if (result != null) { + return result; } } - // Else, produce a no such attribute error. - throw noSuchAttributeException(this); } - - /** String is an implementation of the Stringer interface method. */ - @Override - public String toString() { - return String.format("id: %s, attributes: %s", id, attrs); + for (NamespacedAttribute attr : attrs) { + try { + return attr.tryResolve(vars); + } catch (ErrException ignore) { + } } + // Else, produce a no such attribute error. + throw noSuchAttributeException(this); } - final class RelativeAttribute implements Coster, Qualifier, Attribute { - final long id; - final Interpretable operand; - final List qualifiers; - final TypeAdapter adapter; - final AttributeFactory fac; - - RelativeAttribute( - long id, - Interpretable operand, - List qualifiers, - TypeAdapter adapter, - AttributeFactory fac) { - this.id = id; - this.operand = operand; - this.qualifiers = qualifiers; - this.adapter = adapter; - this.fac = fac; - } + /** String is an implementation of the Stringer interface method. */ + @Override + public String toString() { + return String.format("id: %s, attributes: %s", id, attrs); + } +} - /** ID is an implementation of the Attribute interface method. */ - @Override - public long id() { - return id; - } +record RelativeAttribute( + long id, + Interpretable operand, + List qualifiers, + TypeAdapter adapter, + AttributeFactory fac) + implements Coster, Qualifier, Attribute { + + /** ID is an implementation of the Attribute interface method. */ + @Override + public long id() { + return id; + } - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - Cost c = Cost.estimateCost(operand); - long min = c.min; - long max = c.max; - for (Qualifier qual : qualifiers) { - Cost q = Cost.estimateCost(qual); - min += q.min; - max += q.max; - } - return costOf(min, max); - } + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + Cost c = Cost.estimateCost(operand); + long min = c.min; + long max = c.max; + for (Qualifier qual : qualifiers) { + Cost q = Cost.estimateCost(qual); + min += q.min; + max += q.max; + } + return costOf(min, max); + } - /** AddQualifier implements the Attribute interface method. */ - @Override - public Attribute addQualifier(AttributeFactory.Qualifier qual) { - qualifiers.add(qual); - return this; - } + /** AddQualifier implements the Attribute interface method. */ + @Override + public Attribute addQualifier(Qualifier qual) { + qualifiers.add(qual); + return this; + } - /** Qualify is an implementation of the Qualifier interface method. */ - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - Object val = resolve(vars); - if (isUnknown(val)) { - return val; - } - Qualifier qual = fac.newQualifier(null, id, val); - return qual.qualify(vars, obj); + /** Qualify is an implementation of the Qualifier interface method. */ + @Override + public Object qualify(Activation vars, Object obj) { + Object val = resolve(vars); + if (isUnknown(val)) { + return val; } + Qualifier qual = fac.newQualifier(null, id, val); + return qual.qualify(vars, obj); + } - /** Resolve expression value and qualifier relative to the expression result. */ - @Override - public Object resolve(org.projectnessie.cel.interpreter.Activation vars) { - // First, evaluate the operand. - Val v = operand.eval(vars); - if (isError(v)) { - throw new ErrException("message: %s", v); - } - if (isUnknown(v)) { - return v; - } - // Next, qualify it. Qualification handles unkonwns as well, so there's no need to recheck. - Object obj = v; - for (int i = 0; i < qualifiers.size(); i++) { - Qualifier qual = qualifiers.get(i); - if (obj == null) { - throw noSuchAttributeException(this); - } - if (i == qualifiers.size() - 1 && qual instanceof ValQualifier) { - obj = ((ValQualifier) qual).qualifyToVal(vars, obj); - } else { - obj = qual.qualify(vars, obj); - } - if (obj instanceof Err) { - return obj; - } - } + /** Resolve expression value and qualifier relative to the expression result. */ + @Override + public Object resolve(Activation vars) { + // First, evaluate the operand. + Val v = operand.eval(vars); + if (isError(v)) { + throw new ErrException("message: %s", v); + } + if (isUnknown(v)) { + return v; + } + // Next, qualify it. Qualification handles unkonwns as well, so there's no need to recheck. + Object obj = v; + for (int i = 0; i < qualifiers.size(); i++) { + Qualifier qual = qualifiers.get(i); if (obj == null) { throw noSuchAttributeException(this); } - return obj; - } - - /** String is an implementation of the Stringer interface method. */ - @Override - public String toString() { - return String.format("id: %d, operand: %s", id, operand); - } - } - - static Qualifier newQualifierStatic(TypeAdapter adapter, long id, Object v) { - if (v instanceof Attribute) { - return new AttrQualifier(id, (Attribute) v); - } - - Class c = v.getClass(); - - if (v instanceof Val val) { - switch (val.type().typeEnum()) { - case String: - return new StringQualifier(id, (String) val.value(), val, adapter); - case Double: - return new DoubleQualifier(id, (double) val.value(), val, adapter); - case Int: - return new IntQualifier(id, val.intValue(), val, adapter); - case Uint: - return new UintQualifier(id, val.intValue(), val, adapter); - case Bool: - return new BoolQualifier(id, val.booleanValue(), val, adapter); - case Null: - // Not actually a qualifier, but conformance-tests require this, although it's actually an - // error condition. - return new NullQualifier(id, val, adapter); + if (i == qualifiers.size() - 1 && qual instanceof ValQualifier) { + obj = ((ValQualifier) qual).qualifyToVal(vars, obj); + } else { + obj = qual.qualify(vars, obj); + } + if (obj instanceof Err) { + return obj; } } - - if (c == String.class) { - return new StringQualifier(id, (String) v, stringOf((String) v), adapter); - } - if (c == ULong.class) { - long l = ((ULong) v).longValue(); - return new UintQualifier(id, l, uintOf(l), adapter); - } - if ((c == Byte.class) || (c == Short.class) || (c == Integer.class) || (c == Long.class)) { - long i = ((Number) v).longValue(); - return new IntQualifier(id, i, intOf(i), adapter); - } - if (c == Double.class) { - double b = (Double) v; - return new DoubleQualifier(id, b, doubleOf(b), adapter); - } - if (c == Boolean.class) { - boolean b = (Boolean) v; - return new BoolQualifier(id, b, boolOf(b), adapter); + if (obj == null) { + throw noSuchAttributeException(this); } - - throw new IllegalStateException( - String.format("invalid qualifier type: %s", v.getClass().getName())); + return obj; } - final class AttrQualifier implements Coster, Attribute { - final long id; - final Attribute attribute; - - AttrQualifier(long id, Attribute attribute) { - this.id = id; - this.attribute = attribute; - } - - @Override - public long id() { - return id; - } + /** String is an implementation of the Stringer interface method. */ + @Override + public String toString() { + return String.format("id: %d, operand: %s", id, operand); + } +} - /** Cost returns zero for constant field qualifiers */ - @Override - public Cost cost() { - return Cost.estimateCost(attribute); - } +record AttrQualifier(long id, Attribute attribute) implements Coster, Attribute { - @Override - public Attribute addQualifier(Qualifier q) { - return attribute.addQualifier(q); - } + /** Cost returns zero for constant field qualifiers */ + @Override + public Cost cost() { + return Cost.estimateCost(attribute); + } - @Override - public Object resolve(Activation a) { - return attribute.resolve(a); - } + @Override + public Attribute addQualifier(Qualifier q) { + return attribute.addQualifier(q); + } - @Override - public Object qualify(Activation vars, Object obj) { - return attribute.qualify(vars, obj); - } + @Override + public Object resolve(Activation a) { + return attribute.resolve(a); + } - @Override - public String toString() { - return "AttrQualifier{" + "id=" + id + ", attribute=" + attribute + '}'; - } + @Override + public Object qualify(Activation vars, Object obj) { + return attribute.qualify(vars, obj); } - final class StringQualifier implements Coster, ConstantQualifierEquator, QualifierValueEquator { - final long id; - final String value; - final Val celValue; - final TypeAdapter adapter; + @Override + public String toString() { + return "AttrQualifier{" + "id=" + id + ", attribute=" + attribute + '}'; + } +} - StringQualifier(long id, String value, Val celValue, TypeAdapter adapter) { - this.id = id; - this.value = value; - this.celValue = celValue; - this.adapter = adapter; - } +final class StringQualifier implements Coster, ConstantQualifierEquator, QualifierValueEquator { + final long id; + final String value; + final Val celValue; + final TypeAdapter adapter; + + StringQualifier(long id, String value, Val celValue, TypeAdapter adapter) { + this.id = id; + this.value = value; + this.celValue = celValue; + this.adapter = adapter; + } - /** ID is an implementation of the Qualifier interface method. */ - @Override - public long id() { - return id; - } + /** ID is an implementation of the Qualifier interface method. */ + @Override + public long id() { + return id; + } - /** Qualify implements the Qualifier interface method. */ - @SuppressWarnings("rawtypes") - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - String s = value; - if (obj instanceof Map m) { - obj = m.get(s); - if (obj == null) { - if (m.containsKey(s)) { - return NullValue; - } - throw noSuchKeyException(s); + /** Qualify implements the Qualifier interface method. */ + @SuppressWarnings("rawtypes") + @Override + public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { + String s = value; + if (obj instanceof Map m) { + obj = m.get(s); + if (obj == null) { + if (m.containsKey(s)) { + return NullValue; } - } else if (isUnknown(obj)) { - return obj; - } else { - return refResolve(adapter, celValue, obj); + throw noSuchKeyException(s); } + } else if (isUnknown(obj)) { return obj; + } else { + return refResolve(adapter, celValue, obj); } + return obj; + } - /** Value implements the ConstantQualifier interface */ - @Override - public Val value() { - return celValue; - } - - /** Cost returns zero for constant field qualifiers */ - @Override - public Cost cost() { - return Cost.None; - } + /** Value implements the ConstantQualifier interface */ + @Override + public Val value() { + return celValue; + } - @Override - public boolean qualifierValueEquals(Object value) { - if (value instanceof String) { - return this.value.equals(value); - } - return false; - } + /** Cost returns zero for constant field qualifiers */ + @Override + public Cost cost() { + return Cost.None; + } - @Override - public String toString() { - return "StringQualifier{" - + "id=" - + id - + ", value='" - + value - + '\'' - + ", celValue=" - + celValue - + ", adapter=" - + adapter - + '}'; + @Override + public boolean qualifierValueEquals(Object value) { + if (value instanceof String) { + return this.value.equals(value); } + return false; } - final class DoubleQualifier implements Coster, ConstantQualifierEquator { - final long id; - final double value; - final Val celValue; - final TypeAdapter adapter; + @Override + public String toString() { + return "StringQualifier{" + + "id=" + + id + + ", value='" + + value + + '\'' + + ", celValue=" + + celValue + + ", adapter=" + + adapter + + '}'; + } +} - DoubleQualifier(long id, double value, Val celValue, TypeAdapter adapter) { - this.id = id; - this.value = value; - this.celValue = celValue; - this.adapter = adapter; - } +final class DoubleQualifier implements Coster, ConstantQualifierEquator { + final long id; + final double value; + final Val celValue; + final TypeAdapter adapter; + + DoubleQualifier(long id, double value, Val celValue, TypeAdapter adapter) { + this.id = id; + this.value = value; + this.celValue = celValue; + this.adapter = adapter; + } - /** ID is an implementation of the Qualifier interface method. */ - @Override - public long id() { - return id; - } + /** ID is an implementation of the Qualifier interface method. */ + @Override + public long id() { + return id; + } - /** Qualify implements the Qualifier interface method. */ - @SuppressWarnings("rawtypes") - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - double i = value; - if (obj instanceof Map m) { - obj = m.get(i); - if (obj == null) { - obj = m.get((int) i); - } - if (obj == null) { - if (m.containsKey(i) || m.containsKey((int) i)) { - return null; - } - throw noSuchKeyException(i); - } - return obj; + /** Qualify implements the Qualifier interface method. */ + @SuppressWarnings({"rawtypes", "DuplicatedCode"}) + @Override + public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { + double i = value; + if (obj instanceof Map m) { + obj = m.get(i); + if (obj == null) { + obj = m.get((int) i); } - if (obj.getClass().isArray()) { - int l = Array.getLength(obj); - if (i < 0 || i >= l) { - throw indexOutOfBoundsException(i); + if (obj == null) { + if (m.containsKey(i) || m.containsKey((int) i)) { + return null; } - obj = Array.get(obj, (int) i); - return obj; + throw noSuchKeyException(i); } - if (obj instanceof List list) { - int l = list.size(); - if (i < 0 || i >= l) { - throw indexOutOfBoundsException(i); - } - obj = list.get((int) i); - return obj; + return obj; + } + if (obj.getClass().isArray()) { + int l = Array.getLength(obj); + if (i < 0 || i >= l) { + throw indexOutOfBoundsException(i); } - if (isUnknown(obj)) { - return obj; + obj = Array.get(obj, (int) i); + return obj; + } + if (obj instanceof List list) { + int l = list.size(); + if (i < 0 || i >= l) { + throw indexOutOfBoundsException(i); } - return refResolve(adapter, celValue, obj); + obj = list.get((int) i); + return obj; } - - /** Value implements the ConstantQualifier interface */ - @Override - public Val value() { - return celValue; + if (isUnknown(obj)) { + return obj; } + return refResolve(adapter, celValue, obj); + } - /** Cost returns zero for constant field qualifiers */ - @Override - public Cost cost() { - return Cost.None; - } + /** Value implements the ConstantQualifier interface */ + @Override + public Val value() { + return celValue; + } - @Override - public boolean qualifierValueEquals(Object value) { - if (value instanceof ULong) { - return false; - } - if (value instanceof Number) { - return this.value == ((Number) value).doubleValue(); - } + /** Cost returns zero for constant field qualifiers */ + @Override + public Cost cost() { + return Cost.None; + } + + @Override + public boolean qualifierValueEquals(Object value) { + if (value instanceof ULong) { return false; } - - @Override - public String toString() { - return "DoubleQualifier{" - + "id=" - + id - + ", value=" - + value - + ", celValue=" - + celValue - + ", adapter=" - + adapter - + '}'; + if (value instanceof Number) { + return this.value == ((Number) value).doubleValue(); } + return false; } - final class IntQualifier implements Coster, ConstantQualifierEquator { - final long id; - final long value; - final Val celValue; - final TypeAdapter adapter; + @Override + public String toString() { + return "DoubleQualifier{" + + "id=" + + id + + ", value=" + + value + + ", celValue=" + + celValue + + ", adapter=" + + adapter + + '}'; + } +} - IntQualifier(long id, long value, Val celValue, TypeAdapter adapter) { - this.id = id; - this.value = value; - this.celValue = celValue; - this.adapter = adapter; - } +final class IntQualifier implements Coster, ConstantQualifierEquator { + final long id; + final long value; + final Val celValue; + final TypeAdapter adapter; + + IntQualifier(long id, long value, Val celValue, TypeAdapter adapter) { + this.id = id; + this.value = value; + this.celValue = celValue; + this.adapter = adapter; + } - /** ID is an implementation of the Qualifier interface method. */ - @Override - public long id() { - return id; - } + /** ID is an implementation of the Qualifier interface method. */ + @Override + public long id() { + return id; + } - /** Qualify implements the Qualifier interface method. */ - @SuppressWarnings("rawtypes") - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - long i = value; - if (obj instanceof Map m) { - obj = m.get(i); - if (obj == null) { - obj = m.get((int) i); - } - if (obj == null) { - if (m.containsKey(i) || m.containsKey((int) i)) { - return null; - } - throw noSuchKeyException(i); - } - return obj; + /** Qualify implements the Qualifier interface method. */ + @SuppressWarnings({"rawtypes", "DuplicatedCode"}) + @Override + public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { + long i = value; + if (obj instanceof Map m) { + obj = m.get(i); + if (obj == null) { + obj = m.get((int) i); } - if (obj.getClass().isArray()) { - int l = Array.getLength(obj); - if (i < 0 || i >= l) { - throw indexOutOfBoundsException(i); + if (obj == null) { + if (m.containsKey(i) || m.containsKey((int) i)) { + return null; } - obj = Array.get(obj, (int) i); - return obj; + throw noSuchKeyException(i); } - if (obj instanceof List list) { - int l = list.size(); - if (i < 0 || i >= l) { - throw indexOutOfBoundsException(i); - } - obj = list.get((int) i); - return obj; + return obj; + } + if (obj.getClass().isArray()) { + int l = Array.getLength(obj); + if (i < 0 || i >= l) { + throw indexOutOfBoundsException(i); } - if (isUnknown(obj)) { - return obj; + obj = Array.get(obj, (int) i); + return obj; + } + if (obj instanceof List list) { + int l = list.size(); + if (i < 0 || i >= l) { + throw indexOutOfBoundsException(i); } - return refResolve(adapter, celValue, obj); + obj = list.get((int) i); + return obj; } - - /** Value implements the ConstantQualifier interface */ - @Override - public Val value() { - return celValue; + if (isUnknown(obj)) { + return obj; } + return refResolve(adapter, celValue, obj); + } - /** Cost returns zero for constant field qualifiers */ - @Override - public Cost cost() { - return Cost.None; - } + /** Value implements the ConstantQualifier interface */ + @Override + public Val value() { + return celValue; + } - @Override - public boolean qualifierValueEquals(Object value) { - if (value instanceof ULong) { - return false; - } - if (value instanceof Number) { - return this.value == ((Number) value).longValue(); - } + /** Cost returns zero for constant field qualifiers */ + @Override + public Cost cost() { + return Cost.None; + } + + @Override + public boolean qualifierValueEquals(Object value) { + if (value instanceof ULong) { return false; } - - @Override - public String toString() { - return "IntQualifier{" - + "id=" - + id - + ", value=" - + value - + ", celValue=" - + celValue - + ", adapter=" - + adapter - + '}'; + if (value instanceof Number) { + return this.value == ((Number) value).longValue(); } + return false; } - final class UintQualifier implements Coster, ConstantQualifierEquator { - final long id; - final long value; - final Val celValue; - final TypeAdapter adapter; + @Override + public String toString() { + return "IntQualifier{" + + "id=" + + id + + ", value=" + + value + + ", celValue=" + + celValue + + ", adapter=" + + adapter + + '}'; + } +} - UintQualifier(long id, long value, Val celValue, TypeAdapter adapter) { - this.id = id; - this.value = value; - this.celValue = celValue; - this.adapter = adapter; - } +final class UintQualifier implements Coster, ConstantQualifierEquator { + final long id; + final long value; + final Val celValue; + final TypeAdapter adapter; + + UintQualifier(long id, long value, Val celValue, TypeAdapter adapter) { + this.id = id; + this.value = value; + this.celValue = celValue; + this.adapter = adapter; + } - /** ID is an implementation of the Qualifier interface method. */ - @Override - public long id() { - return id; - } + /** ID is an implementation of the Qualifier interface method. */ + @Override + public long id() { + return id; + } - /** Qualify implements the Qualifier interface method. */ - @SuppressWarnings("rawtypes") - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - long i = value; - if (obj instanceof Map m) { - obj = m.get(ULong.valueOf(i)); - if (obj == null) { - throw noSuchKeyException(i); - } - return obj; - } - if (obj.getClass().isArray()) { - int l = Array.getLength(obj); - if (i < 0 && i >= l) { - throw indexOutOfBoundsException(i); - } - obj = Array.get(obj, (int) i); - return obj; + /** Qualify implements the Qualifier interface method. */ + @SuppressWarnings("rawtypes") + @Override + public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { + long i = value; + if (obj instanceof Map m) { + obj = m.get(ULong.valueOf(i)); + if (obj == null) { + throw noSuchKeyException(i); } - if (isUnknown(obj)) { - return obj; + return obj; + } + if (obj.getClass().isArray()) { + int l = Array.getLength(obj); + if (i < 0 && i >= l) { + throw indexOutOfBoundsException(i); } - return refResolve(adapter, celValue, obj); + obj = Array.get(obj, (int) i); + return obj; } - - /** Value implements the ConstantQualifier interface */ - @Override - public Val value() { - return celValue; + if (isUnknown(obj)) { + return obj; } + return refResolve(adapter, celValue, obj); + } - /** Cost returns zero for constant field qualifiers */ - @Override - public Cost cost() { - return Cost.None; - } + /** Value implements the ConstantQualifier interface */ + @Override + public Val value() { + return celValue; + } - @Override - public boolean qualifierValueEquals(Object value) { - if (value instanceof ULong) { - return this.value == ((ULong) value).longValue(); - } - return false; - } + /** Cost returns zero for constant field qualifiers */ + @Override + public Cost cost() { + return Cost.None; + } - @Override - public String toString() { - return "UintQualifier{" - + "id=" - + id - + ", value=" - + value - + ", celValue=" - + celValue - + ", adapter=" - + adapter - + '}'; + @Override + public boolean qualifierValueEquals(Object value) { + if (value instanceof ULong) { + return this.value == ((ULong) value).longValue(); } + return false; } - final class BoolQualifier implements Coster, ConstantQualifierEquator { - final long id; - final boolean value; - final Val celValue; - final TypeAdapter adapter; + @Override + public String toString() { + return "UintQualifier{" + + "id=" + + id + + ", value=" + + value + + ", celValue=" + + celValue + + ", adapter=" + + adapter + + '}'; + } +} - BoolQualifier(long id, boolean value, Val celValue, TypeAdapter adapter) { - this.id = id; - this.value = value; - this.celValue = celValue; - this.adapter = adapter; - } +final class BoolQualifier implements Coster, ConstantQualifierEquator { + final long id; + final boolean value; + final Val celValue; + final TypeAdapter adapter; + + BoolQualifier(long id, boolean value, Val celValue, TypeAdapter adapter) { + this.id = id; + this.value = value; + this.celValue = celValue; + this.adapter = adapter; + } - /** ID is an implementation of the Qualifier interface method. */ - @Override - public long id() { - return id; - } + /** ID is an implementation of the Qualifier interface method. */ + @Override + public long id() { + return id; + } - /** Qualify implements the Qualifier interface method. */ - @SuppressWarnings("rawtypes") - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - boolean b = value; - if (obj instanceof Map m) { - obj = m.get(b); - if (obj == null) { - if (m.containsKey(b)) { - return null; - } - throw noSuchKeyException(b); + /** Qualify implements the Qualifier interface method. */ + @SuppressWarnings("rawtypes") + @Override + public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { + boolean b = value; + if (obj instanceof Map m) { + obj = m.get(b); + if (obj == null) { + if (m.containsKey(b)) { + return null; } - } else if (isUnknown(obj)) { - return obj; - } else { - return refResolve(adapter, celValue, obj); + throw noSuchKeyException(b); } + } else if (isUnknown(obj)) { return obj; + } else { + return refResolve(adapter, celValue, obj); } + return obj; + } - /** Value implements the ConstantQualifier interface */ - @Override - public Val value() { - return celValue; - } - - /** Cost returns zero for constant field qualifiers */ - @Override - public Cost cost() { - return Cost.None; - } + /** Value implements the ConstantQualifier interface */ + @Override + public Val value() { + return celValue; + } - @Override - public boolean qualifierValueEquals(Object value) { - if (value instanceof Boolean) { - return this.value == (Boolean) value; - } - return false; - } + /** Cost returns zero for constant field qualifiers */ + @Override + public Cost cost() { + return Cost.None; + } - @Override - public String toString() { - return "BoolQualifier{" - + "id=" - + id - + ", value=" - + value - + ", celValue=" - + celValue - + ", adapter=" - + adapter - + '}'; + @Override + public boolean qualifierValueEquals(Object value) { + if (value instanceof Boolean) { + return this.value == (Boolean) value; } + return false; } - /** - * Not actually a qualifier, but conformance-tests require this, although it's actually an error - * condition. - */ - final class NullQualifier implements Coster, ConstantQualifierEquator { - final long id; - final Val celValue; - final TypeAdapter adapter; - - NullQualifier(long id, Val celValue, TypeAdapter adapter) { - this.id = id; - this.celValue = celValue; - this.adapter = adapter; - } + @Override + public String toString() { + return "BoolQualifier{" + + "id=" + + id + + ", value=" + + value + + ", celValue=" + + celValue + + ", adapter=" + + adapter + + '}'; + } +} - /** ID is an implementation of the Qualifier interface method. */ - @Override - public long id() { - return id; - } +/** + * Not actually a qualifier, but conformance-tests require this, although it's actually an error + * condition. + */ +record NullQualifier(long id, Val celValue, TypeAdapter adapter) + implements Coster, ConstantQualifierEquator { - /** Qualify implements the Qualifier interface method. */ - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - return null; - } + /** ID is an implementation of the Qualifier interface method. */ + @Override + public long id() { + return id; + } - /** Value implements the ConstantQualifier interface */ - @Override - public Val value() { - return NullValue; - } + /** Qualify implements the Qualifier interface method. */ + @Override + public Object qualify(Activation vars, Object obj) { + return null; + } - /** Cost returns zero for constant field qualifiers */ - @Override - public Cost cost() { - return Cost.None; - } + /** Value implements the ConstantQualifier interface */ + @Override + public Val value() { + return NullValue; + } - @Override - public boolean qualifierValueEquals(Object value) { - return value == null || value == NullValue; - } + /** Cost returns zero for constant field qualifiers */ + @Override + public Cost cost() { + return Cost.None; + } - @Override - public String toString() { - return "NullQualifier{" - + "id=" - + id - + ", celValue=" - + celValue - + ", adapter=" - + adapter - + '}'; - } + @Override + public boolean qualifierValueEquals(Object value) { + return value == null || value == NullValue; } - /** - * fieldQualifier indicates that the qualification is a well-defined field with a known field - * type. When the field type is known this can be used to improve the speed and efficiency of - * field resolution. - */ - final class FieldQualifier implements Coster, ConstantQualifierEquator, ValQualifier { - final long id; - final String name; - final FieldType fieldType; - final TypeAdapter adapter; - - FieldQualifier(long id, String name, FieldType fieldType, TypeAdapter adapter) { - this.id = id; - this.name = name; - this.fieldType = fieldType; - this.adapter = adapter; - } + @Override + public String toString() { + return "NullQualifier{" + "id=" + id + ", celValue=" + celValue + ", adapter=" + adapter + '}'; + } +} - /** ID is an implementation of the Qualifier interface method. */ - @Override - public long id() { - return id; - } +/** + * fieldQualifier indicates that the qualification is a well-defined field with a known field type. + * When the field type is known this can be used to improve the speed and efficiency of field + * resolution. + */ +record FieldQualifier(long id, String name, FieldType fieldType, TypeAdapter adapter) + implements Coster, ConstantQualifierEquator, ValQualifier { - /** Qualify implements the Qualifier interface method. */ - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - if (obj instanceof Val) { - obj = ((Val) obj).value(); - } - return fieldType.getFrom.getFrom(obj); - } + /** ID is an implementation of the Qualifier interface method. */ + @Override + public long id() { + return id; + } - @Override - public Val qualifyToVal(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - return adapter.nativeToValue(qualify(vars, obj)); + /** Qualify implements the Qualifier interface method. */ + @Override + public Object qualify(Activation vars, Object obj) { + if (obj instanceof Val) { + obj = ((Val) obj).value(); } + return fieldType.getFrom.getFrom(obj); + } - /** Value implements the ConstantQualifier interface */ - @Override - public Val value() { - return stringOf(name); - } + @Override + public Val qualifyToVal(Activation vars, Object obj) { + Object value = qualify(vars, obj); + return adapter.nativeToValue(value); + } - /** Cost returns zero for constant field qualifiers */ - @Override - public Cost cost() { - return Cost.None; - } + /** Value implements the ConstantQualifier interface */ + @Override + public Val value() { + return stringOf(name); + } - @Override - public boolean qualifierValueEquals(Object value) { - if (value instanceof String) { - return this.name.equals(value); - } - return false; - } + /** Cost returns zero for constant field qualifiers */ + @Override + public Cost cost() { + return Cost.None; + } - @Override - public String toString() { - return "FieldQualifier{" - + "id=" - + id - + ", name='" - + name - + '\'' - + ", fieldType=" - + fieldType - + ", adapter=" - + adapter - + '}'; + @Override + public boolean qualifierValueEquals(Object value) { + if (value instanceof String) { + return this.name.equals(value); } + return false; } - /** - * refResolve attempts to convert the value to a CEL value and then uses reflection methods to try - * and resolve the qualifier. - */ - static Val refResolve(TypeAdapter adapter, Val idx, Object obj) { - Val celVal = adapter.nativeToValue(obj); - if (celVal instanceof Mapper mapper) { - Val elem = mapper.find(idx); - if (elem == null) { - return noSuchKey(idx); - } - return elem; - } - if (celVal instanceof Lister list && idx instanceof IntT) { - long intIndex = idx.intValue(); - if (intIndex >= Integer.MIN_VALUE && intIndex <= Integer.MAX_VALUE) { - return list.nativeGetAt((int) intIndex); - } - } - if (celVal instanceof Indexer indexer) { - return indexer.get(idx); - } - if (isUnknown(celVal)) { - return celVal; - } - // TODO: If the types.Err value contains more than just an error message at some point in the - // future, then it would be reasonable to return error values as ref.Val types rather than - // simple go error types. - throwErrorAsIllegalStateException(celVal); - return noSuchOverload(celVal, "ref-resolve", null); + @Override + public String toString() { + return "FieldQualifier{" + + "id=" + + id + + ", name='" + + name + + '\'' + + ", fieldType=" + + fieldType + + ", adapter=" + + adapter + + '}'; } } + +/** + * Exact aggregate field qualifier that deliberately retains the raw host value for a checked + * aggregate source node. + */ diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/DefaultDispatcher.java b/core/src/main/java/org/projectnessie/cel/interpreter/DefaultDispatcher.java new file mode 100644 index 000000000..17380a2d0 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/DefaultDispatcher.java @@ -0,0 +1,62 @@ +/* + * 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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.projectnessie.cel.interpreter.functions.Overload; + +/** Default dispatcher backed by an overload map. */ +final class DefaultDispatcher implements Dispatcher { + private final Dispatcher parent; + private final Map overloads; + + DefaultDispatcher(Dispatcher parent, Map overloads) { + this.parent = parent; + this.overloads = overloads; + } + + @Override + public void add(Overload... overloads) { + for (Overload overload : overloads) { + if (this.overloads.containsKey(overload.operator)) { + throw new IllegalArgumentException( + String.format("overload already exists '%s'", overload.operator)); + } + this.overloads.put(overload.operator, overload); + } + } + + @Override + public Overload findOverload(String overload) { + Overload result = overloads.get(overload); + if (result != null) { + return result; + } + return parent != null ? parent.findOverload(overload) : null; + } + + @Override + public String[] overloadIds() { + List result = new ArrayList<>(overloads.keySet()); + if (parent != null) { + Collections.addAll(result, parent.overloadIds()); + } + return result.toArray(new String[0]); + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/Dispatcher.java b/core/src/main/java/org/projectnessie/cel/interpreter/Dispatcher.java index 2b7e1cae9..db39d18b0 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Dispatcher.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Dispatcher.java @@ -15,11 +15,7 @@ */ package org.projectnessie.cel.interpreter; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; -import java.util.List; -import java.util.Map; import org.projectnessie.cel.interpreter.functions.Overload; /** Dispatcher resolves function calls to their appropriate overload. */ @@ -46,50 +42,4 @@ static Dispatcher newDispatcher() { static Dispatcher extendDispatcher(Dispatcher parent) { return new DefaultDispatcher(parent, new HashMap<>()); } - - /** defaultDispatcher struct which contains an overload map. */ - final class DefaultDispatcher implements Dispatcher { - private final Dispatcher parent; - private final Map overloads; - - DefaultDispatcher(Dispatcher parent, Map overloads) { - this.parent = parent; - this.overloads = overloads; - } - - /** Add implements the Dispatcher.Add interface method. */ - @Override - public void add(Overload... overloads) { - for (Overload o : overloads) { - - // add the overload unless an overload of the same name has already been provided. - if (this.overloads.containsKey(o.operator)) { - throw new IllegalArgumentException( - String.format("overload already exists '%s'", o.operator)); - } - // index the overload by function name. - this.overloads.put(o.operator, o); - } - } - - /** FindOverload implements the Dispatcher.FindOverload interface method. */ - @Override - public Overload findOverload(String overload) { - Overload o = overloads.get(overload); - if (o != null) { - return o; - } - return parent != null ? parent.findOverload(overload) : null; - } - - /** OverloadIds implements the Dispatcher interface method. */ - @Override - public String[] overloadIds() { - List r = new ArrayList<>(overloads.keySet()); - if (parent != null) { - Collections.addAll(r, parent.overloadIds()); - } - return r.toArray(new String[0]); - } - } } diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedAggregateNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedAggregateNodes.java new file mode 100644 index 000000000..919825ce6 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedAggregateNodes.java @@ -0,0 +1,398 @@ +/* + * 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.Err.newErr; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.Err.valOrErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.Util.isUnknownOrError; +import static org.projectnessie.cel.interpreter.Activation.emptyActivation; +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.List; +import org.projectnessie.cel.common.operators.Operator; +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.OptionalT; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.common.types.traits.Mapper; +import org.projectnessie.cel.common.types.traits.Sizer; +import org.projectnessie.cel.common.types.traits.Trait; + +class EvalList extends AbstractEval implements Coster { + final Interpretable[] elems; + final boolean[] optionalIndices; + private final TypeAdapter adapter; + + EvalList(long id, Interpretable[] elems, TypeAdapter adapter) { + this(id, elems, new boolean[elems.length], adapter); + } + + EvalList(long id, Interpretable[] elems, boolean[] optionalIndices, TypeAdapter adapter) { + super(id); + this.elems = elems; + this.optionalIndices = optionalIndices; + this.adapter = adapter; + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(Activation ctx) { + List elemVals = new ArrayList<>(elems.length); + // If any argument is unknown or error early terminate. + for (int i = 0; i < elems.length; i++) { + Interpretable elem = elems[i]; + Val elemVal = elem.eval(ctx); + if (isUnknownOrError(elemVal)) { + return elemVal; + } + if (optionalIndices[i]) { + if (!(elemVal instanceof OptionalT optional)) { + return newErr("optional list element is not optional"); + } + if (!optional.hasValue()) { + continue; + } + elemVal = optional.getValue(); + } + elemVals.add(elemVal); + } + return adapter.nativeToValue(elemVals.toArray(Val[]::new)); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return Interpretable.sumOfCost(elems); + } + + @Override + public String toString() { + return "EvalList{" + "id=" + id + ", elems=" + Arrays.toString(elems) + '}'; + } +} + +class EvalListFold extends AbstractEval implements Coster { + final String iterVar; + final String iterVar2; + final Interpretable iterRange; + final Interpretable filter; + final Interpretable transform; + final TypeAdapter adapter; + + EvalListFold( + long id, + String iterVar, + String iterVar2, + Interpretable iterRange, + Interpretable filter, + Interpretable transform, + TypeAdapter adapter) { + super(id); + this.iterVar = iterVar; + this.iterVar2 = iterVar2; + this.iterRange = iterRange; + this.filter = filter; + this.transform = transform; + this.adapter = adapter; + } + + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(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; + VarActivation iterCtx2 = null; + if (!iterVar2.isEmpty()) { + iterCtx2 = new VarActivation(); + iterCtx2.parent = iterCtx; + iterCtx2.name = iterVar2; + } + List values = new ArrayList<>(listCapacity(foldRange)); + IteratorT it = ((IterableT) foldRange).iterator(); + long index = 0L; + var isLister = foldRange instanceof Lister; + var mapper = (foldRange instanceof Mapper m) ? m : null; + while (it.hasNext() == True) { + Val next = it.next(); + Activation loopCtx = iterCtx; + if (iterCtx2 != null) { + if (isLister) { + iterCtx.val = intOf(index); + iterCtx2.val = next; + } else if (mapper != null) { + iterCtx.val = next; + iterCtx2.val = mapper.get(next); + } else { + return valOrErr( + foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); + } + loopCtx = iterCtx2; + } else { + iterCtx.val = next; + } + index++; + + if (filter != null) { + Val include = filter.eval(loopCtx); + if (include == False) { + continue; + } + if (include != True) { + return noSuchOverload(null, Operator.Conditional.id, include); + } + } + + Val value = transform.eval(loopCtx); + if (isUnknownOrError(value)) { + return value; + } + values.add(value); + } + return ListT.newValArrayList(adapter, values.toArray(new Val[0])); + } + + int listCapacity(Val foldRange) { + if (foldRange.type().hasTrait(Trait.SizerType)) { + int size = ((Sizer) foldRange).nativeSize(); + if (size > 0) { + return size; + } + } + return 0; + } + + @SuppressWarnings("DuplicatedCode") + @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; + if (foldRange instanceof Sizer sizer) { + rangeCnt = sizer.nativeSize(); + } else { + 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 + + '\'' + + ", iterVar2='" + + iterVar2 + + '\'' + + ", iterRange=" + + iterRange + + ", filter=" + + filter + + ", transform=" + + transform + + '}'; + } +} + +class EvalFold extends AbstractEval implements Coster { + // TODO combine with EvalExhaustiveFold + final String accuVar; + final String iterVar; + final String iterVar2; + final Interpretable iterRange; + final Interpretable accu; + final Interpretable cond; + final Interpretable step; + final Interpretable result; + + EvalFold( + long id, + String accuVar, + Interpretable accu, + String iterVar, + String iterVar2, + Interpretable iterRange, + Interpretable cond, + Interpretable step, + Interpretable result) { + super(id); + this.accuVar = accuVar; + this.iterVar = iterVar; + this.iterVar2 = iterVar2; + this.iterRange = iterRange; + this.accu = accu; + this.cond = cond; + this.step = step; + this.result = result; + } + + /** Eval implements the Interpretable interface method. */ + @SuppressWarnings("DuplicatedCode") + @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()); + } + // Configure the fold activation with the accumulator initial value. + VarActivation accuCtx = new VarActivation(); + accuCtx.parent = ctx; + accuCtx.name = accuVar; + accuCtx.val = accu.eval(ctx); + VarActivation iterCtx = new VarActivation(); + iterCtx.parent = accuCtx; + iterCtx.name = iterVar; + VarActivation iterCtx2 = null; + if (!iterVar2.isEmpty()) { + iterCtx2 = new VarActivation(); + iterCtx2.parent = iterCtx; + iterCtx2.name = iterVar2; + } + IteratorT it = ((IterableT) foldRange).iterator(); + long index = 0L; + var isLister = foldRange instanceof Lister; + var mapper = (foldRange instanceof Mapper m) ? m : null; + while (it.hasNext() == True) { + // Modify the iter var in the fold activation. + Val next = it.next(); + Activation loopCtx = iterCtx; + if (iterCtx2 != null) { + if (isLister) { + iterCtx.val = intOf(index); + iterCtx2.val = next; + } else if (mapper != null) { + iterCtx.val = next; + iterCtx2.val = mapper.get(next); + } else { + return valOrErr( + foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); + } + loopCtx = iterCtx2; + } else { + iterCtx.val = next; + } + index++; + + // Evaluate the condition, terminate the loop if false. + Val c = cond.eval(loopCtx); + if (c == False) { + break; + } + + // Evalute the evaluation step into accu var. + accuCtx.val = step.eval(loopCtx); + } + // Compute the result. + return result.eval(accuCtx); + } + + /** Cost implements the Coster interface method. */ + @SuppressWarnings("DuplicatedCode") + @Override + public Cost cost() { + // Compute the cost for evaluating iterRange. + Cost i = estimateCost(iterRange); + + // Compute the size of iterRange. If the size depends on the input, return the maximum + // possible + // cost range. + Val foldRange = iterRange.eval(emptyActivation()); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return Cost.Unknown; + } + long rangeCnt; + if (foldRange instanceof Sizer sizer) { + rangeCnt = sizer.nativeSize(); + } else { + rangeCnt = 0L; + IteratorT it = ((IterableT) foldRange).iterator(); + while (it.hasNext() == True) { + it.next(); + rangeCnt++; + } + } + Cost a = estimateCost(accu); + Cost c = estimateCost(cond); + Cost s = estimateCost(step); + Cost r = estimateCost(result); + + // The cond and step costs are multiplied by size(iterRange). The minimum possible cost incurs + // when the evaluation result can be determined by the first iteration. + return i.add(a) + .add(r) + .add(costOf(c.min, c.max * rangeCnt)) + .add(costOf(s.min, s.max * rangeCnt)); + } + + @Override + public String toString() { + return "EvalFold{" + + "id=" + + id + + ", accuVar='" + + accuVar + + '\'' + + ", iterVar='" + + iterVar + + '\'' + + ", iterVar2='" + + iterVar2 + + '\'' + + ", iterRange=" + + iterRange + + ", accu=" + + accu + + ", cond=" + + cond + + ", step=" + + step + + ", result=" + + result + + '}'; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedCallNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedCallNodes.java new file mode 100644 index 000000000..d5fe665ee --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedCallNodes.java @@ -0,0 +1,706 @@ +/* + * Copyright (C) 2021 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.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.Err.valOrErr; +import static org.projectnessie.cel.common.types.Util.isUnknownOrError; +import static org.projectnessie.cel.interpreter.Coster.Cost.OneOne; +import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; +import static org.projectnessie.cel.interpreter.Interpretable.receiveVarArgs; +import static org.projectnessie.cel.interpreter.Interpretable.sumOfCost; + +import java.util.Arrays; +import java.util.Objects; +import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.ref.FieldType; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Container; +import org.projectnessie.cel.common.types.traits.FieldTester; +import org.projectnessie.cel.common.types.traits.Receiver; +import org.projectnessie.cel.common.types.traits.Trait; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableCall; +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; + +/** Package-private established call nodes. */ +final class EvalTestOnly implements Interpretable, Coster { + private final long id; + private final Interpretable op; + private final StringT field; + private final FieldType fieldType; + + EvalTestOnly(long id, Interpretable op, StringT field, FieldType fieldType) { + this.id = id; + this.op = Objects.requireNonNull(op); + this.field = Objects.requireNonNull(field); + this.fieldType = fieldType; + } + + /** ID implements the Interpretable interface method. */ + @Override + public long id() { + return id; + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + // Handle field selection on a proto in the most efficient way possible. + if (fieldType != null) { + if (op instanceof InterpretableAttribute opAttr) { + Object opVal = opAttr.resolve(ctx); + if (opVal instanceof Val refVal) { + opVal = refVal.value(); + } + if (fieldType.isSet.isSet(opVal)) { + return True; + } + return False; + } + } + + Val obj = op.eval(ctx); + if (obj instanceof FieldTester tester) { + return tester.isSet(field); + } + if (obj instanceof Container container) { + return container.contains(field); + } + return valOrErr(obj, "invalid type for field selection."); + } + + /** + * Cost provides the heuristic cost of a `has(field)` macro. The cost has at least 1 for + * determining if the field exists, apart from the cost of accessing the field. + */ + @Override + public Cost cost() { + Cost c = estimateCost(op); + return c.add(OneOne); + } + + @Override + public String toString() { + return "EvalTestOnly{" + "id=" + id + ", field=" + field + '}'; + } +} + +final class EvalZeroArity extends AbstractEval implements InterpretableCall, Coster { + private final String function; + private final String overload; + private final FunctionOp impl; + + EvalZeroArity(long id, String function, String overload, FunctionOp impl) { + super(id); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.impl = impl; + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation activation) { + return impl.invoke(); + } + + /** Cost returns 1 representing the heuristic cost of the function. */ + @Override + public Cost cost() { + return Cost.OneOne; + } + + /** 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 new Interpretable[0]; + } + + @Override + public String toString() { + return "EvalZeroArity{" + + "id=" + + id + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", impl=" + + impl + + '}'; + } +} + +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); + } + + @SuppressWarnings("DuplicatedCode") + @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); + } + + @SuppressWarnings("DuplicatedCode") + @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); + } + + @SuppressWarnings("DuplicatedCode") + @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; + private final Interpretable[] args; + private final Trait trait; + private final FunctionOp impl; + + public EvalVarArgs( + long id, + String function, + String overload, + Interpretable[] args, + Trait trait, + FunctionOp impl) { + super(id); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.args = Objects.requireNonNull(args); + this.trait = trait; + this.impl = impl; + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val[] argVals = new Val[args.length]; + // Early return if any argument to the function is unknown or error. + for (int i = 0; i < args.length; i++) { + Interpretable arg = args[i]; + argVals[i] = arg.eval(ctx); + if (isUnknownOrError(argVals[i])) { + return argVals[i]; + } + } + // If the implementation is bound and the argument value has the right traits required to + // invoke it, then call the implementation. + Val arg0 = argVals[0]; + if (impl != null && (trait == null || arg0.type().hasTrait(trait))) { + return impl.invoke(argVals); + } + // Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the + // operand (arg0). + if (arg0.type().hasTrait(Trait.ReceiverType)) { + return receiveVarArgs((Receiver) arg0, function, overload, argVals); + } + return noSuchOverload(arg0, function, overload, argVals); + } + + /** 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 "EvalVarArgs{" + + "id=" + + id + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", args=" + + Arrays.toString(args) + + ", trait=" + + trait + + ", impl=" + + impl + + '}'; + } +} + +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; + } + + return switch (args.length) { + case 3 -> evalReceiverTail2(ctx, arg0); + case 4 -> evalReceiverTail3(ctx, arg0); + default -> 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) { + 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) + + '}'; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedConstructionNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedConstructionNodes.java new file mode 100644 index 000000000..55bff9d1c --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedConstructionNodes.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2021 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.Err.newErr; +import static org.projectnessie.cel.common.types.Util.isUnknownOrError; +import static org.projectnessie.cel.interpreter.Interpretable.sumOfCost; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.projectnessie.cel.common.types.MapT; +import org.projectnessie.cel.common.types.OptionalT; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeProvider; +import org.projectnessie.cel.common.types.ref.Val; + +/** Package-private established construction nodes. */ +final class EvalMap extends AbstractEval implements Coster { + final Interpretable[] keys; + final Interpretable[] vals; + final boolean[] optionalEntries; + private final TypeAdapter adapter; + + EvalMap(long id, Interpretable[] keys, Interpretable[] vals, TypeAdapter adapter) { + this(id, keys, vals, new boolean[keys.length], adapter); + } + + EvalMap( + long id, + Interpretable[] keys, + Interpretable[] vals, + boolean[] optionalEntries, + TypeAdapter adapter) { + super(id); + this.keys = keys; + this.vals = vals; + this.optionalEntries = optionalEntries; + this.adapter = adapter; + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Map entries = new HashMap<>(keys.length * 4 / 3 + 1); + // If any argument is unknown or error early terminate. + for (int i = 0; i < keys.length; i++) { + Interpretable key = keys[i]; + Val keyVal = key.eval(ctx); + if (isUnknownOrError(keyVal)) { + return keyVal; + } + if (!MapT.isSupportedLiteralKeyType(keyVal)) { + return newErr("unsupported key type"); + } + Val valVal = vals[i].eval(ctx); + if (isUnknownOrError(valVal)) { + return valVal; + } + if (optionalEntries[i]) { + if (!(valVal instanceof OptionalT optional)) { + return newErr("optional map entry is not optional"); + } + if (!optional.hasValue()) { + continue; + } + valVal = optional.getValue(); + } + if (entries.putIfAbsent(keyVal, valVal) != null) { + // Prevent duplicate keys, error out. + return newErr("Failed with repeated key"); + } + } + return MapT.newWrappedMap(adapter, entries); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + Cost k = sumOfCost(keys); + Cost v = sumOfCost(vals); + return k.add(v); + } + + @Override + public String toString() { + return "EvalMap{" + + "id=" + + id + + ", keys=" + + Arrays.toString(keys) + + ", vals=" + + Arrays.toString(vals) + + '}'; + } +} + +final class EvalObj extends AbstractEval implements Coster { + private final String typeName; + private final String[] fields; + private final Interpretable[] vals; + private final boolean[] optionalEntries; + private final TypeProvider provider; + + EvalObj( + long id, + String typeName, + String[] fields, + Interpretable[] vals, + boolean[] optionalEntries, + TypeProvider provider) { + super(id); + this.typeName = Objects.requireNonNull(typeName); + this.fields = Objects.requireNonNull(fields); + this.vals = Objects.requireNonNull(vals); + this.optionalEntries = optionalEntries; + this.provider = Objects.requireNonNull(provider); + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Map fieldVals = new HashMap<>(); + // If any argument is unknown or error early terminate. + for (int i = 0; i < fields.length; i++) { + String field = fields[i]; + Val val = vals[i].eval(ctx); + if (isUnknownOrError(val)) { + return val; + } + if (optionalEntries[i]) { + if (!(val instanceof OptionalT optional)) { + return newErr("optional message field is not optional"); + } + if (!optional.hasValue()) { + continue; + } + val = optional.getValue(); + } + fieldVals.put(field, val); + } + return provider.newValue(typeName, fieldVals); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return sumOfCost(vals); + } + + @Override + public String toString() { + return "EvalObj{" + + "id=" + + id + + ", typeName='" + + typeName + + '\'' + + ", fields=" + + Arrays.toString(fields) + + ", vals=" + + Arrays.toString(vals) + + ", provider=" + + provider + + '}'; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedControlNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedControlNodes.java new file mode 100644 index 000000000..26ae6c17d --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedControlNodes.java @@ -0,0 +1,207 @@ +/* + * 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.Err.isError; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.common.types.Util.isUnknownOrError; + +import java.util.Objects; +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.OptionalT; +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.InterpretableCall; + +class EvalOr extends AbstractEvalLhsRhs { + // TODO combine with EvalExhaustiveOr + EvalOr(long id, Interpretable lhs, Interpretable rhs) { + super(id, lhs, rhs); + } + + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(Activation ctx) { + // short-circuit lhs. + Val lVal = lhs.eval(ctx); + if (lVal == True) { + return True; + } + // short-circuit on rhs. + Val rVal = rhs.eval(ctx); + if (rVal == True) { + return True; + } + // return if both sides are bool false. + if (lVal == False && rVal == False) { + return False; + } + // TODO: return both values as a set if both are unknown or error. + // prefer left unknown to right unknown. + if (isUnknown(lVal)) { + return lVal; + } + if (isUnknown(rVal)) { + return rVal; + } + // If the left-hand side is non-boolean return it as the error. + if (isError(lVal)) { + return lVal; + } + return noSuchOverload(lVal, Operator.LogicalOr.id, rVal); + } + + @Override + public Cost cost() { + return Interpretable.calShortCircuitBinaryOpsCost(lhs, rhs); + } + + @Override + public String toString() { + return "EvalOr{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; + } +} + +class EvalAnd extends AbstractEvalLhsRhs { + // TODO combine with EvalExhaustiveAnd + EvalAnd(long id, Interpretable lhs, Interpretable rhs) { + super(id, lhs, rhs); + } + + @Override + public Val eval(Activation ctx) { + // short-circuit lhs. + Val lVal = lhs.eval(ctx); + if (lVal == False) { + return False; + } + // short-circuit on rhs. + Val rVal = rhs.eval(ctx); + if (rVal == False) { + return False; + } + // return if both sides are bool true. + if (lVal == True && rVal == True) { + return True; + } + // TODO: return both values as a set if both are unknown or error. + // prefer left unknown to right unknown. + if (isUnknown(lVal)) { + return lVal; + } + if (isUnknown(rVal)) { + return rVal; + } + // If the left-hand side is non-boolean return it as the error. + if (isError(lVal)) { + return lVal; + } + return noSuchOverload(lVal, Operator.LogicalAnd.id, rVal); + } + + @Override + public Cost cost() { + return Interpretable.calShortCircuitBinaryOpsCost(lhs, rhs); + } + + @Override + public String toString() { + return "EvalAnd{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; + } +} + +class EvalOptionalOr extends AbstractEvalLhsRhs implements InterpretableCall { + private final String function; + private final String overload; + private final boolean returnValue; + + EvalOptionalOr( + long id, + String function, + String overload, + Interpretable lhs, + Interpretable rhs, + boolean returnValue) { + super(id, lhs, rhs); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.returnValue = returnValue; + } + + @Override + public Val eval(Activation ctx) { + Val left = lhs.eval(ctx); + if (isUnknownOrError(left)) { + return left; + } + if (left instanceof OptionalT optional && optional.hasValue()) { + return returnValue ? optional.getValue() : optional; + } + + Val right = rhs.eval(ctx); + if (isUnknownOrError(right)) { + return right; + } + if (left.type().hasTrait(Trait.ReceiverType)) { + return ((Receiver) left).receive(function, overload, right); + } + return noSuchOverload(left, function, overload, new Val[] {right}); + } + + @Override + public Cost cost() { + return Interpretable.calShortCircuitBinaryOpsCost(lhs, rhs); + } + + @Override + public String function() { + return function; + } + + @Override + public String overloadID() { + return overload; + } + + @Override + public Interpretable[] args() { + return new Interpretable[] {lhs, rhs}; + } + + @Override + public String toString() { + return "EvalOptionalOr{" + + "id=" + + id + + ", lhs=" + + lhs + + ", rhs=" + + rhs + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", returnValue=" + + returnValue + + '}'; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedDecoratorNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedDecoratorNodes.java new file mode 100644 index 000000000..f2a79161c --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedDecoratorNodes.java @@ -0,0 +1,464 @@ +/* + * Copyright (C) 2021 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.Err.isError; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.common.types.Util.isUnknownOrError; +import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; +import static org.projectnessie.cel.interpreter.Interpretable.calExhaustiveBinaryOpsCost; + +import java.util.Objects; +import java.util.Set; +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; +import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifier; +import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifierEquator; +import org.projectnessie.cel.interpreter.AttributeFactory.Qualifier; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableAttribute; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableConst; +import org.projectnessie.cel.interpreter.InterpretableDecorator.EvalObserver; + +/** Package-private established decorator nodes. */ +final class EvalSetMembership extends AbstractEval implements Coster { + private final Interpretable inst; + private final Interpretable arg; + private final String argTypeName; + private final Set valueSet; + + EvalSetMembership(Interpretable inst, Interpretable arg, String argTypeName, Set valueSet) { + super(inst.id()); + this.inst = inst; + this.arg = arg; + this.argTypeName = argTypeName; + this.valueSet = valueSet; + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val val = arg.eval(ctx); + if (isUnknownOrError(val)) { + return val; + } + if (valueSet.isEmpty()) { + return False; + } + if (!val.type().typeName().equals(argTypeName)) { + return noSuchOverload(null, Operator.In.id, val); + } + return valueSet.contains(val) ? True : False; + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return estimateCost(arg); + } + + @Override + public String toString() { + return "EvalSetMembership{" + + "id=" + + id + + ", inst=" + + inst + + ", arg=" + + arg + + ", argTypeName='" + + argTypeName + + '\'' + + ", valueSet=" + + valueSet + + '}'; + } +} + +final class EvalWatch implements Interpretable, Coster { + private final Interpretable i; + private final EvalObserver observer; + + public EvalWatch(Interpretable i, EvalObserver observer) { + this.i = Objects.requireNonNull(i); + this.observer = Objects.requireNonNull(observer); + } + + @Override + public long id() { + return i.id(); + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val val = i.eval(ctx); + observer.observe(id(), val); + return val; + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return estimateCost(i); + } + + @Override + public String toString() { + return "EvalWatch{" + i + '}'; + } +} + +final class EvalWatchAttr implements Coster, InterpretableAttribute, Attribute { + private final InterpretableAttribute attr; + private final EvalObserver observer; + + public EvalWatchAttr(InterpretableAttribute attr, EvalObserver observer) { + this.attr = Objects.requireNonNull(attr); + this.observer = Objects.requireNonNull(observer); + } + + @Override + public long id() { + return attr.id(); + } + + /** + * AddQualifier creates a wrapper over the incoming qualifier which observes the qualification + * result. + */ + @Override + public Attribute addQualifier(AttributeFactory.Qualifier q) { + if (q instanceof ConstantQualifierEquator cq) { + q = new EvalWatchConstQualEquat(cq, observer, attr.adapter()); + } else if (q instanceof ConstantQualifier cq) { + q = new EvalWatchConstQual(cq, observer, attr.adapter()); + } else { + q = new EvalWatchQual(q, observer, attr.adapter()); + } + attr.addQualifier(q); + return this; + } + + @Override + public Attribute attr() { + return attr.attr(); + } + + @Override + public TypeAdapter adapter() { + return attr.adapter(); + } + + @Override + public Object qualify(Activation vars, Object obj) { + return attr.qualify(vars, obj); + } + + @Override + public Object resolve(Activation act) { + return attr.resolve(act); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return estimateCost(attr); + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val val = attr.eval(ctx); + observer.observe(id(), val); + return val; + } + + @Override + public String toString() { + return "EvalWatchAttr{" + attr + '}'; + } +} + +abstract class AbstractEvalWatch extends AbstractEval + implements Coster, Qualifier { + protected final T delegate; + protected final EvalObserver observer; + protected final TypeAdapter adapter; + + AbstractEvalWatch(T delegate, EvalObserver observer, TypeAdapter adapter) { + super(delegate.id()); + this.delegate = delegate; + this.observer = Objects.requireNonNull(observer); + this.adapter = Objects.requireNonNull(adapter); + } + + /** Qualify observes the qualification of a object via a value computed at runtime. */ + @Override + public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { + Object out = delegate.qualify(vars, obj); + Val val; + if (out != null) { + val = adapter.nativeToValue(out); + } else { + val = newErr(String.format("qualify failed, vars=%s, obj=%s", vars, obj)); + } + observer.observe(id(), val); + return out; + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return estimateCost(delegate); + } +} + +final class EvalWatchConstQualEquat extends AbstractEvalWatch + implements ConstantQualifierEquator { + EvalWatchConstQualEquat( + ConstantQualifierEquator delegate, EvalObserver observer, TypeAdapter adapter) { + super(delegate, observer, adapter); + } + + @Override + public Val eval(Activation activation) { + throw new UnsupportedOperationException("WTF?"); + } + + @Override + public Val value() { + return delegate.value(); + } + + /** + * QualifierValueEquals tests whether the incoming value is equal to the qualificying constant. + */ + @Override + public boolean qualifierValueEquals(Object value) { + return delegate.qualifierValueEquals(value); + } + + @Override + public String toString() { + return "EvalWatchConstQualEquat{" + delegate + '}'; + } +} + +final class EvalWatchConstQual extends AbstractEvalWatch + implements ConstantQualifier, Coster { + EvalWatchConstQual(ConstantQualifier delegate, EvalObserver observer, TypeAdapter adapter) { + super(delegate, observer, adapter); + } + + @Override + public Val eval(Activation activation) { + throw new UnsupportedOperationException("WTF?"); + } + + @Override + public Val value() { + return delegate.value(); + } + + @Override + public String toString() { + return "EvalWatchConstQual{" + delegate + '}'; + } +} + +final class EvalWatchQual extends AbstractEvalWatch { + public EvalWatchQual(Qualifier delegate, EvalObserver observer, TypeAdapter adapter) { + super(delegate, observer, adapter); + } + + @Override + public Val eval(Activation activation) { + throw new UnsupportedOperationException("WTF?"); + } + + @Override + public String toString() { + return "EvalWatchQual{" + delegate + '}'; + } +} + +final class EvalWatchConst implements InterpretableConst, Coster { + private final InterpretableConst c; + private final EvalObserver observer; + + EvalWatchConst(InterpretableConst c, EvalObserver observer) { + this.c = Objects.requireNonNull(c); + this.observer = Objects.requireNonNull(observer); + } + + @Override + public long id() { + return c.id(); + } + + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation activation) { + Val val = value(); + observer.observe(id(), val); + return val; + } + + @Override + public Val value() { + return c.value(); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return estimateCost(c); + } + + @Override + public String toString() { + return "EvalWatchConst{" + c + '}'; + } +} + +final class EvalExhaustiveOr extends AbstractEvalLhsRhs { + // TODO combine with EvalOr + EvalExhaustiveOr(long id, Interpretable lhs, Interpretable rhs) { + super(id, lhs, rhs); + } + + /** Eval implements the Interpretable interface method. */ + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val lVal = lhs.eval(ctx); + Val rVal = rhs.eval(ctx); + if (lVal == True || rVal == True) { + return True; + } + if (lVal == False && rVal == False) { + return False; + } + if (isUnknown(lVal)) { + return lVal; + } + if (isUnknown(rVal)) { + return rVal; + } + // TODO: Combine the errors into a set in the future. + // If the left-hand side is non-boolean return it as the error. + if (isError(lVal)) { + return lVal; + } + return noSuchOverload(lVal, Operator.LogicalOr.id, rVal); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return calExhaustiveBinaryOpsCost(lhs, rhs); + } + + @Override + public String toString() { + return "EvalExhaustiveOr{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; + } +} + +final class EvalExhaustiveAnd extends AbstractEvalLhsRhs { + // TODO combine with EvalAnd + EvalExhaustiveAnd(long id, Interpretable lhs, Interpretable rhs) { + super(id, lhs, rhs); + } + + /** Eval implements the Interpretable interface method. */ + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val lVal = lhs.eval(ctx); + Val rVal = rhs.eval(ctx); + if (lVal == False || rVal == False) { + return False; + } + if (lVal == True && rVal == True) { + return True; + } + if (isUnknown(lVal)) { + return lVal; + } + if (isUnknown(rVal)) { + return rVal; + } + if (isError(lVal)) { + return lVal; + } + return noSuchOverload(lVal, Operator.LogicalAnd.id, rVal); + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return calExhaustiveBinaryOpsCost(lhs, rhs); + } + + @Override + public String toString() { + return "EvalExhaustiveAnd{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; + } +} + +final class EvalExhaustiveConditional extends AbstractEval implements Coster { + // TODO combine with EvalConditional + private final TypeAdapter adapter; + private final ConditionalAttribute attr; + + EvalExhaustiveConditional(long id, TypeAdapter adapter, ConditionalAttribute attr) { + super(id); + this.adapter = Objects.requireNonNull(adapter); + this.attr = Objects.requireNonNull(attr); + } + + /** Eval implements the Interpretable interface method. */ + @Override + public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { + Val cVal = attr.expr().eval(ctx); + Object tVal = attr.truthy().resolve(ctx); + Object fVal = attr.falsy().resolve(ctx); + if (cVal == True) { + return adapter.nativeToValue(tVal); + } else if (cVal == False) { + return adapter.nativeToValue(fVal); + } else { + return noSuchOverload(null, Operator.Conditional.id, cVal); + } + } + + /** Cost implements the Coster interface method. */ + @Override + public Cost cost() { + return attr.cost(); + } + + @Override + public String toString() { + return "EvalExhaustiveConditional{" + "id=" + id + ", attr=" + attr + '}'; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedFoldNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedFoldNodes.java new file mode 100644 index 000000000..8ecf5549b --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedFoldNodes.java @@ -0,0 +1,516 @@ +/* + * Copyright (C) 2021 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.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.Err.valOrErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.Util.isUnknownOrError; +import static org.projectnessie.cel.interpreter.Activation.emptyActivation; +import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.projectnessie.cel.common.operators.Operator; +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.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.common.types.traits.Mapper; +import org.projectnessie.cel.common.types.traits.Sizer; +import org.projectnessie.cel.common.types.traits.Trait; + +/** Package-private established fold nodes. */ +final class EvalMapFold extends AbstractEval implements Coster { + final String iterVar; + final String iterVar2; + final Interpretable iterRange; + final Interpretable filter; + final Interpretable transform; + final TypeAdapter adapter; + + EvalMapFold( + long id, + String iterVar, + String iterVar2, + Interpretable iterRange, + Interpretable filter, + Interpretable transform, + TypeAdapter adapter) { + super(id); + this.iterVar = iterVar; + this.iterVar2 = iterVar2; + this.iterRange = iterRange; + this.filter = filter; + this.transform = transform; + this.adapter = adapter; + } + + @SuppressWarnings("DuplicatedCode") + @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; + VarActivation iterCtx2 = null; + if (!iterVar2.isEmpty()) { + iterCtx2 = new VarActivation(); + iterCtx2.parent = iterCtx; + iterCtx2.name = iterVar2; + } + Map values = new HashMap<>(mapCapacity(foldRange)); + IteratorT it = ((IterableT) foldRange).iterator(); + long index = 0L; + var isLister = foldRange instanceof Lister; + var mapper = (foldRange instanceof Mapper m) ? m : null; + while (it.hasNext() == True) { + Val next = it.next(); + Val key; + Activation loopCtx = iterCtx; + if (iterCtx2 != null) { + if (isLister) { + key = intOf(index); + iterCtx.val = key; + iterCtx2.val = next; + } else if (mapper != null) { + key = next; + iterCtx.val = key; + iterCtx2.val = mapper.get(next); + } else { + return valOrErr( + foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); + } + loopCtx = iterCtx2; + } else { + key = next; + iterCtx.val = next; + } + index++; + + if (filter != null) { + Val include = filter.eval(loopCtx); + if (include == False) { + continue; + } + if (include != True) { + return noSuchOverload(null, Operator.Conditional.id, include); + } + } + + Val value = transform.eval(loopCtx); + if (isUnknownOrError(value)) { + return value; + } + values.put(key, value); + } + return MapT.newWrappedMap(adapter, values); + } + + int mapCapacity(Val foldRange) { + if (foldRange.type().hasTrait(Trait.SizerType)) { + int size = ((Sizer) foldRange).nativeSize(); + if (size > 0) { + long capacity = size * 4L / 3 + 1; + return capacity <= Integer.MAX_VALUE ? (int) capacity : Integer.MAX_VALUE; + } + } + return 0; + } + + @SuppressWarnings("DuplicatedCode") + @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; + if (foldRange instanceof Sizer sizer) { + rangeCnt = sizer.nativeSize(); + } else { + 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 "EvalMapFold{" + + "id=" + + id + + ", iterVar='" + + iterVar + + '\'' + + ", iterVar2='" + + iterVar2 + + '\'' + + ", iterRange=" + + iterRange + + ", filter=" + + filter + + ", transform=" + + transform + + '}'; + } +} + +final class EvalExhaustiveFold extends AbstractEval implements Coster { + // TODO combine with EvalFold + private final String accuVar; + private final String iterVar; + private final String iterVar2; + private final Interpretable iterRange; + private final Interpretable accu; + private final Interpretable cond; + private final Interpretable step; + private final Interpretable result; + + EvalExhaustiveFold( + long id, + Interpretable accu, + String accuVar, + Interpretable iterRange, + String iterVar, + String iterVar2, + Interpretable cond, + Interpretable step, + Interpretable result) { + super(id); + this.accuVar = accuVar; + this.iterVar = iterVar; + this.iterVar2 = iterVar2; + this.iterRange = iterRange; + this.accu = accu; + this.cond = cond; + this.step = step; + this.result = result; + } + + /** Eval implements the Interpretable interface method. */ + @SuppressWarnings("DuplicatedCode") + @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()); + } + // Configure the fold activation with the accumulator initial value. + VarActivation accuCtx = new VarActivation(); + accuCtx.parent = ctx; + accuCtx.name = accuVar; + accuCtx.val = accu.eval(ctx); + VarActivation iterCtx = new VarActivation(); + iterCtx.parent = accuCtx; + iterCtx.name = iterVar; + VarActivation iterCtx2 = null; + if (!iterVar2.isEmpty()) { + iterCtx2 = new VarActivation(); + iterCtx2.parent = iterCtx; + iterCtx2.name = iterVar2; + } + IteratorT it = ((IterableT) foldRange).iterator(); + var isLister = foldRange instanceof Lister; + var mapper = (foldRange instanceof Mapper m) ? m : null; + long index = 0L; + while (it.hasNext() == True) { + // Modify the iter var in the fold activation. + Val next = it.next(); + Activation loopCtx = iterCtx; + if (iterCtx2 != null) { + if (isLister) { + iterCtx.val = intOf(index); + iterCtx2.val = next; + } else if (mapper != null) { + iterCtx.val = next; + iterCtx2.val = mapper.get(next); + } else { + return valOrErr( + foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); + } + loopCtx = iterCtx2; + } else { + iterCtx.val = next; + } + index++; + + // Evaluate the condition, but don't terminate the loop as this is exhaustive eval! + cond.eval(loopCtx); + + // Evalute the evaluation step into accu var. + accuCtx.val = step.eval(loopCtx); + } + // Compute the result. + return result.eval(accuCtx); + } + + /** Cost implements the Coster interface method. */ + @SuppressWarnings("DuplicatedCode") + @Override + public Cost cost() { + // Compute the cost for evaluating iterRange. + Cost i = estimateCost(iterRange); + + // Compute the size of iterRange. If the size depends on the input, return the maximum + // possible + // cost range. + Val foldRange = iterRange.eval(emptyActivation()); + if (!foldRange.type().hasTrait(Trait.IterableType)) { + return Cost.Unknown; + } + long rangeCnt; + if (foldRange instanceof Sizer sizer) { + rangeCnt = sizer.nativeSize(); + } else { + rangeCnt = 0L; + IteratorT it = ((IterableT) foldRange).iterator(); + while (it.hasNext() == True) { + it.next(); + rangeCnt++; + } + } + + Cost a = estimateCost(accu); + Cost c = estimateCost(cond); + Cost s = estimateCost(step); + Cost r = estimateCost(result); + + // The cond and step costs are multiplied by size(iterRange). + return i.add(a).add(c.multiply(rangeCnt)).add(s.multiply(rangeCnt)).add(r); + } + + @Override + public String toString() { + return "EvalExhaustiveFold{" + + "id=" + + id + + ", accuVar='" + + accuVar + + '\'' + + ", iterVar='" + + iterVar + + '\'' + + ", iterVar2='" + + iterVar2 + + '\'' + + ", iterRange=" + + iterRange + + ", accu=" + + accu + + ", cond=" + + cond + + ", step=" + + step + + ", result=" + + result + + '}'; + } +} + +final class EvalExhaustiveListFold extends AbstractEval implements Coster { + private final EvalListFold fold; + + EvalExhaustiveListFold(EvalListFold fold) { + super(fold.id); + this.fold = fold; + } + + @SuppressWarnings("DuplicatedCode") + @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; + VarActivation iterCtx2 = null; + if (!fold.iterVar2.isEmpty()) { + iterCtx2 = new VarActivation(); + iterCtx2.parent = iterCtx; + iterCtx2.name = fold.iterVar2; + } + List values = new ArrayList<>(fold.listCapacity(foldRange)); + Val result = null; + IteratorT it = ((IterableT) foldRange).iterator(); + long index = 0L; + while (it.hasNext() == True) { + Val next = it.next(); + Activation loopCtx = iterCtx; + if (iterCtx2 != null) { + if (foldRange instanceof Lister) { + iterCtx.val = intOf(index); + iterCtx2.val = next; + } else if (foldRange instanceof Mapper) { + iterCtx.val = next; + iterCtx2.val = ((Mapper) foldRange).get(next); + } else { + return valOrErr( + foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); + } + loopCtx = iterCtx2; + } else { + iterCtx.val = next; + } + index++; + + Val include = fold.filter != null ? fold.filter.eval(loopCtx) : True; + Val value = fold.transform.eval(loopCtx); + if (include == False) { + continue; + } + if (include != True) { + if (result == null) { + 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 + '}'; + } +} + +final class EvalExhaustiveMapFold extends AbstractEval implements Coster { + private final EvalMapFold fold; + + EvalExhaustiveMapFold(EvalMapFold fold) { + super(fold.id); + this.fold = fold; + } + + @SuppressWarnings("DuplicatedCode") + @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; + VarActivation iterCtx2 = null; + if (!fold.iterVar2.isEmpty()) { + iterCtx2 = new VarActivation(); + iterCtx2.parent = iterCtx; + iterCtx2.name = fold.iterVar2; + } + Map values = new HashMap<>(fold.mapCapacity(foldRange)); + Val result = null; + IteratorT it = ((IterableT) foldRange).iterator(); + long index = 0L; + while (it.hasNext() == True) { + Val next = it.next(); + Val key; + Activation loopCtx = iterCtx; + if (iterCtx2 != null) { + if (foldRange instanceof Lister) { + key = intOf(index); + iterCtx.val = key; + iterCtx2.val = next; + } else if (foldRange instanceof Mapper) { + key = next; + iterCtx.val = key; + iterCtx2.val = ((Mapper) foldRange).get(next); + } else { + return valOrErr( + foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); + } + loopCtx = iterCtx2; + } else { + key = next; + iterCtx.val = next; + } + index++; + + Val include = fold.filter != null ? fold.filter.eval(loopCtx) : True; + Val value = fold.transform.eval(loopCtx); + if (include == False) { + continue; + } + if (include != True) { + if (result == null) { + result = noSuchOverload(null, Operator.Conditional.id, include); + } + continue; + } + if (result == null) { + if (isUnknownOrError(value)) { + result = value; + } else { + values.put(key, value); + } + } + } + return result != null ? result : MapT.newWrappedMap(fold.adapter, values); + } + + @Override + public Cost cost() { + return fold.cost(); + } + + @Override + public String toString() { + return "EvalExhaustiveMapFold{" + fold + '}'; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedScalarNodes.java b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedScalarNodes.java new file mode 100644 index 000000000..9a5d13bd3 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/EstablishedScalarNodes.java @@ -0,0 +1,479 @@ +/* + * 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.Err.newErr; +import static org.projectnessie.cel.common.types.Err.noSuchAttributeException; +import static org.projectnessie.cel.common.types.Err.noSuchOverload; +import static org.projectnessie.cel.common.types.UnknownT.unknownOf; +import static org.projectnessie.cel.common.types.Util.isUnknownOrError; +import static org.projectnessie.cel.interpreter.Coster.Cost.OneOne; +import static org.projectnessie.cel.interpreter.Coster.Cost.estimateCost; + +import java.util.Objects; +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.Overloads; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +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.Activation.PartialActivation; +import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableAttribute; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableCall; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableConst; +import org.projectnessie.cel.interpreter.functions.BinaryOp; +import org.projectnessie.cel.interpreter.functions.UnaryOp; + +abstract class AbstractEval implements Interpretable { + protected final long id; + + AbstractEval(long id) { + this.id = id; + } + + @Override + public long id() { + return id; + } + + @Override + public String toString() { + return "id=" + id; + } +} + +abstract class AbstractEvalLhsRhs extends AbstractEval implements Coster { + protected final Interpretable lhs; + protected final Interpretable rhs; + + AbstractEvalLhsRhs(long id, Interpretable lhs, Interpretable rhs) { + super(id); + this.lhs = Objects.requireNonNull(lhs); + this.rhs = Objects.requireNonNull(rhs); + } + + @Override + public String toString() { + return "AbstractEvalLhsRhs{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; + } +} + +/** Evaluates a checked top-level variable directly from the activation. */ +class EvalIdent extends AbstractEval implements Coster { + protected final String name; + protected final TypeAdapter adapter; + + EvalIdent(long id, String name, TypeAdapter adapter) { + super(id); + this.name = Objects.requireNonNull(name); + this.adapter = Objects.requireNonNull(adapter); + } + + @SuppressWarnings("DuplicatedCode") + @Override + public Val eval(Activation ctx) { + if (ctx instanceof PartialActivation partialActivation) { + for (AttributePattern pattern : partialActivation.unknownAttributePatterns()) { + if (pattern.variableMatches(name)) { + return unknownOf(id); + } + } + } + + Object value = ctx.resolve(name); + if (Activation.ABSENT == value) { + RuntimeException err = noSuchAttributeException("id: " + id + ", names: [" + name + "]"); + return newErr(err, err.toString()); + } + return adapter.nativeToValue(value); + } + + @SuppressWarnings("DuplicatedCode") + protected final Object resolveRaw(Activation ctx) { + if (ctx instanceof PartialActivation partialActivation) { + for (AttributePattern pattern : partialActivation.unknownAttributePatterns()) { + if (pattern.variableMatches(name)) { + return unknownOf(id); + } + } + } + + Object value = ctx.resolve(name); + if (Activation.ABSENT == value) { + RuntimeException err = noSuchAttributeException("id: " + id + ", names: [" + name + "]"); + return newErr(err, err.toString()); + } + return value; + } + + @Override + public Cost cost() { + return OneOne; + } + + @Override + public String toString() { + return "EvalIdent{" + "id=" + id + ", name='" + name + '\'' + '}'; + } +} + +/** Checked aggregate identifier whose ordinary evaluation uses exact checked materialization. */ +class EvalConst extends AbstractEval implements InterpretableConst, Coster { + private final Val val; + + EvalConst(long id, Val val) { + super(id); + this.val = val; + } + + @Override + public Val eval(Activation activation) { + return val; + } + + @Override + public Cost cost() { + return Cost.None; + } + + @Override + public Val value() { + return val; + } + + @Override + public String toString() { + return "EvalConst{" + "id=" + id + ", val=" + val + '}'; + } +} + +class EvalUnary extends AbstractEval implements InterpretableCall, Coster { + private final String function; + private final String overload; + protected final Interpretable arg; + private final Trait trait; + private final UnaryOp impl; + + EvalUnary( + long id, String function, String overload, Interpretable arg, Trait trait, UnaryOp impl) { + super(id); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.arg = Objects.requireNonNull(arg); + this.trait = trait; + this.impl = impl; + } + + @Override + public Val eval(Activation ctx) { + return evalPrepared(arg.eval(ctx)); + } + + protected final Val evalPrepared(Val argValue) { + if (isUnknownOrError(argValue)) { + return argValue; + } + if (impl != null && (trait == null || argValue.type().hasTrait(trait))) { + return impl.invoke(argValue); + } + if (argValue.type().hasTrait(Trait.ReceiverType)) { + return ((Receiver) argValue).receive(function, overload); + } + return noSuchOverload(argValue, function, overload, new Val[0]); + } + + @Override + public Cost cost() { + return Cost.OneOne.add(estimateCost(arg)); + } + + @Override + public String function() { + return function; + } + + @Override + public String overloadID() { + return overload; + } + + @Override + public Interpretable[] args() { + return new Interpretable[] {arg}; + } + + @Override + public String toString() { + return "EvalUnary{" + + "id=" + + id + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", arg=" + + arg + + ", trait=" + + trait + + ", impl=" + + impl + + '}'; + } +} + +class EvalEq extends AbstractEvalLhsRhs implements InterpretableCall { + EvalEq(long id, Interpretable lhs, Interpretable rhs) { + super(id, lhs, rhs); + } + + @Override + public Val eval(Activation ctx) { + return evalPrepared(lhs.eval(ctx), rhs.eval(ctx)); + } + + protected final Val evalPrepared(Val left, Val right) { + if (isUnknownOrError(left)) { + return left; + } + if (isUnknownOrError(right)) { + return right; + } + return left.equal(right); + } + + @Override + public Cost cost() { + return Interpretable.calExhaustiveBinaryOpsCost(lhs, rhs); + } + + @Override + public String function() { + return Operator.Equals.id; + } + + @Override + public String overloadID() { + return Overloads.Equals; + } + + @Override + public Interpretable[] args() { + return new Interpretable[] {lhs, rhs}; + } + + @Override + public String toString() { + return "EvalEq{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; + } +} + +class EvalNe extends AbstractEvalLhsRhs implements InterpretableCall { + EvalNe(long id, Interpretable lhs, Interpretable rhs) { + super(id, lhs, rhs); + } + + @Override + public Val eval(Activation ctx) { + return evalPrepared(lhs.eval(ctx), rhs.eval(ctx)); + } + + protected final Val evalPrepared(Val left, Val right) { + if (isUnknownOrError(left)) { + return left; + } + if (isUnknownOrError(right)) { + return right; + } + Val equal = left.equal(right); + return switch (equal.type().typeEnum()) { + case Err -> equal; + case Bool -> ((Negater) equal).negate(); + default -> noSuchOverload(left, Operator.NotEquals.id, right); + }; + } + + @Override + public Cost cost() { + return Interpretable.calExhaustiveBinaryOpsCost(lhs, rhs); + } + + @Override + public String function() { + return Operator.NotEquals.id; + } + + @Override + public String overloadID() { + return Overloads.NotEquals; + } + + @Override + public Interpretable[] args() { + return new Interpretable[] {lhs, rhs}; + } + + @Override + public String toString() { + return "EvalNe{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; + } +} + +class EvalBinary extends AbstractEvalLhsRhs implements InterpretableCall { + protected final String function; + protected final String overload; + protected final Trait trait; + protected final BinaryOp impl; + + EvalBinary( + long id, + String function, + String overload, + Interpretable lhs, + Interpretable rhs, + Trait trait, + BinaryOp impl) { + super(id, lhs, rhs); + this.function = Objects.requireNonNull(function); + this.overload = Objects.requireNonNull(overload); + this.trait = trait; + this.impl = impl; + } + + @Override + public Val eval(Activation ctx) { + return evalPrepared(lhs.eval(ctx), rhs.eval(ctx)); + } + + protected final Val evalPrepared(Val left, Val right) { + if (isUnknownOrError(left)) { + return left; + } + if (isUnknownOrError(right)) { + return right; + } + if (impl != null && (trait == null || left.type().hasTrait(trait))) { + return impl.invoke(left, right); + } + if (left.type().hasTrait(Trait.ReceiverType)) { + return ((Receiver) left).receive(function, overload, right); + } + return noSuchOverload(left, function, overload, new Val[] {right}); + } + + @Override + public Cost cost() { + return Interpretable.calExhaustiveBinaryOpsCost(lhs, rhs); + } + + @Override + public String function() { + return function; + } + + @Override + public String overloadID() { + return overload; + } + + @Override + public Interpretable[] args() { + return new Interpretable[] {lhs, rhs}; + } + + @Override + public String toString() { + return "EvalBinary{" + + "id=" + + id + + ", lhs=" + + lhs + + ", rhs=" + + rhs + + ", function='" + + function + + '\'' + + ", overload='" + + overload + + '\'' + + ", trait=" + + trait + + ", impl=" + + impl + + '}'; + } +} + +/** Evaluates an attribute value. */ +class EvalAttr extends AbstractEval + implements InterpretableAttribute, Coster, AttributeFactory.Qualifier, Attribute { + protected final TypeAdapter adapter; + protected Attribute attr; + + EvalAttr(TypeAdapter adapter, Attribute attr) { + super(attr.id()); + this.adapter = Objects.requireNonNull(adapter); + this.attr = Objects.requireNonNull(attr); + } + + @Override + public Attribute addQualifier(AttributeFactory.Qualifier qualifier) { + attr = attr.addQualifier(qualifier); + return attr; + } + + @Override + public Attribute attr() { + return attr; + } + + @Override + public TypeAdapter adapter() { + return adapter; + } + + @Override + public Cost cost() { + return estimateCost(attr); + } + + @Override + public Val eval(Activation ctx) { + try { + return adapter.nativeToValue(attr.resolve(ctx)); + } catch (Exception e) { + return newErr(e, e.toString()); + } + } + + @Override + public Object qualify(Activation ctx, Object obj) { + return attr.qualify(ctx, obj); + } + + @Override + public Object resolve(Activation ctx) { + return attr.resolve(ctx); + } + + @Override + public String toString() { + return "EvalAttr{" + "id=" + id + ", attr=" + attr + '}'; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/ExprInterpreter.java b/core/src/main/java/org/projectnessie/cel/interpreter/ExprInterpreter.java new file mode 100644 index 000000000..30df41489 --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/ExprInterpreter.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2021 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.interpreter.InterpretablePlanner.newPlanner; +import static org.projectnessie.cel.interpreter.InterpretablePlanner.newUncheckedPlanner; + +import com.google.api.expr.v1alpha1.CheckedExpr; +import com.google.api.expr.v1alpha1.Expr; +import com.google.api.expr.v1alpha1.Reference; +import com.google.api.expr.v1alpha1.Type; +import java.util.Map; +import org.projectnessie.cel.common.containers.Container; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeProvider; + +final class ExprInterpreter implements Interpreter { + private final Dispatcher dispatcher; + private final Container container; + private final TypeProvider provider; + private final TypeAdapter adapter; + private final AttributeFactory attrFactory; + + ExprInterpreter( + Dispatcher dispatcher, + Container container, + TypeProvider provider, + TypeAdapter adapter, + AttributeFactory attrFactory) { + this.dispatcher = dispatcher; + this.container = container; + this.provider = provider; + this.adapter = adapter; + this.attrFactory = attrFactory; + } + + @Override + public Interpretable newInterpretable(CheckedExpr checked, InterpretableDecorator... decorators) { + InterpretablePlanner p = + newPlanner(dispatcher, provider, adapter, attrFactory, container, checked, decorators); + return p.plan(checked.getExpr()); + } + + @Override + public Interpretable newInterpretable( + Expr expr, + Map refMap, + Map typeMap, + InterpretableDecorator... decorators) { + InterpretablePlanner p = + newPlanner( + dispatcher, provider, adapter, attrFactory, container, refMap, typeMap, decorators); + return p.plan(expr); + } + + @Override + public Interpretable newUncheckedInterpretable(Expr expr, InterpretableDecorator... decorators) { + InterpretablePlanner p = + newUncheckedPlanner(dispatcher, provider, adapter, attrFactory, container, decorators); + return p.plan(expr); + } +} 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 59a11d5dc..92e20cf35 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java @@ -15,64 +15,16 @@ */ 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.Err.isError; -import static org.projectnessie.cel.common.types.Err.newErr; -import static org.projectnessie.cel.common.types.Err.noSuchAttributeException; -import static org.projectnessie.cel.common.types.Err.noSuchOverload; -import static org.projectnessie.cel.common.types.Err.valOrErr; -import static org.projectnessie.cel.common.types.IntT.intOf; -import static org.projectnessie.cel.common.types.UnknownT.isUnknown; -import static org.projectnessie.cel.common.types.UnknownT.unknownOf; -import static org.projectnessie.cel.common.types.Util.isUnknownOrError; -import static org.projectnessie.cel.interpreter.Activation.emptyActivation; -import static org.projectnessie.cel.interpreter.Coster.Cost.OneOne; 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; -import org.projectnessie.cel.common.operators.Operator; -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.OptionalT; -import org.projectnessie.cel.common.types.Overloads; -import org.projectnessie.cel.common.types.StringT; -import org.projectnessie.cel.common.types.ref.FieldType; import org.projectnessie.cel.common.types.ref.TypeAdapter; -import org.projectnessie.cel.common.types.ref.TypeProvider; import org.projectnessie.cel.common.types.ref.Val; -import org.projectnessie.cel.common.types.traits.Container; -import org.projectnessie.cel.common.types.traits.FieldTester; -import org.projectnessie.cel.common.types.traits.Lister; -import org.projectnessie.cel.common.types.traits.Mapper; -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.PartialActivation; -import org.projectnessie.cel.interpreter.Activation.VarActivation; import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; -import org.projectnessie.cel.interpreter.AttributeFactory.ConditionalAttribute; -import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifier; -import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifierEquator; import org.projectnessie.cel.interpreter.AttributeFactory.Qualifier; import org.projectnessie.cel.interpreter.Coster.Cost; -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; /** * Interpretable can accept a given Activation and produce a value along with an accompanying @@ -149,1982 +101,26 @@ interface InterpretableCall extends Interpretable { Interpretable[] args(); } - // Core Interpretable implementations used during the program planning phase. - - /** evalIdent evaluates a checked top-level variable directly from the activation. */ - final class EvalIdent extends AbstractEval implements Coster { - private final String name; - private final TypeAdapter adapter; - - EvalIdent(long id, String name, TypeAdapter adapter) { - super(id); - this.name = Objects.requireNonNull(name); - this.adapter = Objects.requireNonNull(adapter); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(Activation ctx) { - if (ctx instanceof PartialActivation partialActivation) { - for (AttributePattern pattern : partialActivation.unknownAttributePatterns()) { - if (pattern.variableMatches(name)) { - return unknownOf(id); - } - } - } - - ResolvedValue value = ctx.resolveName(name); - if (!value.present()) { - RuntimeException err = noSuchAttributeException("id: " + id + ", names: [" + name + "]"); - return newErr(err, err.toString()); - } - return adapter.nativeToValue(value.value()); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return OneOne; - } - - @Override - public String toString() { - return "EvalIdent{" + "id=" + id + ", name='" + name + '\'' + '}'; - } - } - - final class EvalTestOnly implements Interpretable, Coster { - private final long id; - private final Interpretable op; - private final StringT field; - private final FieldType fieldType; - - EvalTestOnly(long id, Interpretable op, StringT field, FieldType fieldType) { - this.id = id; - this.op = Objects.requireNonNull(op); - this.field = Objects.requireNonNull(field); - this.fieldType = fieldType; - } - - /** ID implements the Interpretable interface method. */ - @Override - public long id() { - return id; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - // Handle field selection on a proto in the most efficient way possible. - if (fieldType != null) { - if (op instanceof InterpretableAttribute opAttr) { - Object opVal = opAttr.resolve(ctx); - if (opVal instanceof Val refVal) { - opVal = refVal.value(); - } - if (fieldType.isSet.isSet(opVal)) { - return True; - } - return False; - } - } - - Val obj = op.eval(ctx); - if (obj instanceof FieldTester tester) { - return tester.isSet(field); - } - if (obj instanceof Container container) { - return container.contains(field); - } - return valOrErr(obj, "invalid type for field selection."); - } - - /** - * Cost provides the heuristic cost of a `has(field)` macro. The cost has at least 1 for - * determining if the field exists, apart from the cost of accessing the field. - */ - @Override - public Cost cost() { - Cost c = estimateCost(op); - return c.add(OneOne); - } - - @Override - public String toString() { - return "EvalTestOnly{" + "id=" + id + ", field=" + field + '}'; - } - } - /** NewConstValue creates a new constant valued Interpretable. */ - static InterpretableConst newConstValue(long id, Val val) { - return new EvalConst(id, val); - } - - abstract class AbstractEval implements Interpretable { - protected final long id; - - AbstractEval(long id) { - this.id = id; - } - - /** ID implements the Interpretable interface method. */ - @Override - public long id() { - return id; - } - - @Override - public String toString() { - return "id=" + id; - } - } - - abstract class AbstractEvalLhsRhs extends AbstractEval implements Coster { - protected final Interpretable lhs; - protected final Interpretable rhs; - - AbstractEvalLhsRhs(long id, Interpretable lhs, Interpretable rhs) { - super(id); - this.lhs = Objects.requireNonNull(lhs); - this.rhs = Objects.requireNonNull(rhs); - } - - @Override - public String toString() { - return "AbstractEvalLhsRhs{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; - } - } - - final class EvalConst extends AbstractEval implements InterpretableConst, Coster { - private final Val val; - - EvalConst(long id, Val val) { - super(id); - this.val = val; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation activation) { - return val; - } - - /** Cost returns zero for a constant valued Interpretable. */ - @Override - public Cost cost() { - return Cost.None; - } - - /** Value implements the InterpretableConst interface method. */ - @Override - public Val value() { - return val; - } - - @Override - public String toString() { - return "EvalConst{" + "id=" + id + ", val=" + val + '}'; - } - } - - final class EvalOr extends AbstractEvalLhsRhs { - // TODO combine with EvalExhaustiveOr - EvalOr(long id, Interpretable lhs, Interpretable rhs) { - super(id, lhs, rhs); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - // short-circuit lhs. - Val lVal = lhs.eval(ctx); - if (lVal == True) { - return True; - } - // short-circuit on rhs. - Val rVal = rhs.eval(ctx); - if (rVal == True) { - return True; - } - // return if both sides are bool false. - if (lVal == False && rVal == False) { - return False; - } - // TODO: return both values as a set if both are unknown or error. - // prefer left unknown to right unknown. - if (isUnknown(lVal)) { - return lVal; - } - if (isUnknown(rVal)) { - return rVal; - } - // If the left-hand side is non-boolean return it as the error. - if (isError(lVal)) { - return lVal; - } - return noSuchOverload(lVal, Operator.LogicalOr.id, rVal); - } - - /** - * Cost implements the Coster interface method. The minimum possible cost incurs when the - * left-hand side expr is sufficient in determining the evaluation result. - */ - @Override - public Cost cost() { - return calShortCircuitBinaryOpsCost(lhs, rhs); - } - - @Override - public String toString() { - return "EvalOr{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; - } - } - - final class EvalAnd extends AbstractEvalLhsRhs { - // TODO combine with EvalExhaustiveAnd - EvalAnd(long id, Interpretable lhs, Interpretable rhs) { - super(id, lhs, rhs); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - // short-circuit lhs. - Val lVal = lhs.eval(ctx); - if (lVal == False) { - return False; - } - // short-circuit on rhs. - Val rVal = rhs.eval(ctx); - if (rVal == False) { - return False; - } - // return if both sides are bool true. - if (lVal == True && rVal == True) { - return True; - } - // TODO: return both values as a set if both are unknown or error. - // prefer left unknown to right unknown. - if (isUnknown(lVal)) { - return lVal; - } - if (isUnknown(rVal)) { - return rVal; - } - // If the left-hand side is non-boolean return it as the error. - if (isError(lVal)) { - return lVal; - } - return noSuchOverload(lVal, Operator.LogicalAnd.id, rVal); - } - - /** - * Cost implements the Coster interface method. The minimum possible cost incurs when the - * left-hand side expr is sufficient in determining the evaluation result. - */ - @Override - public Cost cost() { - return calShortCircuitBinaryOpsCost(lhs, rhs); - } - - @Override - public String toString() { - return "EvalAnd{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; - } - } - - final class EvalOptionalOr extends AbstractEvalLhsRhs implements InterpretableCall { - private final String function; - private final String overload; - private final boolean returnValue; - - EvalOptionalOr( - long id, - String function, - String overload, - Interpretable lhs, - Interpretable rhs, - boolean returnValue) { - super(id, lhs, rhs); - this.function = Objects.requireNonNull(function); - this.overload = Objects.requireNonNull(overload); - this.returnValue = returnValue; - } - - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val left = lhs.eval(ctx); - if (isUnknownOrError(left)) { - return left; - } - if (left instanceof OptionalT optional && optional.hasValue()) { - return returnValue ? optional.getValue() : optional; - } - - Val right = rhs.eval(ctx); - if (isUnknownOrError(right)) { - return right; - } - if (left.type().hasTrait(Trait.ReceiverType)) { - return ((Receiver) left).receive(function, overload, right); - } - return noSuchOverload(left, function, overload, new Val[] {right}); - } - - @Override - public Cost cost() { - return calShortCircuitBinaryOpsCost(lhs, rhs); - } - - @Override - public String function() { - return function; - } - - @Override - public String overloadID() { - return overload; - } - - @Override - public Interpretable[] args() { - return new Interpretable[] {lhs, rhs}; - } - - @Override - public String toString() { - return "EvalOptionalOr{" - + "id=" - + id - + ", lhs=" - + lhs - + ", rhs=" - + rhs - + ", function='" - + function - + '\'' - + ", overload='" - + overload - + '\'' - + ", returnValue=" - + returnValue - + '}'; - } - } - - static Cost calShortCircuitBinaryOpsCost(Interpretable lhs, Interpretable rhs) { - Cost l = estimateCost(lhs); - Cost r = estimateCost(rhs); - return costOf(l.min, l.max + r.max + 1); - } - - final class EvalEq extends AbstractEvalLhsRhs implements InterpretableCall { - EvalEq(long id, Interpretable lhs, Interpretable rhs) { - super(id, lhs, rhs); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val lVal = lhs.eval(ctx); - Val rVal = rhs.eval(ctx); - // Early return if any argument to the function is unknown or error. - if (isUnknownOrError(lVal)) { - return lVal; - } - if (isUnknownOrError(rVal)) { - return rVal; - } - return lVal.equal(rVal); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return calExhaustiveBinaryOpsCost(lhs, rhs); - } - - /** Function implements the InterpretableCall interface method. */ - @Override - public String function() { - return Operator.Equals.id; - } - - /** OverloadID implements the InterpretableCall interface method. */ - @Override - public String overloadID() { - return Overloads.Equals; - } - - /** Args implements the InterpretableCall interface method. */ - @Override - public Interpretable[] args() { - return new Interpretable[] {lhs, rhs}; - } - - @Override - public String toString() { - return "EvalEq{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; - } - } - - final class EvalNe extends AbstractEvalLhsRhs implements InterpretableCall { - EvalNe(long id, Interpretable lhs, Interpretable rhs) { - super(id, lhs, rhs); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val lVal = lhs.eval(ctx); - Val rVal = rhs.eval(ctx); - // Early return if any argument to the function is unknown or error. - if (isUnknownOrError(lVal)) { - return lVal; - } - if (isUnknownOrError(rVal)) { - return rVal; - } - Val eqVal = lVal.equal(rVal); - return switch (eqVal.type().typeEnum()) { - case Err -> eqVal; - case Bool -> ((Negater) eqVal).negate(); - default -> noSuchOverload(lVal, Operator.NotEquals.id, rVal); - }; - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return calExhaustiveBinaryOpsCost(lhs, rhs); - } - - /** Function implements the InterpretableCall interface method. */ - @Override - public String function() { - return Operator.NotEquals.id; - } - - /** OverloadID implements the InterpretableCall interface method. */ - @Override - public String overloadID() { - return Overloads.NotEquals; - } - - /** Args implements the InterpretableCall interface method. */ - @Override - public Interpretable[] args() { - return new Interpretable[] {lhs, rhs}; - } - - @Override - public String toString() { - return "EvalNe{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; - } - } - - final class EvalZeroArity extends AbstractEval implements InterpretableCall, Coster { - private final String function; - private final String overload; - private final FunctionOp impl; - - EvalZeroArity(long id, String function, String overload, FunctionOp impl) { - super(id); - this.function = Objects.requireNonNull(function); - this.overload = Objects.requireNonNull(overload); - this.impl = impl; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation activation) { - return impl.invoke(); - } - - /** Cost returns 1 representing the heuristic cost of the function. */ - @Override - public Cost cost() { - return Cost.OneOne; - } - - /** 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 new Interpretable[0]; - } - - @Override - public String toString() { - return "EvalZeroArity{" - + "id=" - + id - + ", function='" - + function - + '\'' - + ", overload='" - + overload - + '\'' - + ", impl=" - + impl - + '}'; - } - } - - final class EvalUnary extends AbstractEval implements InterpretableCall, Coster { - private final String function; - private final String overload; - private final Interpretable arg; - private final Trait trait; - private final UnaryOp impl; - - EvalUnary( - long id, String function, String overload, Interpretable arg, Trait trait, UnaryOp impl) { - super(id); - this.function = Objects.requireNonNull(function); - this.overload = Objects.requireNonNull(overload); - this.arg = Objects.requireNonNull(arg); - this.trait = trait; - this.impl = impl; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val argVal = arg.eval(ctx); - // Early return if the argument to the function is unknown or error. - if (isUnknownOrError(argVal)) { - return argVal; - } - // If the implementation is bound and the argument value has the right traits required to - // invoke it, then call the implementation. - if (impl != null && (trait == null || argVal.type().hasTrait(trait))) { - return impl.invoke(argVal); - } - // Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the - // operand (arg0). - if (argVal.type().hasTrait(Trait.ReceiverType)) { - return ((Receiver) argVal).receive(function, overload); - } - return noSuchOverload(argVal, function, overload, new Val[] {}); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - Cost c = estimateCost(arg); - return Cost.OneOne.add(c); // 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 new Interpretable[] {arg}; - } - - @Override - public String toString() { - return "EvalUnary{" - + "id=" - + id - + ", function='" - + function - + '\'' - + ", overload='" - + overload - + '\'' - + ", arg=" - + arg - + ", trait=" - + trait - + ", impl=" - + impl - + '}'; - } - } - - final class EvalBinary extends AbstractEvalLhsRhs implements InterpretableCall { - private final String function; - private final String overload; - private final Trait trait; - private final BinaryOp impl; - - EvalBinary( - long id, - String function, - String overload, - Interpretable lhs, - Interpretable rhs, - Trait trait, - BinaryOp impl) { - super(id, lhs, rhs); - this.function = Objects.requireNonNull(function); - this.overload = Objects.requireNonNull(overload); - this.trait = trait; - this.impl = impl; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val lVal = lhs.eval(ctx); - Val rVal = rhs.eval(ctx); - // Early return if any argument to the function is unknown or error. - if (isUnknownOrError(lVal)) { - return lVal; - } - if (isUnknownOrError(rVal)) { - return rVal; - } - // If the implementation is bound and the argument value has the right traits required to - // invoke it, then call the implementation. - if (impl != null && (trait == null || lVal.type().hasTrait(trait))) { - return impl.invoke(lVal, rVal); - } - // Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the - // operand (arg0). - if (lVal.type().hasTrait(Trait.ReceiverType)) { - return ((Receiver) lVal).receive(function, overload, rVal); - } - return noSuchOverload(lVal, function, overload, new Val[] {rVal}); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return calExhaustiveBinaryOpsCost(lhs, rhs); - } - - /** 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 new Interpretable[] {lhs, rhs}; - } - - @Override - public String toString() { - return "EvalBinary{" - + "id=" - + id - + ", lhs=" - + lhs - + ", rhs=" - + rhs - + ", function='" - + function - + '\'' - + ", overload='" - + overload - + '\'' - + ", trait=" - + trait - + ", impl=" - + impl - + '}'; - } - } - - 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; - private final Interpretable[] args; - private final Trait trait; - private final FunctionOp impl; - - public EvalVarArgs( - long id, - String function, - String overload, - Interpretable[] args, - Trait trait, - FunctionOp impl) { - super(id); - this.function = Objects.requireNonNull(function); - this.overload = Objects.requireNonNull(overload); - this.args = Objects.requireNonNull(args); - this.trait = trait; - this.impl = impl; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val[] argVals = new Val[args.length]; - // Early return if any argument to the function is unknown or error. - for (int i = 0; i < args.length; i++) { - Interpretable arg = args[i]; - argVals[i] = arg.eval(ctx); - if (isUnknownOrError(argVals[i])) { - return argVals[i]; - } - } - // If the implementation is bound and the argument value has the right traits required to - // invoke it, then call the implementation. - Val arg0 = argVals[0]; - if (impl != null && (trait == null || arg0.type().hasTrait(trait))) { - return impl.invoke(argVals); - } - // Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the - // operand (arg0). - if (arg0.type().hasTrait(Trait.ReceiverType)) { - return receiveVarArgs((Receiver) arg0, function, overload, argVals); - } - return noSuchOverload(arg0, function, overload, argVals); - } - - /** 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 "EvalVarArgs{" - + "id=" - + id - + ", function='" - + function - + '\'' - + ", overload='" - + overload - + '\'' - + ", args=" - + Arrays.toString(args) - + ", trait=" - + trait - + ", impl=" - + impl - + '}'; - } - } - - final class EvalList extends AbstractEval implements Coster { - final Interpretable[] elems; - final boolean[] optionalIndices; - private final TypeAdapter adapter; - - EvalList(long id, Interpretable[] elems, TypeAdapter adapter) { - this(id, elems, new boolean[elems.length], adapter); - } - - EvalList(long id, Interpretable[] elems, boolean[] optionalIndices, TypeAdapter adapter) { - super(id); - this.elems = elems; - this.optionalIndices = optionalIndices; - this.adapter = adapter; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - List elemVals = new ArrayList<>(elems.length); - // If any argument is unknown or error early terminate. - for (int i = 0; i < elems.length; i++) { - Interpretable elem = elems[i]; - Val elemVal = elem.eval(ctx); - if (isUnknownOrError(elemVal)) { - return elemVal; - } - if (optionalIndices[i]) { - if (!(elemVal instanceof OptionalT optional)) { - return newErr("optional list element is not optional"); - } - if (!optional.hasValue()) { - continue; - } - elemVal = optional.getValue(); - } - elemVals.add(elemVal); - } - return adapter.nativeToValue(elemVals.toArray(Val[]::new)); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return sumOfCost(elems); - } - - @Override - public String toString() { - return "EvalList{" + "id=" + id + ", elems=" + Arrays.toString(elems) + '}'; - } - } - - final class EvalMap extends AbstractEval implements Coster { - final Interpretable[] keys; - final Interpretable[] vals; - final boolean[] optionalEntries; - private final TypeAdapter adapter; - - EvalMap(long id, Interpretable[] keys, Interpretable[] vals, TypeAdapter adapter) { - this(id, keys, vals, new boolean[keys.length], adapter); - } - - EvalMap( - long id, - Interpretable[] keys, - Interpretable[] vals, - boolean[] optionalEntries, - TypeAdapter adapter) { - super(id); - this.keys = keys; - this.vals = vals; - this.optionalEntries = optionalEntries; - this.adapter = adapter; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Map entries = new HashMap<>(keys.length * 4 / 3 + 1); - // If any argument is unknown or error early terminate. - for (int i = 0; i < keys.length; i++) { - Interpretable key = keys[i]; - Val keyVal = key.eval(ctx); - if (isUnknownOrError(keyVal)) { - return keyVal; - } - if (!MapT.isSupportedLiteralKeyType(keyVal)) { - return newErr("unsupported key type"); - } - Val valVal = vals[i].eval(ctx); - if (isUnknownOrError(valVal)) { - return valVal; - } - if (optionalEntries[i]) { - if (!(valVal instanceof OptionalT optional)) { - return newErr("optional map entry is not optional"); - } - if (!optional.hasValue()) { - continue; - } - valVal = optional.getValue(); - } - if (entries.putIfAbsent(keyVal, valVal) != null) { - // Prevent duplicate keys, error out. - return newErr("Failed with repeated key"); - } - } - return MapT.newWrappedMap(adapter, entries); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - Cost k = sumOfCost(keys); - Cost v = sumOfCost(vals); - return k.add(v); - } - - @Override - public String toString() { - return "EvalMap{" - + "id=" - + id - + ", keys=" - + Arrays.toString(keys) - + ", vals=" - + Arrays.toString(vals) - + '}'; - } - } - - final class EvalObj extends AbstractEval implements Coster { - private final String typeName; - private final String[] fields; - private final Interpretable[] vals; - private final boolean[] optionalEntries; - private final TypeProvider provider; - - EvalObj( - long id, String typeName, String[] fields, Interpretable[] vals, TypeProvider provider) { - this(id, typeName, fields, vals, new boolean[fields.length], provider); - } - - EvalObj( - long id, - String typeName, - String[] fields, - Interpretable[] vals, - boolean[] optionalEntries, - TypeProvider provider) { - super(id); - this.typeName = Objects.requireNonNull(typeName); - this.fields = Objects.requireNonNull(fields); - this.vals = Objects.requireNonNull(vals); - this.optionalEntries = optionalEntries; - this.provider = Objects.requireNonNull(provider); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Map fieldVals = new HashMap<>(); - // If any argument is unknown or error early terminate. - for (int i = 0; i < fields.length; i++) { - String field = fields[i]; - Val val = vals[i].eval(ctx); - if (isUnknownOrError(val)) { - return val; - } - if (optionalEntries[i]) { - if (!(val instanceof OptionalT optional)) { - return newErr("optional message field is not optional"); - } - if (!optional.hasValue()) { - continue; - } - val = optional.getValue(); - } - fieldVals.put(field, val); - } - return provider.newValue(typeName, fieldVals); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return sumOfCost(vals); - } - - @Override - public String toString() { - return "EvalObj{" - + "id=" - + id - + ", typeName='" - + typeName - + '\'' - + ", fields=" - + Arrays.toString(fields) - + ", vals=" - + Arrays.toString(vals) - + ", provider=" - + provider - + '}'; - } - } - - static Cost sumOfCost(Interpretable[] interps) { - long min = 0L; - long max = 0L; - for (Interpretable in : interps) { - Cost t = estimateCost(in); - min += t.min; - max += t.max; - } - return costOf(min, max); - } - - final class EvalFold extends AbstractEval implements Coster { - // TODO combine with EvalExhaustiveFold - final String accuVar; - final String iterVar; - final String iterVar2; - final Interpretable iterRange; - final Interpretable accu; - final Interpretable cond; - final Interpretable step; - final Interpretable result; - - EvalFold( - long id, - String accuVar, - Interpretable accu, - String iterVar, - String iterVar2, - Interpretable iterRange, - Interpretable cond, - Interpretable step, - Interpretable result) { - super(id); - this.accuVar = accuVar; - this.iterVar = iterVar; - this.iterVar2 = iterVar2; - this.iterRange = iterRange; - this.accu = accu; - this.cond = cond; - this.step = step; - this.result = result; - } - - /** Eval implements the Interpretable interface method. */ - @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()); - } - // Configure the fold activation with the accumulator initial value. - VarActivation accuCtx = new VarActivation(); - accuCtx.parent = ctx; - accuCtx.name = accuVar; - accuCtx.val = accu.eval(ctx); - VarActivation iterCtx = new VarActivation(); - iterCtx.parent = accuCtx; - iterCtx.name = iterVar; - VarActivation iterCtx2 = null; - if (!iterVar2.isEmpty()) { - iterCtx2 = new VarActivation(); - iterCtx2.parent = iterCtx; - iterCtx2.name = iterVar2; - } - IteratorT it = ((IterableT) foldRange).iterator(); - long index = 0L; - var isLister = foldRange instanceof Lister; - var mapper = (foldRange instanceof Mapper m) ? m : null; - while (it.hasNext() == True) { - // Modify the iter var in the fold activation. - Val next = it.next(); - Activation loopCtx = iterCtx; - if (iterCtx2 != null) { - if (isLister) { - iterCtx.val = intOf(index); - iterCtx2.val = next; - } else if (mapper != null) { - iterCtx.val = next; - iterCtx2.val = mapper.get(next); - } else { - return valOrErr( - foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); - } - loopCtx = iterCtx2; - } else { - iterCtx.val = next; - } - index++; - - // Evaluate the condition, terminate the loop if false. - Val c = cond.eval(loopCtx); - if (c == False) { - break; - } - - // Evalute the evaluation step into accu var. - accuCtx.val = step.eval(loopCtx); - } - // Compute the result. - return result.eval(accuCtx); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - // Compute the cost for evaluating iterRange. - Cost i = estimateCost(iterRange); - - // Compute the size of iterRange. If the size depends on the input, return the maximum - // possible - // cost range. - Val foldRange = iterRange.eval(emptyActivation()); - if (!foldRange.type().hasTrait(Trait.IterableType)) { - return Cost.Unknown; - } - long rangeCnt; - if (foldRange instanceof Sizer sizer) { - rangeCnt = sizer.nativeSize(); - } else { - rangeCnt = 0L; - IteratorT it = ((IterableT) foldRange).iterator(); - while (it.hasNext() == True) { - it.next(); - rangeCnt++; - } - } - Cost a = estimateCost(accu); - Cost c = estimateCost(cond); - Cost s = estimateCost(step); - Cost r = estimateCost(result); - - // The cond and step costs are multiplied by size(iterRange). The minimum possible cost incurs - // when the evaluation result can be determined by the first iteration. - return i.add(a) - .add(r) - .add(costOf(c.min, c.max * rangeCnt)) - .add(costOf(s.min, s.max * rangeCnt)); - } - - @Override - public String toString() { - return "EvalFold{" - + "id=" - + id - + ", accuVar='" - + accuVar - + '\'' - + ", iterVar='" - + iterVar - + '\'' - + ", iterVar2='" - + iterVar2 - + '\'' - + ", iterRange=" - + iterRange - + ", accu=" - + accu - + ", cond=" - + cond - + ", step=" - + step - + ", result=" - + result - + '}'; - } - } - - final class EvalListFold extends AbstractEval implements Coster { - final String iterVar; - final String iterVar2; - final Interpretable iterRange; - final Interpretable filter; - final Interpretable transform; - private final TypeAdapter adapter; - - EvalListFold( - long id, - String iterVar, - String iterVar2, - Interpretable iterRange, - Interpretable filter, - Interpretable transform, - TypeAdapter adapter) { - super(id); - this.iterVar = iterVar; - this.iterVar2 = iterVar2; - 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; - VarActivation iterCtx2 = null; - if (!iterVar2.isEmpty()) { - iterCtx2 = new VarActivation(); - iterCtx2.parent = iterCtx; - iterCtx2.name = iterVar2; - } - List values = new ArrayList<>(listCapacity(foldRange)); - IteratorT it = ((IterableT) foldRange).iterator(); - long index = 0L; - var isLister = foldRange instanceof Lister; - var mapper = (foldRange instanceof Mapper m) ? m : null; - while (it.hasNext() == True) { - Val next = it.next(); - Activation loopCtx = iterCtx; - if (iterCtx2 != null) { - if (isLister) { - iterCtx.val = intOf(index); - iterCtx2.val = next; - } else if (mapper != null) { - iterCtx.val = next; - iterCtx2.val = mapper.get(next); - } else { - return valOrErr( - foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); - } - loopCtx = iterCtx2; - } else { - iterCtx.val = next; - } - index++; - - if (filter != null) { - Val include = filter.eval(loopCtx); - if (include == False) { - continue; - } - if (include != True) { - return noSuchOverload(null, Operator.Conditional.id, include); - } - } - - Val value = transform.eval(loopCtx); - 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).nativeSize(); - 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; - if (foldRange instanceof Sizer sizer) { - rangeCnt = sizer.nativeSize(); - } else { - 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 - + '\'' - + ", iterVar2='" - + iterVar2 - + '\'' - + ", iterRange=" - + iterRange - + ", filter=" - + filter - + ", transform=" - + transform - + '}'; - } - } - - final class EvalMapFold extends AbstractEval implements Coster { - final String iterVar; - final String iterVar2; - final Interpretable iterRange; - final Interpretable filter; - final Interpretable transform; - private final TypeAdapter adapter; - - EvalMapFold( - long id, - String iterVar, - String iterVar2, - Interpretable iterRange, - Interpretable filter, - Interpretable transform, - TypeAdapter adapter) { - super(id); - this.iterVar = iterVar; - this.iterVar2 = iterVar2; - 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; - VarActivation iterCtx2 = null; - if (!iterVar2.isEmpty()) { - iterCtx2 = new VarActivation(); - iterCtx2.parent = iterCtx; - iterCtx2.name = iterVar2; - } - Map values = new HashMap<>(mapCapacity(foldRange)); - IteratorT it = ((IterableT) foldRange).iterator(); - long index = 0L; - var isLister = foldRange instanceof Lister; - var mapper = (foldRange instanceof Mapper m) ? m : null; - while (it.hasNext() == True) { - Val next = it.next(); - Val key; - Activation loopCtx = iterCtx; - if (iterCtx2 != null) { - if (isLister) { - key = intOf(index); - iterCtx.val = key; - iterCtx2.val = next; - } else if (mapper != null) { - key = next; - iterCtx.val = key; - iterCtx2.val = mapper.get(next); - } else { - return valOrErr( - foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); - } - loopCtx = iterCtx2; - } else { - key = next; - iterCtx.val = next; - } - index++; - - if (filter != null) { - Val include = filter.eval(loopCtx); - if (include == False) { - continue; - } - if (include != True) { - return noSuchOverload(null, Operator.Conditional.id, include); - } - } - - Val value = transform.eval(loopCtx); - if (isUnknownOrError(value)) { - return value; - } - values.put(key, value); - } - return MapT.newWrappedMap(adapter, values); - } - - private int mapCapacity(Val foldRange) { - if (foldRange.type().hasTrait(Trait.SizerType)) { - long size = ((Sizer) foldRange).nativeSize(); - if (size > 0 && size <= Integer.MAX_VALUE) { - long capacity = size * 4 / 3 + 1; - return capacity <= Integer.MAX_VALUE ? (int) capacity : Integer.MAX_VALUE; - } - } - 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; - if (foldRange instanceof Sizer sizer) { - rangeCnt = sizer.nativeSize(); - } else { - 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 "EvalMapFold{" - + "id=" - + id - + ", iterVar='" - + iterVar - + '\'' - + ", iterVar2='" - + iterVar2 - + '\'' - + ", iterRange=" - + iterRange - + ", filter=" - + filter - + ", transform=" - + transform - + '}'; - } - } - - // Optional Intepretable implementations that specialize, subsume, or extend the core evaluation - // plan via decorators. - - /** - * evalSetMembership is an Interpretable implementation which tests whether an input value exists - * within the set of map keys used to model a set. - */ - final class EvalSetMembership extends AbstractEval implements Coster { - private final Interpretable inst; - private final Interpretable arg; - private final String argTypeName; - private final Set valueSet; - - EvalSetMembership( - Interpretable inst, Interpretable arg, String argTypeName, Set valueSet) { - super(inst.id()); - this.inst = inst; - this.arg = arg; - this.argTypeName = argTypeName; - this.valueSet = valueSet; - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val val = arg.eval(ctx); - if (isUnknownOrError(val)) { - return val; - } - if (valueSet.isEmpty()) { - return False; - } - if (!val.type().typeName().equals(argTypeName)) { - return noSuchOverload(null, Operator.In.id, val); - } - return valueSet.contains(val) ? True : False; - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return estimateCost(arg); - } - - @Override - public String toString() { - return "EvalSetMembership{" - + "id=" - + id - + ", inst=" - + inst - + ", arg=" - + arg - + ", argTypeName='" - + argTypeName - + '\'' - + ", valueSet=" - + valueSet - + '}'; - } - } - - 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; - } - - return switch (args.length) { - case 3 -> evalReceiverTail2(ctx, arg0); - case 4 -> evalReceiverTail3(ctx, arg0); - default -> 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) { - 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; - } + static InterpretableConst newConstValue(long id, Val val) { + return new EvalConst(id, val); + } - /** Args returns the argument to the unary function. */ - @Override - public Interpretable[] args() { - return args; - } + static Cost calShortCircuitBinaryOpsCost(Interpretable lhs, Interpretable rhs) { + Cost l = estimateCost(lhs); + Cost r = estimateCost(rhs); + return costOf(l.min, l.max + r.max + 1); + } - @Override - public String toString() { - return "EvalReceiverVarArgs{" - + "id=" - + id - + ", function='" - + function - + '\'' - + ", overload='" - + overload - + '\'' - + ", args=" - + Arrays.toString(args) - + '}'; + static Cost sumOfCost(Interpretable[] interps) { + long min = 0L; + long max = 0L; + for (Interpretable in : interps) { + Cost t = estimateCost(in); + min += t.min; + max += t.max; } + return costOf(min, max); } static Val receiveVarArgs(Receiver receiver, String function, String overload, Val[] argVals) { @@ -2138,785 +134,9 @@ static Val receiveVarArgs(Receiver receiver, String function, String overload, V }; } - /** - * 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. - */ - final class EvalWatch implements Interpretable, Coster { - private final Interpretable i; - private final EvalObserver observer; - - public EvalWatch(Interpretable i, EvalObserver observer) { - this.i = Objects.requireNonNull(i); - this.observer = Objects.requireNonNull(observer); - } - - @Override - public long id() { - return i.id(); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val val = i.eval(ctx); - observer.observe(id(), val); - return val; - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return estimateCost(i); - } - - @Override - public String toString() { - return "EvalWatch{" + i + '}'; - } - } - - /** - * evalWatchAttr describes a watcher of an instAttr Interpretable. - * - *

Since the watcher may be selected against at a later stage in program planning, the watcher - * must implement the instAttr interface by proxy. - */ - final class EvalWatchAttr implements Coster, InterpretableAttribute, Attribute { - private final InterpretableAttribute attr; - private final EvalObserver observer; - - public EvalWatchAttr(InterpretableAttribute attr, EvalObserver observer) { - this.attr = Objects.requireNonNull(attr); - this.observer = Objects.requireNonNull(observer); - } - - @Override - public long id() { - return attr.id(); - } - - /** - * AddQualifier creates a wrapper over the incoming qualifier which observes the qualification - * result. - */ - @Override - public Attribute addQualifier(AttributeFactory.Qualifier q) { - if (q instanceof ConstantQualifierEquator cq) { - q = new EvalWatchConstQualEquat(cq, observer, attr.adapter()); - } else if (q instanceof ConstantQualifier cq) { - q = new EvalWatchConstQual(cq, observer, attr.adapter()); - } else { - q = new EvalWatchQual(q, observer, attr.adapter()); - } - attr.addQualifier(q); - return this; - } - - @Override - public Attribute attr() { - return attr.attr(); - } - - @Override - public TypeAdapter adapter() { - return attr.adapter(); - } - - @Override - public Object qualify(Activation vars, Object obj) { - return attr.qualify(vars, obj); - } - - @Override - public Object resolve(Activation act) { - return attr.resolve(act); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return estimateCost(attr); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val val = attr.eval(ctx); - observer.observe(id(), val); - return val; - } - - @Override - public String toString() { - return "EvalWatchAttr{" + attr + '}'; - } - } - - abstract class AbstractEvalWatch extends AbstractEval - implements Coster, Qualifier { - protected final T delegate; - protected final EvalObserver observer; - protected final TypeAdapter adapter; - - AbstractEvalWatch(T delegate, EvalObserver observer, TypeAdapter adapter) { - super(delegate.id()); - this.delegate = delegate; - this.observer = Objects.requireNonNull(observer); - this.adapter = Objects.requireNonNull(adapter); - } - - /** Qualify observes the qualification of a object via a value computed at runtime. */ - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation vars, Object obj) { - Object out = delegate.qualify(vars, obj); - Val val; - if (out != null) { - val = adapter.nativeToValue(out); - } else { - val = newErr(String.format("qualify failed, vars=%s, obj=%s", vars, obj)); - } - observer.observe(id(), val); - return out; - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return estimateCost(delegate); - } - } - - final class EvalWatchConstQualEquat extends AbstractEvalWatch - implements ConstantQualifierEquator { - EvalWatchConstQualEquat( - ConstantQualifierEquator delegate, EvalObserver observer, TypeAdapter adapter) { - super(delegate, observer, adapter); - } - - @Override - public Val eval(Activation activation) { - throw new UnsupportedOperationException("WTF?"); - } - - @Override - public Val value() { - return delegate.value(); - } - - /** - * QualifierValueEquals tests whether the incoming value is equal to the qualificying constant. - */ - @Override - public boolean qualifierValueEquals(Object value) { - return delegate.qualifierValueEquals(value); - } - - @Override - public String toString() { - return "EvalWatchConstQualEquat{" + delegate + '}'; - } - } - - /** - * evalWatchConstQual observes the qualification of an object using a constant boolean, int, - * string, or uint. - */ - final class EvalWatchConstQual extends AbstractEvalWatch - implements ConstantQualifier, Coster { - EvalWatchConstQual(ConstantQualifier delegate, EvalObserver observer, TypeAdapter adapter) { - super(delegate, observer, adapter); - } - - @Override - public Val eval(Activation activation) { - throw new UnsupportedOperationException("WTF?"); - } - - @Override - public Val value() { - return delegate.value(); - } - - @Override - public String toString() { - return "EvalWatchConstQual{" + delegate + '}'; - } - } - - /** evalWatchQual observes the qualification of an object by a value computed at runtime. */ - final class EvalWatchQual extends AbstractEvalWatch { - public EvalWatchQual(Qualifier delegate, EvalObserver observer, TypeAdapter adapter) { - super(delegate, observer, adapter); - } - - @Override - public Val eval(Activation activation) { - throw new UnsupportedOperationException("WTF?"); - } - - @Override - public String toString() { - return "EvalWatchQual{" + delegate + '}'; - } - } - - /** evalWatchConst describes a watcher of an instConst Interpretable. */ - final class EvalWatchConst implements InterpretableConst, Coster { - private final InterpretableConst c; - private final EvalObserver observer; - - EvalWatchConst(InterpretableConst c, EvalObserver observer) { - this.c = Objects.requireNonNull(c); - this.observer = Objects.requireNonNull(observer); - } - - @Override - public long id() { - return c.id(); - } - - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation activation) { - Val val = value(); - observer.observe(id(), val); - return val; - } - - @Override - public Val value() { - return c.value(); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return estimateCost(c); - } - - @Override - public String toString() { - return "EvalWatchConst{" + c + '}'; - } - } - - /** evalExhaustiveOr is just like evalOr, but does not short-circuit argument evaluation. */ - final class EvalExhaustiveOr extends AbstractEvalLhsRhs { - // TODO combine with EvalOr - EvalExhaustiveOr(long id, Interpretable lhs, Interpretable rhs) { - super(id, lhs, rhs); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val lVal = lhs.eval(ctx); - Val rVal = rhs.eval(ctx); - if (lVal == True || rVal == True) { - return True; - } - if (lVal == False && rVal == False) { - return False; - } - if (isUnknown(lVal)) { - return lVal; - } - if (isUnknown(rVal)) { - return rVal; - } - // TODO: Combine the errors into a set in the future. - // If the left-hand side is non-boolean return it as the error. - if (isError(lVal)) { - return lVal; - } - return noSuchOverload(lVal, Operator.LogicalOr.id, rVal); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return calExhaustiveBinaryOpsCost(lhs, rhs); - } - - @Override - public String toString() { - return "EvalExhaustiveOr{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; - } - } - - /** evalExhaustiveAnd is just like evalAnd, but does not short-circuit argument evaluation. */ - final class EvalExhaustiveAnd extends AbstractEvalLhsRhs { - // TODO combine with EvalAnd - EvalExhaustiveAnd(long id, Interpretable lhs, Interpretable rhs) { - super(id, lhs, rhs); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val lVal = lhs.eval(ctx); - Val rVal = rhs.eval(ctx); - if (lVal == False || rVal == False) { - return False; - } - if (lVal == True && rVal == True) { - return True; - } - if (isUnknown(lVal)) { - return lVal; - } - if (isUnknown(rVal)) { - return rVal; - } - if (isError(lVal)) { - return lVal; - } - return noSuchOverload(lVal, Operator.LogicalAnd.id, rVal); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return calExhaustiveBinaryOpsCost(lhs, rhs); - } - - @Override - public String toString() { - return "EvalExhaustiveAnd{" + "id=" + id + ", lhs=" + lhs + ", rhs=" + rhs + '}'; - } - } - static Cost calExhaustiveBinaryOpsCost(Interpretable lhs, Interpretable rhs) { Cost l = estimateCost(lhs); Cost r = estimateCost(rhs); return Cost.OneOne.add(l).add(r); } - - /** - * evalExhaustiveConditional is like evalConditional, but does not short-circuit argument - * evaluation. - */ - final class EvalExhaustiveConditional extends AbstractEval implements Coster { - // TODO combine with EvalConditional - private final TypeAdapter adapter; - private final ConditionalAttribute attr; - - EvalExhaustiveConditional(long id, TypeAdapter adapter, ConditionalAttribute attr) { - super(id); - this.adapter = Objects.requireNonNull(adapter); - this.attr = Objects.requireNonNull(attr); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - Val cVal = attr.expr.eval(ctx); - Object tVal = attr.truthy.resolve(ctx); - Object fVal = attr.falsy.resolve(ctx); - if (cVal == True) { - return adapter.nativeToValue(tVal); - } else if (cVal == False) { - return adapter.nativeToValue(fVal); - } else { - return noSuchOverload(null, Operator.Conditional.id, cVal); - } - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return attr.cost(); - } - - @Override - public String toString() { - return "EvalExhaustiveConditional{" + "id=" + id + ", attr=" + attr + '}'; - } - } - - /** evalExhaustiveFold is like evalFold, but does not short-circuit argument evaluation. */ - final class EvalExhaustiveFold extends AbstractEval implements Coster { - // TODO combine with EvalFold - private final String accuVar; - private final String iterVar; - private final String iterVar2; - private final Interpretable iterRange; - private final Interpretable accu; - private final Interpretable cond; - private final Interpretable step; - private final Interpretable result; - - EvalExhaustiveFold( - long id, - Interpretable accu, - String accuVar, - Interpretable iterRange, - String iterVar, - String iterVar2, - Interpretable cond, - Interpretable step, - Interpretable result) { - super(id); - this.accuVar = accuVar; - this.iterVar = iterVar; - this.iterVar2 = iterVar2; - this.iterRange = iterRange; - this.accu = accu; - this.cond = cond; - this.step = step; - this.result = result; - } - - /** Eval implements the Interpretable interface method. */ - @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()); - } - // Configure the fold activation with the accumulator initial value. - VarActivation accuCtx = new VarActivation(); - accuCtx.parent = ctx; - accuCtx.name = accuVar; - accuCtx.val = accu.eval(ctx); - VarActivation iterCtx = new VarActivation(); - iterCtx.parent = accuCtx; - iterCtx.name = iterVar; - VarActivation iterCtx2 = null; - if (!iterVar2.isEmpty()) { - iterCtx2 = new VarActivation(); - iterCtx2.parent = iterCtx; - iterCtx2.name = iterVar2; - } - IteratorT it = ((IterableT) foldRange).iterator(); - var isLister = foldRange instanceof Lister; - var mapper = (foldRange instanceof Mapper m) ? m : null; - long index = 0L; - while (it.hasNext() == True) { - // Modify the iter var in the fold activation. - Val next = it.next(); - Activation loopCtx = iterCtx; - if (iterCtx2 != null) { - if (isLister) { - iterCtx.val = intOf(index); - iterCtx2.val = next; - } else if (mapper != null) { - iterCtx.val = next; - iterCtx2.val = mapper.get(next); - } else { - return valOrErr( - foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); - } - loopCtx = iterCtx2; - } else { - iterCtx.val = next; - } - index++; - - // Evaluate the condition, but don't terminate the loop as this is exhaustive eval! - cond.eval(loopCtx); - - // Evalute the evaluation step into accu var. - accuCtx.val = step.eval(loopCtx); - } - // Compute the result. - return result.eval(accuCtx); - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - // Compute the cost for evaluating iterRange. - Cost i = estimateCost(iterRange); - - // Compute the size of iterRange. If the size depends on the input, return the maximum - // possible - // cost range. - Val foldRange = iterRange.eval(emptyActivation()); - if (!foldRange.type().hasTrait(Trait.IterableType)) { - return Cost.Unknown; - } - long rangeCnt; - if (foldRange instanceof Sizer sizer) { - rangeCnt = sizer.nativeSize(); - } else { - rangeCnt = 0L; - IteratorT it = ((IterableT) foldRange).iterator(); - while (it.hasNext() == True) { - it.next(); - rangeCnt++; - } - } - - Cost a = estimateCost(accu); - Cost c = estimateCost(cond); - Cost s = estimateCost(step); - Cost r = estimateCost(result); - - // The cond and step costs are multiplied by size(iterRange). - return i.add(a).add(c.multiply(rangeCnt)).add(s.multiply(rangeCnt)).add(r); - } - - @Override - public String toString() { - return "EvalExhaustiveFold{" - + "id=" - + id - + ", accuVar='" - + accuVar - + '\'' - + ", iterVar='" - + iterVar - + '\'' - + ", iterVar2='" - + iterVar2 - + '\'' - + ", iterRange=" - + iterRange - + ", accu=" - + accu - + ", cond=" - + cond - + ", step=" - + step - + ", result=" - + result - + '}'; - } - } - - /** 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; - VarActivation iterCtx2 = null; - if (!fold.iterVar2.isEmpty()) { - iterCtx2 = new VarActivation(); - iterCtx2.parent = iterCtx; - iterCtx2.name = fold.iterVar2; - } - List values = new ArrayList<>(fold.listCapacity(foldRange)); - Val result = null; - IteratorT it = ((IterableT) foldRange).iterator(); - long index = 0L; - while (it.hasNext() == True) { - Val next = it.next(); - Activation loopCtx = iterCtx; - if (iterCtx2 != null) { - if (foldRange instanceof Lister) { - iterCtx.val = intOf(index); - iterCtx2.val = next; - } else if (foldRange instanceof Mapper) { - iterCtx.val = next; - iterCtx2.val = ((Mapper) foldRange).get(next); - } else { - return valOrErr( - foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); - } - loopCtx = iterCtx2; - } else { - iterCtx.val = next; - } - index++; - - Val include = fold.filter != null ? fold.filter.eval(loopCtx) : True; - Val value = fold.transform.eval(loopCtx); - 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 + '}'; - } - } - - /** EvalExhaustiveMapFold evaluates every filter and transform without short-circuiting. */ - final class EvalExhaustiveMapFold extends AbstractEval implements Coster { - private final EvalMapFold fold; - - EvalExhaustiveMapFold(EvalMapFold 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; - VarActivation iterCtx2 = null; - if (!fold.iterVar2.isEmpty()) { - iterCtx2 = new VarActivation(); - iterCtx2.parent = iterCtx; - iterCtx2.name = fold.iterVar2; - } - Map values = new HashMap<>(fold.mapCapacity(foldRange)); - Val result = null; - IteratorT it = ((IterableT) foldRange).iterator(); - long index = 0L; - while (it.hasNext() == True) { - Val next = it.next(); - Val key; - Activation loopCtx = iterCtx; - if (iterCtx2 != null) { - if (foldRange instanceof Lister) { - key = intOf(index); - iterCtx.val = key; - iterCtx2.val = next; - } else if (foldRange instanceof Mapper) { - key = next; - iterCtx.val = key; - iterCtx2.val = ((Mapper) foldRange).get(next); - } else { - return valOrErr( - foldRange, "got '%s', expected list or map type", foldRange.getClass().getName()); - } - loopCtx = iterCtx2; - } else { - key = next; - iterCtx.val = next; - } - index++; - - Val include = fold.filter != null ? fold.filter.eval(loopCtx) : True; - Val value = fold.transform.eval(loopCtx); - 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.put(key, value); - } - } - } - return result != null ? result : MapT.newWrappedMap(fold.adapter, values); - } - - @Override - public Cost cost() { - return fold.cost(); - } - - @Override - public String toString() { - return "EvalExhaustiveMapFold{" + fold + '}'; - } - } - - /** evalAttr evaluates an Attribute value. */ - final class EvalAttr extends AbstractEval - implements InterpretableAttribute, Coster, Qualifier, Attribute { - private final TypeAdapter adapter; - private Attribute attr; - - EvalAttr(TypeAdapter adapter, Attribute attr) { - super(attr.id()); - this.adapter = Objects.requireNonNull(adapter); - this.attr = Objects.requireNonNull(attr); - } - - /** AddQualifier implements the instAttr interface method. */ - @Override - public Attribute addQualifier(AttributeFactory.Qualifier qualifier) { - attr = attr.addQualifier(qualifier); - return attr; - } - - /** Attr implements the instAttr interface method. */ - @Override - public Attribute attr() { - return attr; - } - - /** Adapter implements the instAttr interface method. */ - @Override - public TypeAdapter adapter() { - return adapter; - } - - /** Cost implements the Coster interface method. */ - @Override - public Cost cost() { - return estimateCost(attr); - } - - /** Eval implements the Interpretable interface method. */ - @Override - public Val eval(org.projectnessie.cel.interpreter.Activation ctx) { - try { - Object v = attr.resolve(ctx); - return adapter.nativeToValue(v); - } catch (Exception e) { - return newErr(e, e.toString()); - } - } - - /** Qualify proxies to the Attribute's Qualify method. */ - @Override - public Object qualify(org.projectnessie.cel.interpreter.Activation ctx, Object obj) { - return attr.qualify(ctx, obj); - } - - /** Resolve proxies to the Attribute's Resolve method. */ - @Override - public Object resolve(org.projectnessie.cel.interpreter.Activation ctx) { - return attr.resolve(ctx); - } - - @Override - public String toString() { - return "EvalAttr{" + "id=" + id + ", attr=" + attr + '}'; - } - } } 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 6af0a60b2..34dd021dc 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretableDecorator.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretableDecorator.java @@ -29,24 +29,6 @@ import org.projectnessie.cel.common.types.ref.Type; import org.projectnessie.cel.common.types.ref.Val; import org.projectnessie.cel.common.types.traits.Lister; -import org.projectnessie.cel.interpreter.AttributeFactory.ConditionalAttribute; -import org.projectnessie.cel.interpreter.Interpretable.EvalAnd; -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.EvalExhaustiveMapFold; -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.EvalMapFold; -import org.projectnessie.cel.interpreter.Interpretable.EvalOr; -import org.projectnessie.cel.interpreter.Interpretable.EvalSetMembership; -import org.projectnessie.cel.interpreter.Interpretable.EvalWatch; -import org.projectnessie.cel.interpreter.Interpretable.EvalWatchAttr; -import org.projectnessie.cel.interpreter.Interpretable.EvalWatchConst; import org.projectnessie.cel.interpreter.Interpretable.InterpretableAttribute; import org.projectnessie.cel.interpreter.Interpretable.InterpretableCall; import org.projectnessie.cel.interpreter.Interpretable.InterpretableConst; 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 efe0d4c74..a5ba3e3c4 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/InterpretablePlanner.java @@ -15,72 +15,15 @@ */ package org.projectnessie.cel.interpreter; -import static org.projectnessie.cel.common.types.BytesT.bytesOf; -import static org.projectnessie.cel.common.types.DoubleT.doubleOf; -import static org.projectnessie.cel.common.types.DurationT.durationOf; -import static org.projectnessie.cel.common.types.IntT.intOf; -import static org.projectnessie.cel.common.types.StringT.stringOf; -import static org.projectnessie.cel.common.types.TimestampT.timestampOf; -import static org.projectnessie.cel.common.types.Types.boolOf; -import static org.projectnessie.cel.common.types.UintT.uintOf; -import static org.projectnessie.cel.interpreter.Interpretable.newConstValue; - import com.google.api.expr.v1alpha1.CheckedExpr; -import com.google.api.expr.v1alpha1.Constant; import com.google.api.expr.v1alpha1.Expr; -import com.google.api.expr.v1alpha1.Expr.Call; -import com.google.api.expr.v1alpha1.Expr.Comprehension; -import com.google.api.expr.v1alpha1.Expr.CreateList; -import com.google.api.expr.v1alpha1.Expr.CreateStruct; -import com.google.api.expr.v1alpha1.Expr.CreateStruct.Entry; -import com.google.api.expr.v1alpha1.Expr.Ident; -import com.google.api.expr.v1alpha1.Expr.Select; import com.google.api.expr.v1alpha1.Reference; import com.google.api.expr.v1alpha1.Type; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.projectnessie.cel.common.containers.Container; -import org.projectnessie.cel.common.operators.Operator; -import org.projectnessie.cel.common.types.NullT; -import org.projectnessie.cel.common.types.ref.FieldType; import org.projectnessie.cel.common.types.ref.TypeAdapter; import org.projectnessie.cel.common.types.ref.TypeProvider; -import org.projectnessie.cel.common.types.ref.Val; -import org.projectnessie.cel.common.types.traits.Trait; -import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; -import org.projectnessie.cel.interpreter.AttributeFactory.Qualifier; -import org.projectnessie.cel.interpreter.Interpretable.EvalAnd; -import org.projectnessie.cel.interpreter.Interpretable.EvalAttr; -import org.projectnessie.cel.interpreter.Interpretable.EvalBinary; -import org.projectnessie.cel.interpreter.Interpretable.EvalEq; -import org.projectnessie.cel.interpreter.Interpretable.EvalFold; -import org.projectnessie.cel.interpreter.Interpretable.EvalIdent; -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.EvalMapFold; -import org.projectnessie.cel.interpreter.Interpretable.EvalNe; -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; -import org.projectnessie.cel.interpreter.Interpretable.EvalZeroArity; -import org.projectnessie.cel.interpreter.Interpretable.InterpretableAttribute; -import org.projectnessie.cel.interpreter.Interpretable.InterpretableConst; -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. */ public interface InterpretablePlanner { @@ -145,1002 +88,4 @@ static InterpretablePlanner newUncheckedPlanner( } /** planner is an implementatio of the interpretablePlanner interface. */ - static final class Planner implements InterpretablePlanner { - private final Dispatcher disp; - private final TypeProvider provider; - private final TypeAdapter adapter; - private final AttributeFactory attrFactory; - private final Container container; - private final Map refMap; - private final Map typeMap; - private final Map fieldTypes = new HashMap<>(); - private final InterpretableDecorator[] decorators; - - Planner( - Dispatcher disp, - TypeProvider provider, - TypeAdapter adapter, - AttributeFactory attrFactory, - Container container, - Map refMap, - Map typeMap, - InterpretableDecorator[] decorators) { - this.disp = disp; - this.provider = provider; - this.adapter = adapter; - this.attrFactory = attrFactory; - this.container = container; - this.refMap = refMap; - this.typeMap = typeMap; - this.decorators = decorators; - } - - /** - * Plan implements the interpretablePlanner interface. This implementation of the Plan method - * also applies decorators to each Interpretable generated as part of the overall plan. - * Decorators are useful for layering functionality into the evaluation that is not natively - * understood by CEL, such as state-tracking, expression re-write, and possibly efficient - * thread-safe memoization of repeated expressions. - */ - @Override - public Interpretable plan(Expr expr) { - return switch (expr.getExprKindCase()) { - case CALL_EXPR -> decorate(planCall(expr)); - case IDENT_EXPR -> decorate(planIdent(expr)); - case SELECT_EXPR -> decorate(planSelect(expr)); - case LIST_EXPR -> decorate(planCreateList(expr)); - case STRUCT_EXPR -> decorate(planCreateStruct(expr)); - case COMPREHENSION_EXPR -> decorate(planComprehension(expr)); - case CONST_EXPR -> decorate(planConst(expr)); - default -> - throw new IllegalArgumentException( - String.format("unsupported expr of kind %s: '%s'", expr.getExprKindCase(), expr)); - }; - } - - /** - * decorate applies the InterpretableDecorator functions to the given Interpretable. Both the - * Interpretable and error generated by a Plan step are accepted as arguments for convenience. - */ - Interpretable decorate(Interpretable i) { - for (InterpretableDecorator dec : decorators) { - i = dec.decorate(i); - if (i == null) { - return null; - } - } - return i; - } - - /** planIdent creates an Interpretable that resolves an identifier from an Activation. */ - Interpretable planIdent(Expr expr) { - // Establish whether the identifier is in the reference map. - Reference identRef = refMap.get(expr.getId()); - if (identRef != null) { - return planCheckedIdent(expr.getId(), identRef); - } - // Create the possible attribute list for the unresolved reference. - Ident ident = expr.getIdentExpr(); - return new EvalAttr(adapter, attrFactory.maybeAttribute(expr.getId(), ident.getName())); - } - - Interpretable planCheckedIdent(long id, Reference identRef) { - String identName = identRef.getName(); - String providerName = identName.startsWith(".") ? identName.substring(1) : identName; - // Plan a constant reference if this is the case for this simple identifier. - if (identRef.getValue() != Reference.getDefaultInstance().getValue()) { - return plan(Expr.newBuilder().setId(id).setConstExpr(identRef.getValue()).build()); - } - - // Check to see whether the type map indicates this is a type name. All types should be - // registered with the provider. - Type cType = typeMap.get(id); - if (cType != null && cType.getType() != Type.getDefaultInstance()) { - Val cVal = provider.findIdent(providerName); - if (cVal == null) { - throw new IllegalStateException( - String.format("reference to undefined type: %s", providerName)); - } - return newConstValue(id, cVal); - } - - // Otherwise, evaluate the checked top-level variable directly for ordinary plans. Decorated - // programs keep the attribute shape because custom decorators may inspect attributes. - if (decorators.length == 0) { - return new EvalIdent(id, identName, adapter); - } - return new EvalAttr(adapter, attrFactory.absoluteAttribute(id, identName)); - } - - /** - * planSelect creates an Interpretable with either: - * - *

    - *
  1. selects a field from a map or proto. - *
  2. creates a field presence test for a select within a has() macro. - *
  3. resolves the select expression to a namespaced identifier. - *
- */ - Interpretable planSelect(Expr expr) { - // If the Select id appears in the reference map from the CheckedExpr proto then it is either - // a namespaced identifier or enum value. - Reference identRef = refMap.get(expr.getId()); - if (identRef != null) { - return planCheckedIdent(expr.getId(), identRef); - } - - Select sel = expr.getSelectExpr(); - // Plan the operand evaluation. - Interpretable op = - decorators.length == 0 ? planSelectOperand(sel.getOperand()) : plan(sel.getOperand()); - - // Determine the field type if this is a proto message type. - FieldType fieldType = null; - Type opType = typeMap.get(sel.getOperand().getId()); - if (opType != null && !opType.getMessageType().isEmpty()) { - FieldType ft = findFieldType(opType.getMessageType(), sel.getField()); - if (ft != null && ft.isSet != null && ft.getFrom != null) { - fieldType = ft; - } - } - - // If the Select was marked TestOnly, this is a presence test. - // - // Note: presence tests are defined for structured (e.g. proto) and dynamic values (map, json) - // as follows: - // - True if the object field has a non-default value, e.g. obj.str != "" - // - True if the dynamic value has the field defined, e.g. key in map - // - // However, presence tests are not defined for qualified identifier names with primitive - // types. - // If a string named 'a.b.c' is declared in the environment and referenced within - // `has(a.b.c)`, - // it is not clear whether has should error or follow the convention defined for structured - // values. - if (sel.getTestOnly()) { - // Return the test only eval expression. - return new EvalTestOnly(expr.getId(), op, stringOf(sel.getField()), fieldType); - } - // Build a qualifier. - Qualifier qual = - fieldType != null - ? new AttributeFactory.FieldQualifier( - expr.getId(), sel.getField(), fieldType, adapter) - : attrFactory.newQualifier(opType, expr.getId(), sel.getField()); - if (qual == null) { - return null; - } - // Lastly, create a field selection Interpretable. - if (op instanceof InterpretableAttribute attr) { - attr.addQualifier(qual); - return attr; - } - - InterpretableAttribute relAttr = relativeAttr(op.id(), op); - if (relAttr == null) { - return null; - } - relAttr.addQualifier(qual); - return relAttr; - } - - private Interpretable planSelectOperand(Expr operand) { - if (operand.getExprKindCase() != Expr.ExprKindCase.IDENT_EXPR) { - return plan(operand); - } - - Reference identRef = refMap.get(operand.getId()); - if (identRef == null || identRef.getValue() != Reference.getDefaultInstance().getValue()) { - return plan(operand); - } - - Type cType = typeMap.get(operand.getId()); - if (cType != null && cType.getType() != Type.getDefaultInstance()) { - return plan(operand); - } - - return new EvalAttr( - adapter, attrFactory.absoluteAttribute(operand.getId(), identRef.getName())); - } - - private FieldType findFieldType(String messageType, String fieldName) { - String key = messageType + '\n' + fieldName; - FieldType ft = fieldTypes.get(key); - if (ft != null) { - return ft; - } - ft = provider.findFieldType(messageType, fieldName); - if (ft != null) { - fieldTypes.put(key, ft); - } - return ft; - } - - /** - * planCall creates a callable Interpretable while specializing for common functions and - * invocation patterns. Specifically, conditional operators &&, ||, ?:, and (in)equality - * functions result in optimized Interpretable values. - */ - Interpretable planCall(Expr expr) { - Call call = expr.getCallExpr(); - ResolvedFunction resolvedFunc = resolveFunction(expr); - // target, fnName, oName := p.resolveFunction(expr) - int argCount = call.getArgsCount(); - int offset = 0; - if (resolvedFunc.target != null) { - argCount++; - offset++; - } - - Interpretable[] args = new Interpretable[argCount]; - if (resolvedFunc.target != null) { - Interpretable arg = plan(resolvedFunc.target); - if (arg == null) { - return null; - } - args[0] = arg; - } - for (int i = 0; i < call.getArgsCount(); i++) { - Expr argExpr = call.getArgs(i); - Interpretable arg = plan(argExpr); - args[i + offset] = arg; - } - - // Generate specialized Interpretable operators by function name if possible. - if (resolvedFunc.fnName.equals(Operator.LogicalAnd.id)) return planCallLogicalAnd(expr, args); - if (resolvedFunc.fnName.equals(Operator.LogicalOr.id)) return planCallLogicalOr(expr, args); - if (resolvedFunc.fnName.equals(Operator.Conditional.id)) - return planCallConditional(expr, args); - if (resolvedFunc.fnName.equals(Operator.Equals.id)) return planCallEqual(expr, args); - if (resolvedFunc.fnName.equals(Operator.NotEquals.id)) return planCallNotEqual(expr, args); - if (resolvedFunc.fnName.equals(Operator.Index.id)) return planCallIndex(expr, args); - if ("optional_or".equals(resolvedFunc.overloadId) - || (resolvedFunc.overloadId.isEmpty() - && "or".equals(resolvedFunc.fnName) - && disp.findOverload("optional_or") != null)) - return new EvalOptionalOr( - expr.getId(), resolvedFunc.fnName, resolvedFunc.overloadId, args[0], args[1], false); - if ("optional_or_value".equals(resolvedFunc.overloadId) - || (resolvedFunc.overloadId.isEmpty() - && "orValue".equals(resolvedFunc.fnName) - && disp.findOverload("optional_or_value") != null)) - return new EvalOptionalOr( - expr.getId(), resolvedFunc.fnName, resolvedFunc.overloadId, args[0], args[1], true); - - // Otherwise, generate Interpretable calls specialized by argument count. - // Try to find the specific function by overload id. - Overload fnDef = null; - if (resolvedFunc.overloadId != null && !resolvedFunc.overloadId.isEmpty()) { - fnDef = disp.findOverload(resolvedFunc.overloadId); - } - // If the overload id couldn't resolve the function, try the simple function name. - if (fnDef == null) { - fnDef = disp.findOverload(resolvedFunc.fnName); - } - return switch (argCount) { - 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); - }; - } - - /** planCallZero generates a zero-arity callable Interpretable. */ - 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)); - } - return new EvalZeroArity(expr.getId(), function, overload, impl.function); - } - - /** planCallUnary generates a unary callable Interpretable. */ - static Interpretable planCallUnary( - Expr expr, String function, String overload, Overload impl, Interpretable[] args) { - UnaryOp fn = null; - Trait trait = null; - if (impl != null) { - if (impl.unary == null) { - throw new IllegalStateException(String.format("no such overload: %s(arg)", function)); - } - fn = impl.unary; - trait = impl.operandTrait; - } - return new EvalUnary(expr.getId(), function, overload, args[0], trait, fn); - } - - /** planCallBinary generates a binary callable Interpretable. */ - static Interpretable planCallBinary( - Expr expr, String function, String overload, Overload impl, Interpretable... args) { - BinaryOp fn = null; - Trait trait = null; - if (impl != null) { - if (impl.binary == null) { - throw new IllegalStateException( - String.format("no such overload: %s(lhs, rhs)", function)); - } - fn = impl.binary; - trait = impl.operandTrait; - } - 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) { - if (impl == null) { - return new EvalReceiverVarArgs(expr.getId(), function, overload, args); - } - FunctionOp fn = null; - Trait trait = null; - 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); - } - - /** planCallEqual generates an equals (==) Interpretable. */ - static Interpretable planCallEqual(Expr expr, Interpretable... args) { - return new EvalEq(expr.getId(), args[0], args[1]); - } - - /** planCallNotEqual generates a not equals (!=) Interpretable. */ - static Interpretable planCallNotEqual(Expr expr, Interpretable... args) { - return new EvalNe(expr.getId(), args[0], args[1]); - } - - /** planCallLogicalAnd generates a logical and (&&) Interpretable. */ - static Interpretable planCallLogicalAnd(Expr expr, Interpretable... args) { - return new EvalAnd(expr.getId(), args[0], args[1]); - } - - /** planCallLogicalOr generates a logical or (||) Interpretable. */ - static Interpretable planCallLogicalOr(Expr expr, Interpretable... args) { - return new EvalOr(expr.getId(), args[0], args[1]); - } - - /** planCallConditional generates a conditional / ternary (c ? t : f) Interpretable. */ - Interpretable planCallConditional(Expr expr, Interpretable... args) { - Interpretable cond = args[0]; - - Interpretable t = args[1]; - Attribute tAttr; - if (t instanceof InterpretableAttribute truthyAttr) { - tAttr = truthyAttr.attr(); - } else { - tAttr = attrFactory.relativeAttribute(t.id(), t); - } - - Interpretable f = args[2]; - Attribute fAttr; - if (f instanceof InterpretableAttribute falsyAttr) { - fAttr = falsyAttr.attr(); - } else { - fAttr = attrFactory.relativeAttribute(f.id(), f); - } - - return new EvalAttr( - adapter, attrFactory.conditionalAttribute(expr.getId(), cond, tAttr, fAttr)); - } - - /** - * planCallIndex either extends an attribute with the argument to the index operation, or - * creates a relative attribute based on the return of a function call or operation. - */ - Interpretable planCallIndex(Expr expr, Interpretable... args) { - Interpretable op = args[0]; - Interpretable ind = args[1]; - InterpretableAttribute opAttr = relativeAttr(op.id(), op); - if (opAttr == null) { - return null; - } - Type opType = typeMap.get(expr.getCallExpr().getTarget().getId()); - if (ind instanceof InterpretableConst indConst) { - Qualifier qual = attrFactory.newQualifier(opType, expr.getId(), indConst.value()); - if (qual == null) { - return null; - } - opAttr.addQualifier(qual); - return opAttr; - } - if (ind instanceof InterpretableAttribute indAttr) { - Qualifier qual = attrFactory.newQualifier(opType, expr.getId(), indAttr); - if (qual == null) { - return null; - } - opAttr.addQualifier(qual); - return opAttr; - } - InterpretableAttribute indQual = relativeAttr(expr.getId(), ind); - if (indQual == null) { - return null; - } - opAttr.addQualifier(indQual); - return opAttr; - } - - /** planCreateList generates a list construction Interpretable. */ - Interpretable planCreateList(Expr expr) { - CreateList list = expr.getListExpr(); - Interpretable[] elems = new Interpretable[list.getElementsCount()]; - boolean[] optionalIndices = new boolean[list.getElementsCount()]; - for (int index : list.getOptionalIndicesList()) { - optionalIndices[index] = true; - } - for (int i = 0; i < list.getElementsCount(); i++) { - Expr elem = list.getElements(i); - Interpretable elemVal = plan(elem); - if (elemVal == null) { - return null; - } - elems[i] = elemVal; - } - return new EvalList(expr.getId(), elems, optionalIndices, adapter); - } - - /** planCreateStruct generates a map or object construction Interpretable. */ - Interpretable planCreateStruct(Expr expr) { - CreateStruct str = expr.getStructExpr(); - if (!str.getMessageName().isEmpty()) { - return planCreateObj(expr); - } - List entries = str.getEntriesList(); - Interpretable[] keys = new Interpretable[entries.size()]; - Interpretable[] vals = new Interpretable[entries.size()]; - boolean[] optionalEntries = new boolean[entries.size()]; - for (int i = 0; i < entries.size(); i++) { - Entry entry = entries.get(i); - optionalEntries[i] = entry.getOptionalEntry(); - Interpretable keyVal = plan(entry.getMapKey()); - if (keyVal == null) { - return null; - } - keys[i] = keyVal; - - Interpretable valVal = plan(entry.getValue()); - if (valVal == null) { - return null; - } - vals[i] = valVal; - } - return new EvalMap(expr.getId(), keys, vals, optionalEntries, adapter); - } - - /** planCreateObj generates an object construction Interpretable. */ - Interpretable planCreateObj(Expr expr) { - CreateStruct obj = expr.getStructExpr(); - String typeName = resolveTypeName(obj.getMessageName()); - if (typeName == null) { - throw new IllegalStateException(String.format("unknown type: %s", obj.getMessageName())); - } - List entries = obj.getEntriesList(); - String[] fields = new String[entries.size()]; - Interpretable[] vals = new Interpretable[entries.size()]; - boolean[] optionalEntries = new boolean[entries.size()]; - for (int i = 0; i < entries.size(); i++) { - Entry entry = entries.get(i); - fields[i] = entry.getFieldKey(); - optionalEntries[i] = entry.getOptionalEntry(); - Interpretable val = plan(entry.getValue()); - if (val == null) { - return null; - } - vals[i] = val; - } - return new EvalObj(expr.getId(), typeName, fields, vals, optionalEntries, provider); - } - - /** planComprehension generates an Interpretable fold operation. */ - Interpretable planComprehension(Expr expr) { - Comprehension fold = expr.getComprehensionExpr(); - MacroMapFold macroMapFold = macroMapFold(fold); - if (macroMapFold != null) { - Interpretable iterRange = plan(fold.getIterRange()); - if (iterRange == null) { - return null; - } - Interpretable filter = null; - if (macroMapFold.filter != null) { - filter = plan(macroMapFold.filter); - if (filter == null) { - return null; - } - } - Interpretable transform = plan(macroMapFold.transform); - if (transform == null) { - return null; - } - return new EvalMapFold( - expr.getId(), - fold.getIterVar(), - fold.getIterVar2(), - iterRange, - filter, - transform, - adapter); - } - - 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(), - fold.getIterVar2(), - iterRange, - filter, - transform, - adapter); - } - - Interpretable accu = plan(fold.getAccuInit()); - if (accu == null) { - return null; - } - Interpretable iterRange = plan(fold.getIterRange()); - if (iterRange == null) { - return null; - } - Interpretable cond = plan(fold.getLoopCondition()); - if (cond == null) { - return null; - } - Interpretable step = plan(fold.getLoopStep()); - if (step == null) { - return null; - } - Interpretable result = plan(fold.getResult()); - if (result == null) { - return null; - } - return new EvalFold( - expr.getId(), - fold.getAccuVar(), - accu, - fold.getIterVar(), - fold.getIterVar2(), - iterRange, - cond, - step, - result); - } - - private static MacroMapFold macroMapFold(Comprehension fold) { - if (!isEmptyMap(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 = mapEntryValue(fold.getIterVar(), step); - if (transform == null - || referencesIdent(transform, fold.getAccuVar()) - || (filter != null && referencesIdent(filter, fold.getAccuVar()))) { - return null; - } - return new MacroMapFold(filter, transform); - } - - private static 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 static 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 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 static boolean isIdent(Expr expr, String name) { - return expr.getExprKindCase() == Expr.ExprKindCase.IDENT_EXPR - && expr.getIdentExpr().getName().equals(name); - } - - private static boolean isEmptyList(Expr expr) { - return expr.getExprKindCase() == Expr.ExprKindCase.LIST_EXPR - && expr.getListExpr().getElementsCount() == 0; - } - - private static boolean isEmptyMap(Expr expr) { - return expr.getExprKindCase() == Expr.ExprKindCase.STRUCT_EXPR - && expr.getStructExpr().getMessageName().isEmpty() - && expr.getStructExpr().getEntriesCount() == 0; - } - - private static Expr mapEntryValue(String keyVar, Expr step) { - if (step.getExprKindCase() != Expr.ExprKindCase.STRUCT_EXPR - || !step.getStructExpr().getMessageName().isEmpty() - || step.getStructExpr().getEntriesCount() != 1) { - return null; - } - Entry entry = step.getStructExpr().getEntries(0); - if (!isIdent(entry.getMapKey(), keyVar)) { - return null; - } - return entry.getValue(); - } - - 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 static 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) - && !comprehension.getIterVar2().equals(name) - && !comprehension.getAccuVar().equals(name) - && referencesIdent(comprehension.getLoopCondition(), name)) - || (!comprehension.getIterVar().equals(name) - && !comprehension.getIterVar2().equals(name) - && !comprehension.getAccuVar().equals(name) - && referencesIdent(comprehension.getLoopStep(), name)) - || (!comprehension.getAccuVar().equals(name) - && referencesIdent(comprehension.getResult(), name)); - default: - return false; - } - } - - private static final class MacroListFold { - final Expr filter; - final Expr transform; - - private MacroListFold(Expr filter, Expr transform) { - this.filter = filter; - this.transform = transform; - } - } - - private static final class MacroMapFold { - final Expr filter; - final Expr transform; - - private MacroMapFold(Expr filter, Expr transform) { - this.filter = filter; - this.transform = transform; - } - } - - /** planConst generates a constant valued Interpretable. */ - static Interpretable planConst(Expr expr) { - Val val = constValue(expr.getConstExpr()); - if (val == null) { - return null; - } - return newConstValue(expr.getId(), val); - } - - /** constValue converts a proto Constant value to a ref.Val. */ - @SuppressWarnings("deprecation") - static Val constValue(Constant c) { - return switch (c.getConstantKindCase()) { - case BOOL_VALUE -> boolOf(c.getBoolValue()); - case BYTES_VALUE -> bytesOf(c.getBytesValue()); - case DOUBLE_VALUE -> doubleOf(c.getDoubleValue()); - case DURATION_VALUE -> durationOf(c.getDurationValue()); - case INT64_VALUE -> intOf(c.getInt64Value()); - case NULL_VALUE -> NullT.NullValue; - case STRING_VALUE -> stringOf(c.getStringValue()); - case TIMESTAMP_VALUE -> timestampOf(c.getTimestampValue()); - case UINT64_VALUE -> uintOf(c.getUint64Value()); - default -> - throw new IllegalArgumentException( - String.format( - "unknown constant type: '%s' of kind '%s'", c, c.getConstantKindCase())); - }; - } - - /** - * resolveTypeName takes a qualified string constructed at parse time, applies the proto - * namespace resolution rules to it in a scan over possible matching types in the TypeProvider. - */ - String resolveTypeName(String typeName) { - for (String qualifiedTypeName : container.resolveCandidateNames(typeName)) { - if (provider.findType(qualifiedTypeName) != null) { - return qualifiedTypeName; - } - } - return null; - } - - static final class ResolvedFunction { - final Expr target; - final String fnName; - final String overloadId; - - ResolvedFunction(Expr target, String fnName, String overloadId) { - this.target = target; - this.fnName = fnName; - this.overloadId = overloadId; - } - } - - /** - * resolveFunction determines the call target, function name, and overload name from a given - * Expr value. - * - *

The resolveFunction resolves ambiguities where a function may either be a receiver-style - * invocation or a qualified global function name. - * - *

    - *
  • The target expression may only consist of ident and select expressions. - *
  • The function is declared in the environment using its fully-qualified name. - *
  • The fully-qualified function name matches the string serialized target value. - *
- */ - ResolvedFunction resolveFunction(Expr expr) { - // Note: similar logic exists within the `checker/checker.go`. If making changes here - // please consider the impact on checker.go and consolidate implementations or mirror code - // as appropriate. - Call call = expr.getCallExpr(); - Expr target = call.hasTarget() ? call.getTarget() : null; - String fnName = call.getFunction(); - - // Checked expressions always have a reference map entry, and _should_ have the fully - // qualified - // function name as the fnName value. - Reference oRef = refMap.get(expr.getId()); - if (oRef != null) { - if (oRef.getOverloadIdCount() == 1) { - return new ResolvedFunction(target, fnName, oRef.getOverloadId(0)); - } - // Note, this namespaced function name will not appear as a fully qualified name in ASTs - // built and stored before cel-go v0.5.0; however, this functionality did not work at all - // before the v0.5.0 release. - return new ResolvedFunction(target, fnName, ""); - } - - // Parse-only expressions need to handle the same logic as is normally performed at check - // time, - // but with potentially much less information. The only reliable source of information about - // which functions are configured is the dispatcher. - if (target == null) { - // If the user has a parse-only expression, then it should have been configured as such in - // the interpreter dispatcher as it may have been omitted from the checker environment. - for (String qualifiedName : container.resolveCandidateNames(fnName)) { - if (disp.findOverload(qualifiedName) != null) { - return new ResolvedFunction(target, qualifiedName, ""); - } - } - // It's possible that the overload was not found, but this situation is accounted for in - // the planCall phase; however, the leading dot used for denoting fully-qualified - // namespaced identifiers must be stripped, as all declarations already use fully-qualified - // names. This stripping behavior is handled automatically by the ResolveCandidateNames - // call. - return new ResolvedFunction(target, stripLeadingDot(fnName), ""); - } - - // Handle the situation where the function target actually indicates a qualified function - // name. - String qualifiedPrefix = toQualifiedName(target); - if (qualifiedPrefix != null) { - String maybeQualifiedName = qualifiedPrefix + "." + fnName; - for (String qualifiedName : container.resolveCandidateNames(maybeQualifiedName)) { - if (disp.findOverload(qualifiedName) != null) { - // Clear the target to ensure the proper arity is used for finding the - // implementation. - return new ResolvedFunction(null, qualifiedName, ""); - } - } - } - // In the default case, the function is exactly as it was advertised: a receiver call on with - // an expression-based target with the given simple function name. - return new ResolvedFunction(target, fnName, ""); - } - - InterpretableAttribute relativeAttr(long id, Interpretable eval) { - InterpretableAttribute eAttr; - if (eval instanceof InterpretableAttribute) { - eAttr = (InterpretableAttribute) eval; - } else { - eAttr = new EvalAttr(adapter, attrFactory.relativeAttribute(id, eval)); - } - Interpretable decAttr = decorate(eAttr); - if (decAttr == null) { - return null; - } - if (!(decAttr instanceof InterpretableAttribute)) { - throw new IllegalStateException( - String.format( - "invalid attribute decoration: %s(%s)", decAttr, decAttr.getClass().getName())); - } - eAttr = (InterpretableAttribute) decAttr; - return eAttr; - } - - /** - * toQualifiedName converts an expression AST into a qualified name if possible, with a boolean - * 'found' value that indicates if the conversion is successful. - */ - String toQualifiedName(Expr operand) { - // If the checker identified the expression as an attribute by the type-checker, then it can't - // possibly be part of qualified name in a namespace. - if (refMap.containsKey(operand.getId())) { - return ""; - } - // Since functions cannot be both namespaced and receiver functions, if the operand is not an - // qualified variable name, return the (possibly) qualified name given the expressions. - return Container.toQualifiedName(operand); - } - - String stripLeadingDot(String name) { - return name.startsWith(".") ? name.substring(1) : name; - } - } } diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/Interpreter.java b/core/src/main/java/org/projectnessie/cel/interpreter/Interpreter.java index dfac180d6..4f351db46 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/Interpreter.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Interpreter.java @@ -19,8 +19,6 @@ import static org.projectnessie.cel.interpreter.InterpretableDecorator.decDisableShortcircuits; import static org.projectnessie.cel.interpreter.InterpretableDecorator.decObserveEval; import static org.projectnessie.cel.interpreter.InterpretableDecorator.decOptimize; -import static org.projectnessie.cel.interpreter.InterpretablePlanner.newPlanner; -import static org.projectnessie.cel.interpreter.InterpretablePlanner.newUncheckedPlanner; import com.google.api.expr.v1alpha1.CheckedExpr; import com.google.api.expr.v1alpha1.Expr; @@ -113,53 +111,4 @@ static Interpreter newStandardInterpreter( dispatcher.add(Overload.standardOverloads()); return newInterpreter(dispatcher, container, provider, adapter, resolver); } - - final class ExprInterpreter implements Interpreter { - private final Dispatcher dispatcher; - private final Container container; - private final TypeProvider provider; - private final TypeAdapter adapter; - private final AttributeFactory attrFactory; - - ExprInterpreter( - Dispatcher dispatcher, - Container container, - TypeProvider provider, - TypeAdapter adapter, - AttributeFactory attrFactory) { - this.dispatcher = dispatcher; - this.container = container; - this.provider = provider; - this.adapter = adapter; - this.attrFactory = attrFactory; - } - - @Override - public Interpretable newInterpretable( - CheckedExpr checked, InterpretableDecorator... decorators) { - InterpretablePlanner p = - newPlanner(dispatcher, provider, adapter, attrFactory, container, checked, decorators); - return p.plan(checked.getExpr()); - } - - @Override - public Interpretable newInterpretable( - Expr expr, - Map refMap, - Map typeMap, - InterpretableDecorator... decorators) { - InterpretablePlanner p = - newPlanner( - dispatcher, provider, adapter, attrFactory, container, refMap, typeMap, decorators); - return p.plan(expr); - } - - @Override - public Interpretable newUncheckedInterpretable( - Expr expr, InterpretableDecorator... decorators) { - InterpretablePlanner p = - newUncheckedPlanner(dispatcher, provider, adapter, attrFactory, container, decorators); - return p.plan(expr); - } - } } diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/Planner.java b/core/src/main/java/org/projectnessie/cel/interpreter/Planner.java new file mode 100644 index 000000000..4adadeead --- /dev/null +++ b/core/src/main/java/org/projectnessie/cel/interpreter/Planner.java @@ -0,0 +1,1055 @@ +/* + * Copyright (C) 2021 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.BytesT.bytesOf; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.DurationT.durationOf; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.TimestampT.timestampOf; +import static org.projectnessie.cel.common.types.Types.boolOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; +import static org.projectnessie.cel.interpreter.Interpretable.newConstValue; + +import com.google.api.expr.v1alpha1.Constant; +import com.google.api.expr.v1alpha1.Expr; +import com.google.api.expr.v1alpha1.Expr.Call; +import com.google.api.expr.v1alpha1.Expr.Comprehension; +import com.google.api.expr.v1alpha1.Expr.CreateList; +import com.google.api.expr.v1alpha1.Expr.CreateStruct; +import com.google.api.expr.v1alpha1.Expr.CreateStruct.Entry; +import com.google.api.expr.v1alpha1.Expr.Ident; +import com.google.api.expr.v1alpha1.Expr.Select; +import com.google.api.expr.v1alpha1.Reference; +import com.google.api.expr.v1alpha1.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.projectnessie.cel.common.containers.Container; +import org.projectnessie.cel.common.operators.Operator; +import org.projectnessie.cel.common.types.NullT; +import org.projectnessie.cel.common.types.ref.FieldType; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.TypeProvider; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Trait; +import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; +import org.projectnessie.cel.interpreter.AttributeFactory.Qualifier; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableAttribute; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableConst; +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; + +final class Planner implements InterpretablePlanner { + private final Dispatcher disp; + private final TypeProvider provider; + private final TypeAdapter adapter; + private final AttributeFactory attrFactory; + private final Container container; + private final Map refMap; + private final Map typeMap; + private final Map fieldTypes = new HashMap<>(); + private final InterpretableDecorator[] decorators; + + Planner( + Dispatcher disp, + TypeProvider provider, + TypeAdapter adapter, + AttributeFactory attrFactory, + Container container, + Map refMap, + Map typeMap, + InterpretableDecorator[] decorators) { + this.disp = disp; + this.provider = provider; + this.adapter = adapter; + this.attrFactory = attrFactory; + this.container = container; + this.refMap = refMap; + this.typeMap = typeMap; + this.decorators = decorators; + } + + /** + * Plan implements the interpretablePlanner interface. This implementation of the Plan method also + * applies decorators to each Interpretable generated as part of the overall plan. Decorators are + * useful for layering functionality into the evaluation that is not natively understood by CEL, + * such as state-tracking, expression re-write, and possibly efficient thread-safe memoization of + * repeated expressions. + */ + @Override + public Interpretable plan(Expr expr) { + return switch (expr.getExprKindCase()) { + case CALL_EXPR -> decorate(planCall(expr)); + case IDENT_EXPR -> decorate(planIdent(expr)); + case SELECT_EXPR -> decorate(planSelect(expr)); + case LIST_EXPR -> decorate(planCreateList(expr)); + case STRUCT_EXPR -> decorate(planCreateStruct(expr)); + case COMPREHENSION_EXPR -> decorate(planComprehension(expr)); + case CONST_EXPR -> decorate(planConst(expr)); + default -> + throw new IllegalArgumentException( + String.format("unsupported expr of kind %s: '%s'", expr.getExprKindCase(), expr)); + }; + } + + /** + * decorate applies the InterpretableDecorator functions to the given Interpretable. Both the + * Interpretable and error generated by a Plan step are accepted as arguments for convenience. + */ + Interpretable decorate(Interpretable i) { + for (InterpretableDecorator dec : decorators) { + i = dec.decorate(i); + if (i == null) { + return null; + } + } + return i; + } + + /** planIdent creates an Interpretable that resolves an identifier from an Activation. */ + Interpretable planIdent(Expr expr) { + // Establish whether the identifier is in the reference map. + Reference identRef = refMap.get(expr.getId()); + if (identRef != null) { + return planCheckedIdent(expr.getId(), identRef); + } + // Create the possible attribute list for the unresolved reference. + Ident ident = expr.getIdentExpr(); + return new EvalAttr(adapter, attrFactory.maybeAttribute(expr.getId(), ident.getName())); + } + + Interpretable planCheckedIdent(long id, Reference identRef) { + String identName = identRef.getName(); + String providerName = identName.startsWith(".") ? identName.substring(1) : identName; + // Plan a constant reference if this is the case for this simple identifier. + if (identRef.getValue() != Reference.getDefaultInstance().getValue()) { + return plan(Expr.newBuilder().setId(id).setConstExpr(identRef.getValue()).build()); + } + + // Check to see whether the type map indicates this is a type name. All types should be + // registered with the provider. + Type cType = typeMap.get(id); + if (cType != null && cType.getType() != Type.getDefaultInstance()) { + Val cVal = provider.findIdent(providerName); + if (cVal == null) { + throw new IllegalStateException( + String.format("reference to undefined type: %s", providerName)); + } + return newConstValue(id, cVal); + } + + // Otherwise, evaluate the checked top-level variable directly for ordinary plans. Decorated + // programs keep the attribute shape because custom decorators may inspect attributes. + if (decorators.length == 0) { + return new EvalIdent(id, identName, adapter); + } + return new EvalAttr(adapter, attrFactory.absoluteAttribute(id, identName)); + } + + /** + * planSelect creates an Interpretable with either: + * + *
    + *
  1. selects a field from a map or proto. + *
  2. creates a field presence test for a select within a has() macro. + *
  3. resolves the select expression to a namespaced identifier. + *
+ */ + Interpretable planSelect(Expr expr) { + // If the Select id appears in the reference map from the CheckedExpr proto then it is either + // a namespaced identifier or enum value. + Reference identRef = refMap.get(expr.getId()); + if (identRef != null) { + return planCheckedIdent(expr.getId(), identRef); + } + + Select sel = expr.getSelectExpr(); + // Plan the operand evaluation. + Interpretable op = + decorators.length == 0 ? planSelectOperand(sel.getOperand()) : plan(sel.getOperand()); + + // Determine the field type if this is a proto message type. + FieldType fieldType = null; + Type opType = typeMap.get(sel.getOperand().getId()); + if (opType != null && !opType.getMessageType().isEmpty()) { + FieldType ft = findFieldType(opType.getMessageType(), sel.getField()); + if (ft != null && ft.isSet != null && ft.getFrom != null) { + fieldType = ft; + } + } + + // If the Select was marked TestOnly, this is a presence test. + // + // Note: presence tests are defined for structured (e.g. proto) and dynamic values (map, json) + // as follows: + // - True if the object field has a non-default value, e.g. obj.str != "" + // - True if the dynamic value has the field defined, e.g. key in map + // + // However, presence tests are not defined for qualified identifier names with primitive + // types. + // If a string named 'a.b.c' is declared in the environment and referenced within + // `has(a.b.c)`, + // it is not clear whether has should error or follow the convention defined for structured + // values. + if (sel.getTestOnly()) { + // Return the test only eval expression. + return new EvalTestOnly(expr.getId(), op, stringOf(sel.getField()), fieldType); + } + // Build a qualifier. + Qualifier qual = + fieldType != null + ? new FieldQualifier(expr.getId(), sel.getField(), fieldType, adapter) + : attrFactory.newQualifier(opType, expr.getId(), sel.getField()); + if (qual == null) { + return null; + } + // Lastly, create a field selection Interpretable. + if (op instanceof InterpretableAttribute attr) { + attr.addQualifier(qual); + return attr; + } + + InterpretableAttribute relAttr = relativeAttr(op.id(), op); + if (relAttr == null) { + return null; + } + relAttr.addQualifier(qual); + return relAttr; + } + + private Interpretable planSelectOperand(Expr operand) { + if (operand.getExprKindCase() != Expr.ExprKindCase.IDENT_EXPR) { + return plan(operand); + } + + Reference identRef = refMap.get(operand.getId()); + if (identRef == null || identRef.getValue() != Reference.getDefaultInstance().getValue()) { + return plan(operand); + } + + Type cType = typeMap.get(operand.getId()); + if (cType != null && cType.getType() != Type.getDefaultInstance()) { + return plan(operand); + } + + return new EvalAttr( + adapter, attrFactory.absoluteAttribute(operand.getId(), identRef.getName())); + } + + private FieldType findFieldType(String messageType, String fieldName) { + String key = messageType + '\n' + fieldName; + FieldType ft = fieldTypes.get(key); + if (ft != null) { + return ft; + } + ft = provider.findFieldType(messageType, fieldName); + if (ft != null) { + fieldTypes.put(key, ft); + } + return ft; + } + + /** + * planCall creates a callable Interpretable while specializing for common functions and + * invocation patterns. Specifically, conditional operators &&, ||, ?:, and (in)equality functions + * result in optimized Interpretable values. + */ + Interpretable planCall(Expr expr) { + Call call = expr.getCallExpr(); + ResolvedFunction resolvedFunc = resolveFunction(expr); + // target, fnName, oName := p.resolveFunction(expr) + int argCount = call.getArgsCount(); + int offset = 0; + if (resolvedFunc.target != null) { + argCount++; + offset++; + } + + Interpretable[] args = new Interpretable[argCount]; + if (resolvedFunc.target != null) { + Interpretable arg = plan(resolvedFunc.target); + if (arg == null) { + return null; + } + args[0] = arg; + } + for (int i = 0; i < call.getArgsCount(); i++) { + Expr argExpr = call.getArgs(i); + Interpretable arg = plan(argExpr); + args[i + offset] = arg; + } + + // Generate specialized Interpretable operators by function name if possible. + if (resolvedFunc.fnName.equals(Operator.LogicalAnd.id)) return planCallLogicalAnd(expr, args); + if (resolvedFunc.fnName.equals(Operator.LogicalOr.id)) return planCallLogicalOr(expr, args); + if (resolvedFunc.fnName.equals(Operator.Conditional.id)) return planCallConditional(expr, args); + if (resolvedFunc.fnName.equals(Operator.Equals.id)) return planCallEqual(expr, args); + if (resolvedFunc.fnName.equals(Operator.NotEquals.id)) return planCallNotEqual(expr, args); + if (resolvedFunc.fnName.equals(Operator.Index.id)) return planCallIndex(expr, args); + if ("optional_or".equals(resolvedFunc.overloadId) + || (resolvedFunc.overloadId.isEmpty() + && "or".equals(resolvedFunc.fnName) + && disp.findOverload("optional_or") != null)) + return new EvalOptionalOr( + expr.getId(), resolvedFunc.fnName, resolvedFunc.overloadId, args[0], args[1], false); + if ("optional_or_value".equals(resolvedFunc.overloadId) + || (resolvedFunc.overloadId.isEmpty() + && "orValue".equals(resolvedFunc.fnName) + && disp.findOverload("optional_or_value") != null)) + return new EvalOptionalOr( + expr.getId(), resolvedFunc.fnName, resolvedFunc.overloadId, args[0], args[1], true); + + // Otherwise, generate Interpretable calls specialized by argument count. + // Try to find the specific function by overload id. + Overload fnDef = null; + if (resolvedFunc.overloadId != null && !resolvedFunc.overloadId.isEmpty()) { + fnDef = disp.findOverload(resolvedFunc.overloadId); + } + // If the overload id couldn't resolve the function, try the simple function name. + if (fnDef == null) { + fnDef = disp.findOverload(resolvedFunc.fnName); + } + return switch (argCount) { + 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); + }; + } + + /** planCallZero generates a zero-arity callable Interpretable. */ + 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)); + } + return new EvalZeroArity(expr.getId(), function, overload, impl.function); + } + + /** planCallUnary generates a unary callable Interpretable. */ + static Interpretable planCallUnary( + Expr expr, String function, String overload, Overload impl, Interpretable[] args) { + UnaryOp fn = null; + Trait trait = null; + if (impl != null) { + if (impl.unary == null) { + throw new IllegalStateException(String.format("no such overload: %s(arg)", function)); + } + fn = impl.unary; + trait = impl.operandTrait; + } + return new EvalUnary(expr.getId(), function, overload, args[0], trait, fn); + } + + /** planCallBinary generates a binary callable Interpretable. */ + static Interpretable planCallBinary( + Expr expr, String function, String overload, Overload impl, Interpretable... args) { + BinaryOp fn = null; + Trait trait = null; + if (impl != null) { + if (impl.binary == null) { + throw new IllegalStateException(String.format("no such overload: %s(lhs, rhs)", function)); + } + fn = impl.binary; + trait = impl.operandTrait; + } + 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) { + if (impl == null) { + return new EvalReceiverVarArgs(expr.getId(), function, overload, args); + } + FunctionOp fn = null; + Trait trait = null; + 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); + } + + /** planCallEqual generates an equals (==) Interpretable. */ + static Interpretable planCallEqual(Expr expr, Interpretable... args) { + return new EvalEq(expr.getId(), args[0], args[1]); + } + + /** planCallNotEqual generates a not equals (!=) Interpretable. */ + static Interpretable planCallNotEqual(Expr expr, Interpretable... args) { + return new EvalNe(expr.getId(), args[0], args[1]); + } + + /** planCallLogicalAnd generates a logical and (&&) Interpretable. */ + static Interpretable planCallLogicalAnd(Expr expr, Interpretable... args) { + return new EvalAnd(expr.getId(), args[0], args[1]); + } + + /** planCallLogicalOr generates a logical or (||) Interpretable. */ + static Interpretable planCallLogicalOr(Expr expr, Interpretable... args) { + return new EvalOr(expr.getId(), args[0], args[1]); + } + + /** planCallConditional generates a conditional / ternary (c ? t : f) Interpretable. */ + Interpretable planCallConditional(Expr expr, Interpretable... args) { + Interpretable cond = args[0]; + + Interpretable t = args[1]; + Attribute tAttr; + if (t instanceof InterpretableAttribute truthyAttr) { + tAttr = truthyAttr.attr(); + } else { + tAttr = attrFactory.relativeAttribute(t.id(), t); + } + + Interpretable f = args[2]; + Attribute fAttr; + if (f instanceof InterpretableAttribute falsyAttr) { + fAttr = falsyAttr.attr(); + } else { + fAttr = attrFactory.relativeAttribute(f.id(), f); + } + + return new EvalAttr( + adapter, attrFactory.conditionalAttribute(expr.getId(), cond, tAttr, fAttr)); + } + + /** + * planCallIndex either extends an attribute with the argument to the index operation, or creates + * a relative attribute based on the return of a function call or operation. + */ + Interpretable planCallIndex(Expr expr, Interpretable... args) { + Interpretable op = args[0]; + Interpretable ind = args[1]; + InterpretableAttribute opAttr = relativeAttr(op.id(), op); + if (opAttr == null) { + return null; + } + Type opType = typeMap.get(expr.getCallExpr().getTarget().getId()); + if (ind instanceof InterpretableConst indConst) { + Qualifier qual = attrFactory.newQualifier(opType, expr.getId(), indConst.value()); + if (qual == null) { + return null; + } + opAttr.addQualifier(qual); + return opAttr; + } + if (ind instanceof InterpretableAttribute indAttr) { + Qualifier qual = attrFactory.newQualifier(opType, expr.getId(), indAttr); + if (qual == null) { + return null; + } + opAttr.addQualifier(qual); + return opAttr; + } + InterpretableAttribute indQual = relativeAttr(expr.getId(), ind); + if (indQual == null) { + return null; + } + opAttr.addQualifier(indQual); + return opAttr; + } + + /** planCreateList generates a list construction Interpretable. */ + Interpretable planCreateList(Expr expr) { + CreateList list = expr.getListExpr(); + Interpretable[] elems = new Interpretable[list.getElementsCount()]; + boolean[] optionalIndices = new boolean[list.getElementsCount()]; + for (int index : list.getOptionalIndicesList()) { + optionalIndices[index] = true; + } + for (int i = 0; i < list.getElementsCount(); i++) { + Expr elem = list.getElements(i); + Interpretable elemVal = plan(elem); + if (elemVal == null) { + return null; + } + elems[i] = elemVal; + } + return new EvalList(expr.getId(), elems, optionalIndices, adapter); + } + + /** planCreateStruct generates a map or object construction Interpretable. */ + Interpretable planCreateStruct(Expr expr) { + CreateStruct str = expr.getStructExpr(); + if (!str.getMessageName().isEmpty()) { + return planCreateObj(expr); + } + List entries = str.getEntriesList(); + Interpretable[] keys = new Interpretable[entries.size()]; + Interpretable[] vals = new Interpretable[entries.size()]; + boolean[] optionalEntries = new boolean[entries.size()]; + for (int i = 0; i < entries.size(); i++) { + Entry entry = entries.get(i); + optionalEntries[i] = entry.getOptionalEntry(); + Interpretable keyVal = plan(entry.getMapKey()); + if (keyVal == null) { + return null; + } + keys[i] = keyVal; + + Interpretable valVal = plan(entry.getValue()); + if (valVal == null) { + return null; + } + vals[i] = valVal; + } + return new EvalMap(expr.getId(), keys, vals, optionalEntries, adapter); + } + + /** planCreateObj generates an object construction Interpretable. */ + Interpretable planCreateObj(Expr expr) { + CreateStruct obj = expr.getStructExpr(); + String typeName = resolveTypeName(obj.getMessageName()); + if (typeName == null) { + throw new IllegalStateException(String.format("unknown type: %s", obj.getMessageName())); + } + List entries = obj.getEntriesList(); + String[] fields = new String[entries.size()]; + Interpretable[] vals = new Interpretable[entries.size()]; + boolean[] optionalEntries = new boolean[entries.size()]; + for (int i = 0; i < entries.size(); i++) { + Entry entry = entries.get(i); + fields[i] = entry.getFieldKey(); + optionalEntries[i] = entry.getOptionalEntry(); + Interpretable val = plan(entry.getValue()); + if (val == null) { + return null; + } + vals[i] = val; + } + return new EvalObj(expr.getId(), typeName, fields, vals, optionalEntries, provider); + } + + /** planComprehension generates an Interpretable fold operation. */ + Interpretable planComprehension(Expr expr) { + Comprehension fold = expr.getComprehensionExpr(); + MacroMapFold macroMapFold = macroMapFold(fold); + if (macroMapFold != null) { + Interpretable iterRange = plan(fold.getIterRange()); + if (iterRange == null) { + return null; + } + Interpretable filter = null; + if (macroMapFold.filter != null) { + filter = plan(macroMapFold.filter); + if (filter == null) { + return null; + } + } + Interpretable transform = plan(macroMapFold.transform); + if (transform == null) { + return null; + } + return new EvalMapFold( + expr.getId(), + fold.getIterVar(), + fold.getIterVar2(), + iterRange, + filter, + transform, + adapter); + } + + 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(), + fold.getIterVar2(), + iterRange, + filter, + transform, + adapter); + } + + Interpretable accu = plan(fold.getAccuInit()); + if (accu == null) { + return null; + } + Interpretable iterRange = plan(fold.getIterRange()); + if (iterRange == null) { + return null; + } + Interpretable cond = plan(fold.getLoopCondition()); + if (cond == null) { + return null; + } + Interpretable step = plan(fold.getLoopStep()); + if (step == null) { + return null; + } + Interpretable result = plan(fold.getResult()); + if (result == null) { + return null; + } + return new EvalFold( + expr.getId(), + fold.getAccuVar(), + accu, + fold.getIterVar(), + fold.getIterVar2(), + iterRange, + cond, + step, + result); + } + + private static MacroMapFold macroMapFold(Comprehension fold) { + if (!isEmptyMap(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 = mapEntryValue(fold.getIterVar(), step); + if (transform == null + || referencesIdent(transform, fold.getAccuVar()) + || (filter != null && referencesIdent(filter, fold.getAccuVar()))) { + return null; + } + return new MacroMapFold(filter, transform); + } + + private static 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 static 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 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 static boolean isIdent(Expr expr, String name) { + return expr.getExprKindCase() == Expr.ExprKindCase.IDENT_EXPR + && expr.getIdentExpr().getName().equals(name); + } + + private static boolean isEmptyList(Expr expr) { + return expr.getExprKindCase() == Expr.ExprKindCase.LIST_EXPR + && expr.getListExpr().getElementsCount() == 0; + } + + private static boolean isEmptyMap(Expr expr) { + return expr.getExprKindCase() == Expr.ExprKindCase.STRUCT_EXPR + && expr.getStructExpr().getMessageName().isEmpty() + && expr.getStructExpr().getEntriesCount() == 0; + } + + private static Expr mapEntryValue(String keyVar, Expr step) { + if (step.getExprKindCase() != Expr.ExprKindCase.STRUCT_EXPR + || !step.getStructExpr().getMessageName().isEmpty() + || step.getStructExpr().getEntriesCount() != 1) { + return null; + } + Entry entry = step.getStructExpr().getEntries(0); + if (!isIdent(entry.getMapKey(), keyVar)) { + return null; + } + return entry.getValue(); + } + + 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 static 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) + && !comprehension.getIterVar2().equals(name) + && !comprehension.getAccuVar().equals(name) + && referencesIdent(comprehension.getLoopCondition(), name)) + || (!comprehension.getIterVar().equals(name) + && !comprehension.getIterVar2().equals(name) + && !comprehension.getAccuVar().equals(name) + && referencesIdent(comprehension.getLoopStep(), name)) + || (!comprehension.getAccuVar().equals(name) + && referencesIdent(comprehension.getResult(), name)); + default: + return false; + } + } + + private static final class MacroListFold { + final Expr filter; + final Expr transform; + + private MacroListFold(Expr filter, Expr transform) { + this.filter = filter; + this.transform = transform; + } + } + + private static final class MacroMapFold { + final Expr filter; + final Expr transform; + + private MacroMapFold(Expr filter, Expr transform) { + this.filter = filter; + this.transform = transform; + } + } + + /** planConst generates a constant valued Interpretable. */ + static Interpretable planConst(Expr expr) { + Val val = constValue(expr.getConstExpr()); + if (val == null) { + return null; + } + return newConstValue(expr.getId(), val); + } + + /** constValue converts a proto Constant value to a ref.Val. */ + @SuppressWarnings("deprecation") + static Val constValue(Constant c) { + return switch (c.getConstantKindCase()) { + case BOOL_VALUE -> boolOf(c.getBoolValue()); + case BYTES_VALUE -> bytesOf(c.getBytesValue()); + case DOUBLE_VALUE -> doubleOf(c.getDoubleValue()); + case DURATION_VALUE -> durationOf(c.getDurationValue()); + case INT64_VALUE -> intOf(c.getInt64Value()); + case NULL_VALUE -> NullT.NullValue; + case STRING_VALUE -> stringOf(c.getStringValue()); + case TIMESTAMP_VALUE -> timestampOf(c.getTimestampValue()); + case UINT64_VALUE -> uintOf(c.getUint64Value()); + default -> + throw new IllegalArgumentException( + String.format( + "unknown constant type: '%s' of kind '%s'", c, c.getConstantKindCase())); + }; + } + + /** + * resolveTypeName takes a qualified string constructed at parse time, applies the proto namespace + * resolution rules to it in a scan over possible matching types in the TypeProvider. + */ + String resolveTypeName(String typeName) { + for (String qualifiedTypeName : container.resolveCandidateNames(typeName)) { + if (provider.findType(qualifiedTypeName) != null) { + return qualifiedTypeName; + } + } + return null; + } + + static final class ResolvedFunction { + final Expr target; + final String fnName; + final String overloadId; + + ResolvedFunction(Expr target, String fnName, String overloadId) { + this.target = target; + this.fnName = fnName; + this.overloadId = overloadId; + } + } + + /** + * resolveFunction determines the call target, function name, and overload name from a given Expr + * value. + * + *

The resolveFunction resolves ambiguities where a function may either be a receiver-style + * invocation or a qualified global function name. + * + *

    + *
  • The target expression may only consist of ident and select expressions. + *
  • The function is declared in the environment using its fully-qualified name. + *
  • The fully-qualified function name matches the string serialized target value. + *
+ */ + ResolvedFunction resolveFunction(Expr expr) { + // Note: similar logic exists within the `checker/checker.go`. If making changes here + // please consider the impact on checker.go and consolidate implementations or mirror code + // as appropriate. + Call call = expr.getCallExpr(); + Expr target = call.hasTarget() ? call.getTarget() : null; + String fnName = call.getFunction(); + + // Checked expressions always have a reference map entry, and _should_ have the fully + // qualified + // function name as the fnName value. + Reference oRef = refMap.get(expr.getId()); + if (oRef != null) { + if (oRef.getOverloadIdCount() == 1) { + return new ResolvedFunction(target, fnName, oRef.getOverloadId(0)); + } + // Note, this namespaced function name will not appear as a fully qualified name in ASTs + // built and stored before cel-go v0.5.0; however, this functionality did not work at all + // before the v0.5.0 release. + return new ResolvedFunction(target, fnName, ""); + } + + // Parse-only expressions need to handle the same logic as is normally performed at check + // time, + // but with potentially much less information. The only reliable source of information about + // which functions are configured is the dispatcher. + if (target == null) { + // If the user has a parse-only expression, then it should have been configured as such in + // the interpreter dispatcher as it may have been omitted from the checker environment. + for (String qualifiedName : container.resolveCandidateNames(fnName)) { + if (disp.findOverload(qualifiedName) != null) { + return new ResolvedFunction(target, qualifiedName, ""); + } + } + // It's possible that the overload was not found, but this situation is accounted for in + // the planCall phase; however, the leading dot used for denoting fully-qualified + // namespaced identifiers must be stripped, as all declarations already use fully-qualified + // names. This stripping behavior is handled automatically by the ResolveCandidateNames + // call. + return new ResolvedFunction(target, stripLeadingDot(fnName), ""); + } + + // Handle the situation where the function target actually indicates a qualified function + // name. + String qualifiedPrefix = toQualifiedName(target); + if (qualifiedPrefix != null) { + String maybeQualifiedName = qualifiedPrefix + "." + fnName; + for (String qualifiedName : container.resolveCandidateNames(maybeQualifiedName)) { + if (disp.findOverload(qualifiedName) != null) { + // Clear the target to ensure the proper arity is used for finding the + // implementation. + return new ResolvedFunction(null, qualifiedName, ""); + } + } + } + // In the default case, the function is exactly as it was advertised: a receiver call on with + // an expression-based target with the given simple function name. + return new ResolvedFunction(target, fnName, ""); + } + + InterpretableAttribute relativeAttr(long id, Interpretable eval) { + InterpretableAttribute eAttr; + if (eval instanceof InterpretableAttribute) { + eAttr = (InterpretableAttribute) eval; + } else { + eAttr = new EvalAttr(adapter, attrFactory.relativeAttribute(id, eval)); + } + Interpretable decAttr = decorate(eAttr); + if (decAttr == null) { + return null; + } + if (!(decAttr instanceof InterpretableAttribute)) { + throw new IllegalStateException( + String.format( + "invalid attribute decoration: %s(%s)", decAttr, decAttr.getClass().getName())); + } + eAttr = (InterpretableAttribute) decAttr; + return eAttr; + } + + /** + * toQualifiedName converts an expression AST into a qualified name if possible, with a boolean + * 'found' value that indicates if the conversion is successful. + */ + String toQualifiedName(Expr operand) { + // If the checker identified the expression as an attribute by the type-checker, then it can't + // possibly be part of qualified name in a namespace. + if (refMap.containsKey(operand.getId())) { + return ""; + } + // Since functions cannot be both namespaced and receiver functions, if the operand is not an + // qualified variable name, return the (possibly) qualified name given the expressions. + return Container.toQualifiedName(operand); + } + + String stripLeadingDot(String name) { + return name.startsWith(".") ? name.substring(1) : name; + } +} diff --git a/core/src/main/java/org/projectnessie/cel/interpreter/ResolvedValue.java b/core/src/main/java/org/projectnessie/cel/interpreter/ResolvedValue.java index 25f238d90..4209dc942 100644 --- a/core/src/main/java/org/projectnessie/cel/interpreter/ResolvedValue.java +++ b/core/src/main/java/org/projectnessie/cel/interpreter/ResolvedValue.java @@ -17,10 +17,22 @@ import java.util.Objects; +/** + * Legacy activation-resolution result distinguishing absent, present-null, and present values. + * + * @deprecated migrate callbacks to {@link ActivationFunction}, using {@link + * ActivationFunction#ABSENT} for absence and ordinary {@code null} for present null + */ +@SuppressWarnings("DeprecatedIsStillUsed") +@Deprecated(forRemoval = true) public final class ResolvedValue { + /** Legacy singleton representing a present {@code null} value. */ public static final ResolvedValue NULL_VALUE = new ResolvedValue(null, true); + + /** Legacy singleton representing an absent binding. */ public static final ResolvedValue ABSENT = new ResolvedValue(null, false); + /** Returns a legacy result containing a non-null value. */ public static ResolvedValue resolvedValue(Object value) { return new ResolvedValue(Objects.requireNonNull(value), true); } @@ -33,10 +45,12 @@ private ResolvedValue(Object value, boolean present) { this.present = present; } + /** Returns the contained value, including {@code null} for {@link #NULL_VALUE}. */ public Object value() { return value; } + /** Returns whether a binding is present. */ public boolean present() { return present; } @@ -63,4 +77,25 @@ public int hashCode() { public String toString() { return "ResolvedValue{present=" + present + ", value=" + value + '}'; } + + static Object mapLegacy(Object o) { + if (o instanceof ResolvedValue resolvedValue) { + if (resolvedValue.present()) { + return resolvedValue.value(); + } + return ActivationFunction.ABSENT; + } + return Objects.requireNonNullElse(o, ActivationFunction.ABSENT); + } + + static ResolvedValue mapTo(Object result) { + if (result instanceof ResolvedValue) { + return (ResolvedValue) result; + } else if (result == null) { + return ResolvedValue.NULL_VALUE; + } else if (result == ActivationFunction.ABSENT) { + return ResolvedValue.ABSENT; + } + return ResolvedValue.resolvedValue(result); + } } diff --git a/core/src/test/java/org/projectnessie/ListContainsTest.java b/core/src/test/java/org/projectnessie/ListContainsTest.java index b965a76e0..e4bb2f044 100644 --- a/core/src/test/java/org/projectnessie/ListContainsTest.java +++ b/core/src/test/java/org/projectnessie/ListContainsTest.java @@ -45,7 +45,7 @@ public void dynamicProtobufFieldLookupSuccess() throws ScriptException { arguments.put("this", 1); arguments.put("rules", rule); - String result = script.execute(String.class, arguments); + String result = script.executeWithActivation(String.class, arguments); assertThat(result).isEqualTo(""); } @@ -66,7 +66,7 @@ public void dynamicProtobufFieldLookupFailure() throws ScriptException { arguments.put("this", 15); arguments.put("rules", rule); - String result = script.execute(String.class, arguments); + String result = script.executeWithActivation(String.class, arguments); assertThat(result).isEqualTo("value must be in list"); } @@ -86,7 +86,7 @@ public void uintInList() throws ScriptException { Map arguments = new HashMap<>(); arguments.put("rules", rule); - Boolean result = script.execute(Boolean.class, arguments); + Boolean result = script.executeWithActivation(Boolean.class, arguments); assertThat(result).isTrue(); } @@ -106,7 +106,7 @@ public void uintNotInList() throws ScriptException { Map arguments = new HashMap<>(); arguments.put("rules", rule); - Boolean result = script.execute(Boolean.class, arguments); + Boolean result = script.executeWithActivation(Boolean.class, arguments); assertThat(result).isFalse(); } } diff --git a/core/src/test/java/org/projectnessie/cel/common/types/NumericSemanticsTest.java b/core/src/test/java/org/projectnessie/cel/common/types/NumericSemanticsTest.java new file mode 100644 index 000000000..0b415e22f --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/common/types/NumericSemanticsTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2021 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.common.types; + +import static org.assertj.core.api.Assertions.assertThat; +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.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.IntT.IntNegOne; +import static org.projectnessie.cel.common.types.IntT.IntOne; +import static org.projectnessie.cel.common.types.IntT.IntZero; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; + +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Comparer; + +class NumericSemanticsTest { + + @Test + void signedUnsignedBoundaries() { + assertComparison(intOf(-1L), uintOf(0L), IntNegOne); + assertComparison(intOf(Long.MAX_VALUE), uintOf(Long.MAX_VALUE), IntZero); + assertComparison(intOf(Long.MAX_VALUE), uintOf(Long.MIN_VALUE), IntNegOne); + assertComparison(uintOf(-1L), intOf(Long.MAX_VALUE), IntOne); + + assertThat(intOf(-1L).equal(uintOf(-1L))).isSameAs(False); + assertThat(uintOf(Long.MAX_VALUE).equal(intOf(Long.MAX_VALUE))).isSameAs(True); + assertThat(uintOf(Long.MIN_VALUE).equal(intOf(Long.MIN_VALUE))).isSameAs(False); + } + + @Test + void floatingPointConversionBoundaries() { + long firstInexactInteger = (1L << 53) + 1L; + DoubleT roundedInteger = doubleOf((double) firstInexactInteger); + assertComparison(intOf(firstInexactInteger), roundedInteger, IntZero); + assertThat(intOf(firstInexactInteger).equal(roundedInteger)).isSameAs(True); + + UintT maxUint = uintOf(-1L); + DoubleT roundedMaxUint = (DoubleT) maxUint.convertToType(DoubleT.DoubleType); + assertComparison(maxUint, roundedMaxUint, IntZero); + assertThat(maxUint.equal(roundedMaxUint)).isSameAs(True); + + assertComparison(intOf(Long.MAX_VALUE), doubleOf(Double.POSITIVE_INFINITY), IntNegOne); + assertComparison(uintOf(-1L), doubleOf(Double.POSITIVE_INFINITY), IntNegOne); + assertComparison(intOf(Long.MIN_VALUE), doubleOf(Double.NEGATIVE_INFINITY), IntOne); + } + + @Test + void nanAndSignedZero() { + DoubleT nan = doubleOf(Double.NaN); + DoubleT zero = doubleOf(0.0d); + DoubleT negativeZero = doubleOf(-0.0d); + + assertThat(nan.equal(nan)).isSameAs(False); + assertComparison(nan, nan, IntZero); + assertComparison(nan, zero, IntOne); + assertComparison(zero, nan, IntNegOne); + + assertThat(zero.equal(negativeZero)).isSameAs(True); + assertComparison(zero, negativeZero, IntZero); + assertComparison(negativeZero, zero, IntZero); + } + + private static void assertComparison(Val left, Val right, IntT expected) { + assertThat(((Comparer) left).compare(right)).isSameAs(expected); + assertThat(((Comparer) right).compare(left)) + .isSameAs(expected == IntZero ? IntZero : expected == IntOne ? IntNegOne : IntOne); + } +} diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/ActivationTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/ActivationTest.java index 75c0a0555..b7ac88769 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/ActivationTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/ActivationTest.java @@ -23,14 +23,22 @@ import static org.projectnessie.cel.interpreter.Activation.newActivation; import static org.projectnessie.cel.interpreter.Activation.newHierarchicalActivation; -import java.time.ZonedDateTime; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.IntStream; import org.junit.jupiter.api.Test; -import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.projectnessie.cel.common.types.ref.Val; public class ActivationTest { @@ -49,8 +57,8 @@ void activation() { @Test void resolve() { Activation activation = newActivation(Collections.singletonMap("a", True)); - assertThat(activation.resolveName("a").present()).isTrue(); - assertThat(activation.resolveName("a").value()).isSameAs(True); + assertThat(activation.resolve("a")).isNotSameAs(Activation.ABSENT); + assertThat(activation.resolve("a")).isSameAs(True); } @Test @@ -59,56 +67,256 @@ void resolveNullAndAbsentFromMapActivation() { map.put("nullValue", null); Activation activation = newActivation(map); - assertThat(activation.resolveName("nullValue")).isSameAs(ResolvedValue.NULL_VALUE); - assertThat(activation.resolveName("absent")).isSameAs(ResolvedValue.ABSENT); + assertThat(activation.resolve("nullValue")).isNull(); + assertThat(activation.resolve("absent")).isSameAs(Activation.ABSENT); } @Test void resolveLazy() { - AtomicReference v = new AtomicReference<>(); - Supplier now = + AtomicInteger invocations = new AtomicInteger(); + Map map = new HashMap<>(); + Supplier supplier = () -> { - if (v.get() == null) { - v.set(DefaultTypeAdapter.Instance.nativeToValue(ZonedDateTime.now())); - } - return v.get(); + invocations.incrementAndGet(); + return stringOf("lazy"); }; - Map map = new HashMap<>(); - map.put("now", now); + map.put("now", supplier); Activation a = newActivation(map); - ResolvedValue first = a.resolveName("now"); - ResolvedValue second = a.resolveName("now"); - assertThat(first.present()).isTrue(); - assertThat(second.present()).isTrue(); - assertThat(first.value()).isSameAs(second.value()); + Object first = a.resolve("now"); + Object second = a.resolve("now"); + assertThat(invocations).hasValue(1); + assertThat(first).isNotSameAs(Activation.ABSENT).isSameAs(second).isEqualTo(stringOf("lazy")); + assertThat(second).isNotSameAs(Activation.ABSENT); + assertThat(map).containsEntry("now", supplier); + } + + @Test + void resolvesLazyBindingFromImmutableMap() { + AtomicInteger invocations = new AtomicInteger(); + Supplier supplier = + () -> { + invocations.incrementAndGet(); + return "lazy"; + }; + Map bindings = Map.of("value", supplier); + Activation activation = newActivation(bindings); + + assertThat(activation.resolve("value")).isEqualTo("lazy"); + assertThat(activation.resolve("value")).isEqualTo("lazy"); + assertThat(invocations).hasValue(1); + assertThat(bindings).containsEntry("value", supplier); + } + + @Test + void memoizesNullSupplierResult() { + AtomicInteger invocations = new AtomicInteger(); + Activation activation = + newActivation( + Map.of( + "value", + (Supplier) + () -> { + invocations.incrementAndGet(); + return null; + })); + + assertThat(activation.resolve("value")).isNull(); + assertThat(activation.resolve("value")).isNull(); + assertThat(invocations).hasValue(1); + } + + @Test + void memoizesAbsentSupplierResult() { + AtomicInteger invocations = new AtomicInteger(); + Activation activation = + newActivation( + Map.of( + "value", + (Supplier) + () -> { + invocations.incrementAndGet(); + return Activation.ABSENT; + })); + + assertThat(activation.resolve("value")).isSameAs(Activation.ABSENT); + assertThat(activation.resolve(".value")).isSameAs(Activation.ABSENT); + assertThat(invocations).hasValue(1); } @Test - void hierarchicalActivation() { + void retriesSupplierAfterFailure() { + AtomicInteger invocations = new AtomicInteger(); + Activation activation = + newActivation( + Map.of( + "value", + (Supplier) + () -> { + if (invocations.incrementAndGet() == 1) { + throw new IllegalStateException("not yet"); + } + return "ready"; + })); + + assertThatThrownBy(() -> activation.resolve("value")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("not yet"); + assertThat(activation.resolve("value")).isEqualTo("ready"); + assertThat(activation.resolve("value")).isEqualTo("ready"); + assertThat(invocations).hasValue(2); + } + + @Test + void treatsSupplierResultAsResolvedValue() { + AtomicInteger outerInvocations = new AtomicInteger(); + AtomicInteger innerInvocations = new AtomicInteger(); + Supplier inner = + () -> { + innerInvocations.incrementAndGet(); + return "inner"; + }; + Activation activation = + newActivation( + Map.of( + "value", + (Supplier) + () -> { + outerInvocations.incrementAndGet(); + return inner; + })); + + assertThat(activation.resolve("value")).isSameAs(inner); + assertThat(activation.resolve("value")).isSameAs(inner); + assertThat(outerInvocations).hasValue(1); + assertThat(innerInvocations).hasValue(0); + } + + @Test + void resolvesSupplierOnceConcurrently() throws Exception { + int concurrency = 8; + AtomicInteger invocations = new AtomicInteger(); + CountDownLatch ready = new CountDownLatch(concurrency); + CountDownLatch start = new CountDownLatch(1); + Activation activation = + newActivation( + Map.of( + "value", + (Supplier) + () -> { + invocations.incrementAndGet(); + return "lazy"; + })); + ExecutorService executor = Executors.newFixedThreadPool(concurrency); + try { + List> futures = + IntStream.range(0, concurrency) + .mapToObj( + ignored -> + executor.submit( + () -> { + ready.countDown(); + assertThat(start.await(30, TimeUnit.SECONDS)).isTrue(); + return activation.resolve("value"); + })) + .toList(); + assertThat(ready.await(30, TimeUnit.SECONDS)).isTrue(); + start.countDown(); + for (Future future : futures) { + assertThat(future.get(30, TimeUnit.SECONDS)).isEqualTo("lazy"); + } + } finally { + executor.shutdownNow(); + assertThat(executor.awaitTermination(30, TimeUnit.SECONDS)).isTrue(); + } + + assertThat(invocations).hasValue(1); + } + + @SuppressWarnings("removal") + @Test + void legacyActivation() { + Function func = + name -> + switch (name) { + case "a" -> stringOf("one"); + case "b" -> ResolvedValue.resolvedValue(stringOf("two")); + case "absent" -> ResolvedValue.ABSENT; + case "null" -> ResolvedValue.NULL_VALUE; + case "real_null" -> null; + default -> throw new RuntimeException("unknown activation name: " + name); + }; + Activation activation = newActivation(func); + assertThat(activation.resolve("a")).isEqualTo(stringOf("one")); + assertThat(activation.resolve("b")).isEqualTo(stringOf("two")); + assertThat(activation.resolve("absent")).isSameAs(ActivationFunction.ABSENT); + assertThat(activation.resolve("real_null")).isSameAs(ActivationFunction.ABSENT); + assertThat(activation.resolve("null")).isNull(); + assertThat(activation.resolveName("a")).isEqualTo(ResolvedValue.resolvedValue(stringOf("one"))); + assertThat(activation.resolveName("absent")).isSameAs(ResolvedValue.ABSENT); + assertThat(activation.resolveName("null")).isSameAs(ResolvedValue.NULL_VALUE); + + assertThat(ResolvedValue.mapTo(null)).isSameAs(ResolvedValue.NULL_VALUE); + assertThat(ResolvedValue.mapTo(ActivationFunction.ABSENT)).isSameAs(ResolvedValue.ABSENT); + assertThat(ResolvedValue.mapTo("foo")).isEqualTo(ResolvedValue.resolvedValue("foo")); + var ref = ResolvedValue.resolvedValue("foo"); + assertThat(ResolvedValue.mapTo(ref)).isSameAs(ref); + + assertThat(ResolvedValue.mapLegacy("foo")).isEqualTo("foo"); + assertThat(ResolvedValue.mapLegacy(null)).isEqualTo(ActivationFunction.ABSENT); + assertThat(ResolvedValue.mapLegacy(ResolvedValue.resolvedValue("foo"))).isEqualTo("foo"); + assertThat(ResolvedValue.mapLegacy(ResolvedValue.NULL_VALUE)).isNull(); + assertThat(ResolvedValue.mapLegacy(ResolvedValue.ABSENT)).isSameAs(ActivationFunction.ABSENT); + } + + @ParameterizedTest + @CsvSource({ + "true,true", + "true,false", + "false,true", + "false,false", + }) + void hierarchicalActivationMap(boolean parentFunction, boolean childFunction) { // compose a parent with more properties than the child Map parentMap = new HashMap<>(); parentMap.put("a", stringOf("world")); parentMap.put("b", intOf(-42)); parentMap.put("d", stringOf("child value for d")); - Activation parent = new Activation.FunctionActivation(parentMap::get); + Activation parent = + parentFunction + ? new FunctionActivation( + name -> parentMap.getOrDefault(name, ActivationFunction.ABSENT)) + : new MapActivation(parentMap); // compose the child such that it shadows the parent Map childMap = new HashMap<>(); childMap.put("a", True); childMap.put("c", stringOf("universe")); - childMap.put("d", ResolvedValue.NULL_VALUE); - Activation child = new Activation.FunctionActivation(childMap::get); + childMap.put("d", null); + Activation child = + childFunction + ? new FunctionActivation(name -> childMap.getOrDefault(name, ActivationFunction.ABSENT)) + : new MapActivation(childMap); + Activation combined = newHierarchicalActivation(parent, child); + assertThat(parent.resolve("a")).isEqualTo(stringOf("world")); + assertThat(parent.resolve("b")).isEqualTo(intOf(-42)); + assertThat(parent.resolve("c")).isSameAs(Activation.ABSENT); + assertThat(parent.resolve("d")).isEqualTo(stringOf("child value for d")); + + assertThat(child.resolve("a")).isEqualTo(True); + assertThat(child.resolve("b")).isSameAs(Activation.ABSENT); + assertThat(child.resolve("c")).isEqualTo(stringOf("universe")); + assertThat(child.resolve("d")).isNull(); + // Resolve the shadowed child value. - assertThat(combined.resolveName("a")).isEqualTo(ResolvedValue.resolvedValue(True)); + assertThat(combined.resolve("a")).isEqualTo(True); // Resolve the parent only value. - assertThat(combined.resolveName("b")).isEqualTo(ResolvedValue.resolvedValue(intOf(-42))); + assertThat(combined.resolve("b")).isEqualTo(intOf(-42)); // Resolve the child only value. - assertThat(combined.resolveName("c")) - .isEqualTo(ResolvedValue.resolvedValue(stringOf("universe"))); + assertThat(combined.resolve("c")).isEqualTo(stringOf("universe")); // Resolve the child value as null without looking to parent. - assertThat(combined.resolveName("d")).isSameAs(ResolvedValue.NULL_VALUE); + assertThat(combined.resolve("d")).isNull(); // Absent - assertThat(combined.resolveName("e")).isSameAs(ResolvedValue.ABSENT); + assertThat(combined.resolve("e")).isSameAs(Activation.ABSENT); } } diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/AttributePatternsTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/AttributePatternsTest.java index e15444dcc..ff500313c 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/AttributePatternsTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/AttributePatternsTest.java @@ -228,6 +228,21 @@ void unknownResolution(PatternTest tst) { } } + @Test + void partialActivationCopiesUnknownPatternArrays() { + AttributePattern original = newAttributePattern("original"); + AttributePattern replacement = newAttributePattern("replacement"); + AttributePattern[] source = {original}; + PartialActivation activation = newPartialActivation(emptyActivation(), source); + + source[0] = replacement; + assertThat(activation.unknownAttributePatterns()).containsExactly(original); + + AttributePattern[] returned = activation.unknownAttributePatterns(); + returned[0] = replacement; + assertThat(activation.unknownAttributePatterns()).containsExactly(original); + } + @Test void crossReference() { TypeRegistry reg = newRegistry(); 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 875d79a2c..07e72a63d 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/InterpreterTest.java @@ -113,12 +113,10 @@ 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; 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; @@ -1956,6 +1954,6 @@ static boolean isFieldQual(Qualifier q, String fieldName) { if (!(q instanceof FieldQualifier)) { return false; } - return ((FieldQualifier) q).name.equals(fieldName); + return ((FieldQualifier) q).name().equals(fieldName); } } diff --git a/core/src/test/java/org/projectnessie/cel/interpreter/OverloadDispatchTest.java b/core/src/test/java/org/projectnessie/cel/interpreter/OverloadDispatchTest.java index d10921a4b..c29e6a718 100644 --- a/core/src/test/java/org/projectnessie/cel/interpreter/OverloadDispatchTest.java +++ b/core/src/test/java/org/projectnessie/cel/interpreter/OverloadDispatchTest.java @@ -64,13 +64,6 @@ 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; diff --git a/core/src/test/java/org/projectnessie/cel/publicapi/InterpreterPublicApiCompileTest.java b/core/src/test/java/org/projectnessie/cel/publicapi/InterpreterPublicApiCompileTest.java new file mode 100644 index 000000000..bf2c9f202 --- /dev/null +++ b/core/src/test/java/org/projectnessie/cel/publicapi/InterpreterPublicApiCompileTest.java @@ -0,0 +1,100 @@ +/* + * 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.publicapi; + +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.cel.common.types.IntT.intOf; + +import com.google.api.expr.v1alpha1.CheckedExpr; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.projectnessie.cel.common.containers.Container; +import org.projectnessie.cel.common.types.pb.ProtoTypeRegistry; +import org.projectnessie.cel.interpreter.Activation; +import org.projectnessie.cel.interpreter.Activation.PartialActivation; +import org.projectnessie.cel.interpreter.AttributeFactory; +import org.projectnessie.cel.interpreter.AttributeFactory.Attribute; +import org.projectnessie.cel.interpreter.AttributeFactory.NamespacedAttribute; +import org.projectnessie.cel.interpreter.AttributeFactory.Qualifier; +import org.projectnessie.cel.interpreter.AttributePattern; +import org.projectnessie.cel.interpreter.Coster; +import org.projectnessie.cel.interpreter.Coster.Cost; +import org.projectnessie.cel.interpreter.Dispatcher; +import org.projectnessie.cel.interpreter.Interpretable; +import org.projectnessie.cel.interpreter.Interpretable.InterpretableConst; +import org.projectnessie.cel.interpreter.InterpretableDecorator; +import org.projectnessie.cel.interpreter.InterpretablePlanner; +import org.projectnessie.cel.interpreter.Interpreter; + +/** + * Compile-time fixture for the intentional public interpreter contracts. + * + *

This test deliberately lives outside {@code org.projectnessie.cel.interpreter} and must not + * name any concrete interpreter implementation. + */ +class InterpreterPublicApiCompileTest { + + @Test + void retainedContractsAndFactoriesRemainUsableOutsideInterpreterPackage() { + ProtoTypeRegistry registry = ProtoTypeRegistry.newEmptyRegistry(); + Container container = Container.defaultContainer; + Dispatcher dispatcher = Dispatcher.newDispatcher(); + Dispatcher extended = Dispatcher.extendDispatcher(dispatcher); + AttributeFactory attributes = + AttributeFactory.newAttributeFactory(container, registry, registry); + + Activation empty = Activation.emptyActivation(); + Activation mapped = Activation.newActivation(Map.of("x", 1L)); + Activation hierarchical = Activation.newHierarchicalActivation(mapped, empty); + PartialActivation partial = + Activation.newPartialActivation( + hierarchical, AttributePattern.newAttributePattern("missing")); + + InterpretableConst constant = Interpretable.newConstValue(1L, intOf(1L)); + NamespacedAttribute namespaced = attributes.absoluteAttribute(2L, "x"); + Attribute relative = attributes.relativeAttribute(3L, constant); + Qualifier qualifier = AttributeFactory.newQualifierStatic(registry, 4L, "field"); + + Interpreter standard = + Interpreter.newStandardInterpreter(container, registry, registry, attributes); + Interpreter configured = + Interpreter.newInterpreter(extended, container, registry, registry, attributes); + InterpretableDecorator optimizer = Interpreter.optimize(); + InterpretablePlanner checked = + InterpretablePlanner.newPlanner( + extended, registry, registry, attributes, container, CheckedExpr.getDefaultInstance()); + InterpretablePlanner checkedMaps = + InterpretablePlanner.newPlanner( + extended, registry, registry, attributes, container, emptyMap(), emptyMap()); + InterpretablePlanner unchecked = + InterpretablePlanner.newUncheckedPlanner( + extended, registry, registry, attributes, container); + assertThat(partial.unknownAttributePatterns()).hasSize(1); + assertThat(namespaced.candidateVariableNames()).containsExactly("x"); + assertThat(relative.addQualifier(qualifier)).isSameAs(relative); + assertThat(constant.value()).isEqualTo(intOf(1L)); + assertThat(standard).isNotNull(); + assertThat(configured).isNotNull(); + assertThat(optimizer).isNotNull(); + assertThat(checked).isNotNull(); + assertThat(checkedMaps).isNotNull(); + assertThat(unchecked).isNotNull(); + assertThat(Coster.costOf(1L, 2L).min).isEqualTo(1L); + assertThat(Coster.costOf(1L, 2L).max).isEqualTo(2L); + assertThat(Cost.estimateCost(constant)).isEqualTo(Cost.None); + } +} diff --git a/jackson/src/jmh/java/org/projectnessie/cel/JacksonEvaluatorBaselineBench.java b/jackson/src/jmh/java/org/projectnessie/cel/JacksonEvaluatorBaselineBench.java new file mode 100644 index 000000000..682bd84b3 --- /dev/null +++ b/jackson/src/jmh/java/org/projectnessie/cel/JacksonEvaluatorBaselineBench.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2021 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; + +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.customTypeProvider; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.EvalOption.OptOptimize; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.interpreter.Activation.newActivation; + +import java.util.Map; +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.AstIssuesTuple; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.interpreter.Activation; +import org.projectnessie.cel.types.jackson.JacksonRegistry; + +/** Measures checked Jackson selection at each evaluator result boundary. */ +@Warmup(iterations = 3, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class JacksonEvaluatorBaselineBench { + + @State(Scope.Benchmark) + public static class EvaluationState { + Program program; + Prog internalProgram; + Map variables; + Activation activation; + Policy policy; + + @Setup + public void init() { + JacksonRegistry registry = (JacksonRegistry) JacksonRegistry.newRegistry(); + Env env = + newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + types(Policy.class, Principal.class), + declarations( + Decls.newVar("policy", Decls.newObjectType(Policy.class.getName())), + Decls.newVar("expected", Decls.String))); + AstIssuesTuple ast = env.compile("policy.owner.email == expected"); + if (ast.hasIssues()) { + throw ast.getIssues().err(); + } + + policy = new Policy(new Principal("alice@example.com")); + variables = Map.of("policy", policy, "expected", "alice@example.com"); + program = env.program(ast.getAst(), evalOptions(OptOptimize)); + internalProgram = (Prog) program; + activation = newActivation(variables); + } + } + + @Benchmark + public Object rawEvaluator(EvaluationState state) { + return state.internalProgram.interpretable.eval(state.activation); + } + + @Benchmark + public Program.EvalResult programEval(EvaluationState state) { + return state.program.eval(state.variables); + } + + @Benchmark + public Object programEvalNative(EvaluationState state) { + return state.internalProgram.e.adapter.valueToNative( + state.program.eval(state.variables).getVal(), Boolean.class); + } + + @Benchmark + public boolean nativeJava(EvaluationState state) { + return state.policy.getOwner().getEmail().equals("alice@example.com"); + } + + public static final class Principal { + private final String email; + + public Principal(String email) { + this.email = email; + } + + public String getEmail() { + return email; + } + } + + public static final class Policy { + private final Principal owner; + + public Policy(Principal owner) { + this.owner = owner; + } + + public Principal getOwner() { + return owner; + } + } +} diff --git a/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2ScriptHostTest.java b/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2ScriptHostTest.java index 0820f8272..7d9a8c35d 100644 --- a/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2ScriptHostTest.java +++ b/jackson/src/test/java/org/projectnessie/cel/types/jackson/Jackson2ScriptHostTest.java @@ -47,8 +47,10 @@ void simple() throws Exception { MetaTest cmMatch = MetaTest.builder().author("foo@bar.baz").build(); MetaTest cmNoMatch = MetaTest.builder().author("foo@foo.foo").build(); - assertThat(script.execute(Boolean.class, singletonMap("param", cmMatch))).isTrue(); - assertThat(script.execute(Boolean.class, singletonMap("param", cmNoMatch))).isFalse(); + assertThat(script.executeWithActivation(Boolean.class, singletonMap("param", cmMatch))) + .isTrue(); + assertThat(script.executeWithActivation(Boolean.class, singletonMap("param", cmNoMatch))) + .isFalse(); script = scriptHost @@ -57,8 +59,9 @@ void simple() throws Exception { .withTypes(MetaTest.class) .build(); - assertThat(script.execute(Object.class, singletonMap("param", cmMatch))).isEqualTo(cmMatch); - assertThat(script.execute(ObjectT.class, singletonMap("param", cmMatch)).value()) + assertThat(script.executeWithActivation(Object.class, singletonMap("param", cmMatch))) + .isEqualTo(cmMatch); + assertThat(script.executeWithActivation(ObjectT.class, singletonMap("param", cmMatch)).value()) .isEqualTo(cmMatch); } @@ -84,7 +87,7 @@ void readmeExample() throws Exception { arguments.put("inp", pojo); arguments.put("checkName", checkName); - assertThat(script.execute(Boolean.class, arguments)).isTrue(); + assertThat(script.executeWithActivation(Boolean.class, arguments)).isTrue(); } @Test @@ -109,7 +112,7 @@ void complexInput() throws Exception { .build()) .build(); - assertThat(script.execute(Boolean.class, singletonMap("param", val))).isTrue(); + assertThat(script.executeWithActivation(Boolean.class, singletonMap("param", val))).isTrue(); // same as above, but use the 'container' @@ -122,7 +125,7 @@ void complexInput() throws Exception { .withTypes(ObjectListEnum.class) .build(); - assertThat(script.execute(Boolean.class, singletonMap("param", val))).isTrue(); + assertThat(script.executeWithActivation(Boolean.class, singletonMap("param", val))).isTrue(); // return the enum @@ -135,7 +138,7 @@ void complexInput() throws Exception { .withTypes(ObjectListEnum.class) .build(); - assertThat(script.execute(Integer.class, singletonMap("param", val))) + assertThat(script.executeWithActivation(Integer.class, singletonMap("param", val))) .isEqualTo(ClassEnum.VAL_2.ordinal()); } } diff --git a/jackson3/src/jmh/java/org/projectnessie/cel/Jackson3EvaluatorBaselineBench.java b/jackson3/src/jmh/java/org/projectnessie/cel/Jackson3EvaluatorBaselineBench.java new file mode 100644 index 000000000..b678e3131 --- /dev/null +++ b/jackson3/src/jmh/java/org/projectnessie/cel/Jackson3EvaluatorBaselineBench.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2021 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; + +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.customTypeAdapter; +import static org.projectnessie.cel.EnvOption.customTypeProvider; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.EvalOption.OptOptimize; +import static org.projectnessie.cel.ProgramOption.evalOptions; +import static org.projectnessie.cel.interpreter.Activation.newActivation; + +import java.util.Map; +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.AstIssuesTuple; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.interpreter.Activation; +import org.projectnessie.cel.types.jackson3.Jackson3Registry; + +/** Measures checked Jackson 3 selection at each evaluator result boundary. */ +@Warmup(iterations = 3, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Fork(1) +@Threads(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class Jackson3EvaluatorBaselineBench { + + @State(Scope.Benchmark) + public static class EvaluationState { + Program program; + Prog internalProgram; + Map variables; + Activation activation; + Policy policy; + + @Setup + public void init() { + Jackson3Registry registry = (Jackson3Registry) Jackson3Registry.newRegistry(); + Env env = + newEnv( + customTypeAdapter(registry), + customTypeProvider(registry), + types(Policy.class, Principal.class), + declarations( + Decls.newVar("policy", Decls.newObjectType(Policy.class.getName())), + Decls.newVar("expected", Decls.String))); + AstIssuesTuple ast = env.compile("policy.owner.email == expected"); + if (ast.hasIssues()) { + throw ast.getIssues().err(); + } + + policy = new Policy(new Principal("alice@example.com")); + variables = Map.of("policy", policy, "expected", "alice@example.com"); + program = env.program(ast.getAst(), evalOptions(OptOptimize)); + internalProgram = (Prog) program; + activation = newActivation(variables); + } + } + + @Benchmark + public Object rawEvaluator(EvaluationState state) { + return state.internalProgram.interpretable.eval(state.activation); + } + + @Benchmark + public Program.EvalResult programEval(EvaluationState state) { + return state.program.eval(state.variables); + } + + @Benchmark + public Object programEvalNative(EvaluationState state) { + return state.internalProgram.e.adapter.valueToNative( + state.program.eval(state.variables).getVal(), Boolean.class); + } + + @Benchmark + public boolean nativeJava(EvaluationState state) { + return state.policy.getOwner().getEmail().equals("alice@example.com"); + } + + public static final class Principal { + private final String email; + + public Principal(String email) { + this.email = email; + } + + public String getEmail() { + return email; + } + } + + public static final class Policy { + private final Principal owner; + + public Policy(Principal owner) { + this.owner = owner; + } + + public Principal getOwner() { + return owner; + } + } +} diff --git a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3ScriptHostTest.java b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3ScriptHostTest.java index 977bd364d..45743da65 100644 --- a/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3ScriptHostTest.java +++ b/jackson3/src/test/java/org/projectnessie/cel/types/jackson3/Jackson3ScriptHostTest.java @@ -48,8 +48,10 @@ void simple() throws Exception { MetaTest cmMatch = MetaTest.builder().author("foo@bar.baz").build(); MetaTest cmNoMatch = MetaTest.builder().author("foo@foo.foo").build(); - assertThat(script.execute(Boolean.class, singletonMap("param", cmMatch))).isTrue(); - assertThat(script.execute(Boolean.class, singletonMap("param", cmNoMatch))).isFalse(); + assertThat(script.executeWithActivation(Boolean.class, singletonMap("param", cmMatch))) + .isTrue(); + assertThat(script.executeWithActivation(Boolean.class, singletonMap("param", cmNoMatch))) + .isFalse(); script = scriptHost @@ -58,8 +60,9 @@ void simple() throws Exception { .withTypes(MetaTest.class) .build(); - assertThat(script.execute(Object.class, singletonMap("param", cmMatch))).isEqualTo(cmMatch); - assertThat(script.execute(ObjectT.class, singletonMap("param", cmMatch)).value()) + assertThat(script.executeWithActivation(Object.class, singletonMap("param", cmMatch))) + .isEqualTo(cmMatch); + assertThat(script.executeWithActivation(ObjectT.class, singletonMap("param", cmMatch)).value()) .isEqualTo(cmMatch); } @@ -86,7 +89,7 @@ void readmeExample() throws Exception { arguments.put("inp", pojo); arguments.put("checkName", checkName); - assertThat(script.execute(Boolean.class, arguments)).isTrue(); + assertThat(script.executeWithActivation(Boolean.class, arguments)).isTrue(); } @Test @@ -112,7 +115,7 @@ void complexInput() throws Exception { .build()) .build(); - assertThat(script.execute(Boolean.class, singletonMap("param", val))).isTrue(); + assertThat(script.executeWithActivation(Boolean.class, singletonMap("param", val))).isTrue(); // same as above, but use the 'container' @@ -125,7 +128,7 @@ void complexInput() throws Exception { .withTypes(ObjectListEnum.class) .build(); - assertThat(script.execute(Boolean.class, singletonMap("param", val))).isTrue(); + assertThat(script.executeWithActivation(Boolean.class, singletonMap("param", val))).isTrue(); // return the enum @@ -138,7 +141,7 @@ void complexInput() throws Exception { .withTypes(ObjectListEnum.class) .build(); - assertThat(script.execute(Integer.class, singletonMap("param", val))) + assertThat(script.executeWithActivation(Integer.class, singletonMap("param", val))) .isEqualTo(ClassEnum.VAL_2.ordinal()); } } diff --git a/tools/src/main/java/org/projectnessie/cel/tools/Script.java b/tools/src/main/java/org/projectnessie/cel/tools/Script.java index 98188dbc1..fb5e658cf 100644 --- a/tools/src/main/java/org/projectnessie/cel/tools/Script.java +++ b/tools/src/main/java/org/projectnessie/cel/tools/Script.java @@ -26,6 +26,7 @@ import org.projectnessie.cel.Program.EvalResult; import org.projectnessie.cel.common.types.Err; import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.ActivationFunction; public final class Script { private final Env env; @@ -36,12 +37,24 @@ public final class Script { this.prg = prg; } + /** Migrate to {@link #executeWithActivation(Class, ActivationFunction)}. */ + @Deprecated(forRemoval = true) public T execute(Class resultType, Function arguments) throws ScriptException { return evaluate(resultType, arguments); } public T execute(Class resultType, Map arguments) throws ScriptException { + return executeWithActivation(resultType, arguments); + } + + public T executeWithActivation(Class resultType, ActivationFunction arguments) + throws ScriptException { + return evaluate(resultType, arguments); + } + + public T executeWithActivation(Class resultType, Map arguments) + throws ScriptException { return evaluate(resultType, arguments); } diff --git a/tools/src/test/java/org/projectnessie/cel/tools/ScriptHostTest.java b/tools/src/test/java/org/projectnessie/cel/tools/ScriptHostTest.java index 9c6d7d715..a8e7f6985 100644 --- a/tools/src/test/java/org/projectnessie/cel/tools/ScriptHostTest.java +++ b/tools/src/test/java/org/projectnessie/cel/tools/ScriptHostTest.java @@ -37,6 +37,7 @@ import org.projectnessie.cel.common.types.MapT; import org.projectnessie.cel.common.types.StringT; import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.ActivationFunction; import org.projectnessie.cel.interpreter.functions.Overload; import org.projectnessie.cel.toolstests.Dummy; @@ -63,7 +64,7 @@ void basic() throws Exception { arguments.put("x", "hello"); arguments.put("y", "world"); - String result = script.execute(String.class, arguments); + String result = script.executeWithActivation(String.class, arguments); assertThat(result).isEqualTo("hello world"); } @@ -81,7 +82,7 @@ void function() throws Exception { .build(); String result = - script.execute( + script.executeWithActivation( String.class, arg -> { if ("x".equals(arg)) { @@ -89,7 +90,7 @@ void function() throws Exception { } else if ("y".equals(arg)) { return "world"; } else { - return null; + return ActivationFunction.ABSENT; } }); @@ -101,12 +102,12 @@ void executeValReturnsCelNativeResult() throws Exception { ScriptHost scriptHost = ScriptHost.newBuilder().build(); Script listScript = scriptHost.buildScript("[1, 2, 3]").build(); - Val list = listScript.execute(Val.class, Collections.emptyMap()); + Val list = listScript.executeWithActivation(Val.class, Collections.emptyMap()); assertThat(list).isInstanceOf(ListT.class); assertThat((Object[]) list.value()).containsExactly(1L, 2L, 3L); Script mapScript = scriptHost.buildScript("{\"a\": 1, \"b\": 2}").build(); - Val map = mapScript.execute(Val.class, Collections.emptyMap()); + Val map = mapScript.executeWithActivation(Val.class, Collections.emptyMap()); assertThat(map).isInstanceOf(MapT.class); Map expectedMap = new HashMap<>(); expectedMap.put("a", 1L); @@ -118,7 +119,7 @@ void executeValReturnsCelNativeResult() throws Exception { void executeObjectStillConvertsToNativeResult() throws Exception { Script script = ScriptHost.newBuilder().build().buildScript("[1, 2, 3]").build(); - Object result = script.execute(Object.class, Collections.emptyMap()); + Object result = script.executeWithActivation(Object.class, Collections.emptyMap()); assertThat(result).isEqualTo(Arrays.asList(1L, 2L, 3L)); } @@ -130,7 +131,8 @@ void execFail() throws Exception { // create the script, will be parsed and checked Script script = scriptHost.buildScript("1/0 != 0").build(); - assertThatThrownBy(() -> script.execute(String.class, singletonMap("x", "hello world"))) + assertThatThrownBy( + () -> script.executeWithActivation(String.class, singletonMap("x", "hello world"))) .isInstanceOf(ScriptExecutionException.class) .hasMessage("divide by zero"); } @@ -177,7 +179,7 @@ public List getProgramOptions() { Script script = scriptHost.buildScript("foo()").withLibraries(new MyLib()).build(); - assertThat(script.execute(Integer.class, Collections.emptyMap())).isEqualTo(42); + assertThat(script.executeWithActivation(Integer.class, Collections.emptyMap())).isEqualTo(42); } @Test @@ -202,7 +204,7 @@ void readmeExample() throws Exception { arguments.put("inp", pojo); arguments.put("checkName", checkName); - assertThat(script.execute(Boolean.class, arguments)).isTrue(); + assertThat(script.executeWithActivation(Boolean.class, arguments)).isTrue(); } @Test @@ -277,7 +279,7 @@ void authorizationExpressionRuntimeErrorIsNonGranting() throws Exception { Map arguments = authorizationArguments("storage.googleapis.com"); arguments.remove("resource.name"); - assertThatThrownBy(() -> script.execute(Boolean.class, arguments)) + assertThatThrownBy(() -> script.executeWithActivation(Boolean.class, arguments)) .isInstanceOf(ScriptExecutionException.class); assertThat(grants(script, arguments)).isFalse(); } @@ -325,7 +327,7 @@ void customGlobalFunctionErrorIsNonGranting() throws Exception { .withLibraries(new AttributeLibrary("prod")) .build(); - assertThatThrownBy(() -> script.execute(Boolean.class, Collections.emptyMap())) + assertThatThrownBy(() -> script.executeWithActivation(Boolean.class, Collections.emptyMap())) .isInstanceOf(ScriptExecutionException.class) .hasMessageContaining("forced getAttribute error"); assertThat(grants(script, Collections.emptyMap())).isFalse(); @@ -375,7 +377,7 @@ void customReceiverFunctionErrorIsNonGranting() throws Exception { .isFalse(); assertThatThrownBy( () -> - script.execute( + script.executeWithActivation( Boolean.class, Collections.singletonMap( "resource.name", "projects/_/buckets/example/objects/reports/q1.csv"))) @@ -415,7 +417,7 @@ private static Map authorizationArguments( private static boolean grants(Script script, Map arguments) { try { - return Boolean.TRUE.equals(script.execute(Boolean.class, arguments)); + return Boolean.TRUE.equals(script.executeWithActivation(Boolean.class, arguments)); } catch (ScriptException | RuntimeException e) { return false; }