Enable type casting#64
Conversation
Coverage report
Show new covered files 🐣
Show files with reduced coverage 🔻
Test suite run success1148 tests passing in 63 suites. Report generated by 🧪jest coverage report action from fe76603 |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces support for CastExpression parsing, type checking, and code generation, alongside implicit type conversion handling during assignments, array initializations, method calls, and binary operations. It also adds string concatenation support and comprehensive test suites. The review feedback highlights critical issues, including an in operator bug on arrays and missing implicit widening in handleImplicitTypeConversion, incorrect CST property usage and blocked class/array casts in CastExpression type checking, potential crashes in handleExplicitTypeConversion for smaller integral types, a null-matching bug for zero-parameter methods, and unsafe constructor.name checks with incorrect char cast restrictions in isCastCompatible.
| function handleImplicitTypeConversion(fromType: string, toType: string, cg: CodeGenerator): number { | ||
| console.debug(`Converting from: ${fromType}, to: ${toType}`) | ||
| if (fromType === toType || toType.replace(/^L|;$/g, '') === 'java/lang/String') { | ||
| return 0; | ||
| } | ||
|
|
||
| if (fromType.startsWith('L') || toType.startsWith('L')) { | ||
| if (areClassTypesCompatible(fromType, toType) || fromType === '') { | ||
| return 0; | ||
| } | ||
| throw new Error(`Unsupported class type conversion: ${fromType} -> ${toType}`) | ||
| } | ||
|
|
||
| const conversionKey = `${fromType}->${toType}` | ||
| if (conversionKey in typeConversionsImplicit) { | ||
| cg.code.push(typeConversionsImplicit[conversionKey]) | ||
| if (!(fromType in ['J', 'D']) && toType in ['J', 'D']) { | ||
| return 1; | ||
| } else if (!(toType in ['J', 'D']) && fromType in ['J', 'D']) { | ||
| return -1; | ||
| } else { | ||
| return 0; | ||
| } | ||
| } else { | ||
| throw new Error(`Unsupported implicit type conversion: ${conversionKey}`) | ||
| } | ||
| } |
There was a problem hiding this comment.
There are two major issues in handleImplicitTypeConversion:
inoperator bug: Theinoperator in JavaScript/TypeScript checks for keys/indices, not values, when used on arrays. Therefore,fromType in ['J', 'D']will evaluate totrueonly iffromTypeis'0'or'1', which is never the case for'J'or'D'. UseArray.prototype.includes()instead.- Implicit widening of smaller types: Implicit widening from smaller integral types (
B,C,S,Z) toI(or other wider types) is not handled, which will cause the compiler to throw anUnsupported implicit type conversionerror for valid Java code. Normalizing these types to'I'first resolves this.
function handleImplicitTypeConversion(fromType: string, toType: string, cg: CodeGenerator): number {
console.debug(`Converting from: ${fromType}, to: ${toType}`)
if (fromType === toType || toType.replace(/^L|;$/g, '') === 'java/lang/String') {
return 0;
}
if (fromType.startsWith('L') || toType.startsWith('L')) {
if (areClassTypesCompatible(fromType, toType) || fromType === '') {
return 0;
}
throw new Error(`Unsupported class type conversion: ${fromType} -> ${toType}`)
}
let normalizedFrom = fromType;
if (['B', 'C', 'S', 'Z'].includes(fromType)) {
normalizedFrom = 'I';
}
if (normalizedFrom === toType) {
return 0;
}
const conversionKey = `${normalizedFrom}->${toType}`
if (conversionKey in typeConversionsImplicit) {
cg.code.push(typeConversionsImplicit[conversionKey])
const isFromWide = ['J', 'D'].includes(normalizedFrom);
const isToWide = ['J', 'D'].includes(toType);
if (!isFromWide && isToWide) {
return 1;
} else if (!isToWide && isFromWide) {
return -1;
} else {
return 0;
}
} else {
throw new Error(`Unsupported implicit type conversion: ${conversionKey}`)
}
}| case 'CastExpression': { | ||
| let castType: Type | TypeCheckerError; | ||
| let expressionType: Type | null = null; | ||
| let expressionResult: Result; | ||
|
|
||
| if ('primitiveType' in node) { | ||
| castType = frame.getType(unannTypeToString(node.primitiveType), node.primitiveType.location); | ||
| } else { | ||
| throw new Error('Invalid CastExpression: Missing type information.'); | ||
| } | ||
|
|
||
| if (castType instanceof TypeCheckerError) { | ||
| return newResult(null, [castType]); | ||
| } | ||
|
|
||
| if ('unaryExpression' in node) { | ||
| expressionResult = typeCheckBody(node.unaryExpression, frame); | ||
| } else { | ||
| throw new Error('Invalid CastExpression: Missing expression.'); | ||
| } | ||
|
|
||
| if (expressionResult.hasErrors) { | ||
| return expressionResult; | ||
| } | ||
|
|
||
| expressionType = expressionResult.currentType; | ||
| if (!expressionType) { | ||
| throw new Error('Expression in cast should have a type.'); | ||
| } | ||
|
|
||
| if ( | ||
| (castType instanceof PrimitiveType && expressionType instanceof PrimitiveType) | ||
| ) { | ||
| if (!isCastCompatible(expressionType, castType)) { | ||
| return newResult(null, [ | ||
| new IncompatibleTypesError(node.location), | ||
| ]); | ||
| } | ||
| } else { | ||
| return newResult(null, [ | ||
| new IncompatibleTypesError(node.location), | ||
| ]); | ||
| } | ||
|
|
||
| // If the cast is valid, return the target type | ||
| return newResult(castType); | ||
| } |
There was a problem hiding this comment.
There are two major issues in the CastExpression type checking block:
- Incorrect property names: The AST node
CastExpressionhastypeandexpressionproperties, but the code checks forprimitiveTypeandunaryExpression(which are CST properties). This causes the type checker to always throw a runtime error. - Class and Array casts blocked: The check restricts casts only to
PrimitiveType, completely blockingClassTypeandArrayTypecasts even thoughisCastCompatiblesupports them. CallingisCastCompatibledirectly resolves this.
case 'CastExpression': {
const castNode = node as CastExpression;
const castType = frame.getType(unannTypeToString(castNode.type), castNode.type.location);
if (castType instanceof TypeCheckerError) {
return newResult(null, [castType]);
}
const expressionResult = typeCheckBody(castNode.expression, frame);
if (expressionResult.hasErrors) {
return expressionResult;
}
const expressionType = expressionResult.currentType;
if (!expressionType) {
throw new Error('Expression in cast should have a type.');
}
if (!isCastCompatible(expressionType, castType)) {
return newResult(null, [
new IncompatibleTypesError(castNode.location),
]);
}
return newResult(castType);
}| function handleExplicitTypeConversion(fromType: string, toType: string, cg: CodeGenerator) { | ||
| if (fromType === toType) { | ||
| return; | ||
| } | ||
| const conversionKey = `${fromType}->${toType}`; | ||
| if (conversionKey in typeConversions) { | ||
| cg.code.push(typeConversions[conversionKey]); | ||
| } else { | ||
| throw new Error(`Unsupported explicit type conversion: ${conversionKey}`); | ||
| } | ||
| } |
There was a problem hiding this comment.
In the JVM, smaller integral types (B, C, S, Z) are already represented as int (I) on the operand stack. Converting them to I is a no-op. However, handleExplicitTypeConversion will throw an error because these conversions are not defined in typeConversions. Adding a guard to treat these as no-ops prevents compiler crashes on valid casts (e.g., casting byte to short or int).
function handleExplicitTypeConversion(fromType: string, toType: string, cg: CodeGenerator) {
if (fromType === toType) {
return;
}
if (['B', 'C', 'S', 'Z'].includes(fromType) && toType === 'I') {
return;
}
const conversionKey = `${fromType}->${toType}`;
if (conversionKey in typeConversions) {
cg.code.push(typeConversions[conversionKey]);
} else {
throw new Error(`Unsupported explicit type conversion: ${conversionKey}`);
}
}| 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}` | ||
| ) | ||
| } |
There was a problem hiding this comment.
If a method has no parameters, paramDescriptor.match(...) returns null. In that case, the check params && params.length !== n.argumentList.length will evaluate to null (falsy) and will not throw an error if arguments are incorrectly passed to a zero-argument method. This leads to a cryptic Unsupported implicit type conversion error later. Defaulting params to an empty array [] resolves this cleanly.
| 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}` | |
| ) | |
| } | |
| 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.length !== n.argumentList.length) { | |
| throw new Error( | |
| `Parameter mismatch: expected ${params.length}, got ${n.argumentList.length}` | |
| ) | |
| } |
| const isCastCompatible = (fromType: Type, toType: Type): boolean => { | ||
| // Handle primitive type compatibility | ||
| if (fromType instanceof PrimitiveType && toType instanceof PrimitiveType) { | ||
| const fromName = fromType.constructor.name; | ||
| const toName = toType.constructor.name; | ||
|
|
||
| console.log(fromName, toName); | ||
|
|
||
| return !(fromName === 'char' && toName !== 'int'); | ||
| } | ||
|
|
||
| // Handle class type compatibility | ||
| if (fromType instanceof ClassType && toType instanceof ClassType) { | ||
| // Allow upcasts (base class to derived class) or downcasts (derived class to base class) | ||
| return fromType.canBeAssigned(toType) || toType.canBeAssigned(fromType); | ||
| } | ||
|
|
||
| // Handle array type compatibility | ||
| if (fromType instanceof ArrayType && toType instanceof ArrayType) { | ||
| // Ensure the content types are compatible | ||
| return isCastCompatible(fromType.getContentType(), toType.getContentType()); | ||
| } | ||
|
|
||
| // Disallow other cases by default | ||
| return false; | ||
| }; |
There was a problem hiding this comment.
There are two issues here:
constructor.namebug: Relying onconstructor.namefor type checking is unsafe because class names can be minified or altered in production builds, breaking the type checker. Useinstanceofchecks instead.- Incorrect
charcast restriction: In Java, any primitive numeric type can be cast to any other primitive numeric type. Restrictingcharcasts only tointis incorrect and prevents valid casts likechartobyteorshort.
const isCastCompatible = (fromType: Type, toType: Type): boolean => {
// Handle primitive type compatibility
if (fromType instanceof PrimitiveType && toType instanceof PrimitiveType) {
if (fromType instanceof Boolean || toType instanceof Boolean) {
return fromType instanceof Boolean && toType instanceof Boolean;
}
return true;
}
// Handle class type compatibility
if (fromType instanceof ClassType && toType instanceof ClassType) {
// Allow upcasts (base class to derived class) or downcasts (derived class to base class)
return fromType.canBeAssigned(toType) || toType.canBeAssigned(fromType);
}
// Handle array type compatibility
if (fromType instanceof ArrayType && toType instanceof ArrayType) {
// Ensure the content types are compatible
return isCastCompatible(fromType.getContentType(), toType.getContentType());
}
// Disallow other cases by default
return false;
};
Enable type casting to support code as follows: