Skip to content

Commit b2b9b73

Browse files
committed
GROOVY-12255: Select isCase on switch-expression arms without widening transform
Resolve a non-intrinsic case label as label.isCase(selector) in the type checker and store the chosen MethodNode on the CaseStatement so static codegen can emit a direct call. Keep ClassCodeExpressionTransformer.transform generic: SwitchExpression.transformExpression still rewrites only the selector and labels, and ResolveVisitor, StaticImportVisitor, static compilation and GINQ visit the node themselves, the same way they walk closures.
1 parent 7942320 commit b2b9b73

16 files changed

Lines changed: 691 additions & 289 deletions

File tree

COMPATIBILITY.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,11 +253,20 @@ assumed a switch expression was a `MethodCallExpression` wrapping a
253253
`SwitchStatement` need to handle `SwitchExpression` and `YieldStatement`.
254254
`GroovyCodeVisitor` supplies default methods so existing visitors keep
255255
compiling. `SwitchExpression.transformExpression` rewrites the selector and
256-
case-label expressions only and does not copy arm statements. A
257-
`ClassCodeExpressionTransformer` (including `ResolveVisitor`) walks the
258-
node in place via `visitSwitchExpression`, so nested arm expressions still
259-
run through resolve and rewrite. A plain `ExpressionTransformer` leaves
260-
arm bodies untouched.
256+
case-label expressions only and does not copy arm statements.
257+
`ClassCodeExpressionTransformer.transform` stays generic — it
258+
delegates to `transformExpression`, the same way it treats
259+
`ClosureExpression`. `ResolveVisitor`, `StaticImportVisitor` and the static-compilation
260+
transformer visit the node from their own `transform` override so
261+
arm expressions still run through resolve, static-import rewrite
262+
and static compilation.
263+
A plain `ExpressionTransformer` leaves arm bodies untouched.
264+
265+
Under `@CompileStatic`, a non-intrinsic `case` label is resolved as
266+
`label.isCase(selector)` and emitted as a direct method call. An
267+
`Object`-typed label therefore uses `isCase(Object, Object)` (equals),
268+
the same as a statically compiled `label.isCase(selector)` call. Dynamic
269+
Groovy still dispatches `isCase` on the label's runtime class.
261270

262271
**What is *not* claimed.** Matching still uses Groovy `isCase` (Class, regex,
263272
Collection, Closure). `tableswitch` / `lookupswitch` are emitted only when
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.groovy.ast.tools;
20+
21+
import org.codehaus.groovy.ast.ClassHelper;
22+
import org.codehaus.groovy.ast.ClassNode;
23+
import org.codehaus.groovy.ast.DynamicVariable;
24+
import org.codehaus.groovy.ast.FieldNode;
25+
import org.codehaus.groovy.ast.expr.ClassExpression;
26+
import org.codehaus.groovy.ast.expr.ConstantExpression;
27+
import org.codehaus.groovy.ast.expr.Expression;
28+
import org.codehaus.groovy.ast.expr.PropertyExpression;
29+
import org.codehaus.groovy.ast.expr.VariableExpression;
30+
import org.codehaus.groovy.ast.stmt.CaseStatement;
31+
32+
import java.util.List;
33+
34+
/**
35+
* Shared classification of switch-expression labels for static compilation.
36+
* Intrinsic dispatch ({@code tableswitch} / {@code lookupswitch} / enum name)
37+
* is used only when every case label is a compile-time constant of a type
38+
* {@code javac} would switch on. Mixed labels fall back to {@code isCase}.
39+
*
40+
* @since 6.0.0
41+
*/
42+
public final class SwitchExpressionUtils {
43+
44+
private SwitchExpressionUtils() {
45+
}
46+
47+
/**
48+
* True when the selector type and every case label permit tableswitch,
49+
* lookupswitch, or enum-name dispatch. An empty case list, a {@code null}
50+
* label, or a single non-constant label (a range, a variable, …) makes
51+
* the whole switch an {@code isCase} switch.
52+
*
53+
* @param selectorType the inferred type of the selector
54+
* @param caseStatements the case arms
55+
* @return {@code true} if codegen can emit an intrinsic switch
56+
*/
57+
public static boolean isOptimizedSwitch(final ClassNode selectorType, final List<CaseStatement> caseStatements) {
58+
if (selectorType == null || caseStatements == null || caseStatements.isEmpty()) {
59+
return false;
60+
}
61+
return isOptimizedIntSwitch(selectorType, caseStatements)
62+
|| isOptimizedStringSwitch(selectorType, caseStatements)
63+
|| isOptimizedEnumSwitch(selectorType, caseStatements);
64+
}
65+
66+
/**
67+
* True when the selector is an integral primitive or wrapper and every
68+
* label is an int-family constant.
69+
*
70+
* @param selectorType the inferred type of the selector
71+
* @param caseStatements the case arms
72+
* @return {@code true} if int tableswitch / lookupswitch applies
73+
*/
74+
public static boolean isOptimizedIntSwitch(final ClassNode selectorType, final List<CaseStatement> caseStatements) {
75+
if (!isIntegralSelector(selectorType) || caseStatements == null || caseStatements.isEmpty()) {
76+
return false;
77+
}
78+
for (CaseStatement caseStatement : caseStatements) {
79+
if (intConstant(caseStatement.getExpression()) == null) {
80+
return false;
81+
}
82+
}
83+
return true;
84+
}
85+
86+
/**
87+
* True when the selector is {@link String} and every label is a string constant.
88+
*
89+
* @param selectorType the inferred type of the selector
90+
* @param caseStatements the case arms
91+
* @return {@code true} if string lookupswitch applies
92+
*/
93+
public static boolean isOptimizedStringSwitch(final ClassNode selectorType, final List<CaseStatement> caseStatements) {
94+
if (selectorType == null || !ClassHelper.isStringType(selectorType)
95+
|| caseStatements == null || caseStatements.isEmpty()) {
96+
return false;
97+
}
98+
for (CaseStatement caseStatement : caseStatements) {
99+
if (stringConstant(caseStatement.getExpression()) == null) {
100+
return false;
101+
}
102+
}
103+
return true;
104+
}
105+
106+
/**
107+
* True when the selector is an enum and every label names a constant of that enum.
108+
*
109+
* @param selectorType the inferred type of the selector
110+
* @param caseStatements the case arms
111+
* @return {@code true} if enum-name dispatch applies
112+
*/
113+
public static boolean isOptimizedEnumSwitch(final ClassNode selectorType, final List<CaseStatement> caseStatements) {
114+
ClassNode enumType = unwrapEnumType(selectorType);
115+
if (enumType == null || !enumType.isEnum() || caseStatements == null || caseStatements.isEmpty()) {
116+
return false;
117+
}
118+
for (CaseStatement caseStatement : caseStatements) {
119+
if (enumConstantName(caseStatement.getExpression(), enumType) == null) {
120+
return false;
121+
}
122+
}
123+
return true;
124+
}
125+
126+
/**
127+
* True for {@code byte}, {@code short}, {@code char}, {@code int} and their wrappers.
128+
*
129+
* @param type the selector type
130+
* @return {@code true} if the type can feed a JVM int switch
131+
*/
132+
public static boolean isIntegralSelector(final ClassNode type) {
133+
return isIntegralType(type) || isIntegralWrapper(type);
134+
}
135+
136+
/**
137+
* True for the integral primitive types {@code javac} switches on.
138+
*
139+
* @param type the type to test
140+
* @return {@code true} if the type is {@code int}, {@code byte}, {@code short}, or {@code char}
141+
*/
142+
public static boolean isIntegralType(final ClassNode type) {
143+
return type != null && (ClassHelper.isPrimitiveInt(type) || ClassHelper.isPrimitiveByte(type)
144+
|| ClassHelper.isPrimitiveShort(type) || ClassHelper.isPrimitiveChar(type));
145+
}
146+
147+
/**
148+
* True for the wrappers of the integral primitive types {@code javac} switches on.
149+
*
150+
* @param type the type to test
151+
* @return {@code true} if the type is {@code Integer}, {@code Byte}, {@code Short}, or {@code Character}
152+
*/
153+
public static boolean isIntegralWrapper(final ClassNode type) {
154+
return type != null && (ClassHelper.isWrapperInteger(type) || ClassHelper.isWrapperByte(type)
155+
|| ClassHelper.isWrapperShort(type) || ClassHelper.isWrapperCharacter(type));
156+
}
157+
158+
/**
159+
* Extracts an int-family constant from a case label, or {@code null}.
160+
*
161+
* @param expression the case label
162+
* @return the constant int value, or {@code null} if the label is not an int-family constant
163+
*/
164+
public static Integer intConstant(final Expression expression) {
165+
if (!(expression instanceof ConstantExpression constant)) return null;
166+
Object value = constant.getValue();
167+
if (value instanceof Integer || value instanceof Byte || value instanceof Short) {
168+
return ((Number) value).intValue();
169+
}
170+
if (value instanceof Character) {
171+
return (int) (Character) value;
172+
}
173+
return null;
174+
}
175+
176+
/**
177+
* Extracts a string constant from a case label, or {@code null}.
178+
*
179+
* @param expression the case label
180+
* @return the string value, or {@code null} if the label is not a string constant
181+
*/
182+
public static String stringConstant(final Expression expression) {
183+
if (expression instanceof ConstantExpression constant && constant.getValue() instanceof String s) {
184+
return s;
185+
}
186+
return null;
187+
}
188+
189+
/**
190+
* Returns the enum type behind {@code type}, or {@code null} if it is not an enum.
191+
*
192+
* @param type the selector type
193+
* @return the enum {@link ClassNode}, or {@code null}
194+
*/
195+
public static ClassNode unwrapEnumType(final ClassNode type) {
196+
if (type == null) return null;
197+
if (type.isEnum()) return type;
198+
return type.redirect().isEnum() ? type.redirect() : null;
199+
}
200+
201+
/**
202+
* Extracts the enum constant name from a case label of {@code enumType}, or {@code null}.
203+
* Accepts {@code EnumType.NAME}, an unqualified name that resolves to that constant,
204+
* or a constant expression holding the enum value.
205+
*
206+
* @param expression the case label
207+
* @param enumType the selector enum type
208+
* @return the constant name, or {@code null} if the label is not a constant of {@code enumType}
209+
*/
210+
public static String enumConstantName(final Expression expression, final ClassNode enumType) {
211+
if (expression instanceof PropertyExpression property
212+
&& property.getObjectExpression() instanceof ClassExpression classExpression
213+
&& classExpression.getType().equals(enumType)
214+
&& property.getProperty() instanceof ConstantExpression name) {
215+
return name.getText();
216+
}
217+
if (expression instanceof VariableExpression variable) {
218+
var accessed = variable.getAccessedVariable();
219+
if (accessed instanceof FieldNode field && field.isEnum()
220+
&& (field.getDeclaringClass() == null || field.getDeclaringClass().equals(enumType))) {
221+
return field.getName();
222+
}
223+
if (accessed == null || accessed instanceof DynamicVariable) {
224+
// a real local or parameter named like a constant is not a constant label
225+
FieldNode field = enumType.getField(variable.getName());
226+
if (field != null && field.isEnum()) {
227+
return variable.getName();
228+
}
229+
}
230+
}
231+
if (expression instanceof ConstantExpression constant && enumType.isResolved()
232+
&& constant.getValue() instanceof Enum<?> e) {
233+
Class<?> declaring = e.getDeclaringClass();
234+
Class<?> enumClass = enumType.getTypeClass();
235+
if (declaring == enumClass || enumClass.isAssignableFrom(declaring)) {
236+
return e.name();
237+
}
238+
}
239+
return null;
240+
}
241+
}

src/main/java/org/codehaus/groovy/ast/expr/SwitchExpression.java

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
*/
1919
package org.codehaus.groovy.ast.expr;
2020

21-
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
2221
import org.codehaus.groovy.ast.GroovyCodeVisitor;
2322
import org.codehaus.groovy.ast.stmt.CaseStatement;
2423
import org.codehaus.groovy.ast.stmt.EmptyStatement;
@@ -39,10 +38,8 @@
3938
* <p>
4039
* Arms stay as {@link CaseStatement}s, the same way a
4140
* {@link ClosureExpression} holds a statement body: the case label is an
42-
* expression, the arm is a statement. A plain {@link ExpressionTransformer}
43-
* rewrites only the selector and case labels. A
44-
* {@link ClassCodeExpressionTransformer} walks the tree in place via
45-
* {@link ClassCodeExpressionTransformer#visitSwitchExpression}.
41+
* expression, the arm is a statement. {@link #transformExpression} rewrites
42+
* the selector and case labels only and shares the arm statements.
4643
*
4744
* @see SwitchStatement
4845
* @see YieldStatement
@@ -153,18 +150,12 @@ public String toString() {
153150
}
154151

155152
/**
156-
* A {@link ClassCodeExpressionTransformer} walks this node in place through
157-
* {@link ClassCodeExpressionTransformer#visitSwitchExpression}, the same
158-
* pattern {@link ClosureExpression} uses so resolve, static-import and
159-
* similar rewrites still see nested arm expressions. Any other transformer
160-
* gets a copy of the selector and case labels; arm statements are shared.
153+
* Returns a copy whose selector and case labels have been rewritten.
154+
* Arm statements are shared: an {@link ExpressionTransformer} does not
155+
* walk statements.
161156
*/
162157
@Override
163158
public Expression transformExpression(final ExpressionTransformer transformer) {
164-
if (transformer instanceof ClassCodeExpressionTransformer visitor) {
165-
visitor.visitSwitchExpression(this);
166-
return this;
167-
}
168159
List<CaseStatement> transformedCases = new ArrayList<>(caseStatements.size());
169160
for (CaseStatement caseStatement : caseStatements) {
170161
CaseStatement copy = new CaseStatement(

0 commit comments

Comments
 (0)