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__/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..c5d468e8 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 = { @@ -576,6 +577,182 @@ 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 + }> = [] + + // 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 } + }, + TernaryExpression: (node: Node, cg: CodeGenerator) => { let maxStack = 0 const { @@ -1723,6 +1900,7 @@ class CodeGenerator { constantPoolManager: ConstantPoolManager maxLocals: number = 0 stackSize: number = 0 + exceptionTable: Array = [] labels: Label[] = [] loopLabels: Label[][] = [] switchLabels: Label[] = [] @@ -1761,6 +1939,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 +1978,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 +1986,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 +1997,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..f0ffbed8 100644 --- a/src/compiler/import/lib-info.ts +++ b/src/compiler/import/lib-info.ts @@ -13,6 +13,63 @@ 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.Error' + }, + { + name: 'public class java.lang.Exception' + }, + { + name: 'public class java.lang.RuntimeException' + }, + { + name: 'public class java.lang.ArithmeticException' + }, + { + name: 'public class java.lang.ArrayIndexOutOfBoundsException' + }, + { + name: 'public class java.lang.ArrayStoreException' + }, + { + name: 'public class java.lang.ClassCastException' + }, + { + name: 'public class java.lang.IllegalArgumentException' + }, + { + name: 'public class java.lang.IllegalMonitorStateException' + }, + { + name: 'public class java.lang.IllegalStateException' + }, + { + name: 'public class java.lang.IndexOutOfBoundsException' + }, + { + name: 'public class java.lang.NegativeArraySizeException' + }, + { + name: 'public class java.lang.NullPointerException' + }, + { + name: 'public class java.lang.NumberFormatException' + }, + { + name: 'public class java.lang.StringIndexOutOfBoundsException' + }, + { + name: 'public class java.lang.UnsupportedOperationException' + }, + { + name: 'public class java.lang.SecurityException' + }, + { + name: 'public class java.lang.IllegalThreadStateException' + }, { 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..b42b1c80 100644 --- a/src/types/checker/environment.ts +++ b/src/types/checker/environment.ts @@ -7,43 +7,119 @@ import { Array } from '../types/arrays' import { Class, ClassType } from '../types/classes' import { Location } from '../ast/specificationTypes' import { isArrayType, removeArraySuffix } from './arrays' +import { libraries } from '../../compiler/import/libs' + +const BUILT_IN_TYPE_FACTORIES: { [name: string]: () => Type } = { + boolean: () => new Primitives.Boolean(), + byte: () => new Primitives.Byte(), + char: () => new Primitives.Char(), + double: () => new Primitives.Double(), + float: () => new Primitives.Float(), + int: () => new Primitives.Int(), + long: () => new Primitives.Long(), + short: () => new Primitives.Short(), + void: () => new NonPrimitives.Void(), + Boolean: () => new NonPrimitives.Boolean(), + Byte: () => new NonPrimitives.Byte(), + Character: () => new NonPrimitives.Character(), + Double: () => new NonPrimitives.Double(), + Float: () => new NonPrimitives.Float(), + Integer: () => new NonPrimitives.Integer(), + Long: () => new NonPrimitives.Long(), + Short: () => new NonPrimitives.Short(), + String: () => new NonPrimitives.String() +} + +const EXCEPTION_INHERITANCE: { [child: string]: string } = { + Error: 'Throwable', + Exception: 'Throwable', + RuntimeException: 'Exception', + ArithmeticException: 'RuntimeException', + ArrayIndexOutOfBoundsException: 'RuntimeException', + ArrayStoreException: 'RuntimeException', + ClassCastException: 'RuntimeException', + IllegalArgumentException: 'RuntimeException', + IllegalMonitorStateException: 'RuntimeException', + IllegalStateException: 'RuntimeException', + IndexOutOfBoundsException: 'RuntimeException', + NegativeArraySizeException: 'RuntimeException', + NullPointerException: 'RuntimeException', + NumberFormatException: 'RuntimeException', + StringIndexOutOfBoundsException: 'IndexOutOfBoundsException', + UnsupportedOperationException: 'RuntimeException', + SecurityException: 'RuntimeException', + IllegalThreadStateException: 'RuntimeException' +} + +const stdlibTypeMap = new Map() + +const createType = (typeName: string): Type => { + if (stdlibTypeMap.has(typeName)) return stdlibTypeMap.get(typeName)! + + const factory = BUILT_IN_TYPE_FACTORIES[typeName] + const type = factory ? factory() : new ClassType(typeName) + stdlibTypeMap.set(typeName, type) + return type +} -const SYSTEM_CLASS = new ClassType('System') -const PRINTSTREAM_CLASS = new ClassType('PrintStream') -SYSTEM_CLASS.addField('out', PRINTSTREAM_CLASS, { startLine: -1, startOffset: -1 }) -const PRINTLN_METHOD_1 = new Method('println') -PRINTLN_METHOD_1.addParameter(new Parameter('message', new NonPrimitives.String())) -const PRINTLN_METHOD_2 = new Method('println') -PRINTLN_METHOD_2.addParameter(new Parameter('message', new Primitives.Int())) -PRINTSTREAM_CLASS.addMethod('println', PRINTLN_METHOD_1, { startLine: -1, startOffset: -1 }) -PRINTSTREAM_CLASS.addMethod('println', PRINTLN_METHOD_2, { startLine: -1, startOffset: -1 }) - -const GLOBAL_TYPE_ENVIRONMENT: { [key: string]: Type } = { - boolean: new Primitives.Boolean(), - byte: new Primitives.Byte(), - char: new Primitives.Char(), - double: new Primitives.Double(), - float: new Primitives.Float(), - int: new Primitives.Int(), - long: new Primitives.Long(), - short: new Primitives.Short(), - void: new NonPrimitives.Void(), - Boolean: new NonPrimitives.Boolean(), - Byte: new NonPrimitives.Byte(), - Character: new NonPrimitives.Character(), - Double: new NonPrimitives.Double(), - Float: new NonPrimitives.Float(), - Integer: new NonPrimitives.Integer(), - Long: new NonPrimitives.Long(), - Short: new NonPrimitives.Short(), - String: new NonPrimitives.String(), - - // Hard coded variables - System: SYSTEM_CLASS, - Throwable: new NonPrimitives.Throwable(), - Exception: new NonPrimitives.Exception() +const parseType = (typeName: string): Type => { + if (typeName.endsWith('[]')) { + return new Array(parseType(typeName.slice(0, -2))) + } + return createType(typeName.replaceAll('/', '.').split('.').pop() || typeName) } +const buildStandardLibraryTypes = (): { [key: string]: Type } => { + // Preload built-in type objects + Object.keys(BUILT_IN_TYPE_FACTORIES).forEach(typeName => createType(typeName)) + + const getSimpleName = (qualifiedName: string) => { + const lastToken = qualifiedName.replaceAll('.', '/').split('/').pop() || qualifiedName + return lastToken + } + + libraries.forEach(pkg => { + pkg.classes.forEach(clazz => { + const className = getSimpleName(clazz.className) + createType(className) + }) + }) + + libraries.forEach(pkg => { + pkg.classes.forEach(clazz => { + const className = getSimpleName(clazz.className) + const classType = createType(className) + if (!(classType instanceof ClassType)) return + + clazz.fields.forEach(field => { + const fieldType = parseType(field.typeName) + classType.addField(field.fieldName, fieldType, { startLine: -1, startOffset: -1 }) + }) + + clazz.methods.forEach(methodInfo => { + const method = new Method(methodInfo.methodName, parseType(methodInfo.returnTypeName)) + methodInfo.argsTypeName.forEach((argTypeName, index) => { + const parameter = new Parameter(`arg${index}`, parseType(argTypeName)) + method.addParameter(parameter) + }) + classType.addMethod(methodInfo.methodName, method, { startLine: -1, startOffset: -1 }) + }) + }) + }) + + Object.entries(EXCEPTION_INHERITANCE).forEach(([child, parent]) => { + const childType = createType(child) + const parentType = createType(parent) + if (childType instanceof ClassType && parentType instanceof ClassType) { + childType.setParentClass(parentType) + } + }) + + return Object.fromEntries(stdlibTypeMap.entries()) +} + +const GLOBAL_TYPE_ENVIRONMENT: { [key: string]: Type } = buildStandardLibraryTypes() + export class Frame { private _currentClass: Class private _methods = new Map() @@ -51,6 +127,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 +149,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(', ')}` } }