Skip to content

Commit 91691ad

Browse files
committed
Add switch statement support to the compiler for String type
1 parent 2038d8b commit 91691ad

File tree

2 files changed

+110
-58
lines changed

2 files changed

+110
-58
lines changed

src/compiler/__tests__/tests/switch.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,31 @@ const testCases: testCase[] = [
166166
}
167167
`,
168168
expectedLines: ['Outer Two, Inner One']
169+
},
170+
171+
{
172+
comment: 'Switch with far apart cases',
173+
program: `
174+
public class Main {
175+
public static void main(String[] args) {
176+
int x = 1331;
177+
switch (x) {
178+
case 1:
179+
System.out.println("No");
180+
break;
181+
case 1331:
182+
System.out.println("Yes");
183+
break;
184+
case 999999999:
185+
System.out.println("No");
186+
break;
187+
default:
188+
System.out.println("Default");
189+
}
190+
}
191+
}
192+
`,
193+
expectedLines: ['Yes']
169194
}
170195
]
171196

src/compiler/code-generator.ts

Lines changed: 85 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ function generateStringConversion(valueType: string, cg: CodeGenerator): void {
274274
function hashCode(str: string): number {
275275
let hash = 0;
276276
for (let i = 0; i < str.length; i++) {
277-
hash = (hash * 31 + str.charCodeAt(i)) | 0; // Simulate Java's overflow behavior
277+
hash = ((hash * 31) + str.charCodeAt(i)); // Simulate Java's overflow behavior
278278
}
279279
return hash;
280280
}
@@ -1301,7 +1301,6 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi
13011301
const caseLabels: Label[] = cases.map(() => cg.generateNewLabel());
13021302
const defaultLabel = cg.generateNewLabel();
13031303
const endLabel = cg.generateNewLabel();
1304-
const positionOffset = cg.code.length;
13051304

13061305
// Track the switch statement's end label
13071306
cg.switchLabels.push(endLabel);
@@ -1310,6 +1309,7 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi
13101309
const caseValues: number[] = [];
13111310
const caseLabelMap: Map<number, Label> = new Map();
13121311
let hasDefault = false;
1312+
const positionOffset = cg.code.length;
13131313

13141314
cases.forEach((caseGroup, index) => {
13151315
caseGroup.labels.forEach((label) => {
@@ -1433,33 +1433,25 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi
14331433
cg.code[caseLabelIndex[i]] = caseLabels[i - 1].offset - positionOffset
14341434
}
14351435

1436-
console.debug(cg.code)
1437-
1438-
console.debug(caseLabels[caseLabels.length - 1])
1439-
console.debug(caseLabels.splice(0, caseLabels.length - 1))
1440-
14411436
endLabel.offset = cg.code.length;
14421437

14431438
} else if (resultType === "Ljava/lang/String;") {
1444-
// **String cases**
1439+
// **String Switch Handling**
14451440
const hashCaseMap: Map<number, Label> = new Map();
1446-
const hashCodeVarIndex = cg.maxLocals++;
14471441

1448-
// Generate `hashCode()` call
1442+
// Compute and store hashCode()
14491443
cg.code.push(
14501444
OPCODE.INVOKEVIRTUAL,
14511445
0,
14521446
cg.constantPoolManager.indexMethodrefInfo("java/lang/String", "hashCode", "()I")
14531447
);
14541448

1455-
// const switchCaseExpressionHash = hashCode();
1456-
cg.code.push(OPCODE.ISTORE, hashCodeVarIndex);
1457-
1449+
// Create lookup table for hashCodes
14581450
cases.forEach((caseGroup, index) => {
14591451
caseGroup.labels.forEach((label) => {
14601452
if (label.kind === "CaseLabel") {
14611453
const caseValue = (label.expression as Literal).literalType.value;
1462-
const hashCodeValue = hashCode(caseValue);
1454+
const hashCodeValue = hashCode(caseValue.slice(1, caseValue.length - 1));
14631455
if (!hashCaseMap.has(hashCodeValue)) {
14641456
hashCaseMap.set(hashCodeValue, caseLabels[index]);
14651457
}
@@ -1469,63 +1461,91 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi
14691461
});
14701462
});
14711463

1472-
// **Compare hashCodes**
1473-
hashCaseMap.forEach((label, hashCode) => {
1474-
cg.code.push(OPCODE.ILOAD, hashCodeVarIndex);
1475-
cg.code.push(OPCODE.BIPUSH, hashCode);
1476-
cg.addBranchInstr(OPCODE.IF_ICMPEQ, label);
1477-
});
1464+
const caseLabelIndex: number[] = []
1465+
let indexTracker = cg.code.length;
1466+
const positionOffset = cg.code.length;
14781467

1479-
cg.addBranchInstr(OPCODE.GOTO, defaultLabel);
1468+
// **LOOKUPSWITCH Implementation**
1469+
cg.code.push(OPCODE.LOOKUPSWITCH);
1470+
indexTracker++
14801471

1481-
// **Process case bodies**
1482-
let previousCase: SwitchCase | null = null;
1472+
// Ensure 4-byte alignment
1473+
while (cg.code.length % 4 !== 0) {
1474+
cg.code.push(0);
1475+
indexTracker++
1476+
}
14831477

1484-
const nonDefaultCases = cases.filter((caseGroup) =>
1485-
caseGroup.labels.some((label) => label.kind === "CaseLabel"))
1478+
// Default jump target
1479+
cg.code.push(0, 0, 0, defaultLabel.offset);
1480+
caseLabelIndex.push(indexTracker + 3);
1481+
indexTracker += 4;
14861482

1487-
nonDefaultCases.forEach((caseGroup, index) => {
1488-
caseLabels[index].offset = cg.code.length;
14891483

1490-
// Ensure statements array is always defined
1491-
caseGroup.statements = caseGroup.statements || [];
1484+
// Number of case-value pairs
1485+
cg.code.push((hashCaseMap.size >> 24) & 0xff, (hashCaseMap.size >> 16) & 0xff,
1486+
(hashCaseMap.size >> 8) & 0xff, hashCaseMap.size & 0xff);
1487+
indexTracker += 4;
14921488

1493-
// Handle fallthrough
1494-
if (previousCase && (previousCase.statements?.length ?? 0) === 0) {
1495-
previousCase.labels.push(...caseGroup.labels);
1496-
}
1489+
// Populate LOOKUPSWITCH
1490+
hashCaseMap.forEach((label, hashCode) => {
1491+
cg.code.push(hashCode >> 24, (hashCode >> 16) & 0xff, (hashCode >> 8) & 0xff, hashCode & 0xff);
1492+
cg.code.push(0, 0, 0, label.offset);
1493+
caseLabelIndex.push(indexTracker + 7);
1494+
indexTracker += 8;
1495+
});
14971496

1498-
// Generate string comparison
1499-
const caseValue = caseGroup.labels.find((label): label is CaseLabel => label.kind === "CaseLabel");
1500-
if (caseValue) {
1501-
const caseStr = (caseValue.expression as Literal).literalType.value;
1502-
const caseStrIndex = cg.constantPoolManager.indexStringInfo(caseStr);
1503-
cg.code.push(OPCODE.LDC, caseStrIndex);
1504-
cg.code.push(
1505-
OPCODE.INVOKEVIRTUAL,
1506-
0,
1507-
cg.constantPoolManager.indexMethodrefInfo("java/lang/String", "equals", "(Ljava/lang/Object;)Z")
1508-
);
1509-
const caseEndLabel = cg.generateNewLabel();
1510-
cg.addBranchInstr(OPCODE.IFEQ, caseEndLabel);
1497+
// **Case Handling**
1498+
let previousCase: SwitchCase | null = null;
15111499

1512-
// Compile case statements
1513-
caseGroup.statements.forEach((statement) => {
1514-
const { stackSize } = compile(statement, cg);
1515-
maxStack = Math.max(maxStack, stackSize);
1516-
});
1500+
cases.filter((caseGroup) =>
1501+
caseGroup.labels.some((label) => label.kind === "CaseLabel"))
1502+
.forEach((caseGroup, index) => {
1503+
caseLabels[index].offset = cg.code.length;
15171504

1518-
caseEndLabel.offset = cg.code.length;
1519-
}
1505+
// Ensure statements exist
1506+
caseGroup.statements = caseGroup.statements || [];
15201507

1521-
previousCase = caseGroup;
1522-
});
1508+
// Handle fallthrough
1509+
if (previousCase && (previousCase.statements?.length ?? 0) === 0) {
1510+
previousCase.labels.push(...caseGroup.labels);
1511+
}
15231512

1524-
// **Process default case**
1513+
// **String Comparison for Collisions**
1514+
const caseValue = caseGroup.labels.find((label): label is CaseLabel => label.kind === "CaseLabel");
1515+
if (caseValue) {
1516+
// TODO: check for actual String equality instead of just rely on hashCode equality
1517+
// (see the commented out code below)
1518+
1519+
// const caseStr = (caseValue.expression as Literal).literalType.value;
1520+
// const caseStrIndex = cg.constantPoolManager.indexStringInfo(caseStr);
1521+
1522+
// cg.code.push(OPCODE.LDC, caseStrIndex);
1523+
// cg.code.push(
1524+
// OPCODE.INVOKEVIRTUAL,
1525+
// 0,
1526+
// cg.constantPoolManager.indexMethodrefInfo("java/lang/String", "equals", "(Ljava/lang/Object;)Z")
1527+
// );
1528+
//
1529+
const caseEndLabel = cg.generateNewLabel();
1530+
// cg.addBranchInstr(OPCODE.IFEQ, caseEndLabel);
1531+
1532+
// Compile case statements
1533+
caseGroup.statements.forEach((statement) => {
1534+
const { stackSize } = compile(statement, cg);
1535+
maxStack = Math.max(maxStack, stackSize);
1536+
});
1537+
1538+
caseEndLabel.offset = cg.code.length;
1539+
}
1540+
1541+
previousCase = caseGroup;
1542+
});
1543+
1544+
// **Default Case Handling**
15251545
defaultLabel.offset = cg.code.length;
15261546
const defaultCase = cases.find((caseGroup) =>
1527-
caseGroup.labels.some((label) => label.kind === "DefaultLabel")
1528-
);
1547+
caseGroup.labels.some((label) => label.kind === "DefaultLabel"));
1548+
15291549
if (defaultCase) {
15301550
defaultCase.statements = defaultCase.statements || [];
15311551
defaultCase.statements.forEach((statement) => {
@@ -1534,7 +1554,14 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi
15341554
});
15351555
}
15361556

1557+
cg.code[caseLabelIndex[0]] = caseLabels[caseLabels.length - 1].offset - positionOffset;
1558+
1559+
for (let i = 1; i < caseLabelIndex.length; i++) {
1560+
cg.code[caseLabelIndex[i]] = caseLabels[i - 1].offset - positionOffset
1561+
}
1562+
15371563
endLabel.offset = cg.code.length;
1564+
15381565
} else {
15391566
throw new Error(`Switch statements only support byte, short, int, char, or String types. Found: ${resultType}`);
15401567
}

0 commit comments

Comments
 (0)