diff --git a/common/exceptions/BUILD.bazel b/common/exceptions/BUILD.bazel new file mode 100644 index 000000000..6ffb2dcb9 --- /dev/null +++ b/common/exceptions/BUILD.bazel @@ -0,0 +1,42 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//:internal"], +) + +java_library( + name = "attribute_not_found", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:attribute_not_found"], +) + +java_library( + name = "divide_by_zero", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:divide_by_zero"], +) + +java_library( + name = "index_out_of_bounds", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:index_out_of_bounds"], +) + +java_library( + name = "bad_format", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:bad_format"], +) + +java_library( + name = "numeric_overflow", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:numeric_overflow"], +) + +java_library( + name = "invalid_argument", + # used_by_android + exports = ["//common/src/main/java/dev/cel/common/exceptions:invalid_argument"], +) diff --git a/common/src/main/java/dev/cel/common/BUILD.bazel b/common/src/main/java/dev/cel/common/BUILD.bazel index 7cd10f392..a5fffe049 100644 --- a/common/src/main/java/dev/cel/common/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/BUILD.bazel @@ -205,7 +205,6 @@ java_library( ], deps = [ ":cel_source", - "//:auto_value", "//common/ast:mutable_expr", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", @@ -249,7 +248,6 @@ java_library( ":source", ":source_location", "//:auto_value", - "//common/annotations", "//common/ast", "//common/internal", "@maven//:com_google_errorprone_error_prone_annotations", @@ -360,8 +358,5 @@ java_library( srcs = ["Operator.java"], tags = [ ], - deps = [ - "//common/ast", - "@maven//:com_google_guava_guava", - ], + deps = ["@maven//:com_google_guava_guava"], ) diff --git a/common/src/main/java/dev/cel/common/CelException.java b/common/src/main/java/dev/cel/common/CelException.java index 9d80a9ba1..55c8623a4 100644 --- a/common/src/main/java/dev/cel/common/CelException.java +++ b/common/src/main/java/dev/cel/common/CelException.java @@ -27,11 +27,6 @@ public CelException(String message, Throwable cause) { super(message, cause); } - public CelException(String message, CelErrorCode errorCode) { - super(message); - this.errorCode = errorCode; - } - public CelException(String message, Throwable cause, CelErrorCode errorCode) { super(message, cause); this.errorCode = errorCode; diff --git a/common/src/main/java/dev/cel/common/CelRuntimeException.java b/common/src/main/java/dev/cel/common/CelRuntimeException.java index 6f194c474..3856d77f5 100644 --- a/common/src/main/java/dev/cel/common/CelRuntimeException.java +++ b/common/src/main/java/dev/cel/common/CelRuntimeException.java @@ -20,15 +20,17 @@ * Wrapper for an unchecked runtime exception with a CelErrorCode supplied. * *

Note: This is not to be confused with the notion of CEL Runtime. Use {@code - * CelEvaluationException} instead to signify an evaluation error. - * - *

TODO: Make this class abstract and define specific exception classes that - * corresponds to the CelErrorCode. + * CelEvaluationException} instead to signify an evaluation error. corresponds to the CelErrorCode. */ @Internal public class CelRuntimeException extends RuntimeException { private final CelErrorCode errorCode; + public CelRuntimeException(String errorMessage, CelErrorCode errorCode) { + super(errorMessage); + this.errorCode = errorCode; + } + public CelRuntimeException(Throwable cause, CelErrorCode errorCode) { super(cause); this.errorCode = errorCode; diff --git a/common/src/main/java/dev/cel/common/exceptions/BUILD.bazel b/common/src/main/java/dev/cel/common/exceptions/BUILD.bazel new file mode 100644 index 000000000..6bd1ad9ca --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/BUILD.bazel @@ -0,0 +1,87 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = [ + "//common/exceptions:__pkg__", + "//publish:__pkg__", + ], +) + +java_library( + name = "attribute_not_found", + srcs = ["CelAttributeNotFoundException.java"], + # used_by_android + tags = [ + ], + deps = [ + "//common:error_codes", + "//common:runtime_exception", + "//common/annotations", + ], +) + +java_library( + name = "divide_by_zero", + srcs = ["CelDivideByZeroException.java"], + # used_by_android + tags = [ + ], + deps = [ + "//common:error_codes", + "//common:runtime_exception", + "//common/annotations", + ], +) + +java_library( + name = "index_out_of_bounds", + srcs = ["CelIndexOutOfBoundsException.java"], + # used_by_android + tags = [ + ], + deps = [ + "//common:error_codes", + "//common:runtime_exception", + "//common/annotations", + ], +) + +java_library( + name = "bad_format", + srcs = ["CelBadFormatException.java"], + # used_by_android + tags = [ + ], + deps = [ + "//common:error_codes", + "//common:runtime_exception", + "//common/annotations", + ], +) + +java_library( + name = "numeric_overflow", + srcs = ["CelNumericOverflowException.java"], + # used_by_android + tags = [ + ], + deps = [ + "//common:error_codes", + "//common:runtime_exception", + "//common/annotations", + ], +) + +java_library( + name = "invalid_argument", + srcs = ["CelInvalidArgumentException.java"], + # used_by_android + tags = [ + ], + deps = [ + "//common:error_codes", + "//common:runtime_exception", + "//common/annotations", + ], +) diff --git a/common/src/main/java/dev/cel/common/exceptions/CelAttributeNotFoundException.java b/common/src/main/java/dev/cel/common/exceptions/CelAttributeNotFoundException.java new file mode 100644 index 000000000..4aa43693d --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/CelAttributeNotFoundException.java @@ -0,0 +1,57 @@ +// 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.common.exceptions; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelRuntimeException; +import dev.cel.common.annotations.Internal; +import java.util.Arrays; +import java.util.Collection; + +/** Indicates an attempt to access a map or object using an invalid attribute or key. */ +@Internal +public final class CelAttributeNotFoundException extends CelRuntimeException { + + public static CelAttributeNotFoundException of(String message) { + return new CelAttributeNotFoundException(message); + } + + public static CelAttributeNotFoundException forMissingMapKey(String key) { + return new CelAttributeNotFoundException(String.format("key '%s' is not present in map.", key)); + } + + public static CelAttributeNotFoundException forFieldResolution(String... fields) { + return forFieldResolution(Arrays.asList(fields)); + } + + public static CelAttributeNotFoundException forFieldResolution(Collection fields) { + return new CelAttributeNotFoundException(formatErrorMessage(fields)); + } + + private static String formatErrorMessage(Collection fields) { + String maybePlural = ""; + if (fields.size() > 1) { + maybePlural = "s"; + } + + return String.format( + "Error resolving field%s '%s'. Field selections must be performed on messages or maps.", + maybePlural, String.join(", ", fields)); + } + + private CelAttributeNotFoundException(String message) { + super(message, CelErrorCode.ATTRIBUTE_NOT_FOUND); + } +} diff --git a/common/src/main/java/dev/cel/common/exceptions/CelBadFormatException.java b/common/src/main/java/dev/cel/common/exceptions/CelBadFormatException.java new file mode 100644 index 000000000..92dde0101 --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/CelBadFormatException.java @@ -0,0 +1,32 @@ +// 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.common.exceptions; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelRuntimeException; +import dev.cel.common.annotations.Internal; + +/** Indicates that a data conversion failed due to a mismatch in the format specification. */ +@Internal +public final class CelBadFormatException extends CelRuntimeException { + + public CelBadFormatException(Throwable cause) { + super(cause, CelErrorCode.BAD_FORMAT); + } + + public CelBadFormatException(String errorMessage) { + super(errorMessage, CelErrorCode.BAD_FORMAT); + } +} diff --git a/common/src/main/java/dev/cel/common/exceptions/CelDivideByZeroException.java b/common/src/main/java/dev/cel/common/exceptions/CelDivideByZeroException.java new file mode 100644 index 000000000..d433d804c --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/CelDivideByZeroException.java @@ -0,0 +1,32 @@ +// 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.common.exceptions; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelRuntimeException; +import dev.cel.common.annotations.Internal; + +/** Indicates that a division by zero occurred. */ +@Internal +public final class CelDivideByZeroException extends CelRuntimeException { + + public CelDivideByZeroException() { + super("/ by zero", CelErrorCode.DIVIDE_BY_ZERO); + } + + public CelDivideByZeroException(Throwable cause) { + super(cause, CelErrorCode.DIVIDE_BY_ZERO); + } +} diff --git a/common/src/main/java/dev/cel/common/exceptions/CelIndexOutOfBoundsException.java b/common/src/main/java/dev/cel/common/exceptions/CelIndexOutOfBoundsException.java new file mode 100644 index 000000000..3c2d0d03a --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/CelIndexOutOfBoundsException.java @@ -0,0 +1,28 @@ +// 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.common.exceptions; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelRuntimeException; +import dev.cel.common.annotations.Internal; + +/** Indicates that a list index access was attempted using an index that is out of bounds. */ +@Internal +public final class CelIndexOutOfBoundsException extends CelRuntimeException { + + public CelIndexOutOfBoundsException(Object index) { + super("Index out of bounds: " + index, CelErrorCode.INDEX_OUT_OF_BOUNDS); + } +} diff --git a/common/src/main/java/dev/cel/common/exceptions/CelInvalidArgumentException.java b/common/src/main/java/dev/cel/common/exceptions/CelInvalidArgumentException.java new file mode 100644 index 000000000..5bbaf6cab --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/CelInvalidArgumentException.java @@ -0,0 +1,28 @@ +// 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.common.exceptions; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelRuntimeException; +import dev.cel.common.annotations.Internal; + +/** Indicates that an invalid argument was supplied to a function. */ +@Internal +public final class CelInvalidArgumentException extends CelRuntimeException { + + public CelInvalidArgumentException(Throwable cause) { + super(cause, CelErrorCode.INVALID_ARGUMENT); + } +} diff --git a/common/src/main/java/dev/cel/common/exceptions/CelNumericOverflowException.java b/common/src/main/java/dev/cel/common/exceptions/CelNumericOverflowException.java new file mode 100644 index 000000000..439d1348f --- /dev/null +++ b/common/src/main/java/dev/cel/common/exceptions/CelNumericOverflowException.java @@ -0,0 +1,35 @@ +// 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.common.exceptions; + +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelRuntimeException; +import dev.cel.common.annotations.Internal; + +/** + * Indicates that a numeric overflow occurred due to arithmetic operations or conversions resulting + * in a value outside the representable range. + */ +@Internal +public final class CelNumericOverflowException extends CelRuntimeException { + + public CelNumericOverflowException(String message) { + super(message, CelErrorCode.NUMERIC_OVERFLOW); + } + + public CelNumericOverflowException(Throwable cause) { + super(cause, CelErrorCode.NUMERIC_OVERFLOW); + } +} diff --git a/common/src/main/java/dev/cel/common/values/CelValueConverter.java b/common/src/main/java/dev/cel/common/values/CelValueConverter.java index 3ab68c80e..b58790eaf 100644 --- a/common/src/main/java/dev/cel/common/values/CelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/CelValueConverter.java @@ -72,8 +72,6 @@ public Object toRuntimeValue(Object value) { .map(this::toRuntimeValue) .map(OptionalValue::create) .orElse(OptionalValue.EMPTY); - } else if (value instanceof Exception) { - return ErrorValue.create((Exception) value); } return normalizePrimitive(value); diff --git a/common/src/main/java/dev/cel/common/values/ErrorValue.java b/common/src/main/java/dev/cel/common/values/ErrorValue.java index 818f86850..6bc04cda4 100644 --- a/common/src/main/java/dev/cel/common/values/ErrorValue.java +++ b/common/src/main/java/dev/cel/common/values/ErrorValue.java @@ -33,6 +33,8 @@ "Immutable") // Exception is technically not immutable as the stacktrace is malleable. public abstract class ErrorValue extends CelValue { + public abstract long exprId(); + @Override public abstract Exception value(); @@ -46,7 +48,7 @@ public CelType celType() { return SimpleType.ERROR; } - public static ErrorValue create(Exception value) { - return new AutoValue_ErrorValue(value); + public static ErrorValue create(long exprId, Exception value) { + return new AutoValue_ErrorValue(exprId, value); } } diff --git a/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java b/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java index e4d767ef4..308d7b510 100644 --- a/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java +++ b/common/src/test/java/dev/cel/common/values/CelValueConverterTest.java @@ -34,15 +34,6 @@ public void toRuntimeValue_optionalValue() { assertThat(optionalValue).isEqualTo(OptionalValue.create("test")); } - @Test - public void toRuntimeValue_errorValue() { - IllegalArgumentException e = new IllegalArgumentException("error"); - - ErrorValue errorValue = (ErrorValue) CEL_VALUE_CONVERTER.toRuntimeValue(e); - - assertThat(errorValue.value()).isEqualTo(e); - } - @Test @SuppressWarnings("unchecked") // Test only public void unwrap_optionalValue() { diff --git a/common/src/test/java/dev/cel/common/values/ErrorValueTest.java b/common/src/test/java/dev/cel/common/values/ErrorValueTest.java index 0db711f72..a6a4edb66 100644 --- a/common/src/test/java/dev/cel/common/values/ErrorValueTest.java +++ b/common/src/test/java/dev/cel/common/values/ErrorValueTest.java @@ -27,7 +27,7 @@ public class ErrorValueTest { @Test public void errorValue_construct() { IllegalArgumentException exception = new IllegalArgumentException("test"); - ErrorValue opaqueValue = ErrorValue.create(exception); + ErrorValue opaqueValue = ErrorValue.create(0L, exception); assertThat(opaqueValue.value()).isEqualTo(exception); assertThat(opaqueValue.isZeroValue()).isFalse(); @@ -35,12 +35,12 @@ public void errorValue_construct() { @Test public void create_nullValue_throws() { - assertThrows(NullPointerException.class, () -> ErrorValue.create(null)); + assertThrows(NullPointerException.class, () -> ErrorValue.create(0L, null)); } @Test public void celTypeTest() { - ErrorValue value = ErrorValue.create(new IllegalArgumentException("test")); + ErrorValue value = ErrorValue.create(0L, new IllegalArgumentException("test")); assertThat(value.celType()).isEqualTo(SimpleType.ERROR); } diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index f3b60d4d7..7760d96b8 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -11,10 +11,10 @@ java_library( exports = [ ":evaluation_exception", ":late_function_binding", + ":metadata", "//runtime/src/main/java/dev/cel/runtime", "//runtime/src/main/java/dev/cel/runtime:descriptor_message_provider", "//runtime/src/main/java/dev/cel/runtime:function_overload", - "//runtime/src/main/java/dev/cel/runtime:metadata", "//runtime/src/main/java/dev/cel/runtime:runtime_type_provider", ], ) @@ -249,3 +249,9 @@ cel_android_library( name = "program_android", exports = ["//runtime/src/main/java/dev/cel/runtime:program_android"], ) + +java_library( + name = "metadata", + visibility = ["//:internal"], + exports = ["//runtime/src/main/java/dev/cel/runtime:metadata"], +) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel index 3ad0c0173..e7d7b8bdd 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel @@ -14,6 +14,7 @@ java_library( ], deps = [ ":attribute", + ":error_metadata", ":eval_and", ":eval_attribute", ":eval_conditional", @@ -25,6 +26,7 @@ java_library( ":eval_unary", ":eval_var_args_call", ":eval_zero_arity", + ":planned_interpretable", ":planned_program", "//:auto_value", "//common:cel_ast", @@ -51,6 +53,9 @@ java_library( name = "planned_program", srcs = ["PlannedProgram.java"], deps = [ + ":error_metadata", + ":planned_interpretable", + ":strict_error_exception", "//:auto_value", "//common:runtime_exception", "//common/values", @@ -68,6 +73,7 @@ java_library( name = "eval_const", srcs = ["EvalConstant.java"], deps = [ + ":planned_interpretable", "//common/values", "//common/values:cel_byte_string", "//runtime:evaluation_listener", @@ -99,6 +105,7 @@ java_library( srcs = ["EvalAttribute.java"], deps = [ ":attribute", + ":planned_interpretable", "//runtime:evaluation_listener", "//runtime:function_resolver", "//runtime:interpretable", @@ -111,6 +118,7 @@ java_library( name = "eval_zero_arity", srcs = ["EvalZeroArity.java"], deps = [ + ":planned_interpretable", "//runtime:evaluation_exception", "//runtime:evaluation_listener", "//runtime:function_resolver", @@ -123,6 +131,8 @@ java_library( name = "eval_unary", srcs = ["EvalUnary.java"], deps = [ + ":eval_helpers", + ":planned_interpretable", "//runtime:evaluation_exception", "//runtime:evaluation_listener", "//runtime:function_resolver", @@ -135,6 +145,8 @@ java_library( name = "eval_var_args_call", srcs = ["EvalVarArgsCall.java"], deps = [ + ":eval_helpers", + ":planned_interpretable", "//runtime:evaluation_exception", "//runtime:evaluation_listener", "//runtime:function_resolver", @@ -148,6 +160,7 @@ java_library( srcs = ["EvalOr.java"], deps = [ ":eval_helpers", + ":planned_interpretable", "//common/values", "//runtime:evaluation_listener", "//runtime:function_resolver", @@ -161,6 +174,7 @@ java_library( srcs = ["EvalAnd.java"], deps = [ ":eval_helpers", + ":planned_interpretable", "//common/values", "//runtime:evaluation_listener", "//runtime:function_resolver", @@ -173,6 +187,7 @@ java_library( name = "eval_conditional", srcs = ["EvalConditional.java"], deps = [ + ":planned_interpretable", "//runtime:evaluation_exception", "//runtime:evaluation_listener", "//runtime:function_resolver", @@ -185,6 +200,7 @@ java_library( name = "eval_create_struct", srcs = ["EvalCreateStruct.java"], deps = [ + ":planned_interpretable", "//common/types", "//common/values", "//common/values:cel_value_provider", @@ -201,6 +217,7 @@ java_library( name = "eval_create_list", srcs = ["EvalCreateList.java"], deps = [ + ":planned_interpretable", "//runtime:evaluation_exception", "//runtime:evaluation_listener", "//runtime:function_resolver", @@ -214,6 +231,7 @@ java_library( name = "eval_create_map", srcs = ["EvalCreateMap.java"], deps = [ + ":planned_interpretable", "//runtime:evaluation_exception", "//runtime:evaluation_listener", "//runtime:function_resolver", @@ -227,7 +245,39 @@ java_library( name = "eval_helpers", srcs = ["EvalHelpers.java"], deps = [ + ":planned_interpretable", + ":strict_error_exception", + "//common:error_codes", + "//common:runtime_exception", "//common/values", "//runtime:interpretable", ], ) + +java_library( + name = "strict_error_exception", + srcs = ["StrictErrorException.java"], + deps = [ + "//common:error_codes", + "//common:runtime_exception", + ], +) + +java_library( + name = "error_metadata", + srcs = ["ErrorMetadata.java"], + deps = [ + "//runtime:metadata", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "planned_interpretable", + srcs = ["PlannedInterpretable.java"], + deps = [ + "//runtime:interpretable", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ErrorMetadata.java b/runtime/src/main/java/dev/cel/runtime/planner/ErrorMetadata.java new file mode 100644 index 000000000..2358bd0f9 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/ErrorMetadata.java @@ -0,0 +1,52 @@ +// 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.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.Metadata; + +@Immutable +final class ErrorMetadata implements Metadata { + + private final ImmutableMap exprIdToPositionMap; + private final String location; + + @Override + public String getLocation() { + return location; + } + + @Override + public int getPosition(long exprId) { + return exprIdToPositionMap.getOrDefault(exprId, 0); + } + + @Override + public boolean hasPosition(long exprId) { + return exprIdToPositionMap.containsKey(exprId); + } + + static ErrorMetadata create(ImmutableMap exprIdToPositionMap, String location) { + return new ErrorMetadata(exprIdToPositionMap, location); + } + + private ErrorMetadata(ImmutableMap exprIdToPositionMap, String location) { + this.exprIdToPositionMap = checkNotNull(exprIdToPositionMap); + this.location = checkNotNull(location); + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java index 4bf9af517..a3a39ce8a 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAnd.java @@ -21,17 +21,16 @@ import dev.cel.runtime.CelEvaluationListener; import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.GlobalResolver; -import dev.cel.runtime.Interpretable; -final class EvalAnd implements Interpretable { +final class EvalAnd extends PlannedInterpretable { @SuppressWarnings("Immutable") - private final Interpretable[] args; + private final PlannedInterpretable[] args; @Override public Object eval(GlobalResolver resolver) { ErrorValue errorValue = null; - for (Interpretable arg : args) { + for (PlannedInterpretable arg : args) { Object argVal = evalNonstrictly(arg, resolver); if (argVal instanceof Boolean) { // Short-circuit on false @@ -75,11 +74,12 @@ public Object eval( throw new UnsupportedOperationException("Not yet supported"); } - static EvalAnd create(Interpretable[] args) { - return new EvalAnd(args); + static EvalAnd create(long exprId, PlannedInterpretable[] args) { + return new EvalAnd(exprId, args); } - private EvalAnd(Interpretable[] args) { + private EvalAnd(long exprId, PlannedInterpretable[] args) { + super(exprId); Preconditions.checkArgument(args.length == 2); this.args = args; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java index 23b7fa63c..6e20d4f59 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalAttribute.java @@ -18,10 +18,9 @@ import dev.cel.runtime.CelEvaluationListener; import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.GlobalResolver; -import dev.cel.runtime.Interpretable; @Immutable -final class EvalAttribute implements Interpretable { +final class EvalAttribute extends PlannedInterpretable { private final Attribute attr; @@ -47,15 +46,15 @@ public Object eval( GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver, CelEvaluationListener listener) { - // TODO: Implement support throw new UnsupportedOperationException("Not yet supported"); } - static EvalAttribute create(Attribute attr) { - return new EvalAttribute(attr); + static EvalAttribute create(long exprId, Attribute attr) { + return new EvalAttribute(exprId, attr); } - private EvalAttribute(Attribute attr) { + private EvalAttribute(long exprId, Attribute attr) { + super(exprId); this.attr = attr; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java index ca32b405c..4445d3e71 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalConditional.java @@ -21,7 +21,7 @@ import dev.cel.runtime.GlobalResolver; import dev.cel.runtime.Interpretable; -final class EvalConditional implements Interpretable { +final class EvalConditional extends PlannedInterpretable { @SuppressWarnings("Immutable") private final Interpretable[] args; @@ -67,11 +67,12 @@ public Object eval( throw new UnsupportedOperationException("Not yet supported"); } - static EvalConditional create(Interpretable[] args) { - return new EvalConditional(args); + static EvalConditional create(long exprId, Interpretable[] args) { + return new EvalConditional(exprId, args); } - private EvalConditional(Interpretable[] args) { + private EvalConditional(long exprId, Interpretable[] args) { + super(exprId); Preconditions.checkArgument(args.length == 3); this.args = args; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java index a1a2dc998..408d04046 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalConstant.java @@ -21,10 +21,9 @@ 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 { +final class EvalConstant extends PlannedInterpretable { // Pre-allocation of common constants private static final EvalConstant NULL_VALUE = new EvalConstant(NullValue.NULL_VALUE); @@ -115,6 +114,7 @@ static EvalConstant create(Object value) { } private EvalConstant(Object constant) { + super(/* exprId= */ -1); // It's not possible to throw while evaluating a constant this.constant = constant; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java index 6a1917475..4ec275eef 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateList.java @@ -23,7 +23,7 @@ import dev.cel.runtime.Interpretable; @Immutable -final class EvalCreateList implements Interpretable { +final class EvalCreateList extends PlannedInterpretable { // Array contents are not mutated @SuppressWarnings("Immutable") @@ -51,17 +51,20 @@ public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctio } @Override - public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver, + public Object eval( + GlobalResolver resolver, + CelFunctionResolver lateBoundFunctionResolver, CelEvaluationListener listener) { // TODO: Implement support throw new UnsupportedOperationException("Not yet supported"); } - static EvalCreateList create(Interpretable[] values) { - return new EvalCreateList(values); + static EvalCreateList create(long exprId, Interpretable[] values) { + return new EvalCreateList(exprId, values); } - private EvalCreateList(Interpretable[] values) { + private EvalCreateList(long exprId, Interpretable[] values) { + super(exprId); this.values = values; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java index a3fb2cb85..38d690303 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateMap.java @@ -24,7 +24,7 @@ import dev.cel.runtime.Interpretable; @Immutable -final class EvalCreateMap implements Interpretable { +final class EvalCreateMap extends PlannedInterpretable { // Array contents are not mutated @SuppressWarnings("Immutable") @@ -58,18 +58,20 @@ public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctio } @Override - public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver, + public Object eval( + GlobalResolver resolver, + CelFunctionResolver lateBoundFunctionResolver, CelEvaluationListener listener) { // TODO: Implement support throw new UnsupportedOperationException("Not yet supported"); } - - static EvalCreateMap create(Interpretable[] keys, Interpretable[] values) { - return new EvalCreateMap(keys, values); + static EvalCreateMap create(long exprId, Interpretable[] keys, Interpretable[] values) { + return new EvalCreateMap(exprId, keys, values); } - private EvalCreateMap(Interpretable[] keys, Interpretable[] values) { + private EvalCreateMap(long exprId, Interpretable[] keys, Interpretable[] values) { + super(exprId); Preconditions.checkArgument(keys.length == values.length); this.keys = keys; this.values = values; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java index 5c1f1d77b..7553add80 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalCreateStruct.java @@ -28,7 +28,7 @@ import java.util.Map; @Immutable -final class EvalCreateStruct implements Interpretable { +final class EvalCreateStruct extends PlannedInterpretable { private final CelValueProvider valueProvider; private final StructType structType; @@ -84,18 +84,21 @@ public Object eval( } static EvalCreateStruct create( + long exprId, CelValueProvider valueProvider, StructType structType, String[] keys, Interpretable[] values) { - return new EvalCreateStruct(valueProvider, structType, keys, values); + return new EvalCreateStruct(exprId, valueProvider, structType, keys, values); } private EvalCreateStruct( + long exprId, CelValueProvider valueProvider, StructType structType, String[] keys, Interpretable[] values) { + super(exprId); this.valueProvider = valueProvider; this.structType = structType; this.keys = keys; diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java index 82bd6124a..3b5bda1bc 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalHelpers.java @@ -14,17 +14,33 @@ package dev.cel.runtime.planner; +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelRuntimeException; import dev.cel.common.values.ErrorValue; import dev.cel.runtime.GlobalResolver; -import dev.cel.runtime.Interpretable; final class EvalHelpers { - static Object evalNonstrictly(Interpretable interpretable, GlobalResolver resolver) { + static Object evalNonstrictly(PlannedInterpretable interpretable, GlobalResolver resolver) { try { return interpretable.eval(resolver); + } catch (StrictErrorException e) { + // Intercept the strict exception to get a more localized expr ID for error reporting purposes + // Example: foo [1] && strict_err [2] -> ID 2 is propagated. + return ErrorValue.create(e.exprId(), e); } catch (Exception e) { - return ErrorValue.create(e); + return ErrorValue.create(interpretable.exprId(), e); + } + } + + static Object evalStrictly(PlannedInterpretable interpretable, GlobalResolver resolver) { + try { + return interpretable.eval(resolver); + } catch (CelRuntimeException e) { + throw new StrictErrorException(e, interpretable.exprId()); + } catch (Exception e) { + throw new StrictErrorException( + e.getCause(), CelErrorCode.INTERNAL_ERROR, interpretable.exprId()); } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java index afa02dfb8..f287bdd59 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalOr.java @@ -21,17 +21,16 @@ import dev.cel.runtime.CelEvaluationListener; import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.GlobalResolver; -import dev.cel.runtime.Interpretable; -final class EvalOr implements Interpretable { +final class EvalOr extends PlannedInterpretable { @SuppressWarnings("Immutable") - private final Interpretable[] args; + private final PlannedInterpretable[] args; @Override public Object eval(GlobalResolver resolver) { ErrorValue errorValue = null; - for (Interpretable arg : args) { + for (PlannedInterpretable arg : args) { Object argVal = evalNonstrictly(arg, resolver); if (argVal instanceof Boolean) { // Short-circuit on true @@ -75,11 +74,12 @@ public Object eval( throw new UnsupportedOperationException("Not yet supported"); } - static EvalOr create(Interpretable[] args) { - return new EvalOr(args); + static EvalOr create(long exprId, PlannedInterpretable[] args) { + return new EvalOr(exprId, args); } - private EvalOr(Interpretable[] args) { + private EvalOr(long exprId, PlannedInterpretable[] args) { + super(exprId); Preconditions.checkArgument(args.length == 2); this.args = args; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java index c6daff4b2..13b59d11e 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalUnary.java @@ -14,21 +14,24 @@ package dev.cel.runtime.planner; +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; +import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; + import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelEvaluationListener; import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.GlobalResolver; -import dev.cel.runtime.Interpretable; -final class EvalUnary implements Interpretable { +final class EvalUnary extends PlannedInterpretable { private final CelResolvedOverload resolvedOverload; - private final Interpretable arg; + private final PlannedInterpretable arg; @Override public Object eval(GlobalResolver resolver) throws CelEvaluationException { - Object argVal = arg.eval(resolver); + Object argVal = + resolvedOverload.isStrict() ? evalStrictly(arg, resolver) : evalNonstrictly(arg, resolver); Object[] arguments = new Object[] {argVal}; return resolvedOverload.getDefinition().apply(arguments); @@ -55,11 +58,13 @@ public Object eval( throw new UnsupportedOperationException("Not yet supported"); } - static EvalUnary create(CelResolvedOverload resolvedOverload, Interpretable arg) { - return new EvalUnary(resolvedOverload, arg); + static EvalUnary create( + long exprId, CelResolvedOverload resolvedOverload, PlannedInterpretable arg) { + return new EvalUnary(exprId, resolvedOverload, arg); } - private EvalUnary(CelResolvedOverload resolvedOverload, Interpretable arg) { + private EvalUnary(long exprId, CelResolvedOverload resolvedOverload, PlannedInterpretable arg) { + super(exprId); this.resolvedOverload = resolvedOverload; this.arg = arg; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java index 48fc7ba04..a2a4c0acc 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalVarArgsCall.java @@ -14,25 +14,30 @@ package dev.cel.runtime.planner; +import static dev.cel.runtime.planner.EvalHelpers.evalNonstrictly; +import static dev.cel.runtime.planner.EvalHelpers.evalStrictly; + import dev.cel.runtime.CelEvaluationException; import dev.cel.runtime.CelEvaluationListener; import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.GlobalResolver; -import dev.cel.runtime.Interpretable; @SuppressWarnings("Immutable") -final class EvalVarArgsCall implements Interpretable { +final class EvalVarArgsCall extends PlannedInterpretable { private final CelResolvedOverload resolvedOverload; - private final Interpretable[] args; + private final PlannedInterpretable[] args; @Override public Object eval(GlobalResolver resolver) throws CelEvaluationException { Object[] argVals = new Object[args.length]; for (int i = 0; i < args.length; i++) { - Interpretable arg = args[i]; - argVals[i] = arg.eval(resolver); + PlannedInterpretable arg = args[i]; + argVals[i] = + resolvedOverload.isStrict() + ? evalStrictly(arg, resolver) + : evalNonstrictly(arg, resolver); } return resolvedOverload.getDefinition().apply(argVals); @@ -59,11 +64,14 @@ public Object eval( throw new UnsupportedOperationException("Not yet supported"); } - static EvalVarArgsCall create(CelResolvedOverload resolvedOverload, Interpretable[] args) { - return new EvalVarArgsCall(resolvedOverload, args); + static EvalVarArgsCall create( + long exprId, CelResolvedOverload resolvedOverload, PlannedInterpretable[] args) { + return new EvalVarArgsCall(exprId, resolvedOverload, args); } - private EvalVarArgsCall(CelResolvedOverload resolvedOverload, Interpretable[] args) { + private EvalVarArgsCall( + long exprId, CelResolvedOverload resolvedOverload, PlannedInterpretable[] args) { + super(exprId); this.resolvedOverload = resolvedOverload; this.args = args; } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java b/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java index 813b84629..628e4a70f 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/EvalZeroArity.java @@ -19,10 +19,8 @@ import dev.cel.runtime.CelFunctionResolver; import dev.cel.runtime.CelResolvedOverload; import dev.cel.runtime.GlobalResolver; -import dev.cel.runtime.Interpretable; - -final class EvalZeroArity implements Interpretable { +final class EvalZeroArity extends PlannedInterpretable { private static final Object[] EMPTY_ARRAY = new Object[0]; private final CelResolvedOverload resolvedOverload; @@ -53,11 +51,12 @@ public Object eval( throw new UnsupportedOperationException("Not yet supported"); } - static EvalZeroArity create(CelResolvedOverload resolvedOverload) { - return new EvalZeroArity(resolvedOverload); + static EvalZeroArity create(long exprId, CelResolvedOverload resolvedOverload) { + return new EvalZeroArity(exprId, resolvedOverload); } - private EvalZeroArity(CelResolvedOverload resolvedOverload) { + private EvalZeroArity(long exprId, CelResolvedOverload resolvedOverload) { + super(exprId); this.resolvedOverload = resolvedOverload; } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java b/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java new file mode 100644 index 000000000..87a1a7dc4 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedInterpretable.java @@ -0,0 +1,31 @@ +// 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.errorprone.annotations.Immutable; +import dev.cel.runtime.Interpretable; + +@Immutable +abstract class PlannedInterpretable implements Interpretable { + private final long exprId; + + long exprId() { + return exprId; + } + + PlannedInterpretable(long exprId) { + this.exprId = exprId; + } +} diff --git a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java index 2c0d402c2..d1214fab0 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/PlannedProgram.java @@ -23,14 +23,15 @@ import dev.cel.runtime.CelEvaluationExceptionBuilder; 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(); + abstract PlannedInterpretable interpretable(); + + abstract ErrorMetadata metadata(); @Override public Object eval() throws CelEvaluationException { @@ -48,34 +49,36 @@ public Object eval(Map mapValue, CelFunctionResolver lateBoundFunctio throw new UnsupportedOperationException("Late bound functions not supported yet"); } - private Object evalOrThrow(Interpretable interpretable, GlobalResolver resolver) + private Object evalOrThrow(PlannedInterpretable interpretable, GlobalResolver resolver) throws CelEvaluationException { try { Object evalResult = interpretable.eval(resolver); if (evalResult instanceof ErrorValue) { ErrorValue errorValue = (ErrorValue) evalResult; - throw newCelEvaluationException(errorValue.value()); + throw newCelEvaluationException(errorValue.exprId(), errorValue.value()); } return evalResult; } catch (RuntimeException e) { - throw newCelEvaluationException(e); + throw newCelEvaluationException(interpretable.exprId(), e); } } - private static CelEvaluationException newCelEvaluationException(Exception e) { + private CelEvaluationException newCelEvaluationException(long exprId, Exception e) { CelEvaluationExceptionBuilder builder; - if (e instanceof CelRuntimeException) { + if (e instanceof StrictErrorException) { // Preserve detailed error, including error codes if one exists. + builder = CelEvaluationExceptionBuilder.newBuilder((CelRuntimeException) e.getCause()); + } else if (e instanceof CelRuntimeException) { builder = CelEvaluationExceptionBuilder.newBuilder((CelRuntimeException) e); } else { builder = CelEvaluationExceptionBuilder.newBuilder(e.getMessage()).setCause(e); } - return builder.build(); + return builder.setMetadata(metadata(), exprId).build(); } - static Program create(Interpretable interpretable) { - return new AutoValue_PlannedProgram(interpretable); + static Program create(PlannedInterpretable interpretable, ErrorMetadata metadata) { + return new AutoValue_PlannedProgram(interpretable, metadata); } } diff --git a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java index bf7729c0f..252695ed7 100644 --- a/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java +++ b/runtime/src/main/java/dev/cel/runtime/planner/ProgramPlanner.java @@ -66,17 +66,19 @@ public final class ProgramPlanner { * CelAbstractSyntaxTree}. */ public Program plan(CelAbstractSyntaxTree ast) throws CelEvaluationException { - Interpretable plannedInterpretable; + PlannedInterpretable plannedInterpretable; try { plannedInterpretable = plan(ast.getExpr(), PlannerContext.create(ast)); } catch (RuntimeException e) { throw CelEvaluationExceptionBuilder.newBuilder(e.getMessage()).setCause(e).build(); } - return PlannedProgram.create(plannedInterpretable); + ErrorMetadata errorMetadata = + ErrorMetadata.create(ast.getSource().getPositionsMap(), ast.getSource().getDescription()); + return PlannedProgram.create(plannedInterpretable, errorMetadata); } - private Interpretable plan(CelExpr celExpr, PlannerContext ctx) { + private PlannedInterpretable plan(CelExpr celExpr, PlannerContext ctx) { switch (celExpr.getKind()) { case CONSTANT: return planConstant(celExpr.constant()); @@ -97,7 +99,7 @@ private Interpretable plan(CelExpr celExpr, PlannerContext ctx) { } } - private Interpretable planConstant(CelConstant celConstant) { + private PlannedInterpretable planConstant(CelConstant celConstant) { switch (celConstant.getKind()) { case NULL_VALUE: return EvalConstant.create(celConstant.nullValue()); @@ -118,16 +120,17 @@ private Interpretable planConstant(CelConstant celConstant) { } } - private Interpretable planIdent(CelExpr celExpr, PlannerContext ctx) { + private PlannedInterpretable planIdent(CelExpr celExpr, PlannerContext ctx) { CelReference ref = ctx.referenceMap().get(celExpr.id()); if (ref != null) { return planCheckedIdent(celExpr.id(), ref, ctx.typeMap()); } - return EvalAttribute.create(attributeFactory.newMaybeAttribute(celExpr.ident().name())); + return EvalAttribute.create( + celExpr.id(), attributeFactory.newMaybeAttribute(celExpr.ident().name())); } - private Interpretable planCheckedIdent( + private PlannedInterpretable planCheckedIdent( long id, CelReference identRef, ImmutableMap typeMap) { if (identRef.value().isPresent()) { return planConstant(identRef.value().get()); @@ -146,10 +149,10 @@ private Interpretable planCheckedIdent( return EvalConstant.create(identType); } - return EvalAttribute.create(attributeFactory.newAbsoluteAttribute(identRef.name())); + return EvalAttribute.create(id, attributeFactory.newAbsoluteAttribute(identRef.name())); } - private Interpretable planCall(CelExpr expr, PlannerContext ctx) { + private PlannedInterpretable planCall(CelExpr expr, PlannerContext ctx) { ResolvedFunction resolvedFunction = resolveFunction(expr, ctx.referenceMap()); CelExpr target = resolvedFunction.target().orElse(null); int argCount = expr.call().args().size(); @@ -157,7 +160,7 @@ private Interpretable planCall(CelExpr expr, PlannerContext ctx) { argCount++; } - Interpretable[] evaluatedArgs = new Interpretable[argCount]; + PlannedInterpretable[] evaluatedArgs = new PlannedInterpretable[argCount]; int offset = 0; if (target != null) { @@ -175,11 +178,11 @@ private Interpretable planCall(CelExpr expr, PlannerContext ctx) { if (operator != null) { switch (operator) { case LOGICAL_OR: - return EvalOr.create(evaluatedArgs); + return EvalOr.create(expr.id(), evaluatedArgs); case LOGICAL_AND: - return EvalAnd.create(evaluatedArgs); + return EvalAnd.create(expr.id(), evaluatedArgs); case CONDITIONAL: - return EvalConditional.create(evaluatedArgs); + return EvalConditional.create(expr.id(), evaluatedArgs); default: // fall-through } @@ -200,15 +203,15 @@ private Interpretable planCall(CelExpr expr, PlannerContext ctx) { switch (argCount) { case 0: - return EvalZeroArity.create(resolvedOverload); + return EvalZeroArity.create(expr.id(), resolvedOverload); case 1: - return EvalUnary.create(resolvedOverload, evaluatedArgs[0]); + return EvalUnary.create(expr.id(), resolvedOverload, evaluatedArgs[0]); default: - return EvalVarArgsCall.create(resolvedOverload, evaluatedArgs); + return EvalVarArgsCall.create(expr.id(), resolvedOverload, evaluatedArgs); } } - private Interpretable planCreateStruct(CelExpr celExpr, PlannerContext ctx) { + private PlannedInterpretable planCreateStruct(CelExpr celExpr, PlannerContext ctx) { CelStruct struct = celExpr.struct(); StructType structType = resolveStructType(struct); @@ -222,28 +225,28 @@ private Interpretable planCreateStruct(CelExpr celExpr, PlannerContext ctx) { values[i] = plan(entry.value(), ctx); } - return EvalCreateStruct.create(valueProvider, structType, keys, values); + return EvalCreateStruct.create(celExpr.id(), valueProvider, structType, keys, values); } - private Interpretable planCreateList(CelExpr celExpr, PlannerContext ctx) { + private PlannedInterpretable planCreateList(CelExpr celExpr, PlannerContext ctx) { CelList list = celExpr.list(); ImmutableList elements = list.elements(); - Interpretable[] values = new Interpretable[elements.size()]; + PlannedInterpretable[] values = new PlannedInterpretable[elements.size()]; for (int i = 0; i < elements.size(); i++) { values[i] = plan(elements.get(i), ctx); } - return EvalCreateList.create(values); + return EvalCreateList.create(celExpr.id(), values); } - private Interpretable planCreateMap(CelExpr celExpr, PlannerContext ctx) { + private PlannedInterpretable planCreateMap(CelExpr celExpr, PlannerContext ctx) { CelMap map = celExpr.map(); ImmutableList entries = map.entries(); - Interpretable[] keys = new Interpretable[entries.size()]; - Interpretable[] values = new Interpretable[entries.size()]; + PlannedInterpretable[] keys = new PlannedInterpretable[entries.size()]; + PlannedInterpretable[] values = new PlannedInterpretable[entries.size()]; for (int i = 0; i < entries.size(); i++) { CelMap.Entry entry = entries.get(i); @@ -251,7 +254,7 @@ private Interpretable planCreateMap(CelExpr celExpr, PlannerContext ctx) { values[i] = plan(entry.value(), ctx); } - return EvalCreateMap.create(keys, values); + return EvalCreateMap.create(celExpr.id(), keys, values); } /** diff --git a/runtime/src/main/java/dev/cel/runtime/planner/StrictErrorException.java b/runtime/src/main/java/dev/cel/runtime/planner/StrictErrorException.java new file mode 100644 index 000000000..3acd6ff27 --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/planner/StrictErrorException.java @@ -0,0 +1,42 @@ +// 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 dev.cel.common.CelErrorCode; +import dev.cel.common.CelRuntimeException; + +/** + * An exception that's raised when a strict call failed to invoke, which includes the source of + * expression ID, along with canonical CelErrorCode. + * + *

Note that StrictErrorException should not be surfaced directly back to the user. + */ +final class StrictErrorException extends CelRuntimeException { + + private final long exprId; + + long exprId() { + return exprId; + } + + StrictErrorException(CelRuntimeException cause, long exprId) { + this(cause, cause.getErrorCode(), exprId); + } + + StrictErrorException(Throwable cause, CelErrorCode errorCode, long exprId) { + super(cause, errorCode); + this.exprId = exprId; + } +} diff --git a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java index 205e2ef8b..bb580cbb3 100644 --- a/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java +++ b/runtime/src/test/java/dev/cel/runtime/planner/ProgramPlannerTest.java @@ -421,7 +421,7 @@ public void plan_call_throws() throws Exception { Program program = PLANNER.plan(ast); CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); - assertThat(e).hasMessageThat().contains("evaluation error: Intentional error"); + assertThat(e).hasMessageThat().contains("evaluation error at :5: Intentional error"); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); } @@ -514,8 +514,8 @@ public void plan_call_logicalOr_throws(String expression) throws Exception { Program program = PLANNER.plan(ast); CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); - // TODO: Tag metadata (source loc) - assertThat(e).hasMessageThat().isEqualTo("evaluation error: / by zero"); + assertThat(e).hasMessageThat().startsWith("evaluation error at :"); + assertThat(e).hasMessageThat().endsWith("/ by zero"); assertThat(e).hasCauseThat().isInstanceOf(ArithmeticException.class); assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.DIVIDE_BY_ZERO); } @@ -546,8 +546,8 @@ public void plan_call_logicalAnd_throws(String expression) throws Exception { Program program = PLANNER.plan(ast); CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); - // TODO: Tag metadata (source loc) - assertThat(e).hasMessageThat().isEqualTo("evaluation error: / by zero"); + assertThat(e).hasMessageThat().startsWith("evaluation error at :"); + assertThat(e).hasMessageThat().endsWith("/ by zero"); assertThat(e).hasCauseThat().isInstanceOf(ArithmeticException.class); assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.DIVIDE_BY_ZERO); } @@ -576,7 +576,8 @@ public void plan_call_conditional_throws(String expression) throws Exception { Program program = PLANNER.plan(ast); CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval); - assertThat(e).hasMessageThat().isEqualTo("evaluation error: / by zero"); + assertThat(e).hasMessageThat().startsWith("evaluation error at :"); + assertThat(e).hasMessageThat().endsWith("/ by zero"); assertThat(e).hasCauseThat().isInstanceOf(ArithmeticException.class); assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.DIVIDE_BY_ZERO); }