Skip to content

Enable type conversions#63

Open
AprupKale wants to merge 11 commits into
mainfrom
type-conversions
Open

Enable type conversions#63
AprupKale wants to merge 11 commits into
mainfrom
type-conversions

Conversation

@AprupKale
Copy link
Copy Markdown
Contributor

Fixes #56

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

github-actions Bot commented Jan 21, 2025

Coverage report

St.
Category Percentage Covered / Total
🟡 Statements
74% (+0.05% 🔼)
7259/9809
🟡 Branches
60.43% (+0.04% 🔼)
2410/3988
🟡 Functions
69.55% (+0.1% 🔼)
1293/1859
🟡 Lines
74.97% (+0.04% 🔼)
6844/9129
Show new covered files 🐣
St.
File Statements Branches Functions Lines
🟢
... / assignmentExpression.test.ts
100% 100% 100% 100%
Show files with reduced coverage 🔻
St.
File Statements Branches Functions Lines
🟢
... / code-generator.ts
91.51% (-2.9% 🔻)
77.45% (-7.2% 🔻)
93.62% (+0.44% 🔼)
91.75% (-2.99% 🔻)

Test suite run success

1136 tests passing in 63 suites.

Report generated by 🧪jest coverage report action from 2a2cc0b

@AprupKale AprupKale requested a review from 1001mei January 28, 2025 07:51
Comment on lines +213 to +215
if (areClassTypesCompatible(fromType, toType) || fromType === '') {
return 0;
}
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.

When will fromType be empty?

Comment on lines +768 to +772
const methodInfo = symbolInfos[symbolInfos.length - 1] as MethodInfos
if (!methodInfo || methodInfo.length === 0) {
throw new Error(`No method information found for ${n.identifier}`)
}

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.

perhaps can rename methodInfo to methodInfos, then can remove the methodInfos in line 796 to remove duplication

Comment on lines +773 to +789
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)
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.

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);
Copy link
Copy Markdown
Contributor

@1001mei 1001mei Feb 8, 2025

Choose a reason for hiding this comment

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

can remove this commented out line~

@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 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.

Comment on lines +986 to +1001
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';
}
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

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';
        }

Comment on lines +1013 to +1020
if (leftType !== 'I') {
cg.code.fill(typeConversionsImplicit[conversionKeyLeft],
insertConversionIndex, insertConversionIndex + 1)
}
if (rightType !== 'I') {
cg.code.push(typeConversionsImplicit[conversionKeyRight])
}
finalType = 'I';
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

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

Comment on lines +222 to +225
if (!(fromType in ['J', 'D']) && toType in ['J', 'D']) {
return 1;
} else if (!(toType in ['J', 'D']) && fromType in ['J', 'D']) {
return -1;
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

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.

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

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.

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

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.

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

Only positive stack size changes should be added to the peak stack size calculation to avoid underestimating the stack size when stackSizeChange is negative.

Suggested change
maxStack = Math.max(maxStack, maxStack + rhsSize + stackSizeChange)
maxStack = Math.max(maxStack, maxStack + rhsSize + Math.max(0, stackSizeChange))

Comment on lines +778 to +782
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.

medium

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

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.

Compiler: type conversion

3 participants