Enable type conversions#63
Conversation
Coverage report
Show new covered files 🐣
Show files with reduced coverage 🔻
Test suite run success1136 tests passing in 63 suites. Report generated by 🧪jest coverage report action from 2a2cc0b |
| if (areClassTypesCompatible(fromType, toType) || fromType === '') { | ||
| return 0; | ||
| } |
There was a problem hiding this comment.
When will fromType be empty?
| const methodInfo = symbolInfos[symbolInfos.length - 1] as MethodInfos | ||
| if (!methodInfo || methodInfo.length === 0) { | ||
| throw new Error(`No method information found for ${n.identifier}`) | ||
| } | ||
|
|
There was a problem hiding this comment.
perhaps can rename methodInfo to methodInfos, then can remove the methodInfos in line 796 to remove duplication
| const fullDescriptor = methodInfo[0].typeDescriptor // Full descriptor, e.g., "(Ljava/lang/String;C)V" | ||
| const paramDescriptor = fullDescriptor.slice(1, fullDescriptor.indexOf(')')) // Extract "Ljava/lang/String;C" | ||
| const params = paramDescriptor.match(/(\[+[BCDFIJSZ])|(\[+L[^;]+;)|[BCDFIJSZ]|L[^;]+;/g) | ||
|
|
||
| // Parse individual parameter types | ||
| if (params && params.length !== n.argumentList.length) { | ||
| throw new Error( | ||
| `Parameter mismatch: expected ${params?.length || 0}, got ${n.argumentList.length}` | ||
| ) | ||
| } | ||
|
|
||
| n.argumentList.forEach((x, i) => { | ||
| const argCompileResult = compile(x, cg) | ||
| maxStack = Math.max(maxStack, i + 1 + argCompileResult.stackSize) | ||
|
|
||
| const expectedType = params?.[i] // Expected parameter type | ||
| const stackSizeChange = handleImplicitTypeConversion(argCompileResult.resultType, expectedType ?? '', cg) | ||
| maxStack = Math.max(maxStack, i + 1 + argCompileResult.stackSize + stackSizeChange) |
There was a problem hiding this comment.
i assume your scope are not supporting method overloading? otherwise might have issues of converting type unnecessary because here it's always converted to match the first method in methodInfo array.
| cg.code.push(typeConversionsImplicit[conversionKeyRight]) | ||
| finalType = 'D'; | ||
| } else if (leftType !== 'F' && rightType === 'F') { | ||
| // handleImplicitTypeConversion(leftType, 'F', cg); |
There was a problem hiding this comment.
can remove this commented out line~
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces support for implicit type conversions, string concatenation, and unary operations for various types (long, float, double) in the compiler's code generator, along with corresponding test cases. The reviewer identified several critical and high-severity issues in the code generator: (1) incorrect use of the JavaScript in operator on arrays instead of includes(), (2) potential undefined bytecode generation when implicit conversions for smaller types are not found in typeConversionsImplicit, (3) underestimation of peak stack sizes when stackSizeChange is negative, and (4) a bug in the parameter mismatch check where a method with no parameters could bypass the check if arguments are provided.
| if (leftType !== 'D' && rightType === 'D') { | ||
| cg.code.fill(typeConversionsImplicit[conversionKeyLeft], | ||
| insertConversionIndex, insertConversionIndex + 1) | ||
| finalType = 'D'; | ||
| } else if (leftType === 'D' && rightType !== 'D') { | ||
| cg.code.push(typeConversionsImplicit[conversionKeyRight]) | ||
| finalType = 'D'; | ||
| } else if (leftType !== 'F' && rightType === 'F') { | ||
| // handleImplicitTypeConversion(leftType, 'F', cg); | ||
| cg.code.fill(typeConversionsImplicit[conversionKeyLeft], | ||
| insertConversionIndex, insertConversionIndex + 1) | ||
| finalType = 'F'; | ||
| } else if (leftType === 'F' && rightType !== 'F') { | ||
| cg.code.push(typeConversionsImplicit[conversionKeyRight]) | ||
| finalType = 'F'; | ||
| } |
There was a problem hiding this comment.
Similar to the integer promotion block, if a smaller type like byte ('B') is implicitly converted to double ('D') or float ('F'), the conversion key (e.g., 'B->D') will not be found in typeConversionsImplicit, resulting in undefined being pushed or filled into the bytecode. You should check if the conversion opcode is defined before pushing or filling it.
if (leftType !== 'D' && rightType === 'D') {
const conversionOp = typeConversionsImplicit[conversionKeyLeft];
if (conversionOp !== undefined) {
cg.code.fill(conversionOp, insertConversionIndex, insertConversionIndex + 1);
}
finalType = 'D';
} else if (leftType === 'D' && rightType !== 'D') {
const conversionOp = typeConversionsImplicit[conversionKeyRight];
if (conversionOp !== undefined) {
cg.code.push(conversionOp);
}
finalType = 'D';
} else if (leftType !== 'F' && rightType === 'F') {
const conversionOp = typeConversionsImplicit[conversionKeyLeft];
if (conversionOp !== undefined) {
cg.code.fill(conversionOp, insertConversionIndex, insertConversionIndex + 1);
}
finalType = 'F';
} else if (leftType === 'F' && rightType !== 'F') {
const conversionOp = typeConversionsImplicit[conversionKeyRight];
if (conversionOp !== undefined) {
cg.code.push(conversionOp);
}
finalType = 'F';
}| if (leftType !== 'I') { | ||
| cg.code.fill(typeConversionsImplicit[conversionKeyLeft], | ||
| insertConversionIndex, insertConversionIndex + 1) | ||
| } | ||
| if (rightType !== 'I') { | ||
| cg.code.push(typeConversionsImplicit[conversionKeyRight]) | ||
| } | ||
| finalType = 'I'; |
There was a problem hiding this comment.
If leftType or rightType is a smaller type like byte ('B'), short ('S'), or char ('C'), they are loaded as int ('I') on the JVM stack. However, their resultType is still returned as 'B', 'S', or 'C'. This causes conversionKeyLeft or conversionKeyRight to be something like 'B->I', which is not defined in typeConversionsImplicit. As a result, typeConversionsImplicit[conversionKeyLeft] returns undefined, which is then filled into cg.code, leading to invalid bytecode generation. You should check if the conversion opcode is defined before pushing or filling it.
if (leftType !== 'I') {
const conversionOp = typeConversionsImplicit[conversionKeyLeft];
if (conversionOp !== undefined) {
cg.code.fill(conversionOp, insertConversionIndex, insertConversionIndex + 1);
}
}
if (rightType !== 'I') {
const conversionOp = typeConversionsImplicit[conversionKeyRight];
if (conversionOp !== undefined) {
cg.code.push(conversionOp);
}
}| if (!(fromType in ['J', 'D']) && toType in ['J', 'D']) { | ||
| return 1; | ||
| } else if (!(toType in ['J', 'D']) && fromType in ['J', 'D']) { | ||
| return -1; |
There was a problem hiding this comment.
The in operator in JavaScript/TypeScript checks for the presence of property names/indices, not values, when used on an array. Therefore, fromType in ['J', 'D'] will check if 'J' or 'D' is an index of the array (which evaluates to false), rather than checking if the array contains the value. Use Array.prototype.includes() instead.
| if (!(fromType in ['J', 'D']) && toType in ['J', 'D']) { | |
| return 1; | |
| } else if (!(toType in ['J', 'D']) && fromType in ['J', 'D']) { | |
| return -1; | |
| if (!['J', 'D'].includes(fromType) && ['J', 'D'].includes(toType)) { | |
| return 1; | |
| } else if (!['J', 'D'].includes(toType) && ['J', 'D'].includes(fromType)) { | |
| return -1; |
| maxStack = Math.max(maxStack, compile(vi, cg).stackSize) | ||
| const { stackSize: initializerStackSize, resultType: initializerType } = compile(vi, cg) | ||
| const stackSizeChange = handleImplicitTypeConversion(initializerType, variableInfo.typeDescriptor, cg) | ||
| maxStack = Math.max(maxStack, initializerStackSize + stackSizeChange) |
There was a problem hiding this comment.
When calculating maxStack, if stackSizeChange is negative (for example, during a widening conversion from long to float where stackSizeChange is -1), adding stackSizeChange directly to the expression's stack size will underestimate the peak stack size. The peak stack size is reached before the conversion instruction is executed (when the long value is still on the stack). To prevent stack underflow/overflow errors at runtime, you should only add positive stack size changes to the peak stack calculation.
| maxStack = Math.max(maxStack, initializerStackSize + stackSizeChange) | |
| maxStack = Math.max(maxStack, initializerStackSize + Math.max(0, stackSizeChange)) |
| maxStack = 1 + compile(right, cg).stackSize | ||
| const { stackSize: rhsSize, resultType: rhsType } = compile(right, cg) | ||
| const stackSizeChange = handleImplicitTypeConversion(rhsType, info.typeDescriptor, cg) | ||
| maxStack = Math.max(maxStack, 1 + rhsSize + stackSizeChange) |
There was a problem hiding this comment.
Similar to other stack size calculations, if stackSizeChange is negative (e.g., converting long to float), adding it directly will underestimate the peak stack size. Only positive stack size changes should be added.
| maxStack = Math.max(maxStack, 1 + rhsSize + stackSizeChange) | |
| maxStack = Math.max(maxStack, 1 + rhsSize + Math.max(0, stackSizeChange)) |
| const { stackSize: rhsSize, resultType: rhsType } = compile(right, cg) | ||
| const stackSizeChange = handleImplicitTypeConversion(rhsType, fieldInfo.typeDescriptor, cg) | ||
|
|
||
| maxStack = Math.max(maxStack, maxStack + rhsSize + stackSizeChange) |
There was a problem hiding this comment.
Only positive stack size changes should be added to the peak stack size calculation to avoid underestimating the stack size when stackSizeChange is negative.
| maxStack = Math.max(maxStack, maxStack + rhsSize + stackSizeChange) | |
| maxStack = Math.max(maxStack, maxStack + rhsSize + Math.max(0, stackSizeChange)) |
| if (params && params.length !== n.argumentList.length) { | ||
| throw new Error( | ||
| `Parameter mismatch: expected ${params?.length || 0}, got ${n.argumentList.length}` | ||
| ) | ||
| } |
There was a problem hiding this comment.
If a method has no parameters, paramDescriptor is empty, and params (the result of the regex match) will be null. If the method is called with arguments (i.e., n.argumentList.length > 0), the condition params && params.length !== n.argumentList.length will evaluate to null (falsy), and the compiler will not throw a parameter mismatch error. You should calculate the expected parameter count safely and check against it.
// Parse individual parameter types
const expectedParamCount = params ? params.length : 0;
if (expectedParamCount !== n.argumentList.length) {
throw new Error(
'Parameter mismatch: expected ' + expectedParamCount + ', got ' + n.argumentList.length
)
}
Fixes #56