From 0386196ae0e464f4feac8fdbc6359dcda34c974d Mon Sep 17 00:00:00 2001 From: Jonathan Tatum Date: Mon, 6 Jan 2025 18:23:03 -0800 Subject: [PATCH 1/9] Add support for quoted field selectors in java. PiperOrigin-RevId: 712716800 --- .../main/java/dev/cel/common/CelOptions.java | 13 +++++ .../src/main/java/dev/cel/parser/BUILD.bazel | 2 + .../dev/cel/parser/CelUnparserVisitor.java | 18 +++++- .../src/main/java/dev/cel/parser/Parser.java | 58 +++++++++++++++---- .../src/main/java/dev/cel/parser/gen/CEL.g4 | 36 +++++++----- .../parser/CelParserParameterizedTest.java | 27 +++++++++ .../dev/cel/parser/CelUnparserImplTest.java | 16 ++++- parser/src/test/resources/parser.baseline | 46 ++++++++++++++- .../src/test/resources/parser_errors.baseline | 40 ++++++++++++- .../compile_errors/expected_errors.baseline | 2 +- 10 files changed, 226 insertions(+), 32 deletions(-) diff --git a/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java index 3e841471f..3c07ab26d 100644 --- a/common/src/main/java/dev/cel/common/CelOptions.java +++ b/common/src/main/java/dev/cel/common/CelOptions.java @@ -69,6 +69,8 @@ public enum ProtoUnsetFieldOptions { public abstract boolean enableHiddenAccumulatorVar(); + public abstract boolean enableQuotedIdentifierSyntax(); + // Type-Checker related options public abstract boolean enableCompileTimeOverloadResolution(); @@ -191,6 +193,7 @@ public static Builder newBuilder() { .retainRepeatedUnaryOperators(false) .retainUnbalancedLogicalExpressions(false) .enableHiddenAccumulatorVar(false) + .enableQuotedIdentifierSyntax(false) // Type-Checker options .enableCompileTimeOverloadResolution(false) .enableHomogeneousLiterals(false) @@ -332,6 +335,16 @@ public abstract static class Builder { */ public abstract Builder enableHiddenAccumulatorVar(boolean value); + /** + * Enable quoted identifier syntax. + * + *

This enables the use of quoted identifier syntax when parsing CEL expressions. When + * enabled, the parser will accept identifiers that are surrounded by backticks (`) and will + * treat them as a single identifier. Currently, this is only supported for field specifiers + * over a limited character set. + */ + public abstract Builder enableQuotedIdentifierSyntax(boolean value); + // Type-Checker related options /** diff --git a/parser/src/main/java/dev/cel/parser/BUILD.bazel b/parser/src/main/java/dev/cel/parser/BUILD.bazel index 46f68b0bd..7203876e4 100644 --- a/parser/src/main/java/dev/cel/parser/BUILD.bazel +++ b/parser/src/main/java/dev/cel/parser/BUILD.bazel @@ -127,6 +127,8 @@ java_library( "//common", "//common/ast", "//common/ast:cel_expr_visitor", + "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_re2j_re2j", ], ) diff --git a/parser/src/main/java/dev/cel/parser/CelUnparserVisitor.java b/parser/src/main/java/dev/cel/parser/CelUnparserVisitor.java index 5f2f05e4d..79a6147c5 100644 --- a/parser/src/main/java/dev/cel/parser/CelUnparserVisitor.java +++ b/parser/src/main/java/dev/cel/parser/CelUnparserVisitor.java @@ -13,7 +13,9 @@ // limitations under the License. package dev.cel.parser; +import com.google.common.collect.ImmutableSet; import com.google.protobuf.ByteString; +import com.google.re2j.Pattern; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.CelSource; import dev.cel.common.ast.CelConstant; @@ -43,6 +45,10 @@ public class CelUnparserVisitor extends CelExprVisitor { protected static final String RIGHT_BRACE = "}"; protected static final String COLON = ":"; protected static final String QUESTION_MARK = "?"; + protected static final String BACKTICK = "`"; + private static final Pattern IDENTIFIER_SEGMENT_PATTERN = + Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*"); + private static final ImmutableSet RESTRICTED_FIELD_NAMES = ImmutableSet.of("in"); protected final CelAbstractSyntaxTree ast; protected final CelSource sourceInfo; @@ -60,6 +66,14 @@ public String unparse() { return stringBuilder.toString(); } + private static String maybeQuoteField(String field) { + if (RESTRICTED_FIELD_NAMES.contains(field) + || !IDENTIFIER_SEGMENT_PATTERN.matcher(field).matches()) { + return BACKTICK + field + BACKTICK; + } + return field; + } + @Override public void visit(CelExpr expr) { if (sourceInfo.getMacroCalls().containsKey(expr.id())) { @@ -191,7 +205,7 @@ protected void visit(CelExpr expr, CelStruct struct) { if (e.optionalEntry()) { stringBuilder.append(QUESTION_MARK); } - stringBuilder.append(e.fieldKey()); + stringBuilder.append(maybeQuoteField(e.fieldKey())); stringBuilder.append(COLON).append(SPACE); visit(e.value()); } @@ -263,7 +277,7 @@ private void visitSelect(CelExpr operand, boolean testOnly, String op, String fi } boolean nested = !testOnly && isBinaryOrTernaryOperator(operand); visitMaybeNested(operand, nested); - stringBuilder.append(op).append(field); + stringBuilder.append(op).append(maybeQuoteField(field)); if (testOnly) { stringBuilder.append(RIGHT_PAREN); } diff --git a/parser/src/main/java/dev/cel/parser/Parser.java b/parser/src/main/java/dev/cel/parser/Parser.java index b49f81453..12bf1bbba 100644 --- a/parser/src/main/java/dev/cel/parser/Parser.java +++ b/parser/src/main/java/dev/cel/parser/Parser.java @@ -33,10 +33,13 @@ import cel.parser.internal.CELParser.CreateMapContext; import cel.parser.internal.CELParser.CreateMessageContext; import cel.parser.internal.CELParser.DoubleContext; +import cel.parser.internal.CELParser.EscapeIdentContext; +import cel.parser.internal.CELParser.EscapedIdentifierContext; import cel.parser.internal.CELParser.ExprContext; import cel.parser.internal.CELParser.ExprListContext; import cel.parser.internal.CELParser.FieldInitializerListContext; -import cel.parser.internal.CELParser.IdentOrGlobalCallContext; +import cel.parser.internal.CELParser.GlobalCallContext; +import cel.parser.internal.CELParser.IdentContext; import cel.parser.internal.CELParser.IndexContext; import cel.parser.internal.CELParser.IntContext; import cel.parser.internal.CELParser.ListInitContext; @@ -52,6 +55,7 @@ import cel.parser.internal.CELParser.PrimaryExprContext; import cel.parser.internal.CELParser.RelationContext; import cel.parser.internal.CELParser.SelectContext; +import cel.parser.internal.CELParser.SimpleIdentifierContext; import cel.parser.internal.CELParser.StartContext; import cel.parser.internal.CELParser.StringContext; import cel.parser.internal.CELParser.UintContext; @@ -438,7 +442,7 @@ public CelExpr visitSelect(SelectContext context) { if (context.id == null) { return exprFactory.newExprBuilder(context).build(); } - String id = context.id.getText(); + String id = normalizeEscapedIdent(context.id); if (context.opt != null && context.opt.getText().equals("?")) { if (!options.enableOptionalSyntax()) { @@ -535,7 +539,7 @@ public CelExpr visitCreateMessage(CreateMessageContext context) { } @Override - public CelExpr visitIdentOrGlobalCall(IdentOrGlobalCallContext context) { + public CelExpr visitIdent(IdentContext context) { checkNotNull(context); if (context.id == null) { return exprFactory.newExprBuilder(context).build(); @@ -547,11 +551,25 @@ public CelExpr visitIdentOrGlobalCall(IdentOrGlobalCallContext context) { if (context.leadingDot != null) { id = "." + id; } - if (context.op == null) { - return exprFactory - .newExprBuilder(context.id) - .setIdent(CelExpr.CelIdent.newBuilder().setName(id).build()) - .build(); + + return exprFactory + .newExprBuilder(context.id) + .setIdent(CelExpr.CelIdent.newBuilder().setName(id).build()) + .build(); + } + + @Override + public CelExpr visitGlobalCall(GlobalCallContext context) { + checkNotNull(context); + if (context.id == null) { + return exprFactory.newExprBuilder(context).build(); + } + String id = context.id.getText(); + if (options.enableReservedIds() && RESERVED_IDS.contains(id)) { + return exprFactory.reportError(context, "reserved identifier: %s", id); + } + if (context.leadingDot != null) { + id = "." + id; } return globalCallOrMacro(context, id); @@ -671,6 +689,24 @@ private Optional visitMacro( return expandedMacro; } + private String normalizeEscapedIdent(EscapeIdentContext context) { + if (context instanceof SimpleIdentifierContext) { + return ((SimpleIdentifierContext) context).getText(); + } else if (context instanceof EscapedIdentifierContext) { + if (!options.enableQuotedIdentifierSyntax()) { + exprFactory.reportError(context, "unsupported syntax '`'"); + return ""; + } + String escaped = ((EscapedIdentifierContext) context).getText(); + return escaped.substring(1, escaped.length() - 1); + } + + // This is normally unreachable, but might happen if the parser is in an error state or if the + // grammar is updated and not handled here. + exprFactory.reportError(context, "unsupported identifier"); + return ""; + } + private CelExpr.CelStruct.Builder visitStructFields(FieldInitializerListContext context) { if (context == null || context.cols == null @@ -692,10 +728,10 @@ private CelExpr.CelStruct.Builder visitStructFields(FieldInitializerListContext } // The field may be empty due to a prior error. - if (fieldContext.IDENTIFIER() == null) { + if (fieldContext.escapeIdent() == null) { return CelExpr.CelStruct.newBuilder(); } - String fieldName = fieldContext.IDENTIFIER().getText(); + String fieldName = normalizeEscapedIdent(fieldContext.escapeIdent()); CelExpr.CelStruct.Entry.Builder exprBuilder = CelExpr.CelStruct.Entry.newBuilder() @@ -872,7 +908,7 @@ private CelExpr receiverCallOrMacro(MemberCallContext context, String id, CelExp return macroOrCall(context.args, context.open, id, Optional.of(member), true); } - private CelExpr globalCallOrMacro(IdentOrGlobalCallContext context, String id) { + private CelExpr globalCallOrMacro(GlobalCallContext context, String id) { return macroOrCall(context.args, context.op, id, Optional.empty(), false); } diff --git a/parser/src/main/java/dev/cel/parser/gen/CEL.g4 b/parser/src/main/java/dev/cel/parser/gen/CEL.g4 index fbbd1b434..65f4b830d 100644 --- a/parser/src/main/java/dev/cel/parser/gen/CEL.g4 +++ b/parser/src/main/java/dev/cel/parser/gen/CEL.g4 @@ -11,6 +11,7 @@ // 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. + grammar CEL; // Grammar Rules @@ -44,26 +45,27 @@ calc ; unary - : member # MemberExpr - | (ops+='!')+ member # LogicalNot - | (ops+='-')+ member # Negate + : member # MemberExpr + | (ops+='!')+ member # LogicalNot + | (ops+='-')+ member # Negate ; member - : primary # PrimaryExpr - | member op='.' (opt='?')? id=IDENTIFIER # Select - | member op='.' id=IDENTIFIER open='(' args=exprList? ')' # MemberCall - | member op='[' (opt='?')? index=expr ']' # Index + : primary # PrimaryExpr + | member op='.' (opt='?')? id=escapeIdent # Select + | member op='.' id=IDENTIFIER open='(' args=exprList? ')' # MemberCall + | member op='[' (opt='?')? index=expr ']' # Index ; primary - : leadingDot='.'? id=IDENTIFIER (op='(' args=exprList? ')')? # IdentOrGlobalCall - | '(' e=expr ')' # Nested - | op='[' elems=listInit? ','? ']' # CreateList - | op='{' entries=mapInitializerList? ','? '}' # CreateMap + : leadingDot='.'? id=IDENTIFIER # Ident + | leadingDot='.'? id=IDENTIFIER (op='(' args=exprList? ')') # GlobalCall + | '(' e=expr ')' # Nested + | op='[' elems=listInit? ','? ']' # CreateList + | op='{' entries=mapInitializerList? ','? '}' # CreateMap | leadingDot='.'? ids+=IDENTIFIER (ops+='.' ids+=IDENTIFIER)* - op='{' entries=fieldInitializerList? ','? '}' # CreateMessage - | literal # ConstantLiteral + op='{' entries=fieldInitializerList? ','? '}' # CreateMessage + | literal # ConstantLiteral ; exprList @@ -79,13 +81,18 @@ fieldInitializerList ; optField - : (opt='?')? IDENTIFIER + : (opt='?')? escapeIdent ; mapInitializerList : keys+=optExpr cols+=':' values+=expr (',' keys+=optExpr cols+=':' values+=expr)* ; +escapeIdent + : id=IDENTIFIER # SimpleIdentifier + | id=ESC_IDENTIFIER # EscapedIdentifier + ; + optExpr : (opt='?')? e=expr ; @@ -197,3 +204,4 @@ STRING BYTES : ('b' | 'B') STRING; IDENTIFIER : (LETTER | '_') ( LETTER | DIGIT | '_')*; +ESC_IDENTIFIER : '`' (LETTER | DIGIT | '_' | '.' | '-' | '/' | ' ')+ '`'; diff --git a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java index 22d40117c..7ad52e415 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java @@ -205,6 +205,18 @@ public void parser() { .setOptions(CelOptions.current().enableReservedIds(false).build()) .build(), "while"); + CelParser parserWithQuotedFields = + CelParserImpl.newBuilder() + .setOptions(CelOptions.current().enableQuotedIdentifierSyntax(true).build()) + .build(); + runTest(parserWithQuotedFields, "foo.`bar`"); + runTest(parserWithQuotedFields, "foo.`bar-baz`"); + runTest(parserWithQuotedFields, "foo.`bar baz`"); + runTest(parserWithQuotedFields, "foo.`bar.baz`"); + runTest(parserWithQuotedFields, "foo.`bar/baz`"); + runTest(parserWithQuotedFields, "foo.`bar_baz`"); + runTest(parserWithQuotedFields, "foo.`in`"); + runTest(parserWithQuotedFields, "Struct{`in`: false}"); } @Test @@ -273,6 +285,21 @@ public void parser_errors() { runTest(parserWithoutOptionalSupport, "a.?b && a[?b]"); runTest(parserWithoutOptionalSupport, "Msg{?field: value} && {?'key': value}"); runTest(parserWithoutOptionalSupport, "[?a, ?b]"); + + CelParser parserWithQuotedFields = + CelParserImpl.newBuilder() + .setOptions(CelOptions.current().enableQuotedIdentifierSyntax(true).build()) + .build(); + runTest(parserWithQuotedFields, "`bar`"); + runTest(parserWithQuotedFields, "foo.``"); + runTest(parserWithQuotedFields, "foo.`$bar`"); + + CelParser parserWithoutQuotedFields = + CelParserImpl.newBuilder() + .setOptions(CelOptions.current().enableQuotedIdentifierSyntax(false).build()) + .build(); + runTest(parserWithoutQuotedFields, "foo.`bar`"); + runTest(parserWithoutQuotedFields, "Struct{`bar`: false}"); } @Test diff --git a/parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java b/parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java index 9ef729d94..0f2c00022 100644 --- a/parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java +++ b/parser/src/test/java/dev/cel/parser/CelUnparserImplTest.java @@ -39,7 +39,11 @@ public final class CelUnparserImplTest { private final CelParser parser = CelParserImpl.newBuilder() - .setOptions(CelOptions.newBuilder().populateMacroCalls(true).build()) + .setOptions( + CelOptions.newBuilder() + .enableQuotedIdentifierSyntax(true) + .populateMacroCalls(true) + .build()) .addLibraries(CelOptionalLibrary.INSTANCE) .setStandardMacros(CelStandardMacro.STANDARD_MACROS) .build(); @@ -99,6 +103,15 @@ public List provideValues(Context context) { "a ? (b1 || b2) : (c1 && c2)", "(a ? b : c).method(d)", "a + b + c + d", + "foo.`a.b`", + "foo.`a/b`", + "foo.`a-b`", + "foo.`a b`", + "foo.`in`", + "Foo{`a.b`: foo}", + "Foo{`a/b`: foo}", + "Foo{`a-b`: foo}", + "Foo{`a b`: foo}", // Constants "true", @@ -140,6 +153,7 @@ public List provideValues(Context context) { // Macros "has(x[\"a\"].single_int32)", + "has(x.`foo-bar`.single_int32)", // This is a filter expression but is decompiled back to // map(x, filter_function, x) for which the evaluation is diff --git a/parser/src/test/resources/parser.baseline b/parser/src/test/resources/parser.baseline index 435b92a1f..9b509f61e 100644 --- a/parser/src/test/resources/parser.baseline +++ b/parser/src/test/resources/parser.baseline @@ -1622,4 +1622,48 @@ L: [ I: while =====> P: while^#1:Expr.Ident# -L: while^#1[1,0]# \ No newline at end of file +L: while^#1[1,0]# + +I: foo.`bar` +=====> +P: foo^#1:Expr.Ident#.bar^#2:Expr.Select# +L: foo^#1[1,0]#.bar^#2[1,3]# + +I: foo.`bar-baz` +=====> +P: foo^#1:Expr.Ident#.bar-baz^#2:Expr.Select# +L: foo^#1[1,0]#.bar-baz^#2[1,3]# + +I: foo.`bar baz` +=====> +P: foo^#1:Expr.Ident#.bar baz^#2:Expr.Select# +L: foo^#1[1,0]#.bar baz^#2[1,3]# + +I: foo.`bar.baz` +=====> +P: foo^#1:Expr.Ident#.bar.baz^#2:Expr.Select# +L: foo^#1[1,0]#.bar.baz^#2[1,3]# + +I: foo.`bar/baz` +=====> +P: foo^#1:Expr.Ident#.bar/baz^#2:Expr.Select# +L: foo^#1[1,0]#.bar/baz^#2[1,3]# + +I: foo.`bar_baz` +=====> +P: foo^#1:Expr.Ident#.bar_baz^#2:Expr.Select# +L: foo^#1[1,0]#.bar_baz^#2[1,3]# + +I: foo.`in` +=====> +P: foo^#1:Expr.Ident#.in^#2:Expr.Select# +L: foo^#1[1,0]#.in^#2[1,3]# + +I: Struct{`in`: false} +=====> +P: Struct{ + in:false^#3:bool#^#2:Expr.CreateStruct.Entry# +}^#1:Expr.CreateStruct# +L: Struct{ + in:false^#3[1,13]#^#2[1,11]# +}^#1[1,6]# \ No newline at end of file diff --git a/parser/src/test/resources/parser_errors.baseline b/parser/src/test/resources/parser_errors.baseline index 8547ebed9..8ead6ef3d 100644 --- a/parser/src/test/resources/parser_errors.baseline +++ b/parser/src/test/resources/parser_errors.baseline @@ -255,7 +255,7 @@ E: ERROR: :1:2: mismatched input '' expecting {'[', '{', '}', '(', ' I: t{>C} =====> -E: ERROR: :1:3: extraneous input '>' expecting {'}', ',', '?', IDENTIFIER} +E: ERROR: :1:3: extraneous input '>' expecting {'}', ',', '?', IDENTIFIER, ESC_IDENTIFIER} | t{>C} | ..^ ERROR: :1:5: mismatched input '}' expecting ':' @@ -296,4 +296,40 @@ E: ERROR: :1:2: unsupported syntax '?' | .^ ERROR: :1:6: unsupported syntax '?' | [?a, ?b] - | .....^ \ No newline at end of file + | .....^ + +I: `bar` +=====> +E: ERROR: :1:1: mismatched input '`bar`' expecting {'[', '{', '(', '.', '-', '!', 'true', 'false', 'null', NUM_FLOAT, NUM_INT, NUM_UINT, STRING, BYTES, IDENTIFIER} + | `bar` + | ^ + +I: foo.`` +=====> +E: ERROR: :1:5: token recognition error at: '``' + | foo.`` + | ....^ +ERROR: :1:7: no viable alternative at input '.' + | foo.`` + | ......^ + +I: foo.`$bar` +=====> +E: ERROR: :1:5: token recognition error at: '`$' + | foo.`$bar` + | ....^ +ERROR: :1:10: token recognition error at: '`' + | foo.`$bar` + | .........^ + +I: foo.`bar` +=====> +E: ERROR: :1:5: unsupported syntax '`' + | foo.`bar` + | ....^ + +I: Struct{`bar`: false} +=====> +E: ERROR: :1:8: unsupported syntax '`' + | Struct{`bar`: false} + | .......^ \ No newline at end of file diff --git a/policy/src/test/resources/compile_errors/expected_errors.baseline b/policy/src/test/resources/compile_errors/expected_errors.baseline index a8c0ec047..d03f27135 100644 --- a/policy/src/test/resources/compile_errors/expected_errors.baseline +++ b/policy/src/test/resources/compile_errors/expected_errors.baseline @@ -1,7 +1,7 @@ ERROR: compile_errors/policy.yaml:19:19: undeclared reference to 'spec' (in container '') | expression: spec.labels | ..................^ -ERROR: compile_errors/policy.yaml:21:50: mismatched input 'resource' expecting {'==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', '(', ')', '.', '-', '?', '+', '*', '/', '%%'} +ERROR: compile_errors/policy.yaml:21:50: mismatched input 'resource' expecting {'==', '!=', 'in', '<', '<=', '>=', '>', '&&', '||', '[', ')', '.', '-', '?', '+', '*', '/', '%%'} | expression: variables.want.filter(l, !(lin resource.labels)) | .................................................^ ERROR: compile_errors/policy.yaml:21:66: extraneous input ')' expecting From 9f59a548d6168f817de9e97d8ad78ed29ef8681b Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 8 Jan 2025 14:07:45 -0800 Subject: [PATCH 2/9] Retain the original identifier during parse when quoted identifier is disabled PiperOrigin-RevId: 713410853 --- parser/src/main/java/dev/cel/parser/Parser.java | 10 +++++----- .../dev/cel/parser/CelParserParameterizedTest.java | 2 ++ parser/src/test/resources/parser_errors.baseline | 14 +++++++++++++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/parser/src/main/java/dev/cel/parser/Parser.java b/parser/src/main/java/dev/cel/parser/Parser.java index 12bf1bbba..2e7b08dc0 100644 --- a/parser/src/main/java/dev/cel/parser/Parser.java +++ b/parser/src/main/java/dev/cel/parser/Parser.java @@ -690,21 +690,21 @@ private Optional visitMacro( } private String normalizeEscapedIdent(EscapeIdentContext context) { + String identifier = context.getText(); if (context instanceof SimpleIdentifierContext) { - return ((SimpleIdentifierContext) context).getText(); + return identifier; } else if (context instanceof EscapedIdentifierContext) { if (!options.enableQuotedIdentifierSyntax()) { exprFactory.reportError(context, "unsupported syntax '`'"); - return ""; + return identifier; } - String escaped = ((EscapedIdentifierContext) context).getText(); - return escaped.substring(1, escaped.length() - 1); + return identifier.substring(1, identifier.length() - 1); } // This is normally unreachable, but might happen if the parser is in an error state or if the // grammar is updated and not handled here. exprFactory.reportError(context, "unsupported identifier"); - return ""; + return identifier; } private CelExpr.CelStruct.Builder visitStructFields(FieldInitializerListContext context) { diff --git a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java index 7ad52e415..1a83c3127 100644 --- a/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java +++ b/parser/src/test/java/dev/cel/parser/CelParserParameterizedTest.java @@ -296,10 +296,12 @@ public void parser_errors() { CelParser parserWithoutQuotedFields = CelParserImpl.newBuilder() + .setStandardMacros(CelStandardMacro.HAS) .setOptions(CelOptions.current().enableQuotedIdentifierSyntax(false).build()) .build(); runTest(parserWithoutQuotedFields, "foo.`bar`"); runTest(parserWithoutQuotedFields, "Struct{`bar`: false}"); + runTest(parserWithoutQuotedFields, "has(.`.`"); } @Test diff --git a/parser/src/test/resources/parser_errors.baseline b/parser/src/test/resources/parser_errors.baseline index 8ead6ef3d..9f4b96825 100644 --- a/parser/src/test/resources/parser_errors.baseline +++ b/parser/src/test/resources/parser_errors.baseline @@ -332,4 +332,16 @@ I: Struct{`bar`: false} =====> E: ERROR: :1:8: unsupported syntax '`' | Struct{`bar`: false} - | .......^ \ No newline at end of file + | .......^ + +I: has(.`.` +=====> +E: ERROR: :1:6: no viable alternative at input '.`.`' + | has(.`.` + | .....^ +ERROR: :1:6: unsupported syntax '`' + | has(.`.` + | .....^ +ERROR: :1:9: missing ')' at '' + | has(.`.` + | ........^ From 4cc5853fe5bd042afe7ed9b16a446a7a5dbf4432 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Thu, 9 Jan 2025 13:40:37 -0800 Subject: [PATCH 3/9] Internal Changes PiperOrigin-RevId: 713781331 --- legacy/BUILD.bazel | 25 + legacy/README.md | 3 + .../runtime/async/AsyncCanonicalResolver.java | 19 + .../legacy/runtime/async/AsyncContext.java | 68 + .../legacy/runtime/async/AsyncDispatcher.java | 14 + .../runtime/async/AsyncDispatcherBase.java | 264 +++ .../runtime/async/AsyncGlobalResolver.java | 15 + .../runtime/async/AsyncInterpretable.java | 63 + .../async/AsyncInterpretableOrConstant.java | 81 + .../runtime/async/AsyncInterpreter.java | 32 + .../dev/cel/legacy/runtime/async/BUILD.bazel | 83 + .../runtime/async/Canonicalization.java | 348 ++++ .../runtime/async/CompiledExpression.java | 144 ++ .../runtime/async/DefaultAsyncContext.java | 98 + .../runtime/async/DefaultAsyncDispatcher.java | 40 + .../runtime/async/DummyAsyncContext.java | 26 + .../cel/legacy/runtime/async/DynamicEnv.java | 147 ++ .../dev/cel/legacy/runtime/async/Effect.java | 42 + .../runtime/async/EvaluationHelpers.java | 335 ++++ .../cel/legacy/runtime/async/Evaluator.java | 1232 ++++++++++++ .../runtime/async/ExecutableExpression.java | 28 + .../runtime/async/FunctionRegistrar.java | 340 ++++ .../runtime/async/FunctionResolver.java | 42 + .../runtime/async/FuturesInterpreter.java | 52 + .../legacy/runtime/async/GlobalContext.java | 29 + .../async/IdentifiedCompiledExpression.java | 47 + .../runtime/async/MessageProcessor.java | 241 +++ .../async/MessageProcessorAdapter.java | 230 +++ .../runtime/async/ProtoFieldAssignment.java | 546 ++++++ .../legacy/runtime/async/ResolverAdapter.java | 40 + .../runtime/async/StackOffsetFinder.java | 17 + .../runtime/async/StandardConstructs.java | 342 ++++ .../runtime/async/StandardTypeResolver.java | 242 +++ .../async/TypeDirectedMessageProcessor.java | 286 +++ .../legacy/runtime/async/TypeResolver.java | 67 + .../runtime/async/AsyncInterpreterTest.java | 65 + .../dev/cel/legacy/runtime/async/BUILD.bazel | 102 + .../runtime/async/BaseInterpreterTest.java | 1683 +++++++++++++++++ .../legacy/runtime/async/DynamicEnvTest.java | 56 + .../dev/cel/legacy/runtime/async/Eval.java | 44 + .../cel/legacy/runtime/async/EvalAsync.java | 168 ++ .../runtime/async/FuturesInterpreterTest.java | 558 ++++++ ...esInterpreterWithMessageProcessorTest.java | 414 ++++ 43 files changed, 8718 insertions(+) create mode 100644 legacy/BUILD.bazel create mode 100644 legacy/README.md create mode 100644 legacy/java/dev/cel/legacy/runtime/async/AsyncCanonicalResolver.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/AsyncContext.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/AsyncDispatcher.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/AsyncDispatcherBase.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/AsyncGlobalResolver.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/AsyncInterpretable.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/AsyncInterpretableOrConstant.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/AsyncInterpreter.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/BUILD.bazel create mode 100644 legacy/java/dev/cel/legacy/runtime/async/Canonicalization.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/CompiledExpression.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/DefaultAsyncContext.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/DefaultAsyncDispatcher.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/DummyAsyncContext.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/DynamicEnv.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/Effect.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/EvaluationHelpers.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/Evaluator.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/ExecutableExpression.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/FunctionRegistrar.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/FunctionResolver.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/FuturesInterpreter.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/GlobalContext.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/IdentifiedCompiledExpression.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/MessageProcessor.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/MessageProcessorAdapter.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/ProtoFieldAssignment.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/ResolverAdapter.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/StackOffsetFinder.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/StandardConstructs.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/StandardTypeResolver.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/TypeDirectedMessageProcessor.java create mode 100644 legacy/java/dev/cel/legacy/runtime/async/TypeResolver.java create mode 100644 legacy/javatests/dev/cel/legacy/runtime/async/AsyncInterpreterTest.java create mode 100644 legacy/javatests/dev/cel/legacy/runtime/async/BUILD.bazel create mode 100644 legacy/javatests/dev/cel/legacy/runtime/async/BaseInterpreterTest.java create mode 100644 legacy/javatests/dev/cel/legacy/runtime/async/DynamicEnvTest.java create mode 100644 legacy/javatests/dev/cel/legacy/runtime/async/Eval.java create mode 100644 legacy/javatests/dev/cel/legacy/runtime/async/EvalAsync.java create mode 100644 legacy/javatests/dev/cel/legacy/runtime/async/FuturesInterpreterTest.java create mode 100644 legacy/javatests/dev/cel/legacy/runtime/async/FuturesInterpreterWithMessageProcessorTest.java diff --git a/legacy/BUILD.bazel b/legacy/BUILD.bazel new file mode 100644 index 000000000..df22a6d7a --- /dev/null +++ b/legacy/BUILD.bazel @@ -0,0 +1,25 @@ +load("@rules_java//java:java_library.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = [":legacy_users"], +) + +# See go/cel-java-migration-guide. This package is not accepting new clients. +package_group( + name = "legacy_users", + packages = [ + "//third_party/java/cel/legacy/...", + ], +) + +java_library( + name = "async_runtime", + exports = ["//third_party/java/cel/legacy/java/dev/cel/legacy/runtime/async"], +) + +java_library( + name = "dummy_async_context", + testonly = 1, + exports = ["//third_party/java/cel/legacy/java/dev/cel/legacy/runtime/async:dummy_async_context"], +) diff --git a/legacy/README.md b/legacy/README.md new file mode 100644 index 000000000..b927206d2 --- /dev/null +++ b/legacy/README.md @@ -0,0 +1,3 @@ +This directory contains the deprecated CEL-Java packages that once resided in g3/jcg/api/tools/contract/runtime/interpreter. + +This package is no longer accepting any new clients, and the underlying codebase will not be open sourced. For existing clients, please migrate to the new fluent APIs (go/cel-java-migration-guide) \ No newline at end of file diff --git a/legacy/java/dev/cel/legacy/runtime/async/AsyncCanonicalResolver.java b/legacy/java/dev/cel/legacy/runtime/async/AsyncCanonicalResolver.java new file mode 100644 index 000000000..c7df81607 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/AsyncCanonicalResolver.java @@ -0,0 +1,19 @@ +package dev.cel.legacy.runtime.async; + +import com.google.common.util.concurrent.ListenableFuture; +import java.util.function.Supplier; + +/** + * An interface describing an object that can perform a lookup on a given name, returning a future + * computing the value associated with the so-named global variable. The value must be in canonical + * CEL runtime representation. + */ +public interface AsyncCanonicalResolver { + /** + * Resolves the given name to a future returning its value. Neither the returned supplier nor the + * supplied future can be null, and a value computed by the future must be in canonical CEL + * runtime representation (which also excludes null). Returns a failed future if the name is not + * bound or if the value cannot be represented canonically. + */ + Supplier> resolve(String name); +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/AsyncContext.java b/legacy/java/dev/cel/legacy/runtime/async/AsyncContext.java new file mode 100644 index 000000000..950e76845 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/AsyncContext.java @@ -0,0 +1,68 @@ +package dev.cel.legacy.runtime.async; + +import com.google.common.context.Context; +import com.google.common.context.WithContext; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +/** + * Represents a snapshot of the "global context" that a context-dependent operations may reference. + */ +public interface AsyncContext { + + /** + * "Performs" an operation by obtaining the result future from the given supplier. May optimize + * by, e.g. memoization based on the given keys. (For example, the keys could list the name the + * overload ID of a strict context-dependent function together with a list of its actual + * arguments.) + */ + ListenableFuture perform( + Supplier> resultFutureSupplier, Object... keys); + + /** Retrieves the executor used for the futures-based computation expressed in CEL. */ + Executor executor(); + + default Optional requestContext() { + return Optional.empty(); + } + + /** + * Indicates that the evaluation is a runtime evaluation (as opposed to, e.g., constant-folding or + * other optimizations that occur during compile-time or preprocessing time). + */ + default boolean isRuntime() { + return true; + } + + /** + * Decouples the given future from whatever executor it is currently using and couples it to this + * context. Subsequent transformations that specify {@link MoreExecutors#directExecutor} will run + * on this context's executor. + */ + default ListenableFuture coupleToExecutor(ListenableFuture f) { + return FluentFuture.from(f) + .transform(x -> x, executor()) + .catchingAsync(Throwable.class, Futures::immediateFailedFuture, executor()); + } + + /** + * Runs the given supplier of a future within the request context, if any, and then couples the + * result to the executor. + */ + default ListenableFuture coupleToExecutorInRequestContext( + Supplier> futureSupplier) { + return coupleToExecutor( + requestContext() + .map( + c -> { + try (WithContext wc = WithContext.enter(c)) { + return futureSupplier.get(); + } + }) + .orElseGet(futureSupplier)); + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/AsyncDispatcher.java b/legacy/java/dev/cel/legacy/runtime/async/AsyncDispatcher.java new file mode 100644 index 000000000..4384a0106 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/AsyncDispatcher.java @@ -0,0 +1,14 @@ +package dev.cel.legacy.runtime.async; + +/** + * Interface to an object that combines a {@link FunctionRegistrar} with a corresponding {@link + * FunctionResolver}. + */ +public interface AsyncDispatcher extends FunctionRegistrar, FunctionResolver { + + /** + * Creates an independent copy of the current state of the dispatcher. Further updates to either + * the original or the forked copy do not affect the respective other. + */ + AsyncDispatcher fork(); +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/AsyncDispatcherBase.java b/legacy/java/dev/cel/legacy/runtime/async/AsyncDispatcherBase.java new file mode 100644 index 000000000..98f896cc1 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/AsyncDispatcherBase.java @@ -0,0 +1,264 @@ +package dev.cel.legacy.runtime.async; + +import static dev.cel.legacy.runtime.async.EvaluationHelpers.immediateException; +import static java.util.stream.Collectors.joining; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.MessageLite; +import dev.cel.common.CelErrorCode; +import dev.cel.runtime.InterpreterException; +import dev.cel.runtime.Metadata; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Base implementation of interface {@link AsyncDispatcher}. + * + *

A fresh {@link AsyncDispatcherBase} starts out empty, i.e., initially has no bindings at all. + */ +public class AsyncDispatcherBase implements AsyncDispatcher { + + // Mappings from overload IDs to either call constructors or runtime-overloadable + // (strict) functions. The demains of these two maps are maintained to be disjoint. + private final Map constructors; + private final Map overloads; + + /** Creates new empty registry. */ + public AsyncDispatcherBase() { + this.constructors = new HashMap<>(); + this.overloads = new HashMap<>(); + } + + /** Cloning constructor, for implementing snapshots. */ + protected AsyncDispatcherBase(AsyncDispatcherBase orig) { + this.constructors = new HashMap<>(orig.constructors); + this.overloads = new HashMap<>(orig.overloads); + } + + /** + * Adds a generic call constructor for the given overload ID. Function calls with this overload ID + * will later be handled by the given {@link CallConstructor}. No runtime overloading is possible. + */ + @Override + public void addCallConstructor(String overloadId, CallConstructor callConstructor) { + checkNotAlreadyBound(overloadId); + constructors.put(overloadId, callConstructor); + } + + /** Adds a strict function as one possible binding for a runtime-overloadable function. */ + @Override + public void addStrictFunction( + String overloadId, + List> argumentTypes, + boolean contextIndependent, + StrictFunction strictFunction) { + checkNotAlreadyBound(overloadId); + overloads.put( + overloadId, OverloadInfo.of(overloadId, argumentTypes, contextIndependent, strictFunction)); + } + + /** + * Constructs the compiled CEL expression that implements the call of a function at some call + * site. + * + *

If multiple overload IDs are given, then a runtime dispatch is implemented. All overload IDs + * must refer to strict functions in that case. + */ + // This lambda implements @Immutable interface 'StrictFunction', but 'List' is mutable + @SuppressWarnings("Immutable") + @Override + public CompiledExpression constructCall( + @Nullable Metadata metadata, + long exprId, + String functionName, + List overloadIds, + List compiledArguments, + MessageProcessor messageProcessor, + StackOffsetFinder stackOffsetFinder) + throws InterpreterException { + Preconditions.checkState(!overloadIds.isEmpty(), "no overloads for call of %s", functionName); + if (overloadIds.size() == 1) { + // Unique binding. + String overloadId = overloadIds.get(0); + if (constructors.containsKey(overloadId)) { + return constructors + .get(overloadId) + .construct(metadata, exprId, compiledArguments, messageProcessor, stackOffsetFinder); + } + } + + List candidates = new ArrayList<>(); + List unbound = new ArrayList<>(); + for (String overloadId : overloadIds) { + if (constructors.containsKey(overloadId)) { + throw new InterpreterException.Builder( + "incompatible overload for function '%s': %s must be resolved at compile time", + functionName, overloadId) + .setLocation(metadata, exprId) + .build(); + } else if (overloads.containsKey(overloadId)) { + candidates.add(overloads.get(overloadId)); + } else { + unbound.add(overloadId); + } + } + + if (!unbound.isEmpty()) { + throw new InterpreterException.Builder("no runtime binding for %s", String.join(",", unbound)) + .setLocation(metadata, exprId) + .build(); + } + + if (candidates.size() == 1) { + OverloadInfo overload = candidates.get(0); + return constructStrictCall( + overload.function(), + overload.overloadId(), + overload.contextIndependent(), + compiledArguments); + } + // Key for memoizing the overload dispatch itself. + String memoizationKey = candidates.stream().map(OverloadInfo::overloadId).collect(joining("|")); + boolean contextIndependent = candidates.stream().allMatch(OverloadInfo::contextIndependent); + return constructStrictCall( + (gctx, arguments) -> { + List matching = new ArrayList<>(); + for (OverloadInfo candidate : candidates) { + if (candidate.canHandle(arguments)) { + matching.add(candidate); + } + } + if (matching.isEmpty()) { + return immediateException( + new InterpreterException.Builder( + "No matching overload for function '%s'. Overload candidates: %s", + functionName, String.join(",", overloadIds)) + .setErrorCode(CelErrorCode.OVERLOAD_NOT_FOUND) + .setLocation(metadata, exprId) + .build()); + } + if (matching.size() > 1) { + return immediateException( + new InterpreterException.Builder( + "Ambiguous overloads for function '%s'. Matching candidates: %s", + functionName, + matching.stream().map(OverloadInfo::overloadId).collect(joining(","))) + .setErrorCode(CelErrorCode.AMBIGUOUS_OVERLOAD) + .setLocation(metadata, exprId) + .build()); + } + OverloadInfo match = matching.get(0); + return match.contextIndependent() + ? match.function().apply(gctx, arguments) + : gctx.context() + .perform( + () -> match.function().apply(gctx, arguments), match.overloadId(), arguments); + }, + memoizationKey, + contextIndependent, + compiledArguments); + } + + /** + * Constructs a call of a single strict function. This is a thin wrapper around {@link + * EvaluationHelpers#compileStrictCall}. + */ + private CompiledExpression constructStrictCall( + StrictFunction function, + String overloadId, + boolean contextIndependent, + List compiledArguments) + throws InterpreterException { + return EvaluationHelpers.compileStrictCall( + function, + overloadId, + contextIndependent ? Effect.CONTEXT_INDEPENDENT : Effect.CONTEXT_DEPENDENT, + compiledArguments); + } + + /** + * Creates an independent copy of the current state of the dispatcher. Further updates to either + * the original or the forked copy do not affect the respective other. + */ + @Override + public AsyncDispatcher fork() { + return new AsyncDispatcherBase(this); + } + + // Not to be overridden in subclasses! This method only checks whether it is locally + // (i.e., only with respect to constructors and overloads of this dispatcher) to add + // a new binding for overloadId. + private boolean isLocallyBound(String overloadId) { + return constructors.containsKey(overloadId) || overloads.containsKey(overloadId); + } + + /** + * Determines whether or not the given overload ID corresponds to a known function binding. + * Subclasses that provide additional bindings should override this. + */ + @Override + public boolean isBound(String overloadId) { + return isLocallyBound(overloadId); + } + + /** Helper for making sure that no overload ID is bound more than once. */ + private void checkNotAlreadyBound(String overloadId) { + Preconditions.checkState( + !isLocallyBound(overloadId), "More than one binding for %s.", overloadId); + } + + /** Helper class for storing information about a single overloadable strict function. */ + @AutoValue + abstract static class OverloadInfo { + /** The overload ID in question. */ + abstract String overloadId(); + + /** Java classes of the expected arguments. */ + abstract ImmutableList> argumentTypes(); + + /** True if the function is context-independent. */ + abstract boolean contextIndependent(); + + /** The function that is bound to the overload ID. */ + abstract StrictFunction function(); + + static OverloadInfo of( + String overloadId, + List> argumentTypes, + boolean contextIndependent, + StrictFunction function) { + return new AutoValue_AsyncDispatcherBase_OverloadInfo( + overloadId, ImmutableList.copyOf(argumentTypes), contextIndependent, function); + } + + /** Determines whether this overload can handle a call with the given actual arguments. */ + boolean canHandle(List arguments) { + int arity = argumentTypes().size(); + if (arity != arguments.size()) { + return false; + } + for (int i = 0; i < arity; ++i) { + if (!argMatchesType(arguments.get(i), argumentTypes().get(i))) { + return false; + } + } + return true; + } + + /** Helper for determining runtime argument type matches. */ + private static boolean argMatchesType(Object argument, Class parameterType) { + if (argument != null) { + return parameterType.isAssignableFrom(argument.getClass()); + } + // null can be assigned to messages, maps, and objects + return parameterType == Object.class + || MessageLite.class.isAssignableFrom(parameterType) + || Map.class.isAssignableFrom(parameterType); + } + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/AsyncGlobalResolver.java b/legacy/java/dev/cel/legacy/runtime/async/AsyncGlobalResolver.java new file mode 100644 index 000000000..cf8d34017 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/AsyncGlobalResolver.java @@ -0,0 +1,15 @@ +package dev.cel.legacy.runtime.async; + +import com.google.common.util.concurrent.ListenableFuture; + +/** + * An interface describing an object that can perform a lookup on a given name, returning a future + * computing the value associated with the so-named global variable. + */ +public interface AsyncGlobalResolver { + /** + * Resolves the given name to a future returning its value. The value returned from the future may + * be null, but the future itself must never be null. + */ + ListenableFuture resolve(String name); +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpretable.java b/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpretable.java new file mode 100644 index 000000000..f656d1dcc --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpretable.java @@ -0,0 +1,63 @@ +package dev.cel.legacy.runtime.async; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.CelOptions; +import java.util.List; + +/** Represent an expression which can be interpreted repeatedly with varying global variables. */ +@Immutable +@FunctionalInterface +public interface AsyncInterpretable { + + /** + * Runs interpretation with the given list of local inputs using the given {@link GlobalContext}. + * The resolver within that context is a canonical resolver, so it supplies global name/value + * bindings in canonical CEL runtime representation. It is a precondition failure for the number + * of locals to not match the number of locals that were specified when the {@link + * AsyncInterpretable} was created. The executor given by the context is used to run any + * asynchronous aspects of the interpretation. Errors encountered during interpretation result in + * a failed future that throws an ExecutionException whose cause is an {@link + * InterpreterException} or a {@link RuntimeException} for things like division by zero etc. + */ + ListenableFuture evaluate( + GlobalContext globalContext, List> locals); + + /** + * Runs interpretation with the given list of local inputs as well as a resolver that supplies + * global name/value bindings. Globally resolved values do not have to be in canonical CEL runtime + * representation, but it must be possible to "adapt" them (i.e., coerce them into that + * representation). + * + *

It is a precondition failure for the number of locals to not match the number of locals that + * were specified when the {@link AsyncInterpretable} was created. The executor given by the + * context is used to run any asynchronous aspects of the interpretation. Errors encountered + * during interpretation result in a failed future that throws an ExecutionException whose cause + * is an {@link InterpreterException} or a {@link RuntimeException} for things like division by + * zero etc. + */ + default ListenableFuture eval( + AsyncContext context, AsyncGlobalResolver global, List> locals) { + // Use of the legacy features with ResolverAdapter is generally unsafe. In all known cases this + // method will be overridden such that the correct behavior occurs; however, this method should + // be treated with caution. + return evaluate( + GlobalContext.of(context, new ResolverAdapter(global, CelOptions.LEGACY)), locals); + } + + /** Backward-compatible convenience wrapper without locals. */ + default ListenableFuture eval(AsyncContext context, AsyncGlobalResolver global) { + return eval(context, global, ImmutableList.of()); + } + + /** + * Indicates whether or not this interpretable depends on context (reads global variables or calls + * other context-dependent functions). + * + *

Assumed to be context-dependent unless explicitly overridden. + */ + default Effect effect() { + return Effect.CONTEXT_DEPENDENT; + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpretableOrConstant.java b/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpretableOrConstant.java new file mode 100644 index 000000000..33a87b111 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpretableOrConstant.java @@ -0,0 +1,81 @@ +package dev.cel.legacy.runtime.async; + +import static com.google.common.util.concurrent.Futures.immediateFuture; + +import com.google.auto.value.AutoOneOf; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; + +/** + * A tagged sum of either an {@link AsyncInterpretable} or a constant. + * + *

This is returned by the {@link AsyncInterpreter#createInterpretableOrConstant} method. + */ +@AutoOneOf(AsyncInterpretableOrConstant.Kind.class) +public abstract class AsyncInterpretableOrConstant { + /** + * Represents the choice between either a dynamic computation on the one hand or a compile-time + * constant on the other. + */ + public enum Kind { + INTERPRETABLE, + CONSTANT + } + + public abstract Kind getKind(); + + public abstract AsyncInterpretable interpretable(); + + static AsyncInterpretableOrConstant interpretable( + Effect effect, AsyncInterpretable interpretable) { + return AutoOneOf_AsyncInterpretableOrConstant.interpretable(decorate(effect, interpretable)); + } + + // Constant case uses Optional.empty to describe null. + public abstract Optional constant(); + + static AsyncInterpretableOrConstant constant(Optional constant) { + return AutoOneOf_AsyncInterpretableOrConstant.constant(constant); + } + + /** Recovers the plain constant (including the possibility of null). */ + @Nullable + public Object nullableConstant() { + return constant().orElse(null); + } + + /** + * Return an {@link AsyncInterpretable} regardless of kind (converting constants into the + * corresponding trivial interpretable). + */ + // This lambda implements @Immutable interface 'AsyncInterpretable', but accesses instance + // method(s) 'nullableConstant' on 'AsyncInterpretableOrConstant' which is not @Immutable. + @SuppressWarnings("Immutable") + public AsyncInterpretable toInterpretable() { + switch (getKind()) { + case CONSTANT: + return decorate( + Effect.CONTEXT_INDEPENDENT, (gctx, locals) -> immediateFuture(nullableConstant())); + case INTERPRETABLE: + return interpretable(); + } + throw new RuntimeException("[internal] unexpected kind in toInterpretable()"); + } + + private static AsyncInterpretable decorate(Effect newEffect, AsyncInterpretable interpretable) { + return new AsyncInterpretable() { + @Override + public ListenableFuture evaluate( + GlobalContext gctx, List> locals) { + return interpretable.evaluate(gctx, locals); + } + + @Override + public Effect effect() { + return newEffect; + } + }; + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpreter.java b/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpreter.java new file mode 100644 index 000000000..127376f5e --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpreter.java @@ -0,0 +1,32 @@ +package dev.cel.legacy.runtime.async; + +import dev.cel.expr.CheckedExpr; +import dev.cel.runtime.InterpreterException; +import java.util.List; +import java.util.Map; + +/** Interface to an asynchronous (futures-based) CEL interpreter. */ +public interface AsyncInterpreter { + + /** + * Creates an asynchronous interpretable for the given expression. + * + *

This method may run pre-processing and partial evaluation of the expression it gets passed. + */ + AsyncInterpretable createInterpretable(CheckedExpr checkedExpr) throws InterpreterException; + + /** + * Creates an asynchronous interpretable for the given expression (just like {@link + * #createInterpretable} above). If the compiler discovers that the expression describes a + * compile-time constant, then that constant's value is returned instead. Interpretable or + * constant are packaged up in an {@link AsyncInterpretableOrConstant}. + * + *

When resolving global identifiers, the given mapping from names to compile-time known + * constants is consulted first. Names not bound in this mapping are resolved at runtime. + */ + AsyncInterpretableOrConstant createInterpretableOrConstant( + CheckedExpr checkedExpr, + Map compileTimeConstants, + List localVariables) + throws InterpreterException; +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/BUILD.bazel b/legacy/java/dev/cel/legacy/runtime/async/BUILD.bazel new file mode 100644 index 000000000..938282294 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/BUILD.bazel @@ -0,0 +1,83 @@ +load("@rules_java//java:java_library.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//third_party/java/cel/legacy:__pkg__"], +) + +java_library( + name = "async", + srcs = [ + "AsyncCanonicalResolver.java", + "AsyncContext.java", + "AsyncDispatcher.java", + "AsyncDispatcherBase.java", + "AsyncGlobalResolver.java", + "AsyncInterpretable.java", + "AsyncInterpretableOrConstant.java", + "AsyncInterpreter.java", + "Canonicalization.java", + "CompiledExpression.java", + "DefaultAsyncContext.java", + "DefaultAsyncDispatcher.java", + "DynamicEnv.java", + "Effect.java", + "EvaluationHelpers.java", + "Evaluator.java", + "ExecutableExpression.java", + "FunctionRegistrar.java", + "FunctionResolver.java", + "FuturesInterpreter.java", + "GlobalContext.java", + "IdentifiedCompiledExpression.java", + "MessageProcessor.java", + "MessageProcessorAdapter.java", + "ProtoFieldAssignment.java", + "ResolverAdapter.java", + "StackOffsetFinder.java", + "StandardConstructs.java", + "StandardTypeResolver.java", + "TypeDirectedMessageProcessor.java", + "TypeResolver.java", + ], + tags = [ + "alt_dep=//third_party/java/cel/legacy:async_runtime", + "avoid_dep", + ], + deps = [ + "//:auto_value", + "//checker:checker_legacy_environment", + "//common:error_codes", + "//common:features", + "//common:options", + "//common:proto_ast", + "//common:runtime_exception", + "//common/annotations", + "//common/types", + "//common/types:type_providers", + "//java/com/google/common/context", + "//java/com/google/protobuf/contrib:util", + "//java/com/google/protobuf/contrib/descriptor/pool", + "//runtime:interpreter", + "//runtime:runtime_helper", + "//third_party/java/jsr305_annotations", + "@cel_spec//proto/cel/expr:expr_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_re2j_re2j", + ], +) + +java_library( + name = "dummy_async_context", + testonly = 1, + srcs = ["DummyAsyncContext.java"], + tags = [ + "alt_dep=//third_party/java/cel/legacy:dummy_async_context", + "avoid_dep", + ], + deps = [ + ":async", + "@maven//:com_google_guava_guava", + ], +) diff --git a/legacy/java/dev/cel/legacy/runtime/async/Canonicalization.java b/legacy/java/dev/cel/legacy/runtime/async/Canonicalization.java new file mode 100644 index 000000000..468d260d8 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/Canonicalization.java @@ -0,0 +1,348 @@ +package dev.cel.legacy.runtime.async; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.primitives.UnsignedInts; +import com.google.common.primitives.UnsignedLong; +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; +import com.google.protobuf.BytesValue; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.EnumValueDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.DoubleValue; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.ListValue; +import com.google.protobuf.MapEntry; +import com.google.protobuf.Message; +import com.google.protobuf.MessageFactories; +import com.google.protobuf.MessageOrBuilder; +import com.google.protobuf.Parser; +import com.google.protobuf.StringValue; +import com.google.protobuf.Struct; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import com.google.protobuf.Value; +import com.google.protobuf.contrib.AnyUtil; +import com.google.protobuf.contrib.descriptor.pool.GeneratedDescriptorPool; +import dev.cel.common.CelOptions; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Descriptor-directed value canonicalization. + * + *

This class provides tools for creating {@link Canonicalizer} instances. A {@code + * Canonicalizer} is the mechanism that translates values fetched from proto fields to canonical CEL + * runtime values. + */ +public final class Canonicalization { + + /** + * Represents the transformations from various proto values to their corresponding canonical CEL + * runtime representations. + */ + @Immutable + @FunctionalInterface + public interface Canonicalizer { + Object canonicalize(Object value); + } + + /** + * Returns the {@link Canonicalizer} for the field described by the give {@link FieldDescriptor}. + */ + public static Canonicalizer fieldValueCanonicalizer(FieldDescriptor fd, CelOptions celOptions) { + if (fd.isMapField()) { + return mapCanonicalizer(fd, celOptions); + } + if (fd.isRepeated()) { + return listCanonicalizer(fd, celOptions); + } + return singleFieldCanonicalizer(fd, celOptions); + } + + /** Returns the canonicalizer for a list field. */ + private static Canonicalizer listCanonicalizer(FieldDescriptor fd, CelOptions celOptions) { + Canonicalizer elementCanonicalizer = singleFieldCanonicalizer(fd, celOptions); + if (elementCanonicalizer == IDENTITY) { + return IDENTITY; + } + return l -> + Lists.transform((List) asInstanceOf(List.class, l), elementCanonicalizer::canonicalize); + } + + /** + * Returns the canonicalizer for a map field. It constructs an actual instance of {@link Map} from + * a list of map entries. The argument descriptor describes a map entry. Key and value descriptors + * can be obtained from it. + */ + @SuppressWarnings("unchecked") + private static Canonicalizer mapCanonicalizer(FieldDescriptor fd, CelOptions celOptions) { + Descriptor entryDescriptor = fd.getMessageType(); + FieldDescriptor keyDescriptor = entryDescriptor.findFieldByNumber(1); + FieldDescriptor valueDescriptor = entryDescriptor.findFieldByNumber(2); + Canonicalizer keyCanonicalizer = singleFieldCanonicalizer(keyDescriptor, celOptions); + Canonicalizer valueCanonicalizer = singleFieldCanonicalizer(valueDescriptor, celOptions); + // Map fields aren't fetched as native Java maps but as lists of map entries, so they cannot + // simply be transformed but must be built. In any case, even if they were maps, since there is + // no off-the-shelf transform that also translates keys, it would still be necessary to + // rebuild the map. (A general transform that translates keys would have to deal with + // key collisions, which is probably why no general mechanism exists.) + return entries -> { + Map map = new HashMap<>(); + for (MapEntry entry : (List>) entries) { + map.put( + keyCanonicalizer.canonicalize(entry.getKey()), + valueCanonicalizer.canonicalize(entry.getValue())); + } + return map; + }; + } + + /** + * Canonicalizer for individual values of non-map fields. + * + *

Returns {@code IDENTITY} if the canonicalizer is the identity function. (Using this specific + * instance rather than any old identity function makes it possible for callers to detect the + * situation, as is done in {@code fieldValueCanonicalizer()} above). + * + *

For repeated fields the fetched value is a {@link List} and the constructed {@link + * Canonicalizer} must be applied to each element. + * + *

For map fields two {@link Canonicalizer} instances must be created, one for the key and one + * for the value of a map entry, and they must subsequently be applied entry-by-entry to the + * entire map. + */ + private static Canonicalizer singleFieldCanonicalizer(FieldDescriptor fd, CelOptions celOptions) { + switch (fd.getType()) { + case SFIXED32: + case SINT32: + case INT32: + return value -> ((Number) value).longValue(); + case FIXED32: + case UINT32: + return value -> UnsignedInts.toLong(((Number) value).intValue()); + case ENUM: + return value -> (long) ((EnumValueDescriptor) value).getNumber(); + case FLOAT: + return value -> ((Number) value).doubleValue(); + case MESSAGE: + return protoCanonicalizer(fd.getMessageType(), celOptions); + default: + return IDENTITY; + } + } + + /** + * Returns the {@link Canonicalizer} for arbitrary proto messages. Most messages represent + * themselves, so canonicalization is the identity. This situation is indicated by returning the + * special {@code IDENTITY} canonicalizer. + * + *

Certain well-known proto types are treated specially: those representing JSON values and + * those representing wrapped values. + * + *

JSON values are recursively converted into Java {@link List} and {@link Map} types. + * Primitive JSON values are unwrapped into their canonical CEL (i.e., Java) equivalents. + * + *

Wrapped values are simply unwrapped. Notice that all floating point values are represented + * using {@link Double} and all fixed point values are represented using {@link Long}. + */ + private static Canonicalizer protoCanonicalizer(Descriptor d, CelOptions celOptions) { + Canonicalizer deDynamicalizer = deDynamicalizerFor(d); + switch (d.getFullName()) { + case "google.protobuf.Any": + return value -> + canonicalizeAny( + asInstanceOf(Any.class, deDynamicalizer.canonicalize(value)), celOptions); + case "google.protobuf.Value": + return value -> + canonicalizeJsonValue(asInstanceOf(Value.class, deDynamicalizer.canonicalize(value))); + case "google.protobuf.ListValue": + return value -> + canonicalizeJsonList( + asInstanceOf(ListValue.class, deDynamicalizer.canonicalize(value))); + case "google.protobuf.Struct": + return value -> + canonicalizeJsonStruct(asInstanceOf(Struct.class, deDynamicalizer.canonicalize(value))); + case "google.protobuf.Int64Value": + return value -> + asInstanceOf(Int64Value.class, deDynamicalizer.canonicalize(value)).getValue(); + case "google.protobuf.UInt64Value": + if (celOptions.enableUnsignedLongs()) { + return value -> + UnsignedLong.fromLongBits( + asInstanceOf(UInt64Value.class, deDynamicalizer.canonicalize(value)).getValue()); + } + return value -> + asInstanceOf(UInt64Value.class, deDynamicalizer.canonicalize(value)).getValue(); + case "google.protobuf.Int32Value": + return value -> + (long) asInstanceOf(Int32Value.class, deDynamicalizer.canonicalize(value)).getValue(); + case "google.protobuf.UInt32Value": + if (celOptions.enableUnsignedLongs()) { + return value -> + UnsignedLong.fromLongBits( + Integer.toUnsignedLong( + asInstanceOf(UInt32Value.class, deDynamicalizer.canonicalize(value)) + .getValue())); + } + return value -> + (long) asInstanceOf(UInt32Value.class, deDynamicalizer.canonicalize(value)).getValue(); + case "google.protobuf.DoubleValue": + return value -> + asInstanceOf(DoubleValue.class, deDynamicalizer.canonicalize(value)).getValue(); + case "google.protobuf.FloatValue": + return value -> + (double) asInstanceOf(FloatValue.class, deDynamicalizer.canonicalize(value)).getValue(); + case "google.protobuf.BoolValue": + return value -> + asInstanceOf(BoolValue.class, deDynamicalizer.canonicalize(value)).getValue(); + case "google.protobuf.StringValue": + return value -> + asInstanceOf(StringValue.class, deDynamicalizer.canonicalize(value)).getValue(); + case "google.protobuf.BytesValue": + return value -> + asInstanceOf(BytesValue.class, deDynamicalizer.canonicalize(value)).getValue(); + case "google.protobuf.Timestamp": + case "google.protobuf.Duration": + return deDynamicalizer; + default: + return IDENTITY; + } + } + + /** Converts an arbitrary message object into its canonical CEL equivalent. */ + public static Object canonicalizeProto(Message value, CelOptions celOptions) { + return protoCanonicalizer(value.getDescriptorForType(), celOptions).canonicalize(value); + } + + /** Converts an instance of {@link Any} into its canonical CEL equivalent. */ + private static Object canonicalizeAny(Any any, CelOptions celOptions) { + try { + return canonicalizeProto(AnyUtil.unpack(any), celOptions); + } catch (InvalidProtocolBufferException e) { + throw new IllegalArgumentException(e); + } + } + + /** Recursively converts a JSON {@link Value} into its canonical CEL equivalent. */ + private static Object canonicalizeJsonValue(Value v) { + switch (v.getKindCase()) { + case NULL_VALUE: + return v.getNullValue(); + case NUMBER_VALUE: + return v.getNumberValue(); + case STRING_VALUE: + return v.getStringValue(); + case BOOL_VALUE: + return v.getBoolValue(); + case STRUCT_VALUE: + return canonicalizeJsonStruct(v.getStructValue()); + case LIST_VALUE: + return canonicalizeJsonList(v.getListValue()); + default: + throw new IllegalArgumentException("Invalid JSON value type: " + v.getKindCase()); + } + } + + /** + * Converts a JSON {@link ListValue} into the corresponding canonical Java {@link List} by + * transforming all values recursively. + */ + private static Object canonicalizeJsonList(ListValue lv) { + return Lists.transform(lv.getValuesList(), Canonicalization::canonicalizeJsonValue); + } + + /** + * Converts a JSON {@link Struct} into the corresponding canonical Java {@link Map}. Keys are + * always strings, and values are converted recursively. + */ + private static Object canonicalizeJsonStruct(Struct s) { + return Maps.transformValues(s.getFieldsMap(), Canonicalization::canonicalizeJsonValue); + } + + /** + * Interprets the given value as an instance of the given class, throwing an exception if that + * cannot be done. + */ + static T asInstanceOf(Class clazz, Object value) { + if (clazz.isInstance(value)) { + return clazz.cast(value); + } + throw new IllegalStateException("[internal] not a value of " + clazz + ": " + value); + } + + /** Interprets the given value as an instance of {@link MessageOrBuilder}. */ + static MessageOrBuilder asMessage(Object value) { + return asInstanceOf(MessageOrBuilder.class, value); + } + + /** Determines whether the type of the described field is a wrapper type. */ + static boolean fieldHasWrapperType(FieldDescriptor fd) { + return fd.getType() == FieldDescriptor.Type.MESSAGE + && WRAPPER_TYPE_NAMES.contains(fd.getMessageType().getFullName()); + } + + /** + * Takes a {@link DynamicMessage} object and converts it into its corresponding generated message, + * if possible. + */ + // This lambda implements @Immutable interface 'Canonicalizer', but the declaration of type + // 'com.google.protobuf.Parser' is not annotated with + // @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + private static Canonicalizer deDynamicalizerFor(Descriptor d) { + String messageName = d.getFullName(); + Descriptor generatedDescriptor = + GeneratedDescriptorPool.getInstance().getDescriptorForTypeName(messageName); + if (generatedDescriptor == null) { + return IDENTITY; + } + Message prototype = + MessageFactories.getImmutableMessageFactory().getPrototype(generatedDescriptor); + if (prototype == null) { + return IDENTITY; + } + Parser parser = prototype.getParserForType(); + return object -> { + if (!(object instanceof DynamicMessage)) { + return object; + } + try { + return parser.parseFrom( + ((DynamicMessage) object).toByteArray(), ExtensionRegistry.getGeneratedRegistry()); + } catch (InvalidProtocolBufferException e) { + throw new AssertionError("Failed to convert DynamicMessage to " + messageName, e); + } + }; + } + + private static final ImmutableSet WRAPPER_TYPE_NAMES = + ImmutableSet.of( + "google.protobuf.BoolValue", + "google.protobuf.BytesValue", + "google.protobuf.DoubleValue", + "google.protobuf.FloatValue", + "google.protobuf.Int32Value", + "google.protobuf.Int64Value", + "google.protobuf.StringValue", + "google.protobuf.UInt32Value", + "google.protobuf.UInt64Value"); + + /** + * The identity canonicalizer. Return this value rather than an on-the-fly lambda when + * canonicalization does nothing. The identity of IDENTITY (pun intended) is used to short-circuit + * canonicalization on lists when the element canonicalizer is IDENTITY. + */ + private static final Canonicalizer IDENTITY = x -> x; + + private Canonicalization() {} +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/CompiledExpression.java b/legacy/java/dev/cel/legacy/runtime/async/CompiledExpression.java new file mode 100644 index 000000000..de52fe305 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/CompiledExpression.java @@ -0,0 +1,144 @@ +package dev.cel.legacy.runtime.async; + +import static dev.cel.legacy.runtime.async.EvaluationHelpers.immediateException; +import static dev.cel.legacy.runtime.async.EvaluationHelpers.immediateValue; + +import com.google.auto.value.AutoOneOf; +import com.google.common.base.Pair; +import java.util.Optional; +import javax.annotation.Nullable; + +/** + * A compiled expression is either an {@link ExecutableExpression} (representing a residual + * computation that must be performed at runtime), or a compile-time constant, or an exception that + * is known to be thrown. The compile time constant is used during constant-folding (aka + * compile-time evaluation). + */ +@AutoOneOf(CompiledExpression.Kind.class) +public abstract class CompiledExpression { + /** Type discriminator for CompiledExpression. */ + public enum Kind { + EXECUTABLE_WITH_EFFECT, // Run-time residual computation. + COMPILED_CONSTANT, // Compile-time known value, using Optional to avoid null. + THROWING // Statically known to throw when executed. + } + + abstract Kind getKind(); + + public abstract Pair executableWithEffect(); + + public static CompiledExpression executable(ExecutableExpression expression, Effect effect) { + return AutoOneOf_CompiledExpression.executableWithEffect(Pair.of(expression, effect)); + } + + abstract Optional compiledConstant(); + + static CompiledExpression compiledConstant(Optional value) { + return AutoOneOf_CompiledExpression.compiledConstant(value); + } + + public abstract Throwable throwing(); + + public static CompiledExpression throwing(Throwable t) { + return AutoOneOf_CompiledExpression.throwing(t); + } + + /** Returns effect information. */ + public Effect effect() { + switch (getKind()) { + case EXECUTABLE_WITH_EFFECT: + return executableWithEffect().getSecond(); + default: + return Effect.CONTEXT_INDEPENDENT; + } + } + + /** Creates a constant expression directly from the nullable constant value. */ + public static CompiledExpression constant(@Nullable Object value) { + return compiledConstant(Optional.ofNullable(value)); + } + + /** Returns the actual constant value (which may be null). */ + @Nullable + public Object constant() { + return compiledConstant().orElse(null); + } + + /** Determise whether or not the expression represents a residual computation. */ + public boolean isExecutable() { + return getKind().equals(Kind.EXECUTABLE_WITH_EFFECT); + } + + /** Determines whether or not the expression represents a constant. */ + public boolean isConstant() { + return getKind().equals(Kind.COMPILED_CONSTANT); + } + + /** Determines whether or not the expression throws an exception. */ + public boolean isThrowing() { + return getKind().equals(Kind.THROWING); + } + + /** + * Maps the current expression to some result given three mapping functions, one for each case. + */ + public R map( + EffectMapping mappingForExecutable, + Mapping mappingForConstant, + Mapping mappingForThrowing) + throws E { + switch (getKind()) { + case EXECUTABLE_WITH_EFFECT: + return mappingForExecutable.map( + executableWithEffect().getFirst(), executableWithEffect().getSecond()); + case COMPILED_CONSTANT: + return mappingForConstant.map(constant()); + case THROWING: + return mappingForThrowing.map(throwing()); + } + throw unexpected("CompiledExpression#map"); + } + + /** Coerces the expression to an executable expression, preserving behavior. */ + // This lambda implements @Immutable interface 'ExecutableExpression', but 'Object' is mutable + @SuppressWarnings("Immutable") + public ExecutableExpression toExecutable() { + return map( + (exe, eff) -> exe, v -> stack -> immediateValue(v), t -> stack -> immediateException(t)); + } + + /** + * Maps the current expression to another CompiledExpressions given mappings for executables and + * constants. The mapping for throwing is the identity. + * + *

It must be the case that whenever the current expression throws an exception, the + * constructed expression will throw the same exception. + */ + public CompiledExpression mapNonThrowing( + Mapping mappingForExecutable, + Mapping mappingForConstant) + throws E { + return map( + (e, effect) -> executable(mappingForExecutable.map(e), effect), + mappingForConstant, + CompiledExpression::throwing); + } + + /** Represents a generic mapping from A to B, possibly throwing an E. */ + public interface Mapping { + B map(A argument) throws E; + } + + /** Like {@code Mapping} but carries an extra argument to convey effect information. */ + public interface EffectMapping { + B map(A argument, Effect effect) throws E; + } + + /** + * Creates a dummy exception to be thrown in places where we don't expect control to reach (but + * the Java compiler is not smart enough to know that). + */ + private static RuntimeException unexpected(String where) { + return new RuntimeException("[internal] reached unexpected program point: " + where); + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/DefaultAsyncContext.java b/legacy/java/dev/cel/legacy/runtime/async/DefaultAsyncContext.java new file mode 100644 index 000000000..98efe85f1 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/DefaultAsyncContext.java @@ -0,0 +1,98 @@ +package dev.cel.legacy.runtime.async; + +import static com.google.common.util.concurrent.Futures.immediateFailedFuture; + +import com.google.common.collect.ImmutableList; +import com.google.common.context.Context; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** Implements an {@link AsyncContext} using memoization. */ +public final class DefaultAsyncContext implements AsyncContext { + + private final ConcurrentMap, ListenableFuture> memoTable; + private final Executor executor; + private final Optional requestContext; + + public DefaultAsyncContext(Executor executor) { + // TODO: Tune initial capacity and concurrency level. + this.memoTable = new ConcurrentHashMap<>(); + this.executor = executor; + this.requestContext = Optional.empty(); + } + + public DefaultAsyncContext(Executor executor, Context requestContext) { + // TODO: Tune initial capacity and concurrency level. + this.memoTable = new ConcurrentHashMap<>(); + this.executor = executor; + this.requestContext = Optional.of(requestContext); + } + + @Override + public Optional requestContext() { + return requestContext; + } + + @Override + public ListenableFuture perform( + Supplier> futureSupplier, Object... keys) { + ImmutableList key = ImmutableList.copyOf(keys); + // If a new settable future is created by computeIfAbsent, it will be dropped + // into this reference so that it can subsequently be populated. + // See the comment below on why this cannot be done within the critical region. + AtomicReference> futureReference = new AtomicReference<>(); + ListenableFuture resultFuture = + typedFuture( + memoTable.computeIfAbsent( + key, + k -> { + SettableFuture settableFuture = SettableFuture.create(); + futureReference.set(settableFuture); + return settableFuture; + })); + // If the executor is directExecutor(), then pulling on the supplier must be + // done outside the table's critical region or deadlock can result. + // (This is mostly important for tests where the executor is in fact + // directExecutor(), but it is also a good defense for situations where + // production code accidentally uses a direct executor.) + @Nullable SettableFuture settableFuture = futureReference.get(); + if (settableFuture != null) { + // If the memo table already contained a settable future, + // then this branch will not be taken, thereby avoiding + // to pull on the future supplier and kicking off redundant + // work. + executor.execute(() -> settableFuture.setFuture(obtainSuppliedFuture(futureSupplier))); + } + return resultFuture; + } + + // Obtains the future from its supplier but captures exceptions thrown by the supplier itself, + // turning them into corresponding failed futures. + private static ListenableFuture obtainSuppliedFuture( + Supplier> futureSupplier) { + try { + return futureSupplier.get(); + } catch (Throwable e) { + return immediateFailedFuture(e); + } + } + + // "Recover" static type information that is lost during the round-trip through the + // memo table. Notice that this is only safe as long as there are no key collisions. + @SuppressWarnings("unchecked") + private static ListenableFuture typedFuture(ListenableFuture future) { + return (ListenableFuture) future; + } + + @Override + public Executor executor() { + return executor; + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/DefaultAsyncDispatcher.java b/legacy/java/dev/cel/legacy/runtime/async/DefaultAsyncDispatcher.java new file mode 100644 index 000000000..49c1a8cb3 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/DefaultAsyncDispatcher.java @@ -0,0 +1,40 @@ +package dev.cel.legacy.runtime.async; + +import com.google.common.collect.ImmutableSet; +import dev.cel.common.CelOptions; +import dev.cel.common.ExprFeatures; +import dev.cel.runtime.StandardFunctions; + +/** Backward-compatible helper class for creating instances of {@link AsyncDispatcher}. */ +public final class DefaultAsyncDispatcher { + + /** + * Creates a new dispatcher with all standard functions using a provided type resolver and the + * provided custom set of {@link ExprFeatures} to enable various fixes and features. + * + *

It is recommended that callers supply {@link ExprFeatures#CURRENT} if they wish to + * automatically pick up fixes for CEL-Java conformance issues. + */ + public static AsyncDispatcher create( + TypeResolver typeResolver, ImmutableSet features) { + return create(typeResolver, CelOptions.fromExprFeatures(features)); + } + + public static AsyncDispatcher create(TypeResolver typeResolver, CelOptions celOptions) { + AsyncDispatcher dispatcher = new AsyncDispatcherBase(); + new StandardConstructs(typeResolver, celOptions).addAllTo(dispatcher); + StandardFunctions.addNonInlined(dispatcher, celOptions); + return dispatcher; + } + + /** Creates a new dispatcher with all standard functions and using the standard type resolver. */ + public static AsyncDispatcher create(ImmutableSet features) { + return create(CelOptions.fromExprFeatures(features)); + } + + public static AsyncDispatcher create(CelOptions celOptions) { + return create(StandardTypeResolver.getInstance(celOptions), celOptions); + } + + private DefaultAsyncDispatcher() {} +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/DummyAsyncContext.java b/legacy/java/dev/cel/legacy/runtime/async/DummyAsyncContext.java new file mode 100644 index 000000000..0b21f8f9e --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/DummyAsyncContext.java @@ -0,0 +1,26 @@ +package dev.cel.legacy.runtime.async; + +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + +import com.google.common.util.concurrent.ListenableFuture; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +/** + * Implements an {@link AsyncContext} without memoization and using the direct executor. For use in + * tests only. + */ +public enum DummyAsyncContext implements AsyncContext { + INSTANCE; + + @Override + public ListenableFuture perform( + Supplier> futureSupplier, Object... key) { + return futureSupplier.get(); + } + + @Override + public Executor executor() { + return directExecutor(); + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/DynamicEnv.java b/legacy/java/dev/cel/legacy/runtime/async/DynamicEnv.java new file mode 100644 index 000000000..4f7bd6060 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/DynamicEnv.java @@ -0,0 +1,147 @@ +package dev.cel.legacy.runtime.async; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +/** + * Dynamic environments combine the local context (aka a runtime stack) for local variables with the + * global context comprised of the resolver for global variables as well as the {@link AsyncContext} + * to be used during execution. The local context stores futures rather than plain objects, making + * it possible for comprehensions to treat individual steps lazily and allow for short-circuiting + * evaluations. + */ +public final class DynamicEnv { + + private final GlobalContext globalContext; + private final LocalContext localContext; + private final List globalValueNames; + private final ImmutableList>> cachedGlobals; + + /** Creates a dynamic environment by pairing the given global and local contexts. */ + private DynamicEnv( + GlobalContext globalContext, LocalContext localContext, List globalValueNames) { + this.globalContext = globalContext; + this.localContext = localContext; + this.globalValueNames = globalValueNames; + this.cachedGlobals = + globalValueNames.stream().map(globalContext::resolve).collect(toImmutableList()); + } + + /** Creates a fresh dynamic environment and populates the stack with the given locals. */ + DynamicEnv( + GlobalContext globalContext, + List> locals, + List globalValueNames) { + this(globalContext, LocalContext.create(locals), globalValueNames); + } + + /** Creates a fresh dynamic environment from the given global context and no locals. */ + DynamicEnv(GlobalContext globalContext, List globalValueNames) { + this(globalContext, LocalContext.create(ImmutableList.of()), globalValueNames); + } + + /** Implements the runtime stack for local bindings. */ + private static final class LocalContext { + @Nullable final LocalContext parent; + private final ImmutableList> slots; + + /** + * Effectively clones the parent and creates a new frame into which bindings from the given + * locals are installed. + */ + LocalContext(@Nullable LocalContext parent, List> locals) { + this.parent = parent; + this.slots = locals.stream().map(FluentFuture::from).collect(toImmutableList()); + } + + /** + * Returns a context with just the given local bindings. In the degenerate case where the + * bindings are empty, the result is null. + */ + @Nullable + static LocalContext create(List> locals) { + return locals.isEmpty() ? null : new LocalContext(null, locals); + } + + /** + * Retrieves slot that is offset elements away from the top of the stack. Since the top of the + * stack (corresponding to offset 0) is not a slot itself, valid slot numbers start at 1. + */ + FluentFuture getAtSlotOffset(int offset) { + int numSlots = slots.size(); + // An implicit invariant of the compilation algorithm is that parent is guaranteed + // to be non-null when offset exceeds numSlots. The value numSlots describes the number + // "locally" available slots (within the current frame). The global invariant is that + // a dynamic environment (a "stack") always has sufficiently many slots in its local context + // so that calls of getAtSlotOffset(...) can be satisfied. This implies that when the + // top-most frame does not have enough slots, then these slots must exist in the parent - + // which therefore cannot be null in that case. + return offset > numSlots + ? parent.getAtSlotOffset(offset - numSlots) + : slots.get(numSlots - offset); + } + } + + /** + * Clones the current environment and extends the result with the given values corresponding to + * local variables by pushing them onto the stack. + */ + public DynamicEnv extend(FluentFuture... futures) { + return new DynamicEnv( + globalContext, new LocalContext(localContext, Arrays.asList(futures)), globalValueNames); + } + + /** Obtains the value of a global variable by invoking the resolver-provided supplier. */ + FluentFuture getGlobal(int index) { + return FluentFuture.from(cachedGlobals.get(index).get()); + } + + /** + * Obtains the value of a local variable by accessing the given stack location relative to the + * current top of the stack. + */ + public FluentFuture getLocalAtSlotOffset(int offset) { + return localContext.getAtSlotOffset(offset); + } + + /** + * Clones the stack of this dynamic environment while substituting the global context given by the + * arguments. The result will only have a single frame containing all bindings. + * + *

Background: A {@link DynamicEnv} instance can be viewed as pairing the stack for local + * bindings on the one hand with the global state (which includes global bindings as well as the + * execution context) on the other. + * + *

Most of the time the global state remains fixed while the stack changes according to how the + * flow of execution traverses the local binding structure of the program. But in some situation + * it is useful to keep local bindings fixed while substituting a different global context. This + * method provides the mechanism for that. + * + *

Notice that the current global references must be retained. + */ + public DynamicEnv withGlobalContext(GlobalContext otherGlobalContext) { + return new DynamicEnv(otherGlobalContext, localContext, globalValueNames); + } + + /** Provides access to the global context. */ + public GlobalContext globalContext() { + return globalContext; + } + + /** Provides access to the context. */ + public AsyncContext currentContext() { + return globalContext().context(); + } + + /** Provides access to the global resolver itself. */ + public AsyncCanonicalResolver globalResolver() { + return globalContext().resolver(); + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/Effect.java b/legacy/java/dev/cel/legacy/runtime/async/Effect.java new file mode 100644 index 000000000..ea02c99cb --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/Effect.java @@ -0,0 +1,42 @@ +package dev.cel.legacy.runtime.async; + +/** + * Represents the effect that a CEL computation has. + * + *

Context-independent computations are close approximations of mathematical expressions which + * denote one particular value. Context-independent functions yield equal results when applied to + * equal argumnents. These computations are marked as {@link Effect#CONTEXT_INDEPENDENT}. + * + *

Context-dependent computations implicitly reference the "current state of the world". They are + * not permitted to change the state of the world, but their value may depend on it. An optimizer + * may drop unneeded context-dependent sub-expressions It is even permissible to reorder calls and + * to perform common subexpression elimination involving context-dependent computations (under the + * assumption that the relevant parts of the state of the world do not change during an invocation + * of {@link AsyncInterpretable#eval}). However, evaluation must not be moved from runtime to + * compile time. These computations are marked as {@link Effect#CONTEXT_DEPENDENT}. + */ +public enum Effect { + // No effect, independent of context. May be compile-time evaluated. + CONTEXT_INDEPENDENT { + @Override + public Effect meet(Effect other) { + return other; + } + }, + // Has read effects on the context but must otherwise be pure. Must not be compile-time + // evaluated, but can be subject to reordering, CSE, elimination of unused computations, + // memoization (within one activation context), etc. + CONTEXT_DEPENDENT { + @Override + public Effect meet(Effect other) { + return this; + } + }; + + /** + * Combines effects, taking the greatest lower bound. This is based on a view where + * DEFERRING_CONTEXT_INDEPENDENT is lower than CONTEXT_INDEPENDENT and CONTEXT_DEPENDENT is lower + * than both. + */ + public abstract Effect meet(Effect other); +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/EvaluationHelpers.java b/legacy/java/dev/cel/legacy/runtime/async/EvaluationHelpers.java new file mode 100644 index 000000000..bd8375591 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/EvaluationHelpers.java @@ -0,0 +1,335 @@ +package dev.cel.legacy.runtime.async; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.util.concurrent.Futures.immediateFailedFuture; +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import dev.cel.common.CelErrorCode; +import dev.cel.runtime.InterpreterException; +import dev.cel.runtime.Metadata; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * A collection of helper methods. These provide the implementations of all default (convenience) + * methods in {@link FunctionResolver} and can also be used in custom registrars and resolvers. + */ +public final class EvaluationHelpers { + + /** + * Applies constant folding of a context-independent strict function when all its arguments are + * constants themselves. + */ + private static CompiledExpression constantfoldStrictCall( + FunctionRegistrar.StrictFunction function, List compiledArguments) { + return compiledConstantOrThrowing( + () -> + Futures.getDone( + function.apply( + COMPILE_TIME_GLOBAL_CONTEXT, transform(compiledArguments, e -> e.constant())))); + } + + /** + * Constructs the call of a strict function. Context-independent functions are simply applied, + * while context-dependent function calls are memoized using the {@link AsyncContext#perform} + * method. + */ + // This lambda implements @Immutable interface 'ExecutableExpression', but 'List' is mutable + @SuppressWarnings("Immutable") + private static CompiledExpression constructStrictCall( + FunctionRegistrar.StrictFunction function, + String memoizationKey, + Effect functionEffect, + List compiledArguments) { + List executableArguments = new ArrayList<>(); + Effect effect = functionEffect; + for (CompiledExpression argument : compiledArguments) { + if (argument.isThrowing()) { + return argument; + } + executableArguments.add(argument.toExecutable()); + effect = effect.meet(argument.effect()); + } + if (functionEffect.equals(Effect.CONTEXT_INDEPENDENT)) { + return CompiledExpression.executable( + stack -> + execAllAsList(executableArguments, stack) + .transformAsync( + arguments -> function.apply(stack.globalContext(), arguments), + directExecutor()), + effect); + } + return CompiledExpression.executable( + stack -> + execAllAsList(executableArguments, stack) + .transformAsync( + arguments -> + stack + .currentContext() + .perform( + () -> function.apply(stack.globalContext(), arguments), + memoizationKey, + arguments), + directExecutor()), + effect); + } + + /** + * Compiles the call of an overloadable function by either constant-folding it or by constructing + * the residual runtime call. + */ + public static CompiledExpression compileStrictCall( + FunctionRegistrar.StrictFunction function, + String memoizationKey, + Effect functionEffect, + List identifiedCompiledArguments) + throws InterpreterException { + List compiledArguments = new ArrayList<>(); + boolean allConstant = true; + for (var ca : identifiedCompiledArguments) { + CompiledExpression e = ca.expression(); + compiledArguments.add(e); + if (!e.isConstant()) { + allConstant = false; + } + } + return functionEffect.equals(Effect.CONTEXT_INDEPENDENT) && allConstant + ? constantfoldStrictCall(function, compiledArguments) + : constructStrictCall(function, memoizationKey, functionEffect, compiledArguments); + } + + /** + * Attempts to constant-fold a call of a context-independent "no-barrier" function. Returns an + * empty result if the call needed the value of one of its non-constant arguments. + */ + private static CompiledExpression constantfoldNobarrierCall( + FunctionRegistrar.NobarrierFunction function, List compiledArguments) { + return compiledConstantOrThrowing( + () -> + Futures.getDone( + function.apply( + COMPILE_TIME_GLOBAL_CONTEXT, + transform(compiledArguments, e -> getStaticArgumentFuture(e))))); + } + + /** Constructs the call of a "no-barrier" function. */ + // This lambda implements @Immutable interface 'ExecutableExpression', but 'List' is mutable + @SuppressWarnings("Immutable") + private static CompiledExpression constructNobarrierCall( + FunctionRegistrar.NobarrierFunction function, + Effect functionEffect, + List compiledArguments) { + Effect effect = functionEffect; + List executableArguments = new ArrayList<>(); + for (CompiledExpression argument : compiledArguments) { + executableArguments.add(argument.toExecutable()); + effect = effect.meet(argument.effect()); + } + return CompiledExpression.executable( + stack -> + FluentFuture.from( + function.apply( + stack.globalContext(), + transform(executableArguments, arg -> arg.execute(stack)))), + effect); + } + + /** + * Compiles the call of a "no-barrier" function by either constant-folding it or by constructing + * the residual runtime call. + */ + public static CompiledExpression compileNobarrierCall( + FunctionRegistrar.NobarrierFunction function, + Effect functionEffect, + List identifiedCompiledArguments) + throws InterpreterException { + List compiledArguments = new ArrayList<>(); + boolean allStatic = true; + for (var ia : identifiedCompiledArguments) { + CompiledExpression e = ia.expression(); + compiledArguments.add(e); + if (e.isExecutable()) { + allStatic = false; + } + } + return functionEffect.equals(Effect.CONTEXT_INDEPENDENT) && allStatic + ? constantfoldNobarrierCall(function, compiledArguments) + : constructNobarrierCall(function, functionEffect, compiledArguments); + } + + /** Implements immediateFuture for {@link FluentFuture}s. */ + public static FluentFuture immediateValue(A a) { + return FluentFuture.from(immediateFuture(a)); + } + + /** Implements immediateFailedFuture for {@link FluentFuture}s. */ + public static FluentFuture immediateException(Throwable t) { + return FluentFuture.from(immediateFailedFuture(t)); + } + + /** + * Transforms a list. Unlike {@link Lists#transform} This does not produce a transformed "view" of + * the original. Instead, the result is stored in an immutable list and does not update itself + * should the input get updated later. + */ + public static ImmutableList transform(List input, Function function) { + return input.stream().map(function).collect(toImmutableList()); + } + + /** + * Turns the given list of futures into a future of a list. If at least one of the futures fails, + * the result future fails with the exception of the earliest (lowes-index) failing input future. + */ + public static ListenableFuture> allAsListOrFirstException( + Iterable> futures) { + return buildListOrFirstException(futures.iterator(), ImmutableList.builder()); + } + + /** Helper for {@link #allAsListOrFirstException}. */ + private static ListenableFuture> buildListOrFirstException( + Iterator> futures, ImmutableList.Builder builder) { + if (!futures.hasNext()) { + return immediateFuture(builder.build()); + } + return FluentFuture.from(futures.next()) + .transformAsync(v -> buildListOrFirstException(futures, builder.add(v)), directExecutor()); + } + + /** Executes all expressions and arranges for the results to be combined into a single list. */ + public static FluentFuture> execAllAsList( + List executables, DynamicEnv stack) { + return FluentFuture.from( + allAsListOrFirstException(transform(executables, exp -> exp.execute(stack)))); + } + + /** + * Creates a constant compiled expression from the supplied constant. If the supplier throws an + * exception, a throwing expression is created instead. + */ + public static CompiledExpression compiledConstantOrThrowing(Callable supplier) { + try { + return CompiledExpression.constant(supplier.call()); + } catch (ExecutionException execExn) { + return CompiledExpression.throwing(execExn.getCause()); + } catch (Exception exn) { + return CompiledExpression.throwing(exn); + } + } + + /** Enforces that the given value is a boolean. Throws an explanatory exception otherwise. */ + public static Object expectBoolean(Object b, Metadata metadata, long id) + throws InterpreterException { + if (b instanceof Boolean) { + return b; + } + throw new InterpreterException.Builder("expected boolean value, found: %s", b) + .setErrorCode(CelErrorCode.INVALID_ARGUMENT) + .setLocation(metadata, id) + .build(); + } + + /** Extracts a boolean value. */ + public static boolean asBoolean(Object value) { + return (value instanceof Boolean) && (boolean) value; + } + + /** + * Decorates the given expression to account for the fact that it is expected to compute a boolean + * result. + */ + public static CompiledExpression asBooleanExpression( + CompiledExpression e, Metadata metadata, long id) throws InterpreterException { + return e.map( + (exe, eff) -> + CompiledExpression.executable( + stack -> + exe.execute(stack) + .transformAsync( + obj -> immediateValue(expectBoolean(obj, metadata, id)), + directExecutor()), + eff), + c -> CompiledExpression.constant(expectBoolean(c, metadata, id)), + t -> CompiledExpression.throwing(t)); + } + + /** + * Runs the given {@link ExecutableExpression} with an empty stack and a dummy global context, + * constructing a {@link CompiledExpression} from the resulting value or exception. + * + *

Note: Using this method requires that the expression did not occur within the scope of any + * local bindings, and that it does not make use of the global context during execution - either + * by accessing global variables or by invoking functions that are context-sensitive. + * + *

Not accessing global variables during execution does not mean that the expression + * does not mention any global variables, though. In particular, this can happen when parts + * or all of the expression represent a "suspended" computation. The list of global references + * must contain all mentioned global variables at their correct positions. + */ + static CompiledExpression executeStatically( + ExecutableExpression executable, List globalReferences) { + return compiledConstantOrThrowing( + () -> + Futures.getDone( + executable.execute(new DynamicEnv(COMPILE_TIME_GLOBAL_CONTEXT, globalReferences)))); + } + + /** + * Returns a future of the value or exception corresponding to a constant or throwing computation. + */ + private static FluentFuture getStaticArgumentFuture(CompiledExpression compiled) { + if (compiled.isExecutable()) { + throw new IllegalStateException("non-static argument during constant-folding"); + } + if (compiled.isConstant()) { + return immediateValue(compiled.constant()); + } + return immediateException(compiled.throwing()); + } + + /** Special resolver for global variables during constant folding. */ + private static Supplier> indicateNeededGlobal(String name) { + return () -> { + throw new IllegalStateException("access to global variable during constant folding: " + name); + }; + } + + /** AsyncContext used for compile-time computations. Uses no memoization and a direct executor. */ + private static final AsyncContext COMPILE_TIME_ASYNC_CONTEXT = + new AsyncContext() { + @Override + public ListenableFuture perform( + Supplier> resultFutureSupplier, Object... keys) { + return resultFutureSupplier.get(); + } + + @Override + public Executor executor() { + return directExecutor(); + } + + @Override + public boolean isRuntime() { + return false; + } + }; + + /** + * {@link GlobalContext} used for compile-time computations. Uses no memoization, a direct + * executor, and access to global variables results in an {@link IllegalStateException}. + */ + public static final GlobalContext COMPILE_TIME_GLOBAL_CONTEXT = + GlobalContext.of(COMPILE_TIME_ASYNC_CONTEXT, EvaluationHelpers::indicateNeededGlobal); + + private EvaluationHelpers() {} +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/Evaluator.java b/legacy/java/dev/cel/legacy/runtime/async/Evaluator.java new file mode 100644 index 000000000..7c2cc5758 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/Evaluator.java @@ -0,0 +1,1232 @@ +package dev.cel.legacy.runtime.async; + +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static com.google.common.util.concurrent.MoreExecutors.newSequentialExecutor; +import static dev.cel.legacy.runtime.async.Effect.CONTEXT_INDEPENDENT; +import static dev.cel.legacy.runtime.async.EvaluationHelpers.allAsListOrFirstException; +import static dev.cel.legacy.runtime.async.EvaluationHelpers.asBoolean; +import static dev.cel.legacy.runtime.async.EvaluationHelpers.compiledConstantOrThrowing; +import static dev.cel.legacy.runtime.async.EvaluationHelpers.execAllAsList; +import static dev.cel.legacy.runtime.async.EvaluationHelpers.executeStatically; +import static dev.cel.legacy.runtime.async.EvaluationHelpers.expectBoolean; +import static dev.cel.legacy.runtime.async.EvaluationHelpers.immediateException; +import static dev.cel.legacy.runtime.async.EvaluationHelpers.immediateValue; +import static dev.cel.legacy.runtime.async.EvaluationHelpers.transform; + +import dev.cel.expr.CheckedExpr; +import dev.cel.expr.Constant; +import dev.cel.expr.Expr; +import dev.cel.expr.Expr.Call; +import dev.cel.expr.Expr.Comprehension; +import dev.cel.expr.Expr.CreateList; +import dev.cel.expr.Expr.CreateStruct; +import dev.cel.expr.Expr.Select; +import dev.cel.expr.Reference; +import dev.cel.expr.Type; +import dev.cel.expr.Type.TypeKindCase; +import dev.cel.expr.Value; +import com.google.auto.value.AutoOneOf; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.primitives.UnsignedLong; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.errorprone.annotations.Immutable; +import dev.cel.checker.Types; +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelOptions; +import dev.cel.common.CelProtoAbstractSyntaxTree; +import dev.cel.runtime.DefaultMetadata; +import dev.cel.runtime.InterpreterException; +import dev.cel.runtime.Metadata; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.Executor; +import javax.annotation.Nullable; + +/** + * Implementation of an optimizing CEL interpreter based on futures. + * + *

Evaluation is split into two phases: preparation and execution. + * + *

The preparation phase traverses the abstract syntax, performs optimizations, and produces an + * {@link AsyncInterpretable}. + * + *

The execution phase consists of invocations of {@link AsyncInterpretable#evaluate} or {@link + * AsyncInterpretable#eval}. It is expected that one preparation is amortized over many executions. + * + *

Optimizations include: + * + *

    + *
  • Elimination of interpretative overhead. During execution the interpretable does not inspect + * the abstract syntax anymore. Control flow is directly baked into the interpretable. + *
  • Constant-time access to local variables by stack positions. + *
  • Compile-time resolution of function bindings from the dispatcher whenever the relevant + * overload is determined uniquely by type-checking. + *
  • Compile-time evaluation ("constant folding") of any part of the expression that does not + * depend on global variables or context-dependent functions. Calls of context-independent + * functions with constant arguments are evaluated at compile time. + *
  • Compile-time boolean short-circuiting and elimination of unreachable branches when + * conditions are known constants (based on the above constant-folding mechanism). + *
  • Elimination of code that is unreachable to do static knowledge about thrown exceptions. + *
  • Compile-time execution of comprehension loops with statically known inputs, applying + * constant-folding, short-circuiting, and elimination of unreachable code along the way. + *
+ */ +public class Evaluator implements AsyncInterpreter { + + private final TypeResolver typeResolver; + private final MessageProcessor messageProcessor; + private final FunctionResolver functionResolver; + private final boolean errorOnDuplicateKeys; + private final CelOptions celOptions; + + /** Standard constructor. */ + public Evaluator( + TypeResolver typeResolver, + MessageProcessor messageProcessor, + FunctionResolver functionResolver, + CelOptions celOptions) { + this.typeResolver = typeResolver; + this.messageProcessor = messageProcessor; + this.functionResolver = functionResolver; + this.celOptions = celOptions; + this.errorOnDuplicateKeys = this.celOptions.errorOnDuplicateMapKeys(); + } + + @Override + public AsyncInterpretable createInterpretable(CheckedExpr checkedExpr) + throws InterpreterException { + return createInterpretableOrConstant(checkedExpr, ImmutableMap.of(), ImmutableList.of()) + .toInterpretable(); + } + + // This lambda implements @Immutable interface 'AsyncInterpretable', but 'List' is mutable + @SuppressWarnings("Immutable") + @Override + public AsyncInterpretableOrConstant createInterpretableOrConstant( + CheckedExpr checkedExpr, + Map compileTimeConstants, + List localVariables) + throws InterpreterException { + int expectedNumLocals = localVariables.size(); + Compilation compilation = new Compilation(checkedExpr); + CompiledExpression compiled = + compilation.compiledExpression(compileTimeConstants, localVariables); + List globalReferences = compilation.globalReferences(); + return compiled.map( + (e, effect) -> + AsyncInterpretableOrConstant.interpretable( + effect, + new AsyncInterpretableWithFeatures( + (gctx, locals) -> { + Preconditions.checkArgument(locals.size() == expectedNumLocals); + return e.execute(new DynamicEnv(gctx, locals, globalReferences)); + }, + celOptions)), + c -> AsyncInterpretableOrConstant.constant(Optional.ofNullable(c)), + t -> + AsyncInterpretableOrConstant.interpretable( + Effect.CONTEXT_INDEPENDENT, + new AsyncInterpretableWithFeatures( + (gctx, locals) -> immediateException(t), celOptions))); + } + + /** + * This {@code AsyncInterpretable} implementation ensures that {@code CelOptions} are propagated + * to type-conversion classes used during async interpretation. + */ + @Immutable + private static class AsyncInterpretableWithFeatures implements AsyncInterpretable { + + private final AsyncInterpretable delegate; + private final CelOptions celOptions; + + private AsyncInterpretableWithFeatures(AsyncInterpretable delegate, CelOptions celOptions) { + this.delegate = delegate; + this.celOptions = celOptions; + } + + @Override + public ListenableFuture evaluate( + GlobalContext gctx, List> locals) { + return delegate.evaluate(gctx, locals); + } + + @Override + public ListenableFuture eval( + AsyncContext context, AsyncGlobalResolver global, List> locals) { + return evaluate(GlobalContext.of(context, new ResolverAdapter(global, celOptions)), locals); + } + } + + /** + * Represents the binding of a local variable at compile time. A binding can either be a slot + * number (i.e., a position within the runtime stack) or a concrete value that is known due to + * constant folding. + */ + @AutoOneOf(StaticBinding.Kind.class) + abstract static class StaticBinding { + enum Kind { + SLOT, // Stack slots used at runtime. + BOUND_VALUE // Compile-time known binding to value. Using Optional to avoid null. + } + + abstract Kind getKind(); + + abstract int slot(); + + static StaticBinding slot(int n) { + return AutoOneOf_Evaluator_StaticBinding.slot(n); + } + + abstract Optional boundValue(); + + static StaticBinding boundValue(Optional v) { + return AutoOneOf_Evaluator_StaticBinding.boundValue(v); + } + + /** Returns the actual bound value (which may be null). */ + @Nullable + Object value() { + return boundValue().orElse(null); + } + + /** Constructs a value binding directly from the nullable value. */ + static StaticBinding value(@Nullable Object v) { + return boundValue(Optional.ofNullable(v)); + } + } + + /** Static environments map variable names to their stack positions or to known values. */ + private class StaticEnv { + + private final Map bindings; + private int numSlots; + + /** Creates an empty environment that maps only the given compile-time constants. */ + public StaticEnv(Map compileTimeConstants, List localVariables) { + this.bindings = new HashMap<>(); + for (Map.Entry c : compileTimeConstants.entrySet()) { + this.bindings.put(c.getKey(), StaticBinding.value(c.getValue())); + } + this.numSlots = 0; + for (String localVariable : localVariables) { + this.bindings.put(localVariable, StaticBinding.slot(this.numSlots++)); + } + } + + /** Clones the current environment. */ + private StaticEnv(StaticEnv parent) { + this.bindings = new HashMap<>(parent.bindings); + this.numSlots = parent.numSlots; + } + + /** Adds a slot binding for the given name. The slot is the next available slot on the stack. */ + private void push(String name) { + bindings.put(name, StaticBinding.slot(numSlots++)); + } + + /** Adds a value binding for the given name. This does not take up a stack slot at runtime. */ + private void bind(String name, @Nullable Object value) { + bindings.put(name, StaticBinding.value(value)); + } + + /** + * Clones the current environment and extends the result with slots for the given names, from + * left to right. + */ + public StaticEnv extendWithSlots(String... names) { + StaticEnv result = new StaticEnv(this); + for (String name : names) { + result.push(name); + } + return result; + } + + /** Clones the current environment and binds the given name to the given value in the result. */ + public StaticEnv extendWithValue(String name, @Nullable Object value) { + StaticEnv result = new StaticEnv(this); + result.bind(name, value); + return result; + } + + /** Determines whether the given name is currently mapped to a binding. */ + public boolean isInScope(String name) { + return bindings.containsKey(name); + } + + /** Returns the binding corresponding to the given name, or -1 if it is not in scope. */ + public StaticBinding bindingOf(String name) { + return bindings.get(name); + } + + /** + * Returns the slot number corresponding to the current top of the stack. + * + *

Let se be the static environment and de be its corresponding dynamic environment. If a + * binding in se is a slot s, then the corresponding runtime value will be at + * de.getLocalAtSlotOffset(se.top() - s). + */ + public int top() { + return numSlots; + } + + /** + * Returns the numeric stack offset for the named local variable or otherwise throws an {@link + * InterpreterException}. + */ + public int findStackOffset(@Nullable Metadata metadata, long exprId, String name) + throws InterpreterException { + @Nullable StaticBinding binding = bindingOf(name); + if (binding != null && binding.getKind() == StaticBinding.Kind.SLOT) { + return top() - binding.slot(); + } + throw new InterpreterException.Builder("no stack slot named %s", name) + .setLocation(metadata, exprId) + .build(); + } + } + + /** + * The compilation class encapsulates the compilation step from CEL expressions to {@link + * ExecutableExpression}s. Creating an object of this class initializes the compiler. A subsequent + * call of compiledExpression() performs the actual compilation and returns the {@link + * CompiledExpression}. + */ + private class Compilation { + + private final CheckedExpr checkedExpr; + private final Metadata metadata; + + // Accumulates the list of global variables that are referenced by the code. + private final List globalReferences = new ArrayList<>(); + + /** Initialize the compiler by creating the Compilation. */ + Compilation(CheckedExpr checkedExpr) { + this.checkedExpr = Preconditions.checkNotNull(checkedExpr); + this.metadata = + new DefaultMetadata(CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst()); + } + + /** + * Run the compiler and produce the compiled expression that corresponds to the constructor + * argument. Errors during the compilation step itself result in a thrown {@link + * InterpreterException}. + */ + public CompiledExpression compiledExpression( + Map compileTimeConstants, List localVariables) + throws InterpreterException { + return enforceCompleteness( + compile(checkedExpr.getExpr(), new StaticEnv(compileTimeConstants, localVariables))); + } + + /** Retrieves the list of global variables in index order. */ + public List globalReferences() { + return globalReferences; + } + + // Looks up the index of a global variable, registering it if it is not already known. + private int globalIndex(String name) { + int index = globalReferences.indexOf(name); + if (index < 0) { + index = globalReferences.size(); + globalReferences.add(name); + } + return index; + } + + /** Compiles a general CEL expression relative to the given static environment. */ + private CompiledExpression compile(Expr expr, StaticEnv env) throws InterpreterException { + switch (expr.getExprKindCase()) { + case CONST_EXPR: + return compileConstant(expr.getConstExpr()); + case IDENT_EXPR: + return compileIdent(expr.getId(), env); + case SELECT_EXPR: + return compileSelect(expr.getId(), expr.getSelectExpr(), env); + case CALL_EXPR: + return compileCall(expr.getId(), expr.getCallExpr(), env); + case LIST_EXPR: + return compileList(expr.getListExpr(), env); + case STRUCT_EXPR: + return compileStruct(expr.getId(), expr.getStructExpr(), env); + case COMPREHENSION_EXPR: + return compileComprehension(expr.getComprehensionExpr(), env); + default: + throw new IllegalArgumentException( + "unexpected expression kind: " + expr.getExprKindCase()); + } + } + + /** Evaluates a CEL constant to an Object representing its runtime value. */ + @Nullable + private Object evalConstant(Constant constExpr) throws InterpreterException { + switch (constExpr.getConstantKindCase()) { + case NULL_VALUE: + return constExpr.getNullValue(); + case BOOL_VALUE: + return constExpr.getBoolValue(); + case INT64_VALUE: + return constExpr.getInt64Value(); + case UINT64_VALUE: + if (celOptions.enableUnsignedLongs()) { + return UnsignedLong.fromLongBits(constExpr.getUint64Value()); + } + return constExpr.getUint64Value(); + case DOUBLE_VALUE: + return constExpr.getDoubleValue(); + case STRING_VALUE: + return constExpr.getStringValue(); + case BYTES_VALUE: + return constExpr.getBytesValue(); + default: + throw new IllegalArgumentException( + "unsupported constant case: " + constExpr.getConstantKindCase()); + } + } + + /** Compiles a CEL constant. */ + private CompiledExpression compileConstant(Constant constExpr) throws InterpreterException { + return CompiledExpression.constant(evalConstant(constExpr)); + } + + /** + * Compiles a CEL identifier which may be statically bound to a constant or refer to a variable + * (local or global). + */ + private CompiledExpression compileIdent(long id, StaticEnv env) throws InterpreterException { + return compileIdentReference(checkedExpr.getReferenceMapOrThrow(id), id, env); + } + + /** Compiles a CEL identifier given its corresponding reference. */ + private CompiledExpression compileIdentReference(Reference reference, long id, StaticEnv env) + throws InterpreterException { + if (reference.hasValue()) { + // Bound to a constant. + return compileConstant(reference.getValue()); + } + String name = reference.getName(); + // Local or global Variable. + if (!env.isInScope(name)) { + // Global. + return compileGlobalIdent(id, name); + } + StaticBinding binding = env.bindingOf(name); + switch (binding.getKind()) { + case SLOT: + { + int offset = env.top() - binding.slot(); + return CompiledExpression.executable( + stack -> stack.getLocalAtSlotOffset(offset), Effect.CONTEXT_INDEPENDENT); + } + case BOUND_VALUE: + return CompiledExpression.constant(binding.value()); + } + throw unexpected("compileIdentReference"); + } + + /** + * Compiles a CEL identifier that is known to be a global variable. The result contains a + * runtime check for unbound global variables. + */ + private CompiledExpression compileGlobalIdent(long id, String name) + throws InterpreterException { + // Check whether the type exists in the type check map as a 'type'. + Type checkedType = checkedExpr.getTypeMapMap().get(id); + if (checkedType != null && checkedType.getTypeKindCase() == TypeKindCase.TYPE) { + return resolveTypeIdent(id, checkedType); + } + int index = globalIndex(name); + return CompiledExpression.executable( + stack -> stack.getGlobal(index), Effect.CONTEXT_DEPENDENT); + } + + private CompiledExpression resolveTypeIdent(long id, Type type) throws InterpreterException { + Value typeValue = typeResolver.adaptType(type); + if (typeValue != null) { + return CompiledExpression.constant(typeValue); + } + throw new InterpreterException.Builder( + "expected a runtime type for '%s', but found none.", type) + .setErrorCode(CelErrorCode.TYPE_NOT_FOUND) + .setLocation(metadata, id) + .build(); + } + + private boolean hasMapEntryOrMessageField( + Metadata metadata, long id, Object mapOrMessage, String field) throws InterpreterException { + if (mapOrMessage instanceof Map) { + return ((Map) mapOrMessage).containsKey(field); + } + return messageProcessor.dynamicHasField(metadata, id, mapOrMessage, field); + } + + private Object getMapEntryOrMessageField( + Metadata metadata, long id, Object mapOrMessage, String field) throws InterpreterException { + if (mapOrMessage instanceof Map) { + return getMapEntryFromMap(metadata, id, (Map) mapOrMessage, field); + } + return messageProcessor.dynamicGetField(metadata, id, mapOrMessage, field); + } + + /** Compiles a CEL select expression representing a field presence test. */ + // This lambda implements @Immutable interface 'ExecutableExpression', but accesses instance + // method(s) 'hasMapEntryOrMessageField' on 'Compilation' which is not @Immutable. + @SuppressWarnings("Immutable") + private CompiledExpression compileFieldTest(long id, Select selectExpr, StaticEnv env) + throws InterpreterException { + String field = selectExpr.getField(); + Expr operand = selectExpr.getOperand(); + Type checkedOperandType = checkedExpr.getTypeMapOrDefault(operand.getId(), Types.DYN); + CompiledExpression compiledOperand = compile(operand, env); + if (Types.isDynOrError(checkedOperandType)) { + // No compile-time type information available, so perform a fully dynamic operation. + return compiledOperand.mapNonThrowing( + executableOperand -> + stack -> + executableOperand + .execute(stack) + .transformAsync( + mapOrMessage -> + immediateValue( + hasMapEntryOrMessageField(metadata, id, mapOrMessage, field)), + directExecutor()), + constantOperand -> + compiledConstantOrThrowing( + () -> hasMapEntryOrMessageField(metadata, id, constantOperand, field))); + } + // Use available compile-time type information. + switch (checkedOperandType.getTypeKindCase()) { + case MESSAGE_TYPE: + String messageType = checkedOperandType.getMessageType(); + MessageProcessor.FieldTester fieldTester = + messageProcessor.makeFieldTester(metadata, id, messageType, field); + return compiledOperand.mapNonThrowing( + executableOperand -> + stack -> + executableOperand + .execute(stack) + .transform(fieldTester::hasField, directExecutor()), + constantOperand -> + CompiledExpression.constant(fieldTester.hasField(constantOperand))); + case MAP_TYPE: + return compiledOperand.mapNonThrowing( + executableOperand -> + stack -> + executableOperand + .execute(stack) + .transformAsync( + mapObject -> + immediateValue(hasMapEntry(metadata, id, mapObject, field)), + directExecutor()), + constantOperand -> + compiledConstantOrThrowing( + () -> hasMapEntry(metadata, id, constantOperand, field))); + default: + throw new InterpreterException.Builder( + "[internal] presence test for field '%s' in type %s", field, checkedOperandType) + .setLocation(metadata, id) + .build(); + } + } + + /** Compiles a CEL select expression representing a field access. */ + // This lambda implements @Immutable interface 'ExecutableExpression', but accesses instance + // method(s) 'getMapEntryOrMessageField' on 'Compilation' which is not @Immutable. + @SuppressWarnings("Immutable") + private CompiledExpression compileFieldAccess(long id, Select selectExpr, StaticEnv env) + throws InterpreterException { + String field = selectExpr.getField(); + Expr operand = selectExpr.getOperand(); + Type checkedOperandType = checkedExpr.getTypeMapOrDefault(operand.getId(), Types.DYN); + CompiledExpression compiledOperand = compile(operand, env); + if (Types.isDynOrError(checkedOperandType)) { + // No compile-time type information available, so perform a fully dynamic operation. + return compiledOperand.mapNonThrowing( + executableOperand -> + stack -> + executableOperand + .execute(stack) + .transformAsync( + mapOrMessage -> + immediateValue( + getMapEntryOrMessageField(metadata, id, mapOrMessage, field)), + directExecutor()), + constantOperand -> + compiledConstantOrThrowing( + () -> getMapEntryOrMessageField(metadata, id, constantOperand, field))); + } + // Use available compile-time type information. + switch (checkedOperandType.getTypeKindCase()) { + case MESSAGE_TYPE: + String messageType = checkedOperandType.getMessageType(); + MessageProcessor.FieldGetter fieldGetter = + messageProcessor.makeFieldGetter(metadata, id, messageType, field); + return compiledOperand.mapNonThrowing( + executableOperand -> + stack -> + executableOperand + .execute(stack) + .transform(fieldGetter::getField, directExecutor()), + constantOperand -> + CompiledExpression.constant(fieldGetter.getField(constantOperand))); + case MAP_TYPE: + return compiledOperand.mapNonThrowing( + executableOperand -> + stack -> + executableOperand + .execute(stack) + .transformAsync( + mapObject -> + immediateValue(getMapEntry(metadata, id, mapObject, field)), + directExecutor()), + constantOperand -> + compiledConstantOrThrowing( + () -> getMapEntry(metadata, id, constantOperand, field))); + default: + throw new InterpreterException.Builder( + "[internal] access to field '%s' in type %s", field, checkedOperandType) + .setLocation(metadata, id) + .build(); + } + } + + /** + * Compiles a CEL select expression which may be field selection, field presence test, or an + * access of a variable via a qualified name. + */ + private CompiledExpression compileSelect(long id, Select selectExpr, StaticEnv env) + throws InterpreterException { + Reference reference = checkedExpr.getReferenceMapOrDefault(id, null); + if (reference != null) { + return compileIdentReference(reference, id, env); + } else if (selectExpr.getTestOnly()) { + return compileFieldTest(id, selectExpr, env); + } else { + return compileFieldAccess(id, selectExpr, env); + } + } + + /** Compiles a general expression that is to be used as a function argument. */ + // This lambda implements @Immutable interface 'ScopedExpression', but the declaration of type + // 'dev.cel.legacy.runtime.async.async.Evaluator.StaticEnv' is not + // annotated with @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + private IdentifiedCompiledExpression compileFunctionArg(Expr expr, StaticEnv env) + throws InterpreterException { + return IdentifiedCompiledExpression.of( + slots -> enforceCompleteness(compile(expr, env.extendWithSlots(slots))), + expr.getId(), + checkedExpr.getTypeMapMap()); + } + + /** + * Compiles a function call expression. + * + *

All special cases such as conditionals and logical AND and OR are handled by the {@link + * FunctionResolver}. + */ + // This method reference implements @Immutable interface StackOffsetFinder, but the declaration + // of type 'dev.cel.legacy.runtime.async.async.Evaluator.StaticEnv' is not + // annotated with @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + private CompiledExpression compileCall(long id, Call call, StaticEnv env) + throws InterpreterException { + Reference reference = checkedExpr.getReferenceMapOrThrow(id); + Preconditions.checkArgument(reference.getOverloadIdCount() > 0); + + List compiledArguments = new ArrayList<>(); + if (call.hasTarget()) { + compiledArguments.add(compileFunctionArg(call.getTarget(), env)); + } + for (Expr arg : call.getArgsList()) { + compiledArguments.add(compileFunctionArg(arg, env)); + } + + List overloadIds = reference.getOverloadIdList(); + String functionName = call.getFunction(); + return functionResolver + .constructCall( + metadata, + id, + functionName, + overloadIds, + compiledArguments, + messageProcessor, + env::findStackOffset) + .map( + (executable, effect) -> + // If the constructed result is executable but context-independent, and if + // this call site is not within scope of a local binding, then run it now. + env.top() == 0 && effect == CONTEXT_INDEPENDENT + ? executeStatically(executable, globalReferences()) + : CompiledExpression.executable(executable, effect), + CompiledExpression::constant, + CompiledExpression::throwing); + } + + /** Compiles an expression that is expected to return a boolean. */ + private CompiledExpression compileBoolean(Expr bool, StaticEnv env) + throws InterpreterException { + long id = bool.getId(); + return compile(bool, env) + .mapNonThrowing( + executableBool -> + stack -> + executableBool + .execute(stack) + .transformAsync( + b -> immediateValue(expectBoolean(b, metadata, id)), + directExecutor()), + constantBool -> + compiledConstantOrThrowing(() -> expectBoolean(constantBool, metadata, id))); + } + + /** Compiles a CEL list creation. */ + private CompiledExpression compileList(CreateList listExpr, StaticEnv env) + throws InterpreterException { + List compiledElements = new ArrayList<>(); + boolean onlyConstant = true; + Effect effect = Effect.CONTEXT_INDEPENDENT; + for (Expr e : listExpr.getElementsList()) { + CompiledExpression compiledElement = enforceCompleteness(compile(e, env)); + if (compiledElement.isThrowing()) { + return compiledElement; + } + onlyConstant = onlyConstant && compiledElement.isConstant(); + effect = effect.meet(compiledElement.effect()); + compiledElements.add(compiledElement); + } + if (onlyConstant) { + return CompiledExpression.constant(transform(compiledElements, e -> e.constant())); + } + ImmutableList executableElements = + transform(compiledElements, CompiledExpression::toExecutable); + return CompiledExpression.executable( + stack -> + execAllAsList(executableElements, stack) + .transform(list -> list, directExecutor()), + effect); + } + + /** Compiles a CEL map or message creation. */ + private CompiledExpression compileStruct(long id, CreateStruct structExpr, StaticEnv env) + throws InterpreterException { + Reference reference = checkedExpr.getReferenceMapOrDefault(id, null); + if (reference == null) { + return compileMap(structExpr, env); + } else { + return compileMessage(id, reference.getName(), structExpr, env); + } + } + + /** Compiles a CEL map creation. */ + // This lambda implements @Immutable interface 'ExecutableExpression', but 'IdEntry' has + // non-final field 'key' + @SuppressWarnings("Immutable") + private CompiledExpression compileMap(CreateStruct structExpr, StaticEnv env) + throws InterpreterException { + List> compiledEntries = new ArrayList<>(); + boolean onlyConstant = true; + Effect effect = Effect.CONTEXT_INDEPENDENT; + boolean hasDynamicKeys = false; + Set staticKeys = new HashSet<>(); + for (CreateStruct.Entry entry : structExpr.getEntriesList()) { + CompiledExpression compiledKey = compile(entry.getMapKey(), env); + effect = effect.meet(compiledKey.effect()); + if (compiledKey.isThrowing()) { + return compiledKey; + } + if (compiledKey.isConstant()) { + Object key = compiledKey.constant(); + if (staticKeys.contains(key)) { + if (errorOnDuplicateKeys) { + return CompiledExpression.throwing( + new InterpreterException.Builder("duplicate map key [%s]", key) + .setErrorCode(CelErrorCode.DUPLICATE_ATTRIBUTE) + .setLocation(metadata, entry.getId()) + .build()); + } + } else { + staticKeys.add(compiledKey.constant()); + } + } else { + hasDynamicKeys = true; + onlyConstant = false; + } + CompiledExpression compiledValue = enforceCompleteness(compile(entry.getValue(), env)); + effect = effect.meet(compiledValue.effect()); + if (compiledValue.isThrowing()) { + return compiledValue; + } + if (!compiledValue.isConstant()) { + onlyConstant = false; + } + compiledEntries.add(new IdEntry<>(entry.getId(), compiledKey, compiledValue)); + } + if (onlyConstant) { + Map map = new LinkedHashMap<>(); + compiledEntries.forEach(entry -> map.put(entry.key.constant(), entry.val.constant())); + return CompiledExpression.constant(map); + } else { + ImmutableList> executableEntries = + transform( + compiledEntries, + e -> new IdEntry<>(e.id, e.key.toExecutable(), e.val.toExecutable())); + if (hasDynamicKeys && errorOnDuplicateKeys) { + return CompiledExpression.executable( + stack -> + FluentFuture.from( + allAsListOrFirstException( + transform(executableEntries, e -> executeEntry(e, stack)))) + .transformAsync( + entries -> { + Map map = new LinkedHashMap<>(); + for (IdEntry entry : entries) { + if (map.containsKey(entry.key)) { + return immediateException( + new InterpreterException.Builder( + "duplicate map key [%s]", entry.key) + .setErrorCode(CelErrorCode.DUPLICATE_ATTRIBUTE) + .setLocation(metadata, entry.id) + .build()); + } + map.put(entry.key, entry.val); + } + return immediateValue(map); + }, + directExecutor()), + effect); + } + return CompiledExpression.executable( + stack -> + FluentFuture.from( + allAsListOrFirstException( + transform(executableEntries, e -> executeEntry(e, stack)))) + .transform( + entries -> { + Map map = new LinkedHashMap<>(); + entries.forEach(entry -> map.put(entry.key, entry.val)); + return map; + }, + directExecutor()), + effect); + } + } + + /** Compiles a CEL message creation. */ + private CompiledExpression compileMessage( + long id, String typeName, CreateStruct structExpr, StaticEnv env) + throws InterpreterException { + List labels = new ArrayList<>(); + List types = new ArrayList<>(); + List compiledValues = new ArrayList<>(); + boolean onlyConstant = true; + Effect effect = Effect.CONTEXT_INDEPENDENT; + for (CreateStruct.Entry entry : structExpr.getEntriesList()) { + Expr valueExpr = entry.getValue(); + Type valueType = checkedExpr.getTypeMapOrDefault(valueExpr.getId(), Types.DYN); + types.add(valueType); + CompiledExpression compiledValue = enforceCompleteness(compile(entry.getValue(), env)); + effect = effect.meet(compiledValue.effect()); + if (compiledValue.isThrowing()) { + return compiledValue; + } + onlyConstant = onlyConstant && compiledValue.isConstant(); + String fieldName = entry.getFieldKey(); + labels.add(fieldName); + compiledValues.add(compiledValue); + } + MessageProcessor.MessageCreator messageCreator = + messageProcessor.makeMessageCreator(metadata, id, typeName, labels, types); + if (onlyConstant) { + return compiledConstantOrThrowing( + () -> messageCreator.createMessage(Lists.transform(compiledValues, e -> e.constant()))); + } else { + ImmutableList executableValues = + transform(compiledValues, CompiledExpression::toExecutable); + return CompiledExpression.executable( + stack -> + FluentFuture.from( + allAsListOrFirstException( + Lists.transform(executableValues, v -> v.execute(stack)))) + .transformAsync( + vl -> immediateValue(messageCreator.createMessage(vl)), directExecutor()), + effect); + } + } + + /** + * Represents the mechanism used to compile comprehensions. + * + *

Compilation consists of a number of mutually recursive methods that collectively implement + * compile-time unrolling of comprehensions when the range is statically known. These methods + * all need to have access to the same underlying set of values (= the parts that make up the + * original comprehension expression). To avoid having to pass these parts around as explicit + * arguments they are made universally available as instance variables. + */ + class ComprehensionCompilation { + private final Expr rangeExpr; + private final Expr initExpr; + private final long iterId; + private final String accuVar; + private final String iterVar; + private final Expr conditionExpr; + private final Expr stepExpr; + private final Expr resultExpr; + private final StaticEnv parentEnv; + + /** Initializes the comprehension compiler. */ + ComprehensionCompilation(Comprehension comprehension, StaticEnv parentEnv) { + this.rangeExpr = comprehension.getIterRange(); + this.initExpr = comprehension.getAccuInit(); + this.iterId = rangeExpr.getId(); + this.accuVar = comprehension.getAccuVar(); + this.iterVar = comprehension.getIterVar(); + this.conditionExpr = comprehension.getLoopCondition(); + this.stepExpr = comprehension.getLoopStep(); + this.resultExpr = comprehension.getResult(); + this.parentEnv = parentEnv; + } + + /** Runs the comprehension compiler for the comprehesion that it was instantiated for. */ + public CompiledExpression compileComprehension() throws InterpreterException { + CompiledExpression compiledRange = compile(rangeExpr, parentEnv); + CompiledExpression compiledInit = compile(initExpr, parentEnv); + + /** + * Exceptions in range or init lead to immediate failure and never let the loop run at all. + */ + if (compiledRange.isThrowing()) { + return compiledRange; + } + + if (compiledRange.isConstant()) { + /** The range is constant, so run the loop at compile time as things stay constant. */ + Collection rangeIterable = null; + try { + rangeIterable = getRangeIterable(compiledRange.constant(), metadata, iterId); + } catch (Exception e) { + // Range is illegal, so the entire expression throws an exception. + return CompiledExpression.throwing(e); + } + return constantRangeLoop(rangeIterable.iterator(), compiledInit); + } else { + /** Range is not constant. Generate runtime loop. */ + return compiledRuntimeLoop(compiledRange, compiledInit); + } + } + + /** + * Arranges for the rest of a comprehension loop over a non-empty constant range to be + * performed at runtime. + */ + private CompiledExpression constantRangeLoopTail( + ImmutableList remainingRange, CompiledExpression compiledAccu) + throws InterpreterException { + return compiledRuntimeLoop(CompiledExpression.constant(remainingRange), compiledAccu); + } + + /** + * Executes a comprehension loop over a constant range at compile time as far as everything + * remains static and potentially defers the remaining loop until runtime. + */ + private CompiledExpression constantRangeLoop( + Iterator range, CompiledExpression compiledAccu) throws InterpreterException { + while (range.hasNext()) { + if (!compiledAccu.isConstant()) { + return constantRangeLoopTail(ImmutableList.copyOf(range), compiledAccu); + } + @Nullable Object nextValue = range.next(); + StaticEnv loopEnv = + parentEnv + .extendWithValue(accuVar, compiledAccu.constant()) + .extendWithValue(iterVar, nextValue); + CompiledExpression compiledCondition = compileBoolean(conditionExpr, loopEnv); + if (compiledCondition.isThrowing()) { + return compiledCondition; + } + if (!compiledCondition.isConstant()) { + // The condition is dynamic, so let constantRangeLoopTail handle everything + // (including the condition itself) by pushing nextValue back into the remaining + // range. + return constantRangeLoopTail( + ImmutableList.builder().add(nextValue).addAll(range).build(), compiledAccu); + } + if (!asBoolean(compiledCondition.constant())) { + break; + } + compiledAccu = compile(stepExpr, loopEnv); + } + // Reached the end of the loop, so bind the accumulator to its variable and + // compile the result expression in the corresponding scope. + return compiledAccu.map( + (executableAccu, accuEffect) -> + compile(resultExpr, parentEnv.extendWithSlots(accuVar)) + .map( + (executableResult, resultEffect) -> + CompiledExpression.executable( + stack -> + executableResult.execute( + stack.extend(executableAccu.execute(stack))), + accuEffect.meet(resultEffect)), + CompiledExpression::constant, + CompiledExpression::throwing), + constantAccu -> compile(resultExpr, parentEnv.extendWithValue(accuVar, constantAccu)), + CompiledExpression::throwing); + } + + /** Generates the runtime loop when compile-time unrolling is not feasible. */ + private CompiledExpression compiledRuntimeLoop( + CompiledExpression compiledRange, CompiledExpression compiledInit) + throws InterpreterException { + StaticEnv resultEnv = parentEnv.extendWithSlots(accuVar); + StaticEnv loopEnv = resultEnv.extendWithSlots(iterVar); + + CompiledExpression compiledCondition = compileBoolean(conditionExpr, loopEnv); + CompiledExpression compiledStep = compile(stepExpr, loopEnv); + CompiledExpression compiledResult = compile(resultExpr, resultEnv); + + Effect effect = + compiledRange + .effect() + .meet(compiledInit.effect()) + .meet(compiledCondition.effect()) + .meet(compiledStep.effect()) + .meet(compiledResult.effect()); + + ExecutableExpression executableCondition = compiledCondition.toExecutable(); + ExecutableExpression executableStep = compiledStep.toExecutable(); + ExecutableExpression executableResult = compiledResult.toExecutable(); + ExecutableExpression executableRange = compiledRange.toExecutable(); + ExecutableExpression executableInit = compiledInit.toExecutable(); + + // Calculate the range and the initial value, then construct the range iteration and apply + // it to the initial value. + return CompiledExpression.executable( + stack -> + executableRange + .execute(stack) + .transformAsync( + iterRangeRaw -> + new RangeIteration( + getRangeIterable(iterRangeRaw, metadata, iterId).iterator(), + executableResult, + executableCondition, + executableStep, + stack) + .iterate(executableInit.execute(stack)), + directExecutor()), + effect); + } + } + + /** Compiles a CEL comprehension expression. */ + private CompiledExpression compileComprehension(Comprehension comprehension, StaticEnv env) + throws InterpreterException { + // Initialize the comprehension compiler class and then run it. + return new ComprehensionCompilation(comprehension, env).compileComprehension(); + } + } + + // Private helper functions. + + // Compile-time matters. + + /** + * Wraps an excecutable expression with a runtime check that guards against {@link + * IncompleteData}. + */ + // This lambda implements @Immutable interface 'ExecutableExpression', but the declaration of type + // 'dev.cel.runtime.InterpreterException' is not annotated with + // @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + private static CompiledExpression enforceCompleteness(CompiledExpression compiled) { + return compiled.mapNonThrowing( + executable -> + stack -> + executable + .execute(stack) + .transformAsync(EvaluationHelpers::immediateValue, directExecutor()), + CompiledExpression::constant); + } + + // Runtime matters. + + /** + * Runs the computations for the key/value executable expressions and constructs a future that + * delivers the corresponding values, plus the id. + */ + private static FluentFuture> executeEntry( + IdEntry entry, DynamicEnv stack) { + return FluentFuture.from( + allAsListOrFirstException( + ImmutableList.of(entry.key.execute(stack), entry.val.execute(stack)))) + .transform( + list -> new IdEntry(entry.id, list.get(0), list.get(1)), directExecutor()); + } + + /** + * Obtains (at runtime) the iteration range of a comprehension by inspecting the runtime type of + * the result of evaluating the range expression. + */ + @SuppressWarnings("unchecked") + private static Collection getRangeIterable( + Object iterRangeRaw, Metadata metadata, long id) throws InterpreterException { + if (iterRangeRaw instanceof List) { + return (List) iterRangeRaw; + } else if (iterRangeRaw instanceof Map) { + return ((Map) iterRangeRaw).keySet(); + } else { + throw new InterpreterException.Builder( + "expected a list or a map for iteration range but got '%s'", + iterRangeRaw.getClass().getSimpleName()) + .setErrorCode(CelErrorCode.INVALID_ARGUMENT) + .setLocation(metadata, id) + .build(); + } + } + + /** + * Implements the runtime iteration of the body (condition and step) of a comprehension over its + * range by constructing a recursive function from initial accu value to the future producing the + * final result. + * + *

Since exclusive use of {@code directExecutor()} would cause Java stack overruns for very + * long iterations, direct execution is broken up in regular intervals. + */ + private static class RangeIteration { + private final Iterator range; + private final ExecutableExpression executableResult; + private final ExecutableExpression executableCondition; + private final ExecutableExpression executableStep; + private final DynamicEnv stack; + private final Executor trampoline; + + public RangeIteration( + Iterator range, + ExecutableExpression executableResult, + ExecutableExpression executableCondition, + ExecutableExpression executableStep, + DynamicEnv stack) { + this.range = range; + this.executableResult = executableResult; + this.executableCondition = executableCondition; + this.executableStep = executableStep; + this.stack = stack; + // Using directExecutor() throughout would lead to very deep recursion depths + // and ultimately to Java stack exhaustion when iterating over a very long list. + // Therefore, the loop is broken up by using a sequential executor every + // DIRECT_EXECUTION_BUDGET iterations. A seqential executor acts as a trampoline, + // thus avoiding unlimited recursion depth. + // If the original executor is not the direct executor, then using it directly + // as a the trampoline is potentially better (because it resets the stack depth to + // the toplevel rather than just to the start of the comprehesion loop). + this.trampoline = + stack.currentContext().executor() == directExecutor() + ? newSequentialExecutor(directExecutor()) + : stack.currentContext().executor(); + } + + public FluentFuture iterate(FluentFuture accuFuture) { + // By starting with zero budget the trampoline is initialized right at the + // beginning of the loop. + return loop(accuFuture, 0); + } + + private FluentFuture loop(FluentFuture accuFuture, int budget) { + if (!range.hasNext()) { + return executableResult.execute(stack.extend(accuFuture)); + } + Object elem = range.next(); + DynamicEnv loopStack = stack.extend(accuFuture, immediateValue(elem)); + // Break direct execution up every once in a while to avoid running + // out of Java stack in case of very long iteration ranges. + boolean budgetExhausted = budget <= 0; + Executor executor = budgetExhausted ? trampoline : directExecutor(); + int newBudget = budgetExhausted ? DIRECT_EXECUTION_BUDGET : (budget - 1); + return executableCondition + .execute(loopStack) + .transformAsync( + condition -> + asBoolean(condition) + ? loop( + // Applies trampoline at the loop step to avoid building up a deeply + // nested pending computation in the accumulator. + executableStep.execute(loopStack).transform(a -> a, executor), newBudget) + : executableResult.execute(stack.extend(accuFuture)), + executor); + } + } + + // Miscellaneous. + + /** + * Creates a dummy exception to be thrown in places where we don't expect control to reach (but + * the Java compiler is not smart enough to know that). + */ + private static RuntimeException unexpected(String where) { + return new RuntimeException("[internal] reached unexpected program point: " + where); + } + + private static Map expectMap(Metadata metadata, long id, Object mapObject) + throws InterpreterException { + if (mapObject instanceof Map) { + return (Map) mapObject; + } + throw new InterpreterException.Builder( + "[internal] Expected an instance of 'Map' but found '%s'", + mapObject.getClass().getName()) + .setLocation(metadata, id) + .build(); + } + + private static boolean hasMapEntry(Metadata metadata, long id, Object mapObject, String field) + throws InterpreterException { + return expectMap(metadata, id, mapObject).containsKey(field); + } + + private static Object getMapEntryFromMap(Metadata metadata, long id, Map map, String field) + throws InterpreterException { + Object entry = map.get(field); + if (entry == null) { + throw new InterpreterException.Builder("key '%s' is not present in map.", field) + .setErrorCode(CelErrorCode.ATTRIBUTE_NOT_FOUND) + .setLocation(metadata, id) + .build(); + } + return entry; + } + + private static Object getMapEntry(Metadata metadata, long id, Object mapObject, String field) + throws InterpreterException { + return getMapEntryFromMap(metadata, id, expectMap(metadata, id, mapObject), field); + } + + /** Like Map.Entry, but key and value are the same type, and associates an ID with the entry. */ + private static class IdEntry { + long id; + T key; + T val; + + IdEntry(long id, T key, T val) { + this.id = id; + this.key = key; + this.val = val; + } + } + + // Breaks up direct execution during comprehensions 5% of the time. + private static final int DIRECT_EXECUTION_BUDGET = 20; +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/ExecutableExpression.java b/legacy/java/dev/cel/legacy/runtime/async/ExecutableExpression.java new file mode 100644 index 000000000..f7cacaa45 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/ExecutableExpression.java @@ -0,0 +1,28 @@ +package dev.cel.legacy.runtime.async; + +import com.google.common.util.concurrent.FluentFuture; +import com.google.errorprone.annotations.Immutable; + +/** + * An executable expression is the outcome of "compiling" a CEL expression. The compiler effectively + * implements a form of denotational semantics, and executable expressions are the domain that CEL + * expressions are mapped to by this semantics. + * + *

Executable expressions represent the original CEL program, but with most of the interpretative + * overhead eliminated. In particular, there is no more traversal of abstract syntax during + * execution, and no dispatch on expression types. Local variables are accessed in constant time by + * precomputed stack location. + * + *

Executable expressions are modeled by functions that take the dynamic environment (for + * resolving free local and global variables) and an executor (for handling asynchronous aspects of + * the computation) as arguments and return a future of the result. + * + *

The future is produced without throwing exceptions. Any runtime exceptions are captured by the + * future itself by letting it fail. Interpretation-related errors are signalled by {@link + * InterpreterExceptions} that are the cause of the corresponding {@link ExecutionException}. + */ +@Immutable +@FunctionalInterface +public interface ExecutableExpression { + FluentFuture execute(DynamicEnv env); +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/FunctionRegistrar.java b/legacy/java/dev/cel/legacy/runtime/async/FunctionRegistrar.java new file mode 100644 index 000000000..a2688a01f --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/FunctionRegistrar.java @@ -0,0 +1,340 @@ +package dev.cel.legacy.runtime.async; + +import static com.google.common.util.concurrent.Futures.immediateFailedFuture; +import static com.google.common.util.concurrent.Futures.immediateFuture; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.InterpreterException; +import dev.cel.runtime.Metadata; +import dev.cel.runtime.Registrar; +import java.util.Arrays; +import java.util.List; +import javax.annotation.Nullable; + +/** + * Interface to an object that accepts bindings of Java implementations to CEL functions that can be + * used by the {@link Evaluator} via a corresponding {@link FunctionResolver}. + * + *

A class implementing this interface must define {@link FunctionRegistrar#addCallConstructor} + * and {@link FunctionRegistrar#addStrictFunction}. All other methods already have a default + * implementation in terms of these. + */ +public interface FunctionRegistrar extends Registrar { + + /** Constructs a {@link CompiledExpression} for the given call. */ + @Immutable + @FunctionalInterface + interface CallConstructor { + CompiledExpression construct( + @Nullable Metadata metadata, + long exprId, + List compiledArguments, + MessageProcessor messageProcessor, + StackOffsetFinder stackOffsetFinder) + throws InterpreterException; + } + + /** + * Simplified version of {@link CallConstructor} without the message processor and without the + * stack offset finder. + */ + @Immutable + @FunctionalInterface + interface SimpleCallConstructor { + CompiledExpression construct( + @Nullable Metadata metadata, + long exprId, + List compiledArguments) + throws InterpreterException; + } + + /** Simplified version of {@link CallConstructor} without the stack offset finder. */ + @Immutable + @FunctionalInterface + interface SimpleCallConstructorWithStackAccess { + CompiledExpression construct( + @Nullable Metadata metadata, + long exprId, + List compiledArguments, + StackOffsetFinder stackOffsetFinder) + throws InterpreterException; + } + + /** + * A function bound to one out of several possible overloads of a CEL function. + * + *

Overloadable functions have to be strict (i.e., their arguments get fully evaluated before + * they are called). This is a necessary prerequisite for runtime overload resolution, but it also + * opens the possibility of memoization. + */ + @Immutable + @FunctionalInterface + interface StrictFunction { + ListenableFuture apply(GlobalContext globalContext, List arguments); + } + + // Core functionality. + // + // All convenience methods below are imlpemented in terms of just + // addCallConstructor and addOverloadableFunction. + + /** + * Adds a generic call constructor for the given overload ID. Function calls with this overload ID + * will later be handled by the given {@link CallConstructor}. No runtime overloading is possible. + */ + void addCallConstructor(String overloadId, CallConstructor callConstructor); + + /** Adds a simple generic call constructor. */ + default void addCallConstructor(String overloadId, SimpleCallConstructor simpleCallConstructor) { + addCallConstructor( + overloadId, + (md, id, args, ignoredMessageProcessor, ignoredStackOffsetFinder) -> + simpleCallConstructor.construct(md, id, args)); + } + + /** Adds a simple generic call constructor with stack access. */ + default void addCallConstructor( + String overloadId, + SimpleCallConstructorWithStackAccess simpleCallConstructorWithStackAccess) { + addCallConstructor( + overloadId, + (md, id, args, ignoredMessageProcessor, stackOffsetFinder) -> + simpleCallConstructorWithStackAccess.construct(md, id, args, stackOffsetFinder)); + } + + /** Registers one possible binding for a runtime-overloadable function. */ + void addStrictFunction( + String overloadId, + List> argumentTypes, + boolean contextIndependent, + StrictFunction strictFunction); + + // Convenience methods with default implementations. + // A class implementing interface {@link FunctionRegistry} should not provide + // its own implementations. + + /** Interface to type unary asynchronous function. */ + @Immutable + @FunctionalInterface + interface StrictUnaryFunction { + ListenableFuture apply(T arg); + } + + /** Interface to typed binary asynchronous function with barrier synchronization. */ + @Immutable + @FunctionalInterface + interface StrictBinaryFunction { + ListenableFuture apply(T1 arg1, T2 arg2); + } + + /** + * Interface to a general asynchronous function that operates without implicit barrier + * synchronization on argument evaluation. All arguments are being evaluated, though. These + * functions are also non-strict in the sense that an error in an unused argument will not lead to + * an error in the function application itself. + */ + @Immutable + @FunctionalInterface + interface NobarrierFunction { + ListenableFuture apply( + GlobalContext globalContext, List> args); + } + + /** + * Registers general asynchronous overloadable function. + * + *

Caution: The function's continuation will run on its returned future's executor. Make sure + * to only use this registration method if such an arrangement is safe. If the future's executor + * (which might, for example, be an RPC event manager's executor) is not appropriate, then prefer + * using {@link #addAsync}. + */ + default void addDirect( + String overloadId, List> argTypes, Effect effect, StrictFunction function) { + addStrictFunction(overloadId, argTypes, effect.equals(Effect.CONTEXT_INDEPENDENT), function); + } + + /** + * Registers a general asynchronous {@link StrictFunction} that takes a variable number of + * arguments. + * + *

Caveat: This function cannot participate in runtime overload resolution. + * + *

Caution: The function's continuation will run on its returned future's executor. Make sure + * to only use this registration method if such an arrangement is safe. If the future's executor + * (which might, for example, be an RPC event manager's executor) is not appropriate, then prefer + * using {@link #addAsync}. + */ + default void addDirect(String overloadId, Effect effect, StrictFunction function) { + addCallConstructor( + overloadId, + (metadata, exprId, compiledArguments) -> + EvaluationHelpers.compileStrictCall(function, overloadId, effect, compiledArguments)); + } + + /** + * Registers typed unary asynchronous function. The function's continuation will use the returned + * future's executor. + */ + default void addDirect( + String overloadId, Class argType, Effect effect, StrictUnaryFunction function) { + addDirect( + overloadId, + ImmutableList.of(argType), + effect, + (gctx, args) -> function.apply(argType.cast(args.get(0)))); + } + + /** + * Registers typed binary asynchronous function. The function's continuation will use the returned + * future's executor. + */ + default void addDirect( + String overloadId, + Class argType1, + Class argType2, + Effect effect, + StrictBinaryFunction function) { + addDirect( + overloadId, + ImmutableList.of(argType1, argType2), + effect, + (gctx, args) -> function.apply(argType1.cast(args.get(0)), argType2.cast(args.get(1)))); + } + + /** + * Registers an executor-coupling version of the given asynchronous {@link StrictFunction}. + * Coupling guarantees that transformations on the result run on the context's executor even if + * they specify {@code MoreExecutors#directExecutor}. + */ + default void addAsync( + String overloadId, List> argTypes, Effect effect, StrictFunction function) { + addDirect( + overloadId, + argTypes, + effect, + (gctx, args) -> + gctx.context().coupleToExecutorInRequestContext(() -> function.apply(gctx, args))); + } + + /** Registers variadic asynchronous {@link StrictFunction} in executor-coupling fashion. */ + default void addAsync(String overloadId, Effect effect, StrictFunction function) { + addDirect( + overloadId, + effect, + (gctx, args) -> + gctx.context().coupleToExecutorInRequestContext(() -> function.apply(gctx, args))); + } + + /** Registers typed unary asynchronous function in executor-coupling fashion. */ + default void addAsync( + String overloadId, Class argType, Effect effect, StrictUnaryFunction function) { + addDirect( + overloadId, + ImmutableList.of(argType), + effect, + (gctx, args) -> + gctx.context() + .coupleToExecutorInRequestContext(() -> function.apply(argType.cast(args.get(0))))); + } + + /** Registers typed binary asynchronous function in executor-coupling fashion. */ + default void addAsync( + String overloadId, + Class argType1, + Class argType2, + Effect effect, + StrictBinaryFunction function) { + addDirect( + overloadId, + ImmutableList.of(argType1, argType2), + effect, + (gctx, args) -> + gctx.context() + .coupleToExecutorInRequestContext( + () -> function.apply(argType1.cast(args.get(0)), argType2.cast(args.get(1))))); + } + + /** Registers a no-barrier asynchronous function. */ + default void addNobarrierAsync(String overloadId, Effect effect, NobarrierFunction function) { + addCallConstructor( + overloadId, + (metadata, exprId, compiledArguments) -> + EvaluationHelpers.compileNobarrierCall(function, effect, compiledArguments)); + } + + // Registrar methods + + /** Adds a unary function to the dispatcher. */ + @Override + default void add(String overloadId, Class argType, UnaryFunction function) { + addDirect( + overloadId, + argType, + Effect.CONTEXT_INDEPENDENT, + arg -> { + try { + return immediateFuture(function.apply(arg)); + } catch (RuntimeException e) { + return immediateFailedFuture( + new InterpreterException.Builder( + e, "Function '%s' failed with arg(s) '%s'", overloadId, arg) + .build()); + } catch (Exception e) { + return immediateFailedFuture(InterpreterException.wrapOrThrow(e)); + } + }); + } + + /** Adds a binary function to the dispatcher. */ + @Override + default void add( + String overloadId, Class argType1, Class argType2, BinaryFunction function) { + addDirect( + overloadId, + argType1, + argType2, + Effect.CONTEXT_INDEPENDENT, + (arg1, arg2) -> { + try { + return immediateFuture(function.apply(arg1, arg2)); + } catch (RuntimeException e) { + return immediateFailedFuture( + new InterpreterException.Builder( + e, + "Function '%s' failed with arg(s) '%s'", + overloadId, + Joiner.on(", ").join(Arrays.asList(arg1, arg2))) + .build()); + } catch (Exception e) { + return immediateFailedFuture(InterpreterException.wrapOrThrow(e)); + } + }); + } + + /** Adds a general function to the dispatcher. */ + @Override + default void add(String overloadId, List> argTypes, Function function) { + addDirect( + overloadId, + argTypes, + Effect.CONTEXT_INDEPENDENT, + (gctx, args) -> { + try { + return immediateFuture(function.apply(args.toArray())); + } catch (RuntimeException e) { + return immediateFailedFuture( + new InterpreterException.Builder( + e, + "Function '%s' failed with arg(s) '%s'", + overloadId, + Joiner.on(", ").join(args)) + .build()); + } catch (Exception e) { + return immediateFailedFuture(InterpreterException.wrapOrThrow(e)); + } + }); + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/FunctionResolver.java b/legacy/java/dev/cel/legacy/runtime/async/FunctionResolver.java new file mode 100644 index 000000000..326a3621f --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/FunctionResolver.java @@ -0,0 +1,42 @@ +package dev.cel.legacy.runtime.async; + +import dev.cel.runtime.InterpreterException; +import dev.cel.runtime.Metadata; +import java.util.List; +import javax.annotation.Nullable; + +/** + * Interface to an object that the {@link Evaluator} can use to resolve calls of Java-coded CEL + * functions. The primary method {@link FunctionResolver#constructCall} constructs the compiled + * expression corresponding to a call of the specified function given its compiled arguments. + * + *

Actual function bindings are usually established (although strictly speaking that is not + * necessary from the {@link Evaluator}'s point of view) by combining a {@link FunctionResolver} + * with a corresponding {@link FunctionRegistrar}. The typical use case is codified by {@link + * AsyncDispatcher}. + */ +public interface FunctionResolver { + + /** + * Constructs the compliled CEL expression that implements the call of a function at some call + * site. + * + *

If multiple overload IDs are given, then a runtime dispatch is implemented. All overload IDs + * must refer to strict functions in that case. + * + *

The construction may apply arbitrary optimizations. For example, multiplication with 0 might + * just generate the constant 0, regardless of the second argument. + */ + CompiledExpression constructCall( + @Nullable Metadata metadata, + long exprId, + String functionName, + List overloadIds, + List compiledArguments, + MessageProcessor messageProcessor, + StackOffsetFinder stackOffsetFinder) + throws InterpreterException; + + /** Determines whether or not the given overload ID corresponds to a known function binding. */ + boolean isBound(String overloadId); +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/FuturesInterpreter.java b/legacy/java/dev/cel/legacy/runtime/async/FuturesInterpreter.java new file mode 100644 index 000000000..e2e9bf38d --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/FuturesInterpreter.java @@ -0,0 +1,52 @@ +package dev.cel.legacy.runtime.async; + +import com.google.protobuf.Descriptors.Descriptor; +import dev.cel.common.CelOptions; +import dev.cel.runtime.MessageProvider; +import java.util.Optional; +import java.util.function.Function; + +/** + * Legacy implementation of an optimizing futures-based interpreter for CEL. + * + *

This is a wrapper around {@link Evaluator}, providing the original interface for backward + * compatibility. + */ +public class FuturesInterpreter extends Evaluator { + + /** Standard constructor. */ + public FuturesInterpreter( + TypeResolver typeResolver, + MessageProcessor messageProcessor, + AsyncDispatcher dispatcher, + CelOptions celOptions) { + super(typeResolver, messageProcessor, dispatcher, celOptions); + } + + /** + * Legacy constructor that uses the standard type resolver and adapts a {@link MessageProvider} + * for use as message processor. Uses legacy features. + */ + public FuturesInterpreter( + MessageProvider messageProvider, + AsyncDispatcher dispatcher, + Function> messageLookup) { + this(messageProvider, dispatcher, messageLookup, CelOptions.LEGACY); + } + + /** + * Legacy constructor that uses the standard type resolver and adapts a {@link MessageProvider} + * for use as message processor. Uses legacy features. Uses provided features. + */ + public FuturesInterpreter( + MessageProvider messageProvider, + AsyncDispatcher dispatcher, + Function> messageLookup, + CelOptions celOptions) { + super( + StandardTypeResolver.getInstance(celOptions), + new MessageProcessorAdapter(messageLookup, messageProvider), + dispatcher, + celOptions); + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/GlobalContext.java b/legacy/java/dev/cel/legacy/runtime/async/GlobalContext.java new file mode 100644 index 000000000..546e5533e --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/GlobalContext.java @@ -0,0 +1,29 @@ +package dev.cel.legacy.runtime.async; + +import com.google.auto.value.AutoValue; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.function.Supplier; + +/** Represents the global context of the computation: async context and global variable bindings. */ +@AutoValue +public abstract class GlobalContext { + + /** Retrieves the {@link AsyncContext}. */ + public abstract AsyncContext context(); + + /** Retrieves the {@link AsyncCanonicalResolver}. */ + public abstract AsyncCanonicalResolver resolver(); + + /** + * Creates a new {@link GlobalContext} by pairing an {@link AsyncContext} with the corresponding + * {@link AsyncCanonicalResolver}. + */ + public static GlobalContext of(AsyncContext context, AsyncCanonicalResolver resolver) { + return new AutoValue_GlobalContext(context, resolver); + } + + /** Resolves a global variable using the {@link AsyncCanonicalResolver}. */ + public Supplier> resolve(String name) { + return resolver().resolve(name); + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/IdentifiedCompiledExpression.java b/legacy/java/dev/cel/legacy/runtime/async/IdentifiedCompiledExpression.java new file mode 100644 index 000000000..eec91bf98 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/IdentifiedCompiledExpression.java @@ -0,0 +1,47 @@ +package dev.cel.legacy.runtime.async; + +import dev.cel.expr.Type; +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.Immutable; +import dev.cel.checker.Types; +import dev.cel.runtime.InterpreterException; +import java.util.Map; + +/** + * Represents a {@link CompiledExpression}, possibly further scoped, together with its node ID in + * the abstract syntax and its CEL type. + */ +@AutoValue +public abstract class IdentifiedCompiledExpression { + + /** + * Represents a compiled expression when placed within the scope of some additional stack slots + * (aka local variables). + */ + @Immutable + @FunctionalInterface + public interface ScopedExpression { + CompiledExpression inScopeOf(String... slots) throws InterpreterException; + } + + /** The scoped expression in question. */ + public abstract ScopedExpression scopedExpression(); + + /** The expression with no further scoping. */ + public CompiledExpression expression() throws InterpreterException { + return scopedExpression().inScopeOf(); + } + + /** The node ID of the expression in the original checked expression. */ + public abstract long id(); + + /** The CEL type of the expression. */ + public abstract Type type(); + + /** Constructs an {@link IdentifiedCompiledExpression}. */ + public static IdentifiedCompiledExpression of( + ScopedExpression scopedExpression, long id, Map typeMap) { + return new AutoValue_IdentifiedCompiledExpression( + scopedExpression, id, typeMap.getOrDefault(id, Types.DYN)); + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/MessageProcessor.java b/legacy/java/dev/cel/legacy/runtime/async/MessageProcessor.java new file mode 100644 index 000000000..7311202de --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/MessageProcessor.java @@ -0,0 +1,241 @@ +package dev.cel.legacy.runtime.async; + +import dev.cel.expr.Type; +import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.Message; +import dev.cel.runtime.InterpreterException; +import dev.cel.runtime.Metadata; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the mechanism for creating proto message objects and for accessing proto message + * fields. + */ +public interface MessageProcessor { + + /** Represents the operation of creating a proto message of a specific known type. */ + @Immutable + @FunctionalInterface + interface MessageCreator { + /** + * Creates the message object using the given field values. Field values correspond by position + * to the field names that were provided as {@code fieldNames} when the {@code MessageCreator} + * was made using the {@code makeMessageCreator} method below. + * + *

Field values must be in canonical CEL runtime value representation. + * + *

Throws the exception if runtime field values cannot be adapted to their corresponding + * field types. An implementation can try to perform these checks as much as possible during + * {@code MessageCreator} construction, but if there are field values of dynamic type it is + * still possible that the exception is thrown at message creation time. + */ + Object createMessage(List fieldValues) throws InterpreterException; + } + + /** + * Represents the operation of getting the value of a specific field in a proto message of a + * specific known type. + */ + @Immutable + @FunctionalInterface + interface FieldGetter { + /** + * Retrieves the field's value or the default value if the field is not set. The returned value + * will be in canonical CEL runtime representation. + */ + Object getField(Object message); + } + + /** + * Represents the operation of checking for the presence of a specific field in a proto message of + * a specific known type. + */ + @Immutable + @FunctionalInterface + interface FieldTester { + /** Checks for existence of the field. */ + boolean hasField(Object message); + } + + /** + * Represents the assignment of the given value (after a suitable representation change) to a + * specific field of the given proto message builder. The builder must be a builder for the type + * of message that the field in question is defined within. + */ + @Immutable + @FunctionalInterface + interface FieldAssigner { + /** Assigns the suitably converted value to the field. */ + @CanIgnoreReturnValue + Message.Builder assign(Message.Builder builder, Object value); + } + + /** Represents the clearing of a specific field in the given proto message builder. */ + @Immutable + @FunctionalInterface + interface FieldClearer { + /** Clears the field. */ + @CanIgnoreReturnValue + Message.Builder clear(Message.Builder builder); + } + + /** Represents the creation of a new builder for a specific proto message type. */ + @Immutable + @FunctionalInterface + interface MessageBuilderCreator { + /** Creates a new empty builder. */ + Message.Builder builder(); + } + + /** + * Returns a {@link MessageCreator} for the named message type. When invoking the {@code + * createMessage} method on the result, values for each of the named fields must be provided. + * Field values correspond to field names by position. + * + *

Throws an exception if the message type is unknown or if any of the named fields is not + * defined in it. + */ + // This lambda implements @Immutable interface 'MessageCreator', but 'List' is mutable + @SuppressWarnings("Immutable") + default MessageCreator makeMessageCreator( + Metadata metadata, + long exprId, + String messageName, + List fieldNames, + List fieldTypes) + throws InterpreterException { + final int numFields = fieldNames.size(); + Preconditions.checkArgument(numFields == fieldTypes.size()); + MessageBuilderCreator builderCreator = makeMessageBuilderCreator(metadata, exprId, messageName); + List assigners = new ArrayList<>(); + for (int i = 0; i < numFields; ++i) { + assigners.add( + makeFieldAssigner(metadata, exprId, messageName, fieldNames.get(i), fieldTypes.get(i))); + } + return fieldValues -> { + Preconditions.checkArgument(numFields == fieldValues.size()); + Message.Builder messageBuilder = builderCreator.builder(); + for (int i = 0; i < numFields; ++i) { + try { + assigners.get(i).assign(messageBuilder, fieldValues.get(i)); + } catch (RuntimeException e) { + throw new InterpreterException.Builder(e, e.getMessage()) + .setLocation(metadata, exprId) + .build(); + } + } + return messageBuilder.build(); + }; + } + + /** + * Returns a {@link FieldGetter} for the named field in the named message type. + * + *

Throws an exception if the message type is unknown or if the field is not defined in that + * message type. + */ + FieldGetter makeFieldGetter(Metadata metadata, long exprId, String messageName, String fieldName) + throws InterpreterException; + + /** + * Returns a {@link FieldTester} for the named field in the named message type. + * + *

Throws an exception if the message type is unknown, if the field is not defined in that + * message type, or if presence checks on that field are not supported. + */ + FieldTester makeFieldTester(Metadata metadata, long exprId, String messageName, String fieldName) + throws InterpreterException; + + /** + * Returns a {@link FieldAssigner} for the named field in the named message type. + * + *

Throws an exception if the message type is unknown or if the field is not defined in that + * message type. + * + *

May use the CEL type information for the value to be assigned by the returned assigner in + * order to correctly implement the conversion from CEL runtime representation to the + * representation required by the proto field. + */ + FieldAssigner makeFieldAssigner( + Metadata metadata, long exprId, String messageName, String fieldName, Type fieldType) + throws InterpreterException; + + /** + * Returns a {@link FieldClearer} for the named field in the named message type. + * + *

Throws an exception if the message type is unknown or if the field is not defined in that + * message type. + */ + FieldClearer makeFieldClearer( + Metadata metadata, long exprId, String messageName, String fieldName) + throws InterpreterException; + + /** + * Returns a {@link MessageBuilderCreator} for the named field in the named message type. + * + *

Throws an exception if the message type is unknown. + */ + MessageBuilderCreator makeMessageBuilderCreator( + Metadata metadata, long exprId, String messageName) throws InterpreterException; + + /** + * Finds the field and retrieves its value using only the runtime type of the given message + * object. If the field is not set, the type-specific default value is returned according to + * normal proto message semantics. + * + *

Throws an exception if the object is not a message object or if the field is not defined in + * it. + */ + Object dynamicGetField(Metadata metadata, long exprId, Object messageObject, String fieldName) + throws InterpreterException; + + /** + * Checks for the existence of the field using only the runtime type of the given message object. + * + *

Throws an exception if the object is not a message object or if the field is not defined in + * it. + */ + boolean dynamicHasField(Metadata metadata, long exprId, Object messageObject, String fieldName) + throws InterpreterException; + + /** + * Returns a {@link FieldGetter} for the named extension. + * + *

Throws an exception if the extension is unknown. + */ + FieldGetter makeExtensionGetter(Metadata metadata, long exprId, String extensionName) + throws InterpreterException; + + /** + * Returns a {@link FieldTester} for the named extension. + * + *

Throws an exception if the extesion is unknown, or if presence checks on it are not + * supported. + */ + FieldTester makeExtensionTester(Metadata metadata, long exprId, String extensionName) + throws InterpreterException; + + /** + * Returns a {@link FieldAssigner} for the named extension. + * + *

Throws an exception if the extension is unknown. + * + *

May use the CEL type information for the value to be assigned by the returned assigner in + * order to correctly implement the conversion from CEL runtime representation to the + * representation required by the proto field. + */ + FieldAssigner makeExtensionAssigner( + Metadata metadata, long exprId, String extensionName, Type extensionType) + throws InterpreterException; + + /** + * Returns a {@link FieldClearer} for the named extension. + * + *

Throws an exception if the extension is unknown. + */ + FieldClearer makeExtensionClearer(Metadata metadata, long exprId, String extensionName) + throws InterpreterException; +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/MessageProcessorAdapter.java b/legacy/java/dev/cel/legacy/runtime/async/MessageProcessorAdapter.java new file mode 100644 index 000000000..3d14db59d --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/MessageProcessorAdapter.java @@ -0,0 +1,230 @@ +package dev.cel.legacy.runtime.async; + +import dev.cel.expr.Type; +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Message; +import dev.cel.runtime.InterpreterException; +import dev.cel.runtime.MessageProvider; +import dev.cel.runtime.Metadata; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +/** Adapts a {@link MessageProvider} to act as a {@link MessageProcessor}, using delegation. */ +final class MessageProcessorAdapter implements MessageProcessor { + + private final Function> messageLookup; + private final MessageProvider messageProvider; + + MessageProcessorAdapter( + Function> messageLookup, MessageProvider messageProvider) { + this.messageLookup = messageLookup; + this.messageProvider = messageProvider; + } + + // Overrides the default implementation since doing so is more efficient here. + // (The default implementation is based on FieldAssigners, and those are not efficient + // when adapting a MessageProvider.) + // This lambda implements @Immutable interface 'MessageCreator', but 'List' is mutable + @SuppressWarnings("Immutable") + @Override + public MessageCreator makeMessageCreator( + Metadata metadata, + long exprId, + String messageName, + List fieldNames, + List fieldTypes) // ignored in this implementation; adaptation errors occur at runtime + throws InterpreterException { + Descriptor messageDescriptor = verifyMessageType(metadata, exprId, messageName); + for (String fieldName : fieldNames) { + verifyField(metadata, exprId, messageDescriptor, fieldName); + } + return fieldValues -> { + try { + return messageProvider.createMessage(messageName, makeValueMap(fieldNames, fieldValues)); + } catch (RuntimeException e) { + throw new InterpreterException.Builder(e, e.getMessage()) + .setLocation(metadata, exprId) + .build(); + } + }; + } + + // This lambda implements @Immutable interface 'FieldGetter', but 'MessageProcessorAdapter' has + // field 'messageProvider' of type + // 'com.google.api.tools.contract.runtime.interpreter.MessageProvider', the declaration of type + // 'com.google.api.tools.contract.runtime.interpreter.MessageProvider' is not annotated with + // @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + @Override + public FieldGetter makeFieldGetter( + Metadata metadata, long exprId, String messageName, String fieldName) + throws InterpreterException { + Descriptor messageDescriptor = verifyMessageType(metadata, exprId, messageName); + verifyField(metadata, exprId, messageDescriptor, fieldName); + return message -> { + try { + return messageProvider.selectField(message, fieldName); + } catch (IllegalArgumentException e) { + throw new IllegalStateException("[internal] field selection unexpectedly failed", e); + } + }; + } + + // This lambda implements @Immutable interface 'FieldTester', but 'MessageProcessorAdapter' has + // field 'messageProvider' of type + // 'com.google.api.tools.contract.runtime.interpreter.MessageProvider', the declaration of type + // 'com.google.api.tools.contract.runtime.interpreter.MessageProvider' is not annotated with + // @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + @Override + public FieldTester makeFieldTester( + Metadata metadata, long exprId, String messageName, String fieldName) + throws InterpreterException { + Descriptor messageDescriptor = verifyMessageType(metadata, exprId, messageName); + verifyField(metadata, exprId, messageDescriptor, fieldName); + return message -> { + try { + return messageProvider.hasField(message, fieldName).equals(true); + } catch (IllegalArgumentException e) { + throw new IllegalStateException("[internal] field presence test unexpectedly failed", e); + } + }; + } + + // This lambda implements @Immutable interface 'FieldAssigner', but 'MessageProcessorAdapter' has + // field 'messageProvider' of type + // 'com.google.api.tools.contract.runtime.interpreter.MessageProvider', the declaration of type + // 'com.google.api.tools.contract.runtime.interpreter.MessageProvider' is not annotated with + // @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + @Override + public FieldAssigner makeFieldAssigner( + Metadata metadata, long exprId, String messageName, String fieldName, Type fieldType) + throws InterpreterException { + Descriptor messageDescriptor = verifyMessageType(metadata, exprId, messageName); + verifyField(metadata, exprId, messageDescriptor, fieldName); + FieldDescriptor fd = messageDescriptor.findFieldByName(fieldName); + return (builder, value) -> { + try { + Message singleton = + (Message) messageProvider.createMessage(messageName, ImmutableMap.of(fieldName, value)); + return builder.clearField(fd).mergeFrom(singleton); + } catch (IllegalArgumentException e) { + throw new IllegalStateException("[internal] field assignment unexpectedly failed", e); + } + }; + } + + @Override + public FieldClearer makeFieldClearer( + Metadata metadata, long exprId, String messageName, String fieldName) + throws InterpreterException { + Descriptor messageDescriptor = verifyMessageType(metadata, exprId, messageName); + verifyField(metadata, exprId, messageDescriptor, fieldName); + FieldDescriptor fd = messageDescriptor.findFieldByName(fieldName); + return builder -> builder.clearField(fd); + } + + @Override + public MessageBuilderCreator makeMessageBuilderCreator( + Metadata metadata, long exprId, String messageName) throws InterpreterException { + try { + Message emptyProto = (Message) messageProvider.createMessage(messageName, ImmutableMap.of()); + return emptyProto::toBuilder; + } catch (RuntimeException e) { + throw new InterpreterException.Builder(e, e.getMessage()) + .setLocation(metadata, exprId) + .build(); + } + } + + @Override + public Object dynamicGetField( + Metadata metadata, long exprId, Object messageObject, String fieldName) + throws InterpreterException { + try { + return messageProvider.selectField(messageObject, fieldName); + } catch (IllegalArgumentException e) { + throw new InterpreterException.Builder(e.getMessage()).setLocation(metadata, exprId).build(); + } + } + + @Override + public boolean dynamicHasField( + Metadata metadata, long exprId, Object messageObject, String fieldName) + throws InterpreterException { + try { + return messageProvider.hasField(messageObject, fieldName).equals(true); + } catch (IllegalArgumentException e) { + throw new InterpreterException.Builder(e.getMessage()).setLocation(metadata, exprId).build(); + } + } + + @Override + public FieldGetter makeExtensionGetter(Metadata metadata, long exprId, String extensionName) + throws InterpreterException { + throw unsupportedProtoExtensions(metadata, exprId); + } + + @Override + public FieldTester makeExtensionTester(Metadata metadata, long exprId, String extensionName) + throws InterpreterException { + throw unsupportedProtoExtensions(metadata, exprId); + } + + @Override + public FieldAssigner makeExtensionAssigner( + Metadata metadata, long exprId, String extensionName, Type extensionType) + throws InterpreterException { + throw unsupportedProtoExtensions(metadata, exprId); + } + + @Override + public FieldClearer makeExtensionClearer(Metadata metadata, long exprId, String extensionName) + throws InterpreterException { + throw unsupportedProtoExtensions(metadata, exprId); + } + + private InterpreterException unsupportedProtoExtensions(Metadata metadata, long exprId) { + return new InterpreterException.Builder("proto extensions not supported") + .setLocation(metadata, exprId) + .build(); + } + + private Descriptor verifyMessageType(Metadata metadata, long id, String messageName) + throws InterpreterException { + return messageLookup + .apply(messageName) + .orElseThrow( + () -> + new InterpreterException.Builder("cannot resolve '%s' as a message", messageName) + .setLocation(metadata, id) + .build()); + } + + private static void verifyField( + Metadata metadata, long id, Descriptor messageDescriptor, String field) + throws InterpreterException { + FieldDescriptor fieldDescriptor = messageDescriptor.findFieldByName(field); + if (fieldDescriptor == null) { + throw new InterpreterException.Builder( + "field '%s' is not declared in message '%s'", field, messageDescriptor.getName()) + .setLocation(metadata, id) + .build(); + } + } + + private static ImmutableMap makeValueMap( + List fieldNames, Iterable fieldValues) { + int i = 0; + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Object value : fieldValues) { + builder.put(fieldNames.get(i), value); + ++i; + } + return builder.buildOrThrow(); + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/ProtoFieldAssignment.java b/legacy/java/dev/cel/legacy/runtime/async/ProtoFieldAssignment.java new file mode 100644 index 000000000..5b8a0337e --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/ProtoFieldAssignment.java @@ -0,0 +1,546 @@ +package dev.cel.legacy.runtime.async; + +import static dev.cel.legacy.runtime.async.Canonicalization.asInstanceOf; + +import dev.cel.expr.Type; +import com.google.common.primitives.Ints; +import com.google.common.primitives.UnsignedInts; +import com.google.common.primitives.UnsignedLong; +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; +import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.DoubleValue; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.ListValue; +import com.google.protobuf.MapEntry; +import com.google.protobuf.Message; +import com.google.protobuf.NullValue; +import com.google.protobuf.StringValue; +import com.google.protobuf.Struct; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import com.google.protobuf.Value; +import dev.cel.checker.Types; +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelRuntimeException; +import dev.cel.legacy.runtime.async.MessageProcessor.FieldAssigner; +import dev.cel.runtime.InterpreterException; +import dev.cel.runtime.Metadata; +import java.util.Map; + +/** + * Descriptor- and type-director assignment of values to proto fields. + * + *

This class provides tools for creating {@code Assigner} instances. An {@code Assigner} is the + * mechanism that assigns a value to a proto field after first translating it from its canonical CEL + * runtime representation to the type that is required for the field in question. + */ +final class ProtoFieldAssignment { + + /** + * Represents the transformations to proto field value types from their corresponding canonical + * CEL runtime representations. A {@link Protoizer} is roughly the inverse of a corresponding + * {@link Canonicalization.Canonicalizer}. + * + *

If the value cannot be converted to its proto representation at runtime, a corresponding + * {@code RuntimeException} is thrown. + * + *

Each instance of this interface corresponds to a possible type of proto message field, and T + * is the Java type of that field, i.e., something acceptable to the {@code setField} or {@code + * addRepeatedField} methods of {@code MessageOrBuilder}. + */ + @Immutable + @FunctionalInterface + private interface Protoizer { + T protoize(Object canonicalValue); + } + + /** + * Returns an {@link FieldAssigner} for the described field that assumes that the value to be + * assigned has the given CEL type. + */ + public static FieldAssigner fieldValueAssigner( + Metadata metadata, long id, FieldDescriptor fd, Type type) throws InterpreterException { + if (fd.isMapField()) { + return mapAssigner(metadata, id, fd, type); + } + if (fd.isRepeated()) { + return listAssigner(metadata, id, fd, type); + } else { + Protoizer protoizer = singleFieldProtoizer(metadata, id, fd, type); + return (builder, value) -> builder.setField(fd, protoizer.protoize(value)); + } + } + + /** + * Returns an {@link FieldAssigner} for the described field which must be a repeated field. The + * value to be assigned must be a CEL list, and the given CEL type should reflect that (unless it + * is DYN). + */ + private static FieldAssigner listAssigner( + Metadata metadata, long id, FieldDescriptor fd, Type listType) throws InterpreterException { + Type elementType = Types.DYN; + if (!Types.isDynOrError(listType)) { + if (listType.getTypeKindCase() != Type.TypeKindCase.LIST_TYPE) { + throw new InterpreterException.Builder("value for repeated field does not have type list") + .setLocation(metadata, id) + .build(); + } + elementType = listType.getListType().getElemType(); + } + Protoizer protoizer = singleFieldProtoizer(metadata, id, fd, elementType); + return (builder, listValue) -> { + builder.clearField(fd); + for (Object element : asInstanceOf(Iterable.class, listValue)) { + builder.addRepeatedField(fd, protoizer.protoize(element)); + } + return builder; + }; + } + + /** + * Returns an {@link FieldAssigner} for the described field which must be a map field. The value + * to be assigned must be a CEL map, and the given CEL type should reflect that (unless it is + * DYN). + */ + private static FieldAssigner mapAssigner( + Metadata metadata, long id, FieldDescriptor fd, Type mapType) throws InterpreterException { + Descriptor entryDescriptor = fd.getMessageType(); + FieldDescriptor keyDescriptor = entryDescriptor.findFieldByNumber(1); + FieldDescriptor valueDescriptor = entryDescriptor.findFieldByNumber(2); + Type keyType = Types.DYN; + Type valueType = Types.DYN; + if (!Types.isDynOrError(mapType)) { + switch (mapType.getTypeKindCase()) { + case MAP_TYPE: + keyType = mapType.getMapType().getKeyType(); + valueType = mapType.getMapType().getValueType(); + break; + default: + throw new InterpreterException.Builder("value for map field does not have map type") + .setLocation(metadata, id) + .build(); + } + } + Protoizer keyProtoizer = singleFieldProtoizer(metadata, id, keyDescriptor, keyType); + Protoizer valueProtoizer = singleFieldProtoizer(metadata, id, valueDescriptor, valueType); + MapEntry protoMapEntry = + MapEntry.newDefaultInstance( + entryDescriptor, + keyDescriptor.getLiteType(), + getDefaultValueForMaybeMessage(keyDescriptor), + valueDescriptor.getLiteType(), + getDefaultValueForMaybeMessage(valueDescriptor)); + return (builder, value) -> { + builder.clearField(fd); + ((Map) asInstanceOf(Map.class, value)) + .entrySet() + .forEach( + entry -> + builder.addRepeatedField( + fd, + protoMapEntry.toBuilder() + .setKey(keyProtoizer.protoize(entry.getKey())) + .setValue(valueProtoizer.protoize(entry.getValue())) + .build())); + return builder; + }; + } + + /** Returns the default value for a field that can be a proto message */ + private static Object getDefaultValueForMaybeMessage(FieldDescriptor descriptor) { + if (descriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE) { + return DynamicMessage.getDefaultInstance(descriptor.getMessageType()); + } + return descriptor.getDefaultValue(); + } + + /** + * Returns a {@link Protoizer} for converting CEL values of the specified CEL type to the type + * appropriate for the described field. The field cannot be a map field, and the returned + * protoizer deals with only a single value even if the field is repeated. + */ + private static Protoizer singleFieldProtoizer( + Metadata metadata, long id, FieldDescriptor fd, Type type) throws InterpreterException { + // Possible future improvement: Consider verifying the CEL type. + switch (fd.getType()) { + case SFIXED32: + case SINT32: + case INT32: + return value -> intCheckedCast(((Number) value).longValue()); + case FIXED32: + case UINT32: + return value -> unsignedIntCheckedCast(((Number) value).longValue()); + case FIXED64: + case UINT64: + return value -> ((Number) value).longValue(); + case ENUM: + return value -> + fd.getEnumType().findValueByNumberCreatingIfUnknown((int) ((Number) value).longValue()); + case FLOAT: + return value -> doubleToFloat(((Number) value).doubleValue()); + case MESSAGE: + return protoProtoizer(metadata, id, fd.getMessageType(), type); + default: + return x -> x; + } + } + + /** + * Returns a protoizer that converts a CEL value to a proto message. Such protoizers are simply + * the identity if the CEL value is already a proto message. However, if the target message type + * is `Any` or any of the known wrapper protos or JSON protos, then the result will be a protoizer + * that wraps its non-proto argument accordingly. + */ + private static Protoizer protoProtoizer( + Metadata metadata, long id, Descriptor d, Type type) throws InterpreterException { + switch (d.getFullName()) { + case "google.protobuf.Any": + return anyProtoizer(metadata, id, type); + case "google.protobuf.Value": + return jsonValueProtoizer(metadata, id, type); + case "google.protobuf.ListValue": + return jsonListProtoizer(metadata, id, type); + case "google.protobuf.Struct": + return jsonStructProtoizer(metadata, id, type); + case "google.protobuf.Int64Value": + return ProtoFieldAssignment::toInt64Value; + case "google.protobuf.UInt64Value": + return ProtoFieldAssignment::toUInt64Value; + case "google.protobuf.Int32Value": + return ProtoFieldAssignment::toInt32Value; + case "google.protobuf.UInt32Value": + return ProtoFieldAssignment::toUInt32Value; + case "google.protobuf.DoubleValue": + return ProtoFieldAssignment::toDoubleValue; + case "google.protobuf.FloatValue": + return ProtoFieldAssignment::toFloatValue; + case "google.protobuf.BoolValue": + return ProtoFieldAssignment::toBoolValue; + case "google.protobuf.StringValue": + return ProtoFieldAssignment::toStringValue; + case "google.protobuf.BytesValue": + return ProtoFieldAssignment::toBytesValue; + default: + return x -> asInstanceOf(Message.class, x); + } + } + + /** + * Returns a protoizer that wraps the value in an {@link Any}, after suitably wrapping non-proto + * values in wrapper protos. + */ + private static Protoizer anyProtoizer(Metadata metadata, long id, Type type) + throws InterpreterException { + Protoizer messageProtoizer = wrapIfNecessaryProtoizer(metadata, id, type); + return x -> Any.pack(messageProtoizer.protoize(x)); + } + + /** + * Returns a protoizer that wraps values of the given CEL type in proto wrappes that make them + * suitable for further wrapping them with {@link Any}. Values that are already protos are not + * further wrapped. + * + *

Notice that thanks to the knowledge of the CEL type it is possible to distinguish between + * signed and unsigned values even though the CEL runtime representation does not carry such + * information. + * + *

If the CEL type is not known (i.e., DYN), then a fallback protoizer that uses only dynamic + * information is returned. + */ + private static Protoizer wrapIfNecessaryProtoizer( + Metadata metadata, long id, Type type) throws InterpreterException { + switch (type.getTypeKindCase()) { + case LIST_TYPE: + return jsonListProtoizer(metadata, id, type); + case MAP_TYPE: + return jsonStructProtoizer(metadata, id, type); + case PRIMITIVE: + { + switch (type.getPrimitive()) { + case BOOL: + return ProtoFieldAssignment::toBoolValue; + case INT64: + return ProtoFieldAssignment::toInt64Value; + case UINT64: + return ProtoFieldAssignment::toUInt64Value; + case DOUBLE: + return ProtoFieldAssignment::toDoubleValue; + case STRING: + return ProtoFieldAssignment::toStringValue; + case BYTES: + return ProtoFieldAssignment::toBytesValue; + default: + return ProtoFieldAssignment::dynamicWrapIfNecessary; + } + } + case MESSAGE_TYPE: + return x -> asInstanceOf(Message.class, x); + default: + return ProtoFieldAssignment::dynamicWrapIfNecessary; + } + } + + /** Fallback protoizer returned by {@code dynamicWrapIfNecessary} in the dynamic case. */ + private static Message dynamicWrapIfNecessary(Object value) { + if (value instanceof Message) { + return (Message) value; + } + if (value instanceof Long) { + return Int64Value.of((Long) value); + } + if (value instanceof Double) { + return DoubleValue.of((Double) value); + } + if (value instanceof Boolean) { + return BoolValue.of((Boolean) value); + } + if (value instanceof String) { + return StringValue.of((String) value); + } + if (value instanceof ByteString) { + return BytesValue.of((ByteString) value); + } + if (value instanceof Iterable) { + return dynamicJsonList(value); + } + if (value instanceof Map) { + return dynamicJsonStruct(value); + } + throw new IllegalArgumentException( + "Unsupported type to pack to Any:" + value.getClass().getSimpleName()); + } + + /** + * Returns a protoizer that converts a CEL value to a JSON value, i.e., an instance of {@link + * Value}. + * + *

The construction is type-directed based on the known CEL type of the incoming value, falling + * back to a fully dynamic mechanism where type DYN is encountered. + */ + private static Protoizer jsonValueProtoizer(Metadata metadata, long id, Type type) + throws InterpreterException { + switch (type.getTypeKindCase()) { + case NULL: + return ignored -> dynamicJsonValue(null); + case LIST_TYPE: + { + Protoizer listProtoizer = jsonListProtoizer(metadata, id, type); + return value -> Value.newBuilder().setListValue(listProtoizer.protoize(value)).build(); + } + case MAP_TYPE: + { + Protoizer structProtoizer = jsonStructProtoizer(metadata, id, type); + return value -> + Value.newBuilder().setStructValue(structProtoizer.protoize(value)).build(); + } + case PRIMITIVE: + { + switch (type.getPrimitive()) { + case BOOL: + return x -> Value.newBuilder().setBoolValue(asInstanceOf(Boolean.class, x)).build(); + case INT64: + case UINT64: + case DOUBLE: + return x -> + Value.newBuilder() + .setNumberValue(asInstanceOf(Number.class, x).doubleValue()) + .build(); + case STRING: + return x -> Value.newBuilder().setStringValue(asInstanceOf(String.class, x)).build(); + default: + return ProtoFieldAssignment::dynamicJsonValue; + } + } + default: + return ProtoFieldAssignment::dynamicJsonValue; + } + } + + /** + * Returns a protoizer that turns a CEL list into a JSON list, i.e., an instance of {@link + * ListValue}. + * + *

The construction is type-directed based on the known CEL type of the incoming value, falling + * back to a fully dynamic mechanism where type DYN is encountered. + */ + private static Protoizer jsonListProtoizer(Metadata metadata, long id, Type type) + throws InterpreterException { + FieldAssigner assigner = listAssigner(metadata, id, LIST_VALUE_VALUES, type); + return listValue -> { + ListValue.Builder builder = ListValue.newBuilder(); + assigner.assign(builder, listValue); + return builder.build(); + }; + } + + /** + * Returns a protoizer that turns a CEL map into a JSON struct, i.e., an instance of {@link + * Struct}. + * + *

The construction is type-directed based on the known CEL type of the incoming value, falling + * back to a fully dynamic mechanism where type DYN is encountered. + */ + private static Protoizer jsonStructProtoizer(Metadata metadata, long id, Type type) + throws InterpreterException { + FieldAssigner assigner = mapAssigner(metadata, id, STRUCT_FIELDS, type); + return mapValue -> { + Struct.Builder builder = Struct.newBuilder(); + assigner.assign(builder, mapValue); + return builder.build(); + }; + } + + /** Fallback protoizer returned by {@code jsonValueProtoizer} in the dynamic case. */ + private static Value dynamicJsonValue(Object object) { + Value.Builder builder = Value.newBuilder(); + if (object == null || object instanceof NullValue) { + builder.setNullValue(NullValue.NULL_VALUE); + } else if (object instanceof Boolean) { + builder.setBoolValue((Boolean) object); + } else if (object instanceof Number) { + builder.setNumberValue(((Number) object).doubleValue()); + } else if (object instanceof String) { + builder.setStringValue((String) object); + } else if (object instanceof Map) { + builder.setStructValue(dynamicJsonStruct(object)); + } else if (object instanceof Iterable) { + builder.setListValue(dynamicJsonList(object)); + } else { + throw new IllegalArgumentException("[internal] value cannot be converted to JSON"); + } + return builder.build(); + } + + /** + * Converts a CEL value to a JSON list (i.e., a {@link ListValue} instance). No exception is + * expected to be thrown since a suitable CEL type is passed when constructing the protoizer. + */ + private static ListValue dynamicJsonList(Object object) { + try { + return jsonListProtoizer(DUMMY_METADATA, 0, DYNAMIC_JSON_LIST_TYPE).protoize(object); + } catch (InterpreterException e) { + throw new AssertionError("unexpected exception", e); + } + } + + /** + * Converts a CEL value to a JSON struct (i.e., a {@link Struct} instance). No exception is + * expected to be thrown since a suitable CEL type is passed when constructing the protoizer. + */ + private static Struct dynamicJsonStruct(Object object) { + try { + return jsonStructProtoizer(DUMMY_METADATA, 0, DYNAMIC_JSON_MAP_TYPE).protoize(object); + } catch (InterpreterException e) { + throw new AssertionError("unexpected exception", e); + } + } + + /** Converts a CEL value to a wrapped int64. */ + private static Message toInt64Value(Object x) { + return Int64Value.of(asInstanceOf(Long.class, x)); + } + + /** Converts a CEL value to a wrapped uint64. */ + private static Message toUInt64Value(Object x) { + if (x instanceof UnsignedLong) { + return UInt64Value.of(asInstanceOf(UnsignedLong.class, x).longValue()); + } + return UInt64Value.of(asInstanceOf(Long.class, x)); + } + + /** Converts a CEL value to a wrapped int32. */ + private static Message toInt32Value(Object x) { + return Int32Value.of(intCheckedCast(asInstanceOf(Long.class, x))); + } + + /** Converts a CEL value to a wrapped uint32. */ + private static Message toUInt32Value(Object x) { + if (x instanceof UnsignedLong) { + return UInt64Value.of( + unsignedIntCheckedCast(asInstanceOf(UnsignedLong.class, x).longValue())); + } + return UInt32Value.of(unsignedIntCheckedCast(asInstanceOf(Long.class, x))); + } + + /** Converts a CEL value to a wrapped boolean. */ + private static Message toBoolValue(Object x) { + return BoolValue.of(asInstanceOf(Boolean.class, x)); + } + + /** Converts a CEL value to a wrapped double. */ + private static Message toDoubleValue(Object x) { + return DoubleValue.of(asInstanceOf(Double.class, x)); + } + + /** Converts a CEL value to a wrapped float. */ + private static Message toFloatValue(Object x) { + return FloatValue.of(doubleToFloat(asInstanceOf(Double.class, x))); + } + + /** Converts a CEL value to a wrapped string. */ + private static Message toStringValue(Object x) { + return StringValue.of(asInstanceOf(String.class, x)); + } + + /** Converts a CEL value to a wrapped bytes value. */ + private static Message toBytesValue(Object x) { + return BytesValue.of(asInstanceOf(ByteString.class, x)); + } + + /** + * Coerces a {@code double} into a {@code float}, throwing an {@link IllegalArgumentException} + * when it does not fit. + */ + private static float doubleToFloat(double d) { + float f = (float) d; + if (d != f) { + throw new IllegalArgumentException("double out of range for conversion to float"); + } + return f; + } + + private static final FieldDescriptor STRUCT_FIELDS = + Struct.getDescriptor().findFieldByName("fields"); + private static final FieldDescriptor LIST_VALUE_VALUES = + ListValue.getDescriptor().findFieldByName("values"); + private static final Type DYNAMIC_JSON_MAP_TYPE = Types.createMap(Types.STRING, Types.DYN); + private static final Type DYNAMIC_JSON_LIST_TYPE = Types.createList(Types.DYN); + private static final Metadata DUMMY_METADATA = + new Metadata() { + @Override + public String getLocation() { + return ""; + } + + @Override + public int getPosition(long exprId) { + return 0; + } + }; + + private static int intCheckedCast(long value) { + try { + return Ints.checkedCast(value); + } catch (IllegalArgumentException e) { + throw new CelRuntimeException(e, CelErrorCode.NUMERIC_OVERFLOW); + } + } + + private static int unsignedIntCheckedCast(long value) { + try { + return UnsignedInts.checkedCast(value); + } catch (IllegalArgumentException e) { + throw new CelRuntimeException(e, CelErrorCode.NUMERIC_OVERFLOW); + } + } + + private ProtoFieldAssignment() {} +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/ResolverAdapter.java b/legacy/java/dev/cel/legacy/runtime/async/ResolverAdapter.java new file mode 100644 index 000000000..bae02ed7f --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/ResolverAdapter.java @@ -0,0 +1,40 @@ +package dev.cel.legacy.runtime.async; + +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static dev.cel.legacy.runtime.async.Canonicalization.canonicalizeProto; + +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.protobuf.Message; +import dev.cel.common.CelOptions; +import java.util.function.Supplier; + +/** Adapts an {@link AsyncGlobalResolver} to act as an {@link AsyncCanonicalResolver}. */ +final class ResolverAdapter implements AsyncCanonicalResolver { + + final AsyncGlobalResolver asyncGlobalResolver; + private final CelOptions celOptions; + + ResolverAdapter(AsyncGlobalResolver asyncGlobalResolver, CelOptions celOptions) { + this.asyncGlobalResolver = asyncGlobalResolver; + this.celOptions = celOptions; + } + + @Override + public Supplier> resolve(String name) { + return () -> + FluentFuture.from(asyncGlobalResolver.resolve(name)) + .transformAsync( + value -> { + if (value == null) { + throw new IllegalArgumentException("name not bound: '" + name + "'"); + } + if (!(value instanceof Message)) { + return immediateFuture(value); + } + return immediateFuture(canonicalizeProto((Message) value, celOptions)); + }, + directExecutor()); + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/StackOffsetFinder.java b/legacy/java/dev/cel/legacy/runtime/async/StackOffsetFinder.java new file mode 100644 index 000000000..33cb50d76 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/StackOffsetFinder.java @@ -0,0 +1,17 @@ +package dev.cel.legacy.runtime.async; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.runtime.InterpreterException; +import dev.cel.runtime.Metadata; +import javax.annotation.Nullable; + +/** + * Interface for finding the stack offset of the named local variable relative to an implicit + * lexical scoping structure. + */ +@Immutable +@FunctionalInterface +public interface StackOffsetFinder { + int findStackOffset(@Nullable Metadata metadata, long exprId, String name) + throws InterpreterException; +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/StandardConstructs.java b/legacy/java/dev/cel/legacy/runtime/async/StandardConstructs.java new file mode 100644 index 000000000..173ad6715 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/StandardConstructs.java @@ -0,0 +1,342 @@ +package dev.cel.legacy.runtime.async; + +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static dev.cel.legacy.runtime.async.EvaluationHelpers.asBoolean; +import static dev.cel.legacy.runtime.async.EvaluationHelpers.asBooleanExpression; +import static dev.cel.legacy.runtime.async.EvaluationHelpers.immediateException; +import static dev.cel.legacy.runtime.async.EvaluationHelpers.immediateValue; + +import dev.cel.expr.Value; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.re2j.Pattern; +import dev.cel.common.CelErrorCode; +import dev.cel.common.CelOptions; +import dev.cel.runtime.InterpreterException; +import dev.cel.runtime.Metadata; +import dev.cel.runtime.RuntimeHelpers; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import javax.annotation.Nullable; + +/** + * Provides implementations of all "standard" constructs that are typically inlined by the CEL + * interpreter. + */ +public final class StandardConstructs { + + private final TypeResolver typeResolver; + private final CelOptions celOptions; + + public StandardConstructs(TypeResolver typeResolver, CelOptions celOptions) { + this.typeResolver = typeResolver; + this.celOptions = celOptions; + } + + /** Adds all standard constructs to the given registrar. */ + // This method reference implements @Immutable interface SimpleCallConstructor, but the + // declaration of type + // 'dev.cel.legacy.runtime.async.async.StandardConstructs' is not annotated + // with @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + public void addAllTo(FunctionRegistrar registrar) { + /** Identity function. Calls of this are completely short-circuited and are, thus, "free". */ + registrar.addCallConstructor("identity", (md, id, args) -> args.get(0).expression()); + + /** CEL ternary conditional expression: _ ? _ : _ */ + registrar.addCallConstructor("conditional", StandardConstructs::constructConditional); + + /** CEL logical AND: _ && _ */ + registrar.addCallConstructor( + "logical_and", (md, id, args) -> constructLogicalConnective(true, md, args)); + + /** CEL logical OR: _ || _ */ + registrar.addCallConstructor( + "logical_or", (md, id, args) -> constructLogicalConnective(false, md, args)); + + /** Special internal binding used by certain comprehension macros. */ + registrar.addCallConstructor( + "not_strictly_false", StandardConstructs::constructNotStrictlyFalse); + + /** CEL "type" function. */ + registrar.addCallConstructor("type", this::constructType); + + /** + * CEL regexp matching. This pulls the construction of the matcher into the preparation phase if + * the regexp argument is constant. + */ + registrar.addCallConstructor("matches", this::constructRegexpMatch); + registrar.addCallConstructor("matches_string", this::constructRegexpMatch); + + /** + * CEL list membership: _ in _. This turns constant lists into Java hash sets during the + * preparation phase, for improved performance. + */ + registrar.addCallConstructor( + "in_list", + (md, id, args) -> + constructCollectionMembershipTest( + args, + (element, listObject) -> ((List) listObject).contains(element), + listObject -> ImmutableSet.copyOf((List) listObject))); + + /** + * CEL map key membership: _ in _. This turns the keysets of constant maps into Java hash sets + * during the preparation phase, for improved performance. + */ + registrar.addCallConstructor( + "in_map", + (md, id, args) -> + constructCollectionMembershipTest( + args, + (element, mapObject) -> ((Map) mapObject).containsKey(element), + mapObject -> ImmutableSet.copyOf(((Map) mapObject).keySet()))); + } + + /** Compiles the conditional operator {@code _?_:_} */ + private static CompiledExpression constructConditional( + Metadata metadata, long exprId, List compiledArguments) + throws InterpreterException { + IdentifiedCompiledExpression compiledCondition = compiledArguments.get(0); + CompiledExpression compiledThen = compiledArguments.get(1).expression(); + CompiledExpression compiledElse = compiledArguments.get(2).expression(); + return asBooleanExpression(compiledCondition.expression(), metadata, compiledCondition.id()) + .map( + (executableCondition, conditionEffect) -> { + ExecutableExpression executableThen = compiledThen.toExecutable(); + ExecutableExpression executableElse = compiledElse.toExecutable(); + return CompiledExpression.executable( + stack -> + executableCondition + .execute(stack) + .transformAsync( + condition -> + asBoolean(condition) + ? executableThen.execute(stack) + : executableElse.execute(stack), + directExecutor()), + conditionEffect.meet(compiledThen.effect()).meet(compiledElse.effect())); + }, + constantCondition -> asBoolean(constantCondition) ? compiledThen : compiledElse, + t -> CompiledExpression.throwing(t)); + } + + /** + * Compile a logical connective (AND or OR). This implements short-circuiting on the first operand + * that finishes execution. + * + *

The unit value is the neutral element of the operation in question. If one operand's value + * is unit, then the result is whatever the other operand does. On the other hand, if one operand + * yields the opposite of unit (= !unit), then the overall outcome is !unit regardless of the + * other operand's behavior. + */ + private static CompiledExpression constructLogicalConnective( + boolean unit, Metadata metadata, List compiledArguments) + throws InterpreterException { + IdentifiedCompiledExpression identifiedLeft = compiledArguments.get(0); + IdentifiedCompiledExpression identifiedRight = compiledArguments.get(1); + CompiledExpression compiledLeft = + asBooleanExpression(identifiedLeft.expression(), metadata, identifiedLeft.id()); + CompiledExpression compiledRight = + asBooleanExpression(identifiedRight.expression(), metadata, identifiedRight.id()); + if (compiledLeft.isConstant()) { + return (asBoolean(compiledLeft.constant()) == unit) ? compiledRight : compiledLeft; + } + if (compiledRight.isConstant()) { + return (asBoolean(compiledRight.constant()) == unit) ? compiledLeft : compiledRight; + } + + if (compiledLeft.isThrowing() && compiledRight.isThrowing()) { + // Both operands are throwing: arbitrarily pick the left exception to be propagated. + return compiledLeft; + } + + // Neither operand is constant and not both are simultaneously throwing, so perform everything + // at runtime. + ExecutableExpression executableLeft = compiledLeft.toExecutable(); + ExecutableExpression executableRight = compiledRight.toExecutable(); + Effect effect = compiledLeft.effect().meet(compiledRight.effect()); + + return CompiledExpression.executable( + stack -> { + ImmutableList> orderedFutures = + Futures.inCompletionOrder( + ImmutableList.of(executableLeft.execute(stack), executableRight.execute(stack))); + FluentFuture firstFuture = FluentFuture.from(orderedFutures.get(0)); + FluentFuture secondFuture = FluentFuture.from(orderedFutures.get(1)); + return firstFuture + .transformAsync( + first -> (asBoolean(first) == unit) ? secondFuture : immediateValue(!unit), + directExecutor()) + .catchingAsync( + Exception.class, + firstExn -> + secondFuture.transformAsync( + second -> + (asBoolean(second) == unit) + ? immediateException(firstExn) + : immediateValue(!unit), + directExecutor()), + directExecutor()); + }, + effect); + } + + /** Compiles the internal "not_strictly_false" function. */ + private static CompiledExpression constructNotStrictlyFalse( + Metadata metadata, long id, List compiledArguments) + throws InterpreterException { + IdentifiedCompiledExpression identified = compiledArguments.get(0); + CompiledExpression argument = + asBooleanExpression(identified.expression(), metadata, identified.id()); + return argument.map( + (executableArgument, effect) -> + CompiledExpression.executable( + stack -> + executableArgument + .execute(stack) + .catchingAsync( + Exception.class, t -> immediateValue(true), directExecutor()), + effect), + CompiledExpression::constant, + t -> CompiledExpression.constant(true)); + } + + /** Compiles the CEL "type" function. */ + // This lambda implements @Immutable interface 'ExecutableExpression', but accesses instance + // method(s) 'resolveType' on 'StandardConstructs' which is not @Immutable. + @SuppressWarnings("Immutable") + private CompiledExpression constructType( + Metadata metadata, long id, List compiledArguments) + throws InterpreterException { + IdentifiedCompiledExpression identified = compiledArguments.get(0); + long argId = identified.id(); + Value checkedTypeValue = typeResolver.adaptType(identified.type()); + return identified + .expression() + .mapNonThrowing( + e -> + stack -> + e.execute(stack) + .transformAsync( + t -> immediateValue(resolveType(metadata, argId, t, checkedTypeValue)), + directExecutor()), + c -> CompiledExpression.constant(resolveType(metadata, argId, c, checkedTypeValue))); + } + + /** Helper used by the CEL "type" function implementation. */ + private Object resolveType( + Metadata metadata, long exprId, Object obj, @Nullable Value checkedTypeValue) + throws InterpreterException { + @Nullable Object typeValue = typeResolver.resolveObjectType(obj, checkedTypeValue); + if (typeValue != null) { + return typeValue; + } + throw new InterpreterException.Builder( + "expected a runtime type for '%s', but found none.", obj.getClass().getSimpleName()) + .setErrorCode(CelErrorCode.TYPE_NOT_FOUND) + .setLocation(metadata, exprId) + .build(); + } + + /** Compiles regular expression matching. */ + private CompiledExpression constructRegexpMatch( + Metadata metadata, long id, List compiledArguments) + throws InterpreterException { + CompiledExpression compiledString = compiledArguments.get(0).expression(); + CompiledExpression compiledPattern = compiledArguments.get(1).expression(); + return compiledPattern.map( + (executableRegexp, regexpEffect) -> { + ExecutableExpression executableString = compiledString.toExecutable(); + return CompiledExpression.executable( + stack -> + executableString + .execute(stack) + .transformAsync( + string -> + executableRegexp + .execute(stack) + .transformAsync( + regexp -> + immediateValue( + RuntimeHelpers.matches( + (String) string, (String) regexp, celOptions)), + directExecutor()), + directExecutor()), + regexpEffect.meet(compiledString.effect())); + }, + constantRegexp -> { + Pattern pattern = RuntimeHelpers.compilePattern((String) constantRegexp); + return compiledString.mapNonThrowing( + executableString -> + stack -> + executableString + .execute(stack) + .transformAsync( + string -> { + if (!celOptions.enableRegexPartialMatch()) { + return immediateValue(pattern.matches((String) string)); + } + return immediateValue(pattern.matcher((String) string).find()); + }, + directExecutor()), + constantString -> + CompiledExpression.constant( + !celOptions.enableRegexPartialMatch() + ? pattern.matches((String) constantString) + : pattern.matcher((String) constantString).find())); + }, + t -> CompiledExpression.throwing(t)); + } + + /** Compiles collection membership tests. */ + // This lambda implements @Immutable interface 'ExecutableExpression', but the declaration of type + // 'java.util.function.BiFunction' is not + // annotated with @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + private static CompiledExpression constructCollectionMembershipTest( + List compiledArguments, + BiFunction dynamicTest, // (element, collection) -> boolean + Function> makeConstantSet) + throws InterpreterException { // collection -> set + CompiledExpression compiledElement = compiledArguments.get(0).expression(); + CompiledExpression compiledCollection = compiledArguments.get(1).expression(); + return compiledCollection.map( + (executableCollection, collectionEffect) -> { + ExecutableExpression executableElement = compiledElement.toExecutable(); + return CompiledExpression.executable( + stack -> + executableElement + .execute(stack) + .transformAsync( + element -> + executableCollection + .execute(stack) + .transformAsync( + collection -> + immediateValue(dynamicTest.apply(element, collection)), + directExecutor()), + directExecutor()), + collectionEffect.meet(compiledElement.effect())); + }, + constantCollectionObject -> { + ImmutableSet constantSet = makeConstantSet.apply(constantCollectionObject); + return compiledElement.mapNonThrowing( + executableElement -> + stack -> + executableElement + .execute(stack) + .transformAsync( + element -> immediateValue(constantSet.contains(element)), + directExecutor()), + constantElement -> + CompiledExpression.constant(constantSet.contains(constantElement))); + }, + t -> CompiledExpression.throwing(t)); + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/StandardTypeResolver.java b/legacy/java/dev/cel/legacy/runtime/async/StandardTypeResolver.java new file mode 100644 index 000000000..7c2f943cb --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/StandardTypeResolver.java @@ -0,0 +1,242 @@ +// Copyright 2022 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.legacy.runtime.async; + +import static com.google.common.base.Preconditions.checkNotNull; + +import dev.cel.expr.Type; +import dev.cel.expr.Type.PrimitiveType; +import dev.cel.expr.Type.TypeKindCase; +import dev.cel.expr.Value; +import dev.cel.expr.Value.KindCase; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.UnsignedLong; +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.ByteString; +import com.google.protobuf.MessageOrBuilder; +import com.google.protobuf.NullValue; +import dev.cel.common.CelOptions; +import dev.cel.common.annotations.Internal; +import dev.cel.common.types.CelKind; +import dev.cel.common.types.CelType; +import dev.cel.common.types.TypeType; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import org.jspecify.annotations.Nullable; + +/** + * The {@code StandardTypeResolver} implements the {@link TypeResolver} and resolves types supported + * by the CEL standard environment. + * + *

CEL Library Internals. Do Not Use. + * + * @deprecated Migrate to CEL-Java fluent APIs. See go/cel-java-migration-guide + */ +@Immutable +@Internal +@Deprecated +public final class StandardTypeResolver implements TypeResolver { + + /** + * Obtain a singleton instance of the {@link StandardTypeResolver} appropriate for the {@code + * celOptions} provided. + */ + public static TypeResolver getInstance(CelOptions celOptions) { + return celOptions.enableUnsignedLongs() ? INSTANCE_WITH_UNSIGNED_LONGS : INSTANCE; + } + + private static final TypeResolver INSTANCE = + new StandardTypeResolver(commonTypes(/* unsignedLongs= */ false)); + + private static final TypeResolver INSTANCE_WITH_UNSIGNED_LONGS = + new StandardTypeResolver(commonTypes(/* unsignedLongs= */ true)); + + // Type of type which is modelled as a value instance rather than as a Java POJO. + private static final Value TYPE_VALUE = createType("type"); + + // Built-in types. + private static ImmutableMap> commonTypes(boolean unsignedLongs) { + return ImmutableMap.>builder() + .put(createType("bool"), Boolean.class) + .put(createType("bytes"), ByteString.class) + .put(createType("double"), Double.class) + .put(createType("int"), Long.class) + .put(createType("uint"), unsignedLongs ? UnsignedLong.class : Long.class) + .put(createType("string"), String.class) + .put(createType("null_type"), NullValue.class) + // Aggregate types. + .put(createType("list"), Collection.class) + .put(createType("map"), Map.class) + // Optional type + .put(createType("optional_type"), Optional.class) + .buildOrThrow(); + } + + private final ImmutableMap> types; + + private StandardTypeResolver(ImmutableMap> types) { + this.types = types; + } + + @Nullable + @Override + public Value resolveObjectType(Object obj, @Nullable Value checkedTypeValue) { + if (checkedTypeValue != null && (obj instanceof Long || obj instanceof NullValue)) { + return checkedTypeValue; + } + return resolveObjectType(obj); + } + + @Nullable + private Value resolveObjectType(Object obj) { + for (Value type : types.keySet()) { + Class impl = types.get(type); + // Generally, the type will be an instance of a class. + if (impl.isInstance(obj)) { + return type; + } + } + // In the case 'type' values, the obj will be a api.expr.Value. + if (obj instanceof Value) { + Value objVal = (Value) obj; + if (objVal.getKindCase() == KindCase.TYPE_VALUE) { + return TYPE_VALUE; + } + } + // Otherwise, this is a protobuf type. + if (obj instanceof MessageOrBuilder) { + MessageOrBuilder msg = (MessageOrBuilder) obj; + return createType(msg.getDescriptorForType().getFullName()); + } + return null; + } + + /** {@inheritDoc} */ + @Override + public @Nullable Value adaptType(CelType type) { + checkNotNull(type); + // TODO: Add enum type support here. + Value.Builder typeValue = Value.newBuilder(); + switch (type.kind()) { + case OPAQUE: + case STRUCT: + return typeValue.setTypeValue(type.name()).build(); + case LIST: + return typeValue.setTypeValue("list").build(); + case MAP: + return typeValue.setTypeValue("map").build(); + case TYPE: + CelType typeOfType = ((TypeType) type).type(); + if (typeOfType.kind() == CelKind.DYN) { + return typeValue.setTypeValue("type").build(); + } + return adaptType(typeOfType); + case NULL_TYPE: + return typeValue.setTypeValue("null_type").build(); + case DURATION: + return typeValue.setTypeValue("google.protobuf.Duration").build(); + case TIMESTAMP: + return typeValue.setTypeValue("google.protobuf.Timestamp").build(); + case BOOL: + return typeValue.setTypeValue("bool").build(); + case BYTES: + return typeValue.setTypeValue("bytes").build(); + case DOUBLE: + return typeValue.setTypeValue("double").build(); + case INT: + return typeValue.setTypeValue("int").build(); + case STRING: + return typeValue.setTypeValue("string").build(); + case UINT: + return typeValue.setTypeValue("uint").build(); + default: + break; + } + return null; + } + + /** {@inheritDoc} */ + @Override + @Deprecated + public @Nullable Value adaptType(@Nullable Type type) { + if (type == null) { + return null; + } + // TODO: Add enum type support here. + Value.Builder typeValue = Value.newBuilder(); + switch (type.getTypeKindCase()) { + case ABSTRACT_TYPE: + return typeValue.setTypeValue(type.getAbstractType().getName()).build(); + case MESSAGE_TYPE: + return typeValue.setTypeValue(type.getMessageType()).build(); + case LIST_TYPE: + return typeValue.setTypeValue("list").build(); + case MAP_TYPE: + return typeValue.setTypeValue("map").build(); + case TYPE: + Type typeOfType = type.getType(); + if (typeOfType.getTypeKindCase() == TypeKindCase.DYN) { + return typeValue.setTypeValue("type").build(); + } + return adaptType(typeOfType); + case NULL: + return typeValue.setTypeValue("null_type").build(); + case PRIMITIVE: + return adaptPrimitive(type.getPrimitive()); + case WRAPPER: + return adaptPrimitive(type.getWrapper()); + case WELL_KNOWN: + switch (type.getWellKnown()) { + case DURATION: + return typeValue.setTypeValue("google.protobuf.Duration").build(); + case TIMESTAMP: + return typeValue.setTypeValue("google.protobuf.Timestamp").build(); + default: + break; + } + break; + default: + break; + } + return null; + } + + @Nullable + private static Value adaptPrimitive(PrimitiveType primitiveType) { + Value.Builder typeValue = Value.newBuilder(); + switch (primitiveType) { + case BOOL: + return typeValue.setTypeValue("bool").build(); + case BYTES: + return typeValue.setTypeValue("bytes").build(); + case DOUBLE: + return typeValue.setTypeValue("double").build(); + case INT64: + return typeValue.setTypeValue("int").build(); + case STRING: + return typeValue.setTypeValue("string").build(); + case UINT64: + return typeValue.setTypeValue("uint").build(); + default: + break; + } + return null; + } + + private static Value createType(String name) { + return Value.newBuilder().setTypeValue(name).build(); + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/TypeDirectedMessageProcessor.java b/legacy/java/dev/cel/legacy/runtime/async/TypeDirectedMessageProcessor.java new file mode 100644 index 000000000..538bb4983 --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/TypeDirectedMessageProcessor.java @@ -0,0 +1,286 @@ +package dev.cel.legacy.runtime.async; + +import static dev.cel.legacy.runtime.async.Canonicalization.asMessage; +import static dev.cel.legacy.runtime.async.Canonicalization.fieldHasWrapperType; +import static dev.cel.legacy.runtime.async.Canonicalization.fieldValueCanonicalizer; + +import dev.cel.expr.Type; +import com.google.auto.value.AutoValue; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.ExtensionRegistry.ExtensionInfo; +import com.google.protobuf.Message; +import com.google.protobuf.MessageOrBuilder; +import com.google.protobuf.NullValue; +import dev.cel.common.CelOptions; +import dev.cel.legacy.runtime.async.Canonicalization.Canonicalizer; +import dev.cel.runtime.InterpreterException; +import dev.cel.runtime.Metadata; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * An implementation of {@link MessageProcessor} that performs as much work as possible during the + * static phase by taking advantage of available proto descriptor information as well as CEL type + * information during the first phase (i.e., during the construction of {@link MessageCreator}, + * {@link FieldGetter}, and {@link FieldTester} objects). + */ +public final class TypeDirectedMessageProcessor implements MessageProcessor { + + /** + * Information about a message consisting of the message {@link Descriptor} and a supplier of + * builders for constructing new message instances. + */ + @AutoValue + public abstract static class MessageInfo { + public abstract Descriptor descriptor(); + + public abstract Supplier messageBuilderSupplier(); + + public static MessageInfo of( + Descriptor descriptor, Supplier messageBuilderSupplier) { + return new AutoValue_TypeDirectedMessageProcessor_MessageInfo( + descriptor, messageBuilderSupplier); + } + } + + private final Function> messageInfoLookup; + private final Function> extensionLookup; + private final CelOptions celOptions; + + public TypeDirectedMessageProcessor( + Function> messageInfoLookup, + Function> extensionLookup) { + this(messageInfoLookup, extensionLookup, CelOptions.LEGACY); + } + + public TypeDirectedMessageProcessor( + Function> messageInfoLookup, + Function> extensionLookup, + CelOptions celOptions) { + this.messageInfoLookup = messageInfoLookup; + this.extensionLookup = extensionLookup; + this.celOptions = celOptions; + } + + // This lambda implements @Immutable interface 'FieldGetter', but 'Object' is mutable + @SuppressWarnings("Immutable") + private FieldGetter getterFromDescriptor( + FieldDescriptor fd, Optional maybeDefaultInstance) { + Canonicalizer canonicalizer = fieldValueCanonicalizer(fd, celOptions); + if (fieldHasWrapperType(fd)) { + return value -> { + MessageOrBuilder message = asMessage(value); + return message.hasField(fd) + ? canonicalizer.canonicalize(message.getField(fd)) + : NullValue.NULL_VALUE; + }; + } + if (!fd.isRepeated() && maybeDefaultInstance.isPresent()) { + // If the field is an extension field but is not present, + // then message.getField(fd) will return an instance of + // DynamicMessage. To avoid that, this code explicitly + // uses the default instance from the registry instead. + Object defaultInstance = maybeDefaultInstance.get(); + return value -> { + MessageOrBuilder message = asMessage(value); + return message.hasField(fd) + ? canonicalizer.canonicalize(message.getField(fd)) + : defaultInstance; + }; + } + return value -> canonicalizer.canonicalize(asMessage(value).getField(fd)); + } + + @Override + public FieldGetter makeFieldGetter( + Metadata metadata, long exprId, String messageName, String fieldName) + throws InterpreterException { + Descriptor descriptor = getDescriptor(metadata, exprId, messageName); + FieldDescriptor fd = getFieldDescriptor(metadata, exprId, descriptor, fieldName); + if (fd.getType() == FieldDescriptor.Type.MESSAGE) { + // Check to see whether the message descriptor for the field's type uses + // the "canonical" descriptor for that type (as obtained by messageInfoLookup). + Descriptor fieldValueDescriptor = fd.getMessageType(); + MessageInfo fieldMessageInfo = + getMessageInfo(metadata, exprId, fieldValueDescriptor.getFullName()); + Descriptor canonicalDescriptor = fieldMessageInfo.descriptor(); + if (fieldValueDescriptor != canonicalDescriptor) { // pointer inequality! + // The descriptor is not canonical, so use an explicit presence test + // and use the canonical default instance. Otherwise the default instance + // would use the wrong (non-canonical) descriptor and cause problems down + // the road. + return getterFromDescriptor( + fd, Optional.of(fieldMessageInfo.messageBuilderSupplier().get().build())); + } + } + return getterFromDescriptor(fd, Optional.empty()); + } + + @Override + public FieldTester makeFieldTester( + Metadata metadata, long exprId, String messageName, String fieldName) + throws InterpreterException { + Descriptor descriptor = getDescriptor(metadata, exprId, messageName); + FieldDescriptor fd = getFieldDescriptor(metadata, exprId, descriptor, fieldName); + return fd.isRepeated() + ? value -> asMessage(value).getRepeatedFieldCount(fd) > 0 + : value -> asMessage(value).hasField(fd); + } + + @Override + public FieldAssigner makeFieldAssigner( + Metadata metadata, long exprId, String messageName, String fieldName, Type fieldType) + throws InterpreterException { + Descriptor descriptor = getDescriptor(metadata, exprId, messageName); + FieldDescriptor fd = getFieldDescriptor(metadata, exprId, descriptor, fieldName); + return ProtoFieldAssignment.fieldValueAssigner(metadata, exprId, fd, fieldType); + } + + // This method reference implements @Immutable interface MessageBuilderCreator, but the + // declaration of type 'java.util.function.Supplier' is not + // annotated with @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + @Override + public MessageBuilderCreator makeMessageBuilderCreator( + Metadata metadata, long exprId, String messageName) throws InterpreterException { + Supplier builderSupplier = + getMessageInfo(metadata, exprId, messageName).messageBuilderSupplier(); + return builderSupplier::get; + } + + @Override + public FieldClearer makeFieldClearer( + Metadata metadata, long exprId, String messageName, String fieldName) + throws InterpreterException { + Descriptor descriptor = getDescriptor(metadata, exprId, messageName); + FieldDescriptor fd = getFieldDescriptor(metadata, exprId, descriptor, fieldName); + return builder -> builder.clearField(fd); + } + + @Override + public Object dynamicGetField( + Metadata metadata, long exprId, Object messageObject, String fieldName) + throws InterpreterException { + MessageOrBuilder message = expectMessage(metadata, exprId, messageObject); + Descriptor descriptor = message.getDescriptorForType(); + FieldDescriptor fieldDescriptor = getFieldDescriptor(metadata, exprId, descriptor, fieldName); + if (fieldHasWrapperType(fieldDescriptor) && !message.hasField(fieldDescriptor)) { + return NullValue.NULL_VALUE; + } + Object value = message.getField(fieldDescriptor); + return fieldValueCanonicalizer(fieldDescriptor, celOptions).canonicalize(value); + } + + @Override + public boolean dynamicHasField( + Metadata metadata, long exprId, Object messageObject, String fieldName) + throws InterpreterException { + MessageOrBuilder message = expectMessage(metadata, exprId, messageObject); + Descriptor descriptor = message.getDescriptorForType(); + FieldDescriptor fieldDescriptor = getFieldDescriptor(metadata, exprId, descriptor, fieldName); + return message.hasField(fieldDescriptor); + } + + @Override + public FieldGetter makeExtensionGetter(Metadata metadata, long exprId, String extensionName) + throws InterpreterException { + ExtensionInfo extensionInfo = getExtensionInfo(metadata, exprId, extensionName); + if (extensionInfo.defaultInstance != null) { + Descriptor fieldValueDescriptor = extensionInfo.defaultInstance.getDescriptorForType(); + MessageInfo fieldMessageInfo = + getMessageInfo(metadata, exprId, fieldValueDescriptor.getFullName()); + Descriptor canonicalDescriptor = fieldMessageInfo.descriptor(); + // If the default instance provided by the extension info does not use the + // "canonical" descriptor (as obtained by messageInfoLookup), then generate + // a new canonical default instance instead. + if (fieldValueDescriptor != canonicalDescriptor) { + return getterFromDescriptor( + extensionInfo.descriptor, + Optional.of(fieldMessageInfo.messageBuilderSupplier().get().build())); + } else { + return getterFromDescriptor( + extensionInfo.descriptor, Optional.of(extensionInfo.defaultInstance)); + } + } + return getterFromDescriptor(extensionInfo.descriptor, Optional.empty()); + } + + @Override + public FieldTester makeExtensionTester(Metadata metadata, long exprId, String extensionName) + throws InterpreterException { + FieldDescriptor fd = getExtensionInfo(metadata, exprId, extensionName).descriptor; + return fd.isRepeated() + ? value -> asMessage(value).getRepeatedFieldCount(fd) > 0 + : value -> asMessage(value).hasField(fd); + } + + @Override + public FieldAssigner makeExtensionAssigner( + Metadata metadata, long exprId, String extensionName, Type extensionType) + throws InterpreterException { + FieldDescriptor fd = getExtensionInfo(metadata, exprId, extensionName).descriptor; + return ProtoFieldAssignment.fieldValueAssigner(metadata, exprId, fd, extensionType); + } + + @Override + public FieldClearer makeExtensionClearer(Metadata metadata, long exprId, String extensionName) + throws InterpreterException { + FieldDescriptor fd = getExtensionInfo(metadata, exprId, extensionName).descriptor; + return builder -> builder.clearField(fd); + } + + private MessageInfo getMessageInfo(Metadata metadata, long id, String messageName) + throws InterpreterException { + return messageInfoLookup + .apply(messageName) + .orElseThrow( + () -> + new InterpreterException.Builder("cannot resolve '%s' as a message", messageName) + .setLocation(metadata, id) + .build()); + } + + private Descriptor getDescriptor(Metadata metadata, long id, String messageName) + throws InterpreterException { + return getMessageInfo(metadata, id, messageName).descriptor(); + } + + private static MessageOrBuilder expectMessage( + Metadata metadata, long exprId, Object messageObject) throws InterpreterException { + if (messageObject instanceof MessageOrBuilder) { + return (MessageOrBuilder) messageObject; + } + throw new InterpreterException.Builder( + "expected an instance of 'com.google.protobuf.MessageOrBuilder' " + "but found '%s'", + messageObject.getClass().getName()) + .setLocation(metadata, exprId) + .build(); + } + + private static FieldDescriptor getFieldDescriptor( + Metadata metadata, long exprId, Descriptor descriptor, String fieldName) + throws InterpreterException { + FieldDescriptor fieldDescriptor = descriptor.findFieldByName(fieldName); + if (fieldDescriptor == null) { + throw new InterpreterException.Builder( + "field '%s' is not declared in message '%s'", fieldName, descriptor.getName()) + .setLocation(metadata, exprId) + .build(); + } + return fieldDescriptor; + } + + private ExtensionInfo getExtensionInfo(Metadata metadata, long exprId, String extensionName) + throws InterpreterException { + return extensionLookup + .apply(extensionName) + .orElseThrow( + () -> + new InterpreterException.Builder( + "cannot resolve '%s' as message extension", extensionName) + .setLocation(metadata, exprId) + .build()); + } +} diff --git a/legacy/java/dev/cel/legacy/runtime/async/TypeResolver.java b/legacy/java/dev/cel/legacy/runtime/async/TypeResolver.java new file mode 100644 index 000000000..f3ad4452a --- /dev/null +++ b/legacy/java/dev/cel/legacy/runtime/async/TypeResolver.java @@ -0,0 +1,67 @@ +// Copyright 2023 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.legacy.runtime.async; + +import dev.cel.expr.Type; +import dev.cel.expr.Value; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.annotations.Internal; +import dev.cel.common.types.CelType; +import org.jspecify.annotations.Nullable; + +/** + * The {@code TypeResolver} determines the CEL type of Java-native values and assists with adapting + * check-time types to runtime values. + * + *

CEL Library Internals. Do Not Use. + */ +@Immutable +@Internal +public interface TypeResolver { + + /** + * Resolve the CEL type of the {@code obj}, using the {@code checkedTypeValue} as hint for type + * disambiguation. + * + *

The {@code checkedTypeValue} indicates the statically determined type of the object at + * check-time. Often, the check-time and runtime phases agree, but there are cases where the + * runtime type is ambiguous, as is the case when a {@code Long} value is supplied as this could + * either be an int, uint, or enum type. + * + *

Type resolution is biased toward the runtime value type, given the dynamically typed nature + * of CEL. + */ + @Nullable Value resolveObjectType(Object obj, @Nullable Value checkedTypeValue); + + /** + * Adapt the check-time {@code type} instance to a runtime {@code Value}. + * + *

When the checked {@code type} does not have a runtime equivalent, e.g. {@code Type#DYN}, the + * return value will be {@code null}. + */ + @Nullable Value adaptType(CelType type); + + /** + * Adapt the check-time {@code type} instance to a runtime {@code Value}. + * + *

When the checked {@code type} does not have a runtime equivalent, e.g. {@code Type#DYN}, the + * return value will be {@code null}. + * + * @deprecated use {@link #adaptType(CelType)} instead. This only exists to maintain compatibility + * with legacy async evaluator. + */ + @Deprecated + @Nullable Value adaptType(@Nullable Type type); +} diff --git a/legacy/javatests/dev/cel/legacy/runtime/async/AsyncInterpreterTest.java b/legacy/javatests/dev/cel/legacy/runtime/async/AsyncInterpreterTest.java new file mode 100644 index 000000000..5032608a3 --- /dev/null +++ b/legacy/javatests/dev/cel/legacy/runtime/async/AsyncInterpreterTest.java @@ -0,0 +1,65 @@ +package dev.cel.legacy.runtime.async; + +// import com.google.testing.testsize.MediumTest; +import dev.cel.common.CelOptions; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Tests for {@code Interpreter} and related functionality via the asynchronous evaluator. + * + *

TODO: Remove inheritance from `BaseInterpreterTest` and fork these tests. + */ +// @MediumTest +@RunWith(Parameterized.class) +public class AsyncInterpreterTest extends BaseInterpreterTest { + + private static final CelOptions ASYNC_TEST_OPTIONS = + CelOptions.current() + .enableTimestampEpoch(true) + .enableUnsignedLongs(true) + .enableHeterogeneousNumericComparisons(true) + // comprehension iteration limits are not supported in the async evaluation stacks. + .comprehensionMaxIterations(-1) + .build(); + + public AsyncInterpreterTest(boolean declareWithCelType, Eval eval) { + super(declareWithCelType, eval); + } + + @Parameters + public static List testData() { + return new ArrayList<>( + Arrays.asList( + new Object[][] { + // ASYNC_PROTO_TYPE + { + /* declareWithCelType= */ false, + new EvalAsync( + TEST_FILE_DESCRIPTORS, ASYNC_TEST_OPTIONS, /* typeDirectedProcessor= */ false) + }, + // ASYNC_PROTO_TYPE_DIRECTED_PROCESSOR + { + /* declareWithCelType= */ false, + new EvalAsync( + TEST_FILE_DESCRIPTORS, ASYNC_TEST_OPTIONS, /* typeDirectedProcessor= */ true) + }, + // ASYNC_CEL_TYPE + { + /* declareWithCelType= */ true, + new EvalAsync( + TEST_FILE_DESCRIPTORS, ASYNC_TEST_OPTIONS, /* typeDirectedProcessor= */ false) + }, + // ASYNC_CEL_TYPE_DIRECTED_PROCESSOR + { + /* declareWithCelType= */ true, + new EvalAsync( + TEST_FILE_DESCRIPTORS, ASYNC_TEST_OPTIONS, /* typeDirectedProcessor= */ true) + }, + })); + } +} diff --git a/legacy/javatests/dev/cel/legacy/runtime/async/BUILD.bazel b/legacy/javatests/dev/cel/legacy/runtime/async/BUILD.bazel new file mode 100644 index 000000000..91566fe78 --- /dev/null +++ b/legacy/javatests/dev/cel/legacy/runtime/async/BUILD.bazel @@ -0,0 +1,102 @@ +load("@rules_java//java:java_library.bzl", "java_library") +load("//:testing.bzl", "junit4_test_suites") + +package( + default_applicable_licenses = ["//:license"], +) + +java_library( + name = "DynamicEnvTest", + testonly = 1, + srcs = ["DynamicEnvTest.java"], + deps = [ + "//:java_truth", + "//third_party/java/cel/legacy:async_runtime", + "//third_party/java/cel/legacy:dummy_async_context", + "@maven//:com_google_guava_guava", + "@maven//:junit_junit", + ], +) + +java_library( + name = "mediumtests", + testonly = 1, + srcs = + [ + "AsyncInterpreterTest.java", + "FuturesInterpreterTest.java", + "FuturesInterpreterWithMessageProcessorTest.java", + ], + resources = [ + "//common/resources/testdata/proto3:test_all_types_file_descriptor_set", + "//runtime/testdata", + ], + deps = [ + ":base_interpreter_test", + "//java/com/google/common/context", + # "//java/com/google/testing/testsize:annotations", + "//common", + "//common:options", + "//common/resources/testdata/proto3:standalone_global_enum_java_proto", + "//common/types:cel_proto_types", + "//third_party/java/cel/legacy:async_runtime", + "//runtime:interpreter", + "//testing:cel_baseline_test_case", + "@maven//:junit_junit", + "@cel_spec//proto/cel/expr:expr_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +java_library( + name = "base_interpreter_test", + testonly = 1, + srcs = [ + "BaseInterpreterTest.java", + "Eval.java", + "EvalAsync.java", + ], + resources = [ + "//common/resources/testdata/proto3:test_all_types_file_descriptor_set", + "//runtime/testdata", + ], + deps = [ + "//:java_truth", + "//common", + "//common:options", + "//common:proto_ast", + "//common/internal:cel_descriptor_pools", + "//common/internal:file_descriptor_converter", + "//common/resources/testdata/proto3:standalone_global_enum_java_proto", + "//common/types:cel_proto_types", + "//java/com/google/common/context", + "//java/com/google/protobuf/contrib/descriptor/pool:mutable_descriptor_pool", + "//java/com/google/security/context/testing:fake_unvalidated_security_context_builder", + "//runtime:interpreter", + "//runtime:linked_message_factory", + "//testing:cel_baseline_test_case", + "//third_party/java/cel/legacy:async_runtime", + "@cel_spec//proto/cel/expr:expr_java_proto", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_protobuf_protobuf_java_util", + "@maven//:junit_junit", + ], +) + +junit4_test_suites( + name = "testsuites", + sizes = [ + "small", + "medium", + ], + deps = [ + ":DynamicEnvTest", + ":mediumtests", + ], +) diff --git a/legacy/javatests/dev/cel/legacy/runtime/async/BaseInterpreterTest.java b/legacy/javatests/dev/cel/legacy/runtime/async/BaseInterpreterTest.java new file mode 100644 index 000000000..f4609ad6f --- /dev/null +++ b/legacy/javatests/dev/cel/legacy/runtime/async/BaseInterpreterTest.java @@ -0,0 +1,1683 @@ +package dev.cel.legacy.runtime.async; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; + +import dev.cel.expr.CheckedExpr; +import dev.cel.expr.Type; +import dev.cel.expr.Type.AbstractType; +import com.google.common.base.Ascii; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; +import com.google.common.primitives.UnsignedLong; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; +import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.DoubleValue; +import com.google.protobuf.Duration; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.FloatValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.ListValue; +import com.google.protobuf.NullValue; +import com.google.protobuf.StringValue; +import com.google.protobuf.Struct; +import com.google.protobuf.TextFormat; +import com.google.protobuf.Timestamp; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.UInt64Value; +import com.google.protobuf.Value; +import com.google.protobuf.util.Timestamps; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelProtoAbstractSyntaxTree; +import dev.cel.common.internal.DefaultDescriptorPool; +import dev.cel.common.internal.FileDescriptorSetConverter; +import dev.cel.common.types.CelProtoTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum; +import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; +import dev.cel.runtime.Activation; +import dev.cel.runtime.InterpreterException; +import dev.cel.testing.CelBaselineTestCase; +import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.stream.LongStream; +import org.junit.Test; + +/** Base class for evaluation outputs that can be stored and used as a baseline test. */ +public abstract class BaseInterpreterTest extends CelBaselineTestCase { + + protected static final Descriptor TEST_ALL_TYPE_DYNAMIC_DESCRIPTOR = + getDeserializedTestAllTypeDescriptor(); + + protected static final ImmutableList TEST_FILE_DESCRIPTORS = + ImmutableList.of( + TestAllTypes.getDescriptor().getFile(), + StandaloneGlobalEnum.getDescriptor().getFile(), + TEST_ALL_TYPE_DYNAMIC_DESCRIPTOR.getFile()); + + private final Eval eval; + + private static Descriptor getDeserializedTestAllTypeDescriptor() { + try { + String fdsContent = readResourceContent("testdata/proto3/test_all_types.fds"); + FileDescriptorSet fds = TextFormat.parse(fdsContent, FileDescriptorSet.class); + ImmutableSet fileDescriptors = FileDescriptorSetConverter.convert(fds); + + return fileDescriptors.stream() + .flatMap(f -> f.getMessageTypes().stream()) + .filter( + x -> + x.getFullName().equals("dev.cel.testing.testdata.serialized.proto3.TestAllTypes")) + .findAny() + .orElseThrow( + () -> + new IllegalStateException( + "Could not find deserialized TestAllTypes descriptor.")); + } catch (IOException e) { + throw new RuntimeException("Error loading TestAllTypes descriptor", e); + } + } + + public BaseInterpreterTest(boolean declareWithCelType, Eval eval) { + super(declareWithCelType); + this.eval = eval; + } + + /** Helper to run a test for configured instance variables. */ + @CanIgnoreReturnValue // Test generates a file to diff against baseline. Ignoring Intermediary + // evaluation is not a concern. + private Object runTest(Activation activation) throws Exception { + CelAbstractSyntaxTree ast = prepareTest(eval.fileDescriptors()); + if (ast == null) { + return null; + } + assertAstRoundTrip(ast); + + testOutput().println("bindings: " + activation); + Object result = null; + try { + result = eval.eval(ast, activation); + if (result instanceof ByteString) { + // Note: this call may fail for printing byte sequences that are not valid UTF-8, but works + // pretty well for test purposes. + result = ((ByteString) result).toStringUtf8(); + } + testOutput().println("result: " + result); + } catch (InterpreterException e) { + testOutput().println("error: " + e.getMessage()); + testOutput().println("error_code: " + e.getErrorCode()); + } + testOutput().println(); + return result; + } + + /** + * Checks that the CheckedExpr produced by CelCompiler is equal to the one reproduced by the + * native CelAbstractSyntaxTree + */ + private void assertAstRoundTrip(CelAbstractSyntaxTree ast) { + CheckedExpr checkedExpr = CelProtoAbstractSyntaxTree.fromCelAst(ast).toCheckedExpr(); + CelProtoAbstractSyntaxTree protoAst = CelProtoAbstractSyntaxTree.fromCelAst(ast); + assertThat(checkedExpr).isEqualTo(protoAst.toCheckedExpr()); + } + + @Test + public void arithmInt64() throws Exception { + source = "1 < 2 && 1 <= 1 && 2 > 1 && 1 >= 1 && 1 == 1 && 2 != 1"; + runTest(Activation.EMPTY); + + declareVariable("x", CelProtoTypes.INT64); + source = "1 + 2 - x * 3 / x + (x % 3)"; + runTest(Activation.of("x", -5L)); + + declareVariable("y", CelProtoTypes.DYN); + source = "x + y == 1"; + runTest(Activation.of("x", -5L).extend(Activation.of("y", 6))); + } + + @Test + public void arithmInt64_error() throws Exception { + source = "9223372036854775807 + 1"; + runTest(Activation.EMPTY); + + source = "-9223372036854775808 - 1"; + runTest(Activation.EMPTY); + + source = "-(-9223372036854775808)"; + runTest(Activation.EMPTY); + + source = "5000000000 * 5000000000"; + runTest(Activation.EMPTY); + + source = "(-9223372036854775808)/-1"; + runTest(Activation.EMPTY); + + source = "1 / 0"; + runTest(Activation.EMPTY); + + source = "1 % 0"; + runTest(Activation.EMPTY); + } + + @Test + public void arithmUInt64() throws Exception { + source = "1u < 2u && 1u <= 1u && 2u > 1u && 1u >= 1u && 1u == 1u && 2u != 1u"; + runTest(Activation.EMPTY); + + boolean useUnsignedLongs = eval.celOptions().enableUnsignedLongs(); + declareVariable("x", CelProtoTypes.UINT64); + source = "1u + 2u + x * 3u / x + (x % 3u)"; + runTest(Activation.of("x", useUnsignedLongs ? UnsignedLong.valueOf(5L) : 5L)); + + declareVariable("y", CelProtoTypes.DYN); + source = "x + y == 11u"; + runTest( + Activation.of("x", useUnsignedLongs ? UnsignedLong.valueOf(5L) : 5L) + .extend(Activation.of("y", useUnsignedLongs ? UnsignedLong.valueOf(6L) : 6))); + + source = "x - y == 1u"; + runTest( + Activation.of("x", useUnsignedLongs ? UnsignedLong.valueOf(6L) : 6L) + .extend(Activation.of("y", useUnsignedLongs ? UnsignedLong.valueOf(5) : 5))); + } + + @Test + public void arithmUInt64_error() throws Exception { + source = "18446744073709551615u + 1u"; + runTest(Activation.EMPTY); + + source = "0u - 1u"; + runTest(Activation.EMPTY); + + source = "5000000000u * 5000000000u"; + runTest(Activation.EMPTY); + + source = "1u / 0u"; + runTest(Activation.EMPTY); + + source = "1u % 0u"; + runTest(Activation.EMPTY); + } + + @Test + public void arithmDouble() throws Exception { + source = "1.9 < 2.0 && 1.1 <= 1.1 && 2.0 > 1.9 && 1.1 >= 1.1 && 1.1 == 1.1 && 2.0 != 1.9"; + runTest(Activation.EMPTY); + + declareVariable("x", CelProtoTypes.DOUBLE); + source = "1.0 + 2.3 + x * 3.0 / x"; + runTest(Activation.of("x", 3.33)); + + declareVariable("y", CelProtoTypes.DYN); + source = "x + y == 9.99"; + runTest(Activation.of("x", 3.33d).extend(Activation.of("y", 6.66))); + } + + @Test + public void quantifiers() throws Exception { + source = "[1,-2,3].exists_one(x, x > 0)"; + runTest(Activation.EMPTY); + + source = "[-1,-2,3].exists_one(x, x > 0)"; + runTest(Activation.EMPTY); + + source = "[-1,-2,-3].exists(x, x > 0)"; + runTest(Activation.EMPTY); + + source = "[1,-2,3].exists(x, x > 0)"; + runTest(Activation.EMPTY); + + source = "[1,-2,3].all(x, x > 0)"; + runTest(Activation.EMPTY); + + source = "[1,2,3].all(x, x > 0)"; + runTest(Activation.EMPTY); + } + + @Test + public void arithmTimestamp() throws Exception { + container = Type.getDescriptor().getFile().getPackage(); + declareVariable("ts1", CelProtoTypes.TIMESTAMP); + declareVariable("ts2", CelProtoTypes.TIMESTAMP); + declareVariable("d1", CelProtoTypes.DURATION); + Duration d1 = Duration.newBuilder().setSeconds(15).setNanos(25).build(); + Timestamp ts1 = Timestamp.newBuilder().setSeconds(25).setNanos(35).build(); + Timestamp ts2 = Timestamp.newBuilder().setSeconds(10).setNanos(10).build(); + Activation activation = + Activation.of("d1", d1).extend(Activation.of("ts1", ts1)).extend(Activation.of("ts2", ts2)); + + source = "ts1 - ts2 == d1"; + runTest(activation); + + source = "ts1 - d1 == ts2"; + runTest(activation); + + source = "ts2 + d1 == ts1"; + runTest(activation); + + source = "d1 + ts2 == ts1"; + runTest(activation); + } + + @Test + public void arithmDuration() throws Exception { + container = Type.getDescriptor().getFile().getPackage(); + declareVariable("d1", CelProtoTypes.DURATION); + declareVariable("d2", CelProtoTypes.DURATION); + declareVariable("d3", CelProtoTypes.DURATION); + Duration d1 = Duration.newBuilder().setSeconds(15).setNanos(25).build(); + Duration d2 = Duration.newBuilder().setSeconds(10).setNanos(20).build(); + Duration d3 = Duration.newBuilder().setSeconds(25).setNanos(45).build(); + Activation activation = + Activation.of("d1", d1).extend(Activation.of("d2", d2)).extend(Activation.of("d3", d3)); + + source = "d1 + d2 == d3"; + runTest(activation); + + source = "d3 - d1 == d2"; + runTest(activation); + } + + @Test + public void arithCrossNumericTypes() throws Exception { + if (!eval.celOptions().enableUnsignedLongs()) { + skipBaselineVerification(); + return; + } + source = "1.9 < 2 && 1 < 1.1 && 2u < 2.9 && 1.1 < 2u && 1 < 2u && 2u < 3"; + runTest(Activation.EMPTY); + + source = "1.9 <= 2 && 1 <= 1.1 && 2u <= 2.9 && 1.1 <= 2u && 2 <= 2u && 2u <= 2"; + runTest(Activation.EMPTY); + + source = "1.9 > 2 && 1 > 1.1 && 2u > 2.9 && 1.1 > 2u && 2 > 2u && 2u > 2"; + runTest(Activation.EMPTY); + + source = "1.9 >= 2 && 1 >= 1.1 && 2u >= 2.9 && 1.1 >= 2u && 2 >= 2u && 2u >= 2"; + runTest(Activation.EMPTY); + } + + @Test + public void booleans() throws Exception { + declareVariable("x", CelProtoTypes.BOOL); + source = "x ? 1 : 0"; + runTest(Activation.of("x", true)); + runTest(Activation.of("x", false)); + + source = "(1 / 0 == 0 && false) == (false && 1 / 0 == 0)"; + runTest(Activation.EMPTY); + + source = "(1 / 0 == 0 || true) == (true || 1 / 0 == 0)"; + runTest(Activation.EMPTY); + + declareVariable("y", CelProtoTypes.INT64); + source = "1 / y == 1 || true"; + runTest(Activation.of("y", 0L)); + + source = "1 / y == 1 || false"; + runTest(Activation.of("y", 0L)); + + source = "false || 1 / y == 1"; + runTest(Activation.of("y", 0L)); + + source = "1 / y == 1 && true"; + runTest(Activation.of("y", 0L)); + + source = "true && 1 / y == 1"; + runTest(Activation.of("y", 0L)); + + source = "1 / y == 1 && false"; + runTest(Activation.of("y", 0L)); + + source = "(true > false) == true"; + runTest(Activation.EMPTY); + + source = "(true > true) == false"; + runTest(Activation.EMPTY); + + source = "(false > true) == false"; + runTest(Activation.EMPTY); + + source = "(false > false) == false"; + runTest(Activation.EMPTY); + + source = "(true >= false) == true"; + runTest(Activation.EMPTY); + + source = "(true >= true) == true"; + runTest(Activation.EMPTY); + + source = "(false >= false) == true"; + runTest(Activation.EMPTY); + + source = "(false >= true) == false"; + runTest(Activation.EMPTY); + + source = "(false < true) == true"; + runTest(Activation.EMPTY); + + source = "(false < false) == false"; + runTest(Activation.EMPTY); + + source = "(true < false) == false"; + runTest(Activation.EMPTY); + + source = "(true < true) == false"; + runTest(Activation.EMPTY); + + source = "(false <= true) == true"; + runTest(Activation.EMPTY); + + source = "(false <= false) == true"; + runTest(Activation.EMPTY); + + source = "(true <= false) == false"; + runTest(Activation.EMPTY); + + source = "(true <= true) == true"; + runTest(Activation.EMPTY); + } + + @Test + public void strings() throws Exception { + source = "'a' < 'b' && 'a' <= 'b' && 'b' > 'a' && 'a' >= 'a' && 'a' == 'a' && 'a' != 'b'"; + runTest(Activation.EMPTY); + + declareVariable("x", CelProtoTypes.STRING); + source = + "'abc' + x == 'abcdef' && " + + "x.endsWith('ef') && " + + "x.startsWith('d') && " + + "x.contains('de') && " + + "!x.contains('abcdef')"; + runTest(Activation.of("x", "def")); + } + + @Test + public void messages() throws Exception { + TestAllTypes nestedMessage = + TestAllTypes.newBuilder() + .setSingleNestedMessage(NestedMessage.newBuilder().setBb(43)) + .build(); + declareVariable("x", CelProtoTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); + source = "x.single_nested_message.bb == 43 && has(x.single_nested_message)"; + runTest(Activation.of("x", nestedMessage)); + + declareVariable( + "single_nested_message", + CelProtoTypes.createMessage(NestedMessage.getDescriptor().getFullName())); + source = "single_nested_message.bb == 43"; + runTest(Activation.of("single_nested_message", nestedMessage.getSingleNestedMessage())); + + source = "TestAllTypes{single_int64: 1, single_sfixed64: 2, single_int32: 2}.single_int32 == 2"; + container = TestAllTypes.getDescriptor().getFile().getPackage(); + runTest(Activation.EMPTY); + } + + @Test + public void messages_error() throws Exception { + source = "TestAllTypes{single_int32_wrapper: 12345678900}"; + container = TestAllTypes.getDescriptor().getFile().getPackage(); + runTest(Activation.EMPTY); + + source = "TestAllTypes{}.map_string_string.a"; + runTest(Activation.EMPTY); + } + + @Test + public void has() throws Exception { + TestAllTypes nestedMessage = + TestAllTypes.newBuilder() + .setSingleInt32(1) + .setSingleInt64(0L) + .setSingleBoolWrapper(BoolValue.newBuilder().setValue(false)) + .setSingleInt32Wrapper(Int32Value.newBuilder().setValue(42)) + .setOptionalBool(false) + .setOneofBool(false) + .addRepeatedInt32(1) + .putMapInt32Int64(1, 2L) + .setSingleNestedMessage(NestedMessage.newBuilder().setBb(43)) + .build(); + declareVariable("x", CelProtoTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); + source = + "has(x.single_int32) && !has(x.single_int64) && has(x.single_bool_wrapper)" + + " && has(x.single_int32_wrapper) && !has(x.single_int64_wrapper)" + + " && has(x.repeated_int32) && !has(x.repeated_int64)" + + " && has(x.optional_bool) && !has(x.optional_string)" + + " && has(x.oneof_bool) && !has(x.oneof_type)" + + " && has(x.map_int32_int64) && !has(x.map_string_string)" + + " && has(x.single_nested_message) && !has(x.single_duration)"; + runTest(Activation.of("x", nestedMessage)); + } + + @Test + public void duration() throws Exception { + declareVariable("d1", CelProtoTypes.DURATION); + declareVariable("d2", CelProtoTypes.DURATION); + Duration d1010 = Duration.newBuilder().setSeconds(10).setNanos(10).build(); + Duration d1009 = Duration.newBuilder().setSeconds(10).setNanos(9).build(); + Duration d0910 = Duration.newBuilder().setSeconds(9).setNanos(10).build(); + container = Type.getDescriptor().getFile().getPackage(); + + source = "d1 < d2"; + runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1009))); + runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d0910))); + runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1010))); + runTest(Activation.of("d1", d1009).extend(Activation.of("d2", d1010))); + runTest(Activation.of("d1", d0910).extend(Activation.of("d2", d1010))); + + source = "d1 <= d2"; + runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1009))); + runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d0910))); + runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1010))); + runTest(Activation.of("d1", d1009).extend(Activation.of("d2", d1010))); + runTest(Activation.of("d1", d0910).extend(Activation.of("d2", d1010))); + + source = "d1 > d2"; + runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1009))); + runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d0910))); + runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1010))); + runTest(Activation.of("d1", d1009).extend(Activation.of("d2", d1010))); + runTest(Activation.of("d1", d0910).extend(Activation.of("d2", d1010))); + + source = "d1 >= d2"; + runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1009))); + runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d0910))); + runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1010))); + runTest(Activation.of("d1", d1009).extend(Activation.of("d2", d1010))); + runTest(Activation.of("d1", d0910).extend(Activation.of("d2", d1010))); + } + + @Test + public void timestamp() throws Exception { + declareVariable("t1", CelProtoTypes.TIMESTAMP); + declareVariable("t2", CelProtoTypes.TIMESTAMP); + Timestamp ts1010 = Timestamp.newBuilder().setSeconds(10).setNanos(10).build(); + Timestamp ts1009 = Timestamp.newBuilder().setSeconds(10).setNanos(9).build(); + Timestamp ts0910 = Timestamp.newBuilder().setSeconds(9).setNanos(10).build(); + container = Type.getDescriptor().getFile().getPackage(); + + source = "t1 < t2"; + runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1009))); + runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts0910))); + runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1010))); + runTest(Activation.of("t1", ts1009).extend(Activation.of("t2", ts1010))); + runTest(Activation.of("t1", ts0910).extend(Activation.of("t2", ts1010))); + + source = "t1 <= t2"; + runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1009))); + runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts0910))); + runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1010))); + runTest(Activation.of("t1", ts1009).extend(Activation.of("t2", ts1010))); + runTest(Activation.of("t1", ts0910).extend(Activation.of("t2", ts1010))); + + source = "t1 > t2"; + runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1009))); + runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts0910))); + runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1010))); + runTest(Activation.of("t1", ts1009).extend(Activation.of("t2", ts1010))); + runTest(Activation.of("t1", ts0910).extend(Activation.of("t2", ts1010))); + + source = "t1 >= t2"; + runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1009))); + runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts0910))); + runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1010))); + runTest(Activation.of("t1", ts1009).extend(Activation.of("t2", ts1010))); + runTest(Activation.of("t1", ts0910).extend(Activation.of("t2", ts1010))); + } + + @Test + public void nestedEnums() throws Exception { + TestAllTypes nestedEnum = TestAllTypes.newBuilder().setSingleNestedEnum(NestedEnum.BAR).build(); + declareVariable("x", CelProtoTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); + container = TestAllTypes.getDescriptor().getFile().getPackage(); + source = "x.single_nested_enum == TestAllTypes.NestedEnum.BAR"; + runTest(Activation.of("x", nestedEnum)); + + declareVariable("single_nested_enum", CelProtoTypes.INT64); + source = "single_nested_enum == TestAllTypes.NestedEnum.BAR"; + runTest(Activation.of("single_nested_enum", nestedEnum.getSingleNestedEnumValue())); + + source = + "TestAllTypes{single_nested_enum : TestAllTypes.NestedEnum.BAR}.single_nested_enum == 1"; + runTest(Activation.EMPTY); + } + + @Test + public void globalEnums() throws Exception { + declareVariable("x", CelProtoTypes.INT64); + source = "x == dev.cel.testing.testdata.proto3.StandaloneGlobalEnum.SGAR"; + runTest(Activation.of("x", StandaloneGlobalEnum.SGAR.getNumber())); + } + + @Test + public void lists() throws Exception { + declareVariable("x", CelProtoTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); + declareVariable("y", CelProtoTypes.INT64); + container = TestAllTypes.getDescriptor().getFile().getPackage(); + source = "([1, 2, 3] + x.repeated_int32)[3] == 4"; + runTest(Activation.of("x", TestAllTypes.newBuilder().addRepeatedInt32(4).build())); + + source = "!(y in [1, 2, 3]) && y in [4, 5, 6]"; + runTest(Activation.of("y", 4L)); + + source = "TestAllTypes{repeated_int32: [1,2]}.repeated_int32[1] == 2"; + runTest(Activation.EMPTY); + + source = "1 in TestAllTypes{repeated_int32: [1,2]}.repeated_int32"; + runTest(Activation.EMPTY); + + source = "!(4 in [1, 2, 3]) && 1 in [1, 2, 3]"; + runTest(Activation.EMPTY); + + declareVariable("list", CelProtoTypes.createList(CelProtoTypes.INT64)); + + source = "!(4 in list) && 1 in list"; + runTest(Activation.of("list", ImmutableList.of(1L, 2L, 3L))); + + source = "!(y in list)"; + runTest(Activation.copyOf(ImmutableMap.of("y", 4L, "list", ImmutableList.of(1L, 2L, 3L)))); + + source = "y in list"; + runTest(Activation.copyOf(ImmutableMap.of("y", 1L, "list", ImmutableList.of(1L, 2L, 3L)))); + + source = "list[3]"; + runTest(Activation.copyOf(ImmutableMap.of("y", 1L, "list", ImmutableList.of(1L, 2L, 3L)))); + } + + @Test + public void maps() throws Exception { + declareVariable("x", CelProtoTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); + container = TestAllTypes.getDescriptor().getFile().getPackage(); + source = "{1: 2, 3: 4}[3] == 4"; + runTest(Activation.EMPTY); + + // Constant key in constant map. + source = "3 in {1: 2, 3: 4} && !(4 in {1: 2, 3: 4})"; + runTest(Activation.EMPTY); + + source = "x.map_int32_int64[22] == 23"; + runTest(Activation.of("x", TestAllTypes.newBuilder().putMapInt32Int64(22, 23).build())); + + source = "TestAllTypes{map_int32_int64: {21: 22, 22: 23}}.map_int32_int64[22] == 23"; + runTest(Activation.EMPTY); + + source = + "TestAllTypes{oneof_type: NestedTestAllTypes{payload: x}}" + + ".oneof_type.payload.map_int32_int64[22] == 23"; + runTest(Activation.of("x", TestAllTypes.newBuilder().putMapInt32Int64(22, 23).build())); + + declareVariable("y", CelProtoTypes.INT64); + declareVariable("map", CelProtoTypes.createMap(CelProtoTypes.INT64, CelProtoTypes.INT64)); + + // Constant key in variable map. + source = "!(4 in map) && 1 in map"; + runTest(Activation.of("map", ImmutableMap.of(1L, 4L, 2L, 3L, 3L, 2L))); + + // Variable key in constant map. + source = "!(y in {1: 4, 2: 3, 3: 2}) && y in {5: 3, 4: 2, 3: 3}"; + runTest(Activation.of("y", 4L)); + + // Variable key in variable map. + source = "!(y in map) && (y + 3) in map"; + runTest( + Activation.copyOf( + ImmutableMap.of("y", 1L, "map", ImmutableMap.of(4L, 1L, 5L, 2L, 6L, 3L)))); + + // Message value in map + source = "TestAllTypes{map_int64_nested_type:{42:NestedTestAllTypes{payload:TestAllTypes{}}}}"; + runTest(Activation.EMPTY); + + // Repeated key - constant + source = "{true: 1, false: 2, true: 3}[true]"; + runTest(Activation.EMPTY); + + // Repeated key - expressions + declareVariable("b", CelProtoTypes.BOOL); + source = "{b: 1, !b: 2, b: 3}[true]"; + runTest(Activation.of("b", true)); + } + + @Test + public void comprehension() throws Exception { + source = "[0, 1, 2].map(x, x > 0, x + 1) == [2, 3]"; + runTest(Activation.EMPTY); + + source = "[0, 1, 2].exists(x, x > 0)"; + runTest(Activation.EMPTY); + + source = "[0, 1, 2].exists(x, x > 2)"; + runTest(Activation.EMPTY); + } + + @Test + public void abstractType() throws Exception { + Type typeParam = CelProtoTypes.createTypeParam("T"); + Type abstractType = + Type.newBuilder() + .setAbstractType( + AbstractType.newBuilder().setName("vector").addParameterTypes(typeParam)) + .build(); + // Declare a function to create a vector. + declareFunction( + "vector", + globalOverload( + "vector", + ImmutableList.of(CelProtoTypes.createList(typeParam)), + ImmutableList.of("T"), + abstractType)); + eval.registrar() + .add( + "vector", + ImmutableList.of(List.class), + (Object[] args) -> { + List list = (List) args[0]; + return list.toArray(new Object[0]); + }); + // Declare a function to access element of a vector. + declareFunction( + "at", + memberOverload( + "at", + ImmutableList.of(abstractType, CelProtoTypes.INT64), + ImmutableList.of("T"), + typeParam)); + eval.registrar() + .add( + "at", + ImmutableList.of(Object[].class, Long.class), + (Object[] args) -> { + Object[] array = (Object[]) args[0]; + return array[(int) (long) args[1]]; + }); + + source = "vector([1,2,3]).at(1) == 2"; + runTest(Activation.EMPTY); + + source = "vector([1,2,3]).at(1) + vector([7]).at(0)"; + runTest(Activation.EMPTY); + } + + @Test + public void namespacedFunctions() throws Exception { + declareFunction( + "ns.func", + globalOverload( + "ns_func_overload", ImmutableList.of(CelProtoTypes.STRING), CelProtoTypes.INT64)); + declareFunction( + "member", + memberOverload( + "ns_member_overload", + ImmutableList.of(CelProtoTypes.INT64, CelProtoTypes.INT64), + CelProtoTypes.INT64)); + eval.registrar().add("ns_func_overload", String.class, s -> (long) s.length()); + eval.registrar().add("ns_member_overload", Long.class, Long.class, Long::sum); + source = "ns.func('hello')"; + runTest(Activation.EMPTY); + + source = "ns.func('hello').member(ns.func('test'))"; + runTest(Activation.EMPTY); + + source = "{ns.func('test'): 2}"; + runTest(Activation.EMPTY); + + source = "{2: ns.func('test')}"; + runTest(Activation.EMPTY); + + source = "[ns.func('test'), 2]"; + runTest(Activation.EMPTY); + + source = "[ns.func('test')].map(x, x * 2)"; + runTest(Activation.EMPTY); + + source = "[1, 2].map(x, x * ns.func('test'))"; + runTest(Activation.EMPTY); + + container = "ns"; + // Call with the container set as the function's namespace + source = "ns.func('hello')"; + runTest(Activation.EMPTY); + + source = "func('hello')"; + runTest(Activation.EMPTY); + + source = "func('hello').member(func('test'))"; + runTest(Activation.EMPTY); + } + + @Test + public void namespacedVariables() throws Exception { + container = "ns"; + declareVariable("ns.x", CelProtoTypes.INT64); + source = "x"; + runTest(Activation.of("ns.x", 2)); + + container = "dev.cel.testing.testdata.proto3"; + Type messageType = CelProtoTypes.createMessage("cel.expr.conformance.proto3.TestAllTypes"); + declareVariable("dev.cel.testing.testdata.proto3.msgVar", messageType); + source = "msgVar.single_int32"; + runTest( + Activation.of( + "dev.cel.testing.testdata.proto3.msgVar", + TestAllTypes.newBuilder().setSingleInt32(5).build())); + } + + @Test + public void durationFunctions() throws Exception { + declareVariable("d1", CelProtoTypes.DURATION); + Duration d1 = + Duration.newBuilder().setSeconds(25 * 3600 + 59 * 60 + 1).setNanos(11000000).build(); + Duration d2 = + Duration.newBuilder().setSeconds(-(25 * 3600 + 59 * 60 + 1)).setNanos(-11000000).build(); + container = Type.getDescriptor().getFile().getPackage(); + + source = "d1.getHours()"; + runTest(Activation.of("d1", d1)); + runTest(Activation.of("d1", d2)); + + source = "d1.getMinutes()"; + runTest(Activation.of("d1", d1)); + runTest(Activation.of("d1", d2)); + + source = "d1.getSeconds()"; + runTest(Activation.of("d1", d1)); + runTest(Activation.of("d1", d2)); + + source = "d1.getMilliseconds()"; + runTest(Activation.of("d1", d1)); + runTest(Activation.of("d1", d2)); + + declareVariable("val", CelProtoTypes.INT64); + source = "d1.getHours() < val"; + runTest(Activation.of("d1", d1).extend(Activation.of("val", 30L))); + source = "d1.getMinutes() > val"; + runTest(Activation.of("d1", d1).extend(Activation.of("val", 30L))); + source = "d1.getSeconds() > val"; + runTest(Activation.of("d1", d1).extend(Activation.of("val", 30L))); + source = "d1.getMilliseconds() < val"; + runTest(Activation.of("d1", d1).extend(Activation.of("val", 30L))); + } + + @Test + public void timestampFunctions() throws Exception { + declareVariable("ts1", CelProtoTypes.TIMESTAMP); + container = Type.getDescriptor().getFile().getPackage(); + Timestamp ts1 = Timestamp.newBuilder().setSeconds(1).setNanos(11000000).build(); + Timestamp ts2 = Timestamps.fromSeconds(-1); + + source = "ts1.getFullYear(\"America/Los_Angeles\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getFullYear()"; + runTest(Activation.of("ts1", ts1)); + runTest(Activation.of("ts1", ts2)); + source = "ts1.getFullYear(\"Indian/Cocos\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getFullYear(\"2:00\")"; + runTest(Activation.of("ts1", ts1)); + + source = "ts1.getMonth(\"America/Los_Angeles\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getMonth()"; + runTest(Activation.of("ts1", ts1)); + runTest(Activation.of("ts1", ts2)); + source = "ts1.getMonth(\"Indian/Cocos\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getMonth(\"-8:15\")"; + runTest(Activation.of("ts1", ts1)); + + source = "ts1.getDayOfYear(\"America/Los_Angeles\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getDayOfYear()"; + runTest(Activation.of("ts1", ts1)); + runTest(Activation.of("ts1", ts2)); + source = "ts1.getDayOfYear(\"Indian/Cocos\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getDayOfYear(\"-9:00\")"; + runTest(Activation.of("ts1", ts1)); + + source = "ts1.getDayOfMonth(\"America/Los_Angeles\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getDayOfMonth()"; + runTest(Activation.of("ts1", ts1)); + runTest(Activation.of("ts1", ts2)); + source = "ts1.getDayOfMonth(\"Indian/Cocos\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getDayOfMonth(\"8:00\")"; + runTest(Activation.of("ts1", ts1)); + + source = "ts1.getDate(\"America/Los_Angeles\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getDate()"; + runTest(Activation.of("ts1", ts1)); + runTest(Activation.of("ts1", ts2)); + source = "ts1.getDate(\"Indian/Cocos\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getDate(\"9:30\")"; + runTest(Activation.of("ts1", ts1)); + + Timestamp tsSunday = Timestamps.fromSeconds(3 * 24 * 3600); + source = "ts1.getDayOfWeek(\"America/Los_Angeles\")"; + runTest(Activation.of("ts1", tsSunday)); + source = "ts1.getDayOfWeek()"; + runTest(Activation.of("ts1", tsSunday)); + runTest(Activation.of("ts1", ts2)); + source = "ts1.getDayOfWeek(\"Indian/Cocos\")"; + runTest(Activation.of("ts1", tsSunday)); + source = "ts1.getDayOfWeek(\"-9:30\")"; + runTest(Activation.of("ts1", tsSunday)); + + source = "ts1.getHours(\"America/Los_Angeles\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getHours()"; + runTest(Activation.of("ts1", ts1)); + runTest(Activation.of("ts1", ts2)); + source = "ts1.getHours(\"Indian/Cocos\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getHours(\"6:30\")"; + runTest(Activation.of("ts1", ts1)); + + source = "ts1.getMinutes(\"America/Los_Angeles\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getMinutes()"; + runTest(Activation.of("ts1", ts1)); + runTest(Activation.of("ts1", ts2)); + source = "ts1.getMinutes(\"Indian/Cocos\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getMinutes(\"-8:00\")"; + runTest(Activation.of("ts1", ts1)); + + source = "ts1.getSeconds(\"America/Los_Angeles\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getSeconds()"; + runTest(Activation.of("ts1", ts1)); + runTest(Activation.of("ts1", ts2)); + source = "ts1.getSeconds(\"Indian/Cocos\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getSeconds(\"-8:00\")"; + runTest(Activation.of("ts1", ts1)); + + source = "ts1.getMilliseconds(\"America/Los_Angeles\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getMilliseconds()"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getMilliseconds(\"Indian/Cocos\")"; + runTest(Activation.of("ts1", ts1)); + source = "ts1.getMilliseconds(\"-8:00\")"; + runTest(Activation.of("ts1", ts1)); + + declareVariable("val", CelProtoTypes.INT64); + source = "ts1.getFullYear() < val"; + runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 2013L))); + source = "ts1.getMonth() < val"; + runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 12L))); + source = "ts1.getDayOfYear() < val"; + runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 13L))); + source = "ts1.getDayOfMonth() < val"; + runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 10L))); + source = "ts1.getDate() < val"; + runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); + source = "ts1.getDayOfWeek() < val"; + runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); + source = "ts1.getHours() < val"; + runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); + source = "ts1.getMinutes() < val"; + runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); + source = "ts1.getSeconds() < val"; + runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); + source = "ts1.getMilliseconds() < val"; + runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); + } + + @Test + public void timeConversions() throws Exception { + container = Type.getDescriptor().getFile().getPackage(); + declareVariable("t1", CelProtoTypes.TIMESTAMP); + + source = "timestamp(\"1972-01-01T10:00:20.021-05:00\")"; + runTest(Activation.EMPTY); + + source = "timestamp(123)"; + runTest(Activation.EMPTY); + + source = "duration(\"15.11s\")"; + runTest(Activation.EMPTY); + + source = "int(t1) == 100"; + runTest(Activation.of("t1", Timestamps.fromSeconds(100))); + + source = "duration(\"1h2m3.4s\")"; + runTest(Activation.EMPTY); + + // Not supported. + source = "duration('inf')"; + runTest(Activation.EMPTY); + + source = "duration(duration('15.0s'))"; // Identity + runTest(Activation.EMPTY); + + source = "timestamp(timestamp(123))"; // Identity + runTest(Activation.EMPTY); + } + + @Test + public void sizeTests() throws Exception { + container = Type.getDescriptor().getFile().getPackage(); + declareVariable("str", CelProtoTypes.STRING); + declareVariable("b", CelProtoTypes.BYTES); + + source = "size(b) == 5 && b.size() == 5"; + runTest(Activation.of("b", ByteString.copyFromUtf8("happy"))); + + source = "size(str) == 5 && str.size() == 5"; + runTest(Activation.of("str", "happy")); + runTest(Activation.of("str", "happ\uDBFF\uDFFC")); + + source = "size({1:14, 2:15}) == 2 && {1:14, 2:15}.size() == 2"; + runTest(Activation.EMPTY); + + source = "size([1,2,3]) == 3 && [1,2,3].size() == 3"; + runTest(Activation.EMPTY); + } + + @Test + public void nonstrictQuantifierTests() throws Exception { + // Plain tests. Everything is constant. + source = "[0, 2, 4].exists(x, 4/x == 2 && 4/(4-x) == 2)"; + runTest(Activation.EMPTY); + + source = "![0, 2, 4].all(x, 4/x != 2 && 4/(4-x) != 2)"; + runTest(Activation.EMPTY); + + declareVariable("four", CelProtoTypes.INT64); + + // Condition is dynamic. + source = "[0, 2, 4].exists(x, four/x == 2 && four/(four-x) == 2)"; + runTest(Activation.of("four", 4L)); + + source = "![0, 2, 4].all(x, four/x != 2 && four/(four-x) != 2)"; + runTest(Activation.of("four", 4L)); + + // Both range and condition are dynamic. + source = "[0, 2, four].exists(x, four/x == 2 && four/(four-x) == 2)"; + runTest(Activation.of("four", 4L)); + + source = "![0, 2, four].all(x, four/x != 2 && four/(four-x) != 2)"; + runTest(Activation.of("four", 4L)); + } + + @Test + public void regexpMatchingTests() throws Exception { + // Constant everything. + source = "matches(\"alpha\", \"^al.*\") == true"; + runTest(Activation.EMPTY); + + source = "matches(\"alpha\", \"^.al.*\") == false"; + runTest(Activation.EMPTY); + + source = "matches(\"alpha\", \".*ha$\") == true"; + runTest(Activation.EMPTY); + + source = "matches(\"alpha\", \"^.*ha.$\") == false"; + runTest(Activation.EMPTY); + + source = "matches(\"alpha\", \"\") == true"; + runTest(Activation.EMPTY); + + source = "matches(\"alpha\", \"ph\") == true"; + runTest(Activation.EMPTY); + + source = "matches(\"alpha\", \"^ph\") == false"; + runTest(Activation.EMPTY); + + source = "matches(\"alpha\", \"ph$\") == false"; + runTest(Activation.EMPTY); + + // Constant everything, receiver-style. + source = "\"alpha\".matches(\"^al.*\") == true"; + runTest(Activation.EMPTY); + + source = "\"alpha\".matches(\"^.al.*\") == false"; + runTest(Activation.EMPTY); + + source = "\"alpha\".matches(\".*ha$\") == true"; + runTest(Activation.EMPTY); + + source = "\"alpha\".matches(\".*ha.$\") == false"; + runTest(Activation.EMPTY); + + source = "\"alpha\".matches(\"\") == true"; + runTest(Activation.EMPTY); + + source = "\"alpha\".matches(\"ph\") == true"; + runTest(Activation.EMPTY); + + source = "\"alpha\".matches(\"^ph\") == false"; + runTest(Activation.EMPTY); + + source = "\"alpha\".matches(\"ph$\") == false"; + runTest(Activation.EMPTY); + + // Constant string. + declareVariable("regexp", CelProtoTypes.STRING); + + source = "matches(\"alpha\", regexp) == true"; + runTest(Activation.of("regexp", "^al.*")); + + source = "matches(\"alpha\", regexp) == false"; + runTest(Activation.of("regexp", "^.al.*")); + + source = "matches(\"alpha\", regexp) == true"; + runTest(Activation.of("regexp", ".*ha$")); + + source = "matches(\"alpha\", regexp) == false"; + runTest(Activation.of("regexp", ".*ha.$")); + + // Constant string, receiver-style. + source = "\"alpha\".matches(regexp) == true"; + runTest(Activation.of("regexp", "^al.*")); + + source = "\"alpha\".matches(regexp) == false"; + runTest(Activation.of("regexp", "^.al.*")); + + source = "\"alpha\".matches(regexp) == true"; + runTest(Activation.of("regexp", ".*ha$")); + + source = "\"alpha\".matches(regexp) == false"; + runTest(Activation.of("regexp", ".*ha.$")); + + // Constant regexp. + declareVariable("s", CelProtoTypes.STRING); + + source = "matches(s, \"^al.*\") == true"; + runTest(Activation.of("s", "alpha")); + + source = "matches(s, \"^.al.*\") == false"; + runTest(Activation.of("s", "alpha")); + + source = "matches(s, \".*ha$\") == true"; + runTest(Activation.of("s", "alpha")); + + source = "matches(s, \"^.*ha.$\") == false"; + runTest(Activation.of("s", "alpha")); + + // Constant regexp, receiver-style. + source = "s.matches(\"^al.*\") == true"; + runTest(Activation.of("s", "alpha")); + + source = "s.matches(\"^.al.*\") == false"; + runTest(Activation.of("s", "alpha")); + + source = "s.matches(\".*ha$\") == true"; + runTest(Activation.of("s", "alpha")); + + source = "s.matches(\"^.*ha.$\") == false"; + runTest(Activation.of("s", "alpha")); + + // No constants. + source = "matches(s, regexp) == true"; + runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", "^al.*"))); + runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", ".*ha$"))); + + source = "matches(s, regexp) == false"; + runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", "^.al.*"))); + runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", ".*ha.$"))); + + // No constants, receiver-style. + source = "s.matches(regexp) == true"; + runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", "^al.*"))); + runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", ".*ha$"))); + + source = "s.matches(regexp) == false"; + runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", "^.al.*"))); + runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", ".*ha.$"))); + } + + @Test + public void int64Conversions() throws Exception { + source = "int('-1')"; // string converts to -1 + runTest(Activation.EMPTY); + + source = "int(2.1)"; // double converts to 2 + runTest(Activation.EMPTY); + + source = "int(18446744073709551615u)"; // 2^64-1 should error + runTest(Activation.EMPTY); + + source = "int(1e99)"; // out of range should error + runTest(Activation.EMPTY); + + source = "int(42u)"; // converts to 42 + runTest(Activation.EMPTY); + } + + @Test + public void uint64Conversions() throws Exception { + // The test case `uint(1e19)` succeeds with unsigned longs and fails with longs in a way that + // cannot be easily tested. + if (!eval.celOptions().enableUnsignedLongs()) { + skipBaselineVerification(); + return; + } + source = "uint('1')"; // string converts to 1u + runTest(Activation.EMPTY); + + source = "uint(2.1)"; // double converts to 2u + runTest(Activation.EMPTY); + + source = "uint(-1)"; // should error + runTest(Activation.EMPTY); + + source = "uint(1e19)"; // valid uint but outside of int range + runTest(Activation.EMPTY); + + source = "uint(6.022e23)"; // outside uint range + runTest(Activation.EMPTY); + + source = "uint(42)"; // int converts to 42u + runTest(Activation.EMPTY); + + source = "uint('f1')"; // should error + runTest(Activation.EMPTY); + + source = "uint(1u)"; // identity + runTest(Activation.EMPTY); + + source = "uint(dyn(1u))"; // identity, check dynamic dispatch + runTest(Activation.EMPTY); + } + + @Test + public void doubleConversions() throws Exception { + source = "double('1.1')"; // string converts to 1.1 + runTest(Activation.EMPTY); + + source = "double(2u)"; // uint converts to 2.0 + runTest(Activation.EMPTY); + + source = "double(-1)"; // int converts to -1.0 + runTest(Activation.EMPTY); + + source = "double('bad')"; + runTest(Activation.EMPTY); + + source = "double(1.5)"; // Identity + runTest(Activation.EMPTY); + } + + @Test + public void stringConversions() throws Exception { + source = "string(1.1)"; // double converts to '1.1' + runTest(Activation.EMPTY); + + source = "string(2u)"; // uint converts to '2' + runTest(Activation.EMPTY); + + source = "string(-1)"; // int converts to '-1' + runTest(Activation.EMPTY); + + // Byte literals in Google SQL only take the leading byte of an escape character. + // This means that to translate a byte literal to a UTF-8 encoded string, all bytes must be + // encoded in the literal as they would be laid out in memory for UTF-8, hence the extra octal + // escape to achieve parity with the bidi test below. + source = "string(b'abc\\303\\203')"; + runTest(Activation.EMPTY); // bytes convert to 'abcÃ' + + // Bi-di conversion for strings and bytes for 'abcÃ', note the difference between the string + // and byte literal values. + source = "string(bytes('abc\\303'))"; + runTest(Activation.EMPTY); + + source = "string(timestamp('2009-02-13T23:31:30Z'))"; + runTest(Activation.EMPTY); + + source = "string(duration('1000000s'))"; + runTest(Activation.EMPTY); + + source = "string('hello')"; // Identity + runTest(Activation.EMPTY); + } + + @Test + public void bytes() throws Exception { + source = + "b'a' < b'b' && b'a' <= b'b' && b'b' > b'a' && b'a' >= b'a' && b'a' == b'a' && b'a' !=" + + " b'b'"; + runTest(Activation.EMPTY); + } + + @Test + public void boolConversions() throws Exception { + source = "bool(true)"; + runTest(Activation.EMPTY); // Identity + + source = "bool('true') && bool('TRUE') && bool('True') && bool('t') && bool('1')"; + runTest(Activation.EMPTY); // result is true + + source = "bool('false') || bool('FALSE') || bool('False') || bool('f') || bool('0')"; + runTest(Activation.EMPTY); // result is false + + source = "bool('TrUe')"; + runTest(Activation.EMPTY); // exception + + source = "bool('FaLsE')"; + runTest(Activation.EMPTY); // exception + } + + @Test + public void bytesConversions() throws Exception { + source = "bytes('abc\\303')"; + runTest(Activation.EMPTY); // string converts to abcà in bytes form. + + source = "bytes(bytes('abc\\303'))"; // Identity + runTest(Activation.EMPTY); + } + + @Test + public void dynConversions() throws Exception { + source = "dyn(42)"; + runTest(Activation.EMPTY); + + source = "dyn({'a':1, 'b':2})"; + runTest(Activation.EMPTY); + } + + // This lambda implements @Immutable interface 'Function', but 'InterpreterTest' has field 'eval' + // of type 'com.google.api.expr.cel.testing.Eval', the declaration of + // type + // 'com.google.api.expr.cel.testing.Eval' is not annotated with + // @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + @Test + public void jsonValueTypes() throws Exception { + container = TestAllTypes.getDescriptor().getFile().getPackage(); + declareVariable("x", CelProtoTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); + + // JSON bool selection. + TestAllTypes xBool = + TestAllTypes.newBuilder().setSingleValue(Value.newBuilder().setBoolValue(true)).build(); + source = "x.single_value"; + runTest(Activation.of("x", xBool)); + + // JSON number selection with int comparison. + TestAllTypes xInt = + TestAllTypes.newBuilder().setSingleValue(Value.newBuilder().setNumberValue(1)).build(); + source = "x.single_value == double(1)"; + runTest(Activation.of("x", xInt)); + + // JSON number selection with float comparison. + TestAllTypes xFloat = + TestAllTypes.newBuilder().setSingleValue(Value.newBuilder().setNumberValue(1.1)).build(); + source = "x.single_value == 1.1"; + runTest(Activation.of("x", xFloat)); + + // JSON null selection. + TestAllTypes xNull = + TestAllTypes.newBuilder() + .setSingleValue(Value.newBuilder().setNullValue(NullValue.NULL_VALUE)) + .build(); + source = "x.single_value == null"; + runTest(Activation.of("x", xNull)); + + // JSON string selection. + TestAllTypes xString = + TestAllTypes.newBuilder() + .setSingleValue(Value.newBuilder().setStringValue("hello")) + .build(); + source = "x.single_value == 'hello'"; + runTest(Activation.of("x", xString)); + + // JSON list equality. + TestAllTypes xList = + TestAllTypes.newBuilder() + .setSingleValue( + Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues( + Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues(Value.newBuilder().setStringValue("hello")))) + .addValues(Value.newBuilder().setNumberValue(-1.1)))) + .build(); + source = "x.single_value[0] == [['hello'], -1.1][0]"; + runTest(Activation.of("x", xList)); + + // JSON struct equality. + TestAllTypes xStruct = + TestAllTypes.newBuilder() + .setSingleStruct( + Struct.newBuilder() + .putFields( + "str", + Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addValues(Value.newBuilder().setStringValue("hello"))) + .build()) + .putFields("num", Value.newBuilder().setNumberValue(-1.1).build())) + .build(); + source = "x.single_struct.num == {'str': ['hello'], 'num': -1.1}['num']"; + runTest(Activation.of("x", xStruct)); + + // Build a proto message using a dynamically constructed map and assign the map to a struct + // value. + source = + "TestAllTypes{" + + "single_struct: " + + "TestAllTypes{single_value: {'str': ['hello']}}.single_value" + + "}"; + runTest(Activation.EMPTY); + + // Ensure that types are being wrapped and unwrapped on function dispatch. + declareFunction( + "pair", + globalOverload( + "pair", + ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.STRING), + CelProtoTypes.DYN)); + eval.registrar() + .add( + "pair", + ImmutableList.of(String.class, String.class), + (Object[] args) -> { + String key = (String) args[0]; + String val = (String) args[1]; + return eval.adapt( + Value.newBuilder() + .setStructValue( + Struct.newBuilder() + .putFields(key, Value.newBuilder().setStringValue(val).build())) + .build()); + }); + source = "pair(x.single_struct.str[0], 'val')"; + runTest(Activation.of("x", xStruct)); + } + + @Test + public void typeComparisons() throws Exception { + container = TestAllTypes.getDescriptor().getFile().getPackage(); + + // Test numeric types. + source = + "type(1) == int && type(1u) == uint && " + + "type(1u) != int && type(1) != uint && " + + "type(uint(1.1)) == uint && " + + "type(1.1) == double"; + runTest(Activation.EMPTY); + + // Test string and bytes types. + source = "type('hello') == string && type(b'\277') == bytes"; + runTest(Activation.EMPTY); + + // Test list and map types. + source = "type([1, 2, 3]) == list && type({'a': 1, 'b': 2}) == map"; + runTest(Activation.EMPTY); + + // Test bool types. + source = "type(true) == bool && type(false) == bool"; + runTest(Activation.EMPTY); + + // Test well-known proto-based types. + source = "type(duration('10s')) == google.protobuf.Duration"; + runTest(Activation.EMPTY); + + // Test external proto-based types with container resolution. + source = + "type(TestAllTypes{}) == TestAllTypes && " + + "type(TestAllTypes{}) == proto3.TestAllTypes && " + + "type(TestAllTypes{}) == .cel.expr.conformance.proto3.TestAllTypes && " + + "type(proto3.TestAllTypes{}) == TestAllTypes && " + + "type(proto3.TestAllTypes{}) == proto3.TestAllTypes && " + + "type(proto3.TestAllTypes{}) == .cel.expr.conformance.proto3.TestAllTypes && " + + "type(.cel.expr.conformance.proto3.TestAllTypes{}) == TestAllTypes && " + + "type(.cel.expr.conformance.proto3.TestAllTypes{}) == proto3.TestAllTypes && " + + "type(.cel.expr.conformance.proto3.TestAllTypes{}) == " + + ".cel.expr.conformance.proto3.TestAllTypes"; + runTest(Activation.EMPTY); + + // Test whether a type name is recognized as a type. + source = "type(TestAllTypes) == type"; + runTest(Activation.EMPTY); + + // Test whether the type resolution of a proto object is recognized as the message's type. + source = "type(TestAllTypes{}) == TestAllTypes"; + runTest(Activation.EMPTY); + + // Test whether null resolves to null_type. + source = "type(null) == null_type"; + runTest(Activation.EMPTY); + } + + @Test + public void wrappers() throws Exception { + TestAllTypes.Builder wrapperBindings = + TestAllTypes.newBuilder() + .setSingleBoolWrapper(BoolValue.of(true)) + .setSingleBytesWrapper(BytesValue.of(ByteString.copyFrom(new byte[] {'h', 'i'}))) + .setSingleDoubleWrapper(DoubleValue.of(-3.0)) + .setSingleFloatWrapper(FloatValue.of(1.5f)) + .setSingleInt32Wrapper(Int32Value.of(-12)) + .setSingleInt64Wrapper(Int64Value.of(-34)) + .setSingleStringWrapper(StringValue.of("hello")) + .setSingleUint32Wrapper(UInt32Value.of(12)) + .setSingleUint64Wrapper(UInt64Value.of(34)); + + declareVariable("x", CelProtoTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); + source = + "x.single_bool_wrapper == true && " + + "x.single_bytes_wrapper == b'hi' && " + + "x.single_double_wrapper == -3.0 && " + + "x.single_float_wrapper == 1.5 && " + + "x.single_int32_wrapper == -12 && " + + "x.single_int64_wrapper == -34 && " + + "x.single_string_wrapper == 'hello' && " + + "x.single_uint32_wrapper == 12u && " + + "x.single_uint64_wrapper == 34u"; + runTest(Activation.of("x", wrapperBindings)); + + source = + "x.single_bool_wrapper == google.protobuf.BoolValue{} && " + + "x.single_bytes_wrapper == google.protobuf.BytesValue{value: b'hi'} && " + + "x.single_double_wrapper == google.protobuf.DoubleValue{value: -3.0} && " + + "x.single_float_wrapper == google.protobuf.FloatValue{value: 1.5} && " + + "x.single_int32_wrapper == google.protobuf.Int32Value{value: -12} && " + + "x.single_int64_wrapper == google.protobuf.Int64Value{value: -34} && " + + "x.single_string_wrapper == google.protobuf.StringValue{} && " + + "x.single_uint32_wrapper == google.protobuf.UInt32Value{value: 12u} && " + + "x.single_uint64_wrapper == google.protobuf.UInt64Value{value: 34u}"; + runTest( + Activation.of( + "x", + wrapperBindings + .setSingleBoolWrapper(BoolValue.getDefaultInstance()) + .setSingleStringWrapper(StringValue.getDefaultInstance()))); + + source = + "x.single_bool_wrapper == null && " + + "x.single_bytes_wrapper == null && " + + "x.single_double_wrapper == null && " + + "x.single_float_wrapper == null && " + + "x.single_int32_wrapper == null && " + + "x.single_int64_wrapper == null && " + + "x.single_string_wrapper == null && " + + "x.single_uint32_wrapper == null && " + + "x.single_uint64_wrapper == null"; + runTest(Activation.of("x", TestAllTypes.getDefaultInstance())); + } + + @Test + public void longComprehension() throws Exception { + ImmutableList l = LongStream.range(0L, 1000L).boxed().collect(toImmutableList()); + eval.registrar().add("constantLongList", ImmutableList.of(), args -> l); + + // Comprehension over compile-time constant long list. + declareFunction( + "constantLongList", + globalOverload( + "constantLongList", ImmutableList.of(), CelProtoTypes.createList(CelProtoTypes.INT64))); + source = "size(constantLongList().map(x, x+1)) == 1000"; + runTest(Activation.EMPTY); + + // Comprehension over long list that is not compile-time constant. + declareVariable("longlist", CelProtoTypes.createList(CelProtoTypes.INT64)); + source = "size(longlist.map(x, x+1)) == 1000"; + runTest(Activation.of("longlist", l)); + + // Comprehension over long list where the computation is very slow. + // (This is here pro-forma only since in the synchronous interpreter there + // is no notion of a computation being slow so that another computation can + // build up a stack while waiting.) + eval.registrar().add("f_slow_inc", Long.class, n -> n + 1L); + eval.registrar().add("f_unleash", Object.class, x -> x); + declareFunction( + "f_slow_inc", + globalOverload("f_slow_inc", ImmutableList.of(CelProtoTypes.INT64), CelProtoTypes.INT64)); + declareFunction( + "f_unleash", + globalOverload( + "f_unleash", + ImmutableList.of(CelProtoTypes.createTypeParam("A")), + ImmutableList.of("A"), + CelProtoTypes.createTypeParam("A"))); + source = "f_unleash(longlist.map(x, f_slow_inc(x)))[0] == 1"; + runTest(Activation.of("longlist", l)); + } + + @Test + public void maxComprehension() throws Exception { + if (eval.celOptions().comprehensionMaxIterations() < 0) { + skipBaselineVerification(); + return; + } + // Comprehension over long list that is not compile-time constant. + declareVariable("longlist", CelProtoTypes.createList(CelProtoTypes.INT64)); + source = "size(longlist.map(x, x+1)) == 1000"; + + // Comprehension which exceeds the configured iteration limit. + ImmutableList tooLongList = + LongStream.range(0L, COMPREHENSION_MAX_ITERATIONS + 1).boxed().collect(toImmutableList()); + runTest(Activation.of("longlist", tooLongList)); + + // Sequential iterations within the collective limit of 1000. + source = "longlist.filter(i, i % 2 == 0).map(i, i * 2).map(i, i / 2).size() == 250"; + ImmutableList l = + LongStream.range(0L, COMPREHENSION_MAX_ITERATIONS / 2).boxed().collect(toImmutableList()); + runTest(Activation.of("longlist", l)); + + // Sequential iterations outside the limit of 1000. + source = "(longlist + [0]).filter(i, i % 2 == 0).map(i, i * 2).map(i, i / 2).size() == 251"; + runTest(Activation.of("longlist", l)); + + // Nested iteration within the iteration limit. + // Note, there is some double-counting of the inner-loops which causes the iteration limit to + // get tripped sooner than one might expect for the nested case. + source = "longlist.map(i, longlist.map(j, longlist.map(k, [i, j, k]))).size() == 9"; + l = LongStream.range(0L, 9).boxed().collect(toImmutableList()); + runTest(Activation.of("longlist", l)); + + // Nested iteration which exceeds the iteration limit. This result may be surprising, but the + // limit is tripped precisely because each complete iteration of an inner-loop counts as inner- + // loop + 1 as there's not a clean way to deduct an iteration and only count the inner most + // loop. + l = LongStream.range(0L, 10).boxed().collect(toImmutableList()); + runTest(Activation.of("longlist", l)); + } + + @Test + public void dynamicMessage_adapted() throws Exception { + TestAllTypes wrapperBindings = + TestAllTypes.newBuilder() + .setSingleAny(Any.pack(NestedMessage.newBuilder().setBb(42).build())) + .setSingleBoolWrapper(BoolValue.of(true)) + .setSingleBytesWrapper(BytesValue.of(ByteString.copyFrom(new byte[] {'h', 'i'}))) + .setSingleDoubleWrapper(DoubleValue.of(-3.0)) + .setSingleFloatWrapper(FloatValue.of(1.5f)) + .setSingleInt32Wrapper(Int32Value.of(-12)) + .setSingleInt64Wrapper(Int64Value.of(-34)) + .setSingleStringWrapper(StringValue.of("hello")) + .setSingleUint32Wrapper(UInt32Value.of(12)) + .setSingleUint64Wrapper(UInt64Value.of(34)) + .setSingleDuration(Duration.newBuilder().setSeconds(10).setNanos(20)) + .setSingleTimestamp(Timestamp.newBuilder().setSeconds(100).setNanos(200)) + .setSingleValue(Value.newBuilder().setStringValue("a")) + .setSingleStruct( + Struct.newBuilder().putFields("b", Value.newBuilder().setStringValue("c").build())) + .setListValue( + ListValue.newBuilder().addValues(Value.newBuilder().setStringValue("d")).build()) + .build(); + + Activation activation = + Activation.of( + "msg", + DynamicMessage.parseFrom( + TestAllTypes.getDescriptor(), + wrapperBindings.toByteArray(), + DefaultDescriptorPool.INSTANCE.getExtensionRegistry())); + + declareVariable("msg", CelProtoTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); + + source = "msg.single_any"; + assertThat(runTest(activation)).isInstanceOf(NestedMessage.class); + + source = "msg.single_bool_wrapper"; + assertThat(runTest(activation)).isInstanceOf(Boolean.class); + + source = "msg.single_bytes_wrapper"; + assertThat(runTest(activation)).isInstanceOf(String.class); + + source = "msg.single_double_wrapper"; + assertThat(runTest(activation)).isInstanceOf(Double.class); + + source = "msg.single_float_wrapper"; + assertThat(runTest(activation)).isInstanceOf(Double.class); + + source = "msg.single_int32_wrapper"; + assertThat(runTest(activation)).isInstanceOf(Long.class); + + source = "msg.single_int64_wrapper"; + assertThat(runTest(activation)).isInstanceOf(Long.class); + + source = "msg.single_string_wrapper"; + assertThat(runTest(activation)).isInstanceOf(String.class); + + source = "msg.single_uint32_wrapper"; + assertThat(runTest(activation)) + .isInstanceOf(eval.celOptions().enableUnsignedLongs() ? UnsignedLong.class : Long.class); + + source = "msg.single_uint64_wrapper"; + assertThat(runTest(activation)) + .isInstanceOf(eval.celOptions().enableUnsignedLongs() ? UnsignedLong.class : Long.class); + + source = "msg.single_duration"; + assertThat(runTest(activation)).isInstanceOf(Duration.class); + + source = "msg.single_timestamp"; + assertThat(runTest(activation)).isInstanceOf(Timestamp.class); + + source = "msg.single_value"; + assertThat(runTest(activation)).isInstanceOf(String.class); + + source = "msg.single_struct"; + assertThat(runTest(activation)).isInstanceOf(Map.class); + + source = "msg.list_value"; + assertThat(runTest(activation)).isInstanceOf(List.class); + } + + private static String readResourceContent(String path) throws IOException { + return Resources.toString(Resources.getResource(Ascii.toLowerCase(path)), UTF_8); + } +} diff --git a/legacy/javatests/dev/cel/legacy/runtime/async/DynamicEnvTest.java b/legacy/javatests/dev/cel/legacy/runtime/async/DynamicEnvTest.java new file mode 100644 index 000000000..340684b5e --- /dev/null +++ b/legacy/javatests/dev/cel/legacy/runtime/async/DynamicEnvTest.java @@ -0,0 +1,56 @@ +package dev.cel.legacy.runtime.async; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.Futures.immediateFuture; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.FluentFuture; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class DynamicEnvTest { + @Test + public void withGlobalContext_preservesStack() throws Exception { + DynamicEnv base = + new DynamicEnv( + GlobalContext.of( + DummyAsyncContext.INSTANCE, + name -> () -> immediateFuture("original resolver: " + name)), + ImmutableList.of(immediateFuture("local1"), immediateFuture("local2")), + ImmutableList.of("global1", "global2")); + DynamicEnv extended = base.extend(FluentFuture.from(immediateFuture("local3"))); + DynamicEnv cloned = + extended.withGlobalContext( + GlobalContext.of( + DummyAsyncContext.INSTANCE, + name -> () -> immediateFuture("replaced resolver: " + name))); + + // base has original global bindings + assertThat(base.getGlobal(0).get()).isEqualTo("original resolver: global1"); + assertThat(base.getGlobal(1).get()).isEqualTo("original resolver: global2"); + + // extended also has original global bidings + assertThat(extended.getGlobal(0).get()).isEqualTo("original resolver: global1"); + assertThat(extended.getGlobal(1).get()).isEqualTo("original resolver: global2"); + + // cloned has replaced global bindings + assertThat(cloned.getGlobal(0).get()).isEqualTo("replaced resolver: global1"); + assertThat(cloned.getGlobal(1).get()).isEqualTo("replaced resolver: global2"); + + // base has two locals (slot offsets work in opposite direction!) + assertThat(base.getLocalAtSlotOffset(1).get()).isEqualTo("local2"); + assertThat(base.getLocalAtSlotOffset(2).get()).isEqualTo("local1"); + + // extended has one more local, original bindings are shifted + assertThat(extended.getLocalAtSlotOffset(1).get()).isEqualTo("local3"); + assertThat(extended.getLocalAtSlotOffset(2).get()).isEqualTo("local2"); + assertThat(extended.getLocalAtSlotOffset(3).get()).isEqualTo("local1"); + + // cloned as same locals as extended (from which it was created) + assertThat(cloned.getLocalAtSlotOffset(1).get()).isEqualTo("local3"); + assertThat(cloned.getLocalAtSlotOffset(2).get()).isEqualTo("local2"); + assertThat(cloned.getLocalAtSlotOffset(3).get()).isEqualTo("local1"); + } +} diff --git a/legacy/javatests/dev/cel/legacy/runtime/async/Eval.java b/legacy/javatests/dev/cel/legacy/runtime/async/Eval.java new file mode 100644 index 000000000..ab67f5176 --- /dev/null +++ b/legacy/javatests/dev/cel/legacy/runtime/async/Eval.java @@ -0,0 +1,44 @@ +// Copyright 2022 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.legacy.runtime.async; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CheckReturnValue; +import com.google.protobuf.Descriptors.FileDescriptor; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelOptions; +import dev.cel.runtime.Activation; +import dev.cel.runtime.InterpreterException; +import dev.cel.runtime.Registrar; + +/** + * The {@code Eval} interface is used to model the core concerns of CEL evaluation during testing. + */ +@CheckReturnValue +public interface Eval { + /** Returns the set of file descriptors configured for evaluation. */ + ImmutableList fileDescriptors(); + + /** Returns the function / type registrar used during evaluation. */ + Registrar registrar(); + + CelOptions celOptions(); + + /** Adapts a Java POJO to a CEL value. */ + Object adapt(Object value) throws InterpreterException; + + /** Evaluates an {@code ast} against a set of inputs represented by the {@code Activation}. */ + Object eval(CelAbstractSyntaxTree ast, Activation activation) throws Exception; +} diff --git a/legacy/javatests/dev/cel/legacy/runtime/async/EvalAsync.java b/legacy/javatests/dev/cel/legacy/runtime/async/EvalAsync.java new file mode 100644 index 000000000..982460ccd --- /dev/null +++ b/legacy/javatests/dev/cel/legacy/runtime/async/EvalAsync.java @@ -0,0 +1,168 @@ +package dev.cel.legacy.runtime.async; + +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + +import com.google.common.collect.ImmutableList; +import com.google.common.context.Context; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.contrib.descriptor.pool.MutableDescriptorPool; +import com.google.security.context.testing.FakeUnvalidatedSecurityContextBuilder; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelOptions; +import dev.cel.common.CelProtoAbstractSyntaxTree; +import dev.cel.legacy.runtime.async.TypeDirectedMessageProcessor.MessageInfo; +import dev.cel.runtime.Activation; +import dev.cel.runtime.InterpreterException; +import dev.cel.runtime.LinkedMessageFactory; +import dev.cel.runtime.MessageFactory; +import dev.cel.runtime.MessageProvider; +import dev.cel.runtime.Registrar; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +/** + * The {@code EvalAsync} class implements the {@code Eval} interface and exposes some additional + * methods for async evaluation testing. + */ +public class EvalAsync implements Eval { + + private final ImmutableList fileDescriptors; + private final MutableDescriptorPool pool = new MutableDescriptorPool(); + private final AsyncDispatcher dispatcher; + private final MessageProvider typeProvider; + private final MessageProcessor messageProcessor; + private final AsyncInterpreter asyncInterpreter; + private final CelOptions celOptions; + + public EvalAsync(ImmutableList fileDescriptors, CelOptions celOptions) { + this(fileDescriptors, celOptions, /* typeDirectedProcessor= */ false); + } + + public EvalAsync( + ImmutableList fileDescriptors, + CelOptions celOptions, + boolean typeDirectedProcessor) { + this( + fileDescriptors, + celOptions, + typeDirectedProcessor, + DefaultAsyncDispatcher.create(celOptions).fork()); + } + + private EvalAsync( + ImmutableList fileDescriptors, + CelOptions celOptions, + boolean typeDirectedProcessor, + AsyncDispatcher asyncDispatcher) { + this.dispatcher = asyncDispatcher; + this.celOptions = celOptions; + this.fileDescriptors = fileDescriptors; + this.fileDescriptors.forEach(pool::populateFromFileDescriptor); + this.typeProvider = LinkedMessageFactory.typeProvider(celOptions); + MessageFactory typeFactory = LinkedMessageFactory.typeFactory(); + + if (typeDirectedProcessor) { + this.messageProcessor = + new TypeDirectedMessageProcessor( + typeName -> + Optional.ofNullable(pool.getDescriptorForTypeName(typeName)) + .map( + descriptor -> + MessageInfo.of(descriptor, () -> typeFactory.newBuilder(typeName))), + extName -> + Optional.ofNullable( + ExtensionRegistry.getGeneratedRegistry().findExtensionByName(extName)), + celOptions); + } else { + this.messageProcessor = + new MessageProcessorAdapter( + typeName -> Optional.ofNullable(pool.getDescriptorForTypeName(typeName)), + typeProvider); + } + this.asyncInterpreter = + new FuturesInterpreter( + StandardTypeResolver.getInstance(celOptions), + this.messageProcessor, + this.dispatcher, + celOptions); + } + + @Override + public ImmutableList fileDescriptors() { + return fileDescriptors; + } + + @Override + public Registrar registrar() { + return dispatcher; + } + + @Override + public CelOptions celOptions() { + return celOptions; + } + + @Override + public Object adapt(Object value) throws InterpreterException { + return typeProvider.adapt(value); + } + + @Override + public Object eval(CelAbstractSyntaxTree ast, Activation activation) throws Exception { + Context requestContext = + Context.newBuilder(Context.getCurrentContext()) + // Make security context different from BACKGROUND_SECURITY_CONTEXT. + .replaceSecurityContext( + FakeUnvalidatedSecurityContextBuilder.withPeer("testpeer").buildUnvalidated()) + .build(); + return forceExpressionFuture( + asyncInterpreter + .createInterpretable(CelProtoAbstractSyntaxTree.fromCelAst(ast).toCheckedExpr()) + .eval( + new DefaultAsyncContext(directExecutor(), requestContext), + name -> immediateFuture(activation.resolve(name)))); + } + + /** Returns the {@code MessageProcessor} used for protobuf creation and manipulation. */ + public MessageProcessor messageProcessor() { + return messageProcessor; + } + + /** + * Indicates whether the {@code type_map} from the {@code CheckedExpr} is used to determine + * runtime typing during function and field resolution. + */ + public boolean typeDirected() { + return messageProcessor instanceof TypeDirectedMessageProcessor; + } + + /** Creates a new {@code EvalAsync} instance using the supplied dispatcher. */ + public EvalAsync withDispatcher(AsyncDispatcher dispatcher) { + if (dispatcher == this.dispatcher) { + return this; + } + return new EvalAsync(fileDescriptors, celOptions, typeDirected(), dispatcher); + } + + private Object forceExpressionFuture(ListenableFuture future) + throws InterpreterException { + try { + return future.get(); + } catch (InterruptedException intrExn) { + Thread.currentThread().interrupt(); + throw new RuntimeException(intrExn); + } catch (ExecutionException execExn) { + Throwable cause = execExn.getCause(); + if (cause instanceof InterpreterException) { + throw (InterpreterException) cause; + } else if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else { + throw new RuntimeException(cause); + } + } + } +} diff --git a/legacy/javatests/dev/cel/legacy/runtime/async/FuturesInterpreterTest.java b/legacy/javatests/dev/cel/legacy/runtime/async/FuturesInterpreterTest.java new file mode 100644 index 000000000..2f377f6b3 --- /dev/null +++ b/legacy/javatests/dev/cel/legacy/runtime/async/FuturesInterpreterTest.java @@ -0,0 +1,558 @@ +package dev.cel.legacy.runtime.async; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.util.concurrent.Futures.immediateFailedFuture; +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static dev.cel.common.types.CelProtoTypes.createMessage; +import static dev.cel.legacy.runtime.async.Effect.CONTEXT_DEPENDENT; +import static dev.cel.legacy.runtime.async.Effect.CONTEXT_INDEPENDENT; +import static dev.cel.legacy.runtime.async.EvaluationHelpers.immediateValue; + +import dev.cel.expr.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.context.Context; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import com.google.protobuf.ByteString; +import com.google.protobuf.Descriptors.FileDescriptor; +// import com.google.testing.testsize.MediumTest; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.types.CelProtoTypes; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.legacy.runtime.async.MessageProcessor.FieldAssigner; +import dev.cel.legacy.runtime.async.MessageProcessor.MessageBuilderCreator; +import dev.cel.runtime.Activation; +import dev.cel.runtime.InterpreterException; +import dev.cel.testing.CelBaselineTestCase; +import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.LongStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** Tests for {@link FuturesInterpreter} and related functionality. */ +// @MediumTest +@RunWith(Parameterized.class) +public class FuturesInterpreterTest extends CelBaselineTestCase { + + private final EvalAsync evalAsync; + private final AsyncDispatcher dispatcher; + + @Rule public TestName interpreterTestName = new TestName(); + + private static final ImmutableList TEST_FILE_DESCRIPTORS = + ImmutableList.of( + TestAllTypes.getDescriptor().getFile(), StandaloneGlobalEnum.getDescriptor().getFile()); + + // EvalSync and Async are mutable by design (Ex: adding function to the dispatcher). This has been + // overridden to make the test cases descriptive, as mutability is not a core concern of these + // tests. + @SuppressWarnings("ImmutableEnumChecker") + private enum EvalTestCase { + ASYNC_PROTO_TYPE_PARSER(false, () -> new EvalAsync(TEST_FILE_DESCRIPTORS, TEST_OPTIONS)), + ASYNC_PROTO_TYPE_DIRECTED_PROCESSOR_PARSER( + false, + () -> + new EvalAsync(TEST_FILE_DESCRIPTORS, TEST_OPTIONS, /* typeDirectedProcessor= */ true)), + ASYNC_CEL_TYPE_PARSER(true, () -> new EvalAsync(TEST_FILE_DESCRIPTORS, TEST_OPTIONS)), + ASYNC_CEL_TYPE_DIRECTED_PROCESSOR_PARSER( + true, + () -> + new EvalAsync(TEST_FILE_DESCRIPTORS, TEST_OPTIONS, /* typeDirectedProcessor= */ true)); + + private final boolean declareWithCelType; + private final Supplier eval; + + EvalTestCase(boolean declareWithCelType, Supplier eval) { + this.declareWithCelType = declareWithCelType; + this.eval = eval; + } + } + + @Parameters() + public static ImmutableList evalTestCases() { + return ImmutableList.copyOf(EvalTestCase.values()); + } + + public FuturesInterpreterTest(EvalTestCase testCase) { + super(testCase.declareWithCelType); + this.evalAsync = testCase.eval.get(); + this.dispatcher = (AsyncDispatcher) evalAsync.registrar(); + } + + /** Helper to run a test for configured instance variables. */ + private void runTest(Activation activation, AsyncDispatcher localDispatcher) throws Exception { + CelAbstractSyntaxTree ast = prepareTest(evalAsync.fileDescriptors()); + if (ast == null) { + return; + } + EvalAsync evalAsyncLocal = evalAsync.withDispatcher(localDispatcher); + testOutput().println("bindings: " + activation); + Object result; + try { + result = evalAsyncLocal.eval(ast, activation); + if (result instanceof ByteString) { + // Note: this call may fail for printing byte sequences that are not valid UTF-8, but works + // pretty well for test purposes. + result = ((ByteString) result).toStringUtf8(); + } + testOutput().println("result: " + result); + } catch (InterpreterException e) { + testOutput().println("error: " + e.getMessage()); + } + testOutput().println(); + } + + private void runTest(Activation activation) throws Exception { + runTest(activation, dispatcher); + } + + @Test + public void nobarrierFunction() throws Exception { + declareFunction( + "F", + globalOverload( + "F", + ImmutableList.of(CelProtoTypes.BOOL, CelProtoTypes.INT64, CelProtoTypes.INT64), + CelProtoTypes.INT64)); + dispatcher.addNobarrierAsync( + "F", + CONTEXT_INDEPENDENT, + (ign, args) -> + FluentFuture.from(args.get(0)) + .transformAsync(b -> ((boolean) b) ? args.get(1) : args.get(2), directExecutor())); + + source = "F(true, 2, 1 / 0)"; // Failing argument does not matter, so overall success. + runTest(Activation.EMPTY); + + source = "F(false, 3, 1 / 0)"; // Failing argument matters. + runTest(Activation.EMPTY); + } + + @Test + public void dispatcherSnapshot() throws Exception { + AsyncDispatcher orig = DefaultAsyncDispatcher.create(TEST_OPTIONS); + + AsyncDispatcher snapshot = orig.fork(); + // Function added after snapshot is taken is expected to not impact result. + orig.add("just_undefined", String.class, x -> "was defined after all: " + x); + + declareFunction( + "just_undefined", + globalOverload( + "just_undefined", ImmutableList.of(CelProtoTypes.STRING), CelProtoTypes.STRING)); + + // No definition in snapshot, so this fails with an unbound overload. + source = "just_undefined(\"hi there \") == \"foo\""; + runTest(Activation.EMPTY, snapshot); + } + + // This lambda implements @Immutable interface 'StrictUnaryFunction', but the declaration of type + // 'com.google.common.util.concurrent.SettableFuture' is not annotated with + // @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + @Test + public void longComprehension() throws Exception { + long upper = 1000L; + Type listType = CelProtoTypes.createList(CelProtoTypes.INT64); + ImmutableList l = LongStream.range(0L, upper).boxed().collect(toImmutableList()); + dispatcher.add("constantLongList", ImmutableList.of(), args -> l); + + // Comprehension over compile-time constant long list. + declareFunction( + "constantLongList", globalOverload("constantLongList", ImmutableList.of(), listType)); + source = "size(constantLongList().map(x, x+1)) == 1000"; + runTest(Activation.EMPTY); + + // Comprehension over long list that is not compile-time constant. + declareVariable("longlist", CelProtoTypes.createList(CelProtoTypes.INT64)); + source = "size(longlist.map(x, x+1)) == 1000"; + runTest(Activation.of("longlist", l)); + + // Comprehension over long list where the computation is very slow. + SettableFuture firstFuture = SettableFuture.create(); + dispatcher.addDirect( + "f_slow_inc", + Long.class, + CONTEXT_INDEPENDENT, + n -> { + if (n == 0) { + // stall on the first element + return firstFuture; + } + return immediateValue(n + 1L); + }); + dispatcher.addNobarrierAsync( + "f_unleash", + CONTEXT_INDEPENDENT, + (gctx, args) -> { + firstFuture.set(1L); + return args.get(0); + }); + declareFunction( + "f_slow_inc", + globalOverload("f_slow_inc", ImmutableList.of(CelProtoTypes.INT64), CelProtoTypes.INT64)); + declareFunction( + "f_unleash", + globalOverload( + "f_unleash", + ImmutableList.of(CelProtoTypes.createTypeParam("A")), + ImmutableList.of("A"), + CelProtoTypes.createTypeParam("A"))); + source = "f_unleash(longlist.map(x, f_slow_inc(x)))[0] == 1"; + + runTest(Activation.of("longlist", l)); + } + + @Test + public void functionRegistrar() throws Exception { + // This test case exercises every registration method of FunctionRegistrar and then + // invokes each of the registered methods in such a way that both the constant-fold path + // and the regular non-constant-fold path is invoked. + dispatcher.addCallConstructor( + "f_constructor", (md, id, args) -> CompiledExpression.constant("constructed!")); + dispatcher.addStrictFunction( + "f_strict", + ImmutableList.of(String.class, Long.class), + true, + (gctx, args) -> immediateValue("strict: " + (String) args.get(0) + (Long) args.get(1))); + dispatcher.addDirect( + "f_directN", + ImmutableList.of(String.class, Boolean.class), + CONTEXT_INDEPENDENT, + (gctx, args) -> immediateValue("directN: " + (String) args.get(0) + (Boolean) args.get(1))); + dispatcher.addDirect( + "f_direct", + CONTEXT_INDEPENDENT, + (gctx, args) -> immediateValue("direct: " + (Boolean) args.get(0) + (Long) args.get(1))); + dispatcher.addDirect( + "f_direct1", String.class, CONTEXT_INDEPENDENT, s -> immediateValue("direct1:" + s)); + dispatcher.addDirect( + "f_direct2", + String.class, + Long.class, + CONTEXT_INDEPENDENT, + (s, i) -> immediateValue("direct2: " + s + i)); + dispatcher.addAsync( + "f_asyncN", + ImmutableList.of(String.class, Boolean.class), + CONTEXT_INDEPENDENT, + (gctx, args) -> immediateValue("asyncN: " + (String) args.get(0) + (Boolean) args.get(1))); + dispatcher.addAsync( + "f_async", + CONTEXT_INDEPENDENT, + (gctx, args) -> immediateValue("async: " + (Boolean) args.get(0) + (Long) args.get(1))); + dispatcher.addAsync( + "f_async1", String.class, CONTEXT_INDEPENDENT, s -> immediateValue("async1:" + s)); + dispatcher.addAsync( + "f_async2", + String.class, + Long.class, + CONTEXT_INDEPENDENT, + (s, i) -> immediateValue("async2: " + s + i)); + dispatcher.addDirect( + "f_effect", String.class, CONTEXT_DEPENDENT, s -> immediateValue("effective: " + s)); + dispatcher.addNobarrierAsync( + "f_nobarrier", + CONTEXT_INDEPENDENT, + (gctx, futures) -> + Futures.transform(futures.get(0), x -> "nobarrier: " + (String) x, directExecutor())); + dispatcher.add("f_simple1", String.class, s -> "simple1: " + s); + dispatcher.add("f_simple2", String.class, String.class, (x, y) -> "simple2: " + x + "@" + y); + dispatcher.add( + "f_simpleN", + ImmutableList.of(String.class, Long.class, Long.class), + args -> "simpleN: " + (String) args[0] + (Long) args[1] + (Long) args[2]); + + // A dynamic value that cannot be constant-folded away. + declareVariable("dynamic", CelProtoTypes.STRING); + + declareGlobalFunction("f_constructor", ImmutableList.of(), CelProtoTypes.STRING); + declareGlobalFunction( + "f_strict", + ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.INT64), + CelProtoTypes.STRING); + declareGlobalFunction( + "f_directN", + ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.BOOL), + CelProtoTypes.STRING); + declareGlobalFunction( + "f_direct", + ImmutableList.of(CelProtoTypes.BOOL, CelProtoTypes.INT64), + CelProtoTypes.STRING); + declareGlobalFunction( + "f_direct1", ImmutableList.of(CelProtoTypes.STRING), CelProtoTypes.STRING); + declareGlobalFunction( + "f_direct2", + ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.INT64), + CelProtoTypes.STRING); + declareGlobalFunction( + "f_asyncN", + ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.BOOL), + CelProtoTypes.STRING); + declareGlobalFunction( + "f_async", ImmutableList.of(CelProtoTypes.BOOL, CelProtoTypes.INT64), CelProtoTypes.STRING); + declareGlobalFunction("f_async1", ImmutableList.of(CelProtoTypes.STRING), CelProtoTypes.STRING); + declareGlobalFunction( + "f_async2", + ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.INT64), + CelProtoTypes.STRING); + declareGlobalFunction("f_effect", ImmutableList.of(CelProtoTypes.STRING), CelProtoTypes.STRING); + declareGlobalFunction( + "f_nobarrier", ImmutableList.of(CelProtoTypes.STRING), CelProtoTypes.STRING); + declareGlobalFunction( + "f_simple1", ImmutableList.of(CelProtoTypes.STRING), CelProtoTypes.STRING); + declareGlobalFunction( + "f_simple2", + ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.STRING), + CelProtoTypes.STRING); + declareGlobalFunction( + "f_simpleN", + ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.INT64, CelProtoTypes.INT64), + CelProtoTypes.STRING); + + source = + "f_constructor() + \"\\n\" + " + + "f_strict(\"static\", 13) + \" \" + f_strict(dynamic, 14) + \"\\n\" + " + + "f_directN(\"static\", true) + \" \" + f_directN(dynamic, false) + \"\\n\" + " + + "f_direct(true, 20) + \" \" + f_direct(dynamic == \"foo\", 21) + \"\\n\" + " + + "f_direct1(\"static\") + \" \" + f_direct1(dynamic) + \"\\n\" + " + + "f_direct2(\"static\", 30) + \" \" + f_direct2(dynamic, 31) + \"\\n\" + " + + "f_asyncN(\"static\", true) + \" \" + f_asyncN(dynamic, false) + \"\\n\" + " + + "f_async(true, 20) + \" \" + f_async(dynamic == \"foo\", 21) + \"\\n\" + " + + "f_async1(\"static\") + \" \" + f_async1(dynamic) + \"\\n\" + " + + "f_async2(\"static\", 30) + \" \" + f_async2(dynamic, 31) + \"\\n\" + " + + "f_effect(\"static\") + \" \" + f_effect(dynamic) + \"\\n\" + " + + "f_nobarrier(\"static\") + \" \" + f_nobarrier(dynamic) + \"\\n\" + " + + "f_simple1(\"static\") + \" \" + f_simple1(dynamic) + \"\\n\" + " + + "f_simple2(\"static\", \"foo\") + \" \" + f_simple2(dynamic, \"bar\") + \"\\n\" + " + + "f_simpleN(\"static\", 54, 32) + \" \" + f_simpleN(dynamic, 98,76)"; + + runTest(Activation.of("dynamic", "dynamic")); + } + + @Test + public void contextPropagation() throws Exception { + dispatcher.addAsync( + "f", + ImmutableList.of(), + CONTEXT_DEPENDENT, + (gctx, args) -> + immediateValue( + Context.getCurrentContext().getSecurityContext().getLoggablePeer().getUsername())); + declareGlobalFunction("f", ImmutableList.of(), CelProtoTypes.STRING); + source = "f()"; + runTest(Activation.EMPTY); + } + + @Test + public void delayedEvaluation() throws Exception { + dispatcher.addCallConstructor( + "f_delay", + (md, id, args) -> + args.get(0) + .expression() + .map( + (e, effect) -> + CompiledExpression.executable( + stack -> + FluentFuture.from( + immediateFuture( + Delayed.of( + gctx -> e.execute(stack.withGlobalContext(gctx))))), + CONTEXT_INDEPENDENT), + c -> CompiledExpression.constant(Delayed.fromFuture(immediateFuture(c))), + t -> + CompiledExpression.constant(Delayed.fromFuture(immediateFailedFuture(t))))); + dispatcher.addDirect( + "f_force", + ImmutableList.of(Delayed.class), + CONTEXT_DEPENDENT, + (gctx, args) -> ((Delayed) args.get(0)).force(gctx)); + + declareFunction( + "f_delay", + globalOverload("f_delay", ImmutableList.of(CelProtoTypes.INT64), CelProtoTypes.DYN)); + declareFunction( + "f_force", + globalOverload("f_force", ImmutableList.of(CelProtoTypes.DYN), CelProtoTypes.INT64)); + + // Delayed computation is just a constant. + source = "f_force(f_delay(1 + 2)) == 3"; + runTest(Activation.EMPTY); + + // A dynamic (global) input. + declareVariable("four", CelProtoTypes.INT64); + + // Delayed computation depends on global state. + source = "f_force(f_delay(1 + four)) == 5"; + runTest(Activation.of("four", 4L)); + + // Delayed computation occurs within local scope and captures the value of a local variable. + source = "[1, 2, 3].map(i, f_delay(i + four)).map(d, f_force(d)) == [5, 6, 7]"; + runTest(Activation.of("four", 4L)); + } + + @Test + public void stashedLocal() throws Exception { + // Executes its argument within a stack that has been extended with a slot for %stash%. + // (The value stored there is 123.) + dispatcher.addCallConstructor( + "f_stash", + (md, id, args) -> { + ExecutableExpression executable = + args.get(0).scopedExpression().inScopeOf("%stash%").toExecutable(); + return CompiledExpression.executable( + stack -> executable.execute(stack.extend(immediateValue(123L))), CONTEXT_DEPENDENT); + }); + // Refers to the value at the stack offset that corresponds to %stash%. + dispatcher.addCallConstructor( + "f_grab", + (md, id, args, offsetFinder) -> { + int offset = offsetFinder.findStackOffset(md, id, "%stash%"); + return CompiledExpression.executable( + stack -> stack.getLocalAtSlotOffset(offset), CONTEXT_INDEPENDENT); + }); + // f_stash: (T) -> T + declareFunction( + "f_stash", + globalOverload( + "f_stash", + ImmutableList.of(CelProtoTypes.createTypeParam("T")), + ImmutableList.of("T"), + CelProtoTypes.createTypeParam("T"))); + // f_grab: () -> int + declareFunction("f_grab", globalOverload("f_grab", ImmutableList.of(), CelProtoTypes.INT64)); + + source = "f_stash([1, 2, 3].map(x, x + f_grab())) == [124, 125, 126]"; + runTest(Activation.EMPTY); + } + + // This lambda implements @Immutable interface 'BinaryFunction', but the declaration of type + // 'com.google.api.tools.contract.runtime.interpreter.MessageProcessor' is not annotated with + // @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + @Test + public void fieldManipulation() throws Exception { + String protoName = TestAllTypes.getDescriptor().getFullName(); + Type protoType = createMessage(protoName); + + declareFunction( + "assignSingleInt64", + memberOverload( + "assignSingleInt64", ImmutableList.of(protoType, CelProtoTypes.INT64), protoType)); + declareFunction( + "assignRepeatedInt64", + memberOverload( + "assignRepeatedInt64", + ImmutableList.of(protoType, CelProtoTypes.createList(CelProtoTypes.INT64)), + protoType)); + declareFunction( + "assignMap", + memberOverload( + "assignMap", + ImmutableList.of( + protoType, CelProtoTypes.createMap(CelProtoTypes.INT64, CelProtoTypes.INT64)), + protoType)); + declareFunction( + "clearField", + memberOverload("clearField", ImmutableList.of(protoType, CelProtoTypes.STRING), protoType)); + declareFunction( + "singletonInt64", + globalOverload("singletonInt64", ImmutableList.of(CelProtoTypes.INT64), protoType)); + + MessageProcessor messageProcessor = evalAsync.messageProcessor(); + MessageBuilderCreator builderCreator = + messageProcessor.makeMessageBuilderCreator(null, 0L, protoName); + FieldAssigner singleAssigner = + messageProcessor.makeFieldAssigner( + null, 0L, protoName, "single_int64", CelProtoTypes.INT64); + FieldAssigner repeatedAssigner = + messageProcessor.makeFieldAssigner( + null, 0L, protoName, "repeated_int64", CelProtoTypes.createList(CelProtoTypes.INT64)); + FieldAssigner mapAssigner = + messageProcessor.makeFieldAssigner( + null, + 0L, + protoName, + "map_int32_int64", + CelProtoTypes.createMap(CelProtoTypes.INT64, CelProtoTypes.INT64)); + + dispatcher.add( + "assignSingleInt64", + TestAllTypes.class, + Long.class, + (p, i) -> singleAssigner.assign(p.toBuilder(), i).build()); + dispatcher.add( + "assignRepeatedInt64", + TestAllTypes.class, + List.class, + (p, l) -> repeatedAssigner.assign(p.toBuilder(), l).build()); + dispatcher.add( + "assignMap", + TestAllTypes.class, + Map.class, + (p, m) -> mapAssigner.assign(p.toBuilder(), m).build()); + dispatcher.add( + "clearField", + TestAllTypes.class, + String.class, + (p, n) -> + messageProcessor.makeFieldClearer(null, 0L, protoName, n).clear(p.toBuilder()).build()); + dispatcher.add( + "singletonInt64", + Long.class, + i -> singleAssigner.assign(builderCreator.builder(), i).build()); + + container = TestAllTypes.getDescriptor().getFile().getPackage(); + + source = + "TestAllTypes{single_bool: true}.assignSingleInt64(1) == " + + "TestAllTypes{single_bool: true, single_int64: 1}"; + runTest(Activation.EMPTY); + + source = + "TestAllTypes{repeated_int64: [1, 2]}.assignRepeatedInt64([3, 1, 4]) == " + + "TestAllTypes{repeated_int64: [3, 1, 4]}"; + runTest(Activation.EMPTY); + + source = + "TestAllTypes{single_bool: true, single_int64: 1}.clearField(\"single_bool\") == " + + "TestAllTypes{single_int64: 1}"; + runTest(Activation.EMPTY); + + source = + "TestAllTypes{single_bool: false}.assignMap({13: 26, 22: 42}).map_int32_int64[22] == 42"; + runTest(Activation.EMPTY); + + source = + "TestAllTypes{single_bool: true, repeated_int64: [1, 2]}.clearField(\"repeated_int64\") == " + + "TestAllTypes{single_bool: true}"; + runTest(Activation.EMPTY); + + source = "singletonInt64(12) == TestAllTypes{single_int64: 12}"; + runTest(Activation.EMPTY); + } + + /** Represents a delayed computation, used by the delayedEvaluation test case below. */ + interface Delayed { + ListenableFuture force(GlobalContext gctx); + + static Delayed fromFuture(ListenableFuture future) { + return gctx -> future; + } + + static Delayed of(Delayed d) { + return d; + } + } +} diff --git a/legacy/javatests/dev/cel/legacy/runtime/async/FuturesInterpreterWithMessageProcessorTest.java b/legacy/javatests/dev/cel/legacy/runtime/async/FuturesInterpreterWithMessageProcessorTest.java new file mode 100644 index 000000000..5fb270926 --- /dev/null +++ b/legacy/javatests/dev/cel/legacy/runtime/async/FuturesInterpreterWithMessageProcessorTest.java @@ -0,0 +1,414 @@ +package dev.cel.legacy.runtime.async; + +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static dev.cel.common.types.CelProtoTypes.createMessage; +import static dev.cel.legacy.runtime.async.Effect.CONTEXT_DEPENDENT; + +import dev.cel.expr.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.ByteString; +import com.google.protobuf.Descriptors.FileDescriptor; +// import com.google.testing.testsize.MediumTest; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.types.CelProtoTypes; +import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.legacy.runtime.async.MessageProcessor.FieldAssigner; +import dev.cel.legacy.runtime.async.MessageProcessor.MessageBuilderCreator; +import dev.cel.runtime.Activation; +import dev.cel.runtime.InterpreterException; +import dev.cel.runtime.Registrar.BinaryFunction; +import dev.cel.runtime.Registrar.UnaryFunction; +import dev.cel.testing.CelBaselineTestCase; +import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** Tests for {@link FuturesInterpreter} and related functionality. */ +// @MediumTest +@RunWith(Parameterized.class) +public class FuturesInterpreterWithMessageProcessorTest extends CelBaselineTestCase { + private final EvalAsync evalAsync; + + private static final ImmutableList TEST_FILE_DESCRIPTORS = + ImmutableList.of( + TestAllTypes.getDescriptor().getFile(), + dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor().getFile(), + StandaloneGlobalEnum.getDescriptor().getFile()); + + // EvalSync and Async are mutable by design (Ex: adding function to the dispatcher). This has been + // overridden to make the test cases descriptive, as mutability is not a core concern of these + // tests. + @SuppressWarnings("ImmutableEnumChecker") + private enum EvalTestCase { + ASYNC_PROTO_TYPE_PARSER_DIRECTED_PROCESSOR( + false, + () -> + new EvalAsync(TEST_FILE_DESCRIPTORS, TEST_OPTIONS, /* typeDirectedProcessor= */ true)), + ASYNC_CEL_TYPE_PARSER_DIRECTED_PROCESSOR( + true, + () -> + new EvalAsync(TEST_FILE_DESCRIPTORS, TEST_OPTIONS, /* typeDirectedProcessor= */ true)); + + private final boolean declareWithCelType; + private final Supplier eval; + + EvalTestCase(boolean declareWithCelType, Supplier eval) { + this.declareWithCelType = declareWithCelType; + this.eval = eval; + } + } + + @Parameters() + public static ImmutableList evalTestCases() { + return ImmutableList.copyOf(EvalTestCase.values()); + } + + public FuturesInterpreterWithMessageProcessorTest(EvalTestCase testCase) { + super(testCase.declareWithCelType); + this.evalAsync = testCase.eval.get(); + } + + /** Helper to run a test for configured instance variables. */ + private void runTest(Activation activation) throws Exception { + CelAbstractSyntaxTree ast = prepareTest(evalAsync.fileDescriptors()); + if (ast == null) { + return; + } + testOutput().println("bindings: " + activation); + try { + Object result = evalAsync.eval(ast, activation); + if (result instanceof ByteString) { + // Note: this call may fail for printing byte sequences that are not valid UTF-8, but works + // pretty well for test purposes. + result = ((ByteString) result).toStringUtf8(); + } + testOutput().println("result: " + result); + } catch (InterpreterException e) { + testOutput().println("error: " + e.getMessage()); + } + testOutput().println(); + } + + // Helper for testing late-binding of the MessageProcessor (binary functions). + // This lambda implements @Immutable interface 'CallConstructor', but the declaration of type + // 'java.util.function.Function>' is not annotated with @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + private static void addFunctionWithMessageProcessor( + FunctionRegistrar registrar, + String overloadId, + Class clazzA, + Class clazzB, + Function> functionMaker) { + registrar.addCallConstructor( + overloadId, + (md, id, args, mp, ignoredStackOffsetFinder) -> { + ExecutableExpression executableA = args.get(0).expression().toExecutable(); + ExecutableExpression executableB = args.get(1).expression().toExecutable(); + BinaryFunction function = functionMaker.apply(mp); + return CompiledExpression.executable( + stack -> + executableA + .execute(stack) + .transformAsync( + a -> + executableB + .execute(stack) + .transformAsync( + b -> + immediateFuture( + function.apply(clazzA.cast(a), clazzB.cast(b))), + directExecutor()), + directExecutor()), + CONTEXT_DEPENDENT); + }); + } + + // Helper for testing late-binding of the MessageProcessor (unary functions). + // This lambda implements @Immutable interface 'CallConstructor', but the declaration of type + // 'java.util.function.Function>' is not annotated with @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + private static void addFunctionWithMessageProcessor( + FunctionRegistrar registrar, + String overloadId, + Class clazzA, + Function> functionMaker) { + registrar.addCallConstructor( + overloadId, + (md, id, args, mp, ignoredStackOffsetFinder) -> { + ExecutableExpression executableA = args.get(0).expression().toExecutable(); + UnaryFunction function = functionMaker.apply(mp); + return CompiledExpression.executable( + stack -> + executableA + .execute(stack) + .transformAsync( + a -> immediateFuture(function.apply(clazzA.cast(a))), directExecutor()), + CONTEXT_DEPENDENT); + }); + } + + // This lambda implements @Immutable interface 'BinaryFunction', but the declaration of type + // 'com.google.api.tools.contract.runtime.interpreter.MessageProcessor' is not annotated with + // @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + @Test + public void fieldManipulation() throws Exception { + String protoName = TestAllTypes.getDescriptor().getFullName(); + Type protoType = createMessage(protoName); + + declareFunction( + "assignSingleInt64", + memberOverload( + "assignSingleInt64", ImmutableList.of(protoType, CelProtoTypes.INT64), protoType)); + declareFunction( + "assignRepeatedInt64", + memberOverload( + "assignRepeatedInt64", + ImmutableList.of(protoType, CelProtoTypes.createList(CelProtoTypes.INT64)), + protoType)); + declareFunction( + "assignMap", + memberOverload( + "assignMap", + ImmutableList.of( + protoType, CelProtoTypes.createMap(CelProtoTypes.INT64, CelProtoTypes.INT64)), + protoType)); + declareFunction( + "clearField", + memberOverload("clearField", ImmutableList.of(protoType, CelProtoTypes.STRING), protoType)); + declareFunction( + "singletonInt64", + globalOverload("singletonInt64", ImmutableList.of(CelProtoTypes.INT64), protoType)); + + MessageBuilderCreator builderCreator = + evalAsync.messageProcessor().makeMessageBuilderCreator(null, 0L, protoName); + FieldAssigner singleAssigner = + evalAsync + .messageProcessor() + .makeFieldAssigner(null, 0L, protoName, "single_int64", CelProtoTypes.INT64); + + AsyncDispatcher dispatcher = (AsyncDispatcher) evalAsync.registrar(); + dispatcher.add( + "assignSingleInt64", + TestAllTypes.class, + Long.class, + (p, i) -> singleAssigner.assign(p.toBuilder(), i).build()); + addFunctionWithMessageProcessor( + dispatcher, + "assignRepeatedInt64", + TestAllTypes.class, + List.class, + mp -> + (p, l) -> + mp.makeFieldAssigner( + null, + 0L, + protoName, + "repeated_int64", + CelProtoTypes.createList(CelProtoTypes.INT64)) + .assign(p.toBuilder(), l) + .build()); + addFunctionWithMessageProcessor( + dispatcher, + "assignMap", + TestAllTypes.class, + Map.class, + mp -> + (p, m) -> + mp.makeFieldAssigner( + null, + 0L, + protoName, + "map_int32_int64", + CelProtoTypes.createMap(CelProtoTypes.INT64, CelProtoTypes.INT64)) + .assign(p.toBuilder(), m) + .build()); + addFunctionWithMessageProcessor( + dispatcher, + "clearField", + TestAllTypes.class, + String.class, + mp -> (p, n) -> mp.makeFieldClearer(null, 0L, protoName, n).clear(p.toBuilder()).build()); + dispatcher.add( + "singletonInt64", + Long.class, + i -> singleAssigner.assign(builderCreator.builder(), i).build()); + + container = TestAllTypes.getDescriptor().getFile().getPackage(); + + source = + "TestAllTypes{single_bool: true}.assignSingleInt64(1) == " + + "TestAllTypes{single_bool: true, single_int64: 1}"; + runTest(Activation.EMPTY); + + source = + "TestAllTypes{repeated_int64: [1, 2]}.assignRepeatedInt64([3, 1, 4]) == " + + "TestAllTypes{repeated_int64: [3, 1, 4]}"; + runTest(Activation.EMPTY); + + source = + "TestAllTypes{single_bool: true, single_int64: 1}.clearField(\"single_bool\") == " + + "TestAllTypes{single_int64: 1}"; + runTest(Activation.EMPTY); + + source = + "TestAllTypes{single_bool: false}.assignMap({13: 26, 22: 42}).map_int32_int64[22] == 42"; + runTest(Activation.EMPTY); + + source = + "TestAllTypes{single_bool: true, repeated_int64: [1, 2]}.clearField(\"repeated_int64\") == " + + "TestAllTypes{single_bool: true}"; + runTest(Activation.EMPTY); + + source = "singletonInt64(12) == TestAllTypes{single_int64: 12}"; + runTest(Activation.EMPTY); + } + + // This lambda implements @Immutable interface 'UnaryFunction', but the declaration of type + // 'com.google.api.tools.contract.runtime.interpreter.MessageProcessor' is not annotated with + // @com.google.errorprone.annotations.Immutable + @SuppressWarnings("Immutable") + @Test + public void extensionManipulation() throws Exception { + String extI = "cel.expr.conformance.proto2.int32_ext"; + String extN = "cel.expr.conformance.proto2.nested_ext"; + String extR = "cel.expr.conformance.proto2.repeated_test_all_types"; + String protoName = dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor().getFullName(); + Type protoType = createMessage(protoName); + String holderName = dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor().getFullName(); + Type holderType = createMessage(holderName); + Type holderListType = CelProtoTypes.createList(holderType); + dev.cel.expr.conformance.proto2.TestAllTypes withoutExt = + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder().setSingleInt32(50).build(); + dev.cel.expr.conformance.proto2.TestAllTypes withExt = + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() + .setSingleInt32(100) + .setExtension(TestAllTypesExtensions.int32Ext, 200) + .setExtension(TestAllTypesExtensions.nestedExt, withoutExt) + .addExtension( + TestAllTypesExtensions.repeatedTestAllTypes, + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() + .setSingleString("alpha") + .build()) + .addExtension( + TestAllTypesExtensions.repeatedTestAllTypes, + dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() + .setSingleString("alpha") + .build()) + .build(); + + declareVariable("y", protoType); + declareVariable("n", protoType); + + declareMemberFunction("getI", ImmutableList.of(protoType), CelProtoTypes.INT64); + declareMemberFunction("hasI", ImmutableList.of(protoType), CelProtoTypes.BOOL); + declareMemberFunction("assignI", ImmutableList.of(protoType, CelProtoTypes.INT64), protoType); + declareMemberFunction("clearI", ImmutableList.of(protoType), protoType); + + declareMemberFunction("getN", ImmutableList.of(protoType), protoType); + declareMemberFunction("hasN", ImmutableList.of(protoType), CelProtoTypes.BOOL); + declareMemberFunction("assignN", ImmutableList.of(protoType, protoType), protoType); + declareMemberFunction("clearN", ImmutableList.of(protoType), protoType); + + declareMemberFunction("getR", ImmutableList.of(protoType), holderListType); + declareMemberFunction("assignR", ImmutableList.of(protoType, holderListType), protoType); + declareMemberFunction("clearR", ImmutableList.of(protoType), protoType); + + AsyncDispatcher dispatcher = (AsyncDispatcher) evalAsync.registrar(); + addFunctionWithMessageProcessor( + dispatcher, + "getI", + dev.cel.expr.conformance.proto2.TestAllTypes.class, + mp -> p -> mp.makeExtensionGetter(null, 0L, extI).getField(p)); + addFunctionWithMessageProcessor( + dispatcher, + "hasI", + dev.cel.expr.conformance.proto2.TestAllTypes.class, + mp -> p -> mp.makeExtensionTester(null, 0L, extI).hasField(p)); + addFunctionWithMessageProcessor( + dispatcher, + "assignI", + dev.cel.expr.conformance.proto2.TestAllTypes.class, + Long.class, + mp -> + (p, i) -> + mp.makeExtensionAssigner(null, 0L, extI, CelProtoTypes.INT64) + .assign(p.toBuilder(), i) + .build()); + addFunctionWithMessageProcessor( + dispatcher, + "clearI", + dev.cel.expr.conformance.proto2.TestAllTypes.class, + mp -> p -> mp.makeExtensionClearer(null, 0L, extI).clear(p.toBuilder()).build()); + addFunctionWithMessageProcessor( + dispatcher, + "getN", + dev.cel.expr.conformance.proto2.TestAllTypes.class, + mp -> p -> mp.makeExtensionGetter(null, 0L, extN).getField(p)); + addFunctionWithMessageProcessor( + dispatcher, + "hasN", + dev.cel.expr.conformance.proto2.TestAllTypes.class, + mp -> p -> mp.makeExtensionTester(null, 0L, extN).hasField(p)); + addFunctionWithMessageProcessor( + dispatcher, + "assignN", + dev.cel.expr.conformance.proto2.TestAllTypes.class, + dev.cel.expr.conformance.proto2.TestAllTypes.class, + mp -> + (p, i) -> + mp.makeExtensionAssigner(null, 0L, extN, protoType) + .assign(p.toBuilder(), i) + .build()); + addFunctionWithMessageProcessor( + dispatcher, + "clearN", + dev.cel.expr.conformance.proto2.TestAllTypes.class, + mp -> p -> mp.makeExtensionClearer(null, 0L, extN).clear(p.toBuilder()).build()); + addFunctionWithMessageProcessor( + dispatcher, + "getR", + dev.cel.expr.conformance.proto2.TestAllTypes.class, + mp -> p -> mp.makeExtensionGetter(null, 0L, extR).getField(p)); + addFunctionWithMessageProcessor( + dispatcher, + "assignR", + dev.cel.expr.conformance.proto2.TestAllTypes.class, + List.class, + mp -> + (p, l) -> + mp.makeExtensionAssigner(null, 0L, extR, holderListType) + .assign(p.toBuilder(), l) + .build()); + addFunctionWithMessageProcessor( + dispatcher, + "clearR", + dev.cel.expr.conformance.proto2.TestAllTypes.class, + mp -> p -> mp.makeExtensionClearer(null, 0L, extR).clear(p.toBuilder()).build()); + + container = dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor().getFullName(); + + source = + "[y.hasI(), y.getI() == 200, !n.hasI(), n.getI() == 0,\n" + + " n.assignI(43).hasI(), n.assignI(42).getI() == 42,\n" + + " y.assignI(99).hasI(), y.assignI(31).getI() == 31,\n" + + " !n.clearI().hasI(), !y.clearI().hasI(), y.clearI().getI() == 0,\n" + + " y.hasN(), y.getN().getI() == 0, !y.getN().hasN(), y.getN().getN().getI() == 0,\n" + + " !n.hasN(), n.assignN(y).getN().hasN(),\n" + + " !n.clearN().hasN(), !y.clearN().hasN(),\n" + + " n.getR() == [], y.getR().map(h, h.single_string) == [\"alpha\", \"beta\"],\n" + + " n.assignR([\"a\", \"b\"].map(s, TestAllTypes{single_string:s}))." + + "getR().map(h, h.single_string) == [\"a\", \"b\"],\n" + + " y.clearR().getR() == []]"; + runTest(Activation.copyOf(ImmutableMap.of("y", withExt, "n", withoutExt))); + } +} From 1d78bd6cd1ec20ced2cfc95f8e77e32067a05e10 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 10 Jan 2025 16:06:28 -0800 Subject: [PATCH 4/9] Internal Changes PiperOrigin-RevId: 714261384 --- .../java/dev/cel/common/types/CelKind.java | 1 - .../dev/cel/common/types/CelKindTest.java | 1 - legacy/BUILD.bazel | 25 - legacy/README.md | 3 - .../runtime/async/AsyncCanonicalResolver.java | 19 - .../legacy/runtime/async/AsyncContext.java | 68 - .../legacy/runtime/async/AsyncDispatcher.java | 14 - .../runtime/async/AsyncDispatcherBase.java | 264 --- .../runtime/async/AsyncGlobalResolver.java | 15 - .../runtime/async/AsyncInterpretable.java | 63 - .../async/AsyncInterpretableOrConstant.java | 81 - .../runtime/async/AsyncInterpreter.java | 32 - .../dev/cel/legacy/runtime/async/BUILD.bazel | 83 - .../runtime/async/Canonicalization.java | 348 ---- .../runtime/async/CompiledExpression.java | 144 -- .../runtime/async/DefaultAsyncContext.java | 98 - .../runtime/async/DefaultAsyncDispatcher.java | 40 - .../runtime/async/DummyAsyncContext.java | 26 - .../cel/legacy/runtime/async/DynamicEnv.java | 147 -- .../dev/cel/legacy/runtime/async/Effect.java | 42 - .../runtime/async/EvaluationHelpers.java | 335 ---- .../cel/legacy/runtime/async/Evaluator.java | 1232 ------------ .../runtime/async/ExecutableExpression.java | 28 - .../runtime/async/FunctionRegistrar.java | 340 ---- .../runtime/async/FunctionResolver.java | 42 - .../runtime/async/FuturesInterpreter.java | 52 - .../legacy/runtime/async/GlobalContext.java | 29 - .../async/IdentifiedCompiledExpression.java | 47 - .../runtime/async/MessageProcessor.java | 241 --- .../async/MessageProcessorAdapter.java | 230 --- .../runtime/async/ProtoFieldAssignment.java | 546 ------ .../legacy/runtime/async/ResolverAdapter.java | 40 - .../runtime/async/StackOffsetFinder.java | 17 - .../runtime/async/StandardConstructs.java | 342 ---- .../runtime/async/StandardTypeResolver.java | 242 --- .../async/TypeDirectedMessageProcessor.java | 286 --- .../legacy/runtime/async/TypeResolver.java | 67 - .../runtime/async/AsyncInterpreterTest.java | 65 - .../dev/cel/legacy/runtime/async/BUILD.bazel | 102 - .../runtime/async/BaseInterpreterTest.java | 1683 ----------------- .../legacy/runtime/async/DynamicEnvTest.java | 56 - .../dev/cel/legacy/runtime/async/Eval.java | 44 - .../cel/legacy/runtime/async/EvalAsync.java | 168 -- .../runtime/async/FuturesInterpreterTest.java | 558 ------ ...esInterpreterWithMessageProcessorTest.java | 414 ---- 45 files changed, 8720 deletions(-) delete mode 100644 legacy/BUILD.bazel delete mode 100644 legacy/README.md delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/AsyncCanonicalResolver.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/AsyncContext.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/AsyncDispatcher.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/AsyncDispatcherBase.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/AsyncGlobalResolver.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/AsyncInterpretable.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/AsyncInterpretableOrConstant.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/AsyncInterpreter.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/BUILD.bazel delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/Canonicalization.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/CompiledExpression.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/DefaultAsyncContext.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/DefaultAsyncDispatcher.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/DummyAsyncContext.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/DynamicEnv.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/Effect.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/EvaluationHelpers.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/Evaluator.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/ExecutableExpression.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/FunctionRegistrar.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/FunctionResolver.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/FuturesInterpreter.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/GlobalContext.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/IdentifiedCompiledExpression.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/MessageProcessor.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/MessageProcessorAdapter.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/ProtoFieldAssignment.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/ResolverAdapter.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/StackOffsetFinder.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/StandardConstructs.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/StandardTypeResolver.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/TypeDirectedMessageProcessor.java delete mode 100644 legacy/java/dev/cel/legacy/runtime/async/TypeResolver.java delete mode 100644 legacy/javatests/dev/cel/legacy/runtime/async/AsyncInterpreterTest.java delete mode 100644 legacy/javatests/dev/cel/legacy/runtime/async/BUILD.bazel delete mode 100644 legacy/javatests/dev/cel/legacy/runtime/async/BaseInterpreterTest.java delete mode 100644 legacy/javatests/dev/cel/legacy/runtime/async/DynamicEnvTest.java delete mode 100644 legacy/javatests/dev/cel/legacy/runtime/async/Eval.java delete mode 100644 legacy/javatests/dev/cel/legacy/runtime/async/EvalAsync.java delete mode 100644 legacy/javatests/dev/cel/legacy/runtime/async/FuturesInterpreterTest.java delete mode 100644 legacy/javatests/dev/cel/legacy/runtime/async/FuturesInterpreterWithMessageProcessorTest.java diff --git a/common/src/main/java/dev/cel/common/types/CelKind.java b/common/src/main/java/dev/cel/common/types/CelKind.java index a97fe5b55..7d55ddaf1 100644 --- a/common/src/main/java/dev/cel/common/types/CelKind.java +++ b/common/src/main/java/dev/cel/common/types/CelKind.java @@ -32,7 +32,6 @@ public enum CelKind { BYTES, DOUBLE, DURATION, - FUNCTION, INT, LIST, MAP, diff --git a/common/src/test/java/dev/cel/common/types/CelKindTest.java b/common/src/test/java/dev/cel/common/types/CelKindTest.java index a29a3a1e3..68b487afa 100644 --- a/common/src/test/java/dev/cel/common/types/CelKindTest.java +++ b/common/src/test/java/dev/cel/common/types/CelKindTest.java @@ -40,7 +40,6 @@ public void isPrimitive_false() { assertThat(CelKind.DYN.isPrimitive()).isFalse(); assertThat(CelKind.ANY.isPrimitive()).isFalse(); assertThat(CelKind.DURATION.isPrimitive()).isFalse(); - assertThat(CelKind.FUNCTION.isPrimitive()).isFalse(); assertThat(CelKind.LIST.isPrimitive()).isFalse(); assertThat(CelKind.MAP.isPrimitive()).isFalse(); assertThat(CelKind.NULL_TYPE.isPrimitive()).isFalse(); diff --git a/legacy/BUILD.bazel b/legacy/BUILD.bazel deleted file mode 100644 index df22a6d7a..000000000 --- a/legacy/BUILD.bazel +++ /dev/null @@ -1,25 +0,0 @@ -load("@rules_java//java:java_library.bzl", "java_library") - -package( - default_applicable_licenses = ["//:license"], - default_visibility = [":legacy_users"], -) - -# See go/cel-java-migration-guide. This package is not accepting new clients. -package_group( - name = "legacy_users", - packages = [ - "//third_party/java/cel/legacy/...", - ], -) - -java_library( - name = "async_runtime", - exports = ["//third_party/java/cel/legacy/java/dev/cel/legacy/runtime/async"], -) - -java_library( - name = "dummy_async_context", - testonly = 1, - exports = ["//third_party/java/cel/legacy/java/dev/cel/legacy/runtime/async:dummy_async_context"], -) diff --git a/legacy/README.md b/legacy/README.md deleted file mode 100644 index b927206d2..000000000 --- a/legacy/README.md +++ /dev/null @@ -1,3 +0,0 @@ -This directory contains the deprecated CEL-Java packages that once resided in g3/jcg/api/tools/contract/runtime/interpreter. - -This package is no longer accepting any new clients, and the underlying codebase will not be open sourced. For existing clients, please migrate to the new fluent APIs (go/cel-java-migration-guide) \ No newline at end of file diff --git a/legacy/java/dev/cel/legacy/runtime/async/AsyncCanonicalResolver.java b/legacy/java/dev/cel/legacy/runtime/async/AsyncCanonicalResolver.java deleted file mode 100644 index c7df81607..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/AsyncCanonicalResolver.java +++ /dev/null @@ -1,19 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import com.google.common.util.concurrent.ListenableFuture; -import java.util.function.Supplier; - -/** - * An interface describing an object that can perform a lookup on a given name, returning a future - * computing the value associated with the so-named global variable. The value must be in canonical - * CEL runtime representation. - */ -public interface AsyncCanonicalResolver { - /** - * Resolves the given name to a future returning its value. Neither the returned supplier nor the - * supplied future can be null, and a value computed by the future must be in canonical CEL - * runtime representation (which also excludes null). Returns a failed future if the name is not - * bound or if the value cannot be represented canonically. - */ - Supplier> resolve(String name); -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/AsyncContext.java b/legacy/java/dev/cel/legacy/runtime/async/AsyncContext.java deleted file mode 100644 index 950e76845..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/AsyncContext.java +++ /dev/null @@ -1,68 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import com.google.common.context.Context; -import com.google.common.context.WithContext; -import com.google.common.util.concurrent.FluentFuture; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import java.util.Optional; -import java.util.concurrent.Executor; -import java.util.function.Supplier; - -/** - * Represents a snapshot of the "global context" that a context-dependent operations may reference. - */ -public interface AsyncContext { - - /** - * "Performs" an operation by obtaining the result future from the given supplier. May optimize - * by, e.g. memoization based on the given keys. (For example, the keys could list the name the - * overload ID of a strict context-dependent function together with a list of its actual - * arguments.) - */ - ListenableFuture perform( - Supplier> resultFutureSupplier, Object... keys); - - /** Retrieves the executor used for the futures-based computation expressed in CEL. */ - Executor executor(); - - default Optional requestContext() { - return Optional.empty(); - } - - /** - * Indicates that the evaluation is a runtime evaluation (as opposed to, e.g., constant-folding or - * other optimizations that occur during compile-time or preprocessing time). - */ - default boolean isRuntime() { - return true; - } - - /** - * Decouples the given future from whatever executor it is currently using and couples it to this - * context. Subsequent transformations that specify {@link MoreExecutors#directExecutor} will run - * on this context's executor. - */ - default ListenableFuture coupleToExecutor(ListenableFuture f) { - return FluentFuture.from(f) - .transform(x -> x, executor()) - .catchingAsync(Throwable.class, Futures::immediateFailedFuture, executor()); - } - - /** - * Runs the given supplier of a future within the request context, if any, and then couples the - * result to the executor. - */ - default ListenableFuture coupleToExecutorInRequestContext( - Supplier> futureSupplier) { - return coupleToExecutor( - requestContext() - .map( - c -> { - try (WithContext wc = WithContext.enter(c)) { - return futureSupplier.get(); - } - }) - .orElseGet(futureSupplier)); - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/AsyncDispatcher.java b/legacy/java/dev/cel/legacy/runtime/async/AsyncDispatcher.java deleted file mode 100644 index 4384a0106..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/AsyncDispatcher.java +++ /dev/null @@ -1,14 +0,0 @@ -package dev.cel.legacy.runtime.async; - -/** - * Interface to an object that combines a {@link FunctionRegistrar} with a corresponding {@link - * FunctionResolver}. - */ -public interface AsyncDispatcher extends FunctionRegistrar, FunctionResolver { - - /** - * Creates an independent copy of the current state of the dispatcher. Further updates to either - * the original or the forked copy do not affect the respective other. - */ - AsyncDispatcher fork(); -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/AsyncDispatcherBase.java b/legacy/java/dev/cel/legacy/runtime/async/AsyncDispatcherBase.java deleted file mode 100644 index 98f896cc1..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/AsyncDispatcherBase.java +++ /dev/null @@ -1,264 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static dev.cel.legacy.runtime.async.EvaluationHelpers.immediateException; -import static java.util.stream.Collectors.joining; - -import com.google.auto.value.AutoValue; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import com.google.protobuf.MessageLite; -import dev.cel.common.CelErrorCode; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.Metadata; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.annotation.Nullable; - -/** - * Base implementation of interface {@link AsyncDispatcher}. - * - *

A fresh {@link AsyncDispatcherBase} starts out empty, i.e., initially has no bindings at all. - */ -public class AsyncDispatcherBase implements AsyncDispatcher { - - // Mappings from overload IDs to either call constructors or runtime-overloadable - // (strict) functions. The demains of these two maps are maintained to be disjoint. - private final Map constructors; - private final Map overloads; - - /** Creates new empty registry. */ - public AsyncDispatcherBase() { - this.constructors = new HashMap<>(); - this.overloads = new HashMap<>(); - } - - /** Cloning constructor, for implementing snapshots. */ - protected AsyncDispatcherBase(AsyncDispatcherBase orig) { - this.constructors = new HashMap<>(orig.constructors); - this.overloads = new HashMap<>(orig.overloads); - } - - /** - * Adds a generic call constructor for the given overload ID. Function calls with this overload ID - * will later be handled by the given {@link CallConstructor}. No runtime overloading is possible. - */ - @Override - public void addCallConstructor(String overloadId, CallConstructor callConstructor) { - checkNotAlreadyBound(overloadId); - constructors.put(overloadId, callConstructor); - } - - /** Adds a strict function as one possible binding for a runtime-overloadable function. */ - @Override - public void addStrictFunction( - String overloadId, - List> argumentTypes, - boolean contextIndependent, - StrictFunction strictFunction) { - checkNotAlreadyBound(overloadId); - overloads.put( - overloadId, OverloadInfo.of(overloadId, argumentTypes, contextIndependent, strictFunction)); - } - - /** - * Constructs the compiled CEL expression that implements the call of a function at some call - * site. - * - *

If multiple overload IDs are given, then a runtime dispatch is implemented. All overload IDs - * must refer to strict functions in that case. - */ - // This lambda implements @Immutable interface 'StrictFunction', but 'List' is mutable - @SuppressWarnings("Immutable") - @Override - public CompiledExpression constructCall( - @Nullable Metadata metadata, - long exprId, - String functionName, - List overloadIds, - List compiledArguments, - MessageProcessor messageProcessor, - StackOffsetFinder stackOffsetFinder) - throws InterpreterException { - Preconditions.checkState(!overloadIds.isEmpty(), "no overloads for call of %s", functionName); - if (overloadIds.size() == 1) { - // Unique binding. - String overloadId = overloadIds.get(0); - if (constructors.containsKey(overloadId)) { - return constructors - .get(overloadId) - .construct(metadata, exprId, compiledArguments, messageProcessor, stackOffsetFinder); - } - } - - List candidates = new ArrayList<>(); - List unbound = new ArrayList<>(); - for (String overloadId : overloadIds) { - if (constructors.containsKey(overloadId)) { - throw new InterpreterException.Builder( - "incompatible overload for function '%s': %s must be resolved at compile time", - functionName, overloadId) - .setLocation(metadata, exprId) - .build(); - } else if (overloads.containsKey(overloadId)) { - candidates.add(overloads.get(overloadId)); - } else { - unbound.add(overloadId); - } - } - - if (!unbound.isEmpty()) { - throw new InterpreterException.Builder("no runtime binding for %s", String.join(",", unbound)) - .setLocation(metadata, exprId) - .build(); - } - - if (candidates.size() == 1) { - OverloadInfo overload = candidates.get(0); - return constructStrictCall( - overload.function(), - overload.overloadId(), - overload.contextIndependent(), - compiledArguments); - } - // Key for memoizing the overload dispatch itself. - String memoizationKey = candidates.stream().map(OverloadInfo::overloadId).collect(joining("|")); - boolean contextIndependent = candidates.stream().allMatch(OverloadInfo::contextIndependent); - return constructStrictCall( - (gctx, arguments) -> { - List matching = new ArrayList<>(); - for (OverloadInfo candidate : candidates) { - if (candidate.canHandle(arguments)) { - matching.add(candidate); - } - } - if (matching.isEmpty()) { - return immediateException( - new InterpreterException.Builder( - "No matching overload for function '%s'. Overload candidates: %s", - functionName, String.join(",", overloadIds)) - .setErrorCode(CelErrorCode.OVERLOAD_NOT_FOUND) - .setLocation(metadata, exprId) - .build()); - } - if (matching.size() > 1) { - return immediateException( - new InterpreterException.Builder( - "Ambiguous overloads for function '%s'. Matching candidates: %s", - functionName, - matching.stream().map(OverloadInfo::overloadId).collect(joining(","))) - .setErrorCode(CelErrorCode.AMBIGUOUS_OVERLOAD) - .setLocation(metadata, exprId) - .build()); - } - OverloadInfo match = matching.get(0); - return match.contextIndependent() - ? match.function().apply(gctx, arguments) - : gctx.context() - .perform( - () -> match.function().apply(gctx, arguments), match.overloadId(), arguments); - }, - memoizationKey, - contextIndependent, - compiledArguments); - } - - /** - * Constructs a call of a single strict function. This is a thin wrapper around {@link - * EvaluationHelpers#compileStrictCall}. - */ - private CompiledExpression constructStrictCall( - StrictFunction function, - String overloadId, - boolean contextIndependent, - List compiledArguments) - throws InterpreterException { - return EvaluationHelpers.compileStrictCall( - function, - overloadId, - contextIndependent ? Effect.CONTEXT_INDEPENDENT : Effect.CONTEXT_DEPENDENT, - compiledArguments); - } - - /** - * Creates an independent copy of the current state of the dispatcher. Further updates to either - * the original or the forked copy do not affect the respective other. - */ - @Override - public AsyncDispatcher fork() { - return new AsyncDispatcherBase(this); - } - - // Not to be overridden in subclasses! This method only checks whether it is locally - // (i.e., only with respect to constructors and overloads of this dispatcher) to add - // a new binding for overloadId. - private boolean isLocallyBound(String overloadId) { - return constructors.containsKey(overloadId) || overloads.containsKey(overloadId); - } - - /** - * Determines whether or not the given overload ID corresponds to a known function binding. - * Subclasses that provide additional bindings should override this. - */ - @Override - public boolean isBound(String overloadId) { - return isLocallyBound(overloadId); - } - - /** Helper for making sure that no overload ID is bound more than once. */ - private void checkNotAlreadyBound(String overloadId) { - Preconditions.checkState( - !isLocallyBound(overloadId), "More than one binding for %s.", overloadId); - } - - /** Helper class for storing information about a single overloadable strict function. */ - @AutoValue - abstract static class OverloadInfo { - /** The overload ID in question. */ - abstract String overloadId(); - - /** Java classes of the expected arguments. */ - abstract ImmutableList> argumentTypes(); - - /** True if the function is context-independent. */ - abstract boolean contextIndependent(); - - /** The function that is bound to the overload ID. */ - abstract StrictFunction function(); - - static OverloadInfo of( - String overloadId, - List> argumentTypes, - boolean contextIndependent, - StrictFunction function) { - return new AutoValue_AsyncDispatcherBase_OverloadInfo( - overloadId, ImmutableList.copyOf(argumentTypes), contextIndependent, function); - } - - /** Determines whether this overload can handle a call with the given actual arguments. */ - boolean canHandle(List arguments) { - int arity = argumentTypes().size(); - if (arity != arguments.size()) { - return false; - } - for (int i = 0; i < arity; ++i) { - if (!argMatchesType(arguments.get(i), argumentTypes().get(i))) { - return false; - } - } - return true; - } - - /** Helper for determining runtime argument type matches. */ - private static boolean argMatchesType(Object argument, Class parameterType) { - if (argument != null) { - return parameterType.isAssignableFrom(argument.getClass()); - } - // null can be assigned to messages, maps, and objects - return parameterType == Object.class - || MessageLite.class.isAssignableFrom(parameterType) - || Map.class.isAssignableFrom(parameterType); - } - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/AsyncGlobalResolver.java b/legacy/java/dev/cel/legacy/runtime/async/AsyncGlobalResolver.java deleted file mode 100644 index cf8d34017..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/AsyncGlobalResolver.java +++ /dev/null @@ -1,15 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import com.google.common.util.concurrent.ListenableFuture; - -/** - * An interface describing an object that can perform a lookup on a given name, returning a future - * computing the value associated with the so-named global variable. - */ -public interface AsyncGlobalResolver { - /** - * Resolves the given name to a future returning its value. The value returned from the future may - * be null, but the future itself must never be null. - */ - ListenableFuture resolve(String name); -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpretable.java b/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpretable.java deleted file mode 100644 index f656d1dcc..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpretable.java +++ /dev/null @@ -1,63 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.errorprone.annotations.Immutable; -import dev.cel.common.CelOptions; -import java.util.List; - -/** Represent an expression which can be interpreted repeatedly with varying global variables. */ -@Immutable -@FunctionalInterface -public interface AsyncInterpretable { - - /** - * Runs interpretation with the given list of local inputs using the given {@link GlobalContext}. - * The resolver within that context is a canonical resolver, so it supplies global name/value - * bindings in canonical CEL runtime representation. It is a precondition failure for the number - * of locals to not match the number of locals that were specified when the {@link - * AsyncInterpretable} was created. The executor given by the context is used to run any - * asynchronous aspects of the interpretation. Errors encountered during interpretation result in - * a failed future that throws an ExecutionException whose cause is an {@link - * InterpreterException} or a {@link RuntimeException} for things like division by zero etc. - */ - ListenableFuture evaluate( - GlobalContext globalContext, List> locals); - - /** - * Runs interpretation with the given list of local inputs as well as a resolver that supplies - * global name/value bindings. Globally resolved values do not have to be in canonical CEL runtime - * representation, but it must be possible to "adapt" them (i.e., coerce them into that - * representation). - * - *

It is a precondition failure for the number of locals to not match the number of locals that - * were specified when the {@link AsyncInterpretable} was created. The executor given by the - * context is used to run any asynchronous aspects of the interpretation. Errors encountered - * during interpretation result in a failed future that throws an ExecutionException whose cause - * is an {@link InterpreterException} or a {@link RuntimeException} for things like division by - * zero etc. - */ - default ListenableFuture eval( - AsyncContext context, AsyncGlobalResolver global, List> locals) { - // Use of the legacy features with ResolverAdapter is generally unsafe. In all known cases this - // method will be overridden such that the correct behavior occurs; however, this method should - // be treated with caution. - return evaluate( - GlobalContext.of(context, new ResolverAdapter(global, CelOptions.LEGACY)), locals); - } - - /** Backward-compatible convenience wrapper without locals. */ - default ListenableFuture eval(AsyncContext context, AsyncGlobalResolver global) { - return eval(context, global, ImmutableList.of()); - } - - /** - * Indicates whether or not this interpretable depends on context (reads global variables or calls - * other context-dependent functions). - * - *

Assumed to be context-dependent unless explicitly overridden. - */ - default Effect effect() { - return Effect.CONTEXT_DEPENDENT; - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpretableOrConstant.java b/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpretableOrConstant.java deleted file mode 100644 index 33a87b111..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpretableOrConstant.java +++ /dev/null @@ -1,81 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static com.google.common.util.concurrent.Futures.immediateFuture; - -import com.google.auto.value.AutoOneOf; -import com.google.common.util.concurrent.ListenableFuture; -import java.util.List; -import java.util.Optional; -import javax.annotation.Nullable; - -/** - * A tagged sum of either an {@link AsyncInterpretable} or a constant. - * - *

This is returned by the {@link AsyncInterpreter#createInterpretableOrConstant} method. - */ -@AutoOneOf(AsyncInterpretableOrConstant.Kind.class) -public abstract class AsyncInterpretableOrConstant { - /** - * Represents the choice between either a dynamic computation on the one hand or a compile-time - * constant on the other. - */ - public enum Kind { - INTERPRETABLE, - CONSTANT - } - - public abstract Kind getKind(); - - public abstract AsyncInterpretable interpretable(); - - static AsyncInterpretableOrConstant interpretable( - Effect effect, AsyncInterpretable interpretable) { - return AutoOneOf_AsyncInterpretableOrConstant.interpretable(decorate(effect, interpretable)); - } - - // Constant case uses Optional.empty to describe null. - public abstract Optional constant(); - - static AsyncInterpretableOrConstant constant(Optional constant) { - return AutoOneOf_AsyncInterpretableOrConstant.constant(constant); - } - - /** Recovers the plain constant (including the possibility of null). */ - @Nullable - public Object nullableConstant() { - return constant().orElse(null); - } - - /** - * Return an {@link AsyncInterpretable} regardless of kind (converting constants into the - * corresponding trivial interpretable). - */ - // This lambda implements @Immutable interface 'AsyncInterpretable', but accesses instance - // method(s) 'nullableConstant' on 'AsyncInterpretableOrConstant' which is not @Immutable. - @SuppressWarnings("Immutable") - public AsyncInterpretable toInterpretable() { - switch (getKind()) { - case CONSTANT: - return decorate( - Effect.CONTEXT_INDEPENDENT, (gctx, locals) -> immediateFuture(nullableConstant())); - case INTERPRETABLE: - return interpretable(); - } - throw new RuntimeException("[internal] unexpected kind in toInterpretable()"); - } - - private static AsyncInterpretable decorate(Effect newEffect, AsyncInterpretable interpretable) { - return new AsyncInterpretable() { - @Override - public ListenableFuture evaluate( - GlobalContext gctx, List> locals) { - return interpretable.evaluate(gctx, locals); - } - - @Override - public Effect effect() { - return newEffect; - } - }; - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpreter.java b/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpreter.java deleted file mode 100644 index 127376f5e..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/AsyncInterpreter.java +++ /dev/null @@ -1,32 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import dev.cel.expr.CheckedExpr; -import dev.cel.runtime.InterpreterException; -import java.util.List; -import java.util.Map; - -/** Interface to an asynchronous (futures-based) CEL interpreter. */ -public interface AsyncInterpreter { - - /** - * Creates an asynchronous interpretable for the given expression. - * - *

This method may run pre-processing and partial evaluation of the expression it gets passed. - */ - AsyncInterpretable createInterpretable(CheckedExpr checkedExpr) throws InterpreterException; - - /** - * Creates an asynchronous interpretable for the given expression (just like {@link - * #createInterpretable} above). If the compiler discovers that the expression describes a - * compile-time constant, then that constant's value is returned instead. Interpretable or - * constant are packaged up in an {@link AsyncInterpretableOrConstant}. - * - *

When resolving global identifiers, the given mapping from names to compile-time known - * constants is consulted first. Names not bound in this mapping are resolved at runtime. - */ - AsyncInterpretableOrConstant createInterpretableOrConstant( - CheckedExpr checkedExpr, - Map compileTimeConstants, - List localVariables) - throws InterpreterException; -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/BUILD.bazel b/legacy/java/dev/cel/legacy/runtime/async/BUILD.bazel deleted file mode 100644 index 938282294..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/BUILD.bazel +++ /dev/null @@ -1,83 +0,0 @@ -load("@rules_java//java:java_library.bzl", "java_library") - -package( - default_applicable_licenses = ["//:license"], - default_visibility = ["//third_party/java/cel/legacy:__pkg__"], -) - -java_library( - name = "async", - srcs = [ - "AsyncCanonicalResolver.java", - "AsyncContext.java", - "AsyncDispatcher.java", - "AsyncDispatcherBase.java", - "AsyncGlobalResolver.java", - "AsyncInterpretable.java", - "AsyncInterpretableOrConstant.java", - "AsyncInterpreter.java", - "Canonicalization.java", - "CompiledExpression.java", - "DefaultAsyncContext.java", - "DefaultAsyncDispatcher.java", - "DynamicEnv.java", - "Effect.java", - "EvaluationHelpers.java", - "Evaluator.java", - "ExecutableExpression.java", - "FunctionRegistrar.java", - "FunctionResolver.java", - "FuturesInterpreter.java", - "GlobalContext.java", - "IdentifiedCompiledExpression.java", - "MessageProcessor.java", - "MessageProcessorAdapter.java", - "ProtoFieldAssignment.java", - "ResolverAdapter.java", - "StackOffsetFinder.java", - "StandardConstructs.java", - "StandardTypeResolver.java", - "TypeDirectedMessageProcessor.java", - "TypeResolver.java", - ], - tags = [ - "alt_dep=//third_party/java/cel/legacy:async_runtime", - "avoid_dep", - ], - deps = [ - "//:auto_value", - "//checker:checker_legacy_environment", - "//common:error_codes", - "//common:features", - "//common:options", - "//common:proto_ast", - "//common:runtime_exception", - "//common/annotations", - "//common/types", - "//common/types:type_providers", - "//java/com/google/common/context", - "//java/com/google/protobuf/contrib:util", - "//java/com/google/protobuf/contrib/descriptor/pool", - "//runtime:interpreter", - "//runtime:runtime_helper", - "//third_party/java/jsr305_annotations", - "@cel_spec//proto/cel/expr:expr_java_proto", - "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", - "@maven//:com_google_re2j_re2j", - ], -) - -java_library( - name = "dummy_async_context", - testonly = 1, - srcs = ["DummyAsyncContext.java"], - tags = [ - "alt_dep=//third_party/java/cel/legacy:dummy_async_context", - "avoid_dep", - ], - deps = [ - ":async", - "@maven//:com_google_guava_guava", - ], -) diff --git a/legacy/java/dev/cel/legacy/runtime/async/Canonicalization.java b/legacy/java/dev/cel/legacy/runtime/async/Canonicalization.java deleted file mode 100644 index 468d260d8..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/Canonicalization.java +++ /dev/null @@ -1,348 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.primitives.UnsignedInts; -import com.google.common.primitives.UnsignedLong; -import com.google.errorprone.annotations.Immutable; -import com.google.protobuf.Any; -import com.google.protobuf.BoolValue; -import com.google.protobuf.BytesValue; -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Descriptors.EnumValueDescriptor; -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.DoubleValue; -import com.google.protobuf.DynamicMessage; -import com.google.protobuf.ExtensionRegistry; -import com.google.protobuf.FloatValue; -import com.google.protobuf.Int32Value; -import com.google.protobuf.Int64Value; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.ListValue; -import com.google.protobuf.MapEntry; -import com.google.protobuf.Message; -import com.google.protobuf.MessageFactories; -import com.google.protobuf.MessageOrBuilder; -import com.google.protobuf.Parser; -import com.google.protobuf.StringValue; -import com.google.protobuf.Struct; -import com.google.protobuf.UInt32Value; -import com.google.protobuf.UInt64Value; -import com.google.protobuf.Value; -import com.google.protobuf.contrib.AnyUtil; -import com.google.protobuf.contrib.descriptor.pool.GeneratedDescriptorPool; -import dev.cel.common.CelOptions; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Descriptor-directed value canonicalization. - * - *

This class provides tools for creating {@link Canonicalizer} instances. A {@code - * Canonicalizer} is the mechanism that translates values fetched from proto fields to canonical CEL - * runtime values. - */ -public final class Canonicalization { - - /** - * Represents the transformations from various proto values to their corresponding canonical CEL - * runtime representations. - */ - @Immutable - @FunctionalInterface - public interface Canonicalizer { - Object canonicalize(Object value); - } - - /** - * Returns the {@link Canonicalizer} for the field described by the give {@link FieldDescriptor}. - */ - public static Canonicalizer fieldValueCanonicalizer(FieldDescriptor fd, CelOptions celOptions) { - if (fd.isMapField()) { - return mapCanonicalizer(fd, celOptions); - } - if (fd.isRepeated()) { - return listCanonicalizer(fd, celOptions); - } - return singleFieldCanonicalizer(fd, celOptions); - } - - /** Returns the canonicalizer for a list field. */ - private static Canonicalizer listCanonicalizer(FieldDescriptor fd, CelOptions celOptions) { - Canonicalizer elementCanonicalizer = singleFieldCanonicalizer(fd, celOptions); - if (elementCanonicalizer == IDENTITY) { - return IDENTITY; - } - return l -> - Lists.transform((List) asInstanceOf(List.class, l), elementCanonicalizer::canonicalize); - } - - /** - * Returns the canonicalizer for a map field. It constructs an actual instance of {@link Map} from - * a list of map entries. The argument descriptor describes a map entry. Key and value descriptors - * can be obtained from it. - */ - @SuppressWarnings("unchecked") - private static Canonicalizer mapCanonicalizer(FieldDescriptor fd, CelOptions celOptions) { - Descriptor entryDescriptor = fd.getMessageType(); - FieldDescriptor keyDescriptor = entryDescriptor.findFieldByNumber(1); - FieldDescriptor valueDescriptor = entryDescriptor.findFieldByNumber(2); - Canonicalizer keyCanonicalizer = singleFieldCanonicalizer(keyDescriptor, celOptions); - Canonicalizer valueCanonicalizer = singleFieldCanonicalizer(valueDescriptor, celOptions); - // Map fields aren't fetched as native Java maps but as lists of map entries, so they cannot - // simply be transformed but must be built. In any case, even if they were maps, since there is - // no off-the-shelf transform that also translates keys, it would still be necessary to - // rebuild the map. (A general transform that translates keys would have to deal with - // key collisions, which is probably why no general mechanism exists.) - return entries -> { - Map map = new HashMap<>(); - for (MapEntry entry : (List>) entries) { - map.put( - keyCanonicalizer.canonicalize(entry.getKey()), - valueCanonicalizer.canonicalize(entry.getValue())); - } - return map; - }; - } - - /** - * Canonicalizer for individual values of non-map fields. - * - *

Returns {@code IDENTITY} if the canonicalizer is the identity function. (Using this specific - * instance rather than any old identity function makes it possible for callers to detect the - * situation, as is done in {@code fieldValueCanonicalizer()} above). - * - *

For repeated fields the fetched value is a {@link List} and the constructed {@link - * Canonicalizer} must be applied to each element. - * - *

For map fields two {@link Canonicalizer} instances must be created, one for the key and one - * for the value of a map entry, and they must subsequently be applied entry-by-entry to the - * entire map. - */ - private static Canonicalizer singleFieldCanonicalizer(FieldDescriptor fd, CelOptions celOptions) { - switch (fd.getType()) { - case SFIXED32: - case SINT32: - case INT32: - return value -> ((Number) value).longValue(); - case FIXED32: - case UINT32: - return value -> UnsignedInts.toLong(((Number) value).intValue()); - case ENUM: - return value -> (long) ((EnumValueDescriptor) value).getNumber(); - case FLOAT: - return value -> ((Number) value).doubleValue(); - case MESSAGE: - return protoCanonicalizer(fd.getMessageType(), celOptions); - default: - return IDENTITY; - } - } - - /** - * Returns the {@link Canonicalizer} for arbitrary proto messages. Most messages represent - * themselves, so canonicalization is the identity. This situation is indicated by returning the - * special {@code IDENTITY} canonicalizer. - * - *

Certain well-known proto types are treated specially: those representing JSON values and - * those representing wrapped values. - * - *

JSON values are recursively converted into Java {@link List} and {@link Map} types. - * Primitive JSON values are unwrapped into their canonical CEL (i.e., Java) equivalents. - * - *

Wrapped values are simply unwrapped. Notice that all floating point values are represented - * using {@link Double} and all fixed point values are represented using {@link Long}. - */ - private static Canonicalizer protoCanonicalizer(Descriptor d, CelOptions celOptions) { - Canonicalizer deDynamicalizer = deDynamicalizerFor(d); - switch (d.getFullName()) { - case "google.protobuf.Any": - return value -> - canonicalizeAny( - asInstanceOf(Any.class, deDynamicalizer.canonicalize(value)), celOptions); - case "google.protobuf.Value": - return value -> - canonicalizeJsonValue(asInstanceOf(Value.class, deDynamicalizer.canonicalize(value))); - case "google.protobuf.ListValue": - return value -> - canonicalizeJsonList( - asInstanceOf(ListValue.class, deDynamicalizer.canonicalize(value))); - case "google.protobuf.Struct": - return value -> - canonicalizeJsonStruct(asInstanceOf(Struct.class, deDynamicalizer.canonicalize(value))); - case "google.protobuf.Int64Value": - return value -> - asInstanceOf(Int64Value.class, deDynamicalizer.canonicalize(value)).getValue(); - case "google.protobuf.UInt64Value": - if (celOptions.enableUnsignedLongs()) { - return value -> - UnsignedLong.fromLongBits( - asInstanceOf(UInt64Value.class, deDynamicalizer.canonicalize(value)).getValue()); - } - return value -> - asInstanceOf(UInt64Value.class, deDynamicalizer.canonicalize(value)).getValue(); - case "google.protobuf.Int32Value": - return value -> - (long) asInstanceOf(Int32Value.class, deDynamicalizer.canonicalize(value)).getValue(); - case "google.protobuf.UInt32Value": - if (celOptions.enableUnsignedLongs()) { - return value -> - UnsignedLong.fromLongBits( - Integer.toUnsignedLong( - asInstanceOf(UInt32Value.class, deDynamicalizer.canonicalize(value)) - .getValue())); - } - return value -> - (long) asInstanceOf(UInt32Value.class, deDynamicalizer.canonicalize(value)).getValue(); - case "google.protobuf.DoubleValue": - return value -> - asInstanceOf(DoubleValue.class, deDynamicalizer.canonicalize(value)).getValue(); - case "google.protobuf.FloatValue": - return value -> - (double) asInstanceOf(FloatValue.class, deDynamicalizer.canonicalize(value)).getValue(); - case "google.protobuf.BoolValue": - return value -> - asInstanceOf(BoolValue.class, deDynamicalizer.canonicalize(value)).getValue(); - case "google.protobuf.StringValue": - return value -> - asInstanceOf(StringValue.class, deDynamicalizer.canonicalize(value)).getValue(); - case "google.protobuf.BytesValue": - return value -> - asInstanceOf(BytesValue.class, deDynamicalizer.canonicalize(value)).getValue(); - case "google.protobuf.Timestamp": - case "google.protobuf.Duration": - return deDynamicalizer; - default: - return IDENTITY; - } - } - - /** Converts an arbitrary message object into its canonical CEL equivalent. */ - public static Object canonicalizeProto(Message value, CelOptions celOptions) { - return protoCanonicalizer(value.getDescriptorForType(), celOptions).canonicalize(value); - } - - /** Converts an instance of {@link Any} into its canonical CEL equivalent. */ - private static Object canonicalizeAny(Any any, CelOptions celOptions) { - try { - return canonicalizeProto(AnyUtil.unpack(any), celOptions); - } catch (InvalidProtocolBufferException e) { - throw new IllegalArgumentException(e); - } - } - - /** Recursively converts a JSON {@link Value} into its canonical CEL equivalent. */ - private static Object canonicalizeJsonValue(Value v) { - switch (v.getKindCase()) { - case NULL_VALUE: - return v.getNullValue(); - case NUMBER_VALUE: - return v.getNumberValue(); - case STRING_VALUE: - return v.getStringValue(); - case BOOL_VALUE: - return v.getBoolValue(); - case STRUCT_VALUE: - return canonicalizeJsonStruct(v.getStructValue()); - case LIST_VALUE: - return canonicalizeJsonList(v.getListValue()); - default: - throw new IllegalArgumentException("Invalid JSON value type: " + v.getKindCase()); - } - } - - /** - * Converts a JSON {@link ListValue} into the corresponding canonical Java {@link List} by - * transforming all values recursively. - */ - private static Object canonicalizeJsonList(ListValue lv) { - return Lists.transform(lv.getValuesList(), Canonicalization::canonicalizeJsonValue); - } - - /** - * Converts a JSON {@link Struct} into the corresponding canonical Java {@link Map}. Keys are - * always strings, and values are converted recursively. - */ - private static Object canonicalizeJsonStruct(Struct s) { - return Maps.transformValues(s.getFieldsMap(), Canonicalization::canonicalizeJsonValue); - } - - /** - * Interprets the given value as an instance of the given class, throwing an exception if that - * cannot be done. - */ - static T asInstanceOf(Class clazz, Object value) { - if (clazz.isInstance(value)) { - return clazz.cast(value); - } - throw new IllegalStateException("[internal] not a value of " + clazz + ": " + value); - } - - /** Interprets the given value as an instance of {@link MessageOrBuilder}. */ - static MessageOrBuilder asMessage(Object value) { - return asInstanceOf(MessageOrBuilder.class, value); - } - - /** Determines whether the type of the described field is a wrapper type. */ - static boolean fieldHasWrapperType(FieldDescriptor fd) { - return fd.getType() == FieldDescriptor.Type.MESSAGE - && WRAPPER_TYPE_NAMES.contains(fd.getMessageType().getFullName()); - } - - /** - * Takes a {@link DynamicMessage} object and converts it into its corresponding generated message, - * if possible. - */ - // This lambda implements @Immutable interface 'Canonicalizer', but the declaration of type - // 'com.google.protobuf.Parser' is not annotated with - // @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - private static Canonicalizer deDynamicalizerFor(Descriptor d) { - String messageName = d.getFullName(); - Descriptor generatedDescriptor = - GeneratedDescriptorPool.getInstance().getDescriptorForTypeName(messageName); - if (generatedDescriptor == null) { - return IDENTITY; - } - Message prototype = - MessageFactories.getImmutableMessageFactory().getPrototype(generatedDescriptor); - if (prototype == null) { - return IDENTITY; - } - Parser parser = prototype.getParserForType(); - return object -> { - if (!(object instanceof DynamicMessage)) { - return object; - } - try { - return parser.parseFrom( - ((DynamicMessage) object).toByteArray(), ExtensionRegistry.getGeneratedRegistry()); - } catch (InvalidProtocolBufferException e) { - throw new AssertionError("Failed to convert DynamicMessage to " + messageName, e); - } - }; - } - - private static final ImmutableSet WRAPPER_TYPE_NAMES = - ImmutableSet.of( - "google.protobuf.BoolValue", - "google.protobuf.BytesValue", - "google.protobuf.DoubleValue", - "google.protobuf.FloatValue", - "google.protobuf.Int32Value", - "google.protobuf.Int64Value", - "google.protobuf.StringValue", - "google.protobuf.UInt32Value", - "google.protobuf.UInt64Value"); - - /** - * The identity canonicalizer. Return this value rather than an on-the-fly lambda when - * canonicalization does nothing. The identity of IDENTITY (pun intended) is used to short-circuit - * canonicalization on lists when the element canonicalizer is IDENTITY. - */ - private static final Canonicalizer IDENTITY = x -> x; - - private Canonicalization() {} -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/CompiledExpression.java b/legacy/java/dev/cel/legacy/runtime/async/CompiledExpression.java deleted file mode 100644 index de52fe305..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/CompiledExpression.java +++ /dev/null @@ -1,144 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static dev.cel.legacy.runtime.async.EvaluationHelpers.immediateException; -import static dev.cel.legacy.runtime.async.EvaluationHelpers.immediateValue; - -import com.google.auto.value.AutoOneOf; -import com.google.common.base.Pair; -import java.util.Optional; -import javax.annotation.Nullable; - -/** - * A compiled expression is either an {@link ExecutableExpression} (representing a residual - * computation that must be performed at runtime), or a compile-time constant, or an exception that - * is known to be thrown. The compile time constant is used during constant-folding (aka - * compile-time evaluation). - */ -@AutoOneOf(CompiledExpression.Kind.class) -public abstract class CompiledExpression { - /** Type discriminator for CompiledExpression. */ - public enum Kind { - EXECUTABLE_WITH_EFFECT, // Run-time residual computation. - COMPILED_CONSTANT, // Compile-time known value, using Optional to avoid null. - THROWING // Statically known to throw when executed. - } - - abstract Kind getKind(); - - public abstract Pair executableWithEffect(); - - public static CompiledExpression executable(ExecutableExpression expression, Effect effect) { - return AutoOneOf_CompiledExpression.executableWithEffect(Pair.of(expression, effect)); - } - - abstract Optional compiledConstant(); - - static CompiledExpression compiledConstant(Optional value) { - return AutoOneOf_CompiledExpression.compiledConstant(value); - } - - public abstract Throwable throwing(); - - public static CompiledExpression throwing(Throwable t) { - return AutoOneOf_CompiledExpression.throwing(t); - } - - /** Returns effect information. */ - public Effect effect() { - switch (getKind()) { - case EXECUTABLE_WITH_EFFECT: - return executableWithEffect().getSecond(); - default: - return Effect.CONTEXT_INDEPENDENT; - } - } - - /** Creates a constant expression directly from the nullable constant value. */ - public static CompiledExpression constant(@Nullable Object value) { - return compiledConstant(Optional.ofNullable(value)); - } - - /** Returns the actual constant value (which may be null). */ - @Nullable - public Object constant() { - return compiledConstant().orElse(null); - } - - /** Determise whether or not the expression represents a residual computation. */ - public boolean isExecutable() { - return getKind().equals(Kind.EXECUTABLE_WITH_EFFECT); - } - - /** Determines whether or not the expression represents a constant. */ - public boolean isConstant() { - return getKind().equals(Kind.COMPILED_CONSTANT); - } - - /** Determines whether or not the expression throws an exception. */ - public boolean isThrowing() { - return getKind().equals(Kind.THROWING); - } - - /** - * Maps the current expression to some result given three mapping functions, one for each case. - */ - public R map( - EffectMapping mappingForExecutable, - Mapping mappingForConstant, - Mapping mappingForThrowing) - throws E { - switch (getKind()) { - case EXECUTABLE_WITH_EFFECT: - return mappingForExecutable.map( - executableWithEffect().getFirst(), executableWithEffect().getSecond()); - case COMPILED_CONSTANT: - return mappingForConstant.map(constant()); - case THROWING: - return mappingForThrowing.map(throwing()); - } - throw unexpected("CompiledExpression#map"); - } - - /** Coerces the expression to an executable expression, preserving behavior. */ - // This lambda implements @Immutable interface 'ExecutableExpression', but 'Object' is mutable - @SuppressWarnings("Immutable") - public ExecutableExpression toExecutable() { - return map( - (exe, eff) -> exe, v -> stack -> immediateValue(v), t -> stack -> immediateException(t)); - } - - /** - * Maps the current expression to another CompiledExpressions given mappings for executables and - * constants. The mapping for throwing is the identity. - * - *

It must be the case that whenever the current expression throws an exception, the - * constructed expression will throw the same exception. - */ - public CompiledExpression mapNonThrowing( - Mapping mappingForExecutable, - Mapping mappingForConstant) - throws E { - return map( - (e, effect) -> executable(mappingForExecutable.map(e), effect), - mappingForConstant, - CompiledExpression::throwing); - } - - /** Represents a generic mapping from A to B, possibly throwing an E. */ - public interface Mapping { - B map(A argument) throws E; - } - - /** Like {@code Mapping} but carries an extra argument to convey effect information. */ - public interface EffectMapping { - B map(A argument, Effect effect) throws E; - } - - /** - * Creates a dummy exception to be thrown in places where we don't expect control to reach (but - * the Java compiler is not smart enough to know that). - */ - private static RuntimeException unexpected(String where) { - return new RuntimeException("[internal] reached unexpected program point: " + where); - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/DefaultAsyncContext.java b/legacy/java/dev/cel/legacy/runtime/async/DefaultAsyncContext.java deleted file mode 100644 index 98efe85f1..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/DefaultAsyncContext.java +++ /dev/null @@ -1,98 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static com.google.common.util.concurrent.Futures.immediateFailedFuture; - -import com.google.common.collect.ImmutableList; -import com.google.common.context.Context; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; -import javax.annotation.Nullable; - -/** Implements an {@link AsyncContext} using memoization. */ -public final class DefaultAsyncContext implements AsyncContext { - - private final ConcurrentMap, ListenableFuture> memoTable; - private final Executor executor; - private final Optional requestContext; - - public DefaultAsyncContext(Executor executor) { - // TODO: Tune initial capacity and concurrency level. - this.memoTable = new ConcurrentHashMap<>(); - this.executor = executor; - this.requestContext = Optional.empty(); - } - - public DefaultAsyncContext(Executor executor, Context requestContext) { - // TODO: Tune initial capacity and concurrency level. - this.memoTable = new ConcurrentHashMap<>(); - this.executor = executor; - this.requestContext = Optional.of(requestContext); - } - - @Override - public Optional requestContext() { - return requestContext; - } - - @Override - public ListenableFuture perform( - Supplier> futureSupplier, Object... keys) { - ImmutableList key = ImmutableList.copyOf(keys); - // If a new settable future is created by computeIfAbsent, it will be dropped - // into this reference so that it can subsequently be populated. - // See the comment below on why this cannot be done within the critical region. - AtomicReference> futureReference = new AtomicReference<>(); - ListenableFuture resultFuture = - typedFuture( - memoTable.computeIfAbsent( - key, - k -> { - SettableFuture settableFuture = SettableFuture.create(); - futureReference.set(settableFuture); - return settableFuture; - })); - // If the executor is directExecutor(), then pulling on the supplier must be - // done outside the table's critical region or deadlock can result. - // (This is mostly important for tests where the executor is in fact - // directExecutor(), but it is also a good defense for situations where - // production code accidentally uses a direct executor.) - @Nullable SettableFuture settableFuture = futureReference.get(); - if (settableFuture != null) { - // If the memo table already contained a settable future, - // then this branch will not be taken, thereby avoiding - // to pull on the future supplier and kicking off redundant - // work. - executor.execute(() -> settableFuture.setFuture(obtainSuppliedFuture(futureSupplier))); - } - return resultFuture; - } - - // Obtains the future from its supplier but captures exceptions thrown by the supplier itself, - // turning them into corresponding failed futures. - private static ListenableFuture obtainSuppliedFuture( - Supplier> futureSupplier) { - try { - return futureSupplier.get(); - } catch (Throwable e) { - return immediateFailedFuture(e); - } - } - - // "Recover" static type information that is lost during the round-trip through the - // memo table. Notice that this is only safe as long as there are no key collisions. - @SuppressWarnings("unchecked") - private static ListenableFuture typedFuture(ListenableFuture future) { - return (ListenableFuture) future; - } - - @Override - public Executor executor() { - return executor; - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/DefaultAsyncDispatcher.java b/legacy/java/dev/cel/legacy/runtime/async/DefaultAsyncDispatcher.java deleted file mode 100644 index 49c1a8cb3..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/DefaultAsyncDispatcher.java +++ /dev/null @@ -1,40 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import com.google.common.collect.ImmutableSet; -import dev.cel.common.CelOptions; -import dev.cel.common.ExprFeatures; -import dev.cel.runtime.StandardFunctions; - -/** Backward-compatible helper class for creating instances of {@link AsyncDispatcher}. */ -public final class DefaultAsyncDispatcher { - - /** - * Creates a new dispatcher with all standard functions using a provided type resolver and the - * provided custom set of {@link ExprFeatures} to enable various fixes and features. - * - *

It is recommended that callers supply {@link ExprFeatures#CURRENT} if they wish to - * automatically pick up fixes for CEL-Java conformance issues. - */ - public static AsyncDispatcher create( - TypeResolver typeResolver, ImmutableSet features) { - return create(typeResolver, CelOptions.fromExprFeatures(features)); - } - - public static AsyncDispatcher create(TypeResolver typeResolver, CelOptions celOptions) { - AsyncDispatcher dispatcher = new AsyncDispatcherBase(); - new StandardConstructs(typeResolver, celOptions).addAllTo(dispatcher); - StandardFunctions.addNonInlined(dispatcher, celOptions); - return dispatcher; - } - - /** Creates a new dispatcher with all standard functions and using the standard type resolver. */ - public static AsyncDispatcher create(ImmutableSet features) { - return create(CelOptions.fromExprFeatures(features)); - } - - public static AsyncDispatcher create(CelOptions celOptions) { - return create(StandardTypeResolver.getInstance(celOptions), celOptions); - } - - private DefaultAsyncDispatcher() {} -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/DummyAsyncContext.java b/legacy/java/dev/cel/legacy/runtime/async/DummyAsyncContext.java deleted file mode 100644 index 0b21f8f9e..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/DummyAsyncContext.java +++ /dev/null @@ -1,26 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static com.google.common.util.concurrent.MoreExecutors.directExecutor; - -import com.google.common.util.concurrent.ListenableFuture; -import java.util.concurrent.Executor; -import java.util.function.Supplier; - -/** - * Implements an {@link AsyncContext} without memoization and using the direct executor. For use in - * tests only. - */ -public enum DummyAsyncContext implements AsyncContext { - INSTANCE; - - @Override - public ListenableFuture perform( - Supplier> futureSupplier, Object... key) { - return futureSupplier.get(); - } - - @Override - public Executor executor() { - return directExecutor(); - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/DynamicEnv.java b/legacy/java/dev/cel/legacy/runtime/async/DynamicEnv.java deleted file mode 100644 index 4f7bd6060..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/DynamicEnv.java +++ /dev/null @@ -1,147 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static com.google.common.collect.ImmutableList.toImmutableList; - -import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.FluentFuture; -import com.google.common.util.concurrent.ListenableFuture; -import java.util.Arrays; -import java.util.List; -import java.util.function.Supplier; -import javax.annotation.Nullable; - -/** - * Dynamic environments combine the local context (aka a runtime stack) for local variables with the - * global context comprised of the resolver for global variables as well as the {@link AsyncContext} - * to be used during execution. The local context stores futures rather than plain objects, making - * it possible for comprehensions to treat individual steps lazily and allow for short-circuiting - * evaluations. - */ -public final class DynamicEnv { - - private final GlobalContext globalContext; - private final LocalContext localContext; - private final List globalValueNames; - private final ImmutableList>> cachedGlobals; - - /** Creates a dynamic environment by pairing the given global and local contexts. */ - private DynamicEnv( - GlobalContext globalContext, LocalContext localContext, List globalValueNames) { - this.globalContext = globalContext; - this.localContext = localContext; - this.globalValueNames = globalValueNames; - this.cachedGlobals = - globalValueNames.stream().map(globalContext::resolve).collect(toImmutableList()); - } - - /** Creates a fresh dynamic environment and populates the stack with the given locals. */ - DynamicEnv( - GlobalContext globalContext, - List> locals, - List globalValueNames) { - this(globalContext, LocalContext.create(locals), globalValueNames); - } - - /** Creates a fresh dynamic environment from the given global context and no locals. */ - DynamicEnv(GlobalContext globalContext, List globalValueNames) { - this(globalContext, LocalContext.create(ImmutableList.of()), globalValueNames); - } - - /** Implements the runtime stack for local bindings. */ - private static final class LocalContext { - @Nullable final LocalContext parent; - private final ImmutableList> slots; - - /** - * Effectively clones the parent and creates a new frame into which bindings from the given - * locals are installed. - */ - LocalContext(@Nullable LocalContext parent, List> locals) { - this.parent = parent; - this.slots = locals.stream().map(FluentFuture::from).collect(toImmutableList()); - } - - /** - * Returns a context with just the given local bindings. In the degenerate case where the - * bindings are empty, the result is null. - */ - @Nullable - static LocalContext create(List> locals) { - return locals.isEmpty() ? null : new LocalContext(null, locals); - } - - /** - * Retrieves slot that is offset elements away from the top of the stack. Since the top of the - * stack (corresponding to offset 0) is not a slot itself, valid slot numbers start at 1. - */ - FluentFuture getAtSlotOffset(int offset) { - int numSlots = slots.size(); - // An implicit invariant of the compilation algorithm is that parent is guaranteed - // to be non-null when offset exceeds numSlots. The value numSlots describes the number - // "locally" available slots (within the current frame). The global invariant is that - // a dynamic environment (a "stack") always has sufficiently many slots in its local context - // so that calls of getAtSlotOffset(...) can be satisfied. This implies that when the - // top-most frame does not have enough slots, then these slots must exist in the parent - - // which therefore cannot be null in that case. - return offset > numSlots - ? parent.getAtSlotOffset(offset - numSlots) - : slots.get(numSlots - offset); - } - } - - /** - * Clones the current environment and extends the result with the given values corresponding to - * local variables by pushing them onto the stack. - */ - public DynamicEnv extend(FluentFuture... futures) { - return new DynamicEnv( - globalContext, new LocalContext(localContext, Arrays.asList(futures)), globalValueNames); - } - - /** Obtains the value of a global variable by invoking the resolver-provided supplier. */ - FluentFuture getGlobal(int index) { - return FluentFuture.from(cachedGlobals.get(index).get()); - } - - /** - * Obtains the value of a local variable by accessing the given stack location relative to the - * current top of the stack. - */ - public FluentFuture getLocalAtSlotOffset(int offset) { - return localContext.getAtSlotOffset(offset); - } - - /** - * Clones the stack of this dynamic environment while substituting the global context given by the - * arguments. The result will only have a single frame containing all bindings. - * - *

Background: A {@link DynamicEnv} instance can be viewed as pairing the stack for local - * bindings on the one hand with the global state (which includes global bindings as well as the - * execution context) on the other. - * - *

Most of the time the global state remains fixed while the stack changes according to how the - * flow of execution traverses the local binding structure of the program. But in some situation - * it is useful to keep local bindings fixed while substituting a different global context. This - * method provides the mechanism for that. - * - *

Notice that the current global references must be retained. - */ - public DynamicEnv withGlobalContext(GlobalContext otherGlobalContext) { - return new DynamicEnv(otherGlobalContext, localContext, globalValueNames); - } - - /** Provides access to the global context. */ - public GlobalContext globalContext() { - return globalContext; - } - - /** Provides access to the context. */ - public AsyncContext currentContext() { - return globalContext().context(); - } - - /** Provides access to the global resolver itself. */ - public AsyncCanonicalResolver globalResolver() { - return globalContext().resolver(); - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/Effect.java b/legacy/java/dev/cel/legacy/runtime/async/Effect.java deleted file mode 100644 index ea02c99cb..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/Effect.java +++ /dev/null @@ -1,42 +0,0 @@ -package dev.cel.legacy.runtime.async; - -/** - * Represents the effect that a CEL computation has. - * - *

Context-independent computations are close approximations of mathematical expressions which - * denote one particular value. Context-independent functions yield equal results when applied to - * equal argumnents. These computations are marked as {@link Effect#CONTEXT_INDEPENDENT}. - * - *

Context-dependent computations implicitly reference the "current state of the world". They are - * not permitted to change the state of the world, but their value may depend on it. An optimizer - * may drop unneeded context-dependent sub-expressions It is even permissible to reorder calls and - * to perform common subexpression elimination involving context-dependent computations (under the - * assumption that the relevant parts of the state of the world do not change during an invocation - * of {@link AsyncInterpretable#eval}). However, evaluation must not be moved from runtime to - * compile time. These computations are marked as {@link Effect#CONTEXT_DEPENDENT}. - */ -public enum Effect { - // No effect, independent of context. May be compile-time evaluated. - CONTEXT_INDEPENDENT { - @Override - public Effect meet(Effect other) { - return other; - } - }, - // Has read effects on the context but must otherwise be pure. Must not be compile-time - // evaluated, but can be subject to reordering, CSE, elimination of unused computations, - // memoization (within one activation context), etc. - CONTEXT_DEPENDENT { - @Override - public Effect meet(Effect other) { - return this; - } - }; - - /** - * Combines effects, taking the greatest lower bound. This is based on a view where - * DEFERRING_CONTEXT_INDEPENDENT is lower than CONTEXT_INDEPENDENT and CONTEXT_DEPENDENT is lower - * than both. - */ - public abstract Effect meet(Effect other); -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/EvaluationHelpers.java b/legacy/java/dev/cel/legacy/runtime/async/EvaluationHelpers.java deleted file mode 100644 index bd8375591..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/EvaluationHelpers.java +++ /dev/null @@ -1,335 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.util.concurrent.Futures.immediateFailedFuture; -import static com.google.common.util.concurrent.Futures.immediateFuture; -import static com.google.common.util.concurrent.MoreExecutors.directExecutor; - -import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.FluentFuture; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import dev.cel.common.CelErrorCode; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.Metadata; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.function.Function; -import java.util.function.Supplier; - -/** - * A collection of helper methods. These provide the implementations of all default (convenience) - * methods in {@link FunctionResolver} and can also be used in custom registrars and resolvers. - */ -public final class EvaluationHelpers { - - /** - * Applies constant folding of a context-independent strict function when all its arguments are - * constants themselves. - */ - private static CompiledExpression constantfoldStrictCall( - FunctionRegistrar.StrictFunction function, List compiledArguments) { - return compiledConstantOrThrowing( - () -> - Futures.getDone( - function.apply( - COMPILE_TIME_GLOBAL_CONTEXT, transform(compiledArguments, e -> e.constant())))); - } - - /** - * Constructs the call of a strict function. Context-independent functions are simply applied, - * while context-dependent function calls are memoized using the {@link AsyncContext#perform} - * method. - */ - // This lambda implements @Immutable interface 'ExecutableExpression', but 'List' is mutable - @SuppressWarnings("Immutable") - private static CompiledExpression constructStrictCall( - FunctionRegistrar.StrictFunction function, - String memoizationKey, - Effect functionEffect, - List compiledArguments) { - List executableArguments = new ArrayList<>(); - Effect effect = functionEffect; - for (CompiledExpression argument : compiledArguments) { - if (argument.isThrowing()) { - return argument; - } - executableArguments.add(argument.toExecutable()); - effect = effect.meet(argument.effect()); - } - if (functionEffect.equals(Effect.CONTEXT_INDEPENDENT)) { - return CompiledExpression.executable( - stack -> - execAllAsList(executableArguments, stack) - .transformAsync( - arguments -> function.apply(stack.globalContext(), arguments), - directExecutor()), - effect); - } - return CompiledExpression.executable( - stack -> - execAllAsList(executableArguments, stack) - .transformAsync( - arguments -> - stack - .currentContext() - .perform( - () -> function.apply(stack.globalContext(), arguments), - memoizationKey, - arguments), - directExecutor()), - effect); - } - - /** - * Compiles the call of an overloadable function by either constant-folding it or by constructing - * the residual runtime call. - */ - public static CompiledExpression compileStrictCall( - FunctionRegistrar.StrictFunction function, - String memoizationKey, - Effect functionEffect, - List identifiedCompiledArguments) - throws InterpreterException { - List compiledArguments = new ArrayList<>(); - boolean allConstant = true; - for (var ca : identifiedCompiledArguments) { - CompiledExpression e = ca.expression(); - compiledArguments.add(e); - if (!e.isConstant()) { - allConstant = false; - } - } - return functionEffect.equals(Effect.CONTEXT_INDEPENDENT) && allConstant - ? constantfoldStrictCall(function, compiledArguments) - : constructStrictCall(function, memoizationKey, functionEffect, compiledArguments); - } - - /** - * Attempts to constant-fold a call of a context-independent "no-barrier" function. Returns an - * empty result if the call needed the value of one of its non-constant arguments. - */ - private static CompiledExpression constantfoldNobarrierCall( - FunctionRegistrar.NobarrierFunction function, List compiledArguments) { - return compiledConstantOrThrowing( - () -> - Futures.getDone( - function.apply( - COMPILE_TIME_GLOBAL_CONTEXT, - transform(compiledArguments, e -> getStaticArgumentFuture(e))))); - } - - /** Constructs the call of a "no-barrier" function. */ - // This lambda implements @Immutable interface 'ExecutableExpression', but 'List' is mutable - @SuppressWarnings("Immutable") - private static CompiledExpression constructNobarrierCall( - FunctionRegistrar.NobarrierFunction function, - Effect functionEffect, - List compiledArguments) { - Effect effect = functionEffect; - List executableArguments = new ArrayList<>(); - for (CompiledExpression argument : compiledArguments) { - executableArguments.add(argument.toExecutable()); - effect = effect.meet(argument.effect()); - } - return CompiledExpression.executable( - stack -> - FluentFuture.from( - function.apply( - stack.globalContext(), - transform(executableArguments, arg -> arg.execute(stack)))), - effect); - } - - /** - * Compiles the call of a "no-barrier" function by either constant-folding it or by constructing - * the residual runtime call. - */ - public static CompiledExpression compileNobarrierCall( - FunctionRegistrar.NobarrierFunction function, - Effect functionEffect, - List identifiedCompiledArguments) - throws InterpreterException { - List compiledArguments = new ArrayList<>(); - boolean allStatic = true; - for (var ia : identifiedCompiledArguments) { - CompiledExpression e = ia.expression(); - compiledArguments.add(e); - if (e.isExecutable()) { - allStatic = false; - } - } - return functionEffect.equals(Effect.CONTEXT_INDEPENDENT) && allStatic - ? constantfoldNobarrierCall(function, compiledArguments) - : constructNobarrierCall(function, functionEffect, compiledArguments); - } - - /** Implements immediateFuture for {@link FluentFuture}s. */ - public static FluentFuture immediateValue(A a) { - return FluentFuture.from(immediateFuture(a)); - } - - /** Implements immediateFailedFuture for {@link FluentFuture}s. */ - public static FluentFuture immediateException(Throwable t) { - return FluentFuture.from(immediateFailedFuture(t)); - } - - /** - * Transforms a list. Unlike {@link Lists#transform} This does not produce a transformed "view" of - * the original. Instead, the result is stored in an immutable list and does not update itself - * should the input get updated later. - */ - public static ImmutableList transform(List input, Function function) { - return input.stream().map(function).collect(toImmutableList()); - } - - /** - * Turns the given list of futures into a future of a list. If at least one of the futures fails, - * the result future fails with the exception of the earliest (lowes-index) failing input future. - */ - public static ListenableFuture> allAsListOrFirstException( - Iterable> futures) { - return buildListOrFirstException(futures.iterator(), ImmutableList.builder()); - } - - /** Helper for {@link #allAsListOrFirstException}. */ - private static ListenableFuture> buildListOrFirstException( - Iterator> futures, ImmutableList.Builder builder) { - if (!futures.hasNext()) { - return immediateFuture(builder.build()); - } - return FluentFuture.from(futures.next()) - .transformAsync(v -> buildListOrFirstException(futures, builder.add(v)), directExecutor()); - } - - /** Executes all expressions and arranges for the results to be combined into a single list. */ - public static FluentFuture> execAllAsList( - List executables, DynamicEnv stack) { - return FluentFuture.from( - allAsListOrFirstException(transform(executables, exp -> exp.execute(stack)))); - } - - /** - * Creates a constant compiled expression from the supplied constant. If the supplier throws an - * exception, a throwing expression is created instead. - */ - public static CompiledExpression compiledConstantOrThrowing(Callable supplier) { - try { - return CompiledExpression.constant(supplier.call()); - } catch (ExecutionException execExn) { - return CompiledExpression.throwing(execExn.getCause()); - } catch (Exception exn) { - return CompiledExpression.throwing(exn); - } - } - - /** Enforces that the given value is a boolean. Throws an explanatory exception otherwise. */ - public static Object expectBoolean(Object b, Metadata metadata, long id) - throws InterpreterException { - if (b instanceof Boolean) { - return b; - } - throw new InterpreterException.Builder("expected boolean value, found: %s", b) - .setErrorCode(CelErrorCode.INVALID_ARGUMENT) - .setLocation(metadata, id) - .build(); - } - - /** Extracts a boolean value. */ - public static boolean asBoolean(Object value) { - return (value instanceof Boolean) && (boolean) value; - } - - /** - * Decorates the given expression to account for the fact that it is expected to compute a boolean - * result. - */ - public static CompiledExpression asBooleanExpression( - CompiledExpression e, Metadata metadata, long id) throws InterpreterException { - return e.map( - (exe, eff) -> - CompiledExpression.executable( - stack -> - exe.execute(stack) - .transformAsync( - obj -> immediateValue(expectBoolean(obj, metadata, id)), - directExecutor()), - eff), - c -> CompiledExpression.constant(expectBoolean(c, metadata, id)), - t -> CompiledExpression.throwing(t)); - } - - /** - * Runs the given {@link ExecutableExpression} with an empty stack and a dummy global context, - * constructing a {@link CompiledExpression} from the resulting value or exception. - * - *

Note: Using this method requires that the expression did not occur within the scope of any - * local bindings, and that it does not make use of the global context during execution - either - * by accessing global variables or by invoking functions that are context-sensitive. - * - *

Not accessing global variables during execution does not mean that the expression - * does not mention any global variables, though. In particular, this can happen when parts - * or all of the expression represent a "suspended" computation. The list of global references - * must contain all mentioned global variables at their correct positions. - */ - static CompiledExpression executeStatically( - ExecutableExpression executable, List globalReferences) { - return compiledConstantOrThrowing( - () -> - Futures.getDone( - executable.execute(new DynamicEnv(COMPILE_TIME_GLOBAL_CONTEXT, globalReferences)))); - } - - /** - * Returns a future of the value or exception corresponding to a constant or throwing computation. - */ - private static FluentFuture getStaticArgumentFuture(CompiledExpression compiled) { - if (compiled.isExecutable()) { - throw new IllegalStateException("non-static argument during constant-folding"); - } - if (compiled.isConstant()) { - return immediateValue(compiled.constant()); - } - return immediateException(compiled.throwing()); - } - - /** Special resolver for global variables during constant folding. */ - private static Supplier> indicateNeededGlobal(String name) { - return () -> { - throw new IllegalStateException("access to global variable during constant folding: " + name); - }; - } - - /** AsyncContext used for compile-time computations. Uses no memoization and a direct executor. */ - private static final AsyncContext COMPILE_TIME_ASYNC_CONTEXT = - new AsyncContext() { - @Override - public ListenableFuture perform( - Supplier> resultFutureSupplier, Object... keys) { - return resultFutureSupplier.get(); - } - - @Override - public Executor executor() { - return directExecutor(); - } - - @Override - public boolean isRuntime() { - return false; - } - }; - - /** - * {@link GlobalContext} used for compile-time computations. Uses no memoization, a direct - * executor, and access to global variables results in an {@link IllegalStateException}. - */ - public static final GlobalContext COMPILE_TIME_GLOBAL_CONTEXT = - GlobalContext.of(COMPILE_TIME_ASYNC_CONTEXT, EvaluationHelpers::indicateNeededGlobal); - - private EvaluationHelpers() {} -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/Evaluator.java b/legacy/java/dev/cel/legacy/runtime/async/Evaluator.java deleted file mode 100644 index 7c2cc5758..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/Evaluator.java +++ /dev/null @@ -1,1232 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static com.google.common.util.concurrent.MoreExecutors.directExecutor; -import static com.google.common.util.concurrent.MoreExecutors.newSequentialExecutor; -import static dev.cel.legacy.runtime.async.Effect.CONTEXT_INDEPENDENT; -import static dev.cel.legacy.runtime.async.EvaluationHelpers.allAsListOrFirstException; -import static dev.cel.legacy.runtime.async.EvaluationHelpers.asBoolean; -import static dev.cel.legacy.runtime.async.EvaluationHelpers.compiledConstantOrThrowing; -import static dev.cel.legacy.runtime.async.EvaluationHelpers.execAllAsList; -import static dev.cel.legacy.runtime.async.EvaluationHelpers.executeStatically; -import static dev.cel.legacy.runtime.async.EvaluationHelpers.expectBoolean; -import static dev.cel.legacy.runtime.async.EvaluationHelpers.immediateException; -import static dev.cel.legacy.runtime.async.EvaluationHelpers.immediateValue; -import static dev.cel.legacy.runtime.async.EvaluationHelpers.transform; - -import dev.cel.expr.CheckedExpr; -import dev.cel.expr.Constant; -import dev.cel.expr.Expr; -import dev.cel.expr.Expr.Call; -import dev.cel.expr.Expr.Comprehension; -import dev.cel.expr.Expr.CreateList; -import dev.cel.expr.Expr.CreateStruct; -import dev.cel.expr.Expr.Select; -import dev.cel.expr.Reference; -import dev.cel.expr.Type; -import dev.cel.expr.Type.TypeKindCase; -import dev.cel.expr.Value; -import com.google.auto.value.AutoOneOf; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; -import com.google.common.primitives.UnsignedLong; -import com.google.common.util.concurrent.FluentFuture; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.errorprone.annotations.Immutable; -import dev.cel.checker.Types; -import dev.cel.common.CelErrorCode; -import dev.cel.common.CelOptions; -import dev.cel.common.CelProtoAbstractSyntaxTree; -import dev.cel.runtime.DefaultMetadata; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.Metadata; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.Executor; -import javax.annotation.Nullable; - -/** - * Implementation of an optimizing CEL interpreter based on futures. - * - *

Evaluation is split into two phases: preparation and execution. - * - *

The preparation phase traverses the abstract syntax, performs optimizations, and produces an - * {@link AsyncInterpretable}. - * - *

The execution phase consists of invocations of {@link AsyncInterpretable#evaluate} or {@link - * AsyncInterpretable#eval}. It is expected that one preparation is amortized over many executions. - * - *

Optimizations include: - * - *

    - *
  • Elimination of interpretative overhead. During execution the interpretable does not inspect - * the abstract syntax anymore. Control flow is directly baked into the interpretable. - *
  • Constant-time access to local variables by stack positions. - *
  • Compile-time resolution of function bindings from the dispatcher whenever the relevant - * overload is determined uniquely by type-checking. - *
  • Compile-time evaluation ("constant folding") of any part of the expression that does not - * depend on global variables or context-dependent functions. Calls of context-independent - * functions with constant arguments are evaluated at compile time. - *
  • Compile-time boolean short-circuiting and elimination of unreachable branches when - * conditions are known constants (based on the above constant-folding mechanism). - *
  • Elimination of code that is unreachable to do static knowledge about thrown exceptions. - *
  • Compile-time execution of comprehension loops with statically known inputs, applying - * constant-folding, short-circuiting, and elimination of unreachable code along the way. - *
- */ -public class Evaluator implements AsyncInterpreter { - - private final TypeResolver typeResolver; - private final MessageProcessor messageProcessor; - private final FunctionResolver functionResolver; - private final boolean errorOnDuplicateKeys; - private final CelOptions celOptions; - - /** Standard constructor. */ - public Evaluator( - TypeResolver typeResolver, - MessageProcessor messageProcessor, - FunctionResolver functionResolver, - CelOptions celOptions) { - this.typeResolver = typeResolver; - this.messageProcessor = messageProcessor; - this.functionResolver = functionResolver; - this.celOptions = celOptions; - this.errorOnDuplicateKeys = this.celOptions.errorOnDuplicateMapKeys(); - } - - @Override - public AsyncInterpretable createInterpretable(CheckedExpr checkedExpr) - throws InterpreterException { - return createInterpretableOrConstant(checkedExpr, ImmutableMap.of(), ImmutableList.of()) - .toInterpretable(); - } - - // This lambda implements @Immutable interface 'AsyncInterpretable', but 'List' is mutable - @SuppressWarnings("Immutable") - @Override - public AsyncInterpretableOrConstant createInterpretableOrConstant( - CheckedExpr checkedExpr, - Map compileTimeConstants, - List localVariables) - throws InterpreterException { - int expectedNumLocals = localVariables.size(); - Compilation compilation = new Compilation(checkedExpr); - CompiledExpression compiled = - compilation.compiledExpression(compileTimeConstants, localVariables); - List globalReferences = compilation.globalReferences(); - return compiled.map( - (e, effect) -> - AsyncInterpretableOrConstant.interpretable( - effect, - new AsyncInterpretableWithFeatures( - (gctx, locals) -> { - Preconditions.checkArgument(locals.size() == expectedNumLocals); - return e.execute(new DynamicEnv(gctx, locals, globalReferences)); - }, - celOptions)), - c -> AsyncInterpretableOrConstant.constant(Optional.ofNullable(c)), - t -> - AsyncInterpretableOrConstant.interpretable( - Effect.CONTEXT_INDEPENDENT, - new AsyncInterpretableWithFeatures( - (gctx, locals) -> immediateException(t), celOptions))); - } - - /** - * This {@code AsyncInterpretable} implementation ensures that {@code CelOptions} are propagated - * to type-conversion classes used during async interpretation. - */ - @Immutable - private static class AsyncInterpretableWithFeatures implements AsyncInterpretable { - - private final AsyncInterpretable delegate; - private final CelOptions celOptions; - - private AsyncInterpretableWithFeatures(AsyncInterpretable delegate, CelOptions celOptions) { - this.delegate = delegate; - this.celOptions = celOptions; - } - - @Override - public ListenableFuture evaluate( - GlobalContext gctx, List> locals) { - return delegate.evaluate(gctx, locals); - } - - @Override - public ListenableFuture eval( - AsyncContext context, AsyncGlobalResolver global, List> locals) { - return evaluate(GlobalContext.of(context, new ResolverAdapter(global, celOptions)), locals); - } - } - - /** - * Represents the binding of a local variable at compile time. A binding can either be a slot - * number (i.e., a position within the runtime stack) or a concrete value that is known due to - * constant folding. - */ - @AutoOneOf(StaticBinding.Kind.class) - abstract static class StaticBinding { - enum Kind { - SLOT, // Stack slots used at runtime. - BOUND_VALUE // Compile-time known binding to value. Using Optional to avoid null. - } - - abstract Kind getKind(); - - abstract int slot(); - - static StaticBinding slot(int n) { - return AutoOneOf_Evaluator_StaticBinding.slot(n); - } - - abstract Optional boundValue(); - - static StaticBinding boundValue(Optional v) { - return AutoOneOf_Evaluator_StaticBinding.boundValue(v); - } - - /** Returns the actual bound value (which may be null). */ - @Nullable - Object value() { - return boundValue().orElse(null); - } - - /** Constructs a value binding directly from the nullable value. */ - static StaticBinding value(@Nullable Object v) { - return boundValue(Optional.ofNullable(v)); - } - } - - /** Static environments map variable names to their stack positions or to known values. */ - private class StaticEnv { - - private final Map bindings; - private int numSlots; - - /** Creates an empty environment that maps only the given compile-time constants. */ - public StaticEnv(Map compileTimeConstants, List localVariables) { - this.bindings = new HashMap<>(); - for (Map.Entry c : compileTimeConstants.entrySet()) { - this.bindings.put(c.getKey(), StaticBinding.value(c.getValue())); - } - this.numSlots = 0; - for (String localVariable : localVariables) { - this.bindings.put(localVariable, StaticBinding.slot(this.numSlots++)); - } - } - - /** Clones the current environment. */ - private StaticEnv(StaticEnv parent) { - this.bindings = new HashMap<>(parent.bindings); - this.numSlots = parent.numSlots; - } - - /** Adds a slot binding for the given name. The slot is the next available slot on the stack. */ - private void push(String name) { - bindings.put(name, StaticBinding.slot(numSlots++)); - } - - /** Adds a value binding for the given name. This does not take up a stack slot at runtime. */ - private void bind(String name, @Nullable Object value) { - bindings.put(name, StaticBinding.value(value)); - } - - /** - * Clones the current environment and extends the result with slots for the given names, from - * left to right. - */ - public StaticEnv extendWithSlots(String... names) { - StaticEnv result = new StaticEnv(this); - for (String name : names) { - result.push(name); - } - return result; - } - - /** Clones the current environment and binds the given name to the given value in the result. */ - public StaticEnv extendWithValue(String name, @Nullable Object value) { - StaticEnv result = new StaticEnv(this); - result.bind(name, value); - return result; - } - - /** Determines whether the given name is currently mapped to a binding. */ - public boolean isInScope(String name) { - return bindings.containsKey(name); - } - - /** Returns the binding corresponding to the given name, or -1 if it is not in scope. */ - public StaticBinding bindingOf(String name) { - return bindings.get(name); - } - - /** - * Returns the slot number corresponding to the current top of the stack. - * - *

Let se be the static environment and de be its corresponding dynamic environment. If a - * binding in se is a slot s, then the corresponding runtime value will be at - * de.getLocalAtSlotOffset(se.top() - s). - */ - public int top() { - return numSlots; - } - - /** - * Returns the numeric stack offset for the named local variable or otherwise throws an {@link - * InterpreterException}. - */ - public int findStackOffset(@Nullable Metadata metadata, long exprId, String name) - throws InterpreterException { - @Nullable StaticBinding binding = bindingOf(name); - if (binding != null && binding.getKind() == StaticBinding.Kind.SLOT) { - return top() - binding.slot(); - } - throw new InterpreterException.Builder("no stack slot named %s", name) - .setLocation(metadata, exprId) - .build(); - } - } - - /** - * The compilation class encapsulates the compilation step from CEL expressions to {@link - * ExecutableExpression}s. Creating an object of this class initializes the compiler. A subsequent - * call of compiledExpression() performs the actual compilation and returns the {@link - * CompiledExpression}. - */ - private class Compilation { - - private final CheckedExpr checkedExpr; - private final Metadata metadata; - - // Accumulates the list of global variables that are referenced by the code. - private final List globalReferences = new ArrayList<>(); - - /** Initialize the compiler by creating the Compilation. */ - Compilation(CheckedExpr checkedExpr) { - this.checkedExpr = Preconditions.checkNotNull(checkedExpr); - this.metadata = - new DefaultMetadata(CelProtoAbstractSyntaxTree.fromCheckedExpr(checkedExpr).getAst()); - } - - /** - * Run the compiler and produce the compiled expression that corresponds to the constructor - * argument. Errors during the compilation step itself result in a thrown {@link - * InterpreterException}. - */ - public CompiledExpression compiledExpression( - Map compileTimeConstants, List localVariables) - throws InterpreterException { - return enforceCompleteness( - compile(checkedExpr.getExpr(), new StaticEnv(compileTimeConstants, localVariables))); - } - - /** Retrieves the list of global variables in index order. */ - public List globalReferences() { - return globalReferences; - } - - // Looks up the index of a global variable, registering it if it is not already known. - private int globalIndex(String name) { - int index = globalReferences.indexOf(name); - if (index < 0) { - index = globalReferences.size(); - globalReferences.add(name); - } - return index; - } - - /** Compiles a general CEL expression relative to the given static environment. */ - private CompiledExpression compile(Expr expr, StaticEnv env) throws InterpreterException { - switch (expr.getExprKindCase()) { - case CONST_EXPR: - return compileConstant(expr.getConstExpr()); - case IDENT_EXPR: - return compileIdent(expr.getId(), env); - case SELECT_EXPR: - return compileSelect(expr.getId(), expr.getSelectExpr(), env); - case CALL_EXPR: - return compileCall(expr.getId(), expr.getCallExpr(), env); - case LIST_EXPR: - return compileList(expr.getListExpr(), env); - case STRUCT_EXPR: - return compileStruct(expr.getId(), expr.getStructExpr(), env); - case COMPREHENSION_EXPR: - return compileComprehension(expr.getComprehensionExpr(), env); - default: - throw new IllegalArgumentException( - "unexpected expression kind: " + expr.getExprKindCase()); - } - } - - /** Evaluates a CEL constant to an Object representing its runtime value. */ - @Nullable - private Object evalConstant(Constant constExpr) throws InterpreterException { - switch (constExpr.getConstantKindCase()) { - case NULL_VALUE: - return constExpr.getNullValue(); - case BOOL_VALUE: - return constExpr.getBoolValue(); - case INT64_VALUE: - return constExpr.getInt64Value(); - case UINT64_VALUE: - if (celOptions.enableUnsignedLongs()) { - return UnsignedLong.fromLongBits(constExpr.getUint64Value()); - } - return constExpr.getUint64Value(); - case DOUBLE_VALUE: - return constExpr.getDoubleValue(); - case STRING_VALUE: - return constExpr.getStringValue(); - case BYTES_VALUE: - return constExpr.getBytesValue(); - default: - throw new IllegalArgumentException( - "unsupported constant case: " + constExpr.getConstantKindCase()); - } - } - - /** Compiles a CEL constant. */ - private CompiledExpression compileConstant(Constant constExpr) throws InterpreterException { - return CompiledExpression.constant(evalConstant(constExpr)); - } - - /** - * Compiles a CEL identifier which may be statically bound to a constant or refer to a variable - * (local or global). - */ - private CompiledExpression compileIdent(long id, StaticEnv env) throws InterpreterException { - return compileIdentReference(checkedExpr.getReferenceMapOrThrow(id), id, env); - } - - /** Compiles a CEL identifier given its corresponding reference. */ - private CompiledExpression compileIdentReference(Reference reference, long id, StaticEnv env) - throws InterpreterException { - if (reference.hasValue()) { - // Bound to a constant. - return compileConstant(reference.getValue()); - } - String name = reference.getName(); - // Local or global Variable. - if (!env.isInScope(name)) { - // Global. - return compileGlobalIdent(id, name); - } - StaticBinding binding = env.bindingOf(name); - switch (binding.getKind()) { - case SLOT: - { - int offset = env.top() - binding.slot(); - return CompiledExpression.executable( - stack -> stack.getLocalAtSlotOffset(offset), Effect.CONTEXT_INDEPENDENT); - } - case BOUND_VALUE: - return CompiledExpression.constant(binding.value()); - } - throw unexpected("compileIdentReference"); - } - - /** - * Compiles a CEL identifier that is known to be a global variable. The result contains a - * runtime check for unbound global variables. - */ - private CompiledExpression compileGlobalIdent(long id, String name) - throws InterpreterException { - // Check whether the type exists in the type check map as a 'type'. - Type checkedType = checkedExpr.getTypeMapMap().get(id); - if (checkedType != null && checkedType.getTypeKindCase() == TypeKindCase.TYPE) { - return resolveTypeIdent(id, checkedType); - } - int index = globalIndex(name); - return CompiledExpression.executable( - stack -> stack.getGlobal(index), Effect.CONTEXT_DEPENDENT); - } - - private CompiledExpression resolveTypeIdent(long id, Type type) throws InterpreterException { - Value typeValue = typeResolver.adaptType(type); - if (typeValue != null) { - return CompiledExpression.constant(typeValue); - } - throw new InterpreterException.Builder( - "expected a runtime type for '%s', but found none.", type) - .setErrorCode(CelErrorCode.TYPE_NOT_FOUND) - .setLocation(metadata, id) - .build(); - } - - private boolean hasMapEntryOrMessageField( - Metadata metadata, long id, Object mapOrMessage, String field) throws InterpreterException { - if (mapOrMessage instanceof Map) { - return ((Map) mapOrMessage).containsKey(field); - } - return messageProcessor.dynamicHasField(metadata, id, mapOrMessage, field); - } - - private Object getMapEntryOrMessageField( - Metadata metadata, long id, Object mapOrMessage, String field) throws InterpreterException { - if (mapOrMessage instanceof Map) { - return getMapEntryFromMap(metadata, id, (Map) mapOrMessage, field); - } - return messageProcessor.dynamicGetField(metadata, id, mapOrMessage, field); - } - - /** Compiles a CEL select expression representing a field presence test. */ - // This lambda implements @Immutable interface 'ExecutableExpression', but accesses instance - // method(s) 'hasMapEntryOrMessageField' on 'Compilation' which is not @Immutable. - @SuppressWarnings("Immutable") - private CompiledExpression compileFieldTest(long id, Select selectExpr, StaticEnv env) - throws InterpreterException { - String field = selectExpr.getField(); - Expr operand = selectExpr.getOperand(); - Type checkedOperandType = checkedExpr.getTypeMapOrDefault(operand.getId(), Types.DYN); - CompiledExpression compiledOperand = compile(operand, env); - if (Types.isDynOrError(checkedOperandType)) { - // No compile-time type information available, so perform a fully dynamic operation. - return compiledOperand.mapNonThrowing( - executableOperand -> - stack -> - executableOperand - .execute(stack) - .transformAsync( - mapOrMessage -> - immediateValue( - hasMapEntryOrMessageField(metadata, id, mapOrMessage, field)), - directExecutor()), - constantOperand -> - compiledConstantOrThrowing( - () -> hasMapEntryOrMessageField(metadata, id, constantOperand, field))); - } - // Use available compile-time type information. - switch (checkedOperandType.getTypeKindCase()) { - case MESSAGE_TYPE: - String messageType = checkedOperandType.getMessageType(); - MessageProcessor.FieldTester fieldTester = - messageProcessor.makeFieldTester(metadata, id, messageType, field); - return compiledOperand.mapNonThrowing( - executableOperand -> - stack -> - executableOperand - .execute(stack) - .transform(fieldTester::hasField, directExecutor()), - constantOperand -> - CompiledExpression.constant(fieldTester.hasField(constantOperand))); - case MAP_TYPE: - return compiledOperand.mapNonThrowing( - executableOperand -> - stack -> - executableOperand - .execute(stack) - .transformAsync( - mapObject -> - immediateValue(hasMapEntry(metadata, id, mapObject, field)), - directExecutor()), - constantOperand -> - compiledConstantOrThrowing( - () -> hasMapEntry(metadata, id, constantOperand, field))); - default: - throw new InterpreterException.Builder( - "[internal] presence test for field '%s' in type %s", field, checkedOperandType) - .setLocation(metadata, id) - .build(); - } - } - - /** Compiles a CEL select expression representing a field access. */ - // This lambda implements @Immutable interface 'ExecutableExpression', but accesses instance - // method(s) 'getMapEntryOrMessageField' on 'Compilation' which is not @Immutable. - @SuppressWarnings("Immutable") - private CompiledExpression compileFieldAccess(long id, Select selectExpr, StaticEnv env) - throws InterpreterException { - String field = selectExpr.getField(); - Expr operand = selectExpr.getOperand(); - Type checkedOperandType = checkedExpr.getTypeMapOrDefault(operand.getId(), Types.DYN); - CompiledExpression compiledOperand = compile(operand, env); - if (Types.isDynOrError(checkedOperandType)) { - // No compile-time type information available, so perform a fully dynamic operation. - return compiledOperand.mapNonThrowing( - executableOperand -> - stack -> - executableOperand - .execute(stack) - .transformAsync( - mapOrMessage -> - immediateValue( - getMapEntryOrMessageField(metadata, id, mapOrMessage, field)), - directExecutor()), - constantOperand -> - compiledConstantOrThrowing( - () -> getMapEntryOrMessageField(metadata, id, constantOperand, field))); - } - // Use available compile-time type information. - switch (checkedOperandType.getTypeKindCase()) { - case MESSAGE_TYPE: - String messageType = checkedOperandType.getMessageType(); - MessageProcessor.FieldGetter fieldGetter = - messageProcessor.makeFieldGetter(metadata, id, messageType, field); - return compiledOperand.mapNonThrowing( - executableOperand -> - stack -> - executableOperand - .execute(stack) - .transform(fieldGetter::getField, directExecutor()), - constantOperand -> - CompiledExpression.constant(fieldGetter.getField(constantOperand))); - case MAP_TYPE: - return compiledOperand.mapNonThrowing( - executableOperand -> - stack -> - executableOperand - .execute(stack) - .transformAsync( - mapObject -> - immediateValue(getMapEntry(metadata, id, mapObject, field)), - directExecutor()), - constantOperand -> - compiledConstantOrThrowing( - () -> getMapEntry(metadata, id, constantOperand, field))); - default: - throw new InterpreterException.Builder( - "[internal] access to field '%s' in type %s", field, checkedOperandType) - .setLocation(metadata, id) - .build(); - } - } - - /** - * Compiles a CEL select expression which may be field selection, field presence test, or an - * access of a variable via a qualified name. - */ - private CompiledExpression compileSelect(long id, Select selectExpr, StaticEnv env) - throws InterpreterException { - Reference reference = checkedExpr.getReferenceMapOrDefault(id, null); - if (reference != null) { - return compileIdentReference(reference, id, env); - } else if (selectExpr.getTestOnly()) { - return compileFieldTest(id, selectExpr, env); - } else { - return compileFieldAccess(id, selectExpr, env); - } - } - - /** Compiles a general expression that is to be used as a function argument. */ - // This lambda implements @Immutable interface 'ScopedExpression', but the declaration of type - // 'dev.cel.legacy.runtime.async.async.Evaluator.StaticEnv' is not - // annotated with @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - private IdentifiedCompiledExpression compileFunctionArg(Expr expr, StaticEnv env) - throws InterpreterException { - return IdentifiedCompiledExpression.of( - slots -> enforceCompleteness(compile(expr, env.extendWithSlots(slots))), - expr.getId(), - checkedExpr.getTypeMapMap()); - } - - /** - * Compiles a function call expression. - * - *

All special cases such as conditionals and logical AND and OR are handled by the {@link - * FunctionResolver}. - */ - // This method reference implements @Immutable interface StackOffsetFinder, but the declaration - // of type 'dev.cel.legacy.runtime.async.async.Evaluator.StaticEnv' is not - // annotated with @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - private CompiledExpression compileCall(long id, Call call, StaticEnv env) - throws InterpreterException { - Reference reference = checkedExpr.getReferenceMapOrThrow(id); - Preconditions.checkArgument(reference.getOverloadIdCount() > 0); - - List compiledArguments = new ArrayList<>(); - if (call.hasTarget()) { - compiledArguments.add(compileFunctionArg(call.getTarget(), env)); - } - for (Expr arg : call.getArgsList()) { - compiledArguments.add(compileFunctionArg(arg, env)); - } - - List overloadIds = reference.getOverloadIdList(); - String functionName = call.getFunction(); - return functionResolver - .constructCall( - metadata, - id, - functionName, - overloadIds, - compiledArguments, - messageProcessor, - env::findStackOffset) - .map( - (executable, effect) -> - // If the constructed result is executable but context-independent, and if - // this call site is not within scope of a local binding, then run it now. - env.top() == 0 && effect == CONTEXT_INDEPENDENT - ? executeStatically(executable, globalReferences()) - : CompiledExpression.executable(executable, effect), - CompiledExpression::constant, - CompiledExpression::throwing); - } - - /** Compiles an expression that is expected to return a boolean. */ - private CompiledExpression compileBoolean(Expr bool, StaticEnv env) - throws InterpreterException { - long id = bool.getId(); - return compile(bool, env) - .mapNonThrowing( - executableBool -> - stack -> - executableBool - .execute(stack) - .transformAsync( - b -> immediateValue(expectBoolean(b, metadata, id)), - directExecutor()), - constantBool -> - compiledConstantOrThrowing(() -> expectBoolean(constantBool, metadata, id))); - } - - /** Compiles a CEL list creation. */ - private CompiledExpression compileList(CreateList listExpr, StaticEnv env) - throws InterpreterException { - List compiledElements = new ArrayList<>(); - boolean onlyConstant = true; - Effect effect = Effect.CONTEXT_INDEPENDENT; - for (Expr e : listExpr.getElementsList()) { - CompiledExpression compiledElement = enforceCompleteness(compile(e, env)); - if (compiledElement.isThrowing()) { - return compiledElement; - } - onlyConstant = onlyConstant && compiledElement.isConstant(); - effect = effect.meet(compiledElement.effect()); - compiledElements.add(compiledElement); - } - if (onlyConstant) { - return CompiledExpression.constant(transform(compiledElements, e -> e.constant())); - } - ImmutableList executableElements = - transform(compiledElements, CompiledExpression::toExecutable); - return CompiledExpression.executable( - stack -> - execAllAsList(executableElements, stack) - .transform(list -> list, directExecutor()), - effect); - } - - /** Compiles a CEL map or message creation. */ - private CompiledExpression compileStruct(long id, CreateStruct structExpr, StaticEnv env) - throws InterpreterException { - Reference reference = checkedExpr.getReferenceMapOrDefault(id, null); - if (reference == null) { - return compileMap(structExpr, env); - } else { - return compileMessage(id, reference.getName(), structExpr, env); - } - } - - /** Compiles a CEL map creation. */ - // This lambda implements @Immutable interface 'ExecutableExpression', but 'IdEntry' has - // non-final field 'key' - @SuppressWarnings("Immutable") - private CompiledExpression compileMap(CreateStruct structExpr, StaticEnv env) - throws InterpreterException { - List> compiledEntries = new ArrayList<>(); - boolean onlyConstant = true; - Effect effect = Effect.CONTEXT_INDEPENDENT; - boolean hasDynamicKeys = false; - Set staticKeys = new HashSet<>(); - for (CreateStruct.Entry entry : structExpr.getEntriesList()) { - CompiledExpression compiledKey = compile(entry.getMapKey(), env); - effect = effect.meet(compiledKey.effect()); - if (compiledKey.isThrowing()) { - return compiledKey; - } - if (compiledKey.isConstant()) { - Object key = compiledKey.constant(); - if (staticKeys.contains(key)) { - if (errorOnDuplicateKeys) { - return CompiledExpression.throwing( - new InterpreterException.Builder("duplicate map key [%s]", key) - .setErrorCode(CelErrorCode.DUPLICATE_ATTRIBUTE) - .setLocation(metadata, entry.getId()) - .build()); - } - } else { - staticKeys.add(compiledKey.constant()); - } - } else { - hasDynamicKeys = true; - onlyConstant = false; - } - CompiledExpression compiledValue = enforceCompleteness(compile(entry.getValue(), env)); - effect = effect.meet(compiledValue.effect()); - if (compiledValue.isThrowing()) { - return compiledValue; - } - if (!compiledValue.isConstant()) { - onlyConstant = false; - } - compiledEntries.add(new IdEntry<>(entry.getId(), compiledKey, compiledValue)); - } - if (onlyConstant) { - Map map = new LinkedHashMap<>(); - compiledEntries.forEach(entry -> map.put(entry.key.constant(), entry.val.constant())); - return CompiledExpression.constant(map); - } else { - ImmutableList> executableEntries = - transform( - compiledEntries, - e -> new IdEntry<>(e.id, e.key.toExecutable(), e.val.toExecutable())); - if (hasDynamicKeys && errorOnDuplicateKeys) { - return CompiledExpression.executable( - stack -> - FluentFuture.from( - allAsListOrFirstException( - transform(executableEntries, e -> executeEntry(e, stack)))) - .transformAsync( - entries -> { - Map map = new LinkedHashMap<>(); - for (IdEntry entry : entries) { - if (map.containsKey(entry.key)) { - return immediateException( - new InterpreterException.Builder( - "duplicate map key [%s]", entry.key) - .setErrorCode(CelErrorCode.DUPLICATE_ATTRIBUTE) - .setLocation(metadata, entry.id) - .build()); - } - map.put(entry.key, entry.val); - } - return immediateValue(map); - }, - directExecutor()), - effect); - } - return CompiledExpression.executable( - stack -> - FluentFuture.from( - allAsListOrFirstException( - transform(executableEntries, e -> executeEntry(e, stack)))) - .transform( - entries -> { - Map map = new LinkedHashMap<>(); - entries.forEach(entry -> map.put(entry.key, entry.val)); - return map; - }, - directExecutor()), - effect); - } - } - - /** Compiles a CEL message creation. */ - private CompiledExpression compileMessage( - long id, String typeName, CreateStruct structExpr, StaticEnv env) - throws InterpreterException { - List labels = new ArrayList<>(); - List types = new ArrayList<>(); - List compiledValues = new ArrayList<>(); - boolean onlyConstant = true; - Effect effect = Effect.CONTEXT_INDEPENDENT; - for (CreateStruct.Entry entry : structExpr.getEntriesList()) { - Expr valueExpr = entry.getValue(); - Type valueType = checkedExpr.getTypeMapOrDefault(valueExpr.getId(), Types.DYN); - types.add(valueType); - CompiledExpression compiledValue = enforceCompleteness(compile(entry.getValue(), env)); - effect = effect.meet(compiledValue.effect()); - if (compiledValue.isThrowing()) { - return compiledValue; - } - onlyConstant = onlyConstant && compiledValue.isConstant(); - String fieldName = entry.getFieldKey(); - labels.add(fieldName); - compiledValues.add(compiledValue); - } - MessageProcessor.MessageCreator messageCreator = - messageProcessor.makeMessageCreator(metadata, id, typeName, labels, types); - if (onlyConstant) { - return compiledConstantOrThrowing( - () -> messageCreator.createMessage(Lists.transform(compiledValues, e -> e.constant()))); - } else { - ImmutableList executableValues = - transform(compiledValues, CompiledExpression::toExecutable); - return CompiledExpression.executable( - stack -> - FluentFuture.from( - allAsListOrFirstException( - Lists.transform(executableValues, v -> v.execute(stack)))) - .transformAsync( - vl -> immediateValue(messageCreator.createMessage(vl)), directExecutor()), - effect); - } - } - - /** - * Represents the mechanism used to compile comprehensions. - * - *

Compilation consists of a number of mutually recursive methods that collectively implement - * compile-time unrolling of comprehensions when the range is statically known. These methods - * all need to have access to the same underlying set of values (= the parts that make up the - * original comprehension expression). To avoid having to pass these parts around as explicit - * arguments they are made universally available as instance variables. - */ - class ComprehensionCompilation { - private final Expr rangeExpr; - private final Expr initExpr; - private final long iterId; - private final String accuVar; - private final String iterVar; - private final Expr conditionExpr; - private final Expr stepExpr; - private final Expr resultExpr; - private final StaticEnv parentEnv; - - /** Initializes the comprehension compiler. */ - ComprehensionCompilation(Comprehension comprehension, StaticEnv parentEnv) { - this.rangeExpr = comprehension.getIterRange(); - this.initExpr = comprehension.getAccuInit(); - this.iterId = rangeExpr.getId(); - this.accuVar = comprehension.getAccuVar(); - this.iterVar = comprehension.getIterVar(); - this.conditionExpr = comprehension.getLoopCondition(); - this.stepExpr = comprehension.getLoopStep(); - this.resultExpr = comprehension.getResult(); - this.parentEnv = parentEnv; - } - - /** Runs the comprehension compiler for the comprehesion that it was instantiated for. */ - public CompiledExpression compileComprehension() throws InterpreterException { - CompiledExpression compiledRange = compile(rangeExpr, parentEnv); - CompiledExpression compiledInit = compile(initExpr, parentEnv); - - /** - * Exceptions in range or init lead to immediate failure and never let the loop run at all. - */ - if (compiledRange.isThrowing()) { - return compiledRange; - } - - if (compiledRange.isConstant()) { - /** The range is constant, so run the loop at compile time as things stay constant. */ - Collection rangeIterable = null; - try { - rangeIterable = getRangeIterable(compiledRange.constant(), metadata, iterId); - } catch (Exception e) { - // Range is illegal, so the entire expression throws an exception. - return CompiledExpression.throwing(e); - } - return constantRangeLoop(rangeIterable.iterator(), compiledInit); - } else { - /** Range is not constant. Generate runtime loop. */ - return compiledRuntimeLoop(compiledRange, compiledInit); - } - } - - /** - * Arranges for the rest of a comprehension loop over a non-empty constant range to be - * performed at runtime. - */ - private CompiledExpression constantRangeLoopTail( - ImmutableList remainingRange, CompiledExpression compiledAccu) - throws InterpreterException { - return compiledRuntimeLoop(CompiledExpression.constant(remainingRange), compiledAccu); - } - - /** - * Executes a comprehension loop over a constant range at compile time as far as everything - * remains static and potentially defers the remaining loop until runtime. - */ - private CompiledExpression constantRangeLoop( - Iterator range, CompiledExpression compiledAccu) throws InterpreterException { - while (range.hasNext()) { - if (!compiledAccu.isConstant()) { - return constantRangeLoopTail(ImmutableList.copyOf(range), compiledAccu); - } - @Nullable Object nextValue = range.next(); - StaticEnv loopEnv = - parentEnv - .extendWithValue(accuVar, compiledAccu.constant()) - .extendWithValue(iterVar, nextValue); - CompiledExpression compiledCondition = compileBoolean(conditionExpr, loopEnv); - if (compiledCondition.isThrowing()) { - return compiledCondition; - } - if (!compiledCondition.isConstant()) { - // The condition is dynamic, so let constantRangeLoopTail handle everything - // (including the condition itself) by pushing nextValue back into the remaining - // range. - return constantRangeLoopTail( - ImmutableList.builder().add(nextValue).addAll(range).build(), compiledAccu); - } - if (!asBoolean(compiledCondition.constant())) { - break; - } - compiledAccu = compile(stepExpr, loopEnv); - } - // Reached the end of the loop, so bind the accumulator to its variable and - // compile the result expression in the corresponding scope. - return compiledAccu.map( - (executableAccu, accuEffect) -> - compile(resultExpr, parentEnv.extendWithSlots(accuVar)) - .map( - (executableResult, resultEffect) -> - CompiledExpression.executable( - stack -> - executableResult.execute( - stack.extend(executableAccu.execute(stack))), - accuEffect.meet(resultEffect)), - CompiledExpression::constant, - CompiledExpression::throwing), - constantAccu -> compile(resultExpr, parentEnv.extendWithValue(accuVar, constantAccu)), - CompiledExpression::throwing); - } - - /** Generates the runtime loop when compile-time unrolling is not feasible. */ - private CompiledExpression compiledRuntimeLoop( - CompiledExpression compiledRange, CompiledExpression compiledInit) - throws InterpreterException { - StaticEnv resultEnv = parentEnv.extendWithSlots(accuVar); - StaticEnv loopEnv = resultEnv.extendWithSlots(iterVar); - - CompiledExpression compiledCondition = compileBoolean(conditionExpr, loopEnv); - CompiledExpression compiledStep = compile(stepExpr, loopEnv); - CompiledExpression compiledResult = compile(resultExpr, resultEnv); - - Effect effect = - compiledRange - .effect() - .meet(compiledInit.effect()) - .meet(compiledCondition.effect()) - .meet(compiledStep.effect()) - .meet(compiledResult.effect()); - - ExecutableExpression executableCondition = compiledCondition.toExecutable(); - ExecutableExpression executableStep = compiledStep.toExecutable(); - ExecutableExpression executableResult = compiledResult.toExecutable(); - ExecutableExpression executableRange = compiledRange.toExecutable(); - ExecutableExpression executableInit = compiledInit.toExecutable(); - - // Calculate the range and the initial value, then construct the range iteration and apply - // it to the initial value. - return CompiledExpression.executable( - stack -> - executableRange - .execute(stack) - .transformAsync( - iterRangeRaw -> - new RangeIteration( - getRangeIterable(iterRangeRaw, metadata, iterId).iterator(), - executableResult, - executableCondition, - executableStep, - stack) - .iterate(executableInit.execute(stack)), - directExecutor()), - effect); - } - } - - /** Compiles a CEL comprehension expression. */ - private CompiledExpression compileComprehension(Comprehension comprehension, StaticEnv env) - throws InterpreterException { - // Initialize the comprehension compiler class and then run it. - return new ComprehensionCompilation(comprehension, env).compileComprehension(); - } - } - - // Private helper functions. - - // Compile-time matters. - - /** - * Wraps an excecutable expression with a runtime check that guards against {@link - * IncompleteData}. - */ - // This lambda implements @Immutable interface 'ExecutableExpression', but the declaration of type - // 'dev.cel.runtime.InterpreterException' is not annotated with - // @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - private static CompiledExpression enforceCompleteness(CompiledExpression compiled) { - return compiled.mapNonThrowing( - executable -> - stack -> - executable - .execute(stack) - .transformAsync(EvaluationHelpers::immediateValue, directExecutor()), - CompiledExpression::constant); - } - - // Runtime matters. - - /** - * Runs the computations for the key/value executable expressions and constructs a future that - * delivers the corresponding values, plus the id. - */ - private static FluentFuture> executeEntry( - IdEntry entry, DynamicEnv stack) { - return FluentFuture.from( - allAsListOrFirstException( - ImmutableList.of(entry.key.execute(stack), entry.val.execute(stack)))) - .transform( - list -> new IdEntry(entry.id, list.get(0), list.get(1)), directExecutor()); - } - - /** - * Obtains (at runtime) the iteration range of a comprehension by inspecting the runtime type of - * the result of evaluating the range expression. - */ - @SuppressWarnings("unchecked") - private static Collection getRangeIterable( - Object iterRangeRaw, Metadata metadata, long id) throws InterpreterException { - if (iterRangeRaw instanceof List) { - return (List) iterRangeRaw; - } else if (iterRangeRaw instanceof Map) { - return ((Map) iterRangeRaw).keySet(); - } else { - throw new InterpreterException.Builder( - "expected a list or a map for iteration range but got '%s'", - iterRangeRaw.getClass().getSimpleName()) - .setErrorCode(CelErrorCode.INVALID_ARGUMENT) - .setLocation(metadata, id) - .build(); - } - } - - /** - * Implements the runtime iteration of the body (condition and step) of a comprehension over its - * range by constructing a recursive function from initial accu value to the future producing the - * final result. - * - *

Since exclusive use of {@code directExecutor()} would cause Java stack overruns for very - * long iterations, direct execution is broken up in regular intervals. - */ - private static class RangeIteration { - private final Iterator range; - private final ExecutableExpression executableResult; - private final ExecutableExpression executableCondition; - private final ExecutableExpression executableStep; - private final DynamicEnv stack; - private final Executor trampoline; - - public RangeIteration( - Iterator range, - ExecutableExpression executableResult, - ExecutableExpression executableCondition, - ExecutableExpression executableStep, - DynamicEnv stack) { - this.range = range; - this.executableResult = executableResult; - this.executableCondition = executableCondition; - this.executableStep = executableStep; - this.stack = stack; - // Using directExecutor() throughout would lead to very deep recursion depths - // and ultimately to Java stack exhaustion when iterating over a very long list. - // Therefore, the loop is broken up by using a sequential executor every - // DIRECT_EXECUTION_BUDGET iterations. A seqential executor acts as a trampoline, - // thus avoiding unlimited recursion depth. - // If the original executor is not the direct executor, then using it directly - // as a the trampoline is potentially better (because it resets the stack depth to - // the toplevel rather than just to the start of the comprehesion loop). - this.trampoline = - stack.currentContext().executor() == directExecutor() - ? newSequentialExecutor(directExecutor()) - : stack.currentContext().executor(); - } - - public FluentFuture iterate(FluentFuture accuFuture) { - // By starting with zero budget the trampoline is initialized right at the - // beginning of the loop. - return loop(accuFuture, 0); - } - - private FluentFuture loop(FluentFuture accuFuture, int budget) { - if (!range.hasNext()) { - return executableResult.execute(stack.extend(accuFuture)); - } - Object elem = range.next(); - DynamicEnv loopStack = stack.extend(accuFuture, immediateValue(elem)); - // Break direct execution up every once in a while to avoid running - // out of Java stack in case of very long iteration ranges. - boolean budgetExhausted = budget <= 0; - Executor executor = budgetExhausted ? trampoline : directExecutor(); - int newBudget = budgetExhausted ? DIRECT_EXECUTION_BUDGET : (budget - 1); - return executableCondition - .execute(loopStack) - .transformAsync( - condition -> - asBoolean(condition) - ? loop( - // Applies trampoline at the loop step to avoid building up a deeply - // nested pending computation in the accumulator. - executableStep.execute(loopStack).transform(a -> a, executor), newBudget) - : executableResult.execute(stack.extend(accuFuture)), - executor); - } - } - - // Miscellaneous. - - /** - * Creates a dummy exception to be thrown in places where we don't expect control to reach (but - * the Java compiler is not smart enough to know that). - */ - private static RuntimeException unexpected(String where) { - return new RuntimeException("[internal] reached unexpected program point: " + where); - } - - private static Map expectMap(Metadata metadata, long id, Object mapObject) - throws InterpreterException { - if (mapObject instanceof Map) { - return (Map) mapObject; - } - throw new InterpreterException.Builder( - "[internal] Expected an instance of 'Map' but found '%s'", - mapObject.getClass().getName()) - .setLocation(metadata, id) - .build(); - } - - private static boolean hasMapEntry(Metadata metadata, long id, Object mapObject, String field) - throws InterpreterException { - return expectMap(metadata, id, mapObject).containsKey(field); - } - - private static Object getMapEntryFromMap(Metadata metadata, long id, Map map, String field) - throws InterpreterException { - Object entry = map.get(field); - if (entry == null) { - throw new InterpreterException.Builder("key '%s' is not present in map.", field) - .setErrorCode(CelErrorCode.ATTRIBUTE_NOT_FOUND) - .setLocation(metadata, id) - .build(); - } - return entry; - } - - private static Object getMapEntry(Metadata metadata, long id, Object mapObject, String field) - throws InterpreterException { - return getMapEntryFromMap(metadata, id, expectMap(metadata, id, mapObject), field); - } - - /** Like Map.Entry, but key and value are the same type, and associates an ID with the entry. */ - private static class IdEntry { - long id; - T key; - T val; - - IdEntry(long id, T key, T val) { - this.id = id; - this.key = key; - this.val = val; - } - } - - // Breaks up direct execution during comprehensions 5% of the time. - private static final int DIRECT_EXECUTION_BUDGET = 20; -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/ExecutableExpression.java b/legacy/java/dev/cel/legacy/runtime/async/ExecutableExpression.java deleted file mode 100644 index f7cacaa45..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/ExecutableExpression.java +++ /dev/null @@ -1,28 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import com.google.common.util.concurrent.FluentFuture; -import com.google.errorprone.annotations.Immutable; - -/** - * An executable expression is the outcome of "compiling" a CEL expression. The compiler effectively - * implements a form of denotational semantics, and executable expressions are the domain that CEL - * expressions are mapped to by this semantics. - * - *

Executable expressions represent the original CEL program, but with most of the interpretative - * overhead eliminated. In particular, there is no more traversal of abstract syntax during - * execution, and no dispatch on expression types. Local variables are accessed in constant time by - * precomputed stack location. - * - *

Executable expressions are modeled by functions that take the dynamic environment (for - * resolving free local and global variables) and an executor (for handling asynchronous aspects of - * the computation) as arguments and return a future of the result. - * - *

The future is produced without throwing exceptions. Any runtime exceptions are captured by the - * future itself by letting it fail. Interpretation-related errors are signalled by {@link - * InterpreterExceptions} that are the cause of the corresponding {@link ExecutionException}. - */ -@Immutable -@FunctionalInterface -public interface ExecutableExpression { - FluentFuture execute(DynamicEnv env); -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/FunctionRegistrar.java b/legacy/java/dev/cel/legacy/runtime/async/FunctionRegistrar.java deleted file mode 100644 index a2688a01f..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/FunctionRegistrar.java +++ /dev/null @@ -1,340 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static com.google.common.util.concurrent.Futures.immediateFailedFuture; -import static com.google.common.util.concurrent.Futures.immediateFuture; - -import com.google.common.base.Joiner; -import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.errorprone.annotations.Immutable; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.Metadata; -import dev.cel.runtime.Registrar; -import java.util.Arrays; -import java.util.List; -import javax.annotation.Nullable; - -/** - * Interface to an object that accepts bindings of Java implementations to CEL functions that can be - * used by the {@link Evaluator} via a corresponding {@link FunctionResolver}. - * - *

A class implementing this interface must define {@link FunctionRegistrar#addCallConstructor} - * and {@link FunctionRegistrar#addStrictFunction}. All other methods already have a default - * implementation in terms of these. - */ -public interface FunctionRegistrar extends Registrar { - - /** Constructs a {@link CompiledExpression} for the given call. */ - @Immutable - @FunctionalInterface - interface CallConstructor { - CompiledExpression construct( - @Nullable Metadata metadata, - long exprId, - List compiledArguments, - MessageProcessor messageProcessor, - StackOffsetFinder stackOffsetFinder) - throws InterpreterException; - } - - /** - * Simplified version of {@link CallConstructor} without the message processor and without the - * stack offset finder. - */ - @Immutable - @FunctionalInterface - interface SimpleCallConstructor { - CompiledExpression construct( - @Nullable Metadata metadata, - long exprId, - List compiledArguments) - throws InterpreterException; - } - - /** Simplified version of {@link CallConstructor} without the stack offset finder. */ - @Immutable - @FunctionalInterface - interface SimpleCallConstructorWithStackAccess { - CompiledExpression construct( - @Nullable Metadata metadata, - long exprId, - List compiledArguments, - StackOffsetFinder stackOffsetFinder) - throws InterpreterException; - } - - /** - * A function bound to one out of several possible overloads of a CEL function. - * - *

Overloadable functions have to be strict (i.e., their arguments get fully evaluated before - * they are called). This is a necessary prerequisite for runtime overload resolution, but it also - * opens the possibility of memoization. - */ - @Immutable - @FunctionalInterface - interface StrictFunction { - ListenableFuture apply(GlobalContext globalContext, List arguments); - } - - // Core functionality. - // - // All convenience methods below are imlpemented in terms of just - // addCallConstructor and addOverloadableFunction. - - /** - * Adds a generic call constructor for the given overload ID. Function calls with this overload ID - * will later be handled by the given {@link CallConstructor}. No runtime overloading is possible. - */ - void addCallConstructor(String overloadId, CallConstructor callConstructor); - - /** Adds a simple generic call constructor. */ - default void addCallConstructor(String overloadId, SimpleCallConstructor simpleCallConstructor) { - addCallConstructor( - overloadId, - (md, id, args, ignoredMessageProcessor, ignoredStackOffsetFinder) -> - simpleCallConstructor.construct(md, id, args)); - } - - /** Adds a simple generic call constructor with stack access. */ - default void addCallConstructor( - String overloadId, - SimpleCallConstructorWithStackAccess simpleCallConstructorWithStackAccess) { - addCallConstructor( - overloadId, - (md, id, args, ignoredMessageProcessor, stackOffsetFinder) -> - simpleCallConstructorWithStackAccess.construct(md, id, args, stackOffsetFinder)); - } - - /** Registers one possible binding for a runtime-overloadable function. */ - void addStrictFunction( - String overloadId, - List> argumentTypes, - boolean contextIndependent, - StrictFunction strictFunction); - - // Convenience methods with default implementations. - // A class implementing interface {@link FunctionRegistry} should not provide - // its own implementations. - - /** Interface to type unary asynchronous function. */ - @Immutable - @FunctionalInterface - interface StrictUnaryFunction { - ListenableFuture apply(T arg); - } - - /** Interface to typed binary asynchronous function with barrier synchronization. */ - @Immutable - @FunctionalInterface - interface StrictBinaryFunction { - ListenableFuture apply(T1 arg1, T2 arg2); - } - - /** - * Interface to a general asynchronous function that operates without implicit barrier - * synchronization on argument evaluation. All arguments are being evaluated, though. These - * functions are also non-strict in the sense that an error in an unused argument will not lead to - * an error in the function application itself. - */ - @Immutable - @FunctionalInterface - interface NobarrierFunction { - ListenableFuture apply( - GlobalContext globalContext, List> args); - } - - /** - * Registers general asynchronous overloadable function. - * - *

Caution: The function's continuation will run on its returned future's executor. Make sure - * to only use this registration method if such an arrangement is safe. If the future's executor - * (which might, for example, be an RPC event manager's executor) is not appropriate, then prefer - * using {@link #addAsync}. - */ - default void addDirect( - String overloadId, List> argTypes, Effect effect, StrictFunction function) { - addStrictFunction(overloadId, argTypes, effect.equals(Effect.CONTEXT_INDEPENDENT), function); - } - - /** - * Registers a general asynchronous {@link StrictFunction} that takes a variable number of - * arguments. - * - *

Caveat: This function cannot participate in runtime overload resolution. - * - *

Caution: The function's continuation will run on its returned future's executor. Make sure - * to only use this registration method if such an arrangement is safe. If the future's executor - * (which might, for example, be an RPC event manager's executor) is not appropriate, then prefer - * using {@link #addAsync}. - */ - default void addDirect(String overloadId, Effect effect, StrictFunction function) { - addCallConstructor( - overloadId, - (metadata, exprId, compiledArguments) -> - EvaluationHelpers.compileStrictCall(function, overloadId, effect, compiledArguments)); - } - - /** - * Registers typed unary asynchronous function. The function's continuation will use the returned - * future's executor. - */ - default void addDirect( - String overloadId, Class argType, Effect effect, StrictUnaryFunction function) { - addDirect( - overloadId, - ImmutableList.of(argType), - effect, - (gctx, args) -> function.apply(argType.cast(args.get(0)))); - } - - /** - * Registers typed binary asynchronous function. The function's continuation will use the returned - * future's executor. - */ - default void addDirect( - String overloadId, - Class argType1, - Class argType2, - Effect effect, - StrictBinaryFunction function) { - addDirect( - overloadId, - ImmutableList.of(argType1, argType2), - effect, - (gctx, args) -> function.apply(argType1.cast(args.get(0)), argType2.cast(args.get(1)))); - } - - /** - * Registers an executor-coupling version of the given asynchronous {@link StrictFunction}. - * Coupling guarantees that transformations on the result run on the context's executor even if - * they specify {@code MoreExecutors#directExecutor}. - */ - default void addAsync( - String overloadId, List> argTypes, Effect effect, StrictFunction function) { - addDirect( - overloadId, - argTypes, - effect, - (gctx, args) -> - gctx.context().coupleToExecutorInRequestContext(() -> function.apply(gctx, args))); - } - - /** Registers variadic asynchronous {@link StrictFunction} in executor-coupling fashion. */ - default void addAsync(String overloadId, Effect effect, StrictFunction function) { - addDirect( - overloadId, - effect, - (gctx, args) -> - gctx.context().coupleToExecutorInRequestContext(() -> function.apply(gctx, args))); - } - - /** Registers typed unary asynchronous function in executor-coupling fashion. */ - default void addAsync( - String overloadId, Class argType, Effect effect, StrictUnaryFunction function) { - addDirect( - overloadId, - ImmutableList.of(argType), - effect, - (gctx, args) -> - gctx.context() - .coupleToExecutorInRequestContext(() -> function.apply(argType.cast(args.get(0))))); - } - - /** Registers typed binary asynchronous function in executor-coupling fashion. */ - default void addAsync( - String overloadId, - Class argType1, - Class argType2, - Effect effect, - StrictBinaryFunction function) { - addDirect( - overloadId, - ImmutableList.of(argType1, argType2), - effect, - (gctx, args) -> - gctx.context() - .coupleToExecutorInRequestContext( - () -> function.apply(argType1.cast(args.get(0)), argType2.cast(args.get(1))))); - } - - /** Registers a no-barrier asynchronous function. */ - default void addNobarrierAsync(String overloadId, Effect effect, NobarrierFunction function) { - addCallConstructor( - overloadId, - (metadata, exprId, compiledArguments) -> - EvaluationHelpers.compileNobarrierCall(function, effect, compiledArguments)); - } - - // Registrar methods - - /** Adds a unary function to the dispatcher. */ - @Override - default void add(String overloadId, Class argType, UnaryFunction function) { - addDirect( - overloadId, - argType, - Effect.CONTEXT_INDEPENDENT, - arg -> { - try { - return immediateFuture(function.apply(arg)); - } catch (RuntimeException e) { - return immediateFailedFuture( - new InterpreterException.Builder( - e, "Function '%s' failed with arg(s) '%s'", overloadId, arg) - .build()); - } catch (Exception e) { - return immediateFailedFuture(InterpreterException.wrapOrThrow(e)); - } - }); - } - - /** Adds a binary function to the dispatcher. */ - @Override - default void add( - String overloadId, Class argType1, Class argType2, BinaryFunction function) { - addDirect( - overloadId, - argType1, - argType2, - Effect.CONTEXT_INDEPENDENT, - (arg1, arg2) -> { - try { - return immediateFuture(function.apply(arg1, arg2)); - } catch (RuntimeException e) { - return immediateFailedFuture( - new InterpreterException.Builder( - e, - "Function '%s' failed with arg(s) '%s'", - overloadId, - Joiner.on(", ").join(Arrays.asList(arg1, arg2))) - .build()); - } catch (Exception e) { - return immediateFailedFuture(InterpreterException.wrapOrThrow(e)); - } - }); - } - - /** Adds a general function to the dispatcher. */ - @Override - default void add(String overloadId, List> argTypes, Function function) { - addDirect( - overloadId, - argTypes, - Effect.CONTEXT_INDEPENDENT, - (gctx, args) -> { - try { - return immediateFuture(function.apply(args.toArray())); - } catch (RuntimeException e) { - return immediateFailedFuture( - new InterpreterException.Builder( - e, - "Function '%s' failed with arg(s) '%s'", - overloadId, - Joiner.on(", ").join(args)) - .build()); - } catch (Exception e) { - return immediateFailedFuture(InterpreterException.wrapOrThrow(e)); - } - }); - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/FunctionResolver.java b/legacy/java/dev/cel/legacy/runtime/async/FunctionResolver.java deleted file mode 100644 index 326a3621f..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/FunctionResolver.java +++ /dev/null @@ -1,42 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.Metadata; -import java.util.List; -import javax.annotation.Nullable; - -/** - * Interface to an object that the {@link Evaluator} can use to resolve calls of Java-coded CEL - * functions. The primary method {@link FunctionResolver#constructCall} constructs the compiled - * expression corresponding to a call of the specified function given its compiled arguments. - * - *

Actual function bindings are usually established (although strictly speaking that is not - * necessary from the {@link Evaluator}'s point of view) by combining a {@link FunctionResolver} - * with a corresponding {@link FunctionRegistrar}. The typical use case is codified by {@link - * AsyncDispatcher}. - */ -public interface FunctionResolver { - - /** - * Constructs the compliled CEL expression that implements the call of a function at some call - * site. - * - *

If multiple overload IDs are given, then a runtime dispatch is implemented. All overload IDs - * must refer to strict functions in that case. - * - *

The construction may apply arbitrary optimizations. For example, multiplication with 0 might - * just generate the constant 0, regardless of the second argument. - */ - CompiledExpression constructCall( - @Nullable Metadata metadata, - long exprId, - String functionName, - List overloadIds, - List compiledArguments, - MessageProcessor messageProcessor, - StackOffsetFinder stackOffsetFinder) - throws InterpreterException; - - /** Determines whether or not the given overload ID corresponds to a known function binding. */ - boolean isBound(String overloadId); -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/FuturesInterpreter.java b/legacy/java/dev/cel/legacy/runtime/async/FuturesInterpreter.java deleted file mode 100644 index e2e9bf38d..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/FuturesInterpreter.java +++ /dev/null @@ -1,52 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import com.google.protobuf.Descriptors.Descriptor; -import dev.cel.common.CelOptions; -import dev.cel.runtime.MessageProvider; -import java.util.Optional; -import java.util.function.Function; - -/** - * Legacy implementation of an optimizing futures-based interpreter for CEL. - * - *

This is a wrapper around {@link Evaluator}, providing the original interface for backward - * compatibility. - */ -public class FuturesInterpreter extends Evaluator { - - /** Standard constructor. */ - public FuturesInterpreter( - TypeResolver typeResolver, - MessageProcessor messageProcessor, - AsyncDispatcher dispatcher, - CelOptions celOptions) { - super(typeResolver, messageProcessor, dispatcher, celOptions); - } - - /** - * Legacy constructor that uses the standard type resolver and adapts a {@link MessageProvider} - * for use as message processor. Uses legacy features. - */ - public FuturesInterpreter( - MessageProvider messageProvider, - AsyncDispatcher dispatcher, - Function> messageLookup) { - this(messageProvider, dispatcher, messageLookup, CelOptions.LEGACY); - } - - /** - * Legacy constructor that uses the standard type resolver and adapts a {@link MessageProvider} - * for use as message processor. Uses legacy features. Uses provided features. - */ - public FuturesInterpreter( - MessageProvider messageProvider, - AsyncDispatcher dispatcher, - Function> messageLookup, - CelOptions celOptions) { - super( - StandardTypeResolver.getInstance(celOptions), - new MessageProcessorAdapter(messageLookup, messageProvider), - dispatcher, - celOptions); - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/GlobalContext.java b/legacy/java/dev/cel/legacy/runtime/async/GlobalContext.java deleted file mode 100644 index 546e5533e..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/GlobalContext.java +++ /dev/null @@ -1,29 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import com.google.auto.value.AutoValue; -import com.google.common.util.concurrent.ListenableFuture; -import java.util.function.Supplier; - -/** Represents the global context of the computation: async context and global variable bindings. */ -@AutoValue -public abstract class GlobalContext { - - /** Retrieves the {@link AsyncContext}. */ - public abstract AsyncContext context(); - - /** Retrieves the {@link AsyncCanonicalResolver}. */ - public abstract AsyncCanonicalResolver resolver(); - - /** - * Creates a new {@link GlobalContext} by pairing an {@link AsyncContext} with the corresponding - * {@link AsyncCanonicalResolver}. - */ - public static GlobalContext of(AsyncContext context, AsyncCanonicalResolver resolver) { - return new AutoValue_GlobalContext(context, resolver); - } - - /** Resolves a global variable using the {@link AsyncCanonicalResolver}. */ - public Supplier> resolve(String name) { - return resolver().resolve(name); - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/IdentifiedCompiledExpression.java b/legacy/java/dev/cel/legacy/runtime/async/IdentifiedCompiledExpression.java deleted file mode 100644 index eec91bf98..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/IdentifiedCompiledExpression.java +++ /dev/null @@ -1,47 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import dev.cel.expr.Type; -import com.google.auto.value.AutoValue; -import com.google.errorprone.annotations.Immutable; -import dev.cel.checker.Types; -import dev.cel.runtime.InterpreterException; -import java.util.Map; - -/** - * Represents a {@link CompiledExpression}, possibly further scoped, together with its node ID in - * the abstract syntax and its CEL type. - */ -@AutoValue -public abstract class IdentifiedCompiledExpression { - - /** - * Represents a compiled expression when placed within the scope of some additional stack slots - * (aka local variables). - */ - @Immutable - @FunctionalInterface - public interface ScopedExpression { - CompiledExpression inScopeOf(String... slots) throws InterpreterException; - } - - /** The scoped expression in question. */ - public abstract ScopedExpression scopedExpression(); - - /** The expression with no further scoping. */ - public CompiledExpression expression() throws InterpreterException { - return scopedExpression().inScopeOf(); - } - - /** The node ID of the expression in the original checked expression. */ - public abstract long id(); - - /** The CEL type of the expression. */ - public abstract Type type(); - - /** Constructs an {@link IdentifiedCompiledExpression}. */ - public static IdentifiedCompiledExpression of( - ScopedExpression scopedExpression, long id, Map typeMap) { - return new AutoValue_IdentifiedCompiledExpression( - scopedExpression, id, typeMap.getOrDefault(id, Types.DYN)); - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/MessageProcessor.java b/legacy/java/dev/cel/legacy/runtime/async/MessageProcessor.java deleted file mode 100644 index 7311202de..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/MessageProcessor.java +++ /dev/null @@ -1,241 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import dev.cel.expr.Type; -import com.google.common.base.Preconditions; -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.errorprone.annotations.Immutable; -import com.google.protobuf.Message; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.Metadata; -import java.util.ArrayList; -import java.util.List; - -/** - * Represents the mechanism for creating proto message objects and for accessing proto message - * fields. - */ -public interface MessageProcessor { - - /** Represents the operation of creating a proto message of a specific known type. */ - @Immutable - @FunctionalInterface - interface MessageCreator { - /** - * Creates the message object using the given field values. Field values correspond by position - * to the field names that were provided as {@code fieldNames} when the {@code MessageCreator} - * was made using the {@code makeMessageCreator} method below. - * - *

Field values must be in canonical CEL runtime value representation. - * - *

Throws the exception if runtime field values cannot be adapted to their corresponding - * field types. An implementation can try to perform these checks as much as possible during - * {@code MessageCreator} construction, but if there are field values of dynamic type it is - * still possible that the exception is thrown at message creation time. - */ - Object createMessage(List fieldValues) throws InterpreterException; - } - - /** - * Represents the operation of getting the value of a specific field in a proto message of a - * specific known type. - */ - @Immutable - @FunctionalInterface - interface FieldGetter { - /** - * Retrieves the field's value or the default value if the field is not set. The returned value - * will be in canonical CEL runtime representation. - */ - Object getField(Object message); - } - - /** - * Represents the operation of checking for the presence of a specific field in a proto message of - * a specific known type. - */ - @Immutable - @FunctionalInterface - interface FieldTester { - /** Checks for existence of the field. */ - boolean hasField(Object message); - } - - /** - * Represents the assignment of the given value (after a suitable representation change) to a - * specific field of the given proto message builder. The builder must be a builder for the type - * of message that the field in question is defined within. - */ - @Immutable - @FunctionalInterface - interface FieldAssigner { - /** Assigns the suitably converted value to the field. */ - @CanIgnoreReturnValue - Message.Builder assign(Message.Builder builder, Object value); - } - - /** Represents the clearing of a specific field in the given proto message builder. */ - @Immutable - @FunctionalInterface - interface FieldClearer { - /** Clears the field. */ - @CanIgnoreReturnValue - Message.Builder clear(Message.Builder builder); - } - - /** Represents the creation of a new builder for a specific proto message type. */ - @Immutable - @FunctionalInterface - interface MessageBuilderCreator { - /** Creates a new empty builder. */ - Message.Builder builder(); - } - - /** - * Returns a {@link MessageCreator} for the named message type. When invoking the {@code - * createMessage} method on the result, values for each of the named fields must be provided. - * Field values correspond to field names by position. - * - *

Throws an exception if the message type is unknown or if any of the named fields is not - * defined in it. - */ - // This lambda implements @Immutable interface 'MessageCreator', but 'List' is mutable - @SuppressWarnings("Immutable") - default MessageCreator makeMessageCreator( - Metadata metadata, - long exprId, - String messageName, - List fieldNames, - List fieldTypes) - throws InterpreterException { - final int numFields = fieldNames.size(); - Preconditions.checkArgument(numFields == fieldTypes.size()); - MessageBuilderCreator builderCreator = makeMessageBuilderCreator(metadata, exprId, messageName); - List assigners = new ArrayList<>(); - for (int i = 0; i < numFields; ++i) { - assigners.add( - makeFieldAssigner(metadata, exprId, messageName, fieldNames.get(i), fieldTypes.get(i))); - } - return fieldValues -> { - Preconditions.checkArgument(numFields == fieldValues.size()); - Message.Builder messageBuilder = builderCreator.builder(); - for (int i = 0; i < numFields; ++i) { - try { - assigners.get(i).assign(messageBuilder, fieldValues.get(i)); - } catch (RuntimeException e) { - throw new InterpreterException.Builder(e, e.getMessage()) - .setLocation(metadata, exprId) - .build(); - } - } - return messageBuilder.build(); - }; - } - - /** - * Returns a {@link FieldGetter} for the named field in the named message type. - * - *

Throws an exception if the message type is unknown or if the field is not defined in that - * message type. - */ - FieldGetter makeFieldGetter(Metadata metadata, long exprId, String messageName, String fieldName) - throws InterpreterException; - - /** - * Returns a {@link FieldTester} for the named field in the named message type. - * - *

Throws an exception if the message type is unknown, if the field is not defined in that - * message type, or if presence checks on that field are not supported. - */ - FieldTester makeFieldTester(Metadata metadata, long exprId, String messageName, String fieldName) - throws InterpreterException; - - /** - * Returns a {@link FieldAssigner} for the named field in the named message type. - * - *

Throws an exception if the message type is unknown or if the field is not defined in that - * message type. - * - *

May use the CEL type information for the value to be assigned by the returned assigner in - * order to correctly implement the conversion from CEL runtime representation to the - * representation required by the proto field. - */ - FieldAssigner makeFieldAssigner( - Metadata metadata, long exprId, String messageName, String fieldName, Type fieldType) - throws InterpreterException; - - /** - * Returns a {@link FieldClearer} for the named field in the named message type. - * - *

Throws an exception if the message type is unknown or if the field is not defined in that - * message type. - */ - FieldClearer makeFieldClearer( - Metadata metadata, long exprId, String messageName, String fieldName) - throws InterpreterException; - - /** - * Returns a {@link MessageBuilderCreator} for the named field in the named message type. - * - *

Throws an exception if the message type is unknown. - */ - MessageBuilderCreator makeMessageBuilderCreator( - Metadata metadata, long exprId, String messageName) throws InterpreterException; - - /** - * Finds the field and retrieves its value using only the runtime type of the given message - * object. If the field is not set, the type-specific default value is returned according to - * normal proto message semantics. - * - *

Throws an exception if the object is not a message object or if the field is not defined in - * it. - */ - Object dynamicGetField(Metadata metadata, long exprId, Object messageObject, String fieldName) - throws InterpreterException; - - /** - * Checks for the existence of the field using only the runtime type of the given message object. - * - *

Throws an exception if the object is not a message object or if the field is not defined in - * it. - */ - boolean dynamicHasField(Metadata metadata, long exprId, Object messageObject, String fieldName) - throws InterpreterException; - - /** - * Returns a {@link FieldGetter} for the named extension. - * - *

Throws an exception if the extension is unknown. - */ - FieldGetter makeExtensionGetter(Metadata metadata, long exprId, String extensionName) - throws InterpreterException; - - /** - * Returns a {@link FieldTester} for the named extension. - * - *

Throws an exception if the extesion is unknown, or if presence checks on it are not - * supported. - */ - FieldTester makeExtensionTester(Metadata metadata, long exprId, String extensionName) - throws InterpreterException; - - /** - * Returns a {@link FieldAssigner} for the named extension. - * - *

Throws an exception if the extension is unknown. - * - *

May use the CEL type information for the value to be assigned by the returned assigner in - * order to correctly implement the conversion from CEL runtime representation to the - * representation required by the proto field. - */ - FieldAssigner makeExtensionAssigner( - Metadata metadata, long exprId, String extensionName, Type extensionType) - throws InterpreterException; - - /** - * Returns a {@link FieldClearer} for the named extension. - * - *

Throws an exception if the extension is unknown. - */ - FieldClearer makeExtensionClearer(Metadata metadata, long exprId, String extensionName) - throws InterpreterException; -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/MessageProcessorAdapter.java b/legacy/java/dev/cel/legacy/runtime/async/MessageProcessorAdapter.java deleted file mode 100644 index 3d14db59d..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/MessageProcessorAdapter.java +++ /dev/null @@ -1,230 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import dev.cel.expr.Type; -import com.google.common.collect.ImmutableMap; -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.Message; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.MessageProvider; -import dev.cel.runtime.Metadata; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - -/** Adapts a {@link MessageProvider} to act as a {@link MessageProcessor}, using delegation. */ -final class MessageProcessorAdapter implements MessageProcessor { - - private final Function> messageLookup; - private final MessageProvider messageProvider; - - MessageProcessorAdapter( - Function> messageLookup, MessageProvider messageProvider) { - this.messageLookup = messageLookup; - this.messageProvider = messageProvider; - } - - // Overrides the default implementation since doing so is more efficient here. - // (The default implementation is based on FieldAssigners, and those are not efficient - // when adapting a MessageProvider.) - // This lambda implements @Immutable interface 'MessageCreator', but 'List' is mutable - @SuppressWarnings("Immutable") - @Override - public MessageCreator makeMessageCreator( - Metadata metadata, - long exprId, - String messageName, - List fieldNames, - List fieldTypes) // ignored in this implementation; adaptation errors occur at runtime - throws InterpreterException { - Descriptor messageDescriptor = verifyMessageType(metadata, exprId, messageName); - for (String fieldName : fieldNames) { - verifyField(metadata, exprId, messageDescriptor, fieldName); - } - return fieldValues -> { - try { - return messageProvider.createMessage(messageName, makeValueMap(fieldNames, fieldValues)); - } catch (RuntimeException e) { - throw new InterpreterException.Builder(e, e.getMessage()) - .setLocation(metadata, exprId) - .build(); - } - }; - } - - // This lambda implements @Immutable interface 'FieldGetter', but 'MessageProcessorAdapter' has - // field 'messageProvider' of type - // 'com.google.api.tools.contract.runtime.interpreter.MessageProvider', the declaration of type - // 'com.google.api.tools.contract.runtime.interpreter.MessageProvider' is not annotated with - // @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - @Override - public FieldGetter makeFieldGetter( - Metadata metadata, long exprId, String messageName, String fieldName) - throws InterpreterException { - Descriptor messageDescriptor = verifyMessageType(metadata, exprId, messageName); - verifyField(metadata, exprId, messageDescriptor, fieldName); - return message -> { - try { - return messageProvider.selectField(message, fieldName); - } catch (IllegalArgumentException e) { - throw new IllegalStateException("[internal] field selection unexpectedly failed", e); - } - }; - } - - // This lambda implements @Immutable interface 'FieldTester', but 'MessageProcessorAdapter' has - // field 'messageProvider' of type - // 'com.google.api.tools.contract.runtime.interpreter.MessageProvider', the declaration of type - // 'com.google.api.tools.contract.runtime.interpreter.MessageProvider' is not annotated with - // @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - @Override - public FieldTester makeFieldTester( - Metadata metadata, long exprId, String messageName, String fieldName) - throws InterpreterException { - Descriptor messageDescriptor = verifyMessageType(metadata, exprId, messageName); - verifyField(metadata, exprId, messageDescriptor, fieldName); - return message -> { - try { - return messageProvider.hasField(message, fieldName).equals(true); - } catch (IllegalArgumentException e) { - throw new IllegalStateException("[internal] field presence test unexpectedly failed", e); - } - }; - } - - // This lambda implements @Immutable interface 'FieldAssigner', but 'MessageProcessorAdapter' has - // field 'messageProvider' of type - // 'com.google.api.tools.contract.runtime.interpreter.MessageProvider', the declaration of type - // 'com.google.api.tools.contract.runtime.interpreter.MessageProvider' is not annotated with - // @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - @Override - public FieldAssigner makeFieldAssigner( - Metadata metadata, long exprId, String messageName, String fieldName, Type fieldType) - throws InterpreterException { - Descriptor messageDescriptor = verifyMessageType(metadata, exprId, messageName); - verifyField(metadata, exprId, messageDescriptor, fieldName); - FieldDescriptor fd = messageDescriptor.findFieldByName(fieldName); - return (builder, value) -> { - try { - Message singleton = - (Message) messageProvider.createMessage(messageName, ImmutableMap.of(fieldName, value)); - return builder.clearField(fd).mergeFrom(singleton); - } catch (IllegalArgumentException e) { - throw new IllegalStateException("[internal] field assignment unexpectedly failed", e); - } - }; - } - - @Override - public FieldClearer makeFieldClearer( - Metadata metadata, long exprId, String messageName, String fieldName) - throws InterpreterException { - Descriptor messageDescriptor = verifyMessageType(metadata, exprId, messageName); - verifyField(metadata, exprId, messageDescriptor, fieldName); - FieldDescriptor fd = messageDescriptor.findFieldByName(fieldName); - return builder -> builder.clearField(fd); - } - - @Override - public MessageBuilderCreator makeMessageBuilderCreator( - Metadata metadata, long exprId, String messageName) throws InterpreterException { - try { - Message emptyProto = (Message) messageProvider.createMessage(messageName, ImmutableMap.of()); - return emptyProto::toBuilder; - } catch (RuntimeException e) { - throw new InterpreterException.Builder(e, e.getMessage()) - .setLocation(metadata, exprId) - .build(); - } - } - - @Override - public Object dynamicGetField( - Metadata metadata, long exprId, Object messageObject, String fieldName) - throws InterpreterException { - try { - return messageProvider.selectField(messageObject, fieldName); - } catch (IllegalArgumentException e) { - throw new InterpreterException.Builder(e.getMessage()).setLocation(metadata, exprId).build(); - } - } - - @Override - public boolean dynamicHasField( - Metadata metadata, long exprId, Object messageObject, String fieldName) - throws InterpreterException { - try { - return messageProvider.hasField(messageObject, fieldName).equals(true); - } catch (IllegalArgumentException e) { - throw new InterpreterException.Builder(e.getMessage()).setLocation(metadata, exprId).build(); - } - } - - @Override - public FieldGetter makeExtensionGetter(Metadata metadata, long exprId, String extensionName) - throws InterpreterException { - throw unsupportedProtoExtensions(metadata, exprId); - } - - @Override - public FieldTester makeExtensionTester(Metadata metadata, long exprId, String extensionName) - throws InterpreterException { - throw unsupportedProtoExtensions(metadata, exprId); - } - - @Override - public FieldAssigner makeExtensionAssigner( - Metadata metadata, long exprId, String extensionName, Type extensionType) - throws InterpreterException { - throw unsupportedProtoExtensions(metadata, exprId); - } - - @Override - public FieldClearer makeExtensionClearer(Metadata metadata, long exprId, String extensionName) - throws InterpreterException { - throw unsupportedProtoExtensions(metadata, exprId); - } - - private InterpreterException unsupportedProtoExtensions(Metadata metadata, long exprId) { - return new InterpreterException.Builder("proto extensions not supported") - .setLocation(metadata, exprId) - .build(); - } - - private Descriptor verifyMessageType(Metadata metadata, long id, String messageName) - throws InterpreterException { - return messageLookup - .apply(messageName) - .orElseThrow( - () -> - new InterpreterException.Builder("cannot resolve '%s' as a message", messageName) - .setLocation(metadata, id) - .build()); - } - - private static void verifyField( - Metadata metadata, long id, Descriptor messageDescriptor, String field) - throws InterpreterException { - FieldDescriptor fieldDescriptor = messageDescriptor.findFieldByName(field); - if (fieldDescriptor == null) { - throw new InterpreterException.Builder( - "field '%s' is not declared in message '%s'", field, messageDescriptor.getName()) - .setLocation(metadata, id) - .build(); - } - } - - private static ImmutableMap makeValueMap( - List fieldNames, Iterable fieldValues) { - int i = 0; - ImmutableMap.Builder builder = ImmutableMap.builder(); - for (Object value : fieldValues) { - builder.put(fieldNames.get(i), value); - ++i; - } - return builder.buildOrThrow(); - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/ProtoFieldAssignment.java b/legacy/java/dev/cel/legacy/runtime/async/ProtoFieldAssignment.java deleted file mode 100644 index 5b8a0337e..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/ProtoFieldAssignment.java +++ /dev/null @@ -1,546 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static dev.cel.legacy.runtime.async.Canonicalization.asInstanceOf; - -import dev.cel.expr.Type; -import com.google.common.primitives.Ints; -import com.google.common.primitives.UnsignedInts; -import com.google.common.primitives.UnsignedLong; -import com.google.errorprone.annotations.Immutable; -import com.google.protobuf.Any; -import com.google.protobuf.BoolValue; -import com.google.protobuf.ByteString; -import com.google.protobuf.BytesValue; -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.DoubleValue; -import com.google.protobuf.DynamicMessage; -import com.google.protobuf.FloatValue; -import com.google.protobuf.Int32Value; -import com.google.protobuf.Int64Value; -import com.google.protobuf.ListValue; -import com.google.protobuf.MapEntry; -import com.google.protobuf.Message; -import com.google.protobuf.NullValue; -import com.google.protobuf.StringValue; -import com.google.protobuf.Struct; -import com.google.protobuf.UInt32Value; -import com.google.protobuf.UInt64Value; -import com.google.protobuf.Value; -import dev.cel.checker.Types; -import dev.cel.common.CelErrorCode; -import dev.cel.common.CelRuntimeException; -import dev.cel.legacy.runtime.async.MessageProcessor.FieldAssigner; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.Metadata; -import java.util.Map; - -/** - * Descriptor- and type-director assignment of values to proto fields. - * - *

This class provides tools for creating {@code Assigner} instances. An {@code Assigner} is the - * mechanism that assigns a value to a proto field after first translating it from its canonical CEL - * runtime representation to the type that is required for the field in question. - */ -final class ProtoFieldAssignment { - - /** - * Represents the transformations to proto field value types from their corresponding canonical - * CEL runtime representations. A {@link Protoizer} is roughly the inverse of a corresponding - * {@link Canonicalization.Canonicalizer}. - * - *

If the value cannot be converted to its proto representation at runtime, a corresponding - * {@code RuntimeException} is thrown. - * - *

Each instance of this interface corresponds to a possible type of proto message field, and T - * is the Java type of that field, i.e., something acceptable to the {@code setField} or {@code - * addRepeatedField} methods of {@code MessageOrBuilder}. - */ - @Immutable - @FunctionalInterface - private interface Protoizer { - T protoize(Object canonicalValue); - } - - /** - * Returns an {@link FieldAssigner} for the described field that assumes that the value to be - * assigned has the given CEL type. - */ - public static FieldAssigner fieldValueAssigner( - Metadata metadata, long id, FieldDescriptor fd, Type type) throws InterpreterException { - if (fd.isMapField()) { - return mapAssigner(metadata, id, fd, type); - } - if (fd.isRepeated()) { - return listAssigner(metadata, id, fd, type); - } else { - Protoizer protoizer = singleFieldProtoizer(metadata, id, fd, type); - return (builder, value) -> builder.setField(fd, protoizer.protoize(value)); - } - } - - /** - * Returns an {@link FieldAssigner} for the described field which must be a repeated field. The - * value to be assigned must be a CEL list, and the given CEL type should reflect that (unless it - * is DYN). - */ - private static FieldAssigner listAssigner( - Metadata metadata, long id, FieldDescriptor fd, Type listType) throws InterpreterException { - Type elementType = Types.DYN; - if (!Types.isDynOrError(listType)) { - if (listType.getTypeKindCase() != Type.TypeKindCase.LIST_TYPE) { - throw new InterpreterException.Builder("value for repeated field does not have type list") - .setLocation(metadata, id) - .build(); - } - elementType = listType.getListType().getElemType(); - } - Protoizer protoizer = singleFieldProtoizer(metadata, id, fd, elementType); - return (builder, listValue) -> { - builder.clearField(fd); - for (Object element : asInstanceOf(Iterable.class, listValue)) { - builder.addRepeatedField(fd, protoizer.protoize(element)); - } - return builder; - }; - } - - /** - * Returns an {@link FieldAssigner} for the described field which must be a map field. The value - * to be assigned must be a CEL map, and the given CEL type should reflect that (unless it is - * DYN). - */ - private static FieldAssigner mapAssigner( - Metadata metadata, long id, FieldDescriptor fd, Type mapType) throws InterpreterException { - Descriptor entryDescriptor = fd.getMessageType(); - FieldDescriptor keyDescriptor = entryDescriptor.findFieldByNumber(1); - FieldDescriptor valueDescriptor = entryDescriptor.findFieldByNumber(2); - Type keyType = Types.DYN; - Type valueType = Types.DYN; - if (!Types.isDynOrError(mapType)) { - switch (mapType.getTypeKindCase()) { - case MAP_TYPE: - keyType = mapType.getMapType().getKeyType(); - valueType = mapType.getMapType().getValueType(); - break; - default: - throw new InterpreterException.Builder("value for map field does not have map type") - .setLocation(metadata, id) - .build(); - } - } - Protoizer keyProtoizer = singleFieldProtoizer(metadata, id, keyDescriptor, keyType); - Protoizer valueProtoizer = singleFieldProtoizer(metadata, id, valueDescriptor, valueType); - MapEntry protoMapEntry = - MapEntry.newDefaultInstance( - entryDescriptor, - keyDescriptor.getLiteType(), - getDefaultValueForMaybeMessage(keyDescriptor), - valueDescriptor.getLiteType(), - getDefaultValueForMaybeMessage(valueDescriptor)); - return (builder, value) -> { - builder.clearField(fd); - ((Map) asInstanceOf(Map.class, value)) - .entrySet() - .forEach( - entry -> - builder.addRepeatedField( - fd, - protoMapEntry.toBuilder() - .setKey(keyProtoizer.protoize(entry.getKey())) - .setValue(valueProtoizer.protoize(entry.getValue())) - .build())); - return builder; - }; - } - - /** Returns the default value for a field that can be a proto message */ - private static Object getDefaultValueForMaybeMessage(FieldDescriptor descriptor) { - if (descriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE) { - return DynamicMessage.getDefaultInstance(descriptor.getMessageType()); - } - return descriptor.getDefaultValue(); - } - - /** - * Returns a {@link Protoizer} for converting CEL values of the specified CEL type to the type - * appropriate for the described field. The field cannot be a map field, and the returned - * protoizer deals with only a single value even if the field is repeated. - */ - private static Protoizer singleFieldProtoizer( - Metadata metadata, long id, FieldDescriptor fd, Type type) throws InterpreterException { - // Possible future improvement: Consider verifying the CEL type. - switch (fd.getType()) { - case SFIXED32: - case SINT32: - case INT32: - return value -> intCheckedCast(((Number) value).longValue()); - case FIXED32: - case UINT32: - return value -> unsignedIntCheckedCast(((Number) value).longValue()); - case FIXED64: - case UINT64: - return value -> ((Number) value).longValue(); - case ENUM: - return value -> - fd.getEnumType().findValueByNumberCreatingIfUnknown((int) ((Number) value).longValue()); - case FLOAT: - return value -> doubleToFloat(((Number) value).doubleValue()); - case MESSAGE: - return protoProtoizer(metadata, id, fd.getMessageType(), type); - default: - return x -> x; - } - } - - /** - * Returns a protoizer that converts a CEL value to a proto message. Such protoizers are simply - * the identity if the CEL value is already a proto message. However, if the target message type - * is `Any` or any of the known wrapper protos or JSON protos, then the result will be a protoizer - * that wraps its non-proto argument accordingly. - */ - private static Protoizer protoProtoizer( - Metadata metadata, long id, Descriptor d, Type type) throws InterpreterException { - switch (d.getFullName()) { - case "google.protobuf.Any": - return anyProtoizer(metadata, id, type); - case "google.protobuf.Value": - return jsonValueProtoizer(metadata, id, type); - case "google.protobuf.ListValue": - return jsonListProtoizer(metadata, id, type); - case "google.protobuf.Struct": - return jsonStructProtoizer(metadata, id, type); - case "google.protobuf.Int64Value": - return ProtoFieldAssignment::toInt64Value; - case "google.protobuf.UInt64Value": - return ProtoFieldAssignment::toUInt64Value; - case "google.protobuf.Int32Value": - return ProtoFieldAssignment::toInt32Value; - case "google.protobuf.UInt32Value": - return ProtoFieldAssignment::toUInt32Value; - case "google.protobuf.DoubleValue": - return ProtoFieldAssignment::toDoubleValue; - case "google.protobuf.FloatValue": - return ProtoFieldAssignment::toFloatValue; - case "google.protobuf.BoolValue": - return ProtoFieldAssignment::toBoolValue; - case "google.protobuf.StringValue": - return ProtoFieldAssignment::toStringValue; - case "google.protobuf.BytesValue": - return ProtoFieldAssignment::toBytesValue; - default: - return x -> asInstanceOf(Message.class, x); - } - } - - /** - * Returns a protoizer that wraps the value in an {@link Any}, after suitably wrapping non-proto - * values in wrapper protos. - */ - private static Protoizer anyProtoizer(Metadata metadata, long id, Type type) - throws InterpreterException { - Protoizer messageProtoizer = wrapIfNecessaryProtoizer(metadata, id, type); - return x -> Any.pack(messageProtoizer.protoize(x)); - } - - /** - * Returns a protoizer that wraps values of the given CEL type in proto wrappes that make them - * suitable for further wrapping them with {@link Any}. Values that are already protos are not - * further wrapped. - * - *

Notice that thanks to the knowledge of the CEL type it is possible to distinguish between - * signed and unsigned values even though the CEL runtime representation does not carry such - * information. - * - *

If the CEL type is not known (i.e., DYN), then a fallback protoizer that uses only dynamic - * information is returned. - */ - private static Protoizer wrapIfNecessaryProtoizer( - Metadata metadata, long id, Type type) throws InterpreterException { - switch (type.getTypeKindCase()) { - case LIST_TYPE: - return jsonListProtoizer(metadata, id, type); - case MAP_TYPE: - return jsonStructProtoizer(metadata, id, type); - case PRIMITIVE: - { - switch (type.getPrimitive()) { - case BOOL: - return ProtoFieldAssignment::toBoolValue; - case INT64: - return ProtoFieldAssignment::toInt64Value; - case UINT64: - return ProtoFieldAssignment::toUInt64Value; - case DOUBLE: - return ProtoFieldAssignment::toDoubleValue; - case STRING: - return ProtoFieldAssignment::toStringValue; - case BYTES: - return ProtoFieldAssignment::toBytesValue; - default: - return ProtoFieldAssignment::dynamicWrapIfNecessary; - } - } - case MESSAGE_TYPE: - return x -> asInstanceOf(Message.class, x); - default: - return ProtoFieldAssignment::dynamicWrapIfNecessary; - } - } - - /** Fallback protoizer returned by {@code dynamicWrapIfNecessary} in the dynamic case. */ - private static Message dynamicWrapIfNecessary(Object value) { - if (value instanceof Message) { - return (Message) value; - } - if (value instanceof Long) { - return Int64Value.of((Long) value); - } - if (value instanceof Double) { - return DoubleValue.of((Double) value); - } - if (value instanceof Boolean) { - return BoolValue.of((Boolean) value); - } - if (value instanceof String) { - return StringValue.of((String) value); - } - if (value instanceof ByteString) { - return BytesValue.of((ByteString) value); - } - if (value instanceof Iterable) { - return dynamicJsonList(value); - } - if (value instanceof Map) { - return dynamicJsonStruct(value); - } - throw new IllegalArgumentException( - "Unsupported type to pack to Any:" + value.getClass().getSimpleName()); - } - - /** - * Returns a protoizer that converts a CEL value to a JSON value, i.e., an instance of {@link - * Value}. - * - *

The construction is type-directed based on the known CEL type of the incoming value, falling - * back to a fully dynamic mechanism where type DYN is encountered. - */ - private static Protoizer jsonValueProtoizer(Metadata metadata, long id, Type type) - throws InterpreterException { - switch (type.getTypeKindCase()) { - case NULL: - return ignored -> dynamicJsonValue(null); - case LIST_TYPE: - { - Protoizer listProtoizer = jsonListProtoizer(metadata, id, type); - return value -> Value.newBuilder().setListValue(listProtoizer.protoize(value)).build(); - } - case MAP_TYPE: - { - Protoizer structProtoizer = jsonStructProtoizer(metadata, id, type); - return value -> - Value.newBuilder().setStructValue(structProtoizer.protoize(value)).build(); - } - case PRIMITIVE: - { - switch (type.getPrimitive()) { - case BOOL: - return x -> Value.newBuilder().setBoolValue(asInstanceOf(Boolean.class, x)).build(); - case INT64: - case UINT64: - case DOUBLE: - return x -> - Value.newBuilder() - .setNumberValue(asInstanceOf(Number.class, x).doubleValue()) - .build(); - case STRING: - return x -> Value.newBuilder().setStringValue(asInstanceOf(String.class, x)).build(); - default: - return ProtoFieldAssignment::dynamicJsonValue; - } - } - default: - return ProtoFieldAssignment::dynamicJsonValue; - } - } - - /** - * Returns a protoizer that turns a CEL list into a JSON list, i.e., an instance of {@link - * ListValue}. - * - *

The construction is type-directed based on the known CEL type of the incoming value, falling - * back to a fully dynamic mechanism where type DYN is encountered. - */ - private static Protoizer jsonListProtoizer(Metadata metadata, long id, Type type) - throws InterpreterException { - FieldAssigner assigner = listAssigner(metadata, id, LIST_VALUE_VALUES, type); - return listValue -> { - ListValue.Builder builder = ListValue.newBuilder(); - assigner.assign(builder, listValue); - return builder.build(); - }; - } - - /** - * Returns a protoizer that turns a CEL map into a JSON struct, i.e., an instance of {@link - * Struct}. - * - *

The construction is type-directed based on the known CEL type of the incoming value, falling - * back to a fully dynamic mechanism where type DYN is encountered. - */ - private static Protoizer jsonStructProtoizer(Metadata metadata, long id, Type type) - throws InterpreterException { - FieldAssigner assigner = mapAssigner(metadata, id, STRUCT_FIELDS, type); - return mapValue -> { - Struct.Builder builder = Struct.newBuilder(); - assigner.assign(builder, mapValue); - return builder.build(); - }; - } - - /** Fallback protoizer returned by {@code jsonValueProtoizer} in the dynamic case. */ - private static Value dynamicJsonValue(Object object) { - Value.Builder builder = Value.newBuilder(); - if (object == null || object instanceof NullValue) { - builder.setNullValue(NullValue.NULL_VALUE); - } else if (object instanceof Boolean) { - builder.setBoolValue((Boolean) object); - } else if (object instanceof Number) { - builder.setNumberValue(((Number) object).doubleValue()); - } else if (object instanceof String) { - builder.setStringValue((String) object); - } else if (object instanceof Map) { - builder.setStructValue(dynamicJsonStruct(object)); - } else if (object instanceof Iterable) { - builder.setListValue(dynamicJsonList(object)); - } else { - throw new IllegalArgumentException("[internal] value cannot be converted to JSON"); - } - return builder.build(); - } - - /** - * Converts a CEL value to a JSON list (i.e., a {@link ListValue} instance). No exception is - * expected to be thrown since a suitable CEL type is passed when constructing the protoizer. - */ - private static ListValue dynamicJsonList(Object object) { - try { - return jsonListProtoizer(DUMMY_METADATA, 0, DYNAMIC_JSON_LIST_TYPE).protoize(object); - } catch (InterpreterException e) { - throw new AssertionError("unexpected exception", e); - } - } - - /** - * Converts a CEL value to a JSON struct (i.e., a {@link Struct} instance). No exception is - * expected to be thrown since a suitable CEL type is passed when constructing the protoizer. - */ - private static Struct dynamicJsonStruct(Object object) { - try { - return jsonStructProtoizer(DUMMY_METADATA, 0, DYNAMIC_JSON_MAP_TYPE).protoize(object); - } catch (InterpreterException e) { - throw new AssertionError("unexpected exception", e); - } - } - - /** Converts a CEL value to a wrapped int64. */ - private static Message toInt64Value(Object x) { - return Int64Value.of(asInstanceOf(Long.class, x)); - } - - /** Converts a CEL value to a wrapped uint64. */ - private static Message toUInt64Value(Object x) { - if (x instanceof UnsignedLong) { - return UInt64Value.of(asInstanceOf(UnsignedLong.class, x).longValue()); - } - return UInt64Value.of(asInstanceOf(Long.class, x)); - } - - /** Converts a CEL value to a wrapped int32. */ - private static Message toInt32Value(Object x) { - return Int32Value.of(intCheckedCast(asInstanceOf(Long.class, x))); - } - - /** Converts a CEL value to a wrapped uint32. */ - private static Message toUInt32Value(Object x) { - if (x instanceof UnsignedLong) { - return UInt64Value.of( - unsignedIntCheckedCast(asInstanceOf(UnsignedLong.class, x).longValue())); - } - return UInt32Value.of(unsignedIntCheckedCast(asInstanceOf(Long.class, x))); - } - - /** Converts a CEL value to a wrapped boolean. */ - private static Message toBoolValue(Object x) { - return BoolValue.of(asInstanceOf(Boolean.class, x)); - } - - /** Converts a CEL value to a wrapped double. */ - private static Message toDoubleValue(Object x) { - return DoubleValue.of(asInstanceOf(Double.class, x)); - } - - /** Converts a CEL value to a wrapped float. */ - private static Message toFloatValue(Object x) { - return FloatValue.of(doubleToFloat(asInstanceOf(Double.class, x))); - } - - /** Converts a CEL value to a wrapped string. */ - private static Message toStringValue(Object x) { - return StringValue.of(asInstanceOf(String.class, x)); - } - - /** Converts a CEL value to a wrapped bytes value. */ - private static Message toBytesValue(Object x) { - return BytesValue.of(asInstanceOf(ByteString.class, x)); - } - - /** - * Coerces a {@code double} into a {@code float}, throwing an {@link IllegalArgumentException} - * when it does not fit. - */ - private static float doubleToFloat(double d) { - float f = (float) d; - if (d != f) { - throw new IllegalArgumentException("double out of range for conversion to float"); - } - return f; - } - - private static final FieldDescriptor STRUCT_FIELDS = - Struct.getDescriptor().findFieldByName("fields"); - private static final FieldDescriptor LIST_VALUE_VALUES = - ListValue.getDescriptor().findFieldByName("values"); - private static final Type DYNAMIC_JSON_MAP_TYPE = Types.createMap(Types.STRING, Types.DYN); - private static final Type DYNAMIC_JSON_LIST_TYPE = Types.createList(Types.DYN); - private static final Metadata DUMMY_METADATA = - new Metadata() { - @Override - public String getLocation() { - return ""; - } - - @Override - public int getPosition(long exprId) { - return 0; - } - }; - - private static int intCheckedCast(long value) { - try { - return Ints.checkedCast(value); - } catch (IllegalArgumentException e) { - throw new CelRuntimeException(e, CelErrorCode.NUMERIC_OVERFLOW); - } - } - - private static int unsignedIntCheckedCast(long value) { - try { - return UnsignedInts.checkedCast(value); - } catch (IllegalArgumentException e) { - throw new CelRuntimeException(e, CelErrorCode.NUMERIC_OVERFLOW); - } - } - - private ProtoFieldAssignment() {} -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/ResolverAdapter.java b/legacy/java/dev/cel/legacy/runtime/async/ResolverAdapter.java deleted file mode 100644 index bae02ed7f..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/ResolverAdapter.java +++ /dev/null @@ -1,40 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static com.google.common.util.concurrent.Futures.immediateFuture; -import static com.google.common.util.concurrent.MoreExecutors.directExecutor; -import static dev.cel.legacy.runtime.async.Canonicalization.canonicalizeProto; - -import com.google.common.util.concurrent.FluentFuture; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.protobuf.Message; -import dev.cel.common.CelOptions; -import java.util.function.Supplier; - -/** Adapts an {@link AsyncGlobalResolver} to act as an {@link AsyncCanonicalResolver}. */ -final class ResolverAdapter implements AsyncCanonicalResolver { - - final AsyncGlobalResolver asyncGlobalResolver; - private final CelOptions celOptions; - - ResolverAdapter(AsyncGlobalResolver asyncGlobalResolver, CelOptions celOptions) { - this.asyncGlobalResolver = asyncGlobalResolver; - this.celOptions = celOptions; - } - - @Override - public Supplier> resolve(String name) { - return () -> - FluentFuture.from(asyncGlobalResolver.resolve(name)) - .transformAsync( - value -> { - if (value == null) { - throw new IllegalArgumentException("name not bound: '" + name + "'"); - } - if (!(value instanceof Message)) { - return immediateFuture(value); - } - return immediateFuture(canonicalizeProto((Message) value, celOptions)); - }, - directExecutor()); - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/StackOffsetFinder.java b/legacy/java/dev/cel/legacy/runtime/async/StackOffsetFinder.java deleted file mode 100644 index 33cb50d76..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/StackOffsetFinder.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import com.google.errorprone.annotations.Immutable; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.Metadata; -import javax.annotation.Nullable; - -/** - * Interface for finding the stack offset of the named local variable relative to an implicit - * lexical scoping structure. - */ -@Immutable -@FunctionalInterface -public interface StackOffsetFinder { - int findStackOffset(@Nullable Metadata metadata, long exprId, String name) - throws InterpreterException; -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/StandardConstructs.java b/legacy/java/dev/cel/legacy/runtime/async/StandardConstructs.java deleted file mode 100644 index 173ad6715..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/StandardConstructs.java +++ /dev/null @@ -1,342 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static com.google.common.util.concurrent.MoreExecutors.directExecutor; -import static dev.cel.legacy.runtime.async.EvaluationHelpers.asBoolean; -import static dev.cel.legacy.runtime.async.EvaluationHelpers.asBooleanExpression; -import static dev.cel.legacy.runtime.async.EvaluationHelpers.immediateException; -import static dev.cel.legacy.runtime.async.EvaluationHelpers.immediateValue; - -import dev.cel.expr.Value; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.util.concurrent.FluentFuture; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.re2j.Pattern; -import dev.cel.common.CelErrorCode; -import dev.cel.common.CelOptions; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.Metadata; -import dev.cel.runtime.RuntimeHelpers; -import java.util.List; -import java.util.Map; -import java.util.function.BiFunction; -import java.util.function.Function; -import javax.annotation.Nullable; - -/** - * Provides implementations of all "standard" constructs that are typically inlined by the CEL - * interpreter. - */ -public final class StandardConstructs { - - private final TypeResolver typeResolver; - private final CelOptions celOptions; - - public StandardConstructs(TypeResolver typeResolver, CelOptions celOptions) { - this.typeResolver = typeResolver; - this.celOptions = celOptions; - } - - /** Adds all standard constructs to the given registrar. */ - // This method reference implements @Immutable interface SimpleCallConstructor, but the - // declaration of type - // 'dev.cel.legacy.runtime.async.async.StandardConstructs' is not annotated - // with @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - public void addAllTo(FunctionRegistrar registrar) { - /** Identity function. Calls of this are completely short-circuited and are, thus, "free". */ - registrar.addCallConstructor("identity", (md, id, args) -> args.get(0).expression()); - - /** CEL ternary conditional expression: _ ? _ : _ */ - registrar.addCallConstructor("conditional", StandardConstructs::constructConditional); - - /** CEL logical AND: _ && _ */ - registrar.addCallConstructor( - "logical_and", (md, id, args) -> constructLogicalConnective(true, md, args)); - - /** CEL logical OR: _ || _ */ - registrar.addCallConstructor( - "logical_or", (md, id, args) -> constructLogicalConnective(false, md, args)); - - /** Special internal binding used by certain comprehension macros. */ - registrar.addCallConstructor( - "not_strictly_false", StandardConstructs::constructNotStrictlyFalse); - - /** CEL "type" function. */ - registrar.addCallConstructor("type", this::constructType); - - /** - * CEL regexp matching. This pulls the construction of the matcher into the preparation phase if - * the regexp argument is constant. - */ - registrar.addCallConstructor("matches", this::constructRegexpMatch); - registrar.addCallConstructor("matches_string", this::constructRegexpMatch); - - /** - * CEL list membership: _ in _. This turns constant lists into Java hash sets during the - * preparation phase, for improved performance. - */ - registrar.addCallConstructor( - "in_list", - (md, id, args) -> - constructCollectionMembershipTest( - args, - (element, listObject) -> ((List) listObject).contains(element), - listObject -> ImmutableSet.copyOf((List) listObject))); - - /** - * CEL map key membership: _ in _. This turns the keysets of constant maps into Java hash sets - * during the preparation phase, for improved performance. - */ - registrar.addCallConstructor( - "in_map", - (md, id, args) -> - constructCollectionMembershipTest( - args, - (element, mapObject) -> ((Map) mapObject).containsKey(element), - mapObject -> ImmutableSet.copyOf(((Map) mapObject).keySet()))); - } - - /** Compiles the conditional operator {@code _?_:_} */ - private static CompiledExpression constructConditional( - Metadata metadata, long exprId, List compiledArguments) - throws InterpreterException { - IdentifiedCompiledExpression compiledCondition = compiledArguments.get(0); - CompiledExpression compiledThen = compiledArguments.get(1).expression(); - CompiledExpression compiledElse = compiledArguments.get(2).expression(); - return asBooleanExpression(compiledCondition.expression(), metadata, compiledCondition.id()) - .map( - (executableCondition, conditionEffect) -> { - ExecutableExpression executableThen = compiledThen.toExecutable(); - ExecutableExpression executableElse = compiledElse.toExecutable(); - return CompiledExpression.executable( - stack -> - executableCondition - .execute(stack) - .transformAsync( - condition -> - asBoolean(condition) - ? executableThen.execute(stack) - : executableElse.execute(stack), - directExecutor()), - conditionEffect.meet(compiledThen.effect()).meet(compiledElse.effect())); - }, - constantCondition -> asBoolean(constantCondition) ? compiledThen : compiledElse, - t -> CompiledExpression.throwing(t)); - } - - /** - * Compile a logical connective (AND or OR). This implements short-circuiting on the first operand - * that finishes execution. - * - *

The unit value is the neutral element of the operation in question. If one operand's value - * is unit, then the result is whatever the other operand does. On the other hand, if one operand - * yields the opposite of unit (= !unit), then the overall outcome is !unit regardless of the - * other operand's behavior. - */ - private static CompiledExpression constructLogicalConnective( - boolean unit, Metadata metadata, List compiledArguments) - throws InterpreterException { - IdentifiedCompiledExpression identifiedLeft = compiledArguments.get(0); - IdentifiedCompiledExpression identifiedRight = compiledArguments.get(1); - CompiledExpression compiledLeft = - asBooleanExpression(identifiedLeft.expression(), metadata, identifiedLeft.id()); - CompiledExpression compiledRight = - asBooleanExpression(identifiedRight.expression(), metadata, identifiedRight.id()); - if (compiledLeft.isConstant()) { - return (asBoolean(compiledLeft.constant()) == unit) ? compiledRight : compiledLeft; - } - if (compiledRight.isConstant()) { - return (asBoolean(compiledRight.constant()) == unit) ? compiledLeft : compiledRight; - } - - if (compiledLeft.isThrowing() && compiledRight.isThrowing()) { - // Both operands are throwing: arbitrarily pick the left exception to be propagated. - return compiledLeft; - } - - // Neither operand is constant and not both are simultaneously throwing, so perform everything - // at runtime. - ExecutableExpression executableLeft = compiledLeft.toExecutable(); - ExecutableExpression executableRight = compiledRight.toExecutable(); - Effect effect = compiledLeft.effect().meet(compiledRight.effect()); - - return CompiledExpression.executable( - stack -> { - ImmutableList> orderedFutures = - Futures.inCompletionOrder( - ImmutableList.of(executableLeft.execute(stack), executableRight.execute(stack))); - FluentFuture firstFuture = FluentFuture.from(orderedFutures.get(0)); - FluentFuture secondFuture = FluentFuture.from(orderedFutures.get(1)); - return firstFuture - .transformAsync( - first -> (asBoolean(first) == unit) ? secondFuture : immediateValue(!unit), - directExecutor()) - .catchingAsync( - Exception.class, - firstExn -> - secondFuture.transformAsync( - second -> - (asBoolean(second) == unit) - ? immediateException(firstExn) - : immediateValue(!unit), - directExecutor()), - directExecutor()); - }, - effect); - } - - /** Compiles the internal "not_strictly_false" function. */ - private static CompiledExpression constructNotStrictlyFalse( - Metadata metadata, long id, List compiledArguments) - throws InterpreterException { - IdentifiedCompiledExpression identified = compiledArguments.get(0); - CompiledExpression argument = - asBooleanExpression(identified.expression(), metadata, identified.id()); - return argument.map( - (executableArgument, effect) -> - CompiledExpression.executable( - stack -> - executableArgument - .execute(stack) - .catchingAsync( - Exception.class, t -> immediateValue(true), directExecutor()), - effect), - CompiledExpression::constant, - t -> CompiledExpression.constant(true)); - } - - /** Compiles the CEL "type" function. */ - // This lambda implements @Immutable interface 'ExecutableExpression', but accesses instance - // method(s) 'resolveType' on 'StandardConstructs' which is not @Immutable. - @SuppressWarnings("Immutable") - private CompiledExpression constructType( - Metadata metadata, long id, List compiledArguments) - throws InterpreterException { - IdentifiedCompiledExpression identified = compiledArguments.get(0); - long argId = identified.id(); - Value checkedTypeValue = typeResolver.adaptType(identified.type()); - return identified - .expression() - .mapNonThrowing( - e -> - stack -> - e.execute(stack) - .transformAsync( - t -> immediateValue(resolveType(metadata, argId, t, checkedTypeValue)), - directExecutor()), - c -> CompiledExpression.constant(resolveType(metadata, argId, c, checkedTypeValue))); - } - - /** Helper used by the CEL "type" function implementation. */ - private Object resolveType( - Metadata metadata, long exprId, Object obj, @Nullable Value checkedTypeValue) - throws InterpreterException { - @Nullable Object typeValue = typeResolver.resolveObjectType(obj, checkedTypeValue); - if (typeValue != null) { - return typeValue; - } - throw new InterpreterException.Builder( - "expected a runtime type for '%s', but found none.", obj.getClass().getSimpleName()) - .setErrorCode(CelErrorCode.TYPE_NOT_FOUND) - .setLocation(metadata, exprId) - .build(); - } - - /** Compiles regular expression matching. */ - private CompiledExpression constructRegexpMatch( - Metadata metadata, long id, List compiledArguments) - throws InterpreterException { - CompiledExpression compiledString = compiledArguments.get(0).expression(); - CompiledExpression compiledPattern = compiledArguments.get(1).expression(); - return compiledPattern.map( - (executableRegexp, regexpEffect) -> { - ExecutableExpression executableString = compiledString.toExecutable(); - return CompiledExpression.executable( - stack -> - executableString - .execute(stack) - .transformAsync( - string -> - executableRegexp - .execute(stack) - .transformAsync( - regexp -> - immediateValue( - RuntimeHelpers.matches( - (String) string, (String) regexp, celOptions)), - directExecutor()), - directExecutor()), - regexpEffect.meet(compiledString.effect())); - }, - constantRegexp -> { - Pattern pattern = RuntimeHelpers.compilePattern((String) constantRegexp); - return compiledString.mapNonThrowing( - executableString -> - stack -> - executableString - .execute(stack) - .transformAsync( - string -> { - if (!celOptions.enableRegexPartialMatch()) { - return immediateValue(pattern.matches((String) string)); - } - return immediateValue(pattern.matcher((String) string).find()); - }, - directExecutor()), - constantString -> - CompiledExpression.constant( - !celOptions.enableRegexPartialMatch() - ? pattern.matches((String) constantString) - : pattern.matcher((String) constantString).find())); - }, - t -> CompiledExpression.throwing(t)); - } - - /** Compiles collection membership tests. */ - // This lambda implements @Immutable interface 'ExecutableExpression', but the declaration of type - // 'java.util.function.BiFunction' is not - // annotated with @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - private static CompiledExpression constructCollectionMembershipTest( - List compiledArguments, - BiFunction dynamicTest, // (element, collection) -> boolean - Function> makeConstantSet) - throws InterpreterException { // collection -> set - CompiledExpression compiledElement = compiledArguments.get(0).expression(); - CompiledExpression compiledCollection = compiledArguments.get(1).expression(); - return compiledCollection.map( - (executableCollection, collectionEffect) -> { - ExecutableExpression executableElement = compiledElement.toExecutable(); - return CompiledExpression.executable( - stack -> - executableElement - .execute(stack) - .transformAsync( - element -> - executableCollection - .execute(stack) - .transformAsync( - collection -> - immediateValue(dynamicTest.apply(element, collection)), - directExecutor()), - directExecutor()), - collectionEffect.meet(compiledElement.effect())); - }, - constantCollectionObject -> { - ImmutableSet constantSet = makeConstantSet.apply(constantCollectionObject); - return compiledElement.mapNonThrowing( - executableElement -> - stack -> - executableElement - .execute(stack) - .transformAsync( - element -> immediateValue(constantSet.contains(element)), - directExecutor()), - constantElement -> - CompiledExpression.constant(constantSet.contains(constantElement))); - }, - t -> CompiledExpression.throwing(t)); - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/StandardTypeResolver.java b/legacy/java/dev/cel/legacy/runtime/async/StandardTypeResolver.java deleted file mode 100644 index 7c2f943cb..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/StandardTypeResolver.java +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright 2022 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.legacy.runtime.async; - -import static com.google.common.base.Preconditions.checkNotNull; - -import dev.cel.expr.Type; -import dev.cel.expr.Type.PrimitiveType; -import dev.cel.expr.Type.TypeKindCase; -import dev.cel.expr.Value; -import dev.cel.expr.Value.KindCase; -import com.google.common.collect.ImmutableMap; -import com.google.common.primitives.UnsignedLong; -import com.google.errorprone.annotations.Immutable; -import com.google.protobuf.ByteString; -import com.google.protobuf.MessageOrBuilder; -import com.google.protobuf.NullValue; -import dev.cel.common.CelOptions; -import dev.cel.common.annotations.Internal; -import dev.cel.common.types.CelKind; -import dev.cel.common.types.CelType; -import dev.cel.common.types.TypeType; -import java.util.Collection; -import java.util.Map; -import java.util.Optional; -import org.jspecify.annotations.Nullable; - -/** - * The {@code StandardTypeResolver} implements the {@link TypeResolver} and resolves types supported - * by the CEL standard environment. - * - *

CEL Library Internals. Do Not Use. - * - * @deprecated Migrate to CEL-Java fluent APIs. See go/cel-java-migration-guide - */ -@Immutable -@Internal -@Deprecated -public final class StandardTypeResolver implements TypeResolver { - - /** - * Obtain a singleton instance of the {@link StandardTypeResolver} appropriate for the {@code - * celOptions} provided. - */ - public static TypeResolver getInstance(CelOptions celOptions) { - return celOptions.enableUnsignedLongs() ? INSTANCE_WITH_UNSIGNED_LONGS : INSTANCE; - } - - private static final TypeResolver INSTANCE = - new StandardTypeResolver(commonTypes(/* unsignedLongs= */ false)); - - private static final TypeResolver INSTANCE_WITH_UNSIGNED_LONGS = - new StandardTypeResolver(commonTypes(/* unsignedLongs= */ true)); - - // Type of type which is modelled as a value instance rather than as a Java POJO. - private static final Value TYPE_VALUE = createType("type"); - - // Built-in types. - private static ImmutableMap> commonTypes(boolean unsignedLongs) { - return ImmutableMap.>builder() - .put(createType("bool"), Boolean.class) - .put(createType("bytes"), ByteString.class) - .put(createType("double"), Double.class) - .put(createType("int"), Long.class) - .put(createType("uint"), unsignedLongs ? UnsignedLong.class : Long.class) - .put(createType("string"), String.class) - .put(createType("null_type"), NullValue.class) - // Aggregate types. - .put(createType("list"), Collection.class) - .put(createType("map"), Map.class) - // Optional type - .put(createType("optional_type"), Optional.class) - .buildOrThrow(); - } - - private final ImmutableMap> types; - - private StandardTypeResolver(ImmutableMap> types) { - this.types = types; - } - - @Nullable - @Override - public Value resolveObjectType(Object obj, @Nullable Value checkedTypeValue) { - if (checkedTypeValue != null && (obj instanceof Long || obj instanceof NullValue)) { - return checkedTypeValue; - } - return resolveObjectType(obj); - } - - @Nullable - private Value resolveObjectType(Object obj) { - for (Value type : types.keySet()) { - Class impl = types.get(type); - // Generally, the type will be an instance of a class. - if (impl.isInstance(obj)) { - return type; - } - } - // In the case 'type' values, the obj will be a api.expr.Value. - if (obj instanceof Value) { - Value objVal = (Value) obj; - if (objVal.getKindCase() == KindCase.TYPE_VALUE) { - return TYPE_VALUE; - } - } - // Otherwise, this is a protobuf type. - if (obj instanceof MessageOrBuilder) { - MessageOrBuilder msg = (MessageOrBuilder) obj; - return createType(msg.getDescriptorForType().getFullName()); - } - return null; - } - - /** {@inheritDoc} */ - @Override - public @Nullable Value adaptType(CelType type) { - checkNotNull(type); - // TODO: Add enum type support here. - Value.Builder typeValue = Value.newBuilder(); - switch (type.kind()) { - case OPAQUE: - case STRUCT: - return typeValue.setTypeValue(type.name()).build(); - case LIST: - return typeValue.setTypeValue("list").build(); - case MAP: - return typeValue.setTypeValue("map").build(); - case TYPE: - CelType typeOfType = ((TypeType) type).type(); - if (typeOfType.kind() == CelKind.DYN) { - return typeValue.setTypeValue("type").build(); - } - return adaptType(typeOfType); - case NULL_TYPE: - return typeValue.setTypeValue("null_type").build(); - case DURATION: - return typeValue.setTypeValue("google.protobuf.Duration").build(); - case TIMESTAMP: - return typeValue.setTypeValue("google.protobuf.Timestamp").build(); - case BOOL: - return typeValue.setTypeValue("bool").build(); - case BYTES: - return typeValue.setTypeValue("bytes").build(); - case DOUBLE: - return typeValue.setTypeValue("double").build(); - case INT: - return typeValue.setTypeValue("int").build(); - case STRING: - return typeValue.setTypeValue("string").build(); - case UINT: - return typeValue.setTypeValue("uint").build(); - default: - break; - } - return null; - } - - /** {@inheritDoc} */ - @Override - @Deprecated - public @Nullable Value adaptType(@Nullable Type type) { - if (type == null) { - return null; - } - // TODO: Add enum type support here. - Value.Builder typeValue = Value.newBuilder(); - switch (type.getTypeKindCase()) { - case ABSTRACT_TYPE: - return typeValue.setTypeValue(type.getAbstractType().getName()).build(); - case MESSAGE_TYPE: - return typeValue.setTypeValue(type.getMessageType()).build(); - case LIST_TYPE: - return typeValue.setTypeValue("list").build(); - case MAP_TYPE: - return typeValue.setTypeValue("map").build(); - case TYPE: - Type typeOfType = type.getType(); - if (typeOfType.getTypeKindCase() == TypeKindCase.DYN) { - return typeValue.setTypeValue("type").build(); - } - return adaptType(typeOfType); - case NULL: - return typeValue.setTypeValue("null_type").build(); - case PRIMITIVE: - return adaptPrimitive(type.getPrimitive()); - case WRAPPER: - return adaptPrimitive(type.getWrapper()); - case WELL_KNOWN: - switch (type.getWellKnown()) { - case DURATION: - return typeValue.setTypeValue("google.protobuf.Duration").build(); - case TIMESTAMP: - return typeValue.setTypeValue("google.protobuf.Timestamp").build(); - default: - break; - } - break; - default: - break; - } - return null; - } - - @Nullable - private static Value adaptPrimitive(PrimitiveType primitiveType) { - Value.Builder typeValue = Value.newBuilder(); - switch (primitiveType) { - case BOOL: - return typeValue.setTypeValue("bool").build(); - case BYTES: - return typeValue.setTypeValue("bytes").build(); - case DOUBLE: - return typeValue.setTypeValue("double").build(); - case INT64: - return typeValue.setTypeValue("int").build(); - case STRING: - return typeValue.setTypeValue("string").build(); - case UINT64: - return typeValue.setTypeValue("uint").build(); - default: - break; - } - return null; - } - - private static Value createType(String name) { - return Value.newBuilder().setTypeValue(name).build(); - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/TypeDirectedMessageProcessor.java b/legacy/java/dev/cel/legacy/runtime/async/TypeDirectedMessageProcessor.java deleted file mode 100644 index 538bb4983..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/TypeDirectedMessageProcessor.java +++ /dev/null @@ -1,286 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static dev.cel.legacy.runtime.async.Canonicalization.asMessage; -import static dev.cel.legacy.runtime.async.Canonicalization.fieldHasWrapperType; -import static dev.cel.legacy.runtime.async.Canonicalization.fieldValueCanonicalizer; - -import dev.cel.expr.Type; -import com.google.auto.value.AutoValue; -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.ExtensionRegistry.ExtensionInfo; -import com.google.protobuf.Message; -import com.google.protobuf.MessageOrBuilder; -import com.google.protobuf.NullValue; -import dev.cel.common.CelOptions; -import dev.cel.legacy.runtime.async.Canonicalization.Canonicalizer; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.Metadata; -import java.util.Optional; -import java.util.function.Function; -import java.util.function.Supplier; - -/** - * An implementation of {@link MessageProcessor} that performs as much work as possible during the - * static phase by taking advantage of available proto descriptor information as well as CEL type - * information during the first phase (i.e., during the construction of {@link MessageCreator}, - * {@link FieldGetter}, and {@link FieldTester} objects). - */ -public final class TypeDirectedMessageProcessor implements MessageProcessor { - - /** - * Information about a message consisting of the message {@link Descriptor} and a supplier of - * builders for constructing new message instances. - */ - @AutoValue - public abstract static class MessageInfo { - public abstract Descriptor descriptor(); - - public abstract Supplier messageBuilderSupplier(); - - public static MessageInfo of( - Descriptor descriptor, Supplier messageBuilderSupplier) { - return new AutoValue_TypeDirectedMessageProcessor_MessageInfo( - descriptor, messageBuilderSupplier); - } - } - - private final Function> messageInfoLookup; - private final Function> extensionLookup; - private final CelOptions celOptions; - - public TypeDirectedMessageProcessor( - Function> messageInfoLookup, - Function> extensionLookup) { - this(messageInfoLookup, extensionLookup, CelOptions.LEGACY); - } - - public TypeDirectedMessageProcessor( - Function> messageInfoLookup, - Function> extensionLookup, - CelOptions celOptions) { - this.messageInfoLookup = messageInfoLookup; - this.extensionLookup = extensionLookup; - this.celOptions = celOptions; - } - - // This lambda implements @Immutable interface 'FieldGetter', but 'Object' is mutable - @SuppressWarnings("Immutable") - private FieldGetter getterFromDescriptor( - FieldDescriptor fd, Optional maybeDefaultInstance) { - Canonicalizer canonicalizer = fieldValueCanonicalizer(fd, celOptions); - if (fieldHasWrapperType(fd)) { - return value -> { - MessageOrBuilder message = asMessage(value); - return message.hasField(fd) - ? canonicalizer.canonicalize(message.getField(fd)) - : NullValue.NULL_VALUE; - }; - } - if (!fd.isRepeated() && maybeDefaultInstance.isPresent()) { - // If the field is an extension field but is not present, - // then message.getField(fd) will return an instance of - // DynamicMessage. To avoid that, this code explicitly - // uses the default instance from the registry instead. - Object defaultInstance = maybeDefaultInstance.get(); - return value -> { - MessageOrBuilder message = asMessage(value); - return message.hasField(fd) - ? canonicalizer.canonicalize(message.getField(fd)) - : defaultInstance; - }; - } - return value -> canonicalizer.canonicalize(asMessage(value).getField(fd)); - } - - @Override - public FieldGetter makeFieldGetter( - Metadata metadata, long exprId, String messageName, String fieldName) - throws InterpreterException { - Descriptor descriptor = getDescriptor(metadata, exprId, messageName); - FieldDescriptor fd = getFieldDescriptor(metadata, exprId, descriptor, fieldName); - if (fd.getType() == FieldDescriptor.Type.MESSAGE) { - // Check to see whether the message descriptor for the field's type uses - // the "canonical" descriptor for that type (as obtained by messageInfoLookup). - Descriptor fieldValueDescriptor = fd.getMessageType(); - MessageInfo fieldMessageInfo = - getMessageInfo(metadata, exprId, fieldValueDescriptor.getFullName()); - Descriptor canonicalDescriptor = fieldMessageInfo.descriptor(); - if (fieldValueDescriptor != canonicalDescriptor) { // pointer inequality! - // The descriptor is not canonical, so use an explicit presence test - // and use the canonical default instance. Otherwise the default instance - // would use the wrong (non-canonical) descriptor and cause problems down - // the road. - return getterFromDescriptor( - fd, Optional.of(fieldMessageInfo.messageBuilderSupplier().get().build())); - } - } - return getterFromDescriptor(fd, Optional.empty()); - } - - @Override - public FieldTester makeFieldTester( - Metadata metadata, long exprId, String messageName, String fieldName) - throws InterpreterException { - Descriptor descriptor = getDescriptor(metadata, exprId, messageName); - FieldDescriptor fd = getFieldDescriptor(metadata, exprId, descriptor, fieldName); - return fd.isRepeated() - ? value -> asMessage(value).getRepeatedFieldCount(fd) > 0 - : value -> asMessage(value).hasField(fd); - } - - @Override - public FieldAssigner makeFieldAssigner( - Metadata metadata, long exprId, String messageName, String fieldName, Type fieldType) - throws InterpreterException { - Descriptor descriptor = getDescriptor(metadata, exprId, messageName); - FieldDescriptor fd = getFieldDescriptor(metadata, exprId, descriptor, fieldName); - return ProtoFieldAssignment.fieldValueAssigner(metadata, exprId, fd, fieldType); - } - - // This method reference implements @Immutable interface MessageBuilderCreator, but the - // declaration of type 'java.util.function.Supplier' is not - // annotated with @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - @Override - public MessageBuilderCreator makeMessageBuilderCreator( - Metadata metadata, long exprId, String messageName) throws InterpreterException { - Supplier builderSupplier = - getMessageInfo(metadata, exprId, messageName).messageBuilderSupplier(); - return builderSupplier::get; - } - - @Override - public FieldClearer makeFieldClearer( - Metadata metadata, long exprId, String messageName, String fieldName) - throws InterpreterException { - Descriptor descriptor = getDescriptor(metadata, exprId, messageName); - FieldDescriptor fd = getFieldDescriptor(metadata, exprId, descriptor, fieldName); - return builder -> builder.clearField(fd); - } - - @Override - public Object dynamicGetField( - Metadata metadata, long exprId, Object messageObject, String fieldName) - throws InterpreterException { - MessageOrBuilder message = expectMessage(metadata, exprId, messageObject); - Descriptor descriptor = message.getDescriptorForType(); - FieldDescriptor fieldDescriptor = getFieldDescriptor(metadata, exprId, descriptor, fieldName); - if (fieldHasWrapperType(fieldDescriptor) && !message.hasField(fieldDescriptor)) { - return NullValue.NULL_VALUE; - } - Object value = message.getField(fieldDescriptor); - return fieldValueCanonicalizer(fieldDescriptor, celOptions).canonicalize(value); - } - - @Override - public boolean dynamicHasField( - Metadata metadata, long exprId, Object messageObject, String fieldName) - throws InterpreterException { - MessageOrBuilder message = expectMessage(metadata, exprId, messageObject); - Descriptor descriptor = message.getDescriptorForType(); - FieldDescriptor fieldDescriptor = getFieldDescriptor(metadata, exprId, descriptor, fieldName); - return message.hasField(fieldDescriptor); - } - - @Override - public FieldGetter makeExtensionGetter(Metadata metadata, long exprId, String extensionName) - throws InterpreterException { - ExtensionInfo extensionInfo = getExtensionInfo(metadata, exprId, extensionName); - if (extensionInfo.defaultInstance != null) { - Descriptor fieldValueDescriptor = extensionInfo.defaultInstance.getDescriptorForType(); - MessageInfo fieldMessageInfo = - getMessageInfo(metadata, exprId, fieldValueDescriptor.getFullName()); - Descriptor canonicalDescriptor = fieldMessageInfo.descriptor(); - // If the default instance provided by the extension info does not use the - // "canonical" descriptor (as obtained by messageInfoLookup), then generate - // a new canonical default instance instead. - if (fieldValueDescriptor != canonicalDescriptor) { - return getterFromDescriptor( - extensionInfo.descriptor, - Optional.of(fieldMessageInfo.messageBuilderSupplier().get().build())); - } else { - return getterFromDescriptor( - extensionInfo.descriptor, Optional.of(extensionInfo.defaultInstance)); - } - } - return getterFromDescriptor(extensionInfo.descriptor, Optional.empty()); - } - - @Override - public FieldTester makeExtensionTester(Metadata metadata, long exprId, String extensionName) - throws InterpreterException { - FieldDescriptor fd = getExtensionInfo(metadata, exprId, extensionName).descriptor; - return fd.isRepeated() - ? value -> asMessage(value).getRepeatedFieldCount(fd) > 0 - : value -> asMessage(value).hasField(fd); - } - - @Override - public FieldAssigner makeExtensionAssigner( - Metadata metadata, long exprId, String extensionName, Type extensionType) - throws InterpreterException { - FieldDescriptor fd = getExtensionInfo(metadata, exprId, extensionName).descriptor; - return ProtoFieldAssignment.fieldValueAssigner(metadata, exprId, fd, extensionType); - } - - @Override - public FieldClearer makeExtensionClearer(Metadata metadata, long exprId, String extensionName) - throws InterpreterException { - FieldDescriptor fd = getExtensionInfo(metadata, exprId, extensionName).descriptor; - return builder -> builder.clearField(fd); - } - - private MessageInfo getMessageInfo(Metadata metadata, long id, String messageName) - throws InterpreterException { - return messageInfoLookup - .apply(messageName) - .orElseThrow( - () -> - new InterpreterException.Builder("cannot resolve '%s' as a message", messageName) - .setLocation(metadata, id) - .build()); - } - - private Descriptor getDescriptor(Metadata metadata, long id, String messageName) - throws InterpreterException { - return getMessageInfo(metadata, id, messageName).descriptor(); - } - - private static MessageOrBuilder expectMessage( - Metadata metadata, long exprId, Object messageObject) throws InterpreterException { - if (messageObject instanceof MessageOrBuilder) { - return (MessageOrBuilder) messageObject; - } - throw new InterpreterException.Builder( - "expected an instance of 'com.google.protobuf.MessageOrBuilder' " + "but found '%s'", - messageObject.getClass().getName()) - .setLocation(metadata, exprId) - .build(); - } - - private static FieldDescriptor getFieldDescriptor( - Metadata metadata, long exprId, Descriptor descriptor, String fieldName) - throws InterpreterException { - FieldDescriptor fieldDescriptor = descriptor.findFieldByName(fieldName); - if (fieldDescriptor == null) { - throw new InterpreterException.Builder( - "field '%s' is not declared in message '%s'", fieldName, descriptor.getName()) - .setLocation(metadata, exprId) - .build(); - } - return fieldDescriptor; - } - - private ExtensionInfo getExtensionInfo(Metadata metadata, long exprId, String extensionName) - throws InterpreterException { - return extensionLookup - .apply(extensionName) - .orElseThrow( - () -> - new InterpreterException.Builder( - "cannot resolve '%s' as message extension", extensionName) - .setLocation(metadata, exprId) - .build()); - } -} diff --git a/legacy/java/dev/cel/legacy/runtime/async/TypeResolver.java b/legacy/java/dev/cel/legacy/runtime/async/TypeResolver.java deleted file mode 100644 index f3ad4452a..000000000 --- a/legacy/java/dev/cel/legacy/runtime/async/TypeResolver.java +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2023 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.legacy.runtime.async; - -import dev.cel.expr.Type; -import dev.cel.expr.Value; -import com.google.errorprone.annotations.Immutable; -import dev.cel.common.annotations.Internal; -import dev.cel.common.types.CelType; -import org.jspecify.annotations.Nullable; - -/** - * The {@code TypeResolver} determines the CEL type of Java-native values and assists with adapting - * check-time types to runtime values. - * - *

CEL Library Internals. Do Not Use. - */ -@Immutable -@Internal -public interface TypeResolver { - - /** - * Resolve the CEL type of the {@code obj}, using the {@code checkedTypeValue} as hint for type - * disambiguation. - * - *

The {@code checkedTypeValue} indicates the statically determined type of the object at - * check-time. Often, the check-time and runtime phases agree, but there are cases where the - * runtime type is ambiguous, as is the case when a {@code Long} value is supplied as this could - * either be an int, uint, or enum type. - * - *

Type resolution is biased toward the runtime value type, given the dynamically typed nature - * of CEL. - */ - @Nullable Value resolveObjectType(Object obj, @Nullable Value checkedTypeValue); - - /** - * Adapt the check-time {@code type} instance to a runtime {@code Value}. - * - *

When the checked {@code type} does not have a runtime equivalent, e.g. {@code Type#DYN}, the - * return value will be {@code null}. - */ - @Nullable Value adaptType(CelType type); - - /** - * Adapt the check-time {@code type} instance to a runtime {@code Value}. - * - *

When the checked {@code type} does not have a runtime equivalent, e.g. {@code Type#DYN}, the - * return value will be {@code null}. - * - * @deprecated use {@link #adaptType(CelType)} instead. This only exists to maintain compatibility - * with legacy async evaluator. - */ - @Deprecated - @Nullable Value adaptType(@Nullable Type type); -} diff --git a/legacy/javatests/dev/cel/legacy/runtime/async/AsyncInterpreterTest.java b/legacy/javatests/dev/cel/legacy/runtime/async/AsyncInterpreterTest.java deleted file mode 100644 index 5032608a3..000000000 --- a/legacy/javatests/dev/cel/legacy/runtime/async/AsyncInterpreterTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package dev.cel.legacy.runtime.async; - -// import com.google.testing.testsize.MediumTest; -import dev.cel.common.CelOptions; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; - -/** - * Tests for {@code Interpreter} and related functionality via the asynchronous evaluator. - * - *

TODO: Remove inheritance from `BaseInterpreterTest` and fork these tests. - */ -// @MediumTest -@RunWith(Parameterized.class) -public class AsyncInterpreterTest extends BaseInterpreterTest { - - private static final CelOptions ASYNC_TEST_OPTIONS = - CelOptions.current() - .enableTimestampEpoch(true) - .enableUnsignedLongs(true) - .enableHeterogeneousNumericComparisons(true) - // comprehension iteration limits are not supported in the async evaluation stacks. - .comprehensionMaxIterations(-1) - .build(); - - public AsyncInterpreterTest(boolean declareWithCelType, Eval eval) { - super(declareWithCelType, eval); - } - - @Parameters - public static List testData() { - return new ArrayList<>( - Arrays.asList( - new Object[][] { - // ASYNC_PROTO_TYPE - { - /* declareWithCelType= */ false, - new EvalAsync( - TEST_FILE_DESCRIPTORS, ASYNC_TEST_OPTIONS, /* typeDirectedProcessor= */ false) - }, - // ASYNC_PROTO_TYPE_DIRECTED_PROCESSOR - { - /* declareWithCelType= */ false, - new EvalAsync( - TEST_FILE_DESCRIPTORS, ASYNC_TEST_OPTIONS, /* typeDirectedProcessor= */ true) - }, - // ASYNC_CEL_TYPE - { - /* declareWithCelType= */ true, - new EvalAsync( - TEST_FILE_DESCRIPTORS, ASYNC_TEST_OPTIONS, /* typeDirectedProcessor= */ false) - }, - // ASYNC_CEL_TYPE_DIRECTED_PROCESSOR - { - /* declareWithCelType= */ true, - new EvalAsync( - TEST_FILE_DESCRIPTORS, ASYNC_TEST_OPTIONS, /* typeDirectedProcessor= */ true) - }, - })); - } -} diff --git a/legacy/javatests/dev/cel/legacy/runtime/async/BUILD.bazel b/legacy/javatests/dev/cel/legacy/runtime/async/BUILD.bazel deleted file mode 100644 index 91566fe78..000000000 --- a/legacy/javatests/dev/cel/legacy/runtime/async/BUILD.bazel +++ /dev/null @@ -1,102 +0,0 @@ -load("@rules_java//java:java_library.bzl", "java_library") -load("//:testing.bzl", "junit4_test_suites") - -package( - default_applicable_licenses = ["//:license"], -) - -java_library( - name = "DynamicEnvTest", - testonly = 1, - srcs = ["DynamicEnvTest.java"], - deps = [ - "//:java_truth", - "//third_party/java/cel/legacy:async_runtime", - "//third_party/java/cel/legacy:dummy_async_context", - "@maven//:com_google_guava_guava", - "@maven//:junit_junit", - ], -) - -java_library( - name = "mediumtests", - testonly = 1, - srcs = - [ - "AsyncInterpreterTest.java", - "FuturesInterpreterTest.java", - "FuturesInterpreterWithMessageProcessorTest.java", - ], - resources = [ - "//common/resources/testdata/proto3:test_all_types_file_descriptor_set", - "//runtime/testdata", - ], - deps = [ - ":base_interpreter_test", - "//java/com/google/common/context", - # "//java/com/google/testing/testsize:annotations", - "//common", - "//common:options", - "//common/resources/testdata/proto3:standalone_global_enum_java_proto", - "//common/types:cel_proto_types", - "//third_party/java/cel/legacy:async_runtime", - "//runtime:interpreter", - "//testing:cel_baseline_test_case", - "@maven//:junit_junit", - "@cel_spec//proto/cel/expr:expr_java_proto", - "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", - "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", - ], -) - -java_library( - name = "base_interpreter_test", - testonly = 1, - srcs = [ - "BaseInterpreterTest.java", - "Eval.java", - "EvalAsync.java", - ], - resources = [ - "//common/resources/testdata/proto3:test_all_types_file_descriptor_set", - "//runtime/testdata", - ], - deps = [ - "//:java_truth", - "//common", - "//common:options", - "//common:proto_ast", - "//common/internal:cel_descriptor_pools", - "//common/internal:file_descriptor_converter", - "//common/resources/testdata/proto3:standalone_global_enum_java_proto", - "//common/types:cel_proto_types", - "//java/com/google/common/context", - "//java/com/google/protobuf/contrib/descriptor/pool:mutable_descriptor_pool", - "//java/com/google/security/context/testing:fake_unvalidated_security_context_builder", - "//runtime:interpreter", - "//runtime:linked_message_factory", - "//testing:cel_baseline_test_case", - "//third_party/java/cel/legacy:async_runtime", - "@cel_spec//proto/cel/expr:expr_java_proto", - "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", - "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", - "@maven//:com_google_protobuf_protobuf_java", - "@maven//:com_google_protobuf_protobuf_java_util", - "@maven//:junit_junit", - ], -) - -junit4_test_suites( - name = "testsuites", - sizes = [ - "small", - "medium", - ], - deps = [ - ":DynamicEnvTest", - ":mediumtests", - ], -) diff --git a/legacy/javatests/dev/cel/legacy/runtime/async/BaseInterpreterTest.java b/legacy/javatests/dev/cel/legacy/runtime/async/BaseInterpreterTest.java deleted file mode 100644 index f4609ad6f..000000000 --- a/legacy/javatests/dev/cel/legacy/runtime/async/BaseInterpreterTest.java +++ /dev/null @@ -1,1683 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.truth.Truth.assertThat; -import static java.nio.charset.StandardCharsets.UTF_8; - -import dev.cel.expr.CheckedExpr; -import dev.cel.expr.Type; -import dev.cel.expr.Type.AbstractType; -import com.google.common.base.Ascii; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.io.Resources; -import com.google.common.primitives.UnsignedLong; -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.protobuf.Any; -import com.google.protobuf.BoolValue; -import com.google.protobuf.ByteString; -import com.google.protobuf.BytesValue; -import com.google.protobuf.DescriptorProtos.FileDescriptorSet; -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Descriptors.FileDescriptor; -import com.google.protobuf.DoubleValue; -import com.google.protobuf.Duration; -import com.google.protobuf.DynamicMessage; -import com.google.protobuf.FloatValue; -import com.google.protobuf.Int32Value; -import com.google.protobuf.Int64Value; -import com.google.protobuf.ListValue; -import com.google.protobuf.NullValue; -import com.google.protobuf.StringValue; -import com.google.protobuf.Struct; -import com.google.protobuf.TextFormat; -import com.google.protobuf.Timestamp; -import com.google.protobuf.UInt32Value; -import com.google.protobuf.UInt64Value; -import com.google.protobuf.Value; -import com.google.protobuf.util.Timestamps; -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelProtoAbstractSyntaxTree; -import dev.cel.common.internal.DefaultDescriptorPool; -import dev.cel.common.internal.FileDescriptorSetConverter; -import dev.cel.common.types.CelProtoTypes; -import dev.cel.expr.conformance.proto3.TestAllTypes; -import dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum; -import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; -import dev.cel.runtime.Activation; -import dev.cel.runtime.InterpreterException; -import dev.cel.testing.CelBaselineTestCase; -import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.stream.LongStream; -import org.junit.Test; - -/** Base class for evaluation outputs that can be stored and used as a baseline test. */ -public abstract class BaseInterpreterTest extends CelBaselineTestCase { - - protected static final Descriptor TEST_ALL_TYPE_DYNAMIC_DESCRIPTOR = - getDeserializedTestAllTypeDescriptor(); - - protected static final ImmutableList TEST_FILE_DESCRIPTORS = - ImmutableList.of( - TestAllTypes.getDescriptor().getFile(), - StandaloneGlobalEnum.getDescriptor().getFile(), - TEST_ALL_TYPE_DYNAMIC_DESCRIPTOR.getFile()); - - private final Eval eval; - - private static Descriptor getDeserializedTestAllTypeDescriptor() { - try { - String fdsContent = readResourceContent("testdata/proto3/test_all_types.fds"); - FileDescriptorSet fds = TextFormat.parse(fdsContent, FileDescriptorSet.class); - ImmutableSet fileDescriptors = FileDescriptorSetConverter.convert(fds); - - return fileDescriptors.stream() - .flatMap(f -> f.getMessageTypes().stream()) - .filter( - x -> - x.getFullName().equals("dev.cel.testing.testdata.serialized.proto3.TestAllTypes")) - .findAny() - .orElseThrow( - () -> - new IllegalStateException( - "Could not find deserialized TestAllTypes descriptor.")); - } catch (IOException e) { - throw new RuntimeException("Error loading TestAllTypes descriptor", e); - } - } - - public BaseInterpreterTest(boolean declareWithCelType, Eval eval) { - super(declareWithCelType); - this.eval = eval; - } - - /** Helper to run a test for configured instance variables. */ - @CanIgnoreReturnValue // Test generates a file to diff against baseline. Ignoring Intermediary - // evaluation is not a concern. - private Object runTest(Activation activation) throws Exception { - CelAbstractSyntaxTree ast = prepareTest(eval.fileDescriptors()); - if (ast == null) { - return null; - } - assertAstRoundTrip(ast); - - testOutput().println("bindings: " + activation); - Object result = null; - try { - result = eval.eval(ast, activation); - if (result instanceof ByteString) { - // Note: this call may fail for printing byte sequences that are not valid UTF-8, but works - // pretty well for test purposes. - result = ((ByteString) result).toStringUtf8(); - } - testOutput().println("result: " + result); - } catch (InterpreterException e) { - testOutput().println("error: " + e.getMessage()); - testOutput().println("error_code: " + e.getErrorCode()); - } - testOutput().println(); - return result; - } - - /** - * Checks that the CheckedExpr produced by CelCompiler is equal to the one reproduced by the - * native CelAbstractSyntaxTree - */ - private void assertAstRoundTrip(CelAbstractSyntaxTree ast) { - CheckedExpr checkedExpr = CelProtoAbstractSyntaxTree.fromCelAst(ast).toCheckedExpr(); - CelProtoAbstractSyntaxTree protoAst = CelProtoAbstractSyntaxTree.fromCelAst(ast); - assertThat(checkedExpr).isEqualTo(protoAst.toCheckedExpr()); - } - - @Test - public void arithmInt64() throws Exception { - source = "1 < 2 && 1 <= 1 && 2 > 1 && 1 >= 1 && 1 == 1 && 2 != 1"; - runTest(Activation.EMPTY); - - declareVariable("x", CelProtoTypes.INT64); - source = "1 + 2 - x * 3 / x + (x % 3)"; - runTest(Activation.of("x", -5L)); - - declareVariable("y", CelProtoTypes.DYN); - source = "x + y == 1"; - runTest(Activation.of("x", -5L).extend(Activation.of("y", 6))); - } - - @Test - public void arithmInt64_error() throws Exception { - source = "9223372036854775807 + 1"; - runTest(Activation.EMPTY); - - source = "-9223372036854775808 - 1"; - runTest(Activation.EMPTY); - - source = "-(-9223372036854775808)"; - runTest(Activation.EMPTY); - - source = "5000000000 * 5000000000"; - runTest(Activation.EMPTY); - - source = "(-9223372036854775808)/-1"; - runTest(Activation.EMPTY); - - source = "1 / 0"; - runTest(Activation.EMPTY); - - source = "1 % 0"; - runTest(Activation.EMPTY); - } - - @Test - public void arithmUInt64() throws Exception { - source = "1u < 2u && 1u <= 1u && 2u > 1u && 1u >= 1u && 1u == 1u && 2u != 1u"; - runTest(Activation.EMPTY); - - boolean useUnsignedLongs = eval.celOptions().enableUnsignedLongs(); - declareVariable("x", CelProtoTypes.UINT64); - source = "1u + 2u + x * 3u / x + (x % 3u)"; - runTest(Activation.of("x", useUnsignedLongs ? UnsignedLong.valueOf(5L) : 5L)); - - declareVariable("y", CelProtoTypes.DYN); - source = "x + y == 11u"; - runTest( - Activation.of("x", useUnsignedLongs ? UnsignedLong.valueOf(5L) : 5L) - .extend(Activation.of("y", useUnsignedLongs ? UnsignedLong.valueOf(6L) : 6))); - - source = "x - y == 1u"; - runTest( - Activation.of("x", useUnsignedLongs ? UnsignedLong.valueOf(6L) : 6L) - .extend(Activation.of("y", useUnsignedLongs ? UnsignedLong.valueOf(5) : 5))); - } - - @Test - public void arithmUInt64_error() throws Exception { - source = "18446744073709551615u + 1u"; - runTest(Activation.EMPTY); - - source = "0u - 1u"; - runTest(Activation.EMPTY); - - source = "5000000000u * 5000000000u"; - runTest(Activation.EMPTY); - - source = "1u / 0u"; - runTest(Activation.EMPTY); - - source = "1u % 0u"; - runTest(Activation.EMPTY); - } - - @Test - public void arithmDouble() throws Exception { - source = "1.9 < 2.0 && 1.1 <= 1.1 && 2.0 > 1.9 && 1.1 >= 1.1 && 1.1 == 1.1 && 2.0 != 1.9"; - runTest(Activation.EMPTY); - - declareVariable("x", CelProtoTypes.DOUBLE); - source = "1.0 + 2.3 + x * 3.0 / x"; - runTest(Activation.of("x", 3.33)); - - declareVariable("y", CelProtoTypes.DYN); - source = "x + y == 9.99"; - runTest(Activation.of("x", 3.33d).extend(Activation.of("y", 6.66))); - } - - @Test - public void quantifiers() throws Exception { - source = "[1,-2,3].exists_one(x, x > 0)"; - runTest(Activation.EMPTY); - - source = "[-1,-2,3].exists_one(x, x > 0)"; - runTest(Activation.EMPTY); - - source = "[-1,-2,-3].exists(x, x > 0)"; - runTest(Activation.EMPTY); - - source = "[1,-2,3].exists(x, x > 0)"; - runTest(Activation.EMPTY); - - source = "[1,-2,3].all(x, x > 0)"; - runTest(Activation.EMPTY); - - source = "[1,2,3].all(x, x > 0)"; - runTest(Activation.EMPTY); - } - - @Test - public void arithmTimestamp() throws Exception { - container = Type.getDescriptor().getFile().getPackage(); - declareVariable("ts1", CelProtoTypes.TIMESTAMP); - declareVariable("ts2", CelProtoTypes.TIMESTAMP); - declareVariable("d1", CelProtoTypes.DURATION); - Duration d1 = Duration.newBuilder().setSeconds(15).setNanos(25).build(); - Timestamp ts1 = Timestamp.newBuilder().setSeconds(25).setNanos(35).build(); - Timestamp ts2 = Timestamp.newBuilder().setSeconds(10).setNanos(10).build(); - Activation activation = - Activation.of("d1", d1).extend(Activation.of("ts1", ts1)).extend(Activation.of("ts2", ts2)); - - source = "ts1 - ts2 == d1"; - runTest(activation); - - source = "ts1 - d1 == ts2"; - runTest(activation); - - source = "ts2 + d1 == ts1"; - runTest(activation); - - source = "d1 + ts2 == ts1"; - runTest(activation); - } - - @Test - public void arithmDuration() throws Exception { - container = Type.getDescriptor().getFile().getPackage(); - declareVariable("d1", CelProtoTypes.DURATION); - declareVariable("d2", CelProtoTypes.DURATION); - declareVariable("d3", CelProtoTypes.DURATION); - Duration d1 = Duration.newBuilder().setSeconds(15).setNanos(25).build(); - Duration d2 = Duration.newBuilder().setSeconds(10).setNanos(20).build(); - Duration d3 = Duration.newBuilder().setSeconds(25).setNanos(45).build(); - Activation activation = - Activation.of("d1", d1).extend(Activation.of("d2", d2)).extend(Activation.of("d3", d3)); - - source = "d1 + d2 == d3"; - runTest(activation); - - source = "d3 - d1 == d2"; - runTest(activation); - } - - @Test - public void arithCrossNumericTypes() throws Exception { - if (!eval.celOptions().enableUnsignedLongs()) { - skipBaselineVerification(); - return; - } - source = "1.9 < 2 && 1 < 1.1 && 2u < 2.9 && 1.1 < 2u && 1 < 2u && 2u < 3"; - runTest(Activation.EMPTY); - - source = "1.9 <= 2 && 1 <= 1.1 && 2u <= 2.9 && 1.1 <= 2u && 2 <= 2u && 2u <= 2"; - runTest(Activation.EMPTY); - - source = "1.9 > 2 && 1 > 1.1 && 2u > 2.9 && 1.1 > 2u && 2 > 2u && 2u > 2"; - runTest(Activation.EMPTY); - - source = "1.9 >= 2 && 1 >= 1.1 && 2u >= 2.9 && 1.1 >= 2u && 2 >= 2u && 2u >= 2"; - runTest(Activation.EMPTY); - } - - @Test - public void booleans() throws Exception { - declareVariable("x", CelProtoTypes.BOOL); - source = "x ? 1 : 0"; - runTest(Activation.of("x", true)); - runTest(Activation.of("x", false)); - - source = "(1 / 0 == 0 && false) == (false && 1 / 0 == 0)"; - runTest(Activation.EMPTY); - - source = "(1 / 0 == 0 || true) == (true || 1 / 0 == 0)"; - runTest(Activation.EMPTY); - - declareVariable("y", CelProtoTypes.INT64); - source = "1 / y == 1 || true"; - runTest(Activation.of("y", 0L)); - - source = "1 / y == 1 || false"; - runTest(Activation.of("y", 0L)); - - source = "false || 1 / y == 1"; - runTest(Activation.of("y", 0L)); - - source = "1 / y == 1 && true"; - runTest(Activation.of("y", 0L)); - - source = "true && 1 / y == 1"; - runTest(Activation.of("y", 0L)); - - source = "1 / y == 1 && false"; - runTest(Activation.of("y", 0L)); - - source = "(true > false) == true"; - runTest(Activation.EMPTY); - - source = "(true > true) == false"; - runTest(Activation.EMPTY); - - source = "(false > true) == false"; - runTest(Activation.EMPTY); - - source = "(false > false) == false"; - runTest(Activation.EMPTY); - - source = "(true >= false) == true"; - runTest(Activation.EMPTY); - - source = "(true >= true) == true"; - runTest(Activation.EMPTY); - - source = "(false >= false) == true"; - runTest(Activation.EMPTY); - - source = "(false >= true) == false"; - runTest(Activation.EMPTY); - - source = "(false < true) == true"; - runTest(Activation.EMPTY); - - source = "(false < false) == false"; - runTest(Activation.EMPTY); - - source = "(true < false) == false"; - runTest(Activation.EMPTY); - - source = "(true < true) == false"; - runTest(Activation.EMPTY); - - source = "(false <= true) == true"; - runTest(Activation.EMPTY); - - source = "(false <= false) == true"; - runTest(Activation.EMPTY); - - source = "(true <= false) == false"; - runTest(Activation.EMPTY); - - source = "(true <= true) == true"; - runTest(Activation.EMPTY); - } - - @Test - public void strings() throws Exception { - source = "'a' < 'b' && 'a' <= 'b' && 'b' > 'a' && 'a' >= 'a' && 'a' == 'a' && 'a' != 'b'"; - runTest(Activation.EMPTY); - - declareVariable("x", CelProtoTypes.STRING); - source = - "'abc' + x == 'abcdef' && " - + "x.endsWith('ef') && " - + "x.startsWith('d') && " - + "x.contains('de') && " - + "!x.contains('abcdef')"; - runTest(Activation.of("x", "def")); - } - - @Test - public void messages() throws Exception { - TestAllTypes nestedMessage = - TestAllTypes.newBuilder() - .setSingleNestedMessage(NestedMessage.newBuilder().setBb(43)) - .build(); - declareVariable("x", CelProtoTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); - source = "x.single_nested_message.bb == 43 && has(x.single_nested_message)"; - runTest(Activation.of("x", nestedMessage)); - - declareVariable( - "single_nested_message", - CelProtoTypes.createMessage(NestedMessage.getDescriptor().getFullName())); - source = "single_nested_message.bb == 43"; - runTest(Activation.of("single_nested_message", nestedMessage.getSingleNestedMessage())); - - source = "TestAllTypes{single_int64: 1, single_sfixed64: 2, single_int32: 2}.single_int32 == 2"; - container = TestAllTypes.getDescriptor().getFile().getPackage(); - runTest(Activation.EMPTY); - } - - @Test - public void messages_error() throws Exception { - source = "TestAllTypes{single_int32_wrapper: 12345678900}"; - container = TestAllTypes.getDescriptor().getFile().getPackage(); - runTest(Activation.EMPTY); - - source = "TestAllTypes{}.map_string_string.a"; - runTest(Activation.EMPTY); - } - - @Test - public void has() throws Exception { - TestAllTypes nestedMessage = - TestAllTypes.newBuilder() - .setSingleInt32(1) - .setSingleInt64(0L) - .setSingleBoolWrapper(BoolValue.newBuilder().setValue(false)) - .setSingleInt32Wrapper(Int32Value.newBuilder().setValue(42)) - .setOptionalBool(false) - .setOneofBool(false) - .addRepeatedInt32(1) - .putMapInt32Int64(1, 2L) - .setSingleNestedMessage(NestedMessage.newBuilder().setBb(43)) - .build(); - declareVariable("x", CelProtoTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); - source = - "has(x.single_int32) && !has(x.single_int64) && has(x.single_bool_wrapper)" - + " && has(x.single_int32_wrapper) && !has(x.single_int64_wrapper)" - + " && has(x.repeated_int32) && !has(x.repeated_int64)" - + " && has(x.optional_bool) && !has(x.optional_string)" - + " && has(x.oneof_bool) && !has(x.oneof_type)" - + " && has(x.map_int32_int64) && !has(x.map_string_string)" - + " && has(x.single_nested_message) && !has(x.single_duration)"; - runTest(Activation.of("x", nestedMessage)); - } - - @Test - public void duration() throws Exception { - declareVariable("d1", CelProtoTypes.DURATION); - declareVariable("d2", CelProtoTypes.DURATION); - Duration d1010 = Duration.newBuilder().setSeconds(10).setNanos(10).build(); - Duration d1009 = Duration.newBuilder().setSeconds(10).setNanos(9).build(); - Duration d0910 = Duration.newBuilder().setSeconds(9).setNanos(10).build(); - container = Type.getDescriptor().getFile().getPackage(); - - source = "d1 < d2"; - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1009))); - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d0910))); - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1010))); - runTest(Activation.of("d1", d1009).extend(Activation.of("d2", d1010))); - runTest(Activation.of("d1", d0910).extend(Activation.of("d2", d1010))); - - source = "d1 <= d2"; - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1009))); - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d0910))); - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1010))); - runTest(Activation.of("d1", d1009).extend(Activation.of("d2", d1010))); - runTest(Activation.of("d1", d0910).extend(Activation.of("d2", d1010))); - - source = "d1 > d2"; - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1009))); - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d0910))); - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1010))); - runTest(Activation.of("d1", d1009).extend(Activation.of("d2", d1010))); - runTest(Activation.of("d1", d0910).extend(Activation.of("d2", d1010))); - - source = "d1 >= d2"; - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1009))); - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d0910))); - runTest(Activation.of("d1", d1010).extend(Activation.of("d2", d1010))); - runTest(Activation.of("d1", d1009).extend(Activation.of("d2", d1010))); - runTest(Activation.of("d1", d0910).extend(Activation.of("d2", d1010))); - } - - @Test - public void timestamp() throws Exception { - declareVariable("t1", CelProtoTypes.TIMESTAMP); - declareVariable("t2", CelProtoTypes.TIMESTAMP); - Timestamp ts1010 = Timestamp.newBuilder().setSeconds(10).setNanos(10).build(); - Timestamp ts1009 = Timestamp.newBuilder().setSeconds(10).setNanos(9).build(); - Timestamp ts0910 = Timestamp.newBuilder().setSeconds(9).setNanos(10).build(); - container = Type.getDescriptor().getFile().getPackage(); - - source = "t1 < t2"; - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1009))); - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts0910))); - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1010))); - runTest(Activation.of("t1", ts1009).extend(Activation.of("t2", ts1010))); - runTest(Activation.of("t1", ts0910).extend(Activation.of("t2", ts1010))); - - source = "t1 <= t2"; - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1009))); - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts0910))); - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1010))); - runTest(Activation.of("t1", ts1009).extend(Activation.of("t2", ts1010))); - runTest(Activation.of("t1", ts0910).extend(Activation.of("t2", ts1010))); - - source = "t1 > t2"; - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1009))); - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts0910))); - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1010))); - runTest(Activation.of("t1", ts1009).extend(Activation.of("t2", ts1010))); - runTest(Activation.of("t1", ts0910).extend(Activation.of("t2", ts1010))); - - source = "t1 >= t2"; - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1009))); - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts0910))); - runTest(Activation.of("t1", ts1010).extend(Activation.of("t2", ts1010))); - runTest(Activation.of("t1", ts1009).extend(Activation.of("t2", ts1010))); - runTest(Activation.of("t1", ts0910).extend(Activation.of("t2", ts1010))); - } - - @Test - public void nestedEnums() throws Exception { - TestAllTypes nestedEnum = TestAllTypes.newBuilder().setSingleNestedEnum(NestedEnum.BAR).build(); - declareVariable("x", CelProtoTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); - container = TestAllTypes.getDescriptor().getFile().getPackage(); - source = "x.single_nested_enum == TestAllTypes.NestedEnum.BAR"; - runTest(Activation.of("x", nestedEnum)); - - declareVariable("single_nested_enum", CelProtoTypes.INT64); - source = "single_nested_enum == TestAllTypes.NestedEnum.BAR"; - runTest(Activation.of("single_nested_enum", nestedEnum.getSingleNestedEnumValue())); - - source = - "TestAllTypes{single_nested_enum : TestAllTypes.NestedEnum.BAR}.single_nested_enum == 1"; - runTest(Activation.EMPTY); - } - - @Test - public void globalEnums() throws Exception { - declareVariable("x", CelProtoTypes.INT64); - source = "x == dev.cel.testing.testdata.proto3.StandaloneGlobalEnum.SGAR"; - runTest(Activation.of("x", StandaloneGlobalEnum.SGAR.getNumber())); - } - - @Test - public void lists() throws Exception { - declareVariable("x", CelProtoTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); - declareVariable("y", CelProtoTypes.INT64); - container = TestAllTypes.getDescriptor().getFile().getPackage(); - source = "([1, 2, 3] + x.repeated_int32)[3] == 4"; - runTest(Activation.of("x", TestAllTypes.newBuilder().addRepeatedInt32(4).build())); - - source = "!(y in [1, 2, 3]) && y in [4, 5, 6]"; - runTest(Activation.of("y", 4L)); - - source = "TestAllTypes{repeated_int32: [1,2]}.repeated_int32[1] == 2"; - runTest(Activation.EMPTY); - - source = "1 in TestAllTypes{repeated_int32: [1,2]}.repeated_int32"; - runTest(Activation.EMPTY); - - source = "!(4 in [1, 2, 3]) && 1 in [1, 2, 3]"; - runTest(Activation.EMPTY); - - declareVariable("list", CelProtoTypes.createList(CelProtoTypes.INT64)); - - source = "!(4 in list) && 1 in list"; - runTest(Activation.of("list", ImmutableList.of(1L, 2L, 3L))); - - source = "!(y in list)"; - runTest(Activation.copyOf(ImmutableMap.of("y", 4L, "list", ImmutableList.of(1L, 2L, 3L)))); - - source = "y in list"; - runTest(Activation.copyOf(ImmutableMap.of("y", 1L, "list", ImmutableList.of(1L, 2L, 3L)))); - - source = "list[3]"; - runTest(Activation.copyOf(ImmutableMap.of("y", 1L, "list", ImmutableList.of(1L, 2L, 3L)))); - } - - @Test - public void maps() throws Exception { - declareVariable("x", CelProtoTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); - container = TestAllTypes.getDescriptor().getFile().getPackage(); - source = "{1: 2, 3: 4}[3] == 4"; - runTest(Activation.EMPTY); - - // Constant key in constant map. - source = "3 in {1: 2, 3: 4} && !(4 in {1: 2, 3: 4})"; - runTest(Activation.EMPTY); - - source = "x.map_int32_int64[22] == 23"; - runTest(Activation.of("x", TestAllTypes.newBuilder().putMapInt32Int64(22, 23).build())); - - source = "TestAllTypes{map_int32_int64: {21: 22, 22: 23}}.map_int32_int64[22] == 23"; - runTest(Activation.EMPTY); - - source = - "TestAllTypes{oneof_type: NestedTestAllTypes{payload: x}}" - + ".oneof_type.payload.map_int32_int64[22] == 23"; - runTest(Activation.of("x", TestAllTypes.newBuilder().putMapInt32Int64(22, 23).build())); - - declareVariable("y", CelProtoTypes.INT64); - declareVariable("map", CelProtoTypes.createMap(CelProtoTypes.INT64, CelProtoTypes.INT64)); - - // Constant key in variable map. - source = "!(4 in map) && 1 in map"; - runTest(Activation.of("map", ImmutableMap.of(1L, 4L, 2L, 3L, 3L, 2L))); - - // Variable key in constant map. - source = "!(y in {1: 4, 2: 3, 3: 2}) && y in {5: 3, 4: 2, 3: 3}"; - runTest(Activation.of("y", 4L)); - - // Variable key in variable map. - source = "!(y in map) && (y + 3) in map"; - runTest( - Activation.copyOf( - ImmutableMap.of("y", 1L, "map", ImmutableMap.of(4L, 1L, 5L, 2L, 6L, 3L)))); - - // Message value in map - source = "TestAllTypes{map_int64_nested_type:{42:NestedTestAllTypes{payload:TestAllTypes{}}}}"; - runTest(Activation.EMPTY); - - // Repeated key - constant - source = "{true: 1, false: 2, true: 3}[true]"; - runTest(Activation.EMPTY); - - // Repeated key - expressions - declareVariable("b", CelProtoTypes.BOOL); - source = "{b: 1, !b: 2, b: 3}[true]"; - runTest(Activation.of("b", true)); - } - - @Test - public void comprehension() throws Exception { - source = "[0, 1, 2].map(x, x > 0, x + 1) == [2, 3]"; - runTest(Activation.EMPTY); - - source = "[0, 1, 2].exists(x, x > 0)"; - runTest(Activation.EMPTY); - - source = "[0, 1, 2].exists(x, x > 2)"; - runTest(Activation.EMPTY); - } - - @Test - public void abstractType() throws Exception { - Type typeParam = CelProtoTypes.createTypeParam("T"); - Type abstractType = - Type.newBuilder() - .setAbstractType( - AbstractType.newBuilder().setName("vector").addParameterTypes(typeParam)) - .build(); - // Declare a function to create a vector. - declareFunction( - "vector", - globalOverload( - "vector", - ImmutableList.of(CelProtoTypes.createList(typeParam)), - ImmutableList.of("T"), - abstractType)); - eval.registrar() - .add( - "vector", - ImmutableList.of(List.class), - (Object[] args) -> { - List list = (List) args[0]; - return list.toArray(new Object[0]); - }); - // Declare a function to access element of a vector. - declareFunction( - "at", - memberOverload( - "at", - ImmutableList.of(abstractType, CelProtoTypes.INT64), - ImmutableList.of("T"), - typeParam)); - eval.registrar() - .add( - "at", - ImmutableList.of(Object[].class, Long.class), - (Object[] args) -> { - Object[] array = (Object[]) args[0]; - return array[(int) (long) args[1]]; - }); - - source = "vector([1,2,3]).at(1) == 2"; - runTest(Activation.EMPTY); - - source = "vector([1,2,3]).at(1) + vector([7]).at(0)"; - runTest(Activation.EMPTY); - } - - @Test - public void namespacedFunctions() throws Exception { - declareFunction( - "ns.func", - globalOverload( - "ns_func_overload", ImmutableList.of(CelProtoTypes.STRING), CelProtoTypes.INT64)); - declareFunction( - "member", - memberOverload( - "ns_member_overload", - ImmutableList.of(CelProtoTypes.INT64, CelProtoTypes.INT64), - CelProtoTypes.INT64)); - eval.registrar().add("ns_func_overload", String.class, s -> (long) s.length()); - eval.registrar().add("ns_member_overload", Long.class, Long.class, Long::sum); - source = "ns.func('hello')"; - runTest(Activation.EMPTY); - - source = "ns.func('hello').member(ns.func('test'))"; - runTest(Activation.EMPTY); - - source = "{ns.func('test'): 2}"; - runTest(Activation.EMPTY); - - source = "{2: ns.func('test')}"; - runTest(Activation.EMPTY); - - source = "[ns.func('test'), 2]"; - runTest(Activation.EMPTY); - - source = "[ns.func('test')].map(x, x * 2)"; - runTest(Activation.EMPTY); - - source = "[1, 2].map(x, x * ns.func('test'))"; - runTest(Activation.EMPTY); - - container = "ns"; - // Call with the container set as the function's namespace - source = "ns.func('hello')"; - runTest(Activation.EMPTY); - - source = "func('hello')"; - runTest(Activation.EMPTY); - - source = "func('hello').member(func('test'))"; - runTest(Activation.EMPTY); - } - - @Test - public void namespacedVariables() throws Exception { - container = "ns"; - declareVariable("ns.x", CelProtoTypes.INT64); - source = "x"; - runTest(Activation.of("ns.x", 2)); - - container = "dev.cel.testing.testdata.proto3"; - Type messageType = CelProtoTypes.createMessage("cel.expr.conformance.proto3.TestAllTypes"); - declareVariable("dev.cel.testing.testdata.proto3.msgVar", messageType); - source = "msgVar.single_int32"; - runTest( - Activation.of( - "dev.cel.testing.testdata.proto3.msgVar", - TestAllTypes.newBuilder().setSingleInt32(5).build())); - } - - @Test - public void durationFunctions() throws Exception { - declareVariable("d1", CelProtoTypes.DURATION); - Duration d1 = - Duration.newBuilder().setSeconds(25 * 3600 + 59 * 60 + 1).setNanos(11000000).build(); - Duration d2 = - Duration.newBuilder().setSeconds(-(25 * 3600 + 59 * 60 + 1)).setNanos(-11000000).build(); - container = Type.getDescriptor().getFile().getPackage(); - - source = "d1.getHours()"; - runTest(Activation.of("d1", d1)); - runTest(Activation.of("d1", d2)); - - source = "d1.getMinutes()"; - runTest(Activation.of("d1", d1)); - runTest(Activation.of("d1", d2)); - - source = "d1.getSeconds()"; - runTest(Activation.of("d1", d1)); - runTest(Activation.of("d1", d2)); - - source = "d1.getMilliseconds()"; - runTest(Activation.of("d1", d1)); - runTest(Activation.of("d1", d2)); - - declareVariable("val", CelProtoTypes.INT64); - source = "d1.getHours() < val"; - runTest(Activation.of("d1", d1).extend(Activation.of("val", 30L))); - source = "d1.getMinutes() > val"; - runTest(Activation.of("d1", d1).extend(Activation.of("val", 30L))); - source = "d1.getSeconds() > val"; - runTest(Activation.of("d1", d1).extend(Activation.of("val", 30L))); - source = "d1.getMilliseconds() < val"; - runTest(Activation.of("d1", d1).extend(Activation.of("val", 30L))); - } - - @Test - public void timestampFunctions() throws Exception { - declareVariable("ts1", CelProtoTypes.TIMESTAMP); - container = Type.getDescriptor().getFile().getPackage(); - Timestamp ts1 = Timestamp.newBuilder().setSeconds(1).setNanos(11000000).build(); - Timestamp ts2 = Timestamps.fromSeconds(-1); - - source = "ts1.getFullYear(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getFullYear()"; - runTest(Activation.of("ts1", ts1)); - runTest(Activation.of("ts1", ts2)); - source = "ts1.getFullYear(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getFullYear(\"2:00\")"; - runTest(Activation.of("ts1", ts1)); - - source = "ts1.getMonth(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getMonth()"; - runTest(Activation.of("ts1", ts1)); - runTest(Activation.of("ts1", ts2)); - source = "ts1.getMonth(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getMonth(\"-8:15\")"; - runTest(Activation.of("ts1", ts1)); - - source = "ts1.getDayOfYear(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getDayOfYear()"; - runTest(Activation.of("ts1", ts1)); - runTest(Activation.of("ts1", ts2)); - source = "ts1.getDayOfYear(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getDayOfYear(\"-9:00\")"; - runTest(Activation.of("ts1", ts1)); - - source = "ts1.getDayOfMonth(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getDayOfMonth()"; - runTest(Activation.of("ts1", ts1)); - runTest(Activation.of("ts1", ts2)); - source = "ts1.getDayOfMonth(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getDayOfMonth(\"8:00\")"; - runTest(Activation.of("ts1", ts1)); - - source = "ts1.getDate(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getDate()"; - runTest(Activation.of("ts1", ts1)); - runTest(Activation.of("ts1", ts2)); - source = "ts1.getDate(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getDate(\"9:30\")"; - runTest(Activation.of("ts1", ts1)); - - Timestamp tsSunday = Timestamps.fromSeconds(3 * 24 * 3600); - source = "ts1.getDayOfWeek(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", tsSunday)); - source = "ts1.getDayOfWeek()"; - runTest(Activation.of("ts1", tsSunday)); - runTest(Activation.of("ts1", ts2)); - source = "ts1.getDayOfWeek(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", tsSunday)); - source = "ts1.getDayOfWeek(\"-9:30\")"; - runTest(Activation.of("ts1", tsSunday)); - - source = "ts1.getHours(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getHours()"; - runTest(Activation.of("ts1", ts1)); - runTest(Activation.of("ts1", ts2)); - source = "ts1.getHours(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getHours(\"6:30\")"; - runTest(Activation.of("ts1", ts1)); - - source = "ts1.getMinutes(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getMinutes()"; - runTest(Activation.of("ts1", ts1)); - runTest(Activation.of("ts1", ts2)); - source = "ts1.getMinutes(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getMinutes(\"-8:00\")"; - runTest(Activation.of("ts1", ts1)); - - source = "ts1.getSeconds(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getSeconds()"; - runTest(Activation.of("ts1", ts1)); - runTest(Activation.of("ts1", ts2)); - source = "ts1.getSeconds(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getSeconds(\"-8:00\")"; - runTest(Activation.of("ts1", ts1)); - - source = "ts1.getMilliseconds(\"America/Los_Angeles\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getMilliseconds()"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getMilliseconds(\"Indian/Cocos\")"; - runTest(Activation.of("ts1", ts1)); - source = "ts1.getMilliseconds(\"-8:00\")"; - runTest(Activation.of("ts1", ts1)); - - declareVariable("val", CelProtoTypes.INT64); - source = "ts1.getFullYear() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 2013L))); - source = "ts1.getMonth() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 12L))); - source = "ts1.getDayOfYear() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 13L))); - source = "ts1.getDayOfMonth() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 10L))); - source = "ts1.getDate() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); - source = "ts1.getDayOfWeek() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); - source = "ts1.getHours() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); - source = "ts1.getMinutes() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); - source = "ts1.getSeconds() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); - source = "ts1.getMilliseconds() < val"; - runTest(Activation.of("ts1", ts1).extend(Activation.of("val", 15L))); - } - - @Test - public void timeConversions() throws Exception { - container = Type.getDescriptor().getFile().getPackage(); - declareVariable("t1", CelProtoTypes.TIMESTAMP); - - source = "timestamp(\"1972-01-01T10:00:20.021-05:00\")"; - runTest(Activation.EMPTY); - - source = "timestamp(123)"; - runTest(Activation.EMPTY); - - source = "duration(\"15.11s\")"; - runTest(Activation.EMPTY); - - source = "int(t1) == 100"; - runTest(Activation.of("t1", Timestamps.fromSeconds(100))); - - source = "duration(\"1h2m3.4s\")"; - runTest(Activation.EMPTY); - - // Not supported. - source = "duration('inf')"; - runTest(Activation.EMPTY); - - source = "duration(duration('15.0s'))"; // Identity - runTest(Activation.EMPTY); - - source = "timestamp(timestamp(123))"; // Identity - runTest(Activation.EMPTY); - } - - @Test - public void sizeTests() throws Exception { - container = Type.getDescriptor().getFile().getPackage(); - declareVariable("str", CelProtoTypes.STRING); - declareVariable("b", CelProtoTypes.BYTES); - - source = "size(b) == 5 && b.size() == 5"; - runTest(Activation.of("b", ByteString.copyFromUtf8("happy"))); - - source = "size(str) == 5 && str.size() == 5"; - runTest(Activation.of("str", "happy")); - runTest(Activation.of("str", "happ\uDBFF\uDFFC")); - - source = "size({1:14, 2:15}) == 2 && {1:14, 2:15}.size() == 2"; - runTest(Activation.EMPTY); - - source = "size([1,2,3]) == 3 && [1,2,3].size() == 3"; - runTest(Activation.EMPTY); - } - - @Test - public void nonstrictQuantifierTests() throws Exception { - // Plain tests. Everything is constant. - source = "[0, 2, 4].exists(x, 4/x == 2 && 4/(4-x) == 2)"; - runTest(Activation.EMPTY); - - source = "![0, 2, 4].all(x, 4/x != 2 && 4/(4-x) != 2)"; - runTest(Activation.EMPTY); - - declareVariable("four", CelProtoTypes.INT64); - - // Condition is dynamic. - source = "[0, 2, 4].exists(x, four/x == 2 && four/(four-x) == 2)"; - runTest(Activation.of("four", 4L)); - - source = "![0, 2, 4].all(x, four/x != 2 && four/(four-x) != 2)"; - runTest(Activation.of("four", 4L)); - - // Both range and condition are dynamic. - source = "[0, 2, four].exists(x, four/x == 2 && four/(four-x) == 2)"; - runTest(Activation.of("four", 4L)); - - source = "![0, 2, four].all(x, four/x != 2 && four/(four-x) != 2)"; - runTest(Activation.of("four", 4L)); - } - - @Test - public void regexpMatchingTests() throws Exception { - // Constant everything. - source = "matches(\"alpha\", \"^al.*\") == true"; - runTest(Activation.EMPTY); - - source = "matches(\"alpha\", \"^.al.*\") == false"; - runTest(Activation.EMPTY); - - source = "matches(\"alpha\", \".*ha$\") == true"; - runTest(Activation.EMPTY); - - source = "matches(\"alpha\", \"^.*ha.$\") == false"; - runTest(Activation.EMPTY); - - source = "matches(\"alpha\", \"\") == true"; - runTest(Activation.EMPTY); - - source = "matches(\"alpha\", \"ph\") == true"; - runTest(Activation.EMPTY); - - source = "matches(\"alpha\", \"^ph\") == false"; - runTest(Activation.EMPTY); - - source = "matches(\"alpha\", \"ph$\") == false"; - runTest(Activation.EMPTY); - - // Constant everything, receiver-style. - source = "\"alpha\".matches(\"^al.*\") == true"; - runTest(Activation.EMPTY); - - source = "\"alpha\".matches(\"^.al.*\") == false"; - runTest(Activation.EMPTY); - - source = "\"alpha\".matches(\".*ha$\") == true"; - runTest(Activation.EMPTY); - - source = "\"alpha\".matches(\".*ha.$\") == false"; - runTest(Activation.EMPTY); - - source = "\"alpha\".matches(\"\") == true"; - runTest(Activation.EMPTY); - - source = "\"alpha\".matches(\"ph\") == true"; - runTest(Activation.EMPTY); - - source = "\"alpha\".matches(\"^ph\") == false"; - runTest(Activation.EMPTY); - - source = "\"alpha\".matches(\"ph$\") == false"; - runTest(Activation.EMPTY); - - // Constant string. - declareVariable("regexp", CelProtoTypes.STRING); - - source = "matches(\"alpha\", regexp) == true"; - runTest(Activation.of("regexp", "^al.*")); - - source = "matches(\"alpha\", regexp) == false"; - runTest(Activation.of("regexp", "^.al.*")); - - source = "matches(\"alpha\", regexp) == true"; - runTest(Activation.of("regexp", ".*ha$")); - - source = "matches(\"alpha\", regexp) == false"; - runTest(Activation.of("regexp", ".*ha.$")); - - // Constant string, receiver-style. - source = "\"alpha\".matches(regexp) == true"; - runTest(Activation.of("regexp", "^al.*")); - - source = "\"alpha\".matches(regexp) == false"; - runTest(Activation.of("regexp", "^.al.*")); - - source = "\"alpha\".matches(regexp) == true"; - runTest(Activation.of("regexp", ".*ha$")); - - source = "\"alpha\".matches(regexp) == false"; - runTest(Activation.of("regexp", ".*ha.$")); - - // Constant regexp. - declareVariable("s", CelProtoTypes.STRING); - - source = "matches(s, \"^al.*\") == true"; - runTest(Activation.of("s", "alpha")); - - source = "matches(s, \"^.al.*\") == false"; - runTest(Activation.of("s", "alpha")); - - source = "matches(s, \".*ha$\") == true"; - runTest(Activation.of("s", "alpha")); - - source = "matches(s, \"^.*ha.$\") == false"; - runTest(Activation.of("s", "alpha")); - - // Constant regexp, receiver-style. - source = "s.matches(\"^al.*\") == true"; - runTest(Activation.of("s", "alpha")); - - source = "s.matches(\"^.al.*\") == false"; - runTest(Activation.of("s", "alpha")); - - source = "s.matches(\".*ha$\") == true"; - runTest(Activation.of("s", "alpha")); - - source = "s.matches(\"^.*ha.$\") == false"; - runTest(Activation.of("s", "alpha")); - - // No constants. - source = "matches(s, regexp) == true"; - runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", "^al.*"))); - runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", ".*ha$"))); - - source = "matches(s, regexp) == false"; - runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", "^.al.*"))); - runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", ".*ha.$"))); - - // No constants, receiver-style. - source = "s.matches(regexp) == true"; - runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", "^al.*"))); - runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", ".*ha$"))); - - source = "s.matches(regexp) == false"; - runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", "^.al.*"))); - runTest(Activation.copyOf(ImmutableMap.of("s", "alpha", "regexp", ".*ha.$"))); - } - - @Test - public void int64Conversions() throws Exception { - source = "int('-1')"; // string converts to -1 - runTest(Activation.EMPTY); - - source = "int(2.1)"; // double converts to 2 - runTest(Activation.EMPTY); - - source = "int(18446744073709551615u)"; // 2^64-1 should error - runTest(Activation.EMPTY); - - source = "int(1e99)"; // out of range should error - runTest(Activation.EMPTY); - - source = "int(42u)"; // converts to 42 - runTest(Activation.EMPTY); - } - - @Test - public void uint64Conversions() throws Exception { - // The test case `uint(1e19)` succeeds with unsigned longs and fails with longs in a way that - // cannot be easily tested. - if (!eval.celOptions().enableUnsignedLongs()) { - skipBaselineVerification(); - return; - } - source = "uint('1')"; // string converts to 1u - runTest(Activation.EMPTY); - - source = "uint(2.1)"; // double converts to 2u - runTest(Activation.EMPTY); - - source = "uint(-1)"; // should error - runTest(Activation.EMPTY); - - source = "uint(1e19)"; // valid uint but outside of int range - runTest(Activation.EMPTY); - - source = "uint(6.022e23)"; // outside uint range - runTest(Activation.EMPTY); - - source = "uint(42)"; // int converts to 42u - runTest(Activation.EMPTY); - - source = "uint('f1')"; // should error - runTest(Activation.EMPTY); - - source = "uint(1u)"; // identity - runTest(Activation.EMPTY); - - source = "uint(dyn(1u))"; // identity, check dynamic dispatch - runTest(Activation.EMPTY); - } - - @Test - public void doubleConversions() throws Exception { - source = "double('1.1')"; // string converts to 1.1 - runTest(Activation.EMPTY); - - source = "double(2u)"; // uint converts to 2.0 - runTest(Activation.EMPTY); - - source = "double(-1)"; // int converts to -1.0 - runTest(Activation.EMPTY); - - source = "double('bad')"; - runTest(Activation.EMPTY); - - source = "double(1.5)"; // Identity - runTest(Activation.EMPTY); - } - - @Test - public void stringConversions() throws Exception { - source = "string(1.1)"; // double converts to '1.1' - runTest(Activation.EMPTY); - - source = "string(2u)"; // uint converts to '2' - runTest(Activation.EMPTY); - - source = "string(-1)"; // int converts to '-1' - runTest(Activation.EMPTY); - - // Byte literals in Google SQL only take the leading byte of an escape character. - // This means that to translate a byte literal to a UTF-8 encoded string, all bytes must be - // encoded in the literal as they would be laid out in memory for UTF-8, hence the extra octal - // escape to achieve parity with the bidi test below. - source = "string(b'abc\\303\\203')"; - runTest(Activation.EMPTY); // bytes convert to 'abcÃ' - - // Bi-di conversion for strings and bytes for 'abcÃ', note the difference between the string - // and byte literal values. - source = "string(bytes('abc\\303'))"; - runTest(Activation.EMPTY); - - source = "string(timestamp('2009-02-13T23:31:30Z'))"; - runTest(Activation.EMPTY); - - source = "string(duration('1000000s'))"; - runTest(Activation.EMPTY); - - source = "string('hello')"; // Identity - runTest(Activation.EMPTY); - } - - @Test - public void bytes() throws Exception { - source = - "b'a' < b'b' && b'a' <= b'b' && b'b' > b'a' && b'a' >= b'a' && b'a' == b'a' && b'a' !=" - + " b'b'"; - runTest(Activation.EMPTY); - } - - @Test - public void boolConversions() throws Exception { - source = "bool(true)"; - runTest(Activation.EMPTY); // Identity - - source = "bool('true') && bool('TRUE') && bool('True') && bool('t') && bool('1')"; - runTest(Activation.EMPTY); // result is true - - source = "bool('false') || bool('FALSE') || bool('False') || bool('f') || bool('0')"; - runTest(Activation.EMPTY); // result is false - - source = "bool('TrUe')"; - runTest(Activation.EMPTY); // exception - - source = "bool('FaLsE')"; - runTest(Activation.EMPTY); // exception - } - - @Test - public void bytesConversions() throws Exception { - source = "bytes('abc\\303')"; - runTest(Activation.EMPTY); // string converts to abcà in bytes form. - - source = "bytes(bytes('abc\\303'))"; // Identity - runTest(Activation.EMPTY); - } - - @Test - public void dynConversions() throws Exception { - source = "dyn(42)"; - runTest(Activation.EMPTY); - - source = "dyn({'a':1, 'b':2})"; - runTest(Activation.EMPTY); - } - - // This lambda implements @Immutable interface 'Function', but 'InterpreterTest' has field 'eval' - // of type 'com.google.api.expr.cel.testing.Eval', the declaration of - // type - // 'com.google.api.expr.cel.testing.Eval' is not annotated with - // @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - @Test - public void jsonValueTypes() throws Exception { - container = TestAllTypes.getDescriptor().getFile().getPackage(); - declareVariable("x", CelProtoTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); - - // JSON bool selection. - TestAllTypes xBool = - TestAllTypes.newBuilder().setSingleValue(Value.newBuilder().setBoolValue(true)).build(); - source = "x.single_value"; - runTest(Activation.of("x", xBool)); - - // JSON number selection with int comparison. - TestAllTypes xInt = - TestAllTypes.newBuilder().setSingleValue(Value.newBuilder().setNumberValue(1)).build(); - source = "x.single_value == double(1)"; - runTest(Activation.of("x", xInt)); - - // JSON number selection with float comparison. - TestAllTypes xFloat = - TestAllTypes.newBuilder().setSingleValue(Value.newBuilder().setNumberValue(1.1)).build(); - source = "x.single_value == 1.1"; - runTest(Activation.of("x", xFloat)); - - // JSON null selection. - TestAllTypes xNull = - TestAllTypes.newBuilder() - .setSingleValue(Value.newBuilder().setNullValue(NullValue.NULL_VALUE)) - .build(); - source = "x.single_value == null"; - runTest(Activation.of("x", xNull)); - - // JSON string selection. - TestAllTypes xString = - TestAllTypes.newBuilder() - .setSingleValue(Value.newBuilder().setStringValue("hello")) - .build(); - source = "x.single_value == 'hello'"; - runTest(Activation.of("x", xString)); - - // JSON list equality. - TestAllTypes xList = - TestAllTypes.newBuilder() - .setSingleValue( - Value.newBuilder() - .setListValue( - ListValue.newBuilder() - .addValues( - Value.newBuilder() - .setListValue( - ListValue.newBuilder() - .addValues(Value.newBuilder().setStringValue("hello")))) - .addValues(Value.newBuilder().setNumberValue(-1.1)))) - .build(); - source = "x.single_value[0] == [['hello'], -1.1][0]"; - runTest(Activation.of("x", xList)); - - // JSON struct equality. - TestAllTypes xStruct = - TestAllTypes.newBuilder() - .setSingleStruct( - Struct.newBuilder() - .putFields( - "str", - Value.newBuilder() - .setListValue( - ListValue.newBuilder() - .addValues(Value.newBuilder().setStringValue("hello"))) - .build()) - .putFields("num", Value.newBuilder().setNumberValue(-1.1).build())) - .build(); - source = "x.single_struct.num == {'str': ['hello'], 'num': -1.1}['num']"; - runTest(Activation.of("x", xStruct)); - - // Build a proto message using a dynamically constructed map and assign the map to a struct - // value. - source = - "TestAllTypes{" - + "single_struct: " - + "TestAllTypes{single_value: {'str': ['hello']}}.single_value" - + "}"; - runTest(Activation.EMPTY); - - // Ensure that types are being wrapped and unwrapped on function dispatch. - declareFunction( - "pair", - globalOverload( - "pair", - ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.STRING), - CelProtoTypes.DYN)); - eval.registrar() - .add( - "pair", - ImmutableList.of(String.class, String.class), - (Object[] args) -> { - String key = (String) args[0]; - String val = (String) args[1]; - return eval.adapt( - Value.newBuilder() - .setStructValue( - Struct.newBuilder() - .putFields(key, Value.newBuilder().setStringValue(val).build())) - .build()); - }); - source = "pair(x.single_struct.str[0], 'val')"; - runTest(Activation.of("x", xStruct)); - } - - @Test - public void typeComparisons() throws Exception { - container = TestAllTypes.getDescriptor().getFile().getPackage(); - - // Test numeric types. - source = - "type(1) == int && type(1u) == uint && " - + "type(1u) != int && type(1) != uint && " - + "type(uint(1.1)) == uint && " - + "type(1.1) == double"; - runTest(Activation.EMPTY); - - // Test string and bytes types. - source = "type('hello') == string && type(b'\277') == bytes"; - runTest(Activation.EMPTY); - - // Test list and map types. - source = "type([1, 2, 3]) == list && type({'a': 1, 'b': 2}) == map"; - runTest(Activation.EMPTY); - - // Test bool types. - source = "type(true) == bool && type(false) == bool"; - runTest(Activation.EMPTY); - - // Test well-known proto-based types. - source = "type(duration('10s')) == google.protobuf.Duration"; - runTest(Activation.EMPTY); - - // Test external proto-based types with container resolution. - source = - "type(TestAllTypes{}) == TestAllTypes && " - + "type(TestAllTypes{}) == proto3.TestAllTypes && " - + "type(TestAllTypes{}) == .cel.expr.conformance.proto3.TestAllTypes && " - + "type(proto3.TestAllTypes{}) == TestAllTypes && " - + "type(proto3.TestAllTypes{}) == proto3.TestAllTypes && " - + "type(proto3.TestAllTypes{}) == .cel.expr.conformance.proto3.TestAllTypes && " - + "type(.cel.expr.conformance.proto3.TestAllTypes{}) == TestAllTypes && " - + "type(.cel.expr.conformance.proto3.TestAllTypes{}) == proto3.TestAllTypes && " - + "type(.cel.expr.conformance.proto3.TestAllTypes{}) == " - + ".cel.expr.conformance.proto3.TestAllTypes"; - runTest(Activation.EMPTY); - - // Test whether a type name is recognized as a type. - source = "type(TestAllTypes) == type"; - runTest(Activation.EMPTY); - - // Test whether the type resolution of a proto object is recognized as the message's type. - source = "type(TestAllTypes{}) == TestAllTypes"; - runTest(Activation.EMPTY); - - // Test whether null resolves to null_type. - source = "type(null) == null_type"; - runTest(Activation.EMPTY); - } - - @Test - public void wrappers() throws Exception { - TestAllTypes.Builder wrapperBindings = - TestAllTypes.newBuilder() - .setSingleBoolWrapper(BoolValue.of(true)) - .setSingleBytesWrapper(BytesValue.of(ByteString.copyFrom(new byte[] {'h', 'i'}))) - .setSingleDoubleWrapper(DoubleValue.of(-3.0)) - .setSingleFloatWrapper(FloatValue.of(1.5f)) - .setSingleInt32Wrapper(Int32Value.of(-12)) - .setSingleInt64Wrapper(Int64Value.of(-34)) - .setSingleStringWrapper(StringValue.of("hello")) - .setSingleUint32Wrapper(UInt32Value.of(12)) - .setSingleUint64Wrapper(UInt64Value.of(34)); - - declareVariable("x", CelProtoTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); - source = - "x.single_bool_wrapper == true && " - + "x.single_bytes_wrapper == b'hi' && " - + "x.single_double_wrapper == -3.0 && " - + "x.single_float_wrapper == 1.5 && " - + "x.single_int32_wrapper == -12 && " - + "x.single_int64_wrapper == -34 && " - + "x.single_string_wrapper == 'hello' && " - + "x.single_uint32_wrapper == 12u && " - + "x.single_uint64_wrapper == 34u"; - runTest(Activation.of("x", wrapperBindings)); - - source = - "x.single_bool_wrapper == google.protobuf.BoolValue{} && " - + "x.single_bytes_wrapper == google.protobuf.BytesValue{value: b'hi'} && " - + "x.single_double_wrapper == google.protobuf.DoubleValue{value: -3.0} && " - + "x.single_float_wrapper == google.protobuf.FloatValue{value: 1.5} && " - + "x.single_int32_wrapper == google.protobuf.Int32Value{value: -12} && " - + "x.single_int64_wrapper == google.protobuf.Int64Value{value: -34} && " - + "x.single_string_wrapper == google.protobuf.StringValue{} && " - + "x.single_uint32_wrapper == google.protobuf.UInt32Value{value: 12u} && " - + "x.single_uint64_wrapper == google.protobuf.UInt64Value{value: 34u}"; - runTest( - Activation.of( - "x", - wrapperBindings - .setSingleBoolWrapper(BoolValue.getDefaultInstance()) - .setSingleStringWrapper(StringValue.getDefaultInstance()))); - - source = - "x.single_bool_wrapper == null && " - + "x.single_bytes_wrapper == null && " - + "x.single_double_wrapper == null && " - + "x.single_float_wrapper == null && " - + "x.single_int32_wrapper == null && " - + "x.single_int64_wrapper == null && " - + "x.single_string_wrapper == null && " - + "x.single_uint32_wrapper == null && " - + "x.single_uint64_wrapper == null"; - runTest(Activation.of("x", TestAllTypes.getDefaultInstance())); - } - - @Test - public void longComprehension() throws Exception { - ImmutableList l = LongStream.range(0L, 1000L).boxed().collect(toImmutableList()); - eval.registrar().add("constantLongList", ImmutableList.of(), args -> l); - - // Comprehension over compile-time constant long list. - declareFunction( - "constantLongList", - globalOverload( - "constantLongList", ImmutableList.of(), CelProtoTypes.createList(CelProtoTypes.INT64))); - source = "size(constantLongList().map(x, x+1)) == 1000"; - runTest(Activation.EMPTY); - - // Comprehension over long list that is not compile-time constant. - declareVariable("longlist", CelProtoTypes.createList(CelProtoTypes.INT64)); - source = "size(longlist.map(x, x+1)) == 1000"; - runTest(Activation.of("longlist", l)); - - // Comprehension over long list where the computation is very slow. - // (This is here pro-forma only since in the synchronous interpreter there - // is no notion of a computation being slow so that another computation can - // build up a stack while waiting.) - eval.registrar().add("f_slow_inc", Long.class, n -> n + 1L); - eval.registrar().add("f_unleash", Object.class, x -> x); - declareFunction( - "f_slow_inc", - globalOverload("f_slow_inc", ImmutableList.of(CelProtoTypes.INT64), CelProtoTypes.INT64)); - declareFunction( - "f_unleash", - globalOverload( - "f_unleash", - ImmutableList.of(CelProtoTypes.createTypeParam("A")), - ImmutableList.of("A"), - CelProtoTypes.createTypeParam("A"))); - source = "f_unleash(longlist.map(x, f_slow_inc(x)))[0] == 1"; - runTest(Activation.of("longlist", l)); - } - - @Test - public void maxComprehension() throws Exception { - if (eval.celOptions().comprehensionMaxIterations() < 0) { - skipBaselineVerification(); - return; - } - // Comprehension over long list that is not compile-time constant. - declareVariable("longlist", CelProtoTypes.createList(CelProtoTypes.INT64)); - source = "size(longlist.map(x, x+1)) == 1000"; - - // Comprehension which exceeds the configured iteration limit. - ImmutableList tooLongList = - LongStream.range(0L, COMPREHENSION_MAX_ITERATIONS + 1).boxed().collect(toImmutableList()); - runTest(Activation.of("longlist", tooLongList)); - - // Sequential iterations within the collective limit of 1000. - source = "longlist.filter(i, i % 2 == 0).map(i, i * 2).map(i, i / 2).size() == 250"; - ImmutableList l = - LongStream.range(0L, COMPREHENSION_MAX_ITERATIONS / 2).boxed().collect(toImmutableList()); - runTest(Activation.of("longlist", l)); - - // Sequential iterations outside the limit of 1000. - source = "(longlist + [0]).filter(i, i % 2 == 0).map(i, i * 2).map(i, i / 2).size() == 251"; - runTest(Activation.of("longlist", l)); - - // Nested iteration within the iteration limit. - // Note, there is some double-counting of the inner-loops which causes the iteration limit to - // get tripped sooner than one might expect for the nested case. - source = "longlist.map(i, longlist.map(j, longlist.map(k, [i, j, k]))).size() == 9"; - l = LongStream.range(0L, 9).boxed().collect(toImmutableList()); - runTest(Activation.of("longlist", l)); - - // Nested iteration which exceeds the iteration limit. This result may be surprising, but the - // limit is tripped precisely because each complete iteration of an inner-loop counts as inner- - // loop + 1 as there's not a clean way to deduct an iteration and only count the inner most - // loop. - l = LongStream.range(0L, 10).boxed().collect(toImmutableList()); - runTest(Activation.of("longlist", l)); - } - - @Test - public void dynamicMessage_adapted() throws Exception { - TestAllTypes wrapperBindings = - TestAllTypes.newBuilder() - .setSingleAny(Any.pack(NestedMessage.newBuilder().setBb(42).build())) - .setSingleBoolWrapper(BoolValue.of(true)) - .setSingleBytesWrapper(BytesValue.of(ByteString.copyFrom(new byte[] {'h', 'i'}))) - .setSingleDoubleWrapper(DoubleValue.of(-3.0)) - .setSingleFloatWrapper(FloatValue.of(1.5f)) - .setSingleInt32Wrapper(Int32Value.of(-12)) - .setSingleInt64Wrapper(Int64Value.of(-34)) - .setSingleStringWrapper(StringValue.of("hello")) - .setSingleUint32Wrapper(UInt32Value.of(12)) - .setSingleUint64Wrapper(UInt64Value.of(34)) - .setSingleDuration(Duration.newBuilder().setSeconds(10).setNanos(20)) - .setSingleTimestamp(Timestamp.newBuilder().setSeconds(100).setNanos(200)) - .setSingleValue(Value.newBuilder().setStringValue("a")) - .setSingleStruct( - Struct.newBuilder().putFields("b", Value.newBuilder().setStringValue("c").build())) - .setListValue( - ListValue.newBuilder().addValues(Value.newBuilder().setStringValue("d")).build()) - .build(); - - Activation activation = - Activation.of( - "msg", - DynamicMessage.parseFrom( - TestAllTypes.getDescriptor(), - wrapperBindings.toByteArray(), - DefaultDescriptorPool.INSTANCE.getExtensionRegistry())); - - declareVariable("msg", CelProtoTypes.createMessage(TestAllTypes.getDescriptor().getFullName())); - - source = "msg.single_any"; - assertThat(runTest(activation)).isInstanceOf(NestedMessage.class); - - source = "msg.single_bool_wrapper"; - assertThat(runTest(activation)).isInstanceOf(Boolean.class); - - source = "msg.single_bytes_wrapper"; - assertThat(runTest(activation)).isInstanceOf(String.class); - - source = "msg.single_double_wrapper"; - assertThat(runTest(activation)).isInstanceOf(Double.class); - - source = "msg.single_float_wrapper"; - assertThat(runTest(activation)).isInstanceOf(Double.class); - - source = "msg.single_int32_wrapper"; - assertThat(runTest(activation)).isInstanceOf(Long.class); - - source = "msg.single_int64_wrapper"; - assertThat(runTest(activation)).isInstanceOf(Long.class); - - source = "msg.single_string_wrapper"; - assertThat(runTest(activation)).isInstanceOf(String.class); - - source = "msg.single_uint32_wrapper"; - assertThat(runTest(activation)) - .isInstanceOf(eval.celOptions().enableUnsignedLongs() ? UnsignedLong.class : Long.class); - - source = "msg.single_uint64_wrapper"; - assertThat(runTest(activation)) - .isInstanceOf(eval.celOptions().enableUnsignedLongs() ? UnsignedLong.class : Long.class); - - source = "msg.single_duration"; - assertThat(runTest(activation)).isInstanceOf(Duration.class); - - source = "msg.single_timestamp"; - assertThat(runTest(activation)).isInstanceOf(Timestamp.class); - - source = "msg.single_value"; - assertThat(runTest(activation)).isInstanceOf(String.class); - - source = "msg.single_struct"; - assertThat(runTest(activation)).isInstanceOf(Map.class); - - source = "msg.list_value"; - assertThat(runTest(activation)).isInstanceOf(List.class); - } - - private static String readResourceContent(String path) throws IOException { - return Resources.toString(Resources.getResource(Ascii.toLowerCase(path)), UTF_8); - } -} diff --git a/legacy/javatests/dev/cel/legacy/runtime/async/DynamicEnvTest.java b/legacy/javatests/dev/cel/legacy/runtime/async/DynamicEnvTest.java deleted file mode 100644 index 340684b5e..000000000 --- a/legacy/javatests/dev/cel/legacy/runtime/async/DynamicEnvTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.util.concurrent.Futures.immediateFuture; - -import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.FluentFuture; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public final class DynamicEnvTest { - @Test - public void withGlobalContext_preservesStack() throws Exception { - DynamicEnv base = - new DynamicEnv( - GlobalContext.of( - DummyAsyncContext.INSTANCE, - name -> () -> immediateFuture("original resolver: " + name)), - ImmutableList.of(immediateFuture("local1"), immediateFuture("local2")), - ImmutableList.of("global1", "global2")); - DynamicEnv extended = base.extend(FluentFuture.from(immediateFuture("local3"))); - DynamicEnv cloned = - extended.withGlobalContext( - GlobalContext.of( - DummyAsyncContext.INSTANCE, - name -> () -> immediateFuture("replaced resolver: " + name))); - - // base has original global bindings - assertThat(base.getGlobal(0).get()).isEqualTo("original resolver: global1"); - assertThat(base.getGlobal(1).get()).isEqualTo("original resolver: global2"); - - // extended also has original global bidings - assertThat(extended.getGlobal(0).get()).isEqualTo("original resolver: global1"); - assertThat(extended.getGlobal(1).get()).isEqualTo("original resolver: global2"); - - // cloned has replaced global bindings - assertThat(cloned.getGlobal(0).get()).isEqualTo("replaced resolver: global1"); - assertThat(cloned.getGlobal(1).get()).isEqualTo("replaced resolver: global2"); - - // base has two locals (slot offsets work in opposite direction!) - assertThat(base.getLocalAtSlotOffset(1).get()).isEqualTo("local2"); - assertThat(base.getLocalAtSlotOffset(2).get()).isEqualTo("local1"); - - // extended has one more local, original bindings are shifted - assertThat(extended.getLocalAtSlotOffset(1).get()).isEqualTo("local3"); - assertThat(extended.getLocalAtSlotOffset(2).get()).isEqualTo("local2"); - assertThat(extended.getLocalAtSlotOffset(3).get()).isEqualTo("local1"); - - // cloned as same locals as extended (from which it was created) - assertThat(cloned.getLocalAtSlotOffset(1).get()).isEqualTo("local3"); - assertThat(cloned.getLocalAtSlotOffset(2).get()).isEqualTo("local2"); - assertThat(cloned.getLocalAtSlotOffset(3).get()).isEqualTo("local1"); - } -} diff --git a/legacy/javatests/dev/cel/legacy/runtime/async/Eval.java b/legacy/javatests/dev/cel/legacy/runtime/async/Eval.java deleted file mode 100644 index ab67f5176..000000000 --- a/legacy/javatests/dev/cel/legacy/runtime/async/Eval.java +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2022 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.legacy.runtime.async; - -import com.google.common.collect.ImmutableList; -import com.google.errorprone.annotations.CheckReturnValue; -import com.google.protobuf.Descriptors.FileDescriptor; -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelOptions; -import dev.cel.runtime.Activation; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.Registrar; - -/** - * The {@code Eval} interface is used to model the core concerns of CEL evaluation during testing. - */ -@CheckReturnValue -public interface Eval { - /** Returns the set of file descriptors configured for evaluation. */ - ImmutableList fileDescriptors(); - - /** Returns the function / type registrar used during evaluation. */ - Registrar registrar(); - - CelOptions celOptions(); - - /** Adapts a Java POJO to a CEL value. */ - Object adapt(Object value) throws InterpreterException; - - /** Evaluates an {@code ast} against a set of inputs represented by the {@code Activation}. */ - Object eval(CelAbstractSyntaxTree ast, Activation activation) throws Exception; -} diff --git a/legacy/javatests/dev/cel/legacy/runtime/async/EvalAsync.java b/legacy/javatests/dev/cel/legacy/runtime/async/EvalAsync.java deleted file mode 100644 index 982460ccd..000000000 --- a/legacy/javatests/dev/cel/legacy/runtime/async/EvalAsync.java +++ /dev/null @@ -1,168 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static com.google.common.util.concurrent.Futures.immediateFuture; -import static com.google.common.util.concurrent.MoreExecutors.directExecutor; - -import com.google.common.collect.ImmutableList; -import com.google.common.context.Context; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.protobuf.Descriptors.FileDescriptor; -import com.google.protobuf.ExtensionRegistry; -import com.google.protobuf.contrib.descriptor.pool.MutableDescriptorPool; -import com.google.security.context.testing.FakeUnvalidatedSecurityContextBuilder; -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelOptions; -import dev.cel.common.CelProtoAbstractSyntaxTree; -import dev.cel.legacy.runtime.async.TypeDirectedMessageProcessor.MessageInfo; -import dev.cel.runtime.Activation; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.LinkedMessageFactory; -import dev.cel.runtime.MessageFactory; -import dev.cel.runtime.MessageProvider; -import dev.cel.runtime.Registrar; -import java.util.Optional; -import java.util.concurrent.ExecutionException; - -/** - * The {@code EvalAsync} class implements the {@code Eval} interface and exposes some additional - * methods for async evaluation testing. - */ -public class EvalAsync implements Eval { - - private final ImmutableList fileDescriptors; - private final MutableDescriptorPool pool = new MutableDescriptorPool(); - private final AsyncDispatcher dispatcher; - private final MessageProvider typeProvider; - private final MessageProcessor messageProcessor; - private final AsyncInterpreter asyncInterpreter; - private final CelOptions celOptions; - - public EvalAsync(ImmutableList fileDescriptors, CelOptions celOptions) { - this(fileDescriptors, celOptions, /* typeDirectedProcessor= */ false); - } - - public EvalAsync( - ImmutableList fileDescriptors, - CelOptions celOptions, - boolean typeDirectedProcessor) { - this( - fileDescriptors, - celOptions, - typeDirectedProcessor, - DefaultAsyncDispatcher.create(celOptions).fork()); - } - - private EvalAsync( - ImmutableList fileDescriptors, - CelOptions celOptions, - boolean typeDirectedProcessor, - AsyncDispatcher asyncDispatcher) { - this.dispatcher = asyncDispatcher; - this.celOptions = celOptions; - this.fileDescriptors = fileDescriptors; - this.fileDescriptors.forEach(pool::populateFromFileDescriptor); - this.typeProvider = LinkedMessageFactory.typeProvider(celOptions); - MessageFactory typeFactory = LinkedMessageFactory.typeFactory(); - - if (typeDirectedProcessor) { - this.messageProcessor = - new TypeDirectedMessageProcessor( - typeName -> - Optional.ofNullable(pool.getDescriptorForTypeName(typeName)) - .map( - descriptor -> - MessageInfo.of(descriptor, () -> typeFactory.newBuilder(typeName))), - extName -> - Optional.ofNullable( - ExtensionRegistry.getGeneratedRegistry().findExtensionByName(extName)), - celOptions); - } else { - this.messageProcessor = - new MessageProcessorAdapter( - typeName -> Optional.ofNullable(pool.getDescriptorForTypeName(typeName)), - typeProvider); - } - this.asyncInterpreter = - new FuturesInterpreter( - StandardTypeResolver.getInstance(celOptions), - this.messageProcessor, - this.dispatcher, - celOptions); - } - - @Override - public ImmutableList fileDescriptors() { - return fileDescriptors; - } - - @Override - public Registrar registrar() { - return dispatcher; - } - - @Override - public CelOptions celOptions() { - return celOptions; - } - - @Override - public Object adapt(Object value) throws InterpreterException { - return typeProvider.adapt(value); - } - - @Override - public Object eval(CelAbstractSyntaxTree ast, Activation activation) throws Exception { - Context requestContext = - Context.newBuilder(Context.getCurrentContext()) - // Make security context different from BACKGROUND_SECURITY_CONTEXT. - .replaceSecurityContext( - FakeUnvalidatedSecurityContextBuilder.withPeer("testpeer").buildUnvalidated()) - .build(); - return forceExpressionFuture( - asyncInterpreter - .createInterpretable(CelProtoAbstractSyntaxTree.fromCelAst(ast).toCheckedExpr()) - .eval( - new DefaultAsyncContext(directExecutor(), requestContext), - name -> immediateFuture(activation.resolve(name)))); - } - - /** Returns the {@code MessageProcessor} used for protobuf creation and manipulation. */ - public MessageProcessor messageProcessor() { - return messageProcessor; - } - - /** - * Indicates whether the {@code type_map} from the {@code CheckedExpr} is used to determine - * runtime typing during function and field resolution. - */ - public boolean typeDirected() { - return messageProcessor instanceof TypeDirectedMessageProcessor; - } - - /** Creates a new {@code EvalAsync} instance using the supplied dispatcher. */ - public EvalAsync withDispatcher(AsyncDispatcher dispatcher) { - if (dispatcher == this.dispatcher) { - return this; - } - return new EvalAsync(fileDescriptors, celOptions, typeDirected(), dispatcher); - } - - private Object forceExpressionFuture(ListenableFuture future) - throws InterpreterException { - try { - return future.get(); - } catch (InterruptedException intrExn) { - Thread.currentThread().interrupt(); - throw new RuntimeException(intrExn); - } catch (ExecutionException execExn) { - Throwable cause = execExn.getCause(); - if (cause instanceof InterpreterException) { - throw (InterpreterException) cause; - } else if (cause instanceof RuntimeException) { - throw (RuntimeException) cause; - } else { - throw new RuntimeException(cause); - } - } - } -} diff --git a/legacy/javatests/dev/cel/legacy/runtime/async/FuturesInterpreterTest.java b/legacy/javatests/dev/cel/legacy/runtime/async/FuturesInterpreterTest.java deleted file mode 100644 index 2f377f6b3..000000000 --- a/legacy/javatests/dev/cel/legacy/runtime/async/FuturesInterpreterTest.java +++ /dev/null @@ -1,558 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.util.concurrent.Futures.immediateFailedFuture; -import static com.google.common.util.concurrent.Futures.immediateFuture; -import static com.google.common.util.concurrent.MoreExecutors.directExecutor; -import static dev.cel.common.types.CelProtoTypes.createMessage; -import static dev.cel.legacy.runtime.async.Effect.CONTEXT_DEPENDENT; -import static dev.cel.legacy.runtime.async.Effect.CONTEXT_INDEPENDENT; -import static dev.cel.legacy.runtime.async.EvaluationHelpers.immediateValue; - -import dev.cel.expr.Type; -import com.google.common.collect.ImmutableList; -import com.google.common.context.Context; -import com.google.common.util.concurrent.FluentFuture; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; -import com.google.protobuf.ByteString; -import com.google.protobuf.Descriptors.FileDescriptor; -// import com.google.testing.testsize.MediumTest; -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.types.CelProtoTypes; -import dev.cel.expr.conformance.proto3.TestAllTypes; -import dev.cel.legacy.runtime.async.MessageProcessor.FieldAssigner; -import dev.cel.legacy.runtime.async.MessageProcessor.MessageBuilderCreator; -import dev.cel.runtime.Activation; -import dev.cel.runtime.InterpreterException; -import dev.cel.testing.CelBaselineTestCase; -import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; -import java.util.List; -import java.util.Map; -import java.util.function.Supplier; -import java.util.stream.LongStream; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestName; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; - -/** Tests for {@link FuturesInterpreter} and related functionality. */ -// @MediumTest -@RunWith(Parameterized.class) -public class FuturesInterpreterTest extends CelBaselineTestCase { - - private final EvalAsync evalAsync; - private final AsyncDispatcher dispatcher; - - @Rule public TestName interpreterTestName = new TestName(); - - private static final ImmutableList TEST_FILE_DESCRIPTORS = - ImmutableList.of( - TestAllTypes.getDescriptor().getFile(), StandaloneGlobalEnum.getDescriptor().getFile()); - - // EvalSync and Async are mutable by design (Ex: adding function to the dispatcher). This has been - // overridden to make the test cases descriptive, as mutability is not a core concern of these - // tests. - @SuppressWarnings("ImmutableEnumChecker") - private enum EvalTestCase { - ASYNC_PROTO_TYPE_PARSER(false, () -> new EvalAsync(TEST_FILE_DESCRIPTORS, TEST_OPTIONS)), - ASYNC_PROTO_TYPE_DIRECTED_PROCESSOR_PARSER( - false, - () -> - new EvalAsync(TEST_FILE_DESCRIPTORS, TEST_OPTIONS, /* typeDirectedProcessor= */ true)), - ASYNC_CEL_TYPE_PARSER(true, () -> new EvalAsync(TEST_FILE_DESCRIPTORS, TEST_OPTIONS)), - ASYNC_CEL_TYPE_DIRECTED_PROCESSOR_PARSER( - true, - () -> - new EvalAsync(TEST_FILE_DESCRIPTORS, TEST_OPTIONS, /* typeDirectedProcessor= */ true)); - - private final boolean declareWithCelType; - private final Supplier eval; - - EvalTestCase(boolean declareWithCelType, Supplier eval) { - this.declareWithCelType = declareWithCelType; - this.eval = eval; - } - } - - @Parameters() - public static ImmutableList evalTestCases() { - return ImmutableList.copyOf(EvalTestCase.values()); - } - - public FuturesInterpreterTest(EvalTestCase testCase) { - super(testCase.declareWithCelType); - this.evalAsync = testCase.eval.get(); - this.dispatcher = (AsyncDispatcher) evalAsync.registrar(); - } - - /** Helper to run a test for configured instance variables. */ - private void runTest(Activation activation, AsyncDispatcher localDispatcher) throws Exception { - CelAbstractSyntaxTree ast = prepareTest(evalAsync.fileDescriptors()); - if (ast == null) { - return; - } - EvalAsync evalAsyncLocal = evalAsync.withDispatcher(localDispatcher); - testOutput().println("bindings: " + activation); - Object result; - try { - result = evalAsyncLocal.eval(ast, activation); - if (result instanceof ByteString) { - // Note: this call may fail for printing byte sequences that are not valid UTF-8, but works - // pretty well for test purposes. - result = ((ByteString) result).toStringUtf8(); - } - testOutput().println("result: " + result); - } catch (InterpreterException e) { - testOutput().println("error: " + e.getMessage()); - } - testOutput().println(); - } - - private void runTest(Activation activation) throws Exception { - runTest(activation, dispatcher); - } - - @Test - public void nobarrierFunction() throws Exception { - declareFunction( - "F", - globalOverload( - "F", - ImmutableList.of(CelProtoTypes.BOOL, CelProtoTypes.INT64, CelProtoTypes.INT64), - CelProtoTypes.INT64)); - dispatcher.addNobarrierAsync( - "F", - CONTEXT_INDEPENDENT, - (ign, args) -> - FluentFuture.from(args.get(0)) - .transformAsync(b -> ((boolean) b) ? args.get(1) : args.get(2), directExecutor())); - - source = "F(true, 2, 1 / 0)"; // Failing argument does not matter, so overall success. - runTest(Activation.EMPTY); - - source = "F(false, 3, 1 / 0)"; // Failing argument matters. - runTest(Activation.EMPTY); - } - - @Test - public void dispatcherSnapshot() throws Exception { - AsyncDispatcher orig = DefaultAsyncDispatcher.create(TEST_OPTIONS); - - AsyncDispatcher snapshot = orig.fork(); - // Function added after snapshot is taken is expected to not impact result. - orig.add("just_undefined", String.class, x -> "was defined after all: " + x); - - declareFunction( - "just_undefined", - globalOverload( - "just_undefined", ImmutableList.of(CelProtoTypes.STRING), CelProtoTypes.STRING)); - - // No definition in snapshot, so this fails with an unbound overload. - source = "just_undefined(\"hi there \") == \"foo\""; - runTest(Activation.EMPTY, snapshot); - } - - // This lambda implements @Immutable interface 'StrictUnaryFunction', but the declaration of type - // 'com.google.common.util.concurrent.SettableFuture' is not annotated with - // @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - @Test - public void longComprehension() throws Exception { - long upper = 1000L; - Type listType = CelProtoTypes.createList(CelProtoTypes.INT64); - ImmutableList l = LongStream.range(0L, upper).boxed().collect(toImmutableList()); - dispatcher.add("constantLongList", ImmutableList.of(), args -> l); - - // Comprehension over compile-time constant long list. - declareFunction( - "constantLongList", globalOverload("constantLongList", ImmutableList.of(), listType)); - source = "size(constantLongList().map(x, x+1)) == 1000"; - runTest(Activation.EMPTY); - - // Comprehension over long list that is not compile-time constant. - declareVariable("longlist", CelProtoTypes.createList(CelProtoTypes.INT64)); - source = "size(longlist.map(x, x+1)) == 1000"; - runTest(Activation.of("longlist", l)); - - // Comprehension over long list where the computation is very slow. - SettableFuture firstFuture = SettableFuture.create(); - dispatcher.addDirect( - "f_slow_inc", - Long.class, - CONTEXT_INDEPENDENT, - n -> { - if (n == 0) { - // stall on the first element - return firstFuture; - } - return immediateValue(n + 1L); - }); - dispatcher.addNobarrierAsync( - "f_unleash", - CONTEXT_INDEPENDENT, - (gctx, args) -> { - firstFuture.set(1L); - return args.get(0); - }); - declareFunction( - "f_slow_inc", - globalOverload("f_slow_inc", ImmutableList.of(CelProtoTypes.INT64), CelProtoTypes.INT64)); - declareFunction( - "f_unleash", - globalOverload( - "f_unleash", - ImmutableList.of(CelProtoTypes.createTypeParam("A")), - ImmutableList.of("A"), - CelProtoTypes.createTypeParam("A"))); - source = "f_unleash(longlist.map(x, f_slow_inc(x)))[0] == 1"; - - runTest(Activation.of("longlist", l)); - } - - @Test - public void functionRegistrar() throws Exception { - // This test case exercises every registration method of FunctionRegistrar and then - // invokes each of the registered methods in such a way that both the constant-fold path - // and the regular non-constant-fold path is invoked. - dispatcher.addCallConstructor( - "f_constructor", (md, id, args) -> CompiledExpression.constant("constructed!")); - dispatcher.addStrictFunction( - "f_strict", - ImmutableList.of(String.class, Long.class), - true, - (gctx, args) -> immediateValue("strict: " + (String) args.get(0) + (Long) args.get(1))); - dispatcher.addDirect( - "f_directN", - ImmutableList.of(String.class, Boolean.class), - CONTEXT_INDEPENDENT, - (gctx, args) -> immediateValue("directN: " + (String) args.get(0) + (Boolean) args.get(1))); - dispatcher.addDirect( - "f_direct", - CONTEXT_INDEPENDENT, - (gctx, args) -> immediateValue("direct: " + (Boolean) args.get(0) + (Long) args.get(1))); - dispatcher.addDirect( - "f_direct1", String.class, CONTEXT_INDEPENDENT, s -> immediateValue("direct1:" + s)); - dispatcher.addDirect( - "f_direct2", - String.class, - Long.class, - CONTEXT_INDEPENDENT, - (s, i) -> immediateValue("direct2: " + s + i)); - dispatcher.addAsync( - "f_asyncN", - ImmutableList.of(String.class, Boolean.class), - CONTEXT_INDEPENDENT, - (gctx, args) -> immediateValue("asyncN: " + (String) args.get(0) + (Boolean) args.get(1))); - dispatcher.addAsync( - "f_async", - CONTEXT_INDEPENDENT, - (gctx, args) -> immediateValue("async: " + (Boolean) args.get(0) + (Long) args.get(1))); - dispatcher.addAsync( - "f_async1", String.class, CONTEXT_INDEPENDENT, s -> immediateValue("async1:" + s)); - dispatcher.addAsync( - "f_async2", - String.class, - Long.class, - CONTEXT_INDEPENDENT, - (s, i) -> immediateValue("async2: " + s + i)); - dispatcher.addDirect( - "f_effect", String.class, CONTEXT_DEPENDENT, s -> immediateValue("effective: " + s)); - dispatcher.addNobarrierAsync( - "f_nobarrier", - CONTEXT_INDEPENDENT, - (gctx, futures) -> - Futures.transform(futures.get(0), x -> "nobarrier: " + (String) x, directExecutor())); - dispatcher.add("f_simple1", String.class, s -> "simple1: " + s); - dispatcher.add("f_simple2", String.class, String.class, (x, y) -> "simple2: " + x + "@" + y); - dispatcher.add( - "f_simpleN", - ImmutableList.of(String.class, Long.class, Long.class), - args -> "simpleN: " + (String) args[0] + (Long) args[1] + (Long) args[2]); - - // A dynamic value that cannot be constant-folded away. - declareVariable("dynamic", CelProtoTypes.STRING); - - declareGlobalFunction("f_constructor", ImmutableList.of(), CelProtoTypes.STRING); - declareGlobalFunction( - "f_strict", - ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.INT64), - CelProtoTypes.STRING); - declareGlobalFunction( - "f_directN", - ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.BOOL), - CelProtoTypes.STRING); - declareGlobalFunction( - "f_direct", - ImmutableList.of(CelProtoTypes.BOOL, CelProtoTypes.INT64), - CelProtoTypes.STRING); - declareGlobalFunction( - "f_direct1", ImmutableList.of(CelProtoTypes.STRING), CelProtoTypes.STRING); - declareGlobalFunction( - "f_direct2", - ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.INT64), - CelProtoTypes.STRING); - declareGlobalFunction( - "f_asyncN", - ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.BOOL), - CelProtoTypes.STRING); - declareGlobalFunction( - "f_async", ImmutableList.of(CelProtoTypes.BOOL, CelProtoTypes.INT64), CelProtoTypes.STRING); - declareGlobalFunction("f_async1", ImmutableList.of(CelProtoTypes.STRING), CelProtoTypes.STRING); - declareGlobalFunction( - "f_async2", - ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.INT64), - CelProtoTypes.STRING); - declareGlobalFunction("f_effect", ImmutableList.of(CelProtoTypes.STRING), CelProtoTypes.STRING); - declareGlobalFunction( - "f_nobarrier", ImmutableList.of(CelProtoTypes.STRING), CelProtoTypes.STRING); - declareGlobalFunction( - "f_simple1", ImmutableList.of(CelProtoTypes.STRING), CelProtoTypes.STRING); - declareGlobalFunction( - "f_simple2", - ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.STRING), - CelProtoTypes.STRING); - declareGlobalFunction( - "f_simpleN", - ImmutableList.of(CelProtoTypes.STRING, CelProtoTypes.INT64, CelProtoTypes.INT64), - CelProtoTypes.STRING); - - source = - "f_constructor() + \"\\n\" + " - + "f_strict(\"static\", 13) + \" \" + f_strict(dynamic, 14) + \"\\n\" + " - + "f_directN(\"static\", true) + \" \" + f_directN(dynamic, false) + \"\\n\" + " - + "f_direct(true, 20) + \" \" + f_direct(dynamic == \"foo\", 21) + \"\\n\" + " - + "f_direct1(\"static\") + \" \" + f_direct1(dynamic) + \"\\n\" + " - + "f_direct2(\"static\", 30) + \" \" + f_direct2(dynamic, 31) + \"\\n\" + " - + "f_asyncN(\"static\", true) + \" \" + f_asyncN(dynamic, false) + \"\\n\" + " - + "f_async(true, 20) + \" \" + f_async(dynamic == \"foo\", 21) + \"\\n\" + " - + "f_async1(\"static\") + \" \" + f_async1(dynamic) + \"\\n\" + " - + "f_async2(\"static\", 30) + \" \" + f_async2(dynamic, 31) + \"\\n\" + " - + "f_effect(\"static\") + \" \" + f_effect(dynamic) + \"\\n\" + " - + "f_nobarrier(\"static\") + \" \" + f_nobarrier(dynamic) + \"\\n\" + " - + "f_simple1(\"static\") + \" \" + f_simple1(dynamic) + \"\\n\" + " - + "f_simple2(\"static\", \"foo\") + \" \" + f_simple2(dynamic, \"bar\") + \"\\n\" + " - + "f_simpleN(\"static\", 54, 32) + \" \" + f_simpleN(dynamic, 98,76)"; - - runTest(Activation.of("dynamic", "dynamic")); - } - - @Test - public void contextPropagation() throws Exception { - dispatcher.addAsync( - "f", - ImmutableList.of(), - CONTEXT_DEPENDENT, - (gctx, args) -> - immediateValue( - Context.getCurrentContext().getSecurityContext().getLoggablePeer().getUsername())); - declareGlobalFunction("f", ImmutableList.of(), CelProtoTypes.STRING); - source = "f()"; - runTest(Activation.EMPTY); - } - - @Test - public void delayedEvaluation() throws Exception { - dispatcher.addCallConstructor( - "f_delay", - (md, id, args) -> - args.get(0) - .expression() - .map( - (e, effect) -> - CompiledExpression.executable( - stack -> - FluentFuture.from( - immediateFuture( - Delayed.of( - gctx -> e.execute(stack.withGlobalContext(gctx))))), - CONTEXT_INDEPENDENT), - c -> CompiledExpression.constant(Delayed.fromFuture(immediateFuture(c))), - t -> - CompiledExpression.constant(Delayed.fromFuture(immediateFailedFuture(t))))); - dispatcher.addDirect( - "f_force", - ImmutableList.of(Delayed.class), - CONTEXT_DEPENDENT, - (gctx, args) -> ((Delayed) args.get(0)).force(gctx)); - - declareFunction( - "f_delay", - globalOverload("f_delay", ImmutableList.of(CelProtoTypes.INT64), CelProtoTypes.DYN)); - declareFunction( - "f_force", - globalOverload("f_force", ImmutableList.of(CelProtoTypes.DYN), CelProtoTypes.INT64)); - - // Delayed computation is just a constant. - source = "f_force(f_delay(1 + 2)) == 3"; - runTest(Activation.EMPTY); - - // A dynamic (global) input. - declareVariable("four", CelProtoTypes.INT64); - - // Delayed computation depends on global state. - source = "f_force(f_delay(1 + four)) == 5"; - runTest(Activation.of("four", 4L)); - - // Delayed computation occurs within local scope and captures the value of a local variable. - source = "[1, 2, 3].map(i, f_delay(i + four)).map(d, f_force(d)) == [5, 6, 7]"; - runTest(Activation.of("four", 4L)); - } - - @Test - public void stashedLocal() throws Exception { - // Executes its argument within a stack that has been extended with a slot for %stash%. - // (The value stored there is 123.) - dispatcher.addCallConstructor( - "f_stash", - (md, id, args) -> { - ExecutableExpression executable = - args.get(0).scopedExpression().inScopeOf("%stash%").toExecutable(); - return CompiledExpression.executable( - stack -> executable.execute(stack.extend(immediateValue(123L))), CONTEXT_DEPENDENT); - }); - // Refers to the value at the stack offset that corresponds to %stash%. - dispatcher.addCallConstructor( - "f_grab", - (md, id, args, offsetFinder) -> { - int offset = offsetFinder.findStackOffset(md, id, "%stash%"); - return CompiledExpression.executable( - stack -> stack.getLocalAtSlotOffset(offset), CONTEXT_INDEPENDENT); - }); - // f_stash: (T) -> T - declareFunction( - "f_stash", - globalOverload( - "f_stash", - ImmutableList.of(CelProtoTypes.createTypeParam("T")), - ImmutableList.of("T"), - CelProtoTypes.createTypeParam("T"))); - // f_grab: () -> int - declareFunction("f_grab", globalOverload("f_grab", ImmutableList.of(), CelProtoTypes.INT64)); - - source = "f_stash([1, 2, 3].map(x, x + f_grab())) == [124, 125, 126]"; - runTest(Activation.EMPTY); - } - - // This lambda implements @Immutable interface 'BinaryFunction', but the declaration of type - // 'com.google.api.tools.contract.runtime.interpreter.MessageProcessor' is not annotated with - // @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - @Test - public void fieldManipulation() throws Exception { - String protoName = TestAllTypes.getDescriptor().getFullName(); - Type protoType = createMessage(protoName); - - declareFunction( - "assignSingleInt64", - memberOverload( - "assignSingleInt64", ImmutableList.of(protoType, CelProtoTypes.INT64), protoType)); - declareFunction( - "assignRepeatedInt64", - memberOverload( - "assignRepeatedInt64", - ImmutableList.of(protoType, CelProtoTypes.createList(CelProtoTypes.INT64)), - protoType)); - declareFunction( - "assignMap", - memberOverload( - "assignMap", - ImmutableList.of( - protoType, CelProtoTypes.createMap(CelProtoTypes.INT64, CelProtoTypes.INT64)), - protoType)); - declareFunction( - "clearField", - memberOverload("clearField", ImmutableList.of(protoType, CelProtoTypes.STRING), protoType)); - declareFunction( - "singletonInt64", - globalOverload("singletonInt64", ImmutableList.of(CelProtoTypes.INT64), protoType)); - - MessageProcessor messageProcessor = evalAsync.messageProcessor(); - MessageBuilderCreator builderCreator = - messageProcessor.makeMessageBuilderCreator(null, 0L, protoName); - FieldAssigner singleAssigner = - messageProcessor.makeFieldAssigner( - null, 0L, protoName, "single_int64", CelProtoTypes.INT64); - FieldAssigner repeatedAssigner = - messageProcessor.makeFieldAssigner( - null, 0L, protoName, "repeated_int64", CelProtoTypes.createList(CelProtoTypes.INT64)); - FieldAssigner mapAssigner = - messageProcessor.makeFieldAssigner( - null, - 0L, - protoName, - "map_int32_int64", - CelProtoTypes.createMap(CelProtoTypes.INT64, CelProtoTypes.INT64)); - - dispatcher.add( - "assignSingleInt64", - TestAllTypes.class, - Long.class, - (p, i) -> singleAssigner.assign(p.toBuilder(), i).build()); - dispatcher.add( - "assignRepeatedInt64", - TestAllTypes.class, - List.class, - (p, l) -> repeatedAssigner.assign(p.toBuilder(), l).build()); - dispatcher.add( - "assignMap", - TestAllTypes.class, - Map.class, - (p, m) -> mapAssigner.assign(p.toBuilder(), m).build()); - dispatcher.add( - "clearField", - TestAllTypes.class, - String.class, - (p, n) -> - messageProcessor.makeFieldClearer(null, 0L, protoName, n).clear(p.toBuilder()).build()); - dispatcher.add( - "singletonInt64", - Long.class, - i -> singleAssigner.assign(builderCreator.builder(), i).build()); - - container = TestAllTypes.getDescriptor().getFile().getPackage(); - - source = - "TestAllTypes{single_bool: true}.assignSingleInt64(1) == " - + "TestAllTypes{single_bool: true, single_int64: 1}"; - runTest(Activation.EMPTY); - - source = - "TestAllTypes{repeated_int64: [1, 2]}.assignRepeatedInt64([3, 1, 4]) == " - + "TestAllTypes{repeated_int64: [3, 1, 4]}"; - runTest(Activation.EMPTY); - - source = - "TestAllTypes{single_bool: true, single_int64: 1}.clearField(\"single_bool\") == " - + "TestAllTypes{single_int64: 1}"; - runTest(Activation.EMPTY); - - source = - "TestAllTypes{single_bool: false}.assignMap({13: 26, 22: 42}).map_int32_int64[22] == 42"; - runTest(Activation.EMPTY); - - source = - "TestAllTypes{single_bool: true, repeated_int64: [1, 2]}.clearField(\"repeated_int64\") == " - + "TestAllTypes{single_bool: true}"; - runTest(Activation.EMPTY); - - source = "singletonInt64(12) == TestAllTypes{single_int64: 12}"; - runTest(Activation.EMPTY); - } - - /** Represents a delayed computation, used by the delayedEvaluation test case below. */ - interface Delayed { - ListenableFuture force(GlobalContext gctx); - - static Delayed fromFuture(ListenableFuture future) { - return gctx -> future; - } - - static Delayed of(Delayed d) { - return d; - } - } -} diff --git a/legacy/javatests/dev/cel/legacy/runtime/async/FuturesInterpreterWithMessageProcessorTest.java b/legacy/javatests/dev/cel/legacy/runtime/async/FuturesInterpreterWithMessageProcessorTest.java deleted file mode 100644 index 5fb270926..000000000 --- a/legacy/javatests/dev/cel/legacy/runtime/async/FuturesInterpreterWithMessageProcessorTest.java +++ /dev/null @@ -1,414 +0,0 @@ -package dev.cel.legacy.runtime.async; - -import static com.google.common.util.concurrent.Futures.immediateFuture; -import static com.google.common.util.concurrent.MoreExecutors.directExecutor; -import static dev.cel.common.types.CelProtoTypes.createMessage; -import static dev.cel.legacy.runtime.async.Effect.CONTEXT_DEPENDENT; - -import dev.cel.expr.Type; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.protobuf.ByteString; -import com.google.protobuf.Descriptors.FileDescriptor; -// import com.google.testing.testsize.MediumTest; -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.types.CelProtoTypes; -import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; -import dev.cel.expr.conformance.proto3.TestAllTypes; -import dev.cel.legacy.runtime.async.MessageProcessor.FieldAssigner; -import dev.cel.legacy.runtime.async.MessageProcessor.MessageBuilderCreator; -import dev.cel.runtime.Activation; -import dev.cel.runtime.InterpreterException; -import dev.cel.runtime.Registrar.BinaryFunction; -import dev.cel.runtime.Registrar.UnaryFunction; -import dev.cel.testing.CelBaselineTestCase; -import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.function.Supplier; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; - -/** Tests for {@link FuturesInterpreter} and related functionality. */ -// @MediumTest -@RunWith(Parameterized.class) -public class FuturesInterpreterWithMessageProcessorTest extends CelBaselineTestCase { - private final EvalAsync evalAsync; - - private static final ImmutableList TEST_FILE_DESCRIPTORS = - ImmutableList.of( - TestAllTypes.getDescriptor().getFile(), - dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor().getFile(), - StandaloneGlobalEnum.getDescriptor().getFile()); - - // EvalSync and Async are mutable by design (Ex: adding function to the dispatcher). This has been - // overridden to make the test cases descriptive, as mutability is not a core concern of these - // tests. - @SuppressWarnings("ImmutableEnumChecker") - private enum EvalTestCase { - ASYNC_PROTO_TYPE_PARSER_DIRECTED_PROCESSOR( - false, - () -> - new EvalAsync(TEST_FILE_DESCRIPTORS, TEST_OPTIONS, /* typeDirectedProcessor= */ true)), - ASYNC_CEL_TYPE_PARSER_DIRECTED_PROCESSOR( - true, - () -> - new EvalAsync(TEST_FILE_DESCRIPTORS, TEST_OPTIONS, /* typeDirectedProcessor= */ true)); - - private final boolean declareWithCelType; - private final Supplier eval; - - EvalTestCase(boolean declareWithCelType, Supplier eval) { - this.declareWithCelType = declareWithCelType; - this.eval = eval; - } - } - - @Parameters() - public static ImmutableList evalTestCases() { - return ImmutableList.copyOf(EvalTestCase.values()); - } - - public FuturesInterpreterWithMessageProcessorTest(EvalTestCase testCase) { - super(testCase.declareWithCelType); - this.evalAsync = testCase.eval.get(); - } - - /** Helper to run a test for configured instance variables. */ - private void runTest(Activation activation) throws Exception { - CelAbstractSyntaxTree ast = prepareTest(evalAsync.fileDescriptors()); - if (ast == null) { - return; - } - testOutput().println("bindings: " + activation); - try { - Object result = evalAsync.eval(ast, activation); - if (result instanceof ByteString) { - // Note: this call may fail for printing byte sequences that are not valid UTF-8, but works - // pretty well for test purposes. - result = ((ByteString) result).toStringUtf8(); - } - testOutput().println("result: " + result); - } catch (InterpreterException e) { - testOutput().println("error: " + e.getMessage()); - } - testOutput().println(); - } - - // Helper for testing late-binding of the MessageProcessor (binary functions). - // This lambda implements @Immutable interface 'CallConstructor', but the declaration of type - // 'java.util.function.Function>' is not annotated with @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - private static void addFunctionWithMessageProcessor( - FunctionRegistrar registrar, - String overloadId, - Class clazzA, - Class clazzB, - Function> functionMaker) { - registrar.addCallConstructor( - overloadId, - (md, id, args, mp, ignoredStackOffsetFinder) -> { - ExecutableExpression executableA = args.get(0).expression().toExecutable(); - ExecutableExpression executableB = args.get(1).expression().toExecutable(); - BinaryFunction function = functionMaker.apply(mp); - return CompiledExpression.executable( - stack -> - executableA - .execute(stack) - .transformAsync( - a -> - executableB - .execute(stack) - .transformAsync( - b -> - immediateFuture( - function.apply(clazzA.cast(a), clazzB.cast(b))), - directExecutor()), - directExecutor()), - CONTEXT_DEPENDENT); - }); - } - - // Helper for testing late-binding of the MessageProcessor (unary functions). - // This lambda implements @Immutable interface 'CallConstructor', but the declaration of type - // 'java.util.function.Function>' is not annotated with @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - private static void addFunctionWithMessageProcessor( - FunctionRegistrar registrar, - String overloadId, - Class clazzA, - Function> functionMaker) { - registrar.addCallConstructor( - overloadId, - (md, id, args, mp, ignoredStackOffsetFinder) -> { - ExecutableExpression executableA = args.get(0).expression().toExecutable(); - UnaryFunction function = functionMaker.apply(mp); - return CompiledExpression.executable( - stack -> - executableA - .execute(stack) - .transformAsync( - a -> immediateFuture(function.apply(clazzA.cast(a))), directExecutor()), - CONTEXT_DEPENDENT); - }); - } - - // This lambda implements @Immutable interface 'BinaryFunction', but the declaration of type - // 'com.google.api.tools.contract.runtime.interpreter.MessageProcessor' is not annotated with - // @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - @Test - public void fieldManipulation() throws Exception { - String protoName = TestAllTypes.getDescriptor().getFullName(); - Type protoType = createMessage(protoName); - - declareFunction( - "assignSingleInt64", - memberOverload( - "assignSingleInt64", ImmutableList.of(protoType, CelProtoTypes.INT64), protoType)); - declareFunction( - "assignRepeatedInt64", - memberOverload( - "assignRepeatedInt64", - ImmutableList.of(protoType, CelProtoTypes.createList(CelProtoTypes.INT64)), - protoType)); - declareFunction( - "assignMap", - memberOverload( - "assignMap", - ImmutableList.of( - protoType, CelProtoTypes.createMap(CelProtoTypes.INT64, CelProtoTypes.INT64)), - protoType)); - declareFunction( - "clearField", - memberOverload("clearField", ImmutableList.of(protoType, CelProtoTypes.STRING), protoType)); - declareFunction( - "singletonInt64", - globalOverload("singletonInt64", ImmutableList.of(CelProtoTypes.INT64), protoType)); - - MessageBuilderCreator builderCreator = - evalAsync.messageProcessor().makeMessageBuilderCreator(null, 0L, protoName); - FieldAssigner singleAssigner = - evalAsync - .messageProcessor() - .makeFieldAssigner(null, 0L, protoName, "single_int64", CelProtoTypes.INT64); - - AsyncDispatcher dispatcher = (AsyncDispatcher) evalAsync.registrar(); - dispatcher.add( - "assignSingleInt64", - TestAllTypes.class, - Long.class, - (p, i) -> singleAssigner.assign(p.toBuilder(), i).build()); - addFunctionWithMessageProcessor( - dispatcher, - "assignRepeatedInt64", - TestAllTypes.class, - List.class, - mp -> - (p, l) -> - mp.makeFieldAssigner( - null, - 0L, - protoName, - "repeated_int64", - CelProtoTypes.createList(CelProtoTypes.INT64)) - .assign(p.toBuilder(), l) - .build()); - addFunctionWithMessageProcessor( - dispatcher, - "assignMap", - TestAllTypes.class, - Map.class, - mp -> - (p, m) -> - mp.makeFieldAssigner( - null, - 0L, - protoName, - "map_int32_int64", - CelProtoTypes.createMap(CelProtoTypes.INT64, CelProtoTypes.INT64)) - .assign(p.toBuilder(), m) - .build()); - addFunctionWithMessageProcessor( - dispatcher, - "clearField", - TestAllTypes.class, - String.class, - mp -> (p, n) -> mp.makeFieldClearer(null, 0L, protoName, n).clear(p.toBuilder()).build()); - dispatcher.add( - "singletonInt64", - Long.class, - i -> singleAssigner.assign(builderCreator.builder(), i).build()); - - container = TestAllTypes.getDescriptor().getFile().getPackage(); - - source = - "TestAllTypes{single_bool: true}.assignSingleInt64(1) == " - + "TestAllTypes{single_bool: true, single_int64: 1}"; - runTest(Activation.EMPTY); - - source = - "TestAllTypes{repeated_int64: [1, 2]}.assignRepeatedInt64([3, 1, 4]) == " - + "TestAllTypes{repeated_int64: [3, 1, 4]}"; - runTest(Activation.EMPTY); - - source = - "TestAllTypes{single_bool: true, single_int64: 1}.clearField(\"single_bool\") == " - + "TestAllTypes{single_int64: 1}"; - runTest(Activation.EMPTY); - - source = - "TestAllTypes{single_bool: false}.assignMap({13: 26, 22: 42}).map_int32_int64[22] == 42"; - runTest(Activation.EMPTY); - - source = - "TestAllTypes{single_bool: true, repeated_int64: [1, 2]}.clearField(\"repeated_int64\") == " - + "TestAllTypes{single_bool: true}"; - runTest(Activation.EMPTY); - - source = "singletonInt64(12) == TestAllTypes{single_int64: 12}"; - runTest(Activation.EMPTY); - } - - // This lambda implements @Immutable interface 'UnaryFunction', but the declaration of type - // 'com.google.api.tools.contract.runtime.interpreter.MessageProcessor' is not annotated with - // @com.google.errorprone.annotations.Immutable - @SuppressWarnings("Immutable") - @Test - public void extensionManipulation() throws Exception { - String extI = "cel.expr.conformance.proto2.int32_ext"; - String extN = "cel.expr.conformance.proto2.nested_ext"; - String extR = "cel.expr.conformance.proto2.repeated_test_all_types"; - String protoName = dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor().getFullName(); - Type protoType = createMessage(protoName); - String holderName = dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor().getFullName(); - Type holderType = createMessage(holderName); - Type holderListType = CelProtoTypes.createList(holderType); - dev.cel.expr.conformance.proto2.TestAllTypes withoutExt = - dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder().setSingleInt32(50).build(); - dev.cel.expr.conformance.proto2.TestAllTypes withExt = - dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() - .setSingleInt32(100) - .setExtension(TestAllTypesExtensions.int32Ext, 200) - .setExtension(TestAllTypesExtensions.nestedExt, withoutExt) - .addExtension( - TestAllTypesExtensions.repeatedTestAllTypes, - dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() - .setSingleString("alpha") - .build()) - .addExtension( - TestAllTypesExtensions.repeatedTestAllTypes, - dev.cel.expr.conformance.proto2.TestAllTypes.newBuilder() - .setSingleString("alpha") - .build()) - .build(); - - declareVariable("y", protoType); - declareVariable("n", protoType); - - declareMemberFunction("getI", ImmutableList.of(protoType), CelProtoTypes.INT64); - declareMemberFunction("hasI", ImmutableList.of(protoType), CelProtoTypes.BOOL); - declareMemberFunction("assignI", ImmutableList.of(protoType, CelProtoTypes.INT64), protoType); - declareMemberFunction("clearI", ImmutableList.of(protoType), protoType); - - declareMemberFunction("getN", ImmutableList.of(protoType), protoType); - declareMemberFunction("hasN", ImmutableList.of(protoType), CelProtoTypes.BOOL); - declareMemberFunction("assignN", ImmutableList.of(protoType, protoType), protoType); - declareMemberFunction("clearN", ImmutableList.of(protoType), protoType); - - declareMemberFunction("getR", ImmutableList.of(protoType), holderListType); - declareMemberFunction("assignR", ImmutableList.of(protoType, holderListType), protoType); - declareMemberFunction("clearR", ImmutableList.of(protoType), protoType); - - AsyncDispatcher dispatcher = (AsyncDispatcher) evalAsync.registrar(); - addFunctionWithMessageProcessor( - dispatcher, - "getI", - dev.cel.expr.conformance.proto2.TestAllTypes.class, - mp -> p -> mp.makeExtensionGetter(null, 0L, extI).getField(p)); - addFunctionWithMessageProcessor( - dispatcher, - "hasI", - dev.cel.expr.conformance.proto2.TestAllTypes.class, - mp -> p -> mp.makeExtensionTester(null, 0L, extI).hasField(p)); - addFunctionWithMessageProcessor( - dispatcher, - "assignI", - dev.cel.expr.conformance.proto2.TestAllTypes.class, - Long.class, - mp -> - (p, i) -> - mp.makeExtensionAssigner(null, 0L, extI, CelProtoTypes.INT64) - .assign(p.toBuilder(), i) - .build()); - addFunctionWithMessageProcessor( - dispatcher, - "clearI", - dev.cel.expr.conformance.proto2.TestAllTypes.class, - mp -> p -> mp.makeExtensionClearer(null, 0L, extI).clear(p.toBuilder()).build()); - addFunctionWithMessageProcessor( - dispatcher, - "getN", - dev.cel.expr.conformance.proto2.TestAllTypes.class, - mp -> p -> mp.makeExtensionGetter(null, 0L, extN).getField(p)); - addFunctionWithMessageProcessor( - dispatcher, - "hasN", - dev.cel.expr.conformance.proto2.TestAllTypes.class, - mp -> p -> mp.makeExtensionTester(null, 0L, extN).hasField(p)); - addFunctionWithMessageProcessor( - dispatcher, - "assignN", - dev.cel.expr.conformance.proto2.TestAllTypes.class, - dev.cel.expr.conformance.proto2.TestAllTypes.class, - mp -> - (p, i) -> - mp.makeExtensionAssigner(null, 0L, extN, protoType) - .assign(p.toBuilder(), i) - .build()); - addFunctionWithMessageProcessor( - dispatcher, - "clearN", - dev.cel.expr.conformance.proto2.TestAllTypes.class, - mp -> p -> mp.makeExtensionClearer(null, 0L, extN).clear(p.toBuilder()).build()); - addFunctionWithMessageProcessor( - dispatcher, - "getR", - dev.cel.expr.conformance.proto2.TestAllTypes.class, - mp -> p -> mp.makeExtensionGetter(null, 0L, extR).getField(p)); - addFunctionWithMessageProcessor( - dispatcher, - "assignR", - dev.cel.expr.conformance.proto2.TestAllTypes.class, - List.class, - mp -> - (p, l) -> - mp.makeExtensionAssigner(null, 0L, extR, holderListType) - .assign(p.toBuilder(), l) - .build()); - addFunctionWithMessageProcessor( - dispatcher, - "clearR", - dev.cel.expr.conformance.proto2.TestAllTypes.class, - mp -> p -> mp.makeExtensionClearer(null, 0L, extR).clear(p.toBuilder()).build()); - - container = dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor().getFullName(); - - source = - "[y.hasI(), y.getI() == 200, !n.hasI(), n.getI() == 0,\n" - + " n.assignI(43).hasI(), n.assignI(42).getI() == 42,\n" - + " y.assignI(99).hasI(), y.assignI(31).getI() == 31,\n" - + " !n.clearI().hasI(), !y.clearI().hasI(), y.clearI().getI() == 0,\n" - + " y.hasN(), y.getN().getI() == 0, !y.getN().hasN(), y.getN().getN().getI() == 0,\n" - + " !n.hasN(), n.assignN(y).getN().hasN(),\n" - + " !n.clearN().hasN(), !y.clearN().hasN(),\n" - + " n.getR() == [], y.getR().map(h, h.single_string) == [\"alpha\", \"beta\"],\n" - + " n.assignR([\"a\", \"b\"].map(s, TestAllTypes{single_string:s}))." - + "getR().map(h, h.single_string) == [\"a\", \"b\"],\n" - + " y.clearR().getR() == []]"; - runTest(Activation.copyOf(ImmutableMap.of("y", withExt, "n", withoutExt))); - } -} From a9678c4b04aabf346cc91fa2021130d2f254119e Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 10 Jan 2025 16:44:26 -0800 Subject: [PATCH 5/9] Internal Changes PiperOrigin-RevId: 714271078 --- runtime/BUILD.bazel | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index 3635c0961..7058bfc8b 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -12,7 +12,6 @@ java_library( java_library( name = "interpreter", - visibility = ["//visibility:public"], exports = ["//runtime/src/main/java/dev/cel/runtime:interpreter"], ) @@ -41,3 +40,8 @@ java_library( name = "evaluation_listener", exports = ["//runtime/src/main/java/dev/cel/runtime:evaluation_listener"], ) + +java_library( + name = "base", + exports = ["//runtime/src/main/java/dev/cel/runtime:base"], +) From 6ee7f7e9f007399d52dd597922659a18bea6d16d Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 13 Jan 2025 11:34:53 -0800 Subject: [PATCH 6/9] Add CelOptions for designating Regex program size Addresses https://github.com/google/cel-java/issues/545 PiperOrigin-RevId: 715048086 --- WORKSPACE | 2 +- .../test/java/dev/cel/bundle/CelImplTest.java | 33 +++++++++++++++++++ .../main/java/dev/cel/common/CelOptions.java | 20 +++++++++-- .../dev/cel/runtime/CelStandardFunctions.java | 5 ++- .../java/dev/cel/runtime/RuntimeHelpers.java | 16 +++++++-- 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index 2c9bc18a2..d6e54d6ea 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -78,7 +78,7 @@ maven_install( "com.google.guava:guava-testlib:33.3.1-jre", "com.google.protobuf:protobuf-java:4.28.3", "com.google.protobuf:protobuf-java-util:4.28.3", - "com.google.re2j:re2j:1.7", + "com.google.re2j:re2j:1.8", "com.google.testparameterinjector:test-parameter-injector:1.18", "com.google.truth.extensions:truth-java8-extension:1.4.4", "com.google.truth.extensions:truth-proto-extension:1.4.4", diff --git a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java index db4b60fe1..a5a8d7db1 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java @@ -2032,6 +2032,39 @@ public void program_comprehensionDisabled_throws() throws Exception { assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.ITERATION_BUDGET_EXCEEDED); } + @Test + public void program_regexProgramSizeUnderLimit_success() throws Exception { + Cel cel = + standardCelBuilderWithMacros() + .setOptions(CelOptions.current().maxRegexProgramSize(7).build()) + .build(); + // See + // https://github.com/google/re2j/blob/84237cbbd0fbd637c6eb6856717c1e248daae729/javatests/com/google/re2j/PatternTest.java#L175 for program size + CelAbstractSyntaxTree ast = cel.compile("'foo'.matches('(a+b)')").getAst(); + + assertThat(cel.createProgram(ast).eval()).isEqualTo(false); + } + + @Test + public void program_regexProgramSizeExceedsLimit_throws() throws Exception { + Cel cel = + standardCelBuilderWithMacros() + .setOptions(CelOptions.current().maxRegexProgramSize(6).build()) + .build(); + // See + // https://github.com/google/re2j/blob/84237cbbd0fbd637c6eb6856717c1e248daae729/javatests/com/google/re2j/PatternTest.java#L175 for program size + CelAbstractSyntaxTree ast = cel.compile("'foo'.matches('(a+b)')").getAst(); + + CelEvaluationException e = + assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval()); + assertThat(e) + .hasMessageThat() + .contains( + "evaluation error: Regex pattern exceeds allowed program size. Allowed: 6, Provided:" + + " 7"); + assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.INVALID_ARGUMENT); + } + @Test public void toBuilder_isImmutable() { CelBuilder celBuilder = CelFactory.standardCelBuilder(); diff --git a/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java index 3c07ab26d..7c968ebb2 100644 --- a/common/src/main/java/dev/cel/common/CelOptions.java +++ b/common/src/main/java/dev/cel/common/CelOptions.java @@ -39,7 +39,7 @@ public enum ProtoUnsetFieldOptions { // Do not bind a field if it is unset. Repeated fields are bound as empty list. SKIP, // Bind the (proto api) default value for a field. - BIND_DEFAULT; + BIND_DEFAULT } public static final CelOptions DEFAULT = current().build(); @@ -121,6 +121,8 @@ public enum ProtoUnsetFieldOptions { public abstract boolean enableComprehension(); + public abstract int maxRegexProgramSize(); + public abstract Builder toBuilder(); public ImmutableSet toExprFeatures() { @@ -218,7 +220,8 @@ public static Builder newBuilder() { .enableStringConversion(true) .enableStringConcatenation(true) .enableListConcatenation(true) - .enableComprehension(true); + .enableComprehension(true) + .maxRegexProgramSize(-1); } /** @@ -571,6 +574,19 @@ public abstract static class Builder { */ public abstract Builder enableComprehension(boolean value); + /** + * Set maximum program size for RE2J regex. + * + *

The program size is a very approximate measure of a regexp's "cost". Larger numbers are + * more expensive than smaller numbers. + * + *

A negative {@code value} will disable the check. + * + *

There's no guarantee that RE2 program size has the exact same value across other CEL + * implementations (C++ and Go). + */ + public abstract Builder maxRegexProgramSize(int value); + public abstract CelOptions build(); } } diff --git a/runtime/src/main/java/dev/cel/runtime/CelStandardFunctions.java b/runtime/src/main/java/dev/cel/runtime/CelStandardFunctions.java index bd1fc45f9..dc6ca979b 100644 --- a/runtime/src/main/java/dev/cel/runtime/CelStandardFunctions.java +++ b/runtime/src/main/java/dev/cel/runtime/CelStandardFunctions.java @@ -28,7 +28,6 @@ import com.google.protobuf.Timestamp; import com.google.protobuf.util.Durations; import com.google.protobuf.util.Timestamps; -import com.google.re2j.PatternSyntaxException; import dev.cel.common.CelErrorCode; import dev.cel.common.CelOptions; import dev.cel.common.internal.ComparisonFunctions; @@ -1000,7 +999,7 @@ public enum StringMatchers implements StandardOverload { (String string, String regexp) -> { try { return RuntimeHelpers.matches(string, regexp, bindingHelper.celOptions); - } catch (PatternSyntaxException e) { + } catch (RuntimeException e) { throw new CelEvaluationException( e.getMessage(), e, CelErrorCode.INVALID_ARGUMENT); } @@ -1015,7 +1014,7 @@ public enum StringMatchers implements StandardOverload { (String string, String regexp) -> { try { return RuntimeHelpers.matches(string, regexp, bindingHelper.celOptions); - } catch (PatternSyntaxException e) { + } catch (RuntimeException e) { throw new CelEvaluationException( e.getMessage(), e, CelErrorCode.INVALID_ARGUMENT); } diff --git a/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java b/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java index ffb979842..b885bb55d 100644 --- a/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java @@ -74,12 +74,22 @@ public static boolean matches(String string, String regexp) { } public static boolean matches(String string, String regexp, CelOptions celOptions) { + Pattern pattern = Pattern.compile(regexp); + int maxProgramSize = celOptions.maxRegexProgramSize(); + if (maxProgramSize >= 0 && pattern.programSize() > maxProgramSize) { + throw new IllegalArgumentException( + String.format( + "Regex pattern exceeds allowed program size. Allowed: %d, Provided: %d", + maxProgramSize, pattern.programSize())); + } + if (!celOptions.enableRegexPartialMatch()) { // Uses re2 for consistency across languages. - return Pattern.matches(regexp, string); + return pattern.matcher(string).matches(); } - // Return an unanchored match for the presence of the regexp anywher in the string. - return Pattern.compile(regexp).matcher(string).find(); + + // Return an unanchored match for the presence of the regexp anywhere in the string. + return pattern.matcher(string).find(); } /** Create a compiled pattern for the given regular expression. */ From b810258b3604323809a5fc52094869d6ea1f916f Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 13 Jan 2025 15:11:49 -0800 Subject: [PATCH 7/9] Create a separate maven artifact for adapting canonical CEL protos (CelProtoAbstractSyntaxTree) PiperOrigin-RevId: 715123218 --- publish/BUILD.bazel | 53 +++++++++++++++++++++++++++++++-------------- publish/publish.sh | 2 +- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/publish/BUILD.bazel b/publish/BUILD.bazel index 050292eab..1d0530077 100644 --- a/publish/BUILD.bazel +++ b/publish/BUILD.bazel @@ -47,10 +47,14 @@ OPTIMIZER_TARGETS = [ "//optimizer/src/main/java/dev/cel/optimizer/optimizers:common_subexpression_elimination", ] -V1ALPHA1_UTILITY_TARGETS = [ +V1ALPHA1_AST_TARGETS = [ "//common/src/main/java/dev/cel/common:proto_v1alpha1_ast", ] +CANONICAL_AST_TARGETS = [ + "//common/src/main/java/dev/cel/common:proto_ast", +] + EXTENSION_TARGETS = [ "//extensions/src/main/java/dev/cel/extensions", "//extensions/src/main/java/dev/cel/extensions:optional_library", @@ -58,7 +62,12 @@ EXTENSION_TARGETS = [ ALL_TARGETS = [ "//bundle/src/main/java/dev/cel/bundle:cel", -] + RUNTIME_TARGETS + COMPILER_TARGETS + EXTENSION_TARGETS + V1ALPHA1_UTILITY_TARGETS + OPTIMIZER_TARGETS + VALIDATOR_TARGETS +] + RUNTIME_TARGETS + COMPILER_TARGETS + EXTENSION_TARGETS + V1ALPHA1_AST_TARGETS + CANONICAL_AST_TARGETS + OPTIMIZER_TARGETS + VALIDATOR_TARGETS + +# Excluded from the JAR as their source of truth is elsewhere +EXCLUDED_TARGETS = [ + "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", +] pom_file( name = "cel_pom", @@ -74,9 +83,7 @@ pom_file( java_export( name = "cel", - deploy_env = [ - "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", - ], + deploy_env = EXCLUDED_TARGETS, maven_coordinates = "dev.cel:cel:%s" % CEL_VERSION, pom_template = ":cel_pom", runtime_deps = ALL_TARGETS, @@ -96,9 +103,7 @@ pom_file( java_export( name = "cel_compiler", - deploy_env = [ - "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", - ], + deploy_env = EXCLUDED_TARGETS, maven_coordinates = "dev.cel:compiler:%s" % CEL_VERSION, pom_template = ":cel_compiler_pom", runtime_deps = COMPILER_TARGETS, @@ -118,9 +123,7 @@ pom_file( java_export( name = "cel_runtime", - deploy_env = [ - "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", - ], + deploy_env = EXCLUDED_TARGETS, maven_coordinates = "dev.cel:runtime:%s" % CEL_VERSION, pom_template = ":cel_runtime_pom", runtime_deps = RUNTIME_TARGETS, @@ -134,16 +137,34 @@ pom_file( "PACKAGE_NAME": "CEL Java v1alpha1 Utility", "PACKAGE_DESC": "Common Expression Language Utility for supporting v1alpha1 protobuf definitions", }, - targets = V1ALPHA1_UTILITY_TARGETS, + targets = V1ALPHA1_AST_TARGETS, template_file = "pom_template.xml", ) java_export( name = "cel_v1alpha1", - deploy_env = [ - "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", - ], + deploy_env = EXCLUDED_TARGETS, maven_coordinates = "dev.cel:v1alpha1:%s" % CEL_VERSION, pom_template = ":cel_v1alpha1_pom", - runtime_deps = V1ALPHA1_UTILITY_TARGETS, + runtime_deps = V1ALPHA1_AST_TARGETS, +) + +pom_file( + name = "cel_protobuf_pom", + substitutions = { + "CEL_VERSION": CEL_VERSION, + "CEL_ARTIFACT_ID": "protobuf", + "PACKAGE_NAME": "CEL Java Protobuf adapter", + "PACKAGE_DESC": "Common Expression Language Adapter for converting canonical cel.expr protobuf definitions", + }, + targets = CANONICAL_AST_TARGETS, + template_file = "pom_template.xml", +) + +java_export( + name = "cel_protobuf", + deploy_env = EXCLUDED_TARGETS, + maven_coordinates = "dev.cel:protobuf:%s" % CEL_VERSION, + pom_template = ":cel_protobuf_pom", + runtime_deps = CANONICAL_AST_TARGETS, ) diff --git a/publish/publish.sh b/publish/publish.sh index b5d301ce2..8a284ba48 100755 --- a/publish/publish.sh +++ b/publish/publish.sh @@ -25,7 +25,7 @@ # 2. You will need to enter the key's password. The prompt appears in GUI, not in terminal. The publish operation will eventually timeout if the password is not entered. -ALL_TARGETS=("//publish:cel.publish" "//publish:cel_compiler.publish" "//publish:cel_runtime.publish" "//publish:cel_v1alpha1.publish") +ALL_TARGETS=("//publish:cel.publish" "//publish:cel_compiler.publish" "//publish:cel_runtime.publish" "//publish:cel_v1alpha1.publish" "//publish:cel_protobuf.publish") function publish_maven_remote() { maven_repo_url=$1 From 6090008a237af73b1f68a5770546017423ca0679 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 13 Jan 2025 17:13:43 -0800 Subject: [PATCH 8/9] Isolate CelAbstractSyntaxTree and CelSource from common build target PiperOrigin-RevId: 715161676 --- common/BUILD.bazel | 17 ++++++- .../src/main/java/dev/cel/common/BUILD.bazel | 51 ++++++++++++++++++- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/common/BUILD.bazel b/common/BUILD.bazel index af0bf8abb..78ab669bd 100644 --- a/common/BUILD.bazel +++ b/common/BUILD.bazel @@ -6,8 +6,13 @@ package( ) java_library( + # TODO: Split this target and migrate consumers name = "common", - exports = ["//common/src/main/java/dev/cel/common"], + exports = [ + "//common/src/main/java/dev/cel/common", + "//common/src/main/java/dev/cel/common:cel_ast", + "//common/src/main/java/dev/cel/common:cel_source", + ], ) java_library( @@ -72,3 +77,13 @@ java_library( name = "source_location", exports = ["//common/src/main/java/dev/cel/common:source_location"], ) + +java_library( + name = "cel_source", + exports = ["//common/src/main/java/dev/cel/common:cel_source"], +) + +java_library( + name = "cel_ast", + exports = ["//common/src/main/java/dev/cel/common:cel_ast"], +) diff --git a/common/src/main/java/dev/cel/common/BUILD.bazel b/common/src/main/java/dev/cel/common/BUILD.bazel index 31fd401c5..0592bce6d 100644 --- a/common/src/main/java/dev/cel/common/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/BUILD.bazel @@ -10,11 +10,9 @@ package( # keep sorted COMMON_SOURCES = [ - "CelAbstractSyntaxTree.java", "CelDescriptorUtil.java", "CelDescriptors.java", "CelException.java", - "CelSource.java", ] # keep sorted @@ -49,6 +47,8 @@ java_library( tags = [ ], deps = [ + ":cel_ast", + ":cel_source", ":error_codes", ":source", ":source_location", @@ -72,6 +72,8 @@ java_library( tags = [ ], deps = [ + ":cel_ast", + ":cel_source", ":common", ":source", ":source_location", @@ -116,6 +118,8 @@ java_library( tags = [ ], deps = [ + ":cel_ast", + ":cel_source", "//common", "//common/ast:expr_converter", "//common/types:cel_proto_types", @@ -131,7 +135,10 @@ java_library( tags = [ ], deps = [ + ":cel_ast", + ":cel_source", ":common", + "//common:cel_source", "//common/ast:expr_v1alpha1_converter", "//common/types:cel_v1alpha1_types", "@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto", @@ -164,6 +171,7 @@ java_library( tags = [ ], deps = [ + ":cel_ast", ":mutable_source", "//common", "//common/ast", @@ -178,6 +186,7 @@ java_library( tags = [ ], deps = [ + ":cel_source", ":common", "//:auto_value", "//common/ast:mutable_expr", @@ -213,6 +222,44 @@ java_library( ], ) +java_library( + name = "cel_source", + srcs = ["CelSource.java"], + tags = [ + "alt_dep=//common:cel_source", + "avoid_dep", + ], + deps = [ + ":source", + ":source_location", + "//:auto_value", + "//common/annotations", + "//common/ast", + "//common/internal", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "cel_ast", + srcs = ["CelAbstractSyntaxTree.java"], + tags = [ + "alt_dep=//common:cel_ast", + "avoid_dep", + ], + deps = [ + ":cel_source", + "//:auto_value", + "//common/annotations", + "//common/ast", + "//common/types", + "//common/types:type_providers", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) + java_library( name = "source", srcs = SOURCE_SOURCES, From b59c3fd59fe64cc6060ecf75b65051f2345539cd Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Mon, 13 Jan 2025 17:37:22 -0800 Subject: [PATCH 9/9] Release 0.9.1 PiperOrigin-RevId: 715168038 --- publish/cel_version.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/publish/cel_version.bzl b/publish/cel_version.bzl index a0ebb4dd7..1847a1923 100644 --- a/publish/cel_version.bzl +++ b/publish/cel_version.bzl @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. """Maven artifact version for CEL.""" -CEL_VERSION = "0.9.0" +CEL_VERSION = "0.9.1"