From e95497237754d0cdf44a3156ce7d29ac95847e4e Mon Sep 17 00:00:00 2001 From: Srikanth Sankaran Date: Wed, 4 Dec 2024 14:37:46 +0530 Subject: [PATCH] [Enhanced Switch] "Retrofit" some design into code generation for switch statement * Fixes https://github.com/eclipse-jdt/eclipse.jdt.core/issues/3430 --- .../internal/compiler/ast/CaseStatement.java | 9 +- .../compiler/ast/SwitchStatement.java | 568 ++++++++---------- .../compiler/codegen/BranchLabel.java | 3 - .../internal/compiler/codegen/CaseLabel.java | 28 +- .../internal/compiler/codegen/CodeStream.java | 10 +- 5 files changed, 265 insertions(+), 353 deletions(-) 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 43f98c90a54..325e1ba53d3 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 @@ -40,7 +40,6 @@ public class CaseStatement extends Statement { public BranchLabel targetLabel; public Expression[] constantExpressions; // case with multiple expressions - if you want a under-the-hood view, use peeledLabelExpressions() - public BranchLabel[] targetLabels; // for multiple expressions public boolean isSwitchRule = false; public SwitchStatement swich; // owning switch @@ -324,13 +323,9 @@ public FlowInfo analyseCode(BlockScope currentScope, FlowContext flowContext, Fl public void generateCode(BlockScope currentScope, CodeStream codeStream) { if ((this.bits & ASTNode.IsReachable) == 0) return; + int pc = codeStream.position; - if (this.targetLabels != null) { - for (BranchLabel label : this.targetLabels) - label.place(); - } - if (this.targetLabel != null) - this.targetLabel.place(); + this.targetLabel.place(); if (containsPatternVariable(true)) { 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 1099e0bf112..da9760ba39e 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 @@ -21,6 +21,7 @@ import static org.eclipse.jdt.internal.compiler.ClassFile.CONSTANT_BOOTSTRAP__GET_STATIC_FINAL; import static org.eclipse.jdt.internal.compiler.ClassFile.CONSTANT_BOOTSTRAP__PRIMITIVE_CLASS; +import static org.eclipse.jdt.internal.compiler.ast.SwitchStatement.SwitchTranslator.newSwitchTranslator; import java.lang.invoke.ConstantBootstraps; import java.util.ArrayList; @@ -38,6 +39,7 @@ import org.eclipse.jdt.internal.compiler.codegen.CaseLabel; import org.eclipse.jdt.internal.compiler.codegen.CodeStream; import org.eclipse.jdt.internal.compiler.codegen.ConstantPool; +import org.eclipse.jdt.internal.compiler.codegen.Label; import org.eclipse.jdt.internal.compiler.codegen.Opcodes; import org.eclipse.jdt.internal.compiler.flow.FlowContext; import org.eclipse.jdt.internal.compiler.flow.FlowInfo; @@ -56,7 +58,6 @@ import org.eclipse.jdt.internal.compiler.lookup.TypeIds; import org.eclipse.jdt.internal.compiler.problem.ProblemSeverities; -@SuppressWarnings("rawtypes") public class SwitchStatement extends Expression { /** Descriptor for a bootstrap method that is created only once but can be used more than once. */ @@ -72,13 +73,15 @@ public static record SingletonBootstrap(String id, char[] selector, char[] signa public Statement[] statements; public BlockScope scope; public int explicitDeclarations; + public int blockStart; public BranchLabel breakLabel; + private CaseLabel defaultLabel; + + public int caseCount; // count of all cases *including* default public CaseStatement[] cases; // all cases *including* default public CaseStatement defaultCase; public CaseStatement nullCase; // convenience pointer for pattern switches - public int blockStart; - public int caseCount; // count of all cases *including* default public static final LabelExpression[] NO_LABEL_EXPRESSIONS = new LabelExpression[0]; public LabelExpression[] labelExpressions = NO_LABEL_EXPRESSIONS; @@ -461,369 +464,282 @@ else if ((statement.bits & ASTNode.DocumentedFallthrough) == 0) // the case is n if (this.scope != null) this.scope.enclosingCase = null; // no longer inside switch case block } } - /** - * Switch on String code generation - * This assumes that hashCode() specification for java.lang.String is API - * and is stable. - * - * @see "http://download.oracle.com/javase/6/docs/api/java/lang/String.html" - * - * @param currentScope org.eclipse.jdt.internal.compiler.lookup.BlockScope - * @param codeStream org.eclipse.jdt.internal.compiler.codegen.CodeStream - */ - public void generateCodeForStringSwitch(BlockScope currentScope, CodeStream codeStream) { - - try { - int pc = codeStream.position; + abstract static sealed class SwitchTranslator { - class StringSwitchCase implements Comparable { - int hashCode; - String string; - BranchLabel label; - public StringSwitchCase(int hashCode, String string, BranchLabel label) { - this.hashCode = hashCode; - this.string = string; - this.label = label; - } - @Override - public int compareTo(Object o) { - StringSwitchCase that = (StringSwitchCase) o; - if (this.hashCode == that.hashCode) { - return 0; - } - if (this.hashCode > that.hashCode) { - return 1; - } - return -1; - } - @Override - public String toString() { - return "StringSwitchCase :\n" + //$NON-NLS-1$ - "case " + this.hashCode + ":(" + this.string + ")\n"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } - } - /* - * With multi constant case statements, the number of case statements (hence branch labels) - * and number of constants (hence hashcode labels) could be different. For e.g: - - switch(s) { - case "FB", "c": - System.out.println("A/C"); - break; - case "Ea": - System.out.println("B"); - break; + protected SwitchStatement swich; - With the above code, we will have - 2 branch labels for FB and c - 3 stringCases for FB, c and Ea - 2 hashCodeCaseLabels one for FB, Ea and one for c - - Should produce something like this: - lookupswitch { // 2 - 99: 32 - 2236: 44 - default: 87 - - "FB" and "Ea" producing the same hashcode values, but still belonging in different case statements. - First, produce the two branch labels pertaining to the case statements - And the three string cases. - */ - final boolean hasCases = this.caseCount > 1 || (this.caseCount == 1 && this.defaultCase == null); - int constSize = hasCases ? this.labelExpressions.length : 0; - BranchLabel[] sourceCaseLabels = this.gatherLabels(codeStream, new BranchLabel[this.nConstants], BranchLabel::new); - StringSwitchCase [] stringCases = new StringSwitchCase[constSize]; // may have to shrink later if multiple strings hash to same code. - CaseLabel [] hashCodeCaseLabels = new CaseLabel[constSize]; - int [] hashCodes = new int[constSize]; - for (int i = 0; i < constSize; i++) { - String literal = this.labelExpressions[i].constant.stringValue(); - stringCases[i] = new StringSwitchCase(literal.hashCode(), literal, sourceCaseLabels[i]); - hashCodeCaseLabels[i] = new CaseLabel(codeStream); - } - Arrays.sort(stringCases); - - int uniqHashCount = 0; - int lastHashCode = 0; - for (int i = 0, length = constSize; i < length; ++i) { - int hashCode = stringCases[i].hashCode; - if (i == 0 || hashCode != lastHashCode) { - lastHashCode = hashCodes[uniqHashCount++] = hashCode; - } - } - - if (uniqHashCount != constSize) { // multiple keys hashed to the same value. - System.arraycopy(hashCodes, 0, hashCodes = new int[uniqHashCount], 0, uniqHashCount); - System.arraycopy(hashCodeCaseLabels, 0, hashCodeCaseLabels = new CaseLabel[uniqHashCount], 0, uniqHashCount); - } - int[] sortedIndexes = new int[uniqHashCount]; // hash code are sorted already anyways. - for (int i = 0; i < uniqHashCount; i++) { - sortedIndexes[i] = i; - } - - CaseLabel defaultCaseLabel = new CaseLabel(codeStream); + public SwitchTranslator(SwitchStatement swich) { + this.swich = swich; + } + protected void generatePrologue(BlockScope currentScope, CodeStream codeStream) { // prepare the labels and constants - this.breakLabel.initialize(codeStream); + this.swich.breakLabel.initialize(codeStream); + CaseLabel[] caseLabels = this.swich.gatherLabels(codeStream, new CaseLabel[this.swich.nConstants], CaseLabel::new); - BranchLabel defaultBranchLabel = new BranchLabel(codeStream); - if (this.defaultCase != null) { - this.defaultCase.targetLabel = defaultBranchLabel; - } - // generate expression - this.expression.generateCode(currentScope, codeStream, true); - codeStream.store(this.selector, true); // leaves string on operand stack - codeStream.addVariable(this.selector); - codeStream.invokeStringHashCode(); - if (hasCases) { - codeStream.lookupswitch(defaultCaseLabel, hashCodes, sortedIndexes, hashCodeCaseLabels); - for (int i = 0, j = 0, max = constSize; i < max; i++) { - int hashCode = stringCases[i].hashCode; - if (i == 0 || hashCode != lastHashCode) { - lastHashCode = hashCode; - if (i != 0) { - codeStream.goto_(defaultBranchLabel); - } - hashCodeCaseLabels[j++].place(); - } - codeStream.load(this.selector); - codeStream.ldc(stringCases[i].string); - codeStream.invokeStringEquals(); - codeStream.ifne(stringCases[i].label); + this.swich.defaultLabel = this.swich.defaultCase != null ? (CaseLabel) this.swich.defaultCase.targetLabel : new CaseLabel(codeStream); + final boolean hasCases = this.swich.caseCount > 1 || (this.swich.caseCount == 1 && this.swich.defaultCase == null); + + final TypeBinding selectorType = this.swich.expression.resolvedType; + int constantCount = this.swich.labelExpressions.length; + int [] constants = new int[constantCount]; + if (this.swich.needPatternDispatchCopy()) { + this.swich.generateCodeSwitchPatternPrologue(currentScope, codeStream); + for (int i = 0, j = 0, length = this.swich.labelExpressions.length; i < length; ++i) { + if (this.swich.nullCase == null && this.swich.labelExpressions[i].expression == this.swich.totalPattern) + this.swich.labelExpressions[i].index = -1; + constants[i] = this.swich.labelExpressions[i].index - j; + if (this.swich.labelExpressions[i].expression instanceof NullLiteral) + j = 1; // since we yank null out to -1, shift down everything beyond. + } + } else { + for (int i = 0, length = this.swich.labelExpressions.length; i < length; ++i) + constants[i] = this.swich.labelExpressions[i].intValue(); + if (selectorType.isEnum()) { + // go through the translation table in order to guarantee binary compatibility promises of "13.4.26 Evolution of Enum Classes" + codeStream.invoke(Opcodes.OPC_invokestatic, this.swich.synthetic, null /* default declaringClass */); + this.swich.expression.generateCode(currentScope, codeStream, true); + codeStream.invokeEnumOrdinal(selectorType.constantPoolName()); + codeStream.iaload(); + } else { + this.swich.expression.generateCode(currentScope, codeStream, true); + if (selectorType.id == TypeIds.T_JavaLangBoolean) + codeStream.generateUnboxingConversion(TypeIds.T_boolean); // optimize by avoiding indy typeSwitch } - codeStream.goto_(defaultBranchLabel); + } + if (hasCases) { // generate the appropriate switch table/lookup bytecode + int[] sortedIndexes = new int[constantCount]; + for (int i = 0; i < constantCount; i++) + sortedIndexes[i] = i; + int[] localKeysCopy; + System.arraycopy(constants, 0, (localKeysCopy = new int[constantCount]), 0, constantCount); + CodeStream.sort(localKeysCopy, 0, constantCount - 1, sortedIndexes); + + int max = localKeysCopy[constantCount - 1]; + int min = localKeysCopy[0]; + if ((long) (constantCount * 2.5) > ((long) max - (long) min)) + codeStream.tableswitch(this.swich.defaultLabel, min, max, constants, sortedIndexes, caseLabels); + else + codeStream.lookupswitch(this.swich.defaultLabel, constants, sortedIndexes, caseLabels); + codeStream.recordPositionsFrom(codeStream.position, this.swich.expression.sourceEnd); } else { codeStream.pop(); } + } - // generate the switch block statements - if (this.statements != null) { - for (Statement statement : this.statements) { + protected final void generateSwitchBlock(BlockScope currentScope, CodeStream codeStream) { + if (this.swich.statements != null) { + for (Statement statement : this.swich.statements) { if (statement instanceof CaseStatement caseStatement) { - this.scope.enclosingCase = caseStatement; // record entering in a switch case block - if (this.preSwitchInitStateIndex != -1) - codeStream.removeNotDefinitelyAssignedVariables(currentScope, this.preSwitchInitStateIndex); - if (statement == this.defaultCase) - defaultCaseLabel.place(); // branch label gets placed by generateCode below. + this.swich.scope.enclosingCase = caseStatement; // record entering in a switch case block + if (this.swich.preSwitchInitStateIndex != -1) + codeStream.removeNotDefinitelyAssignedVariables(currentScope, this.swich.preSwitchInitStateIndex); } - statement.generateCode(this.scope, codeStream); - if ((this.switchBits & LabeledRules) != 0 && statement instanceof Block && statement.canCompleteNormally()) - codeStream.goto_(this.breakLabel); + statement.generateCode(this.swich.scope, codeStream); + if ((this.swich.switchBits & LabeledRules) != 0 && statement instanceof Block && statement.canCompleteNormally()) + codeStream.goto_(this.swich.breakLabel); } } + } - // May loose some local variable initializations : affecting the local variable attributes - if (this.mergedInitStateIndex != -1) { - codeStream.removeNotDefinitelyAssignedVariables(currentScope, this.mergedInitStateIndex); - codeStream.addDefinitelyAssignedVariables(currentScope, this.mergedInitStateIndex); - } - codeStream.removeVariable(this.selector); - if (this.scope != currentScope) { - codeStream.exitUserScope(this.scope); + protected final void generateDefault(BlockScope currentScope, CodeStream codeStream) { + if (this.swich.defaultCase == null) { + boolean needsThrowingDefault = false; + needsThrowingDefault = this.swich.expression.resolvedType.isEnum() && (this.swich instanceof SwitchExpression || this.swich.containsNull); + needsThrowingDefault |= this.swich.isExhaustive(); // pattern switches: + if (needsThrowingDefault) { + if (this.swich.preSwitchInitStateIndex != -1) + codeStream.removeNotDefinitelyAssignedVariables(currentScope, this.swich.preSwitchInitStateIndex); + if (this.swich.scope.compilerOptions().complianceLevel >= ClassFileConstants.JDK19) { // since 19 we have MatchException for this + if (this.swich.statements.length > 0 && this.swich.statements[this.swich.statements.length - 1].canCompleteNormally()) + codeStream.goto_(this.swich.breakLabel); // hop, skip and jump over match exception throw. + this.swich.defaultLabel.place(); + codeStream.newJavaLangMatchException(); + codeStream.dup(); + codeStream.aconst_null(); + codeStream.aconst_null(); + codeStream.invokeJavaLangMatchExceptionConstructor(); + codeStream.athrow(); + } else { // old style using IncompatibleClassChangeError: + this.swich.defaultLabel.place(); + codeStream.newJavaLangIncompatibleClassChangeError(); + codeStream.dup(); + codeStream.invokeJavaLangIncompatibleClassChangeErrorDefaultConstructor(); + codeStream.athrow(); + } + } } + } + + protected final void generateEpilogue(BlockScope currentScope, CodeStream codeStream) { // place the trailing labels (for break and default case) - this.breakLabel.place(); - if (this.defaultCase == null) { - // we want to force an line number entry to get an end position after the switch statement - codeStream.recordPositionsFrom(codeStream.position, this.sourceEnd, true); - defaultCaseLabel.place(); - defaultBranchLabel.place(); + this.swich.breakLabel.place(); + if (this.swich.defaultLabel.position == Label.POS_NOT_SET) { + codeStream.recordPositionsFrom(codeStream.position, this.swich.sourceEnd, true); // force a line number entry to get an end position after the switch + this.swich.defaultLabel.place(); } - codeStream.recordPositionsFrom(pc, this.sourceStart); - } finally { - if (this.scope != null) this.scope.enclosingCase = null; // no longer inside switch case block - } - } - private T[] gatherLabels(CodeStream codeStream, T[] caseLabels, - Function newLabel) - { - for (int i = 0, j = 0, max = this.caseCount; i < max; i++) { - CaseStatement stmt = this.cases[i]; - final Expression[] peeledLabelExpressions = stmt.peeledLabelExpressions(); - int length = peeledLabelExpressions.length; - BranchLabel[] targetLabels = new BranchLabel[length]; - int count = 0; - for (int k = 0; k < length; ++k) { - Expression e = peeledLabelExpressions[k]; - if (e instanceof FakeDefaultLiteral) continue; - targetLabels[count++] = (caseLabels[j++] = newLabel.apply(codeStream)); - if (e == this.totalPattern) - this.defaultCase = stmt; + // May loose some local variable initializations : affecting the local variable attributes + if (this.swich.mergedInitStateIndex != -1) { + codeStream.removeNotDefinitelyAssignedVariables(currentScope, this.swich.mergedInitStateIndex); + codeStream.addDefinitelyAssignedVariables(currentScope, this.swich.mergedInitStateIndex); } - System.arraycopy(targetLabels, 0, stmt.targetLabels = new BranchLabel[count], 0, count); + codeStream.removeVariable(this.swich.selector); + if (this.swich.scope != currentScope) + codeStream.exitUserScope(this.swich.scope); } - return caseLabels; - } - /** - * Switch code generation - * - * @param currentScope org.eclipse.jdt.internal.compiler.lookup.BlockScope - * @param codeStream org.eclipse.jdt.internal.compiler.codegen.CodeStream - */ - @Override - public void generateCode(BlockScope currentScope, CodeStream codeStream) { - if ((this.bits & IsReachable) == 0) - return; - if (this.expression.resolvedType.id == TypeIds.T_JavaLangString && !this.isNonTraditional) { - generateCodeForStringSwitch(currentScope, codeStream); - return; + public final void generateCode(BlockScope currentScope, CodeStream codeStream) { + if ((this.swich.bits & IsReachable) == 0) + return; + try { + int pc = codeStream.position; + generatePrologue(currentScope, codeStream); + generateSwitchBlock(currentScope, codeStream); + generateDefault(currentScope, codeStream); + generateEpilogue(currentScope, codeStream); + codeStream.recordPositionsFrom(pc, this.swich.sourceStart); + } finally { + if (this.swich.scope != null) + this.swich.scope.enclosingCase = null; // no longer inside switch case block + } } - try { - int pc = codeStream.position; - // prepare the labels and constants - this.breakLabel.initialize(codeStream); - CaseLabel[] caseLabels = this.gatherLabels(codeStream, new CaseLabel[this.nConstants], CaseLabel::new); + final static class ClassicSwitchTranslator extends SwitchTranslator { + public ClassicSwitchTranslator(SwitchStatement swich) { + super(swich); + } + } - CaseLabel defaultLabel = new CaseLabel(codeStream); - final boolean hasCases = this.caseCount > 1 || (this.caseCount == 1 && this.defaultCase == null); - if (this.defaultCase != null) { - this.defaultCase.targetLabel = defaultLabel; + final static class StringSwitchTranslator extends SwitchTranslator { + public StringSwitchTranslator(SwitchStatement swich) { + super(swich); } - final TypeBinding resolvedType1 = this.expression.resolvedType; - boolean valueRequired = false; - int constantCount = this.labelExpressions.length; - int [] constants = new int[constantCount]; - if (needPatternDispatchCopy()) { - generateCodeSwitchPatternPrologue(currentScope, codeStream); - valueRequired = true; - for (int i = 0, j = 0, length = this.labelExpressions.length; i < length; ++i) { - if (this.nullCase == null && this.labelExpressions[i].expression == this.totalPattern) - this.labelExpressions[i].index = -1; - constants[i] = this.labelExpressions[i].index - j; - if (this.labelExpressions[i].expression instanceof NullLiteral) - j = 1; // since we yank null out to -1, shift down everything beyond. - } - } else { - for (int i = 0, length = this.labelExpressions.length; i < length; ++i) - constants[i] = this.labelExpressions[i].intValue(); - if (resolvedType1.isEnum()) { - // go through the translation table - codeStream.invoke(Opcodes.OPC_invokestatic, this.synthetic, null /* default declaringClass */); - this.expression.generateCode(currentScope, codeStream, true); - // get enum constant ordinal() - codeStream.invokeEnumOrdinal(resolvedType1.constantPoolName()); - codeStream.iaload(); - if (!hasCases) { - // we can get rid of the generated ordinal value - codeStream.pop(); - } - valueRequired = hasCases; - } else { - 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 + @Override + protected final void generatePrologue(BlockScope currentScope, CodeStream codeStream) { + record StringSwitchCase(int hashKode, String string, BranchLabel label) implements Comparable { + @Override + public int compareTo(StringSwitchCase that) { + return this.hashKode == that.hashKode ? 0 : this.hashKode > that.hashKode ? 1 : -1; // can't use just '-' due to potential overflow/underflow } } - } - // generate the appropriate switch table/lookup bytecode - if (hasCases) { - int[] sortedIndexes = new int[constantCount]; - // we sort the keys to be able to generate the code for tableswitch or lookupswitch - for (int i = 0; i < constantCount; i++) { - sortedIndexes[i] = i; + + BranchLabel[] sourceCaseLabels = this.swich.gatherLabels(codeStream, new BranchLabel[this.swich.nConstants], BranchLabel::new); + final StringSwitchCase [] stringCases = new StringSwitchCase[this.swich.nConstants]; + CaseLabel [] hashCodeCaseLabels = new CaseLabel[this.swich.nConstants]; + int [] hashCodes = new int[this.swich.nConstants]; + for (int i = 0; i < this.swich.nConstants; i++) { + String literal = this.swich.labelExpressions[i].constant.stringValue(); + stringCases[i] = new StringSwitchCase(literal.hashCode(), literal, sourceCaseLabels[i]); + hashCodeCaseLabels[i] = new CaseLabel(codeStream); + } + Arrays.sort(stringCases); + + int uniqHashCount = 0; + int lastHashCode = 0; + for (int i = 0; i < this.swich.nConstants; ++i) { + int hashCode = stringCases[i].hashKode; + if (i == 0 || hashCode != lastHashCode) + lastHashCode = hashCodes[uniqHashCount++] = hashCode; } - int[] localKeysCopy; - System.arraycopy(constants, 0, (localKeysCopy = new int[constantCount]), 0, constantCount); - CodeStream.sort(localKeysCopy, 0, constantCount - 1, sortedIndexes); - int max = localKeysCopy[constantCount - 1]; - int min = localKeysCopy[0]; - if ((long) (constantCount * 2.5) > ((long) max - (long) min)) { - codeStream.tableswitch( - defaultLabel, - min, - max, - constants, - sortedIndexes, - caseLabels); - } else { - codeStream.lookupswitch(defaultLabel, constants, sortedIndexes, caseLabels); + if (uniqHashCount != this.swich.nConstants) { // multiple keys hashed to the same value. + System.arraycopy(hashCodes, 0, hashCodes = new int[uniqHashCount], 0, uniqHashCount); + System.arraycopy(hashCodeCaseLabels, 0, hashCodeCaseLabels = new CaseLabel[uniqHashCount], 0, uniqHashCount); } - codeStream.recordPositionsFrom(codeStream.position, this.expression.sourceEnd); - } else if (valueRequired) { - codeStream.pop(); - } + int[] sortedIndexes = new int[uniqHashCount]; // hash code are sorted already anyways. + for (int i = 0; i < uniqHashCount; i++) + sortedIndexes[i] = i; - // generate the switch block statements - if (this.statements != null) { - for (Statement statement : this.statements) { - if (statement instanceof CaseStatement caseStatement) { - this.scope.enclosingCase = caseStatement; // record entering in a switch case block - if (this.preSwitchInitStateIndex != -1) - codeStream.removeNotDefinitelyAssignedVariables(currentScope, this.preSwitchInitStateIndex); + this.swich.defaultLabel = new CaseLabel(codeStream, true /* allow narrow branch to */); + if (this.swich.defaultCase != null) + this.swich.defaultCase.targetLabel = this.swich.defaultLabel; + + this.swich.breakLabel.initialize(codeStream); + + // generate expression + this.swich.expression.generateCode(currentScope, codeStream, true); + codeStream.store(this.swich.selector, true); // leaves string on operand stack + codeStream.addVariable(this.swich.selector); + codeStream.invokeStringHashCode(); + + final boolean hasCases = this.swich.caseCount > 1 || (this.swich.caseCount == 1 && this.swich.defaultCase == null); + if (hasCases) { + codeStream.lookupswitch(this.swich.defaultLabel, hashCodes, sortedIndexes, hashCodeCaseLabels); + for (int i = 0, j = 0; i < this.swich.nConstants; i++) { + int hashCode = stringCases[i].hashKode; + if (i == 0 || hashCode != lastHashCode) { + lastHashCode = hashCode; + if (i != 0) + codeStream.goto_(this.swich.defaultLabel); + hashCodeCaseLabels[j++].place(); + } + codeStream.load(this.swich.selector); + codeStream.ldc(stringCases[i].string); + codeStream.invokeStringEquals(); + codeStream.ifne(stringCases[i].label); } - statement.generateCode(this.scope, codeStream); - if ((this.switchBits & LabeledRules) != 0 && statement instanceof Block && statement.canCompleteNormally()) - codeStream.goto_(this.breakLabel); + codeStream.goto_(this.swich.defaultLabel); + } else { + codeStream.pop(); } } - boolean needsThrowingDefault = false; - if (this.defaultCase == null) { - // enum: - needsThrowingDefault = resolvedType1.isEnum() && (this instanceof SwitchExpression || this.containsNull); - // pattern switches: - needsThrowingDefault |= isExhaustive(); + } + + final static class EnumSwitchTranslator extends SwitchTranslator { + public EnumSwitchTranslator(SwitchStatement swich) { + super(swich); } - if (needsThrowingDefault) { - // we want to force an line number entry to get an end position after the switch statement - if (this.preSwitchInitStateIndex != -1) { - codeStream.removeNotDefinitelyAssignedVariables(currentScope, this.preSwitchInitStateIndex); - } - /* a default case is not needed for an exhaustive switch expression - * we need to handle the default case to throw an error in order to make the stack map consistent. - * All cases will return a value on the stack except the missing default case. - * There is no returned value for the default case so we handle it with an exception thrown. - */ - if (this.scope.compilerOptions().complianceLevel >= ClassFileConstants.JDK19) { - // since 19 we have MatchException for this - if (this.statements.length > 0 && this.statements[this.statements.length - 1].canCompleteNormally()) - codeStream.goto_(this.breakLabel); // hop, skip and jump over match exception throw. - defaultLabel.place(); - codeStream.newJavaLangMatchException(); - codeStream.dup(); - codeStream.aconst_null(); - codeStream.aconst_null(); - codeStream.invokeJavaLangMatchExceptionConstructor(); - codeStream.athrow(); - } else { - // old style using IncompatibleClassChangeError: - defaultLabel.place(); - codeStream.newJavaLangIncompatibleClassChangeError(); - codeStream.dup(); - codeStream.invokeJavaLangIncompatibleClassChangeErrorDefaultConstructor(); - codeStream.athrow(); - } + } + + final static class PatternSwitchTranslator extends SwitchTranslator { + public PatternSwitchTranslator(SwitchStatement swich) { + super(swich); } - // May loose some local variable initializations : affecting the local variable attributes - if (this.mergedInitStateIndex != -1) { - codeStream.removeNotDefinitelyAssignedVariables(currentScope, this.mergedInitStateIndex); - codeStream.addDefinitelyAssignedVariables(currentScope, this.mergedInitStateIndex); + } + + final static class PrimitivePatternSwitchTranslator extends SwitchTranslator { + public PrimitivePatternSwitchTranslator(SwitchStatement swich) { + super(swich); } - generateCodeSwitchPatternEpilogue(codeStream); - if (this.scope != currentScope) - codeStream.exitUserScope(this.scope); - // place the trailing labels (for break and default case) - this.breakLabel.place(); - if (this.defaultCase == null && !needsThrowingDefault) { - // we want to force an line number entry to get an end position after the switch statement - codeStream.recordPositionsFrom(codeStream.position, this.sourceEnd, true); - defaultLabel.place(); + } + + public static SwitchTranslator newSwitchTranslator(SwitchStatement swich) { + if (swich.expression.resolvedType.id == TypeIds.T_JavaLangString && !swich.isNonTraditional) + return new StringSwitchTranslator(swich); + else if (!swich.needPatternDispatchCopy()) { + if (swich.expression.resolvedType.isEnum()) + return new EnumSwitchTranslator(swich); + else + return new ClassicSwitchTranslator(swich); + } else { + if (swich.isPrimitiveSwitch) + return new PrimitivePatternSwitchTranslator(swich); + else + return new PatternSwitchTranslator(swich); } - codeStream.recordPositionsFrom(pc, this.sourceStart); - } finally { - if (this.scope != null) this.scope.enclosingCase = null; // no longer inside switch case block } } - private void generateCodeSwitchPatternEpilogue(CodeStream codeStream) { - if (needPatternDispatchCopy()) - codeStream.removeVariable(this.selector); + private T[] gatherLabels(CodeStream codeStream, T[] caseLabels, Function newLabel) { + for (int i = 0, j = 0, max = this.caseCount; i < max; i++) { + CaseStatement stmt = this.cases[i]; + T label; + stmt.targetLabel = label = newLabel.apply(codeStream); + for (Expression e : stmt.peeledLabelExpressions()) { + if (e instanceof FakeDefaultLiteral) continue; + caseLabels[j++] = label; + if (e == this.totalPattern) + this.defaultCase = stmt; + } + } + return caseLabels; + } + + @Override + public void generateCode(BlockScope currentScope, CodeStream codeStream) { + newSwitchTranslator(this).generateCode(currentScope, codeStream); } private void generateCodeSwitchPatternPrologue(BlockScope currentScope, CodeStream codeStream) { @@ -1293,7 +1209,9 @@ private void releaseUnusedSecretVariable() { } else if (needPatternDispatchCopy()) { this.selector.useFlag = LocalVariableBinding.USED; this.selector.type = this.expression.resolvedType; - } // else gets released by virtue of not being tagged USED. + } else { + this.selector = null; + } } } protected void reportMissingEnumConstantCase(BlockScope upperScope, FieldBinding enumConstant) { @@ -1408,4 +1326,4 @@ public boolean continueCompletes() { public StringBuilder printExpression(int indent, StringBuilder output) { return printStatement(indent, output); } -} +} \ No newline at end of file diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/BranchLabel.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/BranchLabel.java index 5f2ef70dd47..0360a8032f6 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/BranchLabel.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/BranchLabel.java @@ -211,9 +211,6 @@ void branch() { trackStackDepth(true); } -/* -* No support for wide branches yet -*/ void branchWide() { if (this.delegate != null) { this.delegate.branchWide(); diff --git a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/CaseLabel.java b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/CaseLabel.java index 291fb3b23c2..0ee5938af58 100644 --- a/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/CaseLabel.java +++ b/org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/codegen/CaseLabel.java @@ -16,21 +16,29 @@ public class CaseLabel extends BranchLabel { public int instructionPosition = POS_NOT_SET; + private BranchLabel branchLabel; // doppelganger -/** - * CaseLabel constructor comment. - * @param codeStream org.eclipse.jdt.internal.compiler.codegen.CodeStream - */ public CaseLabel(CodeStream codeStream) { super(codeStream); } +public CaseLabel(CodeStream codeStream, boolean allowNarrowBranch) { + super(codeStream); + if (allowNarrowBranch) + this.branchLabel = new BranchLabel(codeStream); +} + +@Override +void branch() { + this.branchLabel.branch(); +} + /* * Put down a reference to the array at the location in the codestream. * #placeInstruction() must be performed prior to any #branch() */ @Override -void branch() { +void branchWide() { if (this.position == POS_NOT_SET) { addForwardReference(this.codeStream.position); // Leave 4 bytes free to generate the jump offset afterwards @@ -45,14 +53,6 @@ void branch() { trackStackDepth(true); } -/* -* No support for wide branches yet -*/ -@Override -void branchWide() { - branch(); // case label branch is already wide -} - @Override public boolean isCaseLabel() { return true; @@ -77,6 +77,8 @@ public void place() { this.codeStream.addLabel(this); } trackStackDepth(false); + if (this.branchLabel != null) + this.branchLabel.place(); } /* 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 b84e96d4f8a..dcad3e52df3 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 @@ -6348,11 +6348,11 @@ public void lookupswitch(CaseLabel defaultLabel, int[] keys, int[] sortedIndexes this.position++; this.bCodeStream[this.classFileOffset++] = 0; } - defaultLabel.branch(); + defaultLabel.branchWide(); writeSignedWord(length); for (int i = 0; i < length; i++) { writeSignedWord(keys[sortedIndexes[i]]); - casesLabel[sortedIndexes[i]].branch(); + casesLabel[sortedIndexes[i]].branchWide(); } this.lastAbruptCompletion = this.position; // multiway goto, with no fall through } @@ -7519,7 +7519,7 @@ public void tableswitch(CaseLabel defaultLabel, int low, int high, int[] keys, i this.position++; this.bCodeStream[this.classFileOffset++] = 0; } - defaultLabel.branch(); + defaultLabel.branchWide(); writeSignedWord(low); writeSignedWord(high); int i = low, j = 0; @@ -7530,11 +7530,11 @@ public void tableswitch(CaseLabel defaultLabel, int low, int high, int[] keys, i int index = sortedIndexes[j]; int key = keys[index]; if (key == i) { - casesLabel[index].branch(); + casesLabel[index].branchWide(); j++; if (i == high) break; // if high is maxint, then avoids wrapping to minint. } else { - defaultLabel.branch(); + defaultLabel.branchWide(); } i++; }