From 38a2408ca17b6974f85624253b2e77de24745556 Mon Sep 17 00:00:00 2001 From: kjw142857 <122250318+kjw142857@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:21:25 +0800 Subject: [PATCH 1/4] Implement exception handling in java-slang (#96) * jvm changes * include try/catch/finally support in code generator * fix try statement logic * add parser and type checker integration * fix finally bug * add tests and fix syntax error * Patch grammar logic for throws keyword * Revert "Patch grammar logic for throws keyword" This reverts commit 8e933b623ec015d1d381039bd4473e9515f82d95. * Patch grammar logic for throws keyword * Add fix for execption table finally logic * Add more tests * fix finally bug and missing test imports --------- Co-authored-by: Martin Henz --- src/ast/__tests__/statement-extractor.test.ts | 114 ++++++++- src/ast/astExtractor/class-extractor.ts | 9 +- src/ast/astExtractor/statement-extractor.ts | 85 ++++++- src/ast/types/blocks-and-statements.ts | 43 +++- .../__tests__/__utils__/test-utils.ts | 30 ++- src/compiler/__tests__/try.test.ts | 202 ++++++++++++++++ src/compiler/code-generator.ts | 224 +++++++++++++++++- src/compiler/grammar.pegjs | 84 ++++++- src/compiler/grammar.ts | 84 ++++++- src/compiler/import/lib-info.ts | 6 + src/jvm/__tests__/thread.ts | 40 ++++ src/jvm/exception-table.ts | 60 +++-- src/jvm/types/class/Attributes.ts | 12 +- src/jvm/types/class/Method.ts | 3 +- .../disassembler/utils/readAttributes.ts | 2 +- .../checker/__tests__/tryStatement.test.ts | 63 ++++- src/types/checker/environment.ts | 21 ++ src/types/checker/index.ts | 108 +++++++-- src/types/errors.ts | 6 + src/types/typeFactories/methodFactory.ts | 15 ++ src/types/types/methods.ts | 9 + src/types/types/throws.ts | 15 +- 22 files changed, 1148 insertions(+), 87 deletions(-) create mode 100644 src/compiler/__tests__/try.test.ts diff --git a/src/ast/__tests__/statement-extractor.test.ts b/src/ast/__tests__/statement-extractor.test.ts index 8ef346a9..5ebfae06 100644 --- a/src/ast/__tests__/statement-extractor.test.ts +++ b/src/ast/__tests__/statement-extractor.test.ts @@ -20,6 +20,7 @@ describe("extract ExpressionStatement correctly", () => { kind: "NormalClassDeclaration", classModifier: [], typeIdentifier: "Test", + sclass: undefined, classBody: [ { kind: "MethodDeclaration", @@ -203,8 +204,7 @@ describe("extract ExpressionStatement correctly", () => { const ast = parse(programStr); expect(ast).toEqual(expectedAst); }); - - it("extract Assignment Expression simple ExpressionName correctly", () => { + it("extract Assignment LeftHandSide qualified ExpressionName correctly", () => { const programStr = ` class Test { void test() { @@ -716,6 +716,7 @@ describe("extract ReturnStatement correctly", () => { kind: "NormalClassDeclaration", classModifier: [], typeIdentifier: "Test", + sclass: undefined, classBody: [ { kind: "MethodDeclaration", @@ -804,6 +805,115 @@ describe("extract ReturnStatement correctly", () => { location: expect.anything(), }; + const ast = parse(programStr); + console.log(JSON.stringify(ast, null, 2)); + expect(ast).toEqual(expectedAst); + }); +}); + +describe("extract TryStatement and ThrowStatement correctly", () => { + it("extract ThrowStatement inside catch block correctly", () => { + const programStr = ` + class Test { + void test() { + try { + throw new Exception(); + } catch (Exception e) { + throw new Exception(); + } + } + } + `; + + const expectedAst: AST = { + kind: "CompilationUnit", + importDeclarations: [], + topLevelClassOrInterfaceDeclarations: [ + { + kind: "NormalClassDeclaration", + classModifier: [], + typeIdentifier: "Test", + classBody: [ + { + kind: "MethodDeclaration", + methodModifier: [], + methodHeader: { + result: "void", + identifier: "test", + formalParameterList: [], + }, + methodBody: { + kind: "Block", + blockStatements: [ + { + kind: "TryStatement", + block: { + kind: "Block", + blockStatements: [ + { + kind: "ThrowStatement", + expression: { + kind: "ClassInstanceCreationExpression", + identifier: "Exception", + argumentList: [], + location: expect.anything(), + }, + location: expect.anything(), + }, + ], + location: expect.anything(), + }, + catches: { + kind: "Catches", + catchClauses: [ + { + kind: "CatchClause", + catchFormalParameter: { + kind: "CatchFormalParameter", + catchType: { + kind: "CatchType", + unannClassType: "Exception", + location: expect.anything(), + }, + variableDeclaratorId: "e", + location: expect.anything(), + }, + block: { + kind: "Block", + blockStatements: [ + { + kind: "ThrowStatement", + expression: { + kind: "ClassInstanceCreationExpression", + identifier: "Exception", + argumentList: [], + location: expect.anything(), + }, + location: expect.anything(), + }, + ], + location: expect.anything(), + }, + location: expect.anything(), + }, + ], + location: expect.anything(), + }, + finally: undefined, + location: expect.anything(), + }, + ], + location: expect.anything(), + }, + location: expect.anything(), + }, + ], + location: expect.anything(), + }, + ], + location: expect.anything(), + }; + const ast = parse(programStr); expect(ast).toEqual(expectedAst); }); diff --git a/src/ast/astExtractor/class-extractor.ts b/src/ast/astExtractor/class-extractor.ts index e043e6e7..cd55cace 100644 --- a/src/ast/astExtractor/class-extractor.ts +++ b/src/ast/astExtractor/class-extractor.ts @@ -27,14 +27,17 @@ export class ClassExtractor extends BaseJavaCstVisitorWithDefaults { extract(cst: ClassDeclarationCstNode): ClassDeclaration { this.visit(cst); - return { + const result: NormalClassDeclaration = { kind: "NormalClassDeclaration", classModifier: this.modifier, typeIdentifier: this.identifier, classBody: this.body, - sclass: this.sclass, location: cst.location, - } as NormalClassDeclaration; + }; + if (this.sclass) { + result.sclass = this.sclass; + } + return result; } classModifier(ctx: ClassModifierCtx) { diff --git a/src/ast/astExtractor/statement-extractor.ts b/src/ast/astExtractor/statement-extractor.ts index 13e9c02a..309ae541 100644 --- a/src/ast/astExtractor/statement-extractor.ts +++ b/src/ast/astExtractor/statement-extractor.ts @@ -25,6 +25,13 @@ import { SwitchBlockCtx, SwitchLabelCtx, SwitchBlockStatementGroupCtx, + ThrowStatementCtx, + TryStatementCtx, + CatchClauseCtx, + CatchFormalParameterCtx, + CatchTypeCtx, + CatchesCtx, + FinallyCtx, StatementCstNode, StatementExpressionCtx, StatementWithoutTrailingSubstatementCtx, @@ -97,6 +104,10 @@ export class StatementExtractor extends BaseJavaCstVisitorWithDefaults { exp: returnStatementExp, location: ctx.returnStatement[0].location, }; + } else if (ctx.throwStatement) { + return this.visit(ctx.throwStatement); + } else if (ctx.tryStatement) { + return this.visit(ctx.tryStatement); } } @@ -356,6 +367,69 @@ export class StatementExtractor extends BaseJavaCstVisitorWithDefaults { return ctx.expression.map((e) => expressionExtractor.extract(e)); } + throwStatement(ctx: ThrowStatementCtx) { + const expressionExtractor = new ExpressionExtractor(); + return { + kind: "ThrowStatement", + expression: expressionExtractor.extract(ctx.expression[0]), + location: ctx.Throw[0], + }; + } + + tryStatement(ctx: TryStatementCtx) { + return { + kind: "TryStatement", + block: ctx.block ? this.visit(ctx.block) : { kind: "Block", blockStatements: [], location: ctx.Try![0] }, + catches: ctx.catches ? this.visit(ctx.catches) : undefined, + finally: ctx.finally ? this.visit(ctx.finally) : undefined, + location: ctx.Try![0], + }; + } + + catches(ctx: CatchesCtx) { + return { + kind: "Catches", + catchClauses: ctx.catchClause.map((catchClause) => this.visit(catchClause)), + location: ctx.catchClause[0].location, + }; + } + + catchClause(ctx: CatchClauseCtx) { + return { + kind: "CatchClause", + catchFormalParameter: this.visit(ctx.catchFormalParameter), + block: this.visit(ctx.block), + location: ctx.Catch[0], + }; + } + + catchFormalParameter(ctx: CatchFormalParameterCtx) { + return { + kind: "CatchFormalParameter", + catchType: this.visit(ctx.catchType[0]), + variableDeclaratorId: + ctx.variableDeclaratorId[0].children.Identifier[0].image, + location: ctx.catchType[0].location, + }; + } + + catchType(ctx: CatchTypeCtx) { + const result = new TypeExtractor().visit(ctx.unannClassType[0] as any); + return { + kind: "CatchType", + unannClassType: result, + location: ctx.unannClassType[0].location, + }; + } + + finally(ctx: FinallyCtx) { + return { + kind: "Finally", + block: this.visit(ctx.block), + location: ctx.Finally[0], + }; + } + fqnOrRefType(ctx: FqnOrRefTypeCtx) { // Assignment LHS, MethodInvocation identifier let { name, location } = this.visit(ctx.fqnOrRefTypePartFirst); @@ -419,8 +493,15 @@ export class StatementExtractor extends BaseJavaCstVisitorWithDefaults { } block(ctx: BlockCtx): Statement { - if (ctx.blockStatements) return this.visit(ctx.blockStatements); - return { kind: "EmptyStatement" }; + const location = + (ctx.blockStatements?.[0] as any)?.location || + (ctx.LCurly?.[0] as any)?.location || + (ctx.RCurly?.[0] as any)?.location; + if (ctx.blockStatements) { + const block = this.visit(ctx.blockStatements) as Statement; + return { ...block, location }; + } + return { kind: "EmptyStatement", location }; } blockStatements(ctx: BlockStatementsCtx): Statement { diff --git a/src/ast/types/blocks-and-statements.ts b/src/ast/types/blocks-and-statements.ts index da4c3899..440329fc 100644 --- a/src/ast/types/blocks-and-statements.ts +++ b/src/ast/types/blocks-and-statements.ts @@ -101,7 +101,48 @@ export type StatementWithoutTrailingSubstatement = | DoStatement | ReturnStatement | BreakStatement - | ContinueStatement; + | ContinueStatement + | ThrowStatement + | TryStatement; + +export interface ThrowStatement extends BaseNode { + kind: "ThrowStatement"; + expression: Expression; +} + +export interface CatchClause extends BaseNode { + kind: "CatchClause"; + catchFormalParameter: CatchFormalParameter; + block: Block; +} + +export interface Catches extends BaseNode { + kind: "Catches"; + catchClauses: Array; +} + +export interface CatchFormalParameter extends BaseNode { + kind: "CatchFormalParameter"; + catchType: CatchType; + variableDeclaratorId: Identifier; +} + +export interface CatchType extends BaseNode { + kind: "CatchType"; + unannClassType: UnannType; +} + +export interface Finally extends BaseNode { + kind: "Finally"; + block: Block; +} + +export interface TryStatement extends BaseNode { + kind: "TryStatement"; + block: Block; + catches?: Catches; + finally?: Finally; +} export interface ExpressionStatement extends BaseNode { kind: "ExpressionStatement"; diff --git a/src/compiler/__tests__/__utils__/test-utils.ts b/src/compiler/__tests__/__utils__/test-utils.ts index 382c3907..7a7ae759 100644 --- a/src/compiler/__tests__/__utils__/test-utils.ts +++ b/src/compiler/__tests__/__utils__/test-utils.ts @@ -5,6 +5,7 @@ import { AST } from '../../../ast/types/packages-and-modules' import { javaPegGrammar } from '../../grammar' import { peggyFunctions } from '../../peggy-functions' import { execSync } from 'child_process' +import * as path from 'path' import * as peggy from 'peggy' import * as fs from 'fs' @@ -16,11 +17,9 @@ export type testCase = { } const debug = false -const pathToTestDir = './src/compiler/__tests__/' const parser = peggy.generate(peggyFunctions + javaPegGrammar, { allowedStartRules: ['CompilationUnit'] }) -const binaryWriter = new BinaryWriter() export function runTest(program: string, expectedLines: string[]) { const ast = parser.parse(program) @@ -30,18 +29,23 @@ export function runTest(program: string, expectedLines: string[]) { console.log(inspect(ast, false, null, true)) } - const classes = compile(ast as AST) - for (let c of classes) { - binaryWriter.writeBinary(c.classFile, pathToTestDir) - } + // Create a temporary directory for this test run to avoid race conditions + const tempDir = fs.mkdtempSync(path.join(process.cwd(), 'test-temp-')) + try { + const binaryWriter = new BinaryWriter() + const classes = compile(ast as AST) + for (let c of classes) { + binaryWriter.writeBinary(c.classFile, tempDir + path.sep) + } - const prevDir = process.cwd() - process.chdir(pathToTestDir) - execSync('java -noverify Main > output.log 2> err.log') + execSync('java -noverify Main > output.log 2> err.log', { cwd: tempDir }) - // ignore difference between \r\n and \n - const actualLines = fs.readFileSync('./output.log', 'utf-8').split(/\r?\n/).slice(0, -1) - process.chdir(prevDir) + // ignore difference between \r?\n and \n + const actualLines = fs.readFileSync(path.join(tempDir, 'output.log'), 'utf-8').split(/\r?\n/).slice(0, -1) - expect(actualLines).toStrictEqual(expectedLines) + expect(actualLines).toStrictEqual(expectedLines) + } finally { + // Clean up temporary directory + fs.rmSync(tempDir, { recursive: true, force: true }) + } } diff --git a/src/compiler/__tests__/try.test.ts b/src/compiler/__tests__/try.test.ts new file mode 100644 index 00000000..6c9afdce --- /dev/null +++ b/src/compiler/__tests__/try.test.ts @@ -0,0 +1,202 @@ +import { runTest, testCase } from "./__utils__/test-utils"; +import { check } from "../../types/checker"; +import { parse as parseTypeChecker } from "../../types/ast"; +import { TypeCheckerError, UnhandledExceptionError } from "../../types/errors"; + +const testCases: testCase[] = [ + { + comment: "try/catch block without exception", + program: ` + public class Main { + public static void main(String[] args) { + try { + System.out.println(1); + } catch (Exception e) { + System.out.println(2); + } + System.out.println(0); + } + } + `, + expectedLines: ["1", "0"], + }, + { + comment: "try/catch/finally block with exception handled", + program: ` + public class Main { + public static void main(String[] args) { + try { + int y = 1 / 0; + } catch (Exception e) { + System.out.println(2); + } finally { + System.out.println(3); + } + System.out.println(4); + } + } + `, + expectedLines: ["2", "3", "4"], + } + , + { + comment: "static helper method throws exception and catch handles it", + program: ` + public class Main { + public static int bar(int x) throws Exception { + int z = 1 / 0; + return x; + } + + public static void main(String[] args) { + try { + int y = bar(5); + } catch (Exception e) { + System.out.println(2); + } finally { + System.out.println(3); + } + System.out.println(4); + } + } + `, + expectedLines: ["2", "3", "4"], + }, + { + comment: "instance method calls static helper that throws exception and catch handles it", + program: ` + public class Main { + public int foo(int x) throws Exception { + int z = bar(x); + return x; + } + + public static int bar(int x) throws Exception { + int z = 1 / 0; + return x; + } + + public static void main(String[] args) { + try { + Main main = new Main(); + int y = main.foo(5); + } catch (Exception e) { + System.out.println(2); + } finally { + System.out.println(3); + } + System.out.println(4); + } + } + `, + expectedLines: ["2", "3", "4"], + }, + { + comment: "static helper method does not throw and catch is skipped", + program: ` + public class Main { + public static int bar(int x) throws Exception { + int z = 1; + return x; + } + + public static void main(String[] args) { + try { + int y = bar(5); + } catch (Exception e) { + System.out.println(2); + } finally { + System.out.println(3); + } + System.out.println(4); + } + } + `, + expectedLines: ["3", "4"], + }, + { + comment: "instance method calls static helper without throwing and catch is skipped", + program: ` + public class Main { + public int foo(int x) throws Exception { + int z = bar(x); + return x; + } + + public static int bar(int x) throws Exception { + int z = 1; + return x; + } + + public static void main(String[] args) { + try { + Main main = new Main(); + int y = main.foo(5); + } catch (Exception e) { + System.out.println(2); + } finally { + System.out.println(3); + } + System.out.println(4); + } + } + `, + expectedLines: ["3", "4"], + } +]; + +describe("try/catch", () => { + for (const testCase of testCases) { + it(testCase.comment, () => runTest(testCase.program, testCase.expectedLines)); + } +}); + +const typeCheckErrorCases = [ + { + comment: "static method declares checked exception but is not handled or propagated", + program: ` + public class Main { + public static int bar(int x) throws Exception { + int z = 1; + return x; + } + + public static void main(String[] args) { + bar(5); + } + } + ` + }, + { + comment: "instance method declares checked exception but is not handled or propagated", + program: ` + public class Main { + public int foo(int x) throws Exception { + int z = bar(x); + return x; + } + + public static int bar(int x) throws Exception { + int z = 1; + return x; + } + + public static void main(String[] args) { + Main main = new Main(); + main.foo(5); + } + } + ` + } +]; + +describe("try/catch type checking errors", () => { + for (const testCase of typeCheckErrorCases) { + it(testCase.comment, () => { + const ast = parseTypeChecker(testCase.program); + if (ast instanceof TypeCheckerError) throw new Error('Program parsing returns null.'); + const result = check(ast); + expect(result.errors.some(error => error instanceof UnhandledExceptionError)).toBe(true); + }); + } +}); diff --git a/src/compiler/code-generator.ts b/src/compiler/code-generator.ts index 773bde90..2eea5203 100644 --- a/src/compiler/code-generator.ts +++ b/src/compiler/code-generator.ts @@ -37,6 +37,7 @@ import { ConstructNotSupportedError, NoMethodMatchingSignatureError } from './error' +import { unannTypeToString } from '../types/ast/utils' import { FieldInfo, MethodInfos, SymbolInfo, SymbolTable, VariableInfo } from './symbol-table' type Label = { @@ -442,6 +443,15 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi ReturnStatement: (node: Node, cg: CodeGenerator) => { const { exp: expr } = node as ReturnStatement + + // Emit finally blocks from innermost to outermost before returning + for (let i = cg.finallyBlockStack.length - 1; i >= 0; i--) { + const finallyBlock = cg.finallyBlockStack[i] as any + finallyBlock.blockStatements.forEach((stmt: any) => { + compile(stmt, cg) + }) + } + if (expr) { const { stackSize: stackSize, resultType: resultType } = compile(expr, cg) cg.code.push(resultType in returnOp ? returnOp[resultType] : OPCODE.ARETURN) @@ -453,6 +463,14 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi }, BreakStatement: (node: Node, cg: CodeGenerator) => { + // Emit finally blocks from innermost to outermost before breaking + for (let i = cg.finallyBlockStack.length - 1; i >= 0; i--) { + const finallyBlock = cg.finallyBlockStack[i] as any + finallyBlock.blockStatements.forEach((stmt: any) => { + compile(stmt, cg) + }) + } + if (cg.loopLabels.length > 0) { // If inside a loop, break jumps to the end of the loop cg.addBranchInstr(OPCODE.GOTO, cg.loopLabels[cg.loopLabels.length - 1][1]) @@ -466,6 +484,14 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi }, ContinueStatement: (node: Node, cg: CodeGenerator) => { + // Emit finally blocks from innermost to outermost before continuing + for (let i = cg.finallyBlockStack.length - 1; i >= 0; i--) { + const finallyBlock = cg.finallyBlockStack[i] as any + finallyBlock.blockStatements.forEach((stmt: any) => { + compile(stmt, cg) + }) + } + cg.addBranchInstr(OPCODE.GOTO, cg.loopLabels[cg.loopLabels.length - 1][0]) return { stackSize: 0, resultType: EMPTY_TYPE } }, @@ -576,6 +602,194 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi return { stackSize: maxStack, resultType: resType } }, + TryStatement: (node: Node, cg: CodeGenerator) => { + let maxStack = 0 + const { block, catches } = node as any + const finallyNode: any = (node as any).finally + + const hasCatches = catches && catches.catchClauses && catches.catchClauses.length > 0 + + if (!hasCatches && !finallyNode) { + return { stackSize: compile(block, cg).stackSize, resultType: EMPTY_TYPE } + } + + if (hasCatches || finallyNode) { + maxStack = Math.max(maxStack, 1) + } + + const localExceptionTable: Array<{ + startPc: number + endPc: number + handlerLabel: Label + catchType: number + }> = [] + + // Push finally block onto stack so return/break/continue can access it + if (finallyNode) { + cg.finallyBlockStack.push(finallyNode.block) + } + + try { + // mark start of protected region + const tryStart = cg.generateNewLabel() + tryStart.offset = cg.code.length + + // compile try block + maxStack = Math.max(maxStack, compile(block, cg).stackSize) + + // end of protected region (first instruction after try block) + const tryEnd = cg.generateNewLabel() + tryEnd.offset = cg.code.length + + const catchAllLabel = finallyNode ? cg.generateNewLabel() : null + + // For normal path: run finally block if it exists + if (finallyNode) { + finallyNode.block.blockStatements.forEach((stmt: any) => { + const { stackSize } = compile(stmt, cg) + maxStack = Math.max(maxStack, stackSize) + }) + } + + // jump over handlers when try completes normally + const afterHandlers = cg.generateNewLabel() + cg.addBranchInstr(OPCODE.GOTO, afterHandlers) + + // For each catch clause, emit a handler and an exception table entry + if (hasCatches) { + for (const catchClause of catches.catchClauses) { + const handlerLabel = cg.generateNewLabel() + handlerLabel.offset = cg.code.length + + // determine catch type index (constant pool) + const catchTypeNode = catchClause.catchFormalParameter.catchType + const catchTypeName = unannTypeToString(catchTypeNode.unannClassType) + let catchClassName = 'java/lang/Throwable' + try { + catchClassName = cg.symbolTable.queryClass(catchTypeName).name + } catch (e) { + catchClassName = catchTypeName.includes('/') ? catchTypeName : catchTypeName.replace(/\./g, '/') + } + const catchTypeIndex = cg.constantPoolManager.indexClassInfo(catchClassName) + + // add exception table entry (startPc, endPc, handlerPc, catchType) + localExceptionTable.push({ + startPc: tryStart.offset, + endPc: tryEnd.offset, + handlerLabel: handlerLabel, + catchType: catchTypeIndex + }) + + // create scope for catch variable + cg.symbolTable.extend() + const varName = catchClause.catchFormalParameter.variableDeclaratorId + const varTypeStr = unannTypeToString(catchTypeNode.unannClassType) + const varInfo = { + name: varName, + accessFlags: 0, + index: cg.maxLocals, + typeName: varTypeStr, + typeDescriptor: cg.symbolTable.generateFieldDescriptor(varTypeStr) + } + cg.symbolTable.insertVariableInfo(varInfo) + if (['J', 'D'].includes(varInfo.typeDescriptor)) { + cg.maxLocals += 2 + } else { + cg.maxLocals++ + } + + // at handler entry, the exception object is on the stack; store it into the local + cg.code.push(OPCODE.ASTORE, varInfo.index) + + const catchStartOffset = cg.code.length + + // compile catch block statements + const catchBlock = catchClause.block + catchBlock.blockStatements.forEach((stmt: any) => { + const { stackSize } = compile(stmt, cg) + maxStack = Math.max(maxStack, stackSize) + }) + + const catchEndOffset = cg.code.length + + // teardown catch scope + cg.symbolTable.teardown() + + // If finally exists, add catch-all entry for this catch block + if (finallyNode && catchAllLabel && catchStartOffset < catchEndOffset) { + localExceptionTable.push({ + startPc: catchStartOffset, + endPc: catchEndOffset, + handlerLabel: catchAllLabel, + catchType: 0 + }) + } + + // For caught path: run finally block if it exists + if (finallyNode) { + finallyNode.block.blockStatements.forEach((stmt: any) => { + const { stackSize } = compile(stmt, cg) + maxStack = Math.max(maxStack, stackSize) + }) + } + + // after handler, jump to afterHandlers + cg.addBranchInstr(OPCODE.GOTO, afterHandlers) + } + } + + // If finally exists, add catch-all entry for the try block after all specific catch handlers. + // This ensures the catch clauses are matched before the generic finally rethrow path. + if (finallyNode && catchAllLabel) { + localExceptionTable.push({ + startPc: tryStart.offset, + endPc: tryEnd.offset, + handlerLabel: catchAllLabel, + catchType: 0 + }) + } + + // If finally exists, add a catch-all handler that runs finally then rethrows + if (finallyNode && catchAllLabel) { + catchAllLabel.offset = cg.code.length + + // allocate temp local to store exception + const tempIndex = cg.maxLocals + cg.maxLocals += 1 + cg.code.push(OPCODE.ASTORE, tempIndex) + + // compile finally block inside catch-all + finallyNode.block.blockStatements.forEach((stmt: any) => { + const { stackSize } = compile(stmt, cg) + maxStack = Math.max(maxStack, stackSize) + }) + + // reload exception and rethrow + cg.code.push(OPCODE.ALOAD, tempIndex, OPCODE.ATHROW) + } + + // place after-handlers label + afterHandlers.offset = cg.code.length + + // Now that all labels are resolved, push to cg.exceptionTable + localExceptionTable.forEach(entry => { + cg.exceptionTable.push({ + startPc: entry.startPc, + endPc: entry.endPc, + handlerPc: entry.handlerLabel.offset, + catchType: entry.catchType + }) + }) + + return { stackSize: maxStack, resultType: EMPTY_TYPE } + } finally { + // Pop finally block from stack when exiting + if (finallyNode) { + cg.finallyBlockStack.pop() + } + } + }, + TernaryExpression: (node: Node, cg: CodeGenerator) => { let maxStack = 0 const { @@ -1723,9 +1937,11 @@ class CodeGenerator { constantPoolManager: ConstantPoolManager maxLocals: number = 0 stackSize: number = 0 + exceptionTable: Array = [] labels: Label[] = [] loopLabels: Label[][] = [] switchLabels: Label[] = [] + finallyBlockStack: Node[] = [] code: number[] = [] currentClass: string @@ -1761,6 +1977,7 @@ class CodeGenerator { generateCode(currentClass: string, methodNode: MethodDeclaration) { this.symbolTable.extend() this.currentClass = currentClass + this.exceptionTable = [] if (!methodNode.methodModifier.includes('static')) { this.maxLocals++ } @@ -1799,7 +2016,6 @@ class CodeGenerator { } this.resolveLabels() - const exceptionTable: Array = [] const attributes: Array = [] const codeBuf = new Uint8Array(this.code).buffer const dataView = new DataView(codeBuf) @@ -1808,7 +2024,7 @@ class CodeGenerator { const attributeLength = 12 + this.code.length + - 8 * exceptionTable.length + + 8 * this.exceptionTable.length + attributes.map(attr => attr.attributeLength + 6).reduce((acc, val) => acc + val, 0) this.symbolTable.teardown() @@ -1819,8 +2035,8 @@ class CodeGenerator { maxLocals: this.maxLocals, codeLength: this.code.length, code: dataView, - exceptionTableLength: exceptionTable.length, - exceptionTable: exceptionTable, + exceptionTableLength: this.exceptionTable.length, + exceptionTable: this.exceptionTable, attributesCount: attributes.length, attributes: attributes } diff --git a/src/compiler/grammar.pegjs b/src/compiler/grammar.pegjs index 505f648e..ce756b3d 100755 --- a/src/compiler/grammar.pegjs +++ b/src/compiler/grammar.pegjs @@ -659,7 +659,21 @@ VariableModifier = final Throws - = throw TO_BE_ADDED + = throws et:ExceptionTypeList { + return addLocInfo({ + kind: "Throws", + exceptionTypeList: et, + }) + } + +ExceptionTypeList + = e:ExceptionType es:(comma @ExceptionType)* { + return [e, ...es]; + } + +ExceptionType + = ClassType + / TypeIdentifier ConstructorDeclaration = cm:ConstructorModifier* cd:ConstructorDeclarator Throws? cb:ConstructorBody { @@ -854,8 +868,74 @@ ThrowStatement SynchronizedStatement = synchronized lparen Expression rparen Block +Catches + = catchClauses:CatchClause+ { + return addLocInfo({ + kind: "Catches", + catchClauses, + }) + } + +CatchClause + = catch lparen catchFormalParameter:CatchFormalParameter rparen block:Block { + return addLocInfo({ + kind: "CatchClause", + catchFormalParameter, + block, + }) + } + +CatchFormalParameter + = variableModifiers:VariableModifier* catchType:CatchType variableDeclaratorId:VariableDeclaratorId { + return addLocInfo({ + kind: "CatchFormalParameter", + variableModifiers, + catchType, + variableDeclaratorId, + }) + } + +CatchType + = unannClassType:UnannClassType classTypes:( _ '|' _ c:ClassType { return c })* { + return addLocInfo({ + kind: "CatchType", + unannClassType, + classTypes: classTypes.length ? classTypes : undefined, + }) + } + +UnannClassType + = typeIdentifier:TypeIdentifier { + return addLocInfo({ + kind: "UnannClassType", + typeIdentifier: { identifier: typeIdentifier }, + }) + } + +Finally + = finally block:Block { + return addLocInfo({ + kind: "Finally", + block, + }) + } + TryStatement - = TO_BE_ADDED + = try block:Block catches:Catches finallyNode:Finally? { + return addLocInfo({ + kind: "TryStatement", + block, + catches, + finally: finallyNode, + }) + } + / try block:Block finallyNode:Finally { + return addLocInfo({ + kind: "TryStatement", + block, + finally: finallyNode, + }) + } IfStatement = if lparen expr:Expression rparen c:Statement a:(else @Statement)? { diff --git a/src/compiler/grammar.ts b/src/compiler/grammar.ts index c7417294..4ddd9aba 100755 --- a/src/compiler/grammar.ts +++ b/src/compiler/grammar.ts @@ -661,7 +661,21 @@ VariableModifier = final Throws - = throw TO_BE_ADDED + = throws et:ExceptionTypeList { + return addLocInfo({ + kind: "Throws", + exceptionTypeList: et, + }) + } + +ExceptionTypeList + = e:ExceptionType es:(comma @ExceptionType)* { + return [e, ...es]; + } + +ExceptionType + = ClassType + / TypeIdentifier ConstructorDeclaration = cm:ConstructorModifier* cd:ConstructorDeclarator Throws? cb:ConstructorBody { @@ -856,8 +870,74 @@ ThrowStatement SynchronizedStatement = synchronized lparen Expression rparen Block +Catches + = catchClauses:CatchClause+ { + return addLocInfo({ + kind: "Catches", + catchClauses, + }) + } + +CatchClause + = catch lparen catchFormalParameter:CatchFormalParameter rparen block:Block { + return addLocInfo({ + kind: "CatchClause", + catchFormalParameter, + block, + }) + } + +CatchFormalParameter + = variableModifiers:VariableModifier* catchType:CatchType variableDeclaratorId:VariableDeclaratorId { + return addLocInfo({ + kind: "CatchFormalParameter", + variableModifiers, + catchType, + variableDeclaratorId, + }) + } + +CatchType + = unannClassType:UnannClassType classTypes:( _ '|' _ c:ClassType { return c })* { + return addLocInfo({ + kind: "CatchType", + unannClassType, + classTypes: classTypes.length ? classTypes : undefined, + }) + } + +UnannClassType + = typeIdentifier:TypeIdentifier { + return addLocInfo({ + kind: "UnannClassType", + typeIdentifier: { identifier: typeIdentifier }, + }) + } + +Finally + = finally block:Block { + return addLocInfo({ + kind: "Finally", + block, + }) + } + TryStatement - = TO_BE_ADDED + = try block:Block catches:Catches finallyNode:Finally? { + return addLocInfo({ + kind: "TryStatement", + block, + catches, + finally: finallyNode, + }) + } + / try block:Block finallyNode:Finally { + return addLocInfo({ + kind: "TryStatement", + block, + finally: finallyNode, + }) + } IfStatement = if lparen expr:Expression rparen c:Statement a:(else @Statement)? { diff --git a/src/compiler/import/lib-info.ts b/src/compiler/import/lib-info.ts index 8db187d6..720ced9d 100644 --- a/src/compiler/import/lib-info.ts +++ b/src/compiler/import/lib-info.ts @@ -13,6 +13,12 @@ export const rawLibInfo = { name: 'public final java.lang.System', fields: ['public static final java.io.PrintStream out'] }, + { + name: 'public class java.lang.Throwable' + }, + { + name: 'public class java.lang.Exception' + }, { name: 'public final java.lang.Math', methods: [ diff --git a/src/jvm/__tests__/thread.ts b/src/jvm/__tests__/thread.ts index e4ae911e..07395191 100644 --- a/src/jvm/__tests__/thread.ts +++ b/src/jvm/__tests__/thread.ts @@ -4,7 +4,9 @@ import { ReferenceClassData } from '../types/class/ClassData' import { JvmObject } from '../types/reference/Object' import Thread from '../../jvm/thread' import JVM from '../../jvm/jvm' +import { JavaStackFrame } from '../../jvm/stackframe' import { setupTest, TestThreadPool } from './__utils__/test-utils' +import { METHOD_FLAGS } from '../../ClassFile/types/methods' let thread: Thread let threadClass: ReferenceClassData @@ -67,4 +69,42 @@ describe('Thread', () => { test('should manage wide (64-bit) values on the operand stack correctly', () => { // TODO }) + + test('should route an exception to a matching try-catch handler in the current method', () => { + const setup = setupTest() + const { testLoader, thread: testThread, classes } = setup + const exceptionMethodClass = testLoader.createClass({ + className: 'TryCatchTest', + loader: testLoader, + methods: [ + { + accessFlags: [METHOD_FLAGS.ACC_PUBLIC], + name: 'test0', + descriptor: '()V', + attributes: [], + code: new DataView(new ArrayBuffer(1)), + exceptionTable: [ + { + startPc: 0, + endPc: 1, + handlerPc: 0, + catchType: 'java/lang/NullPointerException' + } + ] + } + ], + }) as ReferenceClassData + + const method = exceptionMethodClass.getMethod('test0()V') + expect(method).not.toBeNull() + + testThread.invokeStackFrame( + new JavaStackFrame(exceptionMethodClass, method as any, 0, []) + ) + const exceptionObj = classes.NullPointerException.instantiate() + testThread.throwException(exceptionObj) + + expect(testThread.getPC()).toBe(0) + expect(testThread.peekStackFrame().operandStack).toEqual([exceptionObj]) + }) }) diff --git a/src/jvm/exception-table.ts b/src/jvm/exception-table.ts index 15248a87..7bcf0d99 100644 --- a/src/jvm/exception-table.ts +++ b/src/jvm/exception-table.ts @@ -1,33 +1,45 @@ -import { ClassData } from "./types/class/ClassData" - -class Entry { - from: number - to: number - target: number - type: ClassData - - constructor(from: number, to: number, target: number, type: ClassData) { - this.from = from; - this.to = to; - this.target = target; - this.type = type; - } +import { ClassData } from './types/class/ClassData' + +export interface ExceptionTableEntry { + startPc: number + endPc: number + handlerPc: number + catchType: any | null } -export class ExceptionTable { - private entries: Entry[] +export class ExceptionTable implements Iterable { + private entries: ExceptionTableEntry[] + + constructor(entries?: ExceptionTableEntry[]) { + this.entries = entries ? entries.slice() : [] + } - retrieve(line: number): Entry | null { - this.entries.forEach(entry => { - if (line >= entry.from && line <= entry.to) { - return entry + retrieve(pc: number): ExceptionTableEntry | null { + for (let i = 0; i < this.entries.length; i++) { + const e = this.entries[i] + if (pc >= e.startPc && pc < e.endPc) { + return e } - }) + } return null } - insert(from: number, to: number, target: number, type: ClassData): void { - var entry = new Entry(from, to, target, type) - this.entries.push(entry) + insert(startPc: number, endPc: number, handlerPc: number, catchType: ClassData | null): void { + this.entries.push({ startPc, endPc, handlerPc, catchType }) + } + + toArray(): ExceptionTableEntry[] { + return this.entries.slice() + } + + [Symbol.iterator](): Iterator { + return this.entries[Symbol.iterator]() + } + forEach(cb: (entry: ExceptionTableEntry, idx?: number) => void) { + this.entries.forEach(cb) + } + + get length() { + return this.entries.length } } \ No newline at end of file diff --git a/src/jvm/types/class/Attributes.ts b/src/jvm/types/class/Attributes.ts index 92b5ee33..f3f2a051 100644 --- a/src/jvm/types/class/Attributes.ts +++ b/src/jvm/types/class/Attributes.ts @@ -15,6 +15,7 @@ import { SourceFileAttribute, StackMapFrame } from '../../../ClassFile/types/attributes' +import { ExceptionTable } from '../../exception-table' import { ConstantPool } from '../../constant-pool' import { ConstantClass, @@ -45,7 +46,8 @@ export const info2Attribute = (info: AttributeInfo, constantPool: ConstantPool): case 'Code': const code = info as CodeAttribute const attr: { [attributeName: string]: IAttribute } = {} - const exceptionTable = code.exceptionTable.map(handler => { + const exceptionTable = new ExceptionTable( + code.exceptionTable.map(handler => { return { startPc: handler.startPc, endPc: handler.endPc, @@ -54,6 +56,7 @@ export const info2Attribute = (info: AttributeInfo, constantPool: ConstantPool): handler.catchType === 0 ? null : (constantPool.get(handler.catchType) as ConstantClass) } }) + ) code.attributes.forEach(element => { attr[(constantPool.get(element.attributeNameIndex) as ConstantUtf8).get()] = info2Attribute( element, @@ -244,12 +247,7 @@ export interface Code extends IAttribute { codeLength: number code: DataView exceptionTableLength: number - exceptionTable: Array<{ - startPc: number - endPc: number - handlerPc: number - catchType: ConstantClass | null - }> + exceptionTable: ExceptionTable attributes: { [attributeName: string]: IAttribute } diff --git a/src/jvm/types/class/Method.ts b/src/jvm/types/class/Method.ts index b0adb49c..8e803643 100644 --- a/src/jvm/types/class/Method.ts +++ b/src/jvm/types/class/Method.ts @@ -6,6 +6,7 @@ import { attrInfo2Interface, parseMethodDescriptor, getArgs, logger } from '../. import { ErrorResult, ImmediateResult, ResultType, SuccessResult } from '../Result' import { JavaType, JvmObject } from '../reference/Object' import { Code, Exceptions, IAttribute, NestHost, Signature } from './Attributes' +import { ExceptionTable } from '../../exception-table' import { ReferenceClassData, ArrayClassData, ClassData } from './ClassData' import { ConstantClass, ConstantMethodref, ConstantNameAndType, ConstantUtf8 } from './Constants' @@ -484,7 +485,7 @@ export class Method { codeLength: dv.buffer.byteLength, code: dv, exceptionTableLength: 0, - exceptionTable: [], + exceptionTable: new ExceptionTable(), attributes: {} } as Code }, diff --git a/src/jvm/utils/disassembler/utils/readAttributes.ts b/src/jvm/utils/disassembler/utils/readAttributes.ts index a2c624fa..0f94bdea 100644 --- a/src/jvm/utils/disassembler/utils/readAttributes.ts +++ b/src/jvm/utils/disassembler/utils/readAttributes.ts @@ -186,7 +186,7 @@ function readCodeAttribute( throw new Error('Class format error: Code attribute invalid length') } - const code = new DataView(view.buffer, offset, codeLength) + const code = new DataView(view.buffer, view.byteOffset + offset, codeLength) offset += codeLength const exceptionTableLength = view.getUint16(offset) diff --git a/src/types/checker/__tests__/tryStatement.test.ts b/src/types/checker/__tests__/tryStatement.test.ts index 975a7246..90860225 100644 --- a/src/types/checker/__tests__/tryStatement.test.ts +++ b/src/types/checker/__tests__/tryStatement.test.ts @@ -1,8 +1,8 @@ import { check } from '..' import { parse } from '../../ast' import { - ExceptionHasAlreadyBeenCaughtError, IncompatibleTypesError, + UnhandledExceptionError, TypeCheckerError } from '../../errors' import { Type } from '../../types/type' @@ -14,11 +14,17 @@ const createProgram = (statement: string) => ` } } ` +const createClass = (body: string) => ` + public class Main { + ${body} + } +` const testcases: { input: string result: { type: Type | null; errors: Error[] } only?: boolean + fullProgram?: boolean }[] = [ { input: ` @@ -30,9 +36,58 @@ const testcases: { input: ` try {} catch (Throwable e) {} - catch (Exception e) {} `, - result: { type: null, errors: [new ExceptionHasAlreadyBeenCaughtError()] } + result: { type: null, errors: [] } + }, + { + input: ` + public static void foo() throws Exception { + throw new Exception(); + } + public static void main(String args[]) { + foo(); + } + `, + fullProgram: true, + result: { type: null, errors: [new UnhandledExceptionError()] } + }, + { + input: ` + public static void foo() throws Exception { + throw new Exception(); + } + public static void main(String args[]) throws Exception { + foo(); + } + `, + fullProgram: true, + result: { type: null, errors: [] } + }, + { + input: ` + public static void foo() throws Exception { + throw new Exception(); + } + public static void main(String args[]) { + try { + foo(); + } catch (Exception e) { + } + } + `, + fullProgram: true, + result: { type: null, errors: [] } + }, + { + input: ` + try { + throw new Exception(); + } catch (Exception e) { + throw new Exception(); + } finally { + } + `, + result: { type: null, errors: [] } }, { input: ` @@ -48,7 +103,7 @@ describe('Type Checker', () => { let it = test if (testcase.only) it = test.only it(`Checking try statements for ${testcase.input}`, () => { - const program = createProgram(testcase.input) + const program = testcase.fullProgram ? createClass(testcase.input) : createProgram(testcase.input) const ast = parse(program) if (!ast) throw new Error('Program parsing returns null.') if (ast instanceof TypeCheckerError) throw new Error('Test case is invalid.') diff --git a/src/types/checker/environment.ts b/src/types/checker/environment.ts index 6ef2ad2b..526fdca5 100644 --- a/src/types/checker/environment.ts +++ b/src/types/checker/environment.ts @@ -51,6 +51,8 @@ export class Frame { private _variables = new Map() private _returnType: Type | null = null + private _throws: any[] = [] + private _activeCaughtExceptions: any[] = [] private _parentFrame: Frame | null = null private _childrenFrames: Frame[] = [] @@ -71,6 +73,25 @@ export class Frame { throw new Error('cannot find return type') } + public setThrows(exceptions: any[]): void { + this._throws = exceptions.slice() + } + + public getThrows(): any[] { + if (this._throws && this._throws.length > 0) return this._throws.slice() + if (this._parentFrame) return this._parentFrame.getThrows() + return [] + } + + public setActiveCaughtExceptions(exceptions: any[]): void { + this._activeCaughtExceptions = exceptions.slice() + } + + public getActiveCaughtExceptions(): any[] { + const parentCaught = this._parentFrame ? this._parentFrame.getActiveCaughtExceptions() : [] + return parentCaught.concat(this._activeCaughtExceptions) + } + public getType(name: string, location: Location): Type | TypeCheckerError { if (isArrayType(name)) { const typePrefix = removeArraySuffix(name) diff --git a/src/types/checker/index.ts b/src/types/checker/index.ts index 77491719..be7c16e5 100644 --- a/src/types/checker/index.ts +++ b/src/types/checker/index.ts @@ -7,10 +7,12 @@ import { BadOperandTypesError, CannotFindSymbolError, IncompatibleTypesError, + MethodCannotBeAppliedError, NotApplicableToExpressionTypeError, TypeCheckerError, TypeCheckerInternalError, VariableAlreadyDefinedError + ,UnhandledExceptionError } from '../errors' import { Boolean, @@ -477,14 +479,60 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R if (argumentList instanceof TypeCheckerError) return newResult(null, [...errors, argumentList]) - for (let i = 0; i < methods.length - 1; i++) { + // Resolve overload: find the first applicable method + let selectedMethod: Method | null = null + let selectedReturnType: Type | TypeCheckerError | null = null + let lastInvokeError: TypeCheckerError | null = null + for (let i = 0; i < methods.length; i++) { const result = methods[i].invoke(argumentList) - if (result instanceof TypeCheckerError) continue - return newResult(result, errors) + if (result instanceof TypeCheckerError) { + lastInvokeError = result + continue + } + selectedMethod = methods[i] + selectedReturnType = result + break + } + if (selectedMethod === null || selectedReturnType === null) { + // If there was exactly one candidate and it produced a specific + // type-check error (e.g. incompatible types), surface that error + // instead of the generic "method cannot be applied" message. + if (methods.length === 1 && lastInvokeError) return newResult(null, [...errors, lastInvokeError]) + return newResult(null, [...errors, new MethodCannotBeAppliedError(node.location)]) } - const returnType = methods[methods.length - 1].invoke(argumentList) - if (returnType instanceof TypeCheckerError) return newResult(null, [...errors, returnType]) - return newResult(returnType, errors) + + // Enforce declared exceptions from the invoked method: any checked exception + // must either be caught by an enclosing try/catch or declared by the current method. + const declaredExceptions: any[] = + (selectedMethod as any).getThrownExceptions?.() || [] + if (declaredExceptions.length > 0) { + const exceptionBase = frame.getType('Exception', node.location) + for (const declaredException of declaredExceptions) { + // If we cannot determine checkedness, be conservative and treat as checked + let isChecked = true + if (!(exceptionBase instanceof TypeCheckerError)) { + // checked if it's an Exception subtype + isChecked = (exceptionBase as any).canBeAssigned(declaredException) + } + if (!isChecked) continue + + // check if caught by any active catch in scope + const activeCaught = frame.getActiveCaughtExceptions() + const isCaught = activeCaught.some(caughtType => caughtType.canBeAssigned(declaredException)) + if (isCaught) continue + + // check if current method declares it + const declaredByCurrent = frame.getThrows() + const isDeclared = declaredByCurrent.some(declared => declared.canBeAssigned(declaredException)) + if (isDeclared) continue + + return newResult(null, [new UnhandledExceptionError(node.location)]) + } + } + + if (selectedReturnType instanceof TypeCheckerError) + return newResult(null, [...errors, selectedReturnType]) + return newResult(selectedReturnType, errors) } case 'NormalClassDeclaration': { const errors: TypeCheckerError[] = [] @@ -521,6 +569,10 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R errors.push(...constructorMethodErrors) break } + // set declared throws for constructor body checking + if (constructor.getThrownExceptions) { + methodFrame.setThrows(constructor.getThrownExceptions()) + } const { errors: checkErrors } = typeCheckBody( bodyDeclaration.constructorBody, methodFrame @@ -565,6 +617,10 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R const methodFrame = classFrame.newChildFrame() const methodErrors: TypeCheckerError[] = [] methodFrame.setReturnType(method.getReturnType()) + // set declared throws for method body checking + if (method.getThrownExceptions) { + methodFrame.setThrows(method.getThrownExceptions()) + } method.mapParameters((name, type, isVarargs) => { const error = methodFrame.setVariable(name, type, { startLine: -1, startOffset: -1 }) if (error) methodErrors.push(error) @@ -672,12 +728,13 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R return newResult(null, [new IncompatibleTypesError(node.expression.location)]) } case 'TryStatement': { - const checkBlockStatements = typeCheckBody(node.block, frame) - if (checkBlockStatements.hasErrors) return checkBlockStatements const errors: TypeCheckerError[] = [] + + // Collect and validate catch parameter types first so the try block + // can be type-checked with knowledge of active caught exceptions. + const catchParameters: Type[] = [] if (node.catches) { - const catchParameters: Type[] = [] - node.catches.catchClauses.forEach(catchClause => { + for (const catchClause of node.catches.catchClauses) { const catchTypeNode = catchClause.catchFormalParameter.catchType const catchType = frame.getType( unannTypeToString(catchTypeNode.unannClassType), @@ -685,7 +742,7 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R ) if (catchType instanceof TypeCheckerError) { errors.push(catchType) - return + continue } const checkCatchTypeError = checkTryCatchType( catchType, @@ -694,12 +751,29 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R ) if (checkCatchTypeError instanceof TypeCheckerError) { errors.push(checkCatchTypeError) - return + continue } catchParameters.push(catchType) + } + } + + // Type-check the try block with active caught exceptions available + const tryFrame = frame.newChildFrame() + tryFrame.setActiveCaughtExceptions(catchParameters) + const tryBlockCheck = typeCheckBody(node.block, tryFrame) + if (tryBlockCheck.hasErrors) errors.push(...tryBlockCheck.errors) + + // Now type-check each catch clause body with its parameter bound + if (node.catches) { + for (const catchClause of node.catches.catchClauses) { + const catchTypeNode = catchClause.catchFormalParameter.catchType + const catchType = frame.getType( + unannTypeToString(catchTypeNode.unannClassType), + catchTypeNode.location + ) + if (catchType instanceof TypeCheckerError) continue const catchFrame = frame.newChildFrame() - const catchTypeParameter = - catchClause.catchFormalParameter.variableDeclaratorId.identifier + const catchTypeParameter = catchClause.catchFormalParameter.variableDeclaratorId.identifier const error = catchFrame.setVariable( catchTypeParameter.identifier, catchType, @@ -707,16 +781,18 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R ) if (error instanceof TypeCheckerError) { errors.push(error) - return + continue } const catchBlockCheck = typeCheckBody(catchClause.block, catchFrame) if (catchBlockCheck.hasErrors) errors.push(...catchBlockCheck.errors) - }) + } } + if (node.finally) { const finallyBlockCheck = typeCheckBody(node.finally.block, frame) if (finallyBlockCheck.hasErrors) errors.push(...finallyBlockCheck.errors) } + return newResult(null, errors) } case 'UnaryExpression': { diff --git a/src/types/errors.ts b/src/types/errors.ts index 6428e4c2..b1dc889f 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -165,3 +165,9 @@ export class VariableAlreadyDefinedError extends TypeCheckerError { super('variable is already defined', location) } } + +export class UnhandledExceptionError extends TypeCheckerError { + constructor(location?: Location) { + super('unhandled exception', location) + } +} diff --git a/src/types/typeFactories/methodFactory.ts b/src/types/typeFactories/methodFactory.ts index 2b413ede..c415fd10 100644 --- a/src/types/typeFactories/methodFactory.ts +++ b/src/types/typeFactories/methodFactory.ts @@ -67,5 +67,20 @@ export const createMethod = ( } // TODO: Add exceptions for method signatures + // Add declared exceptions (throws clause) if present + const throwsNode: any = + node.kind === 'MethodDeclaration' ? node.methodHeader.throws : node.throws + if (throwsNode && (throwsNode as any).exceptionTypeList) { + for (const exceptionTypeNode of (throwsNode as any).exceptionTypeList) { + const exceptionType = frame.getType( + unannTypeToString(exceptionTypeNode), + exceptionTypeNode.location + ) + if (exceptionType instanceof Error) return exceptionType + // store declared exception on method + method.addThrownException(exceptionType) + } + } + return method } diff --git a/src/types/types/methods.ts b/src/types/types/methods.ts index d7df067a..96902e03 100644 --- a/src/types/types/methods.ts +++ b/src/types/types/methods.ts @@ -202,6 +202,15 @@ export class Method implements Type { } } + public addThrownException(exception: any): void { + // `exception` is expected to be a Class (ClassType). We avoid strong coupling here. + this.throws.addException(exception) + } + + public getThrownExceptions(): any[] { + return this.throws.getExceptions() + } + public toString(): string { return `${this.modifiers.toString()} ${this.returnType.toString()} ${this.methodName}${this.parameters.toString()} ${this.throws.toString()}` } diff --git a/src/types/types/throws.ts b/src/types/types/throws.ts index 103da1d8..21b9baf7 100644 --- a/src/types/types/throws.ts +++ b/src/types/types/throws.ts @@ -8,13 +8,18 @@ export class Throws { private exceptions: Class[] = [] public constructor() {} - // public addThrowable( - // throwsClauseType: ThrowsClauseType, - // throwable: Class, - // location: Location, - // ): void | TypeCheckerError {} + public addException(exception: Class): void { + // avoid duplicates + if (this.exceptions.some(e => e === exception)) return + this.exceptions.push(exception) + } + + public getExceptions(): Class[] { + return this.exceptions.slice() + } public toString(): string { + if (this.exceptions.length === 0) return '' return `throws ${this.exceptions.map(exception => exception.getClassName()).join(', ')}` } } From 34be01f7aeb1d63c433da7eee3e142439f28162c Mon Sep 17 00:00:00 2001 From: kjw142857 <122250318+kjw142857@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:46:43 +0800 Subject: [PATCH 2/4] Add feature details in README (#88) * Include current and planned features in README * Update compiler README * Delete src/compiler/__tests__/tests/typeConversion.test.ts * Delete eslint.config.mjs * Add files via upload * Add files via upload --------- Co-authored-by: Martin Henz --- README.md | 1 + src/compiler/README.md | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 2f498ced..e7d7f7f8 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ The Java language in Source Academy currently supports a host of available featu - Explicit type conversion (type narrowing) - Implicit type conversion for system calls (e.g. int input to System.out.println) - Single nested class +- Exceptions ## Future Features diff --git a/src/compiler/README.md b/src/compiler/README.md index c63de654..7fa0faab 100644 --- a/src/compiler/README.md +++ b/src/compiler/README.md @@ -1,7 +1,5 @@ This is a bookkeeping of the planned scope of the compiler. It will be updated from time to time to reflect the current status of the compiler and to make the scope clearer. For a more formal treatment of what features are being supported, see scope.txt for a BNF-form of the Java sub-language. -Note that the compiler is separate from the Java Playground in the online version of Source Academy, which runs in tandem with the ECE. As such, any program run in the Playground will follow the features implemented in the ECE (e.g. widening type conversions), rather than the features below. - **Features that are already supported** - Single source file, single public class, with exactly one main method From 478ce864e35d67c6e5e2ee787c42efec095bf325 Mon Sep 17 00:00:00 2001 From: Martin Henz Date: Wed, 2 Sep 2026 10:56:44 +0800 Subject: [PATCH 3/4] Fix CI coverage-comment failure on push to main (#100) * Fix pre-existing lint/type errors blocking commits exception-table.ts declared ExceptionTableEntry.catchType as `any | null`, which eslint flags as a redundant union and which also masked a latent type mismatch (the field actually holds a ConstantClass, not a ClassData, until resolved). Narrow it to ConstantClass | null to match how it's populated in Attributes.ts and consumed in thread.ts. The pre-commit hook's repo-wide `eslint --fix` and `prettier --write` also picked up and applied a handful of pre-existing import-order and formatting fixes in other files that were tripping the hook. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TujL5U9Kg6vHKvRR7zryE4 * Skip PR coverage comment on push events jest-coverage-report-action tries to post a PR comment even on direct pushes to main, where no PR exists, causing the build job to fail with "Resource not accessible by integration". Only run the coverage-comment action on pull_request events, and run plain yarn test on push. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TujL5U9Kg6vHKvRR7zryE4 * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude Sonnet 5 Co-authored-by: kjw142857 <122250318+kjw142857@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++ src/compiler/binary-writer.ts | 4 +- src/compiler/code-generator.ts | 74 +++++++++++++--------- src/compiler/compiler.ts | 9 ++- src/compiler/error.ts | 2 +- src/compiler/symbol-table.ts | 18 +++--- src/jvm/exception-table.ts | 78 ++++++++++++------------ src/jvm/types/class/Method.ts | 2 +- src/types/typeFactories/methodFactory.ts | 4 +- 9 files changed, 112 insertions(+), 83 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f419239..788314ba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,6 +42,10 @@ jobs: distribution: 'oracle' java-version: '17' - uses: ArtiomTr/jest-coverage-report-action@v2 + if: github.event_name == 'pull_request' with: test-script: yarn test annotations: none + - name: Run tests + if: github.event_name != 'pull_request' + run: yarn test diff --git a/src/compiler/binary-writer.ts b/src/compiler/binary-writer.ts index 8b97dfb0..e4f09b77 100644 --- a/src/compiler/binary-writer.ts +++ b/src/compiler/binary-writer.ts @@ -49,7 +49,9 @@ export class BinaryWriter { fs.writeFileSync(filename, binary) } - private normalizeClassFile(classFile: ClassFile | Class | Array | Array): ClassFile { + private normalizeClassFile( + classFile: ClassFile | Class | Array | Array + ): ClassFile { if (Array.isArray(classFile)) { if (classFile.length === 0) { throw new Error('BinaryWriter expected a non-empty array of classes') diff --git a/src/compiler/code-generator.ts b/src/compiler/code-generator.ts index 2eea5203..9506a1f2 100644 --- a/src/compiler/code-generator.ts +++ b/src/compiler/code-generator.ts @@ -31,13 +31,13 @@ import { CaseLabel } from '../ast/types/blocks-and-statements' import { MethodDeclaration, UnannType } from '../ast/types/classes' +import { unannTypeToString } from '../types/ast/utils' import { ConstantPoolManager } from './constant-pool-manager' import { AmbiguousMethodCallError, ConstructNotSupportedError, NoMethodMatchingSignatureError } from './error' -import { unannTypeToString } from '../types/ast/utils' import { FieldInfo, MethodInfos, SymbolInfo, SymbolTable, VariableInfo } from './symbol-table' type Label = { @@ -191,19 +191,19 @@ const EMPTY_TYPE: string = '' function areClassTypesCompatible(fromType: string, toType: string, cg: CodeGenerator): boolean { const cleanFrom = fromType.replace(/^L|;$/g, '') const cleanTo = toType.replace(/^L|;$/g, '') - if (cleanFrom === cleanTo) return true; + if (cleanFrom === cleanTo) return true try { - let current = cg.symbolTable.queryClass(cleanFrom); + let current = cg.symbolTable.queryClass(cleanFrom) while (current.parentClassName) { - const parentClean = current.parentClassName; - if (parentClean === cleanTo) return true; - current = cg.symbolTable.queryClass(parentClean); + const parentClean = current.parentClassName + if (parentClean === cleanTo) return true + current = cg.symbolTable.queryClass(parentClean) } } catch (e) { - return false; + return false } - return false; + return false } function handleImplicitTypeConversion(fromType: string, toType: string, cg: CodeGenerator): number { @@ -443,7 +443,7 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi ReturnStatement: (node: Node, cg: CodeGenerator) => { const { exp: expr } = node as ReturnStatement - + // Emit finally blocks from innermost to outermost before returning for (let i = cg.finallyBlockStack.length - 1; i >= 0; i--) { const finallyBlock = cg.finallyBlockStack[i] as any @@ -451,7 +451,7 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi compile(stmt, cg) }) } - + if (expr) { const { stackSize: stackSize, resultType: resultType } = compile(expr, cg) cg.code.push(resultType in returnOp ? returnOp[resultType] : OPCODE.ARETURN) @@ -470,7 +470,7 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi compile(stmt, cg) }) } - + if (cg.loopLabels.length > 0) { // If inside a loop, break jumps to the end of the loop cg.addBranchInstr(OPCODE.GOTO, cg.loopLabels[cg.loopLabels.length - 1][1]) @@ -491,7 +491,7 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi compile(stmt, cg) }) } - + cg.addBranchInstr(OPCODE.GOTO, cg.loopLabels[cg.loopLabels.length - 1][0]) return { stackSize: 0, resultType: EMPTY_TYPE } }, @@ -668,7 +668,9 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi try { catchClassName = cg.symbolTable.queryClass(catchTypeName).name } catch (e) { - catchClassName = catchTypeName.includes('/') ? catchTypeName : catchTypeName.replace(/\./g, '/') + catchClassName = catchTypeName.includes('/') + ? catchTypeName + : catchTypeName.replace(/\./g, '/') } const catchTypeIndex = cg.constantPoolManager.indexClassInfo(catchClassName) @@ -1052,30 +1054,30 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi // --- Handle super. calls --- if (n.identifier.startsWith('super.')) { candidateMethods = cg.symbolTable.queryMethod(n.identifier.slice(6)) as MethodInfos - candidateMethods = candidateMethods.filter(method => - method.className == cg.symbolTable.queryClass(cg.currentClass).parentClassName) - cg.code.push(OPCODE.ALOAD, 0); + candidateMethods = candidateMethods.filter( + method => method.className == cg.symbolTable.queryClass(cg.currentClass).parentClassName + ) + cg.code.push(OPCODE.ALOAD, 0) } // --- Handle qualified calls (e.g. System.out.println or p.show) --- else if (n.identifier.includes('.')) { - const lastDot = n.identifier.lastIndexOf('.'); - const receiverStr = n.identifier.slice(0, lastDot); + const lastDot = n.identifier.lastIndexOf('.') + const receiverStr = n.identifier.slice(0, lastDot) if (receiverStr === 'this') { candidateMethods = cg.symbolTable.queryMethod(n.identifier.slice(5)) as MethodInfos - candidateMethods = candidateMethods.filter(method => - method.className == cg.currentClass) - cg.code.push(OPCODE.ALOAD, 0); + candidateMethods = candidateMethods.filter(method => method.className == cg.currentClass) + cg.code.push(OPCODE.ALOAD, 0) } else { - const recvRes = compile({ kind: 'ExpressionName', name: receiverStr }, cg); - maxStack = Math.max(maxStack, recvRes.stackSize); + const recvRes = compile({ kind: 'ExpressionName', name: receiverStr }, cg) + maxStack = Math.max(maxStack, recvRes.stackSize) candidateMethods = cg.symbolTable.queryMethod(n.identifier).pop() as MethodInfos } } // --- Handle unqualified calls --- else { candidateMethods = cg.symbolTable.queryMethod(n.identifier) as MethodInfos - unqualifiedCall = true; + unqualifiedCall = true } // Filter candidate methods by matching the argument list. @@ -1117,11 +1119,15 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi .slice(1, methodMatches[i].typeDescriptor.indexOf(')')) .match(/(\[+[BCDFIJSZ])|(\[+L[^;]+;)|[BCDFIJSZ]|L[^;]+;/g) || [] if ( - candParams.map((p, idx) => isSubtype(p, currParams[idx], cg)).reduce((a, b) => a && b, true) + candParams + .map((p, idx) => isSubtype(p, currParams[idx], cg)) + .reduce((a, b) => a && b, true) ) { selectedMethod = methodMatches[i] } else if ( - !currParams.map((p, idx) => isSubtype(p, candParams[idx], cg)).reduce((a, b) => a && b, true) + !currParams + .map((p, idx) => isSubtype(p, candParams[idx], cg)) + .reduce((a, b) => a && b, true) ) { throw new AmbiguousMethodCallError(n.identifier + argDescs.join(',')) } @@ -1480,7 +1486,7 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi try { info = cg.symbolTable.queryVariable(name) } catch (e) { - return { stackSize: 1, resultType: 'Ljava/lang/Class;' }; + return { stackSize: 1, resultType: 'Ljava/lang/Class;' } } if (Array.isArray(info)) { const fieldInfos = info @@ -1684,7 +1690,12 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi // Generate lookup table (pairs of case values and corresponding labels) caseValues.forEach((value, index) => { // push 4-byte key - cg.code.push((value >> 24) & 0xff, (value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff) + cg.code.push( + (value >> 24) & 0xff, + (value >> 16) & 0xff, + (value >> 8) & 0xff, + value & 0xff + ) // reserve 4 bytes for the branch target cg.code.push(0, 0, 0, 0) // label offset starts after the 4-byte key @@ -1827,7 +1838,12 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi // Populate LOOKUPSWITCH const hashLabels: Label[] = [] hashCaseMap.forEach((label, hashCode) => { - cg.code.push((hashCode >> 24) & 0xff, (hashCode >> 16) & 0xff, (hashCode >> 8) & 0xff, hashCode & 0xff) + cg.code.push( + (hashCode >> 24) & 0xff, + (hashCode >> 16) & 0xff, + (hashCode >> 8) & 0xff, + hashCode & 0xff + ) // reserve 4 bytes for the branch target cg.code.push(0, 0, 0, 0) // label offset starts after the 4-byte key diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 3e116800..a186c598 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -58,15 +58,18 @@ export class Compiler { const className = decl.typeIdentifier const parentClassName = decl.sclass ? decl.sclass : 'java/lang/Object' const accessFlags = generateClassAccessFlags(decl.classModifier) - this.symbolTable.insertClassInfo( - { name: className, accessFlags: accessFlags, parentClassName: parentClassName }) + this.symbolTable.insertClassInfo({ + name: className, + accessFlags: accessFlags, + parentClassName: parentClassName + }) this.symbolTable.returnToRoot() }) ast.topLevelClassOrInterfaceDeclarations.forEach(decl => { this.resetClassFileState() const classFile = this.compileClass(decl) - classFiles.push({classFile: classFile, className: this.className}) + classFiles.push({ classFile: classFile, className: this.className }) }) return classFiles diff --git a/src/compiler/error.ts b/src/compiler/error.ts index 1044d4fd..e0355528 100644 --- a/src/compiler/error.ts +++ b/src/compiler/error.ts @@ -50,4 +50,4 @@ export class OverrideFinalMethodError extends CompileError { constructor(name: string) { super(`Cannot override final method ${name}`) } -} \ No newline at end of file +} diff --git a/src/compiler/symbol-table.ts b/src/compiler/symbol-table.ts index 394ffd34..314ebcc4 100644 --- a/src/compiler/symbol-table.ts +++ b/src/compiler/symbol-table.ts @@ -1,18 +1,19 @@ import { UnannType } from '../ast/types/classes' import { ImportDeclaration } from '../ast/types/packages-and-modules' +import { METHOD_FLAGS } from '../ClassFile/types/methods' import { generateClassAccessFlags, generateFieldAccessFlags, generateMethodAccessFlags } from './compiler-utils' import { - InvalidMethodCallError, OverrideFinalMethodError, + InvalidMethodCallError, + OverrideFinalMethodError, SymbolCannotBeResolvedError, SymbolNotFoundError, SymbolRedeclarationError } from './error' import { libraries } from './import/libs' -import { METHOD_FLAGS } from '../ClassFile/types/methods' export const typeMap = new Map([ ['byte', 'B'], @@ -209,14 +210,17 @@ export class SymbolTable { const key = generateSymbol(info.name, SymbolType.METHOD) for (let i = this.curClassIdx - 1; i > 0; i--) { - const parentTable = this.tables[i]; + const parentTable = this.tables[i] if (parentTable.has(key)) { - const parentMethods = parentTable.get(key)!.info; + const parentMethods = parentTable.get(key)!.info if (Array.isArray(parentMethods)) { for (const m of parentMethods) { - if (m.typeDescriptor === info.typeDescriptor && (m.accessFlags & METHOD_FLAGS.ACC_FINAL) - && m.className == info.parentClassName) { - throw new OverrideFinalMethodError(info.name); + if ( + m.typeDescriptor === info.typeDescriptor && + m.accessFlags & METHOD_FLAGS.ACC_FINAL && + m.className == info.parentClassName + ) { + throw new OverrideFinalMethodError(info.name) } } } diff --git a/src/jvm/exception-table.ts b/src/jvm/exception-table.ts index 7bcf0d99..b9974fd9 100644 --- a/src/jvm/exception-table.ts +++ b/src/jvm/exception-table.ts @@ -1,45 +1,45 @@ -import { ClassData } from './types/class/ClassData' +import { ConstantClass } from './types/class/Constants' export interface ExceptionTableEntry { - startPc: number - endPc: number - handlerPc: number - catchType: any | null + startPc: number + endPc: number + handlerPc: number + catchType: ConstantClass | null } export class ExceptionTable implements Iterable { - private entries: ExceptionTableEntry[] - - constructor(entries?: ExceptionTableEntry[]) { - this.entries = entries ? entries.slice() : [] - } - - retrieve(pc: number): ExceptionTableEntry | null { - for (let i = 0; i < this.entries.length; i++) { - const e = this.entries[i] - if (pc >= e.startPc && pc < e.endPc) { - return e - } - } - return null - } - - insert(startPc: number, endPc: number, handlerPc: number, catchType: ClassData | null): void { - this.entries.push({ startPc, endPc, handlerPc, catchType }) - } - - toArray(): ExceptionTableEntry[] { - return this.entries.slice() + private entries: ExceptionTableEntry[] + + constructor(entries?: ExceptionTableEntry[]) { + this.entries = entries ? entries.slice() : [] + } + + retrieve(pc: number): ExceptionTableEntry | null { + for (let i = 0; i < this.entries.length; i++) { + const e = this.entries[i] + if (pc >= e.startPc && pc < e.endPc) { + return e + } } - - [Symbol.iterator](): Iterator { - return this.entries[Symbol.iterator]() - } - forEach(cb: (entry: ExceptionTableEntry, idx?: number) => void) { - this.entries.forEach(cb) - } - - get length() { - return this.entries.length - } -} \ No newline at end of file + return null + } + + insert(startPc: number, endPc: number, handlerPc: number, catchType: ConstantClass | null): void { + this.entries.push({ startPc, endPc, handlerPc, catchType }) + } + + toArray(): ExceptionTableEntry[] { + return this.entries.slice() + } + + [Symbol.iterator](): Iterator { + return this.entries[Symbol.iterator]() + } + forEach(cb: (entry: ExceptionTableEntry, idx?: number) => void) { + this.entries.forEach(cb) + } + + get length() { + return this.entries.length + } +} diff --git a/src/jvm/types/class/Method.ts b/src/jvm/types/class/Method.ts index 8e803643..a6c3ec74 100644 --- a/src/jvm/types/class/Method.ts +++ b/src/jvm/types/class/Method.ts @@ -5,8 +5,8 @@ import Thread from '../../thread' import { attrInfo2Interface, parseMethodDescriptor, getArgs, logger } from '../../utils' import { ErrorResult, ImmediateResult, ResultType, SuccessResult } from '../Result' import { JavaType, JvmObject } from '../reference/Object' -import { Code, Exceptions, IAttribute, NestHost, Signature } from './Attributes' import { ExceptionTable } from '../../exception-table' +import { Code, Exceptions, IAttribute, NestHost, Signature } from './Attributes' import { ReferenceClassData, ArrayClassData, ClassData } from './ClassData' import { ConstantClass, ConstantMethodref, ConstantNameAndType, ConstantUtf8 } from './Constants' diff --git a/src/types/typeFactories/methodFactory.ts b/src/types/typeFactories/methodFactory.ts index c415fd10..4190d62e 100644 --- a/src/types/typeFactories/methodFactory.ts +++ b/src/types/typeFactories/methodFactory.ts @@ -70,8 +70,8 @@ export const createMethod = ( // Add declared exceptions (throws clause) if present const throwsNode: any = node.kind === 'MethodDeclaration' ? node.methodHeader.throws : node.throws - if (throwsNode && (throwsNode as any).exceptionTypeList) { - for (const exceptionTypeNode of (throwsNode as any).exceptionTypeList) { + if (throwsNode?.exceptionTypeList) { + for (const exceptionTypeNode of throwsNode.exceptionTypeList) { const exceptionType = frame.getType( unannTypeToString(exceptionTypeNode), exceptionTypeNode.location From 30621d80efdbdf69d165a7564ab7ebb0dca6e45f Mon Sep 17 00:00:00 2001 From: kjw142857 <122250318+kjw142857@users.noreply.github.com> Date: Mon, 7 Sep 2026 12:04:18 +0800 Subject: [PATCH 4/4] Fix bug with emitting of finally instruction (#102) * fix bug with emitting of finally instn before return statement * add extra test for break statement --- src/compiler/__tests__/try.test.ts | 64 +++++++++++++++++++++++++++++- src/compiler/code-generator.ts | 25 ++++++++---- 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/src/compiler/__tests__/try.test.ts b/src/compiler/__tests__/try.test.ts index 6c9afdce..09f0be0f 100644 --- a/src/compiler/__tests__/try.test.ts +++ b/src/compiler/__tests__/try.test.ts @@ -37,8 +37,68 @@ const testCases: testCase[] = [ } `, expectedLines: ["2", "3", "4"], - } - , + }, + { + comment: "try/catch/finally runs finally after a throwing return expression", + program: ` + public class Main { + public static int divide(int x, int y) { + try { + return x / y; + } catch (Exception e) { + System.out.println(1); + return 0; + } finally { + System.out.println(2); + } + } + + public static void main(String[] args) { + int z = divide(1, 0); + System.out.println(0); + } + } + `, + expectedLines: ["1", "2", "0"], + }, + { + comment: "try/finally runs finally after a normal return expression", + program: ` + public class Main { + public static int value() { + try { + return 1; + } finally { + System.out.println(2); + } + } + + public static void main(String[] args) { + System.out.println(value()); + } + } + `, + expectedLines: ["2", "1"], + }, + { + comment: "try/finally runs finally before a break exits its loop", + program: ` + public class Main { + public static void main(String[] args) { + while (true) { + try { + System.out.println(1); + break; + } finally { + System.out.println(2); + } + } + System.out.println(0); + } + } + `, + expectedLines: ["1", "2", "0"], + }, { comment: "static helper method throws exception and catch handles it", program: ` diff --git a/src/compiler/code-generator.ts b/src/compiler/code-generator.ts index 9506a1f2..32121dad 100644 --- a/src/compiler/code-generator.ts +++ b/src/compiler/code-generator.ts @@ -444,20 +444,29 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi ReturnStatement: (node: Node, cg: CodeGenerator) => { const { exp: expr } = node as ReturnStatement - // Emit finally blocks from innermost to outermost before returning - for (let i = cg.finallyBlockStack.length - 1; i >= 0; i--) { - const finallyBlock = cg.finallyBlockStack[i] as any - finallyBlock.blockStatements.forEach((stmt: any) => { - compile(stmt, cg) - }) - } - if (expr) { const { stackSize: stackSize, resultType: resultType } = compile(expr, cg) + if (cg.finallyBlockStack.length > 0) { + const tempIndex = cg.maxLocals + cg.maxLocals += ['J', 'D'].includes(resultType) ? 2 : 1 + cg.code.push(resultType in normalStoreOp ? normalStoreOp[resultType] : OPCODE.ASTORE, tempIndex) + + for (let i = cg.finallyBlockStack.length - 1; i >= 0; i--) { + const finallyBlock = cg.finallyBlockStack[i] as any + finallyBlock.blockStatements.forEach((stmt: any) => compile(stmt, cg)) + } + + cg.code.push(resultType in normalLoadOp ? normalLoadOp[resultType] : OPCODE.ALOAD, tempIndex) + } cg.code.push(resultType in returnOp ? returnOp[resultType] : OPCODE.ARETURN) return { stackSize, resultType } } + for (let i = cg.finallyBlockStack.length - 1; i >= 0; i--) { + const finallyBlock = cg.finallyBlockStack[i] as any + finallyBlock.blockStatements.forEach((stmt: any) => compile(stmt, cg)) + } + cg.code.push(OPCODE.RETURN) return { stackSize: 0, resultType: cg.symbolTable.generateFieldDescriptor('void') } },