From 6b7f2848dc8981314881084bbb7d6c2569df8e83 Mon Sep 17 00:00:00 2001 From: Stephan Herrmann Date: Thu, 29 Aug 2024 17:44:29 +0200 Subject: [PATCH] [23] JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview) (#2866) + implement code generation for switch on primitive/boxed + new error for illegal constant type in switch on newly allowed type + systematic test combinatorics of primitives in instanceof / switch See https://github.com/eclipse-jdt/eclipse.jdt.core/pull/2866 for details. --- .../eclipse/jdt/core/compiler/IProblem.java | 7 + .../jdt/internal/compiler/ClassFile.java | 89 +- .../internal/compiler/ast/CaseStatement.java | 20 + .../compiler/ast/LambdaExpression.java | 2 +- .../compiler/ast/ReferenceExpression.java | 2 +- .../compiler/ast/SwitchStatement.java | 83 +- .../internal/compiler/codegen/CodeStream.java | 15 +- .../compiler/codegen/ConstantPool.java | 1 + .../codegen/TypeAnnotationCodeStream.java | 4 +- .../compiler/impl/BooleanConstant.java | 12 + .../compiler/lookup/TypeConstants.java | 4 + .../compiler/problem/ProblemReporter.java | 8 + .../compiler/problem/messages.properties | 3 + .../regression/AbstractRegressionTest.java | 5 +- .../regression/CompilerInvocationTests.java | 2 + .../compiler/regression/ConstantTest.java | 7 +- .../regression/PrimitiveInPatternsTestSH.java | 1621 +++++++++++++++++ .../tests/compiler/regression/TestAll.java | 1 + 18 files changed, 1846 insertions(+), 40 deletions(-) create mode 100644 org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/PrimitiveInPatternsTestSH.java diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/core/compiler/IProblem.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/core/compiler/IProblem.java index 4e6a6ca33b1..9b73b9d0472 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/core/compiler/IProblem.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/core/compiler/IProblem.java @@ -2683,4 +2683,11 @@ public interface IProblem { * @noreference preview feature */ int ConstructorCallNotAllowedHere = PreviewRelated + 2031; + + /** + * @since 3.39 + * @noreference preview feature + */ + int WrongCaseType = PreviewRelated + 2100; + } diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ClassFile.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ClassFile.java index c4212907909..234007215ea 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ClassFile.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ClassFile.java @@ -83,6 +83,7 @@ import org.eclipse.jdt.internal.compiler.ast.SingleNameReference; import org.eclipse.jdt.internal.compiler.ast.StringLiteral; import org.eclipse.jdt.internal.compiler.ast.SwitchStatement; +import org.eclipse.jdt.internal.compiler.ast.SwitchStatement.SingletonBootstrap; import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration; import org.eclipse.jdt.internal.compiler.ast.TypeParameter; import org.eclipse.jdt.internal.compiler.ast.TypeReference; @@ -100,6 +101,7 @@ import org.eclipse.jdt.internal.compiler.codegen.StackMapFrameCodeStream.ExceptionMarker; import org.eclipse.jdt.internal.compiler.codegen.TypeAnnotationCodeStream; import org.eclipse.jdt.internal.compiler.codegen.VerificationTypeInfo; +import org.eclipse.jdt.internal.compiler.impl.BooleanConstant; import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; import org.eclipse.jdt.internal.compiler.impl.Constant; import org.eclipse.jdt.internal.compiler.impl.StringConstant; @@ -205,8 +207,12 @@ public class ClassFile implements TypeConstants, TypeIds { public static final String ENUMDESC_OF = "EnumDesc.of"; //$NON-NLS-1$ public static final String CLASSDESC = "ClassDesc"; //$NON-NLS-1$ public static final String CLASSDESC_OF = "ClassDesc.of"; //$NON-NLS-1$ + public static final String CONSTANT_BOOTSTRAP__GET_STATIC_FINAL = "ConstantBootStraps.getStaticFinal"; //$NON-NLS-1$ + public static final String CONSTANT_BOOTSTRAP__PRIMITIVE_CLASS = "ConstantBootStraps.primitiveClass"; //$NON-NLS-1$ public static final String[] BOOTSTRAP_METHODS = { ALTMETAFACTORY_STRING, METAFACTORY_STRING, BOOTSTRAP_STRING, - TYPESWITCH_STRING, ENUMSWITCH_STRING, CONCAT_CONSTANTS, INVOKE_STRING, ENUMDESC_OF, CLASSDESC, CLASSDESC_OF}; + TYPESWITCH_STRING, ENUMSWITCH_STRING, CONCAT_CONSTANTS, INVOKE_STRING, ENUMDESC_OF, CLASSDESC, CLASSDESC_OF, + CONSTANT_BOOTSTRAP__GET_STATIC_FINAL, CONSTANT_BOOTSTRAP__PRIMITIVE_CLASS }; + /** * INTERNAL USE-ONLY * Request the creation of a ClassFile compatible representation of a problematic type @@ -3675,6 +3681,8 @@ private int generateBootstrapMethods(List bootStrapMethodsList) { localContentsOffset = addBootStrapTypeCaseConstantEntry(localContentsOffset, (ResolvedCase) o, fPtr); } else if (o instanceof TypeBinding) { localContentsOffset = addClassDescBootstrap(localContentsOffset, (TypeBinding) o, fPtr); + } else if (o instanceof SingletonBootstrap sb) { + localContentsOffset = addSingletonBootstrap(localContentsOffset, sb, fPtr); } } @@ -3959,6 +3967,32 @@ private int addClassDescBootstrap(int localContentsOffset, TypeBinding type, Map return localContentsOffset; } + + private int addSingletonBootstrap(int localContentsOffset, SingletonBootstrap sb, Map fPtr) { + final int contentsEntries = 4; + int idx = fPtr.get(sb.id()); + if (contentsEntries + localContentsOffset >= this.contents.length) { + resizeContents(contentsEntries); + } + if (idx == 0) { + idx = this.constantPool.literalIndexForMethodHandle( + ClassFileConstants.MethodHandleRefKindInvokeStatic, + this.referenceBinding.scope.getJavaLangInvokeConstantBootstraps(), + sb.selector(), + sb.signature(), + false); + fPtr.put(sb.id(), idx); + } + this.contents[localContentsOffset++] = (byte) (idx >> 8); + this.contents[localContentsOffset++] = (byte) idx; + + // u2 num_bootstrap_arguments + this.contents[localContentsOffset++] = 0; + this.contents[localContentsOffset++] = (byte) 0; + + return localContentsOffset; + } + private int addBootStrapTypeSwitchEntry(int localContentsOffset, SwitchStatement switchStatement, Map fPtr) { CaseStatement.ResolvedCase[] constants = switchStatement.otherConstants; int numArgs = constants.length; @@ -3984,10 +4018,18 @@ private int addBootStrapTypeSwitchEntry(int localContentsOffset, SwitchStatement localContentsOffset += 2; for (CaseStatement.ResolvedCase c : constants) { if (c.isPattern()) { - char[] typeName = c.t.constantPoolName(); - int typeIndex = this.constantPool.literalIndexForType(typeName); - this.contents[localContentsOffset++] = (byte) (typeIndex >> 8); - this.contents[localContentsOffset++] = (byte) typeIndex; + int typeOrDynIndex; + if ((switchStatement.switchBits & SwitchStatement.Primitive) != 0) { + // Dynamic for Class.getPrimitiveClass(Z) or such + typeOrDynIndex = this.constantPool.literalIndexForDynamic(c.primitivesBootstrapIdx, + c.t.signature(), + ConstantPool.JavaLangClassSignature); + } else { + char[] typeName = c.t.constantPoolName(); + typeOrDynIndex = this.constantPool.literalIndexForType(typeName); + } + this.contents[localContentsOffset++] = (byte) (typeOrDynIndex >> 8); + this.contents[localContentsOffset++] = (byte) typeOrDynIndex; } else if (c.isQualifiedEnum()){ int typeIndex = this.constantPool.literalIndexForDynamic(c.enumDescIdx, ConstantPool.INVOKE_METHOD_METHOD_NAME, @@ -4001,10 +4043,24 @@ private int addBootStrapTypeSwitchEntry(int localContentsOffset, SwitchStatement this.contents[localContentsOffset++] = (byte) intValIdx; } else { if (c.e instanceof NullLiteral) continue; - int intValIdx = + int valIdx = switch (c.t.id) { + case TypeIds.T_boolean -> // Dynamic for Boolean.getStaticFinal(TRUE|FALSE) : + this.constantPool.literalIndexForDynamic(c.primitivesBootstrapIdx, + c.c.booleanValue() ? BooleanConstant.TRUE_STRING : BooleanConstant.FALSE_STRING, + ConstantPool.JavaLangBooleanSignature); + case TypeIds.T_byte, TypeIds.T_char, TypeIds.T_short, TypeIds.T_int -> this.constantPool.literalIndex(c.intValue()); - this.contents[localContentsOffset++] = (byte) (intValIdx >> 8); - this.contents[localContentsOffset++] = (byte) intValIdx; + case TypeIds.T_long -> + this.constantPool.literalIndex(c.c.longValue()); + case TypeIds.T_float -> + this.constantPool.literalIndex(c.c.floatValue()); + case TypeIds.T_double -> + this.constantPool.literalIndex(c.c.doubleValue()); + default -> + throw new IllegalArgumentException("Switch has unexpected type: "+switchStatement); //$NON-NLS-1$ + }; + this.contents[localContentsOffset++] = (byte) (valIdx >> 8); + this.contents[localContentsOffset++] = (byte) valIdx; } } @@ -6398,6 +6454,23 @@ public int recordBootstrapMethod(TypeBinding type) { this.bootstrapMethods.add(type); return this.bootstrapMethods.size() - 1; } + /** + * Record a singleton bootstrap method for the given token. + * @param descriptor represents the method to be bootstrapped + * @return the bootstrap index + */ + public int recordSingletonBootstrapMethod(SingletonBootstrap descriptor) { + if (this.bootstrapMethods == null) { + this.bootstrapMethods = new ArrayList<>(); + } else { + int idx = this.bootstrapMethods.indexOf(descriptor); + if (idx != -1) { + return idx; + } + } + this.bootstrapMethods.add(descriptor); + return this.bootstrapMethods.size() - 1; + } public int recordBootstrapMethod(String expression) { if (this.bootstrapMethods == null) { this.bootstrapMethods = new ArrayList<>(); diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/CaseStatement.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/CaseStatement.java index a5d650f675f..7b33fd14412 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/CaseStatement.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/CaseStatement.java @@ -137,6 +137,7 @@ public static class ResolvedCase { private final boolean isQualifiedEnum; public int enumDescIdx; public int classDescIdx; + public int primitivesBootstrapIdx; // index for a bootstrap method to args to indy typeSwitch for primitives ResolvedCase(Constant c, Expression e, TypeBinding t, int index, boolean isQualifiedEnum) { this.c = c; this.e = e; @@ -234,6 +235,25 @@ public ResolvedCase[] resolveCase(BlockScope scope, TypeBinding switchExpression } } } else { + // check from ยง14.11.1 (JEP 455): + // For each case constant associated with the switch block that is a constant expression, one of the following is true: + // - [...] + // - if T is one of long, float, double, or boolean, the type of the case constant is T. + // - if T is one of Long, Float, Double, or Boolean, the type of the case constant is, respectively, long, float, double, or boolean. + TypeBinding expectedCaseType = switchExpressionType; + if (switchExpressionType.isBoxedPrimitiveType()) { + expectedCaseType = scope.environment().computeBoxingType(switchExpressionType); // in this case it's actually 'computeUnboxingType()' + } + switch (expectedCaseType.id) { + case TypeIds.T_long, TypeIds.T_float, TypeIds.T_double, TypeIds.T_boolean -> { + if (caseType.id != expectedCaseType.id) { + scope.problemReporter().caseExpressionWrongType(e, switchExpressionType, expectedCaseType); + continue; + } + switchExpressionType = expectedCaseType; + } + } + // Constant con = resolveConstantExpression(scope, caseType, switchExpressionType, switchStatement, e, cases); if (con != Constant.NotAConstant) { int index = this == switchStatement.nullCase && e instanceof NullLiteral ? diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/LambdaExpression.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/LambdaExpression.java index c620e39f118..eea43c61b13 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/LambdaExpression.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/LambdaExpression.java @@ -240,7 +240,7 @@ public void generateCode(BlockScope currentScope, CodeStream codeStream, boolean } int invokeDynamicNumber = codeStream.classFile.recordBootstrapMethod(this); codeStream.invokeDynamic(invokeDynamicNumber, (this.shouldCaptureInstance ? 1 : 0) + this.outerLocalVariablesSlotSize, 1, this.descriptor.selector, signature.toString().toCharArray(), - this.resolvedType.id, this.resolvedType); + this.resolvedType); if (!valueRequired) codeStream.pop(); codeStream.recordPositionsFrom(pc, this.sourceStart); diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/ReferenceExpression.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/ReferenceExpression.java index 65c09475f90..ee73869bd2a 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/ReferenceExpression.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/ReferenceExpression.java @@ -433,7 +433,7 @@ public void generateCode(BlockScope currentScope, CodeStream codeStream, boolean int invokeDynamicNumber = codeStream.classFile.recordBootstrapMethod(this); codeStream.invokeDynamic(invokeDynamicNumber, argumentsSize, 1, this.descriptor.selector, buffer.toString().toCharArray(), this.isConstructorReference(), (this.lhs instanceof TypeReference? (TypeReference) this.lhs : null), this.typeArguments, - this.resolvedType.id, this.resolvedType); + this.resolvedType); if (!valueRequired) codeStream.pop(); codeStream.recordPositionsFrom(pc, this.sourceStart); diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/SwitchStatement.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/SwitchStatement.java index bd57ce20c6c..570cdcbfc50 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/SwitchStatement.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/ast/SwitchStatement.java @@ -8,6 +8,10 @@ * * SPDX-License-Identifier: EPL-2.0 * + * This is an implementation of an early-draft specification developed under the Java + * Community Process (JCP) and is made available for testing and evaluation purposes + * only. The code is not compatible with any specification of the JCP. + * * Contributors: * IBM Corporation - initial API and implementation * Stephan Herrmann - Contributions for @@ -19,11 +23,17 @@ *******************************************************************************/ package org.eclipse.jdt.internal.compiler.ast; +import static org.eclipse.jdt.internal.compiler.ClassFile.CONSTANT_BOOTSTRAP__PRIMITIVE_CLASS; +import static org.eclipse.jdt.internal.compiler.ClassFile.CONSTANT_BOOTSTRAP__GET_STATIC_FINAL; + +import java.lang.invoke.ConstantBootstraps; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Function; import java.util.function.IntPredicate; + +import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.internal.compiler.ASTVisitor; import org.eclipse.jdt.internal.compiler.ast.CaseStatement.ResolvedCase; import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; @@ -53,6 +63,15 @@ @SuppressWarnings("rawtypes") public class SwitchStatement extends Expression { + /** Descriptor for a bootstrap method that is created only once but can be used more than once. */ + public static record SingletonBootstrap(String id, char[] selector, char[] signature) { } + /** represents {@link ConstantBootstraps#primitiveClass(java.lang.invoke.MethodHandles.Lookup, String, Class)}*/ + public static final SingletonBootstrap PRIMITIVE_CLASS__BOOTSTRAP = new SingletonBootstrap( + CONSTANT_BOOTSTRAP__PRIMITIVE_CLASS, PRIMITIVE_CLASS, PRIMITIVE_CLASS__SIGNATURE); + /** represents {@link ConstantBootstraps#getStaticFinal(java.lang.invoke.MethodHandles.Lookup, String, Class)}*/ + public static final SingletonBootstrap GET_STATIC_FINAL__BOOTSTRAP = new SingletonBootstrap( + CONSTANT_BOOTSTRAP__GET_STATIC_FINAL, GET_STATIC_FINAL, GET_STATIC_FINAL__SIGNATURE); + public Expression expression; public Statement[] statements; public BlockScope scope; @@ -88,6 +107,7 @@ public class SwitchStatement extends Expression { public final static int TotalPattern = ASTNode.Bit3; public final static int Exhaustive = ASTNode.Bit4; public final static int QualifiedEnum = ASTNode.Bit5; + public final static int Primitive = ASTNode.Bit6; // for switch on strings private static final char[] SecretStringVariableName = " switchDispatchString".toCharArray(); //$NON-NLS-1$ @@ -789,6 +809,9 @@ public void generateCode(BlockScope currentScope, CodeStream codeStream) { valueRequired = this.expression.constant == Constant.NotAConstant || hasCases; // generate expression this.expression.generateCode(currentScope, codeStream, valueRequired); + if (resolvedType1.id == TypeIds.T_JavaLangBoolean) { + codeStream.generateUnboxingConversion(TypeIds.T_boolean); // optimize by avoiding indy typeSwitch + } } // generate the appropriate switch table/lookup bytecode if (hasCases) { @@ -954,7 +977,7 @@ private void generateCodeSwitchPatternEpilogue(CodeStream codeStream) { private void generateCodeSwitchPatternPrologue(BlockScope currentScope, CodeStream codeStream) { this.expression.generateCode(currentScope, codeStream, true); - if ((this.switchBits & NullCase) == 0) { + if ((this.switchBits & NullCase) == 0 && (this.switchBits & Primitive) == 0) { codeStream.dup(); codeStream.invokeJavaUtilObjectsrequireNonNull(); codeStream.pop(); @@ -984,6 +1007,18 @@ private void generateCodeSwitchPatternPrologue(BlockScope currentScope, CodeStre if (hasQualifiedEnums) { c.index = i; } + if ((this.switchBits & Primitive) != 0) { + SingletonBootstrap descriptor = null; + if (c.isPattern()) { + descriptor = PRIMITIVE_CLASS__BOOTSTRAP; + } else if (c.t.id == TypeIds.T_boolean) { + descriptor = GET_STATIC_FINAL__BOOTSTRAP; + } + if (descriptor != null) { + c.primitivesBootstrapIdx = codeStream.classFile.recordSingletonBootstrapMethod(descriptor); + } + continue; + } if (!c.isQualifiedEnum()) continue; int classdescIdx = codeStream.classFile.recordBootstrapMethod(c.t); @@ -993,14 +1028,27 @@ private void generateCodeSwitchPatternPrologue(BlockScope currentScope, CodeStre } } private void generateTypeSwitchPatternPrologue(CodeStream codeStream, int invokeDynamicNumber) { + TypeBinding exprType = this.expression.resolvedType; + char[] signature =typeSwitchSignature(exprType); + int argsSize = TypeIds.getCategory(exprType.id) + 1; // Object | PRIM, restartIndex (PRIM = Z|S|I..) codeStream.invokeDynamic(invokeDynamicNumber, - 2, // Object, restartIndex + argsSize, 1, // int ConstantPool.TYPESWITCH, - "(Ljava/lang/Object;I)I".toCharArray(), //$NON-NLS-1$ - TypeIds.T_int, + signature, TypeBinding.INT); } + char[] typeSwitchSignature(TypeBinding exprType) { + char[] arg1 = switch (exprType.id) { + case TypeIds.T_JavaLangLong, TypeIds.T_JavaLangFloat, TypeIds.T_JavaLangDouble, TypeIds.T_JavaLangBoolean -> + exprType.signature(); + default -> + (this.switchBits & Primitive) != 0 + ? exprType.signature() + : "Ljava/lang/Object;".toCharArray(); //$NON-NLS-1$ + }; + return CharOperation.concat("(".toCharArray(), arg1, "I)I".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ + } private void generateEnumSwitchPatternPrologue(CodeStream codeStream, int invokeDynamicNumber) { String genericTypeSignature = new String(this.expression.resolvedType.genericTypeSignature()); String callingParams = "(" + genericTypeSignature + "I)I"; //$NON-NLS-1$ //$NON-NLS-2$ @@ -1009,7 +1057,6 @@ private void generateEnumSwitchPatternPrologue(CodeStream codeStream, int invoke 1, // int "enumSwitch".toCharArray(), //$NON-NLS-1$ callingParams.toCharArray(), - TypeIds.T_int, TypeBinding.INT); } protected void statementGenerateCode(BlockScope currentScope, CodeStream codeStream, Statement statement) { @@ -1089,6 +1136,9 @@ public void resolve(BlockScope upperScope) { expressionType = null; // fault-tolerance: ignore type mismatch from constants from hereon break checkType; } else if (expressionType.isBaseType()) { + if (JavaFeature.PRIMITIVES_IN_PATTERNS.isSupported(compilerOptions)) { + this.switchBits |= Primitive; + } if (this.expression.isConstantValueOfTypeAssignableToType(expressionType, TypeBinding.INT)) break checkType; if (expressionType.isCompatibleWith(TypeBinding.INT)) @@ -1112,8 +1162,10 @@ public void resolve(BlockScope upperScope) { break checkType; } if (!JavaFeature.PATTERN_MATCHING_IN_SWITCH.isSupported(compilerOptions) || (expressionType.isBaseType() && expressionType.id != T_null && expressionType.id != T_void)) { - upperScope.problemReporter().incorrectSwitchType(this.expression, expressionType); - expressionType = null; // fault-tolerance: ignore type mismatch from constants from hereon + if ((this.switchBits & Primitive) == 0) { // when Primitive is set it is approved above + upperScope.problemReporter().incorrectSwitchType(this.expression, expressionType); + expressionType = null; // fault-tolerance: ignore type mismatch from constants from hereon + } } else { this.isNonTraditional = true; } @@ -1168,13 +1220,17 @@ public void resolve(BlockScope upperScope) { } for (int j = 0; j < counter; j++) { IntPredicate check = idx -> { - Constant c2 = this.otherConstants[idx].c; + ResolvedCase otherResolvedCase = this.otherConstants[idx]; + Constant c2 = otherResolvedCase.c; if (con.typeID() == TypeIds.T_JavaLangString) { return c2.stringValue().equals(con.stringValue()); } else { if (c2.typeID() == TypeIds.T_JavaLangString) return false; - if (con.intValue() == c2.intValue()) + int id = c.t.id, otherId = otherResolvedCase.t.id; + if (id == TypeIds.T_null || otherId == TypeIds.T_null) + return id == otherId; // 'null' shares IntConstant(-1) + if (con.equals(c2)) return true; return this.constants[idx] == c1; } @@ -1204,9 +1260,7 @@ public void resolve(BlockScope upperScope) { } else { if (!c.isPattern() && check.test(j)) { if (this.isNonTraditional) { - if (c.e instanceof NullLiteral && this.otherConstants[j].e instanceof NullLiteral) { - reportDuplicateCase(c.e, this.otherConstants[j].e, length); - } + reportDuplicateCase(c.e, this.otherConstants[j].e, length); } else { reportDuplicateCase(caseStmt, this.cases[caseIndex[j]], length); } @@ -1436,6 +1490,11 @@ private boolean needPatternDispatchCopy() { TypeBinding eType = this.expression != null ? this.expression.resolvedType : null; if (eType == null) return false; + switch (eType.id) { + case TypeIds.T_JavaLangLong, TypeIds.T_JavaLangFloat, TypeIds.T_JavaLangDouble: + return true; + // note: if no patterns are present we optimize Boolean to use unboxing rather than indy typeSwitch + } return !(eType.isPrimitiveOrBoxedPrimitiveType() || eType.isEnum() || eType.id == TypeIds.T_JavaLangString); // classic selectors } private void addSecretPatternSwitchVariables(BlockScope upperScope) { diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/CodeStream.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/CodeStream.java index 1bcf9f30115..734a630d208 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/CodeStream.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/CodeStream.java @@ -2524,7 +2524,6 @@ public void invokeDynamicForStringConcat(StringBuilder recipe, List 1, // Ljava/lang/String; ConstantPool.ConcatWithConstants, signature.toString().toCharArray(), - TypeIds.T_JavaLangObject, getPopularBinding(ConstantPool.JavaLangStringConstantPoolName)); } /** @@ -2951,7 +2950,7 @@ public void generateSyntheticBodyForDeserializeLambda(SyntheticMethodBinding met } // Example: invokeDynamic(0, 0, 1, "m".toCharArray(), "()Lcom/foo/X$Foo;".toCharArray()); invokeDynamic(funcEx.bootstrapMethodNumber, index, 1, funcEx.descriptor.selector, - sig.toString().toCharArray(), funcEx.resolvedType.id, funcEx.resolvedType); + sig.toString().toCharArray(), funcEx.resolvedType); areturn(); if (j < count - 1) { nextOne.place(); @@ -3385,7 +3384,7 @@ public void generateSyntheticBodyForRecordEquals(SyntheticMethodBinding methodBi String sig = new String(methodBinding.signature()); sig = sig.substring(0, 1)+ new String(methodBinding.declaringClass.signature()) + sig.substring(1); invokeDynamic(index, methodBinding.parameters.length, 1, methodBinding.selector, sig.toCharArray(), - TypeIds.T_boolean, TypeBinding.BOOLEAN); + TypeBinding.BOOLEAN); ireturn(); } public void generateSyntheticBodyForRecordHashCode(SyntheticMethodBinding methodBinding, int index) { @@ -3393,7 +3392,7 @@ public void generateSyntheticBodyForRecordHashCode(SyntheticMethodBinding method String sig = new String(methodBinding.signature()); sig = sig.substring(0, 1)+ new String(methodBinding.declaringClass.signature()) + sig.substring(1); invokeDynamic(index, methodBinding.parameters.length, 1, methodBinding.selector, sig.toCharArray(), - TypeIds.T_int, TypeBinding.INT); + TypeBinding.INT); ireturn(); } public void generateSyntheticBodyForRecordToString(SyntheticMethodBinding methodBinding, int index) { @@ -3401,7 +3400,7 @@ public void generateSyntheticBodyForRecordToString(SyntheticMethodBinding method String sig = new String(methodBinding.signature()); sig = sig.substring(0, 1)+ new String(methodBinding.declaringClass.signature()) + sig.substring(1); invokeDynamic(index, methodBinding.parameters.length, 1, methodBinding.selector, sig.toCharArray(), - TypeIds.T_JavaLangObject, getPopularBinding(ConstantPool.JavaLangStringConstantPoolName)); + getPopularBinding(ConstantPool.JavaLangStringConstantPoolName)); areturn(); } @@ -4549,12 +4548,12 @@ private void invoke18(byte opcode, int receiverAndArgsSize, int returnTypeSize, } public void invokeDynamic(int bootStrapIndex, int argsSize, int returnTypeSize, char[] selector, char[] signature, - int typeId, TypeBinding type) { - this.invokeDynamic(bootStrapIndex, argsSize, returnTypeSize, selector, signature, false, null, null, typeId, type); + TypeBinding type) { + this.invokeDynamic(bootStrapIndex, argsSize, returnTypeSize, selector, signature, false, null, null, type); } public void invokeDynamic(int bootStrapIndex, int argsSize, int returnTypeSize, char[] selector, char[] signature, boolean isConstructorReference, TypeReference lhsTypeReference, TypeReference [] typeArguments, - int typeId, TypeBinding type) { + TypeBinding type) { if (this.classFileOffset + 4 >= this.bCodeStream.length) { resizeByteArray(); } diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/ConstantPool.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/ConstantPool.java index dc45f2d4e60..a60cfbc2349 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/ConstantPool.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/ConstantPool.java @@ -163,6 +163,7 @@ public class ConstantPool implements ClassFileConstants, TypeIds { public static final char[] JavaIoPrintStreamSignature = "Ljava/io/PrintStream;".toCharArray(); //$NON-NLS-1$ public static final char[] JavaLangAssertionErrorConstantPoolName = "java/lang/AssertionError".toCharArray(); //$NON-NLS-1$ public static final char[] JavaLangBooleanConstantPoolName = "java/lang/Boolean".toCharArray(); //$NON-NLS-1$ + public static final char[] JavaLangBooleanSignature = "Ljava/lang/Boolean;".toCharArray(); //$NON-NLS-1$ public static final char[] JavaLangByteConstantPoolName = "java/lang/Byte".toCharArray(); //$NON-NLS-1$ public static final char[] JavaLangCharacterConstantPoolName = "java/lang/Character".toCharArray(); //$NON-NLS-1$ public static final char[] JavaLangClassConstantPoolName = "java/lang/Class".toCharArray(); //$NON-NLS-1$ diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/TypeAnnotationCodeStream.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/TypeAnnotationCodeStream.java index 30722b013b5..2a8879a07d8 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/TypeAnnotationCodeStream.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/TypeAnnotationCodeStream.java @@ -135,7 +135,7 @@ public void invoke(byte opcode, MethodBinding methodBinding, TypeBinding declari @Override public void invokeDynamic(int bootStrapIndex, int argsSize, int returnTypeSize, char[] selector, char[] signature, boolean isConstructorReference, TypeReference lhsTypeReference, TypeReference [] typeArguments, - int typeId, TypeBinding type) { + TypeBinding type) { if (lhsTypeReference != null && (lhsTypeReference.bits & ASTNode.HasTypeAnnotations) != 0) { if (isConstructorReference) { addAnnotationContext(lhsTypeReference, this.position, 0, AnnotationTargetTypeConstants.CONSTRUCTOR_REFERENCE); @@ -155,7 +155,7 @@ public void invokeDynamic(int bootStrapIndex, int argsSize, int returnTypeSize, } } } - super.invokeDynamic(bootStrapIndex, argsSize, returnTypeSize, selector, signature, isConstructorReference, lhsTypeReference, typeArguments, typeId, type); + super.invokeDynamic(bootStrapIndex, argsSize, returnTypeSize, selector, signature, isConstructorReference, lhsTypeReference, typeArguments, type); } @Override diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/impl/BooleanConstant.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/impl/BooleanConstant.java index ff18abec9a1..1a6da485436 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/impl/BooleanConstant.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/impl/BooleanConstant.java @@ -8,6 +8,10 @@ * * SPDX-License-Identifier: EPL-2.0 * + * This is an implementation of an early-draft specification developed under the Java + * Community Process (JCP) and is made available for testing and evaluation purposes + * only. The code is not compatible with any specification of the JCP. + * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ @@ -15,6 +19,9 @@ public class BooleanConstant extends Constant { + public static final char[] TRUE_STRING = "TRUE".toCharArray(); //$NON-NLS-1$ + public static final char[] FALSE_STRING = "FALSE".toCharArray(); //$NON-NLS-1$ + private final boolean value; private static final BooleanConstant TRUE = new BooleanConstant(true); @@ -33,6 +40,11 @@ public boolean booleanValue() { return this.value; } + @Override + public int intValue() { + return this.value ? 1 : 0; + } + @Override public String stringValue() { // spec 15.17.11 diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/lookup/TypeConstants.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/lookup/TypeConstants.java index ef341d219c1..0656d91fefc 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/lookup/TypeConstants.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/lookup/TypeConstants.java @@ -263,6 +263,10 @@ public interface TypeConstants { char[][] JAVA_LANG_RUNTIME_OBJECTMETHODS = {JAVA, LANG, RUNTIME, "ObjectMethods".toCharArray()}; //$NON-NLS-1$ char[][] JAVA_LANG_RUNTIME_SWITCHBOOTSTRAPS = {JAVA, LANG, RUNTIME, "SwitchBootstraps".toCharArray()}; //$NON-NLS-1$ char[][] JAVA_LANG_INVOKE_CONSTANTBOOTSTRAP = {JAVA, LANG, INVOKE, "ConstantBootstraps".toCharArray()}; //$NON-NLS-1$ + char[] PRIMITIVE_CLASS = "primitiveClass".toCharArray(); //$NON-NLS-1$ + char[] PRIMITIVE_CLASS__SIGNATURE = "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Class;".toCharArray(); //$NON-NLS-1$ + char[] GET_STATIC_FINAL = "getStaticFinal".toCharArray(); //$NON-NLS-1$ + char[] GET_STATIC_FINAL__SIGNATURE = "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;".toCharArray(); //$NON-NLS-1$ char[][] JAVA_LANG_ENUM_ENUMDESC = {JAVA, LANG, "Enum$EnumDesc".toCharArray()}; //$NON-NLS-1$ char[][] JAVA_LANG_CONSTANT_CLASSDESC = {JAVA, LANG, "constant".toCharArray(), "ClassDesc".toCharArray()}; //$NON-NLS-1$ //$NON-NLS-2$ diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/problem/ProblemReporter.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/problem/ProblemReporter.java index eedf2bf2c4c..9763d988761 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/problem/ProblemReporter.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/problem/ProblemReporter.java @@ -1660,6 +1660,14 @@ public void caseExpressionMustBeConstant(Expression expression) { expression.sourceStart, expression.sourceEnd); } +public void caseExpressionWrongType(Expression expression, TypeBinding switchBinding, TypeBinding selectorBinding) { + this.handle( + IProblem.WrongCaseType, + new String[] {String.valueOf(switchBinding.readableName()), String.valueOf(selectorBinding.readableName())}, + new String[] {String.valueOf(switchBinding.shortReadableName()), String.valueOf(selectorBinding.shortReadableName())}, + expression.sourceStart, + expression.sourceEnd); +} public void classExtendFinalClass(SourceTypeBinding type, TypeReference superclass, TypeBinding superTypeBinding) { String name = new String(type.sourceName()); String superTypeFullName = new String(superTypeBinding.readableName()); diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/problem/messages.properties b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/problem/messages.properties index 90bc9415780..2326802a998 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/problem/messages.properties +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/problem/messages.properties @@ -1193,6 +1193,9 @@ 2030 = Cannot assign field ''{0}'' in an early construction context, because it has an initializer 2031 = Constructor call is not allowed here +# JEP 455 Primitive Types in Patterns, instanceof, and switch (Preview) +2100 = Case constants in a switch on ''{0}'' must have type ''{1}'' + ### ELABORATIONS ## Access restrictions 78592 = The type ''{1}'' is not API (restriction on classpath entry ''{0}'') diff --git a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/AbstractRegressionTest.java b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/AbstractRegressionTest.java index 7f49369137d..614019875fd 100644 --- a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/AbstractRegressionTest.java +++ b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/AbstractRegressionTest.java @@ -585,8 +585,9 @@ static int minorFromRawVersion (String version, String rawVersion) { } } if (version == JavaCore.VERSION_23) { - if ("23-ea".equals(rawVersion)) { - return 0000; + switch(rawVersion) { + case "23-ea", "23": + return 0000; } } throw new RuntimeException("unknown raw javac version: " + rawVersion); diff --git a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/CompilerInvocationTests.java b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/CompilerInvocationTests.java index d3b1253de05..9882952c33b 100644 --- a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/CompilerInvocationTests.java +++ b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/CompilerInvocationTests.java @@ -1218,6 +1218,7 @@ class ProblemAttributes { expectedProblemAttributes.put("WildcardConstructorInvocation", new ProblemAttributes(CategorizedProblem.CAT_TYPE)); expectedProblemAttributes.put("WildcardFieldAssignment", new ProblemAttributes(CategorizedProblem.CAT_TYPE)); expectedProblemAttributes.put("WildcardMethodInvocation", new ProblemAttributes(CategorizedProblem.CAT_TYPE)); + expectedProblemAttributes.put("WrongCaseType", new ProblemAttributes(CategorizedProblem.CAT_PREVIEW_RELATED)); expectedProblemAttributes.put("illFormedParameterizationOfFunctionalInterface", new ProblemAttributes(CategorizedProblem.CAT_TYPE)); expectedProblemAttributes.put("lambdaParameterTypeMismatched", new ProblemAttributes(CategorizedProblem.CAT_TYPE)); expectedProblemAttributes.put("lambdaSignatureMismatched", new ProblemAttributes(CategorizedProblem.CAT_TYPE)); @@ -2333,6 +2334,7 @@ class ProblemAttributes { expectedProblemAttributes.put("WildcardConstructorInvocation", SKIP); expectedProblemAttributes.put("WildcardFieldAssignment", SKIP); expectedProblemAttributes.put("WildcardMethodInvocation", SKIP); + expectedProblemAttributes.put("WrongCaseType", SKIP); expectedProblemAttributes.put("illFormedParameterizationOfFunctionalInterface", SKIP); expectedProblemAttributes.put("lambdaParameterTypeMismatched", SKIP); expectedProblemAttributes.put("lambdaSignatureMismatched", SKIP); diff --git a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/ConstantTest.java b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/ConstantTest.java index 291c061ff57..4f31200a54e 100644 --- a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/ConstantTest.java +++ b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/ConstantTest.java @@ -1326,12 +1326,7 @@ private void verifyValues( // intValue() byteConstant.intValue(); charConstant.intValue(); - try { - booleanConstant.intValue(); - assertTrue(false); - } catch(ShouldNotImplement e) { - // ignore - } + booleanConstant.intValue(); doubleConstant.intValue(); floatConstant.intValue(); intConstant.intValue(); diff --git a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/PrimitiveInPatternsTestSH.java b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/PrimitiveInPatternsTestSH.java new file mode 100644 index 00000000000..7fc14e9801f --- /dev/null +++ b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/PrimitiveInPatternsTestSH.java @@ -0,0 +1,1621 @@ +/******************************************************************************* + * Copyright (c) 2024 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * This is an implementation of an early-draft specification developed under the Java + * Community Process (JCP) and is made available for testing and evaluation purposes + * only. The code is not compatible with any specification of the JCP. + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.core.tests.compiler.regression; + +import java.util.Map; + +import org.eclipse.jdt.internal.compiler.batch.FileSystem; +import org.eclipse.jdt.internal.compiler.env.INameEnvironment; +import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; + +import junit.framework.Test; + +public class PrimitiveInPatternsTestSH extends AbstractRegressionTest9 { + + private static final JavacTestOptions JAVAC_OPTIONS = new JavacTestOptions("--enable-preview -source 23 -Xlint:-preview"); + private static final String[] VMARGS = new String[] {"--enable-preview"}; + + private static final String[] PRIMITIVES = { "boolean", "byte", "char", "short", "int", "long", "float", "double" }; + private static final String[] BOXES = { "Boolean", "Byte", "Character", "Short", "Integer", "Long", "Float", "Double" }; + // note: Character.MAX_VALUE doesn't play well with stream handling around TestVerifier, so we avoid non-ascii chars during print(): + private static final String[] MAXVALUES = { "true", "Byte.MAX_VALUE", "'z'", "Short.MAX_VALUE", "Integer.MAX_VALUE", "Long.MAX_VALUE", "Float.MAX_VALUE", "Double.MAX_VALUE" }; + private static final String[] GOODVALUES = { "true", "49", "'1'", "49", "49", "49L", "49.0f", "49.0d" }; // 49 ~ '1' + private static final String[] NEGVALUES = { "false", "-1", "'-'", "-1", "-1", "-1L", "-1.0f", "-1.0d" }; + + // larger then MAX of previous type, still needs suffix added via toConstantOfType + private static final String[] CONSTANTS = { "true", "1", "'1'", "300", "40000", "5000000000", "6.0E20", "7.0E40" }; + private static final boolean[] IS_NUMERICAL = { false, true, false, true, true, true, true, true }; + private static String MAX_VALUES_STRING = "true|127|z|32767|2147483647|9223372036854775807|3.4028235E38|1.7976931348623157E308|"; + /** + * Test programs may use the following placeholders, which are filled in by this method: + *
    + *
  • PRIM a primitive type + *
  • BOX the corresponding boxing type + *
  • NEGVAL a value of that type signaling failure + *
  • VAL a regular value + *
+ * @param template the template with placeholders + * @param idx index into {@link #PRIMITIVES} etc. + * @return the program snippet with placeholders filled in. + */ + private static String fillIn(String template, int idx) { + return template.replaceAll("PRIM", PRIMITIVES[idx]).replaceAll("BOX", BOXES[idx]) + .replace("NEGVAL", NEGVALUES[idx]).replace("VAL", GOODVALUES[idx]).replace("MAX", MAXVALUES[idx]); + } + /** like {@link #fillIn(String, int)}, but may use {@link #MAXVALUES} if 'maxValue' is true. */ + private static String fillInMax(String template, int idx, boolean useMax) { + return template.replaceAll("PRIM", PRIMITIVES[idx]).replaceAll("BOX", BOXES[idx]) + .replace("NEGVAL", NEGVALUES[idx]).replace("VAL", useMax ? MAXVALUES[idx] : GOODVALUES[idx]); + } + + static String toConstantOfType(String constVal, String ptype) { + return switch (ptype) { + case "long" -> constVal+"L"; + case "float" -> constVal+"f"; + case "double" -> constVal+"d"; + default -> constVal; + }; + } + + + static { +// TESTS_NUMBERS = new int [] { 1 }; +// TESTS_RANGE = new int[] { 1, -1 }; +// TESTS_NAMES = new String[] { "testPrimitivePatternInSwitch" }; + } + private String extraLibPath; + public static Class testClass() { + return PrimitiveInPatternsTestSH.class; + } + public static Test suite() { + return buildMinimalComplianceTestSuite(testClass(), F_23); + } + public PrimitiveInPatternsTestSH(String testName) { + super(testName); + } + // Enables the tests to run individually + protected Map getCompilerOptions(boolean preview) { + Map defaultOptions = super.getCompilerOptions(); + defaultOptions.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_23); + defaultOptions.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_23); + defaultOptions.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_23); + defaultOptions.put(CompilerOptions.OPTION_EnablePreviews, preview ? CompilerOptions.ENABLED : CompilerOptions.DISABLED); + defaultOptions.put(CompilerOptions.OPTION_ReportPreviewFeatures, CompilerOptions.WARNING); + return defaultOptions; + } + + protected Map getCompilerOptions() { + return getCompilerOptions(false); + } + protected String[] getDefaultClassPaths() { + String[] libs = DefaultJavaRuntimeEnvironment.getDefaultClassPaths(); + if (this.extraLibPath != null) { + String[] l = new String[libs.length + 1]; + System.arraycopy(libs, 0, l, 0, libs.length); + l[libs.length] = this.extraLibPath; + return l; + } + return libs; + } + @Override + protected INameEnvironment getNameEnvironment(final String[] testFiles, String[] classPaths, Map options) { + this.classpaths = classPaths == null ? getDefaultClassPaths() : classPaths; + INameEnvironment[] classLibs = getClassLibs(false, options); + for (INameEnvironment nameEnvironment : classLibs) { + ((FileSystem) nameEnvironment).scanForModules(createParser()); + } + return new InMemoryNameEnvironment9(testFiles, this.moduleMap, classLibs); + } + @Override + protected void runConformTest(String[] testFiles, String expectedOutput) { + runConformTest(testFiles, expectedOutput, getCompilerOptions(true), VMARGS, JAVAC_OPTIONS); + } + @Override + protected void runConformTest(String[] testFiles, String expectedOutput, Map customOptions) { + if(!isJRE23Plus) + return; + runConformTest(testFiles, expectedOutput, customOptions, VMARGS, JAVAC_OPTIONS); + } + protected void runConformTest( + String[] testFiles, + String expectedOutputString, + String[] classLibraries, + boolean shouldFlushOutputDirectory, + String[] vmArguments) { + runTest( + // test directory preparation + shouldFlushOutputDirectory /* should flush output directory */, + testFiles /* test files */, + // compiler options + classLibraries /* class libraries */, + null /* no custom options */, + false /* do not perform statements recovery */, + null /* no custom requestor */, + // compiler results + false /* expecting no compiler errors */, + null /* do not check compiler log */, + // runtime options + false /* do not force execution */, + vmArguments /* vm arguments */, + // runtime results + expectedOutputString /* expected output string */, + null /* do not check error string */, + // javac options + JavacTestOptions.DEFAULT /* default javac test options */); + } + protected void runNegativeTest(String[] testFiles, String expectedCompilerLog) { + Map customOptions = getCompilerOptions(true); + Runner runner = new Runner(); + runner.testFiles = testFiles; + runner.expectedCompilerLog = expectedCompilerLog; + runner.javacTestOptions = JAVAC_OPTIONS; + runner.customOptions = customOptions; + runner.expectedJavacOutputString = null; + runner.runNegativeTest(); + } + protected void runNegativeTest( + String[] testFiles, + String expectedCompilerLog, + String javacLog, + String[] classLibraries, + boolean shouldFlushOutputDirectory, + Map customOptions) { + Runner runner = new Runner(); + runner.testFiles = testFiles; + runner.expectedCompilerLog = expectedCompilerLog; + runner.javacTestOptions = JAVAC_OPTIONS; + runner.customOptions = customOptions; + runner.expectedJavacOutputString = javacLog; + runner.runNegativeTest(); + } + + // https://cr.openjdk.org/~abimpoudis/instanceof/jep455-20240424/specs/instanceof-jls.html#jls-5.1.2 + // 5.7 Testing Contexts + // Identity Conversion + public void testIdentity() { + StringBuilder methods = new StringBuilder(); + StringBuilder calls = new StringBuilder(); + String methodTmpl = + """ + public static PRIM fooPRIM(PRIM v) { + if (v instanceof PRIM) { + return v; + } + return NEGVAL; + } + """; + String callTmpl = + """ + PRIM vPRIM = VAL; + System.out.print(X.fooPRIM(vPRIM)); + System.out.print('|'); + """; + // for all primitive types: + for (int i = 0; i < PRIMITIVES.length; i++) { + methods.append(fillIn(methodTmpl, i)); + calls.append(fillInMax(callTmpl, i, true)); + } + StringBuilder classX = new StringBuilder("public class X {\n"); + classX.append(methods.toString()); + classX.append("public static void main(String[] args) {\n"); + classX.append(calls); + classX.append("}}\n"); + runConformTest(new String[] { "X.java", classX.toString() }, MAX_VALUES_STRING); + } + public void testIdentityPattern() { + StringBuilder methods = new StringBuilder(); + StringBuilder calls = new StringBuilder(); + String methodTmpl = + """ + public static PRIM fooPRIM(PRIM v) { + if (v instanceof PRIM vv) { + return vv; + } + return NEGVAL; + } + """; + String callTmpl = + """ + PRIM vPRIM = VAL; + System.out.print(X.fooPRIM(vPRIM)); + System.out.print('|'); + """; + // for all primitive types: + for (int i = 0; i < PRIMITIVES.length; i++) { + methods.append(fillIn(methodTmpl, i)); + calls.append(fillInMax(callTmpl, i, true)); + } + StringBuilder classX = new StringBuilder("public class X {\n"); + classX.append(methods.toString()); + classX.append("public static void main(String[] args) {\n"); + classX.append(calls); + classX.append("}}\n"); + runConformTest(new String[] { "X.java", classX.toString() }, MAX_VALUES_STRING); + } + + public void testIdentity_functionLhs() { + // one sample should suffice here: + runConformTest(new String[] { + "X.java", + """ + public class X { + public static byte foo() { + if (bar() instanceof byte) { + byte b = (byte) bar(); + return b; + } + return -1; + } + public static byte bar() { + byte b = 1; + return b; + } + public static void main(String[] args) { + System.out.println(X.foo()); + } + } + """ + }, + "1"); + } + + public void testIdentityPattern_functionLhs() { + StringBuilder methods = new StringBuilder(); + StringBuilder calls = new StringBuilder(); + String methodTmpl = + """ + public static PRIM fooPRIM() { + if (barPRIM() instanceof PRIM vv) { + return vv; + } + return NEGVAL; + } + public static PRIM barPRIM() { + return VAL; + } + """; + String callTmpl = + """ + System.out.print(X.fooPRIM()); + System.out.print('|'); + """; + // for all primitive types: + for (int i = 0; i < PRIMITIVES.length; i++) { + methods.append(fillIn(methodTmpl, i)); + calls.append(fillIn(callTmpl, i)); + } + StringBuilder classX = new StringBuilder("public class X {\n"); + classX.append(methods.toString()); + classX.append("public static void main(String[] args) {\n"); + classX.append(calls); + classX.append("}}\n"); + runConformTest(new String[] { "X.java", classX.toString() }, + "true|49|1|49|49|49|49.0|49.0|"); + } + + // Widening primitive conversions + // 5.1.2: [...] exact widening primitive conversion [...]. Such a conversion can be one of the following: + // * from an integral type to another integral type + // * from byte, short, or char to a floating-point type + // * from int to double + // * from float to double + // inexact widening conversions: + // * from int to float, or from long to float, or from long to double + private void testWideningFrom(String from, int idx, boolean useMax, String expectedOut) { + assert from.equals(PRIMITIVES[idx]) : "mismatch between from and idx"; + // example (from="long", idx=5, useMax=false, ...): + // public class X { + // public static float foofloat(long v) { + // if (v instanceof float) { + // float vv = (float) v; + // return vv; + // } + // return -1.0f; + // } + // public static double foodouble(long v) { + // if (v instanceof double) { + // double vv = (double) v; + // return vv; + // } + // return -1.0d; + // } + // public static void main(String[] args) { + // long v = 49L; + // System.out.print(X.foofloat(v)); + // System.out.print('|'); + // System.out.print(X.foodouble(v)); + // System.out.print('|'); + // }} + + StringBuilder methods = new StringBuilder(); + StringBuilder calls = new StringBuilder(); + String methodTmpl = + """ + public static PRIM fooPRIM(FROM v) { + if (v instanceof PRIM) { + PRIM vv = (PRIM) v; + return vv; + } + return NEGVAL; + } + """; + String callTmpl = + """ + System.out.print(X.fooPRIM(v)); + System.out.print('|'); + """; + // for all numerical primitive types "greater" than 'from': + for (int i = idx+1; i < PRIMITIVES.length; i++) { + if (!IS_NUMERICAL[i]) continue; + methods.append(fillIn(methodTmpl.replace("FROM", from), i)); + calls.append(fillIn(callTmpl, i)); + } + StringBuilder classX = new StringBuilder("public class X {\n"); + classX.append(methods.toString()); + classX.append("public static void main(String[] args) {\n"); + classX.append(fillInMax("PRIM v = VAL;\n", idx, useMax)); + classX.append(calls); + classX.append("}}\n"); + runConformTest(new String[] { "X.java", classX.toString() }, expectedOut); + } + private void testWideningFrom_pattern(String from, int idx, boolean useMax, String expectedOut) { + assert from.equals(PRIMITIVES[idx]) : "mismatch between from and idx"; + // example (from="long", idx=5, useMax=false, ..): + // public class X { + // public static float foofloat() { + // if (bar() instanceof float vv) { + // return vv; + // } + // return -1.0f; + // } + // public static double foodouble() { + // if (bar() instanceof double vv) { + // return vv; + // } + // return -1.0d; + // } + // static long bar() { + // return 49L; + // } + // public static void main(String[] args) { + // System.out.print(X.foofloat()); + // System.out.print('|'); + // System.out.print(X.foodouble()); + // System.out.print('|'); + // } + // } + + StringBuilder methods = new StringBuilder(); + StringBuilder calls = new StringBuilder(); + String methodTmpl = + """ + public static PRIM fooPRIM() { + if (bar() instanceof PRIM vv) { + return vv; + } + return NEGVAL; + } + """; + String methodBar = fillInMax(""" + static PRIM bar() { + return VAL; + } + """, + idx, useMax); + String callTmpl = + """ + System.out.print(X.fooPRIM()); + System.out.print('|'); + """; + // for all numerical primitive types "greater" than 'from': + for (int i = idx+1; i < PRIMITIVES.length; i++) { + if (!IS_NUMERICAL[i]) continue; + methods.append(fillIn(methodTmpl, i)); + calls.append(fillIn(callTmpl, i)); + } + StringBuilder classX = new StringBuilder("public class X {\n"); + classX.append(methods.toString()); + classX.append(methodBar); + classX.append("public static void main(String[] args) {\n"); + classX.append(calls); + classX.append("}}\n"); + runConformTest(new String[] { "X.java", classX.toString() }, expectedOut); + } + private void testWideningFrom_both(String prim, int idx, boolean useMax, String expectedOut) { + testWideningFrom(prim, idx, useMax, expectedOut); + testWideningFrom_pattern(prim, idx, useMax, expectedOut); + } + public void testWideningByte() { + testWideningFrom_both("byte", 1, false, "49|49|49|49.0|49.0|"); + testWideningFrom_both("byte", 1, true, "127|127|127|127.0|127.0|"); + } + public void testWideningChar() { + testWideningFrom_both("char", 2, false, "49|49|49|49.0|49.0|"); // '1' + testWideningFrom_both("char", 2, true, "122|122|122|122.0|122.0|"); // 'z' + } + public void testWideningShort() { + testWideningFrom_both("short", 3, false, "49|49|49.0|49.0|"); + testWideningFrom_both("short", 3, true, "32767|32767|32767.0|32767.0|"); + } + public void testWideningInt() { + testWideningFrom_both("int", 4, false, "49|49.0|49.0|"); + // max-int -> float is not exact + testWideningFrom_both("int", 4, true, "2147483647|-1.0|"+String.valueOf((double) Integer.MAX_VALUE)+'|'); + } + public void testWideningLong() { + testWideningFrom_both("long", 5, false, "49.0|49.0|"); + // max-long -> float/double is not exact + testWideningFrom_both("long", 5, true, "-1.0|-1.0|"); + } + public void testWideningFloat() { + testWideningFrom_both("float", 6, false, "49.0|"); + testWideningFrom_both("float", 6, true, String.valueOf((double) Float.MAX_VALUE)+"|"); + } + + private void testNarrowingFrom(String from, int idx, boolean useMax, String expectedOut) { + assert from.equals(PRIMITIVES[idx]) : "mismatch between from and idx"; + // example (from="short", idx=3, useMax=false): + // public class X { + // public static byte foobyte(short v) { + // if (v instanceof byte) { + // byte vv = (byte) v; + // return vv; + // } + // return -1; + // } + // public static char foochar(short v) { + // if (v instanceof char) { + // char vv = (char) v; + // return vv; + // } + // return '-'; + // } + // static void print(Object o) { + // if (o instanceof Character && ((int)((char) o) > 127)) + // System.out.print((int)((char) o)); // avoid char encoding issues + // else + // System.out.print(o); + // System.out.print('|'); + // } + // public static void main(String[] args) { + // short v = 49; + // print(X.foobyte(v)); + // print(X.foochar(v)); + // } + // } + + StringBuilder methods = new StringBuilder(); + StringBuilder calls = new StringBuilder(); + String methodTmpl = + """ + public static PRIM fooPRIM(FROM v) { + if (v instanceof PRIM) { + PRIM vv = (PRIM) v; + return vv; + } + return NEGVAL; + } + """; + String methodPrint = """ + static void print(Object o) { + if (o instanceof Character && ((int)((char) o) > 127)) + System.out.print((int)((char) o)); // avoid char encoding issues + else + System.out.print(o); + System.out.print('|'); + } + """; + String callTmpl = + """ + print(X.fooPRIM(v)); + """; + // for all primitive types "smaller" than 'from' (except for boolean): + for (int i = 1; i < idx; i++) { + methods.append(fillIn(methodTmpl.replace("FROM", from), i)); + calls.append(fillIn(callTmpl, i)); + } + StringBuilder classX = new StringBuilder("public class X {\n"); + classX.append(methods.toString()); + classX.append(methodPrint); + classX.append("public static void main(String[] args) {\n"); + classX.append(fillInMax("PRIM v = VAL;\n", idx, useMax)); + classX.append(calls); + classX.append("}}\n"); + runConformTest(new String[] { "X.java", classX.toString() }, expectedOut); + } + private void testNarrowingFrom_pattern(String from, int idx, boolean useMax, String expectedOut) { + assert from.equals(PRIMITIVES[idx]) : "mismatch between from and idx"; + // example (from="short", idx=3, useMax=false, ...): + // public class X { + // public static byte foobyte() { + // if (bar() instanceof byte vv) { + // return vv; + // } + // return -1; + // } + // public static char foochar() { + // if (bar() instanceof char vv) { + // return vv; + // } + // return '-'; + // } + // static short bar() { + // return 49; + // } + // static void print(Object o) { + // if (o instanceof Character && (int)((char) o) > 127) + // System.out.print((int)((char) o)); // avoid char encoding issues + // else + // System.out.print(o); + // System.out.print('|'); + // } + // public static void main(String[] args) { + // print(X.foobyte()); + // print(X.foochar()); + // } + // } + + StringBuilder methods = new StringBuilder(); + StringBuilder calls = new StringBuilder(); + String methodTmpl = + """ + public static PRIM fooPRIM() { + if (bar() instanceof PRIM vv) { + return vv; + } + return NEGVAL; + } + """; + String methodBar = fillInMax(""" + static PRIM bar() { + return VAL; + } + """, + idx, useMax); + String methodPrint = """ + static void print(Object o) { + if (o instanceof Character && ((int)((char) o) > 127)) + System.out.print((int)((char) o)); // avoid char encoding issues + else + System.out.print(o); + System.out.print('|'); + } + """; + String callTmpl = + """ + print(X.fooPRIM()); + """; + // for all primitive types "smaller" than 'from' (except for boolean): + for (int i = 1; i < idx; i++) { + methods.append(fillIn(methodTmpl, i)); + calls.append(fillIn(callTmpl, i)); + } + StringBuilder classX = new StringBuilder("public class X {\n"); + classX.append(methods.toString()); + classX.append(methodBar); + classX.append(methodPrint); + classX.append("public static void main(String[] args) {\n"); + classX.append(calls); + classX.append("}}\n"); + runConformTest(new String[] { "X.java", classX.toString() }, expectedOut); + } + private void testNarrowingFrom_both(String prim, int idx, boolean useMax, String expectedOut) { + testNarrowingFrom(prim, idx, useMax, expectedOut); + testNarrowingFrom_pattern(prim, idx, useMax, expectedOut); + } + public void testNarrowingDouble() { + testNarrowingFrom_both("double", 7, false, "49|1|49|49|49|49.0|"); + testNarrowingFrom_both("double", 7, true, "-1|-|-1|-1|-1|-1.0|"); + } + public void testNarrowingFloat() { + testNarrowingFrom_both("float", 6, false, "49|1|49|49|49|"); + testNarrowingFrom_both("float", 6, true, "-1|-|-1|-1|-1|"); + } + public void testNarrowingLong() { + testNarrowingFrom_both("long", 5, false, "49|1|49|49|"); + testNarrowingFrom_both("long", 5, true, "-1|-|-1|-1|"); + } + public void testNarrowingInt() { + testNarrowingFrom_both("int", 4, false, "49|1|49|"); + testNarrowingFrom_both("int", 4, true, "-1|-|-1|"); + } + public void testNarrowingShort() { + testNarrowingFrom_both("short", 3, false, "49|1|"); + testNarrowingFrom_both("short", 3, true, "-1|32767|"); + } + public void testNarrowingChar() { + testNarrowingFrom_both("char", 2, false, "49|"); // '1' + } + + public void testNarrowingChar_various() { + runConformTest(new String[] { + "X.java", + """ + public class X { + public static char b2c(byte b) { + if (b instanceof char) { + return (char) b; + } + return '-'; + } + public static char s2c(short s) { + if (s instanceof char) { + return (char) s; + } + return '-'; + } + public static short c2s(char c) { + if (c instanceof short) { + return (short) c; + } + return -1; + } + public static char b2c_pat(byte b) { + if (b instanceof char v) { + return v; + } + return '-'; + } + public static char s2c_pat(short s) { + if (s instanceof char v) { + return v; + } + return '-'; + } + public static short c2s_pat(char c) { + if (c instanceof short v) { + return v; + } + return -1; + } + public static void main(String[] args) { + byte b=49, bmax=Byte.MAX_VALUE; + short s=49, smax=Short.MAX_VALUE; + char c='1', cmax=Character.MAX_VALUE; + print(X.b2c(b)); + print(X.s2c(s)); + print(X.c2s(c)); + print(X.b2c(bmax)); + print(X.s2c(smax)); + print(X.c2s(cmax)); + System.out.println(); + print(X.b2c_pat(b)); + print(X.s2c_pat(s)); + print(X.c2s_pat(c)); + print(X.b2c_pat(bmax)); + print(X.s2c_pat(smax)); + print(X.c2s_pat(cmax)); + } + static void print(Object s) { + if (s instanceof Character) + System.out.print((int)((char) s)); // avoid char encoding issues + else + System.out.print(s); + System.out.print('|'); + } + } + """ + }, + "49|49|49|127|32767|-1|\n" + + "49|49|49|127|32767|-1|"); + } + + public void testBoxing() { + // public class X { + // public static Boolean boolean2Boolean(boolean v) { + // if (v instanceof Boolean) { + // return (Boolean) v; + // } + // return false; + // } + // public static Byte byte2Byte(byte v) { + // if (v instanceof Byte) { + // return (Byte) v; + // } + // return -1; + // } + // [...] + // public static void main(String[] args) { + // boolean vboolean = true; + // System.out.print(X.boolean2Boolean(vboolean)); + // System.out.print('|'); + // byte vbyte = 49; + // System.out.print(X.byte2Byte(vbyte)); + // System.out.print('|'); + // [...] + // } + // } + + StringBuilder methods = new StringBuilder(); + StringBuilder calls = new StringBuilder(); + String methodTmpl = + """ + public static BOX PRIM2BOX(PRIM v) { + if (v instanceof BOX) { + return (BOX) v; + } + return NEGVAL; + } + """; + String callTmpl = + """ + PRIM vPRIM = VAL; + System.out.print(X.PRIM2BOX(vPRIM)); + System.out.print('|'); + """; + // for all primitive types: + for (int i = 0; i < PRIMITIVES.length; i++) { + methods.append(fillIn(methodTmpl, i)); + calls.append(fillIn(callTmpl, i)); + } + StringBuilder classX = new StringBuilder("public class X {\n"); + classX.append(methods.toString()); + classX.append("public static void main(String[] args) {\n"); + classX.append(calls); + classX.append("}}\n"); + runConformTest(new String[] { "X.java", classX.toString() }, + "true|49|1|49|49|49|49.0|49.0|"); + } + public void testBoxing_pattern() { + // public class X { + // public static Boolean boolean2Boolean() { + // if (barboolean() instanceof Boolean v) { + // return v; + // } + // return false; + // } + // static boolean barboolean() { + // return true; + // } + // public static Byte byte2Byte() { + // if (barbyte() instanceof Byte v) { + // return v; + // } + // return -1; + // } + // static byte barbyte() { + // return 49; + // } + // [...] + // public static void main(String[] args) { + // System.out.print(X.boolean2Boolean()); + // System.out.print('|'); + // System.out.print(X.byte2Byte()); + // System.out.print('|'); + // [...] + // } + // } + + StringBuilder methods = new StringBuilder(); + StringBuilder calls = new StringBuilder(); + String methodTmpl = + """ + public static BOX PRIM2BOX() { + if (barPRIM() instanceof BOX v) { + return v; + } + return NEGVAL; + } + static PRIM barPRIM() { + return VAL; + } + """; + String callTmpl = + """ + System.out.print(X.PRIM2BOX()); + System.out.print('|'); + """; + // for all primitive types: + for (int i = 0; i < PRIMITIVES.length; i++) { + methods.append(fillIn(methodTmpl, i)); + calls.append(fillIn(callTmpl, i)); + } + StringBuilder classX = new StringBuilder("public class X {\n"); + classX.append(methods.toString()); + classX.append("public static void main(String[] args) {\n"); + classX.append(calls); + classX.append("}}\n"); + runConformTest(new String[] { "X.java", classX.toString() }, + "true|49|1|49|49|49|49.0|49.0|"); + } + + public void testUnboxing() { + // public class X { + // public static boolean Boolean2boolean(Boolean v) { + // if (v instanceof boolean) { + // return (boolean) v; + // } + // return false; + // } + // public static byte Byte2byte(Byte v) { + // if (v instanceof byte) { + // return (byte) v; + // } + // return -1; + // } + // [...] + // public static void main(String[] args) { + // Boolean vBoolean = true; + // System.out.print(X.Boolean2boolean(vBoolean)); + // System.out.print('|'); + // Byte vByte = 49; + // System.out.print(X.Byte2byte(vByte)); + // System.out.print('|'); + // [...] + // } + // } + + StringBuilder methods = new StringBuilder(); + StringBuilder calls = new StringBuilder(); + String methodTmpl = + """ + public static PRIM BOX2PRIM(BOX v) { + if (v instanceof PRIM) { + return (PRIM) v; + } + return NEGVAL; + } + """; + String callTmpl = + """ + BOX vBOX = VAL; + System.out.print(X.BOX2PRIM(vBOX)); + System.out.print('|'); + """; + // for all primitive types: + for (int i = 0; i < PRIMITIVES.length; i++) { + methods.append(fillIn(methodTmpl, i)); + calls.append(fillIn(callTmpl, i)); + } + StringBuilder classX = new StringBuilder("public class X {\n"); + classX.append(methods.toString()); + classX.append("public static void main(String[] args) {\n"); + classX.append(calls); + classX.append("}}\n"); + runConformTest(new String[] { "X.java", classX.toString() }, + "true|49|1|49|49|49|49.0|49.0|"); + } + public void testUnboxing_pattern() { + // public class X { + // public static Boolean boolean2Boolean() { + // if (barboolean() instanceof Boolean v) { + // return v; + // } + // return false; + // } + // static boolean barboolean() { + // return true; + // } + // public static Byte byte2Byte() { + // if (barbyte() instanceof Byte v) { + // return v; + // } + // return -1; + // } + // static byte barbyte() { + // return 49; + // } + // [...] + // public static void main(String[] args) { + // System.out.print(X.boolean2Boolean()); + // System.out.print('|'); + // System.out.print(X.byte2Byte()); + // System.out.print('|'); + // [...] + // } + // } + + StringBuilder methods = new StringBuilder(); + StringBuilder calls = new StringBuilder(); + String methodTmpl = + """ + public static PRIM BOX2PRIM() { + if (barBOX() instanceof PRIM v) { + return v; + } + return NEGVAL; + } + static BOX barBOX() { + return VAL; + } + """; + String callTmpl = + """ + System.out.print(X.BOX2PRIM()); + System.out.print('|'); + """; + // for all primitive types: + for (int i = 0; i < PRIMITIVES.length; i++) { + methods.append(fillIn(methodTmpl, i)); + calls.append(fillIn(callTmpl, i)); + } + StringBuilder classX = new StringBuilder("public class X {\n"); + classX.append(methods.toString()); + classX.append("public static void main(String[] args) {\n"); + classX.append(calls); + classX.append("}}\n"); + runConformTest(new String[] { "X.java", classX.toString() }, + "true|49|1|49|49|49|49.0|49.0|"); + } + + // boxing and widening reference conversion + + private void primitive2Comparable(String prim, int idx, String expectedOut) { + String methodTmpl = + """ + @SuppressWarnings("rawtypes") + public static Comparable foo1(PRIM v) { + if (v instanceof Comparable r) { + return r; + } + return null; + } + @SuppressWarnings({"rawtypes", "unchecked" }) + public static Comparable foo2(PRIM v) { + if (v instanceof Comparable r) { + return r; + } + return null; + } + @SuppressWarnings("unchecked") + public static Comparable foo3(PRIM v) { + if (v instanceof Comparable r) { + return r; + } + return null; + } + """; + String callTmpl = + """ + PRIM vPRIM = VAL; + System.out.print(X.foo1(vPRIM)); + System.out.print('|'); + System.out.print(X.foo2(vPRIM)); + System.out.print('|'); + System.out.print(X.foo3(vPRIM)); + System.out.print('|'); + """; + if (IS_NUMERICAL[idx]) { + methodTmpl += + """ + public static Number foo4(PRIM v) { + if (v instanceof Number r) { + return r; + } + return null; + } + """; + callTmpl += + """ + System.out.print(X.foo4(vPRIM)); + System.out.print('|'); + """; + } + StringBuilder classX = new StringBuilder("public class X {\n"); + classX.append(fillIn(methodTmpl, idx)); + classX.append("public static void main(String[] args) {\n"); + classX.append(fillIn(callTmpl, idx)); + classX.append("}}\n"); + runConformTest(new String[] { "X.java", classX.toString() }, expectedOut); + } + + public void test2Comparable_boolean() { + primitive2Comparable("boolean", 0, "true|true|true|"); + } + public void test2Comparable_byte() { + primitive2Comparable("byte", 1, "49|49|49|49|"); + } + public void test2Comparable_char() { + primitive2Comparable("char", 2, "1|1|1|"); // '1' + } + public void test2Comparable_short() { + primitive2Comparable("short", 3, "49|49|49|49|"); + } + public void test2Comparable_int() { + primitive2Comparable("int", 4, "49|49|49|49|"); + } + public void test2Comparable_long() { + primitive2Comparable("long", 5, "49|49|49|49|"); + } + public void test2Comparable_float() { + primitive2Comparable("float", 6, "49.0|49.0|49.0|49.0|"); + } + public void test2Comparable_double() { + primitive2Comparable("double", 7, "49.0|49.0|49.0|49.0|"); + } + public void test2Number_NOK() { + runNegativeTest(new String[] { + "X.java", + """ + public class X { + public static Number foo1(boolean b) { + if (b instanceof Number r) { + return r; + } + return null; + } + public static Number foo2(char c) { + if (c instanceof Number r) { + return r; + } + return null; + } + } + """ + }, + """ + ---------- + 1. ERROR in X.java (at line 3) + if (b instanceof Number r) { + ^^^^^^^^^^^^^^^^^^^^^ + Incompatible conditional operand types boolean and Number + ---------- + 2. ERROR in X.java (at line 9) + if (c instanceof Number r) { + ^^^^^^^^^^^^^^^^^^^^^ + Incompatible conditional operand types char and Number + ---------- + """); + } + + public void testNonPrim001() { + runConformTest(new String[] { + "X.java", + """ + class Y { + public boolean foo(T t) { + if (t instanceof T) { + return false; + } + return true; + } + } + + public class X { + public static void main(String argv[]) { + System.out.println(new Y().foo(null)); + } + } + """ + }, + "true"); + } + + public void testPrimitivePatternInSwitch() { + StringBuilder methods = new StringBuilder(); + StringBuilder calls = new StringBuilder(); + String methodTmpl = + """ + public static PRIM switchPRIM(PRIM in) { + return switch (in) { + case MAX -> NEGVAL; + case PRIM v -> v; + }; + } + """; + String callTmpl = + """ + PRIM vPRIM = VAL; + System.out.print(X.switchPRIM(vPRIM)); + System.out.print('|'); + vPRIM = MAX; + System.out.print(X.switchPRIM(vPRIM)); + System.out.print('|'); + """; + // for all primitive types: + for (int i = 0; i < PRIMITIVES.length; i++) { + methods.append(fillIn(methodTmpl, i)); + calls.append(fillIn(callTmpl, i)); + } + StringBuilder classX = new StringBuilder("public class X {\n"); + classX.append(methods.toString()); + classX.append("public static void main(String[] args) {\n"); + classX.append(calls); + classX.append("}}\n"); + runConformTest(new String[] { "X.java", classX.toString() }, + "false|false|49|-1|1|-|49|-1|49|-1|49|-1|49.0|-1.0|49.0|-1.0|"); + } + + public void testPrimitivePatternInSwitch_more() { + runConformTest(new String[] { + "X.java", + """ + public class X { + public static String switchbool(boolean in) { + // generic test couldn't differentiate cases by output + return switch (in) { + case true -> "true"; + case boolean v -> "v="+String.valueOf(v); + }; + } + public static String switchfloatMoreCases(float f) { + return switch (f) { + case 1.0f -> "1.0"; + case 1.5f -> "1.5"; + case float v -> "v="+String.valueOf(v); + }; + } + public static void main(String... args) { + System.out.print(switchbool(true)); + System.out.print("|"); + System.out.print(switchbool(false)); + System.out.print("|"); + System.out.print(switchfloatMoreCases(1.0f)); + System.out.print("|"); + System.out.print(switchfloatMoreCases(1.5f)); + System.out.print("|"); + System.out.print(switchfloatMoreCases(1.6f)); + System.out.print("|"); + } + } + """}, + "true|v=false|1.0|1.5|v=1.6|"); + } + + private void testNarrowingInSwitchFrom(String from, int idx, String expectedOut) { + // case statements (constant & type pattern) apply narrowing to each smaller numerical type + + assert from.equals(PRIMITIVES[idx]) : "mismatch between from and idx"; + // example (from="short", idx=3): + // public class X { + // public static int doswitch(short v) { // return type: at least 'int' or + // return switch(v) { + // case 1 -> 10; + // case byte vv -> 10+vv; + // case 300 -> 30; + // case short vv -> 30+vv; + // } + // } + // static void print(Object o) { + // System.out.print(o); + // System.out.print('|'); + // } + // public static void main(String[] args) { + // print(X.doswitch((short)1); + // print(X.doswitch((short)(1+1)); + // print(X.doswitch((short)300); + // print(X.doswitch((short)(300+300)); + // } + // } + + String classTmpl = + """ + public class X { + public static RET doswitch(FROM v) { + return switch(v) { + BODY + }; + } + static void print(Object o) { + System.out.print(o); + System.out.print('|'); + } + public static void main(String[] args) { + CALLS + } + } + """; + String casesTmpl = + """ + case CONST -> VAL; + case PRIM vv -> VAL+vv; + """; + String callsTmpl = + """ + print(X.doswitch((FROM)CONST)); + print(X.doswitch((FROM)(CONST+CONST))); + """; + // for all numerical primitive types up-to 'from': + StringBuilder cases = new StringBuilder(); + StringBuilder calls = new StringBuilder(); + for (int i = 0; i <= idx; i++) { + if (!IS_NUMERICAL[i]) continue; + String constVal = toConstantOfType(CONSTANTS[i], from); + String val10 = String.valueOf(i*10); + cases.append(casesTmpl.replaceAll("PRIM", PRIMITIVES[i]).replace("CONST", constVal).replace("VAL", val10)); + calls.append(callsTmpl.replaceAll("FROM", from).replaceAll("CONST", constVal)); + } + String retType = idx <= 4 /*int*/ ? "int" : from; // no syntax exists for constants below int + String classX = classTmpl.replace("FROM", from).replace("RET", retType) + .replace("BODY", cases.toString()) + .replace("CALLS", calls.toString()); + runConformTest(new String[] { "X.java", classX }, expectedOut); + } + public void testNarrowingInSwitchFromShort() { + testNarrowingInSwitchFrom("short", 3, "10|12|30|630|"); + } + public void testNarrowingInSwitchFromInt() { + testNarrowingInSwitchFrom("int", 4, "10|12|30|630|40|80040|"); + } + public void testNarrowingInSwitchFromLong() { + testNarrowingInSwitchFrom("long", 5, "10|12|30|630|40|80040|50|10000000050|"); + } + public void testNarrowingInSwitchFromFloat() { + testNarrowingInSwitchFrom("float", 6, "10.0|12.0|30.0|630.0|40.0|80040.0|50.0|1.0E10|60.0|1.2E21|"); + } + public void testNarrowingInSwitchFromDouble() { + testNarrowingInSwitchFrom("double", 7, "10.0|12.0|30.0|630.0|40.0|80040.0|50.0|1.000000005E10|60.0|1.2E21|70.0|1.4E41|"); + } + + public void testSwitchOn_long_wrongSelector() { + runNegativeTest(new String[] { + "X.java", + """ + public class X { + int m1(long in) { + return switch(in) { + case 1 -> 1; + case 'a' -> 2; + case 3L -> 3; + case 4.0f -> 4; + case 5.0d -> 5; + default -> -1; + }; + } + } + """ + }, + """ + ---------- + 1. ERROR in X.java (at line 4) + case 1 -> 1; + ^ + Case constants in a switch on 'long' must have type 'long' + ---------- + 2. ERROR in X.java (at line 5) + case 'a' -> 2; + ^^^ + Case constants in a switch on 'long' must have type 'long' + ---------- + 3. ERROR in X.java (at line 7) + case 4.0f -> 4; + ^^^^ + Case constants in a switch on 'long' must have type 'long' + ---------- + 4. ERROR in X.java (at line 8) + case 5.0d -> 5; + ^^^^ + Case constants in a switch on 'long' must have type 'long' + ---------- + """); + } + public void testSwitchOn_Float_wrongSelector() { + runNegativeTest(new String[] { + "X.java", + """ + public class X { + int m1(Float in) { + return switch(in) { + case 1 -> 1; + case 'a' -> 2; + case 3L -> 3; + case 4.0f -> 4; + case 5.0d -> 5; + default -> -1; + }; + } + } + """ + }, + """ + ---------- + 1. ERROR in X.java (at line 4) + case 1 -> 1; + ^ + Case constants in a switch on 'Float' must have type 'float' + ---------- + 2. ERROR in X.java (at line 5) + case 'a' -> 2; + ^^^ + Case constants in a switch on 'Float' must have type 'float' + ---------- + 3. ERROR in X.java (at line 6) + case 3L -> 3; + ^^ + Case constants in a switch on 'Float' must have type 'float' + ---------- + 4. ERROR in X.java (at line 8) + case 5.0d -> 5; + ^^^^ + Case constants in a switch on 'Float' must have type 'float' + ---------- + """); + } + public void testSwitchOnBoxed_OK() { + // constant cases for all boxed primitive types except Boolean + // run as separate tests. + String classTmpl = """ + public class XBOX { + static int m1(BOX in) { + return switch(in) { + case VAL -> 1; + case MAX -> 2; + default -> -2; + }; + } + public static void main(String... args) { + CALLS + } + } + """; + String callsTmpl = + """ + System.out.print(m1((PRIM)VAL)); + System.out.print(m1((PRIM)MAX)); + System.out.print(m1((PRIM)NEGVAL)); + """; + // for all primitive types other than boolean (boolean would have duplicate cases): + for (int i = 1; i < PRIMITIVES.length; i++) { // 1 + String calls = fillIn(callsTmpl, i); + String classX = fillIn(classTmpl, i) + .replace("CALLS", calls); + runConformTest(new String[] { "XBOX.java".replace("BOX", BOXES[i]), classX }, "12-2"); + } + } + public void testSwitchOn_Boolean_OK() { + runConformTest(new String[] { + "X.java", + """ + public class X { + static int m1(Boolean in) { + return switch(in) { + case true-> 1; + default -> -1; + }; + } + public static void main(String... args) { + System.out.print(m1(true)); + System.out.print(m1(false)); + } + } + """ + }, + "1-1"); + } + + public void testDuplicateBoolCase() { + // saw SOE when executing bogus byte code: + runNegativeTest(new String[] { + "XBoolean.java", + """ + public class XBoolean { + static int m1(Boolean in) { + return switch(in) { + case true -> 1; + case true -> 2; + default -> -1; + }; + } + public static void main(String... args) { + System.out.print(m1(true)); + System.out.print(m1(true)); + System.out.print(m1(false)); + + } + } + """ + }, + """ + ---------- + 1. ERROR in XBoolean.java (at line 4) + case true -> 1; + ^^^^ + Duplicate case + ---------- + 2. ERROR in XBoolean.java (at line 5) + case true -> 2; + ^^^^ + Duplicate case + ---------- + """); + } + // test from spec + public void _testSpec001() { + runConformTest(new String[] { + "X.java", + """ + public class X { + public int getStatus() { + return 100; + } + public static int foo(X x) { + return switch (x.getStatus()) { + case int i -> i; + default -> -1; + }; + } + public static void main(String[] args) { + X x = new X(); + System.out.println(X.foo(x)); + } + } + """ + }, + "100"); + } + public void _testSpec002() { + runConformTest(new String[] { + "X.java", + """ + public class X { + public int getStatus() { + return 100; + } + public static int foo(X x) { + return switch (x.getStatus()) { + case int i when i > 10 -> i * i; + case int i -> i; + default -> -1; + }; + } + public static void main(String[] args) { + X x = new X(); + System.out.println(X.foo(x)); + } + } + """ + }, + "100"); + } + public void _testSpec003() { + runConformTest(new String[] { + "X.java", + """ + import java.util.Map; + + sealed interface JsonValue {} + record JsonString(String s) implements JsonValue { } + record JsonNumber(double d) implements JsonValue { } + record JsonObject(Map map) implements JsonValue { } + + + public class X { + + public static void foo() { + var json = new JsonObject(Map.of("name", new JsonString("John"), + "age", new JsonNumber(30))); + JsonValue v = json.map().get("age"); + System.out.println(v); + } + public static void main(String[] args) { + X.foo(); + } + } + """ + }, + "JsonNumber[d=30.0]"); + } + public void _testSpec004() { + runConformTest(new String[] { + "X.java", + """ + import java.util.Map; + + sealed interface JsonValue {} + record JsonString(String s) implements JsonValue { } + record JsonNumber(double d) implements JsonValue { } + record JsonObject(Map map) implements JsonValue { } + + + public class X { + + public static JsonObject foo() { + var json = new JsonObject(Map.of("name", new JsonString("John"), + "age", new JsonNumber(30))); + return json; + } + public static void bar(Object json) { + if (json instanceof JsonObject(var map) + && map.get("name") instanceof JsonString(String n) + && map.get("age") instanceof JsonNumber(double a)) { + int age = (int)a; // unavoidable (and potentially lossy!) cast + System.out.println(age); + } + } + public static void main(String[] args) { + X.bar(X.foo()); + } + } + """ + }, + "30"); + } + public void _testSpec005() { + runConformTest(new String[] { + "X.java", + """ + import java.util.HashMap; + import java.util.Map; + + sealed interface I {} + record ZNumber(double d) implements I { } + record ZObject(Map map) implements I { } + + + public class X { + + public static ZObject foo() { + Map myMap = new HashMap<>(); + myMap.put("age", new ZNumber(30)); + return new ZObject(myMap); + } + public static void bar(Object json) { + if (json instanceof ZObject(var map)) { + if (map.get("age") instanceof ZNumber(double d)) { + System.out.println("double:"+d); + } + } + } + public static void main(String[] args) { + X.bar(X.foo()); + } + } + """ + }, + "double:30.0"); + } + public void _testSpec006() { + runConformTest(new String[] { + "X.java", + """ + import java.util.HashMap; + import java.util.Map; + + sealed interface I {} + record ZNumber(double d) implements I { } + record ZObject(Map map) implements I { } + + + public class X { + + public static ZObject foo() { + Map myMap = new HashMap<>(); + myMap.put("age", new ZNumber(30)); + return new ZObject(myMap); + } + public static void bar(Object json) { + if (json instanceof ZObject(var map)) { + if (map.get("age") instanceof ZNumber(int i)) { + System.out.println("int:"+i); + } else if (map.get("age") instanceof ZNumber(double d)) { + System.out.println("double:"+d); + } + } + } + public static void main(String[] args) { + X.bar(X.foo()); + } + } + """ + }, + "int:30"); + } + public void _testSpec00X() { + runNegativeTest(new String[] { + "X.java", + """ + """ + }, + "----------\n" + + "2. ERROR in X.java (at line 16)\n" + + " Zork();\n" + + " ^^^^\n" + + "The method Zork() is undefined for the type X\n" + + "----------\n"); + } + +} diff --git a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/TestAll.java b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/TestAll.java index 44678b970ee..551bd039dc1 100644 --- a/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/TestAll.java +++ b/org.eclipse.jdt.core.tests.compiler/src/org/eclipse/jdt/core/tests/compiler/regression/TestAll.java @@ -261,6 +261,7 @@ public static Test suite() { since_23.add(SuperAfterStatementsTest.class); since_23.add(ImplicitlyDeclaredClassesTest.class); since_23.add(PrimitiveInPatternsTest.class); + since_23.add(PrimitiveInPatternsTestSH.class); since_23.add(MarkdownCommentsTest.class); // Build final test suite