Skip to content

Commit 690b422

Browse files
l46kokcopybara-github
authored andcommitted
Implement CelMutableExpr
PiperOrigin-RevId: 622263802
1 parent bbe5ed4 commit 690b422

File tree

5 files changed

+306
-0
lines changed

5 files changed

+306
-0
lines changed

common/ast/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,8 @@ java_library(
3232
name = "expr_util",
3333
exports = ["//common/src/main/java/dev/cel/common/ast:expr_util"],
3434
)
35+
36+
java_library(
37+
name = "mutable_ast",
38+
exports = ["//common/src/main/java/dev/cel/common/ast:mutable_ast"],
39+
)

common/src/main/java/dev/cel/common/ast/BUILD.bazel

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ EXPR_FACTORY_SOURCES = [
3131
"CelExprIdGeneratorFactory.java",
3232
]
3333

34+
# keep sorted
35+
MUTABLE_AST_SOURCES = [
36+
"CelMutableExpr.java",
37+
]
38+
3439
java_library(
3540
name = "ast",
3641
srcs = AST_SOURCES,
@@ -109,3 +114,14 @@ java_library(
109114
"@maven//:com_google_guava_guava",
110115
],
111116
)
117+
118+
java_library(
119+
name = "mutable_ast",
120+
srcs = MUTABLE_AST_SOURCES,
121+
tags = [
122+
],
123+
deps = [
124+
":ast",
125+
"@maven//:com_google_guava_guava",
126+
],
127+
)
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.common.ast;
16+
17+
import static com.google.common.base.Preconditions.checkArgument;
18+
19+
import dev.cel.common.ast.CelExpr.CelNotSet;
20+
import dev.cel.common.ast.CelExpr.ExprKind;
21+
import dev.cel.common.ast.CelExpr.ExprKind.Kind;
22+
23+
/**
24+
* An abstract representation of a common expression that allows mutation in any of its properties.
25+
* The expressions are semantically the same as that of the immutable {@link CelExpr}.
26+
*
27+
* <p>This allows for an efficient optimization of an AST without having to traverse and rebuild the
28+
* entire tree.
29+
*
30+
* <p>This class is not thread-safe by design.
31+
*/
32+
public final class CelMutableExpr {
33+
private long id;
34+
private ExprKind.Kind exprKind;
35+
private CelNotSet notSet;
36+
private CelConstant constant;
37+
private int hash = 0;
38+
39+
public long id() {
40+
return id;
41+
}
42+
43+
public void setId(long id) {
44+
this.id = id;
45+
}
46+
47+
public ExprKind.Kind getKind() {
48+
return exprKind;
49+
}
50+
51+
public CelNotSet notSet() {
52+
checkExprKind(Kind.NOT_SET);
53+
return notSet;
54+
}
55+
56+
public CelConstant constant() {
57+
checkExprKind(Kind.CONSTANT);
58+
return constant;
59+
}
60+
61+
public void setConstant(CelConstant constant) {
62+
this.exprKind = ExprKind.Kind.CONSTANT;
63+
this.constant = constant;
64+
}
65+
66+
public static CelMutableExpr ofConstant(CelConstant constant) {
67+
return ofConstant(0L, constant);
68+
}
69+
70+
public static CelMutableExpr ofConstant(long id, CelConstant constant) {
71+
return new CelMutableExpr(id, constant);
72+
}
73+
74+
public static CelMutableExpr ofNotSet() {
75+
return ofNotSet(0L);
76+
}
77+
78+
public static CelMutableExpr ofNotSet(long id) {
79+
return new CelMutableExpr(id);
80+
}
81+
82+
private CelMutableExpr(long id, CelConstant mutableConstant) {
83+
this.id = id;
84+
setConstant(mutableConstant);
85+
}
86+
87+
private CelMutableExpr(long id) {
88+
this();
89+
this.id = id;
90+
}
91+
92+
private CelMutableExpr() {
93+
this.notSet = CelExpr.newBuilder().build().exprKind().notSet();
94+
this.exprKind = ExprKind.Kind.NOT_SET;
95+
}
96+
97+
private Object exprValue() {
98+
switch (this.exprKind) {
99+
case NOT_SET:
100+
return notSet();
101+
case CONSTANT:
102+
return constant();
103+
case IDENT:
104+
case SELECT:
105+
case CALL:
106+
case CREATE_LIST:
107+
case CREATE_STRUCT:
108+
case CREATE_MAP:
109+
case COMPREHENSION:
110+
// fall-through (not implemented yet)
111+
}
112+
113+
throw new IllegalStateException("Unexpected expr kind: " + this.exprKind);
114+
}
115+
116+
private void checkExprKind(ExprKind.Kind exprKind) {
117+
checkArgument(this.exprKind.equals(exprKind), "Invalid ExprKind: %s", exprKind);
118+
}
119+
120+
@Override
121+
public boolean equals(Object obj) {
122+
if (obj == this) {
123+
return true;
124+
}
125+
if (obj instanceof CelMutableExpr) {
126+
CelMutableExpr that = (CelMutableExpr) obj;
127+
if (this.id != that.id() || !this.exprKind.equals(that.getKind())) {
128+
return false;
129+
}
130+
// When both objects' hashes are cached and they do not match, they can never be equal.
131+
if (this.hash != 0 && that.hash != 0 && this.hash != that.hash) {
132+
return false;
133+
}
134+
return this.exprValue().equals(that.exprValue());
135+
}
136+
137+
return false;
138+
}
139+
140+
@Override
141+
public int hashCode() {
142+
if (hash == 0) {
143+
int h = 1;
144+
h *= 1000003;
145+
h ^= (int) ((id >>> 32) ^ id);
146+
h *= 1000003;
147+
h ^= this.exprValue().hashCode();
148+
149+
if (h == 0) {
150+
h = 1;
151+
}
152+
hash = h;
153+
}
154+
155+
return hash;
156+
}
157+
}

common/src/test/java/dev/cel/common/ast/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ java_library(
1818
"//common/ast:expr_converter",
1919
"//common/ast:expr_factory",
2020
"//common/ast:expr_v1alpha1_converter",
21+
"//common/ast:mutable_ast",
2122
"//common/resources/testdata/proto3:test_all_types_java_proto",
2223
"//common/types",
2324
"//compiler",
@@ -28,6 +29,7 @@ java_library(
2829
"@cel_spec//proto/cel/expr:expr_java_proto",
2930
"@com_google_googleapis//google/api/expr/v1alpha1:expr_java_proto",
3031
"@maven//:com_google_guava_guava",
32+
"@maven//:com_google_guava_guava_testlib",
3133
"@maven//:com_google_protobuf_protobuf_java",
3234
"@maven//:com_google_testparameterinjector_test_parameter_injector",
3335
"@maven//:junit_junit",
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.common.ast;
16+
17+
import static com.google.common.truth.Truth.assertThat;
18+
import static org.junit.Assert.assertThrows;
19+
20+
import com.google.common.testing.EqualsTester;
21+
import com.google.testing.junit.testparameterinjector.TestParameter;
22+
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
23+
import dev.cel.common.ast.CelExpr.ExprKind.Kind;
24+
import org.junit.Test;
25+
import org.junit.runner.RunWith;
26+
27+
@RunWith(TestParameterInjector.class)
28+
public class CelMutableExprTest {
29+
30+
@Test
31+
public void ofNotSet() {
32+
CelMutableExpr mutableExpr = CelMutableExpr.ofNotSet();
33+
34+
assertThat(mutableExpr.id()).isEqualTo(0L);
35+
assertThat(mutableExpr.notSet()).isNotNull();
36+
}
37+
38+
@Test
39+
public void ofNotSet_withId() {
40+
CelMutableExpr mutableExpr = CelMutableExpr.ofNotSet(1L);
41+
42+
assertThat(mutableExpr.id()).isEqualTo(1L);
43+
assertThat(mutableExpr.notSet()).isNotNull();
44+
}
45+
46+
@Test
47+
public void ofConstant() {
48+
CelMutableExpr mutableExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(5L));
49+
50+
assertThat(mutableExpr.id()).isEqualTo(0L);
51+
assertThat(mutableExpr.constant()).isEqualTo(CelConstant.ofValue(5L));
52+
}
53+
54+
@Test
55+
public void ofConstant_withId() {
56+
CelMutableExpr mutableExpr = CelMutableExpr.ofConstant(1L, CelConstant.ofValue(5L));
57+
58+
assertThat(mutableExpr.id()).isEqualTo(1L);
59+
assertThat(mutableExpr.constant()).isEqualTo(CelConstant.ofValue(5L));
60+
}
61+
62+
@Test
63+
public void setId_success() {
64+
CelMutableExpr mutableExpr = CelMutableExpr.ofConstant(CelConstant.ofValue(5L));
65+
66+
mutableExpr.setId(2L);
67+
68+
assertThat(mutableExpr.id()).isEqualTo(2L);
69+
}
70+
71+
@Test
72+
public void equalityTest() {
73+
new EqualsTester()
74+
.addEqualityGroup(CelMutableExpr.ofNotSet())
75+
.addEqualityGroup(CelMutableExpr.ofNotSet(1L), CelMutableExpr.ofNotSet(1L))
76+
.addEqualityGroup(CelMutableExpr.ofConstant(1L, CelConstant.ofValue(2L)))
77+
.addEqualityGroup(
78+
CelMutableExpr.ofConstant(5L, CelConstant.ofValue("hello")),
79+
CelMutableExpr.ofConstant(5L, CelConstant.ofValue("hello")))
80+
.testEquals();
81+
}
82+
83+
@SuppressWarnings("Immutable") // Mutable by design
84+
private enum MutableExprKindTestCase {
85+
NOT_SET(CelMutableExpr.ofNotSet(1L)),
86+
CONSTANT(CelMutableExpr.ofConstant(CelConstant.ofValue(2L)));
87+
88+
private final CelMutableExpr mutableExpr;
89+
90+
MutableExprKindTestCase(CelMutableExpr mutableExpr) {
91+
this.mutableExpr = mutableExpr;
92+
}
93+
}
94+
95+
@Test
96+
public void getExprValue_invalidKind_throws(@TestParameter MutableExprKindTestCase testCase) {
97+
Kind testCaseKind = testCase.mutableExpr.getKind();
98+
if (!testCaseKind.equals(Kind.NOT_SET)) {
99+
assertThrows(IllegalArgumentException.class, testCase.mutableExpr::notSet);
100+
}
101+
if (!testCaseKind.equals(Kind.CONSTANT)) {
102+
assertThrows(IllegalArgumentException.class, testCase.mutableExpr::constant);
103+
}
104+
}
105+
106+
@SuppressWarnings("Immutable") // Mutable by design
107+
private enum HashCodeTestCase {
108+
NOT_SET(CelMutableExpr.ofNotSet(1L), -722379961),
109+
CONSTANT(CelMutableExpr.ofConstant(2L, CelConstant.ofValue("test")), -724279919);
110+
111+
private final CelMutableExpr mutableExpr;
112+
private final int expectedHashCode;
113+
114+
HashCodeTestCase(CelMutableExpr mutableExpr, int expectedHashCode) {
115+
this.mutableExpr = mutableExpr;
116+
this.expectedHashCode = expectedHashCode;
117+
}
118+
}
119+
120+
@Test
121+
public void hashCodeTest(@TestParameter HashCodeTestCase testCase) {
122+
assertThat(testCase.mutableExpr.hashCode()).isEqualTo(testCase.expectedHashCode);
123+
// Run it twice to ensure cached value is stable
124+
assertThat(testCase.mutableExpr.hashCode()).isEqualTo(testCase.expectedHashCode);
125+
}
126+
}

0 commit comments

Comments
 (0)