Enable switch statements#67
Conversation
…int, byte, short, char)
Coverage report
Show new covered files 🐣
Show files with reduced coverage 🔻
Test suite run success1159 tests passing in 64 suites. Report generated by 🧪jest coverage report action from 91691ad |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces support for parsing, type-checking, and compiling Java-style CastExpression and SwitchStatement constructs. Key changes include updating the PEG.js grammar, extending AST types, implementing AST extractors, handling implicit and explicit type conversions, and generating JVM bytecode for switch statements (including TABLESWITCH, LOOKUPSWITCH, and string switches). Comprehensive test suites have also been added. The review feedback highlights several critical issues: a bug in the implicit type conversion logic where the in operator is incorrectly used instead of Array.prototype.includes(), unsafe deep property access in the expression extractor that could lead to crashes, and a parsing issue where parseInt is incorrectly applied to character literals in switch cases. Additionally, it is recommended to clean up leftover console.log statements and commented-out code blocks.
| if (!(fromType in ['J', 'D']) && toType in ['J', 'D']) { | ||
| return 1; | ||
| } else if (!(toType in ['J', 'D']) && fromType in ['J', 'D']) { | ||
| return -1; | ||
| } else { |
There was a problem hiding this comment.
The in operator in JavaScript/TypeScript checks for the presence of a property/index in an object or array, not the presence of a value in an array. For example, 'J' in ['J', 'D'] evaluates to false because the array only has indices '0' and '1'. This will cause the implicit type conversion logic to fail or behave incorrectly.
Use Array.prototype.includes() instead to check if the array contains the type.
| if (!(fromType in ['J', 'D']) && toType in ['J', 'D']) { | |
| return 1; | |
| } else if (!(toType in ['J', 'D']) && fromType in ['J', 'D']) { | |
| return -1; | |
| } else { | |
| if (!['J', 'D'].includes(fromType) && ['J', 'D'].includes(toType)) { | |
| return 1; | |
| } else if (!['J', 'D'].includes(toType) && ['J', 'D'].includes(fromType)) { | |
| return -1; | |
| } else { |
| cases.forEach((caseGroup, index) => { | ||
| caseGroup.labels.forEach((label) => { | ||
| if (label.kind === "CaseLabel") { | ||
| const value = parseInt((label.expression as Literal).literalType.value); |
There was a problem hiding this comment.
Using parseInt directly on the literal value will fail for character literals (e.g., 'A'), returning NaN. This will break switch statements over char types.
You should check if the literal is a CharacterLiteral and use charCodeAt(0) to get its integer value, or handle it appropriately based on the literal kind.
| const value = parseInt((label.expression as Literal).literalType.value); | |
| const literal = label.expression as Literal; | |
| const value = literal.literalType.kind === "CharacterLiteral" | |
| ? literal.literalType.value.charCodeAt(0) | |
| : parseInt(literal.literalType.value); |
| if (ctx.primary[0].children.primaryPrefix[0].children.castExpression) { | ||
| return this.visit(ctx.primary[0].children.primaryPrefix[0].children.castExpression); | ||
| } |
There was a problem hiding this comment.
This expression performs deep nested property access (ctx.primary[0].children.primaryPrefix[0].children.castExpression) without checking if the intermediate arrays or objects exist. If ctx.primary is empty, or primaryPrefix is empty/undefined, this will throw a TypeError and crash the parser.
Use optional chaining to safely check for the existence of these properties.
| if (ctx.primary[0].children.primaryPrefix[0].children.castExpression) { | |
| return this.visit(ctx.primary[0].children.primaryPrefix[0].children.castExpression); | |
| } | |
| if (ctx.primary?.[0]?.children?.primaryPrefix?.[0]?.children?.castExpression) { | |
| return this.visit(ctx.primary[0].children.primaryPrefix[0].children.castExpression); | |
| } |
| switchBlockStatementGroup(ctx: SwitchBlockStatementGroupCtx): SwitchCase { | ||
| const blockStatementExtractor = new BlockStatementExtractor(); | ||
|
|
||
| console.log(ctx.switchLabel) |
| // switchLabel(ctx: SwitchLabelCtx): CaseLabel | DefaultLabel { | ||
| // // Check if the context contains a "case" label | ||
| // if (ctx.caseOrDefaultLabel?.[0]?.children?.Case) { | ||
| // const expressionExtractor = new ExpressionExtractor(); | ||
| // // @ts-ignore | ||
| // const expressionCtx = ctx.caseOrDefaultLabel[0].children.caseLabelElement[0] | ||
| // .children.caseConstant[0].children.ternaryExpression[0].children; | ||
| // | ||
| // // Ensure the expression context is valid before proceeding | ||
| // if (!expressionCtx) { | ||
| // throw new Error("Invalid Case expression in switch label"); | ||
| // } | ||
| // | ||
| // const expression = expressionExtractor.ternaryExpression(expressionCtx); | ||
| // | ||
| // return { | ||
| // kind: "CaseLabel", | ||
| // expression: expression, | ||
| // }; | ||
| // } | ||
| // | ||
| // // Check if the context contains a "default" label | ||
| // if (ctx.caseOrDefaultLabel?.[0]?.children?.Default) { | ||
| // return { kind: "DefaultLabel" }; | ||
| // } | ||
| // | ||
| // // Throw an error if the context does not match expected patterns | ||
| // throw new Error("Invalid switch label: Neither 'case' nor 'default' found"); | ||
| // } |
| // function generateBooleanConversion(type: string, cg: CodeGenerator): number { | ||
| // let stackChange = 0; // Tracks changes to the stack size | ||
| // | ||
| // switch (type) { | ||
| // case 'I': // int | ||
| // case 'B': // byte | ||
| // case 'S': // short | ||
| // case 'C': // char | ||
| // // For integer-like types, compare with zero | ||
| // cg.code.push(OPCODE.ICONST_0); // Push 0 | ||
| // stackChange += 1; // `ICONST_0` pushes a value onto the stack | ||
| // cg.code.push(OPCODE.IF_ICMPNE); // Compare and branch | ||
| // stackChange -= 2; // `IF_ICMPNE` consumes two values from the stack | ||
| // break; | ||
| // | ||
| // case 'J': // long | ||
| // // For long, compare with zero | ||
| // cg.code.push(OPCODE.LCONST_0); // Push 0L | ||
| // stackChange += 2; // `LCONST_0` pushes two values onto the stack (long takes 2 slots) | ||
| // cg.code.push(OPCODE.LCMP); // Compare top two longs | ||
| // stackChange -= 4; // `LCMP` consumes four values (two long operands) and pushes one result | ||
| // cg.code.push(OPCODE.IFNE); // If not equal, branch | ||
| // stackChange -= 1; // `IFNE` consumes one value (the comparison result) | ||
| // break; | ||
| // | ||
| // case 'F': // float | ||
| // // For float, compare with zero | ||
| // cg.code.push(OPCODE.FCONST_0); // Push 0.0f | ||
| // stackChange += 1; // `FCONST_0` pushes a value onto the stack | ||
| // cg.code.push(OPCODE.FCMPL); // Compare top two floats | ||
| // stackChange -= 2; // `FCMPL` consumes two values (float operands) and pushes one result | ||
| // cg.code.push(OPCODE.IFNE); // If not equal, branch | ||
| // stackChange -= 1; // `IFNE` consumes one value (the comparison result) | ||
| // break; | ||
| // | ||
| // case 'D': // double | ||
| // // For double, compare with zero | ||
| // cg.code.push(OPCODE.DCONST_0); // Push 0.0d | ||
| // stackChange += 2; // `DCONST_0` pushes two values onto the stack (double takes 2 slots) | ||
| // cg.code.push(OPCODE.DCMPL); // Compare top two doubles | ||
| // stackChange -= 4; // `DCMPL` consumes four values (two double operands) and pushes one result | ||
| // cg.code.push(OPCODE.IFNE); // If not equal, branch | ||
| // stackChange -= 1; // `IFNE` consumes one value (the comparison result) | ||
| // break; | ||
| // | ||
| // case 'Z': // boolean | ||
| // // Already a boolean, no conversion needed | ||
| // break; | ||
| // | ||
| // default: | ||
| // throw new Error(`Cannot convert type ${type} to boolean.`); | ||
| // } | ||
| // | ||
| // return stackChange; // Return the net change in stack size | ||
| // } |
Enable switch statements to support code as follows: