Skip to content

Enable type casting#64

Open
AprupKale wants to merge 13 commits into
mainfrom
type-casts
Open

Enable type casting#64
AprupKale wants to merge 13 commits into
mainfrom
type-casts

Conversation

@AprupKale
Copy link
Copy Markdown
Contributor

@AprupKale AprupKale commented Jan 26, 2025

Enable type casting to support code as follows:

class Main {
    public static void main(String[] args) {
        int a = 65;
        char c = (char) a;
        System.out.println(c);
    }
}

@AprupKale AprupKale added the enhancement New feature or request label Jan 26, 2025
@AprupKale AprupKale self-assigned this Jan 26, 2025
@github-actions
Copy link
Copy Markdown

Coverage report

St.
Category Percentage Covered / Total
🟡 Statements
73.9% (-0.05% 🔻)
7315/9898
🟡 Branches
60.31% (-0.08% 🔻)
2442/4049
🟡 Functions
69.68% (+0.23% 🔼)
1301/1867
🟡 Lines
74.85% (-0.09% 🔻)
6897/9215
Show new covered files 🐣
St.
File Statements Branches Functions Lines
🟢
... / assignmentExpression.test.ts
100% 100% 100% 100%
🟢
... / castExpression.test.ts
100% 100% 100% 100%
Show files with reduced coverage 🔻
St.
File Statements Branches Functions Lines
🟡
... / index.ts
70.8% (-3.57% 🔻)
48% (-5.01% 🔻)
91.67% (-3.99% 🔻)
79.3% (-4.94% 🔻)
🟢
... / code-generator.ts
91.2% (-3.21% 🔻)
77.55% (-7.11% 🔻)
93.88% (+0.7% 🔼)
91.43% (-3.32% 🔻)

Test suite run success

1148 tests passing in 63 suites.

Report generated by 🧪jest coverage report action from fe76603

@martin-henz
Copy link
Copy Markdown
Member

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +206 to +232
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}`)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There are two major issues in handleImplicitTypeConversion:

  1. in operator bug: The in operator in JavaScript/TypeScript checks for keys/indices, not values, when used on arrays. Therefore, fromType in ['J', 'D'] will evaluate to true only if fromType is '0' or '1', which is never the case for 'J' or 'D'. Use Array.prototype.includes() instead.
  2. Implicit widening of smaller types: Implicit widening from smaller integral types (B, C, S, Z) to I (or other wider types) is not handled, which will cause the compiler to throw an Unsupported implicit type conversion error 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}`)
  }
}

Comment on lines +223 to +269
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);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There are two major issues in the CastExpression type checking block:

  1. Incorrect property names: The AST node CastExpression has type and expression properties, but the code checks for primitiveType and unaryExpression (which are CST properties). This causes the type checker to always throw a runtime error.
  2. Class and Array casts blocked: The check restricts casts only to PrimitiveType, completely blocking ClassType and ArrayType casts even though isCastCompatible supports them. Calling isCastCompatible directly 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);
    }

Comment on lines +234 to +244
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}`);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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}`);
  }
}

Comment on lines +773 to +782
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}`
)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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}`
)
}

Comment on lines +66 to +91
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;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There are two issues here:

  1. constructor.name bug: Relying on constructor.name for type checking is unsafe because class names can be minified or altered in production builds, breaking the type checker. Use instanceof checks instead.
  2. Incorrect char cast restriction: In Java, any primitive numeric type can be cast to any other primitive numeric type. Restricting char casts only to int is incorrect and prevents valid casts like char to byte or short.
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;
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants