diff --git a/runtime/planner/BUILD.bazel b/runtime/planner/BUILD.bazel new file mode 100644 index 000000000..8da29f270 --- /dev/null +++ b/runtime/planner/BUILD.bazel @@ -0,0 +1,11 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//:internal"], +) + +java_library( + name = "program_planner", + exports = ["//runtime/src/main/java/dev/cel/runtime/planner:program_planner"], +) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel new file mode 100644 index 000000000..e3fb32d30 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -0,0 +1,58 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = [ + "//runtime/planner:__pkg__", + ], +) + +java_library( + name = "program_planner", + srcs = ["ProgramPlanner.java"], + tags = [ + ], + deps = [ + ":eval_const", + ":planned_program", + "//:auto_value", + "//common:cel_ast", + "//common/annotations", + "//common/ast", + "//common/types:type_providers", + "//runtime:evaluation_exception", + "//runtime:evaluation_exception_builder", + "//runtime:interpretable", + "//runtime:program", + "@maven//:com_google_code_findbugs_annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "planned_program", + srcs = ["PlannedProgram.java"], + deps = [ + "//:auto_value", + "//runtime:evaluation_exception", + "//runtime:function_resolver", + "//runtime:interpretable", + "//runtime:program", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "eval_const", + srcs = ["EvalConstant.java"], + deps = [ + "//common/values", + "//common/values:cel_byte_string", + "//runtime:evaluation_listener", + "//runtime:function_resolver", + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java new file mode 100644 index 000000000..ca4ff49ec --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java @@ -0,0 +1,116 @@ +// Copyright 2025 Google LLC +// +// 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 +// +// https://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 dev.cel.runtime.planner; + +import com.google.common.primitives.UnsignedLong; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.NullValue; +import dev.cel.runtime.CelEvaluationListener; +import dev.cel.runtime.CelFunctionResolver; +import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.Interpretable; + +@Immutable +final class EvalConstant implements Interpretable { + + // Pre-allocation of common constants + private static final EvalConstant NULL_VALUE = new EvalConstant(NullValue.NULL_VALUE); + private static final EvalConstant TRUE = new EvalConstant(true); + private static final EvalConstant FALSE = new EvalConstant(false); + private static final EvalConstant ZERO = new EvalConstant(0L); + private static final EvalConstant ONE = new EvalConstant(1L); + private static final EvalConstant UNSIGNED_ZERO = new EvalConstant(UnsignedLong.ZERO); + private static final EvalConstant UNSIGNED_ONE = new EvalConstant(UnsignedLong.ONE); + private static final EvalConstant EMPTY_STRING = new EvalConstant(""); + private static final EvalConstant EMPTY_BYTES = new EvalConstant(CelByteString.EMPTY); + + @SuppressWarnings("Immutable") // Known CEL constants that aren't mutated are stored + private final Object constant; + + @Override + public Object eval(GlobalResolver resolver) { + return constant; + } + + @Override + public Object eval(GlobalResolver resolver, CelEvaluationListener listener) { + return constant; + } + + @Override + public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) { + return constant; + } + + @Override + public Object eval( + GlobalResolver resolver, + CelFunctionResolver lateBoundFunctionResolver, + CelEvaluationListener listener) { + return constant; + } + + static EvalConstant create(boolean value) { + return value ? TRUE : FALSE; + } + + static EvalConstant create(String value) { + if (value.isEmpty()) { + return EMPTY_STRING; + } + + return new EvalConstant(value); + } + + static EvalConstant create(long value) { + if (value == 0L) { + return ZERO; + } else if (value == 1L) { + return ONE; + } + + return new EvalConstant(Long.valueOf(value)); + } + + static EvalConstant create(double value) { + return new EvalConstant(Double.valueOf(value)); + } + + static EvalConstant create(UnsignedLong unsignedLong) { + if (unsignedLong.longValue() == 0L) { + return UNSIGNED_ZERO; + } else if (unsignedLong.longValue() == 1L) { + return UNSIGNED_ONE; + } + + return new EvalConstant(unsignedLong); + } + + static EvalConstant create(NullValue unused) { + return NULL_VALUE; + } + + static EvalConstant create(CelByteString byteString) { + if (byteString.isEmpty()) { + return EMPTY_BYTES; + } + return new EvalConstant(byteString); + } + + private EvalConstant(Object constant) { + this.constant = constant; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java new file mode 100644 index 000000000..bc6a113cd --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java @@ -0,0 +1,50 @@ +// Copyright 2025 Google LLC +// +// 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 +// +// https://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 dev.cel.runtime.planner; + +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelFunctionResolver; +import dev.cel.runtime.GlobalResolver; +import dev.cel.runtime.Interpretable; +import dev.cel.runtime.Program; +import java.util.Map; + +@Immutable +@AutoValue +abstract class PlannedProgram implements Program { + abstract Interpretable interpretable(); + + @Override + public Object eval() throws CelEvaluationException { + return interpretable().eval(GlobalResolver.EMPTY); + } + + @Override + public Object eval(Map mapValue) throws CelEvaluationException { + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctionResolver) + throws CelEvaluationException { + throw new UnsupportedOperationException("Late bound functions not supported yet"); + } + + static Program create(Interpretable interpretable) { + return new AutoValue_PlannedProgram(interpretable); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java new file mode 100644 index 000000000..192f9791c --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -0,0 +1,103 @@ +// Copyright 2025 Google LLC +// +// 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 +// +// https://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 dev.cel.runtime.planner; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableMap; +import javax.annotation.concurrent.ThreadSafe; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.annotations.Internal; +import dev.cel.common.ast.CelConstant; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.ast.CelReference; +import dev.cel.common.types.CelType; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelEvaluationExceptionBuilder; +import dev.cel.runtime.Interpretable; +import dev.cel.runtime.Program; + +/** + * {@code ProgramPlanner} resolves functions, types, and identifiers at plan time given a + * parsed-only or a type-checked expression. + */ +@ThreadSafe +@Internal +public final class ProgramPlanner { + + /** + * Plans a {@link Program} from the provided parsed-only or type-checked {@link + * CelAbstractSyntaxTree}. + */ + public Program plan(CelAbstractSyntaxTree ast) throws CelEvaluationException { + Interpretable plannedInterpretable; + try { + plannedInterpretable = plan(ast.getExpr(), PlannerContext.create(ast)); + } catch (RuntimeException e) { + throw CelEvaluationExceptionBuilder.newBuilder(e.getMessage()).setCause(e).build(); + } + + return PlannedProgram.create(plannedInterpretable); + } + + private Interpretable plan(CelExpr celExpr, PlannerContext unused) { + switch (celExpr.getKind()) { + case CONSTANT: + return planConstant(celExpr.constant()); + case NOT_SET: + throw new UnsupportedOperationException("Unsupported kind: " + celExpr.getKind()); + default: + throw new IllegalArgumentException("Not yet implemented kind: " + celExpr.getKind()); + } + } + + private Interpretable planConstant(CelConstant celConstant) { + switch (celConstant.getKind()) { + case NULL_VALUE: + return EvalConstant.create(celConstant.nullValue()); + case BOOLEAN_VALUE: + return EvalConstant.create(celConstant.booleanValue()); + case INT64_VALUE: + return EvalConstant.create(celConstant.int64Value()); + case UINT64_VALUE: + return EvalConstant.create(celConstant.uint64Value()); + case DOUBLE_VALUE: + return EvalConstant.create(celConstant.doubleValue()); + case STRING_VALUE: + return EvalConstant.create(celConstant.stringValue()); + case BYTES_VALUE: + return EvalConstant.create(celConstant.bytesValue()); + default: + throw new IllegalStateException("Unsupported kind: " + celConstant.getKind()); + } + } + + @AutoValue + abstract static class PlannerContext { + + abstract ImmutableMap referenceMap(); + + abstract ImmutableMap typeMap(); + + private static PlannerContext create(CelAbstractSyntaxTree ast) { + return new AutoValue_ProgramPlanner_PlannerContext(ast.getReferenceMap(), ast.getTypeMap()); + } + } + + public static ProgramPlanner newPlanner() { + return new ProgramPlanner(); + } + + private ProgramPlanner() {} +} diff --git a/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel new file mode 100644 index 000000000..08c7a7a7b --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel @@ -0,0 +1,42 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:testing.bzl", "junit4_test_suites") + +package( + default_applicable_licenses = ["//:license"], + default_testonly = True, +) + +java_library( + name = "tests", + testonly = 1, + srcs = glob( + ["*.java"], + ), + deps = [ + "//:java_truth", + "//common:cel_ast", + "//common:cel_source", + "//common/ast", + "//common/values", + "//common/values:cel_byte_string", + "//compiler", + "//compiler:compiler_builder", + "//runtime", + "//runtime:program", + "//runtime/planner:program_planner", + "@maven//:com_google_guava_guava", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +junit4_test_suites( + name = "test_suites", + sizes = [ + "small", + ], + src_dir = "src/test/java", + deps = [ + ":tests", + ], +) diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java new file mode 100644 index 000000000..4dbb181b7 --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -0,0 +1,92 @@ +// Copyright 2025 Google LLC +// +// 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 +// +// https://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 dev.cel.runtime.planner; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertThrows; + +import com.google.common.primitives.UnsignedLong; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelSource; +import dev.cel.common.ast.CelExpr; +import dev.cel.common.values.CelByteString; +import dev.cel.common.values.NullValue; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.Program; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class ProgramPlannerTest { + private static final ProgramPlanner PLANNER = ProgramPlanner.newPlanner(); + private static final CelCompiler CEL_COMPILER = + CelCompilerFactory.standardCelCompilerBuilder().build(); + + @TestParameter boolean isParseOnly; + + @Test + public void plan_notSet_throws() { + CelAbstractSyntaxTree invalidAst = + CelAbstractSyntaxTree.newParsedAst(CelExpr.ofNotSet(0L), CelSource.newBuilder().build()); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> PLANNER.plan(invalidAst)); + + assertThat(e).hasMessageThat().contains("evaluation error: Unsupported kind: NOT_SET"); + } + + @Test + public void plan_constant(@TestParameter ConstantTestCase testCase) throws Exception { + CelAbstractSyntaxTree ast = compile(testCase.expression); + Program program = PLANNER.plan(ast); + + Object result = program.eval(); + + assertThat(result).isEqualTo(testCase.expected); + } + + private CelAbstractSyntaxTree compile(String expression) throws Exception { + CelAbstractSyntaxTree ast = CEL_COMPILER.parse(expression).getAst(); + if (isParseOnly) { + return ast; + } + + return CEL_COMPILER.check(ast).getAst(); + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum ConstantTestCase { + NULL("null", NullValue.NULL_VALUE), + BOOLEAN("true", true), + INT64("42", 42L), + UINT64("42u", UnsignedLong.valueOf(42)), + DOUBLE("1.5", 1.5d), + STRING("'hello world'", "hello world"), + BYTES("b'abc'", CelByteString.of("abc".getBytes(UTF_8))); + + private final String expression; + private final Object expected; + + ConstantTestCase(String expression, Object expected) { + this.expression = expression; + this.expected = expected; + } + } +}