From 810656f0db6fa23bf99365c4eb7ee77c57a546e2 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Tue, 16 Jun 2026 07:14:34 +0800 Subject: [PATCH 01/13] jvm changes --- 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 +- 5 files changed, 84 insertions(+), 33 deletions(-) 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) From b1593a34b437df419177098cded829295daf6bb9 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Tue, 16 Jun 2026 12:08:16 +0800 Subject: [PATCH 02/13] include try/catch/finally support in code generator --- src/compiler/code-generator.ts | 127 +++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 4 deletions(-) diff --git a/src/compiler/code-generator.ts b/src/compiler/code-generator.ts index 4777a0b0..fecb78d8 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,123 @@ 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 + + // If no catches, just compile the try block + if (!catches || !catches.catchClauses || catches.catchClauses.length === 0) { + return { stackSize: compile(block, cg).stackSize, resultType: EMPTY_TYPE } + } + + // 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 + + // 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 + 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) + cg.exceptionTable.push({ startPc: tryStart.offset, endPc: tryEnd.offset, handlerPc: handlerLabel.offset, 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) + + // compile catch block statements + const catchBlock = catchClause.block + catchBlock.blockStatements.forEach((stmt: any) => { + const { stackSize } = compile(stmt, cg) + maxStack = Math.max(maxStack, stackSize) + }) + + // teardown catch scope + cg.symbolTable.teardown() + + // after handler, jump to afterHandlers + cg.addBranchInstr(OPCODE.GOTO, afterHandlers) + } + + // If finally exists, add a catch-all handler that runs finally then rethrows + const finallyNode: any = (node as any).finally + if (finallyNode) { + const catchAllLabel = cg.generateNewLabel() + catchAllLabel.offset = cg.code.length + cg.exceptionTable.push({ startPc: tryStart.offset, endPc: tryEnd.offset, handlerPc: catchAllLabel.offset, catchType: 0 }) + + // 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.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) + + // normal finally path: compile finally once for normal/handled flows + const finallyLabel = cg.generateNewLabel() + finallyLabel.offset = cg.code.length + finallyNode.blockStatements.forEach((stmt: any) => { + const { stackSize } = compile(stmt, cg) + maxStack = Math.max(maxStack, stackSize) + }) + + // place after-handlers label + afterHandlers.offset = cg.code.length + } else { + // no finally: place after-handlers label + afterHandlers.offset = cg.code.length + } + + return { stackSize: maxStack, resultType: EMPTY_TYPE } + }, + TernaryExpression: (node: Node, cg: CodeGenerator) => { let maxStack = 0 const { @@ -1684,6 +1802,7 @@ class CodeGenerator { constantPoolManager: ConstantPoolManager maxLocals: number = 0 stackSize: number = 0 + exceptionTable: Array = [] labels: Label[] = [] loopLabels: Label[][] = [] switchLabels: Label[] = [] @@ -1722,6 +1841,7 @@ class CodeGenerator { generateCode(currentClass: string, methodNode: MethodDeclaration) { this.symbolTable.extend() this.currentClass = currentClass + this.exceptionTable = [] if (!methodNode.methodModifier.includes('static')) { this.maxLocals++ } @@ -1760,7 +1880,6 @@ class CodeGenerator { } this.resolveLabels() - const exceptionTable: Array = [] const attributes: Array = [] const codeBuf = new Uint8Array(this.code).buffer const dataView = new DataView(codeBuf) @@ -1769,7 +1888,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() @@ -1780,8 +1899,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 } From 12926df9a0d78c3e4beaf3bcb5bcf308b5806c48 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 24 Jun 2026 08:56:18 +0800 Subject: [PATCH 03/13] fix try statement logic --- src/compiler/code-generator.ts | 186 +++++++++++++++++++++------------ 1 file changed, 122 insertions(+), 64 deletions(-) diff --git a/src/compiler/code-generator.ts b/src/compiler/code-generator.ts index fecb78d8..de3d16c7 100644 --- a/src/compiler/code-generator.ts +++ b/src/compiler/code-generator.ts @@ -580,12 +580,25 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi 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 no catches, just compile the try block - if (!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 @@ -597,70 +610,116 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi const tryEnd = cg.generateNewLabel() tryEnd.offset = cg.code.length + const catchAllLabel = finallyNode ? cg.generateNewLabel() : null + + // If finally exists, add catch-all entry for the try block + if (finallyNode && catchAllLabel) { + localExceptionTable.push({ + startPc: tryStart.offset, + endPc: tryEnd.offset, + handlerLabel: catchAllLabel, + catchType: 0 + }) + } + + // For normal path: run finally block if it exists + if (finallyNode) { + finallyNode.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 - 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) + 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 + }) - // add exception table entry (startPc, endPc, handlerPc, catchType) - cg.exceptionTable.push({ startPc: tryStart.offset, endPc: tryEnd.offset, handlerPc: handlerLabel.offset, 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++ + } - // 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) - // 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) - }) + // 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() + // 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.blockStatements.forEach((stmt: any) => { + const { stackSize } = compile(stmt, cg) + maxStack = Math.max(maxStack, stackSize) + }) + } - // after handler, jump to afterHandlers - cg.addBranchInstr(OPCODE.GOTO, afterHandlers) + // after handler, jump to afterHandlers + cg.addBranchInstr(OPCODE.GOTO, afterHandlers) + } } // If finally exists, add a catch-all handler that runs finally then rethrows - const finallyNode: any = (node as any).finally - if (finallyNode) { - const catchAllLabel = cg.generateNewLabel() + if (finallyNode && catchAllLabel) { catchAllLabel.offset = cg.code.length - cg.exceptionTable.push({ startPc: tryStart.offset, endPc: tryEnd.offset, handlerPc: catchAllLabel.offset, catchType: 0 }) // allocate temp local to store exception const tempIndex = cg.maxLocals @@ -675,21 +734,20 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi // reload exception and rethrow cg.code.push(OPCODE.ALOAD, tempIndex, OPCODE.ATHROW) + } - // normal finally path: compile finally once for normal/handled flows - const finallyLabel = cg.generateNewLabel() - finallyLabel.offset = cg.code.length - finallyNode.blockStatements.forEach((stmt: any) => { - const { stackSize } = compile(stmt, cg) - maxStack = Math.max(maxStack, stackSize) - }) + // place after-handlers label + afterHandlers.offset = cg.code.length - // place after-handlers label - afterHandlers.offset = cg.code.length - } else { - // no finally: 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 } }, From 8f7d8b4d0aab49c09824fca2b1e3145e31f90b92 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 1 Jul 2026 08:44:44 +0800 Subject: [PATCH 04/13] add parser and type checker integration --- 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 ++++++- .../checker/__tests__/tryStatement.test.ts | 11 ++ 5 files changed, 254 insertions(+), 8 deletions(-) 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/types/checker/__tests__/tryStatement.test.ts b/src/types/checker/__tests__/tryStatement.test.ts index 975a7246..4d4383d0 100644 --- a/src/types/checker/__tests__/tryStatement.test.ts +++ b/src/types/checker/__tests__/tryStatement.test.ts @@ -34,6 +34,17 @@ const testcases: { `, result: { type: null, errors: [new ExceptionHasAlreadyBeenCaughtError()] } }, + { + input: ` + try { + throw new Exception(); + } catch (Exception e) { + throw new Exception(); + } finally { + } + `, + result: { type: null, errors: [] } + }, { input: ` try {} From 79a4e6b09457b21e846c0da821ddcc03c037c6a3 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 1 Jul 2026 08:57:46 +0800 Subject: [PATCH 05/13] fix finally bug --- src/compiler/code-generator.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/compiler/code-generator.ts b/src/compiler/code-generator.ts index 3bae20b7..f563128c 100644 --- a/src/compiler/code-generator.ts +++ b/src/compiler/code-generator.ts @@ -624,7 +624,7 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi // For normal path: run finally block if it exists if (finallyNode) { - finallyNode.blockStatements.forEach((stmt: any) => { + finallyNode.block.blockStatements.forEach((stmt: any) => { const { stackSize } = compile(stmt, cg) maxStack = Math.max(maxStack, stackSize) }) @@ -706,7 +706,7 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi // For caught path: run finally block if it exists if (finallyNode) { - finallyNode.blockStatements.forEach((stmt: any) => { + finallyNode.block.blockStatements.forEach((stmt: any) => { const { stackSize } = compile(stmt, cg) maxStack = Math.max(maxStack, stackSize) }) @@ -727,7 +727,7 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi cg.code.push(OPCODE.ASTORE, tempIndex) // compile finally block inside catch-all - finallyNode.blockStatements.forEach((stmt: any) => { + finallyNode.block.blockStatements.forEach((stmt: any) => { const { stackSize } = compile(stmt, cg) maxStack = Math.max(maxStack, stackSize) }) From 2daa965076553d387fc91e7f3e6e18a835f77979 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 8 Jul 2026 08:44:50 +0800 Subject: [PATCH 06/13] add tests and fix syntax error --- src/compiler/__tests__/try.test.ts | 26 +++++ src/compiler/grammar.pegjs | 68 ++++++++++++- src/compiler/grammar.ts | 68 ++++++++++++- src/compiler/import/lib-info.ts | 6 ++ .../checker/__tests__/tryStatement.test.ts | 52 +++++++++- src/types/checker/environment.ts | 21 ++++ src/types/checker/index.ts | 97 ++++++++++++++++--- src/types/errors.ts | 6 ++ src/types/typeFactories/methodFactory.ts | 15 +++ src/types/types/methods.ts | 9 ++ src/types/types/throws.ts | 15 ++- 11 files changed, 357 insertions(+), 26 deletions(-) create mode 100644 src/compiler/__tests__/try.test.ts diff --git a/src/compiler/__tests__/try.test.ts b/src/compiler/__tests__/try.test.ts new file mode 100644 index 00000000..071b1a32 --- /dev/null +++ b/src/compiler/__tests__/try.test.ts @@ -0,0 +1,26 @@ +import { runTest, testCase } from "./__utils__/test-utils"; + +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"], + }, +]; + +describe("try/catch", () => { + for (const testCase of testCases) { + it(testCase.comment, () => runTest(testCase.program, testCase.expectedLines)); + } +}); diff --git a/src/compiler/grammar.pegjs b/src/compiler/grammar.pegjs index 505f648e..0f593f3d 100755 --- a/src/compiler/grammar.pegjs +++ b/src/compiler/grammar.pegjs @@ -854,8 +854,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..226aec51 100755 --- a/src/compiler/grammar.ts +++ b/src/compiler/grammar.ts @@ -856,8 +856,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/types/checker/__tests__/tryStatement.test.ts b/src/types/checker/__tests__/tryStatement.test.ts index 4d4383d0..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,47 @@ 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: ` @@ -59,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..5aff4ec2 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,51 @@ 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 + for (let i = 0; i < methods.length; i++) { const result = methods[i].invoke(argumentList) if (result instanceof TypeCheckerError) continue - return newResult(result, errors) + selectedMethod = methods[i] + selectedReturnType = result + break } - const returnType = methods[methods.length - 1].invoke(argumentList) - if (returnType instanceof TypeCheckerError) return newResult(null, [...errors, returnType]) - return newResult(returnType, errors) + if (selectedMethod === null || selectedReturnType === null) + return newResult(null, [...errors, new MethodCannotBeAppliedError(node.location)]) + + // 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 +560,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 +608,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 +719,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 +733,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 +742,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 +772,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 8e933b623ec015d1d381039bd4473e9515f82d95 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Thu, 16 Jul 2026 08:02:39 +0800 Subject: [PATCH 07/13] Patch grammar logic for throws keyword --- parse_exact.js | 23 +++++++++++++++++++++++ parse_parent.js | 21 +++++++++++++++++++++ parse_test.js | 20 ++++++++++++++++++++ peggy_parse.js | 24 ++++++++++++++++++++++++ reproduce_bad_wrap.js | 21 +++++++++++++++++++++ src/ast/parser.ts | 14 +++++++++++++- src/compiler/grammar.pegjs | 16 +++++++++++++++- src/compiler/grammar.ts | 16 +++++++++++++++- src/types/checker/index.ts | 13 +++++++++++-- tools/inspectSnippet.js | 0 10 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 parse_exact.js create mode 100644 parse_parent.js create mode 100644 parse_test.js create mode 100644 peggy_parse.js create mode 100644 reproduce_bad_wrap.js create mode 100644 tools/inspectSnippet.js diff --git a/parse_exact.js b/parse_exact.js new file mode 100644 index 00000000..f8231e7f --- /dev/null +++ b/parse_exact.js @@ -0,0 +1,23 @@ +const jp = require('java-parser'); +const src = `class Main { + public static void foo() throws Exception { + throw new Exception(); + } + + public static void main(String args[]) { + try { + foo(); + } catch (Exception e) { + } + } +}`; +console.log('Source length:', src.length); +try { + const cst = jp.parse(src); + console.log('Parsed OK'); + // Print a small snippet of cst root type to confirm + console.log('Root type:', cst.name || Object.keys(cst)[0]); +} catch (e) { + console.error('ERROR:', e.message); + if (e.location) console.error('Location:', JSON.stringify(e.location)); +} diff --git a/parse_parent.js b/parse_parent.js new file mode 100644 index 00000000..2c40da7e --- /dev/null +++ b/parse_parent.js @@ -0,0 +1,21 @@ +const jp = require('java-parser'); +const src = `class Parent { + public int multiply(int x) throws Exception { + return 0; + } +} + +public class Main extends Parent { + + public static void main(String[] args) throws Exception { + Parent t = new Parent(); + int y = t.multiply(5); + } +}`; +try { + jp.parse(src); + console.log('Parsed OK'); +} catch (e) { + console.error('ERROR:', e.message); + if (e.location) console.error('Location:', JSON.stringify(e.location)); +} diff --git a/parse_test.js b/parse_test.js new file mode 100644 index 00000000..a5f63335 --- /dev/null +++ b/parse_test.js @@ -0,0 +1,20 @@ +const jp = require('java-parser'); +const src = `class Main { + public static void foo() throws Exception { + throw new Exception(); + } + public static void main(String args[]) { + try { + foo(); + } catch (Exception e) { + } + } +}`; +try { + jp.parse(src); + console.log('Parsed OK'); +} catch (e) { + console.error('ERROR:', e.message); + if (e.location) console.error('Location:', JSON.stringify(e.location)); + console.error(e.stack); +} diff --git a/peggy_parse.js b/peggy_parse.js new file mode 100644 index 00000000..ff737730 --- /dev/null +++ b/peggy_parse.js @@ -0,0 +1,24 @@ +const fs = require('fs'); +const peggy = require('peggy'); +const grammar = fs.readFileSync('src/compiler/grammar.pegjs', 'utf8'); +const parser = peggy.generate(grammar); +const src = `class Parent { + public int multiply(int x) throws Exception { + return 0; + } +} + +public class Main extends Parent { + + public static void main(String[] args) throws Exception { + Parent t = new Parent(); + int y = t.multiply(5); + } +}`; +try { + parser.parse(src); + console.log('PEG Parsed OK'); +} catch (e) { + console.error('PEG ERROR:', e.message); + if (e.location) console.error('Location:', JSON.stringify(e.location)); +} diff --git a/reproduce_bad_wrap.js b/reproduce_bad_wrap.js new file mode 100644 index 00000000..c15a6af2 --- /dev/null +++ b/reproduce_bad_wrap.js @@ -0,0 +1,21 @@ +const jp = require('java-parser'); +const inner = `public static void foo() throws Exception { + throw new Exception(); +} +public static void main(String args[]) { + try { + foo(); + } catch (Exception e) { + } +}`; +const wrapped = `public class Main { public static void main(String args[]) { ${inner} } }`; +console.log('---SOURCE---'); +console.log(wrapped); +console.log('---PARSE OUTPUT---'); +try { + jp.parse(wrapped); + console.log('Parsed OK'); +} catch (e) { + console.error('ERROR:', e.message); + if (e.location) console.error('Location:', JSON.stringify(e.location)); +} diff --git a/src/ast/parser.ts b/src/ast/parser.ts index afeb1484..91cae48c 100644 --- a/src/ast/parser.ts +++ b/src/ast/parser.ts @@ -15,6 +15,18 @@ export const parse = (programStr: string): AST => { const ast = astExtractor.extract(cst); return ast; } catch (e) { - throw new SyntaxError(e); + // Attach a short snippet of the source to help with debugging frontend submissions + try { + const msg = typeof e === 'string' ? e : (e && e.message) ? e.message : String(e); + const previewLen = 200; + const preview = programStr + ? (programStr.length <= previewLen ? programStr : programStr.slice(0, previewLen) + '\n...') + : ''; + const enhanced = `${msg}\n--- source preview (${Math.min(programStr ? programStr.length : 0, previewLen)} chars) ---\n${preview}`; + throw new SyntaxError(enhanced); + } catch (inner) { + // Fallback to original error if something goes wrong building the enhanced message + throw new SyntaxError(e); + } } } diff --git a/src/compiler/grammar.pegjs b/src/compiler/grammar.pegjs index 0f593f3d..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 { diff --git a/src/compiler/grammar.ts b/src/compiler/grammar.ts index 226aec51..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 { diff --git a/src/types/checker/index.ts b/src/types/checker/index.ts index 5aff4ec2..be7c16e5 100644 --- a/src/types/checker/index.ts +++ b/src/types/checker/index.ts @@ -482,15 +482,24 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R // 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 + if (result instanceof TypeCheckerError) { + lastInvokeError = result + continue + } selectedMethod = methods[i] selectedReturnType = result break } - if (selectedMethod === null || selectedReturnType === null) + 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)]) + } // 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. diff --git a/tools/inspectSnippet.js b/tools/inspectSnippet.js new file mode 100644 index 00000000..e69de29b From 181ae6186a29d7f786d8a9b3ad102590b564a81e Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Thu, 16 Jul 2026 08:04:40 +0800 Subject: [PATCH 08/13] Revert "Patch grammar logic for throws keyword" This reverts commit 8e933b623ec015d1d381039bd4473e9515f82d95. --- parse_exact.js | 23 ----------------------- parse_parent.js | 21 --------------------- parse_test.js | 20 -------------------- peggy_parse.js | 24 ------------------------ reproduce_bad_wrap.js | 21 --------------------- src/ast/parser.ts | 14 +------------- src/compiler/grammar.pegjs | 16 +--------------- src/compiler/grammar.ts | 16 +--------------- src/types/checker/index.ts | 13 ++----------- tools/inspectSnippet.js | 0 10 files changed, 5 insertions(+), 163 deletions(-) delete mode 100644 parse_exact.js delete mode 100644 parse_parent.js delete mode 100644 parse_test.js delete mode 100644 peggy_parse.js delete mode 100644 reproduce_bad_wrap.js delete mode 100644 tools/inspectSnippet.js diff --git a/parse_exact.js b/parse_exact.js deleted file mode 100644 index f8231e7f..00000000 --- a/parse_exact.js +++ /dev/null @@ -1,23 +0,0 @@ -const jp = require('java-parser'); -const src = `class Main { - public static void foo() throws Exception { - throw new Exception(); - } - - public static void main(String args[]) { - try { - foo(); - } catch (Exception e) { - } - } -}`; -console.log('Source length:', src.length); -try { - const cst = jp.parse(src); - console.log('Parsed OK'); - // Print a small snippet of cst root type to confirm - console.log('Root type:', cst.name || Object.keys(cst)[0]); -} catch (e) { - console.error('ERROR:', e.message); - if (e.location) console.error('Location:', JSON.stringify(e.location)); -} diff --git a/parse_parent.js b/parse_parent.js deleted file mode 100644 index 2c40da7e..00000000 --- a/parse_parent.js +++ /dev/null @@ -1,21 +0,0 @@ -const jp = require('java-parser'); -const src = `class Parent { - public int multiply(int x) throws Exception { - return 0; - } -} - -public class Main extends Parent { - - public static void main(String[] args) throws Exception { - Parent t = new Parent(); - int y = t.multiply(5); - } -}`; -try { - jp.parse(src); - console.log('Parsed OK'); -} catch (e) { - console.error('ERROR:', e.message); - if (e.location) console.error('Location:', JSON.stringify(e.location)); -} diff --git a/parse_test.js b/parse_test.js deleted file mode 100644 index a5f63335..00000000 --- a/parse_test.js +++ /dev/null @@ -1,20 +0,0 @@ -const jp = require('java-parser'); -const src = `class Main { - public static void foo() throws Exception { - throw new Exception(); - } - public static void main(String args[]) { - try { - foo(); - } catch (Exception e) { - } - } -}`; -try { - jp.parse(src); - console.log('Parsed OK'); -} catch (e) { - console.error('ERROR:', e.message); - if (e.location) console.error('Location:', JSON.stringify(e.location)); - console.error(e.stack); -} diff --git a/peggy_parse.js b/peggy_parse.js deleted file mode 100644 index ff737730..00000000 --- a/peggy_parse.js +++ /dev/null @@ -1,24 +0,0 @@ -const fs = require('fs'); -const peggy = require('peggy'); -const grammar = fs.readFileSync('src/compiler/grammar.pegjs', 'utf8'); -const parser = peggy.generate(grammar); -const src = `class Parent { - public int multiply(int x) throws Exception { - return 0; - } -} - -public class Main extends Parent { - - public static void main(String[] args) throws Exception { - Parent t = new Parent(); - int y = t.multiply(5); - } -}`; -try { - parser.parse(src); - console.log('PEG Parsed OK'); -} catch (e) { - console.error('PEG ERROR:', e.message); - if (e.location) console.error('Location:', JSON.stringify(e.location)); -} diff --git a/reproduce_bad_wrap.js b/reproduce_bad_wrap.js deleted file mode 100644 index c15a6af2..00000000 --- a/reproduce_bad_wrap.js +++ /dev/null @@ -1,21 +0,0 @@ -const jp = require('java-parser'); -const inner = `public static void foo() throws Exception { - throw new Exception(); -} -public static void main(String args[]) { - try { - foo(); - } catch (Exception e) { - } -}`; -const wrapped = `public class Main { public static void main(String args[]) { ${inner} } }`; -console.log('---SOURCE---'); -console.log(wrapped); -console.log('---PARSE OUTPUT---'); -try { - jp.parse(wrapped); - console.log('Parsed OK'); -} catch (e) { - console.error('ERROR:', e.message); - if (e.location) console.error('Location:', JSON.stringify(e.location)); -} diff --git a/src/ast/parser.ts b/src/ast/parser.ts index 91cae48c..afeb1484 100644 --- a/src/ast/parser.ts +++ b/src/ast/parser.ts @@ -15,18 +15,6 @@ export const parse = (programStr: string): AST => { const ast = astExtractor.extract(cst); return ast; } catch (e) { - // Attach a short snippet of the source to help with debugging frontend submissions - try { - const msg = typeof e === 'string' ? e : (e && e.message) ? e.message : String(e); - const previewLen = 200; - const preview = programStr - ? (programStr.length <= previewLen ? programStr : programStr.slice(0, previewLen) + '\n...') - : ''; - const enhanced = `${msg}\n--- source preview (${Math.min(programStr ? programStr.length : 0, previewLen)} chars) ---\n${preview}`; - throw new SyntaxError(enhanced); - } catch (inner) { - // Fallback to original error if something goes wrong building the enhanced message - throw new SyntaxError(e); - } + throw new SyntaxError(e); } } diff --git a/src/compiler/grammar.pegjs b/src/compiler/grammar.pegjs index ce756b3d..0f593f3d 100755 --- a/src/compiler/grammar.pegjs +++ b/src/compiler/grammar.pegjs @@ -659,21 +659,7 @@ VariableModifier = final Throws - = throws et:ExceptionTypeList { - return addLocInfo({ - kind: "Throws", - exceptionTypeList: et, - }) - } - -ExceptionTypeList - = e:ExceptionType es:(comma @ExceptionType)* { - return [e, ...es]; - } - -ExceptionType - = ClassType - / TypeIdentifier + = throw TO_BE_ADDED ConstructorDeclaration = cm:ConstructorModifier* cd:ConstructorDeclarator Throws? cb:ConstructorBody { diff --git a/src/compiler/grammar.ts b/src/compiler/grammar.ts index 4ddd9aba..226aec51 100755 --- a/src/compiler/grammar.ts +++ b/src/compiler/grammar.ts @@ -661,21 +661,7 @@ VariableModifier = final Throws - = throws et:ExceptionTypeList { - return addLocInfo({ - kind: "Throws", - exceptionTypeList: et, - }) - } - -ExceptionTypeList - = e:ExceptionType es:(comma @ExceptionType)* { - return [e, ...es]; - } - -ExceptionType - = ClassType - / TypeIdentifier + = throw TO_BE_ADDED ConstructorDeclaration = cm:ConstructorModifier* cd:ConstructorDeclarator Throws? cb:ConstructorBody { diff --git a/src/types/checker/index.ts b/src/types/checker/index.ts index be7c16e5..5aff4ec2 100644 --- a/src/types/checker/index.ts +++ b/src/types/checker/index.ts @@ -482,24 +482,15 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R // 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) { - lastInvokeError = result - continue - } + if (result instanceof TypeCheckerError) 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]) + if (selectedMethod === null || selectedReturnType === null) return newResult(null, [...errors, new MethodCannotBeAppliedError(node.location)]) - } // 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. diff --git a/tools/inspectSnippet.js b/tools/inspectSnippet.js deleted file mode 100644 index e69de29b..00000000 From ee949275b07964c4b074f86faa8dc879b65588bd Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Thu, 16 Jul 2026 08:11:41 +0800 Subject: [PATCH 09/13] Patch grammar logic for throws keyword --- src/compiler/grammar.pegjs | 16 +++++++++++++++- src/compiler/grammar.ts | 16 +++++++++++++++- src/types/checker/index.ts | 13 +++++++++++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/compiler/grammar.pegjs b/src/compiler/grammar.pegjs index 0f593f3d..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 { diff --git a/src/compiler/grammar.ts b/src/compiler/grammar.ts index 226aec51..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 { diff --git a/src/types/checker/index.ts b/src/types/checker/index.ts index 5aff4ec2..be7c16e5 100644 --- a/src/types/checker/index.ts +++ b/src/types/checker/index.ts @@ -482,15 +482,24 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R // 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 + if (result instanceof TypeCheckerError) { + lastInvokeError = result + continue + } selectedMethod = methods[i] selectedReturnType = result break } - if (selectedMethod === null || selectedReturnType === null) + 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)]) + } // 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. From 63f0301bfc12cf3ef24bd6e46e196775feead0e2 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 22 Jul 2026 04:57:39 +0800 Subject: [PATCH 10/13] Add fix for execption table finally logic --- src/compiler/__tests__/try.test.ts | 18 ++++++++++++++++++ src/compiler/code-generator.ts | 21 +++++++++++---------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/compiler/__tests__/try.test.ts b/src/compiler/__tests__/try.test.ts index 071b1a32..1d7f2b84 100644 --- a/src/compiler/__tests__/try.test.ts +++ b/src/compiler/__tests__/try.test.ts @@ -17,6 +17,24 @@ const testCases: testCase[] = [ `, 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"], + } ]; describe("try/catch", () => { diff --git a/src/compiler/code-generator.ts b/src/compiler/code-generator.ts index f563128c..c5d468e8 100644 --- a/src/compiler/code-generator.ts +++ b/src/compiler/code-generator.ts @@ -612,16 +612,6 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi const catchAllLabel = finallyNode ? cg.generateNewLabel() : null - // If finally exists, add catch-all entry for the try block - if (finallyNode && catchAllLabel) { - localExceptionTable.push({ - startPc: tryStart.offset, - endPc: tryEnd.offset, - handlerLabel: catchAllLabel, - catchType: 0 - }) - } - // For normal path: run finally block if it exists if (finallyNode) { finallyNode.block.blockStatements.forEach((stmt: any) => { @@ -717,6 +707,17 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi } } + // 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 From df4fedeb27bfbd8227cc40c00bff6e24f49fbba3 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 22 Jul 2026 05:18:58 +0800 Subject: [PATCH 11/13] Add more tests --- src/compiler/__tests__/try.test.ts | 158 +++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/src/compiler/__tests__/try.test.ts b/src/compiler/__tests__/try.test.ts index 1d7f2b84..6c9afdce 100644 --- a/src/compiler/__tests__/try.test.ts +++ b/src/compiler/__tests__/try.test.ts @@ -1,4 +1,7 @@ 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[] = [ { @@ -35,6 +38,111 @@ const testCases: testCase[] = [ `, 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", () => { @@ -42,3 +150,53 @@ describe("try/catch", () => { 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); + }); + } +}); From e35730ad636e413f59c627732bcbef49225f70bf Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 22 Jul 2026 05:58:29 +0800 Subject: [PATCH 12/13] Add java.lang.Throwable subclasses to compiler/type checker imports --- src/compiler/import/lib-info.ts | 51 +++++++++++++++++++++++++++ src/types/checker/environment.ts | 59 ++++++++++++++++++++++++++++++-- 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/src/compiler/import/lib-info.ts b/src/compiler/import/lib-info.ts index 720ced9d..f0ffbed8 100644 --- a/src/compiler/import/lib-info.ts +++ b/src/compiler/import/lib-info.ts @@ -16,9 +16,60 @@ export const rawLibInfo = { { 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/types/checker/environment.ts b/src/types/checker/environment.ts index 526fdca5..bf17e700 100644 --- a/src/types/checker/environment.ts +++ b/src/types/checker/environment.ts @@ -18,6 +18,44 @@ 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 THROWABLE_CLASS = new NonPrimitives.Throwable() +const EXCEPTION_CLASS = new NonPrimitives.Exception() +const RUNTIME_EXCEPTION_CLASS = new ClassType('RuntimeException') +RUNTIME_EXCEPTION_CLASS.setParentClass(EXCEPTION_CLASS) +const ERROR_CLASS = new ClassType('Error') +ERROR_CLASS.setParentClass(THROWABLE_CLASS) + +const ARITHMETIC_EXCEPTION_CLASS = new ClassType('ArithmeticException') +ARITHMETIC_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) +const ARRAY_INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS = new ClassType('ArrayIndexOutOfBoundsException') +ARRAY_INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) +const ARRAY_STORE_EXCEPTION_CLASS = new ClassType('ArrayStoreException') +ARRAY_STORE_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) +const CLASS_CAST_EXCEPTION_CLASS = new ClassType('ClassCastException') +CLASS_CAST_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) +const ILLEGAL_ARGUMENT_EXCEPTION_CLASS = new ClassType('IllegalArgumentException') +ILLEGAL_ARGUMENT_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) +const ILLEGAL_MONITOR_STATE_EXCEPTION_CLASS = new ClassType('IllegalMonitorStateException') +ILLEGAL_MONITOR_STATE_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) +const ILLEGAL_STATE_EXCEPTION_CLASS = new ClassType('IllegalStateException') +ILLEGAL_STATE_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) +const INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS = new ClassType('IndexOutOfBoundsException') +INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) +const NEGATIVE_ARRAY_SIZE_EXCEPTION_CLASS = new ClassType('NegativeArraySizeException') +NEGATIVE_ARRAY_SIZE_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) +const NULL_POINTER_EXCEPTION_CLASS = new ClassType('NullPointerException') +NULL_POINTER_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) +const NUMBER_FORMAT_EXCEPTION_CLASS = new ClassType('NumberFormatException') +NUMBER_FORMAT_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) +const STRING_INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS = new ClassType('StringIndexOutOfBoundsException') +STRING_INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS.setParentClass(INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS) +const UNSUPPORTED_OPERATION_EXCEPTION_CLASS = new ClassType('UnsupportedOperationException') +UNSUPPORTED_OPERATION_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) +const SECURITY_EXCEPTION_CLASS = new ClassType('SecurityException') +SECURITY_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) +const ILLEGAL_THREAD_STATE_EXCEPTION_CLASS = new ClassType('IllegalThreadStateException') +ILLEGAL_THREAD_STATE_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) + const GLOBAL_TYPE_ENVIRONMENT: { [key: string]: Type } = { boolean: new Primitives.Boolean(), byte: new Primitives.Byte(), @@ -40,8 +78,25 @@ const GLOBAL_TYPE_ENVIRONMENT: { [key: string]: Type } = { // Hard coded variables System: SYSTEM_CLASS, - Throwable: new NonPrimitives.Throwable(), - Exception: new NonPrimitives.Exception() + Throwable: THROWABLE_CLASS, + Error: ERROR_CLASS, + Exception: EXCEPTION_CLASS, + RuntimeException: RUNTIME_EXCEPTION_CLASS, + ArithmeticException: ARITHMETIC_EXCEPTION_CLASS, + ArrayIndexOutOfBoundsException: ARRAY_INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS, + ArrayStoreException: ARRAY_STORE_EXCEPTION_CLASS, + ClassCastException: CLASS_CAST_EXCEPTION_CLASS, + IllegalArgumentException: ILLEGAL_ARGUMENT_EXCEPTION_CLASS, + IllegalMonitorStateException: ILLEGAL_MONITOR_STATE_EXCEPTION_CLASS, + IllegalStateException: ILLEGAL_STATE_EXCEPTION_CLASS, + IndexOutOfBoundsException: INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS, + NegativeArraySizeException: NEGATIVE_ARRAY_SIZE_EXCEPTION_CLASS, + NullPointerException: NULL_POINTER_EXCEPTION_CLASS, + NumberFormatException: NUMBER_FORMAT_EXCEPTION_CLASS, + StringIndexOutOfBoundsException: STRING_INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS, + UnsupportedOperationException: UNSUPPORTED_OPERATION_EXCEPTION_CLASS, + SecurityException: SECURITY_EXCEPTION_CLASS, + IllegalThreadStateException: ILLEGAL_THREAD_STATE_EXCEPTION_CLASS } export class Frame { From e8985da136a95303ad57dd29498d339d4f83c147 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 29 Jul 2026 04:18:34 +0800 Subject: [PATCH 13/13] remove some hardcoding for typechecker --- src/types/checker/environment.ts | 199 +++++++++++++++++-------------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/types/checker/environment.ts b/src/types/checker/environment.ts index bf17e700..b42b1c80 100644 --- a/src/types/checker/environment.ts +++ b/src/types/checker/environment.ts @@ -7,98 +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 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 THROWABLE_CLASS = new NonPrimitives.Throwable() -const EXCEPTION_CLASS = new NonPrimitives.Exception() -const RUNTIME_EXCEPTION_CLASS = new ClassType('RuntimeException') -RUNTIME_EXCEPTION_CLASS.setParentClass(EXCEPTION_CLASS) -const ERROR_CLASS = new ClassType('Error') -ERROR_CLASS.setParentClass(THROWABLE_CLASS) - -const ARITHMETIC_EXCEPTION_CLASS = new ClassType('ArithmeticException') -ARITHMETIC_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) -const ARRAY_INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS = new ClassType('ArrayIndexOutOfBoundsException') -ARRAY_INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) -const ARRAY_STORE_EXCEPTION_CLASS = new ClassType('ArrayStoreException') -ARRAY_STORE_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) -const CLASS_CAST_EXCEPTION_CLASS = new ClassType('ClassCastException') -CLASS_CAST_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) -const ILLEGAL_ARGUMENT_EXCEPTION_CLASS = new ClassType('IllegalArgumentException') -ILLEGAL_ARGUMENT_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) -const ILLEGAL_MONITOR_STATE_EXCEPTION_CLASS = new ClassType('IllegalMonitorStateException') -ILLEGAL_MONITOR_STATE_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) -const ILLEGAL_STATE_EXCEPTION_CLASS = new ClassType('IllegalStateException') -ILLEGAL_STATE_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) -const INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS = new ClassType('IndexOutOfBoundsException') -INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) -const NEGATIVE_ARRAY_SIZE_EXCEPTION_CLASS = new ClassType('NegativeArraySizeException') -NEGATIVE_ARRAY_SIZE_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) -const NULL_POINTER_EXCEPTION_CLASS = new ClassType('NullPointerException') -NULL_POINTER_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) -const NUMBER_FORMAT_EXCEPTION_CLASS = new ClassType('NumberFormatException') -NUMBER_FORMAT_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) -const STRING_INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS = new ClassType('StringIndexOutOfBoundsException') -STRING_INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS.setParentClass(INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS) -const UNSUPPORTED_OPERATION_EXCEPTION_CLASS = new ClassType('UnsupportedOperationException') -UNSUPPORTED_OPERATION_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) -const SECURITY_EXCEPTION_CLASS = new ClassType('SecurityException') -SECURITY_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) -const ILLEGAL_THREAD_STATE_EXCEPTION_CLASS = new ClassType('IllegalThreadStateException') -ILLEGAL_THREAD_STATE_EXCEPTION_CLASS.setParentClass(RUNTIME_EXCEPTION_CLASS) - -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: THROWABLE_CLASS, - Error: ERROR_CLASS, - Exception: EXCEPTION_CLASS, - RuntimeException: RUNTIME_EXCEPTION_CLASS, - ArithmeticException: ARITHMETIC_EXCEPTION_CLASS, - ArrayIndexOutOfBoundsException: ARRAY_INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS, - ArrayStoreException: ARRAY_STORE_EXCEPTION_CLASS, - ClassCastException: CLASS_CAST_EXCEPTION_CLASS, - IllegalArgumentException: ILLEGAL_ARGUMENT_EXCEPTION_CLASS, - IllegalMonitorStateException: ILLEGAL_MONITOR_STATE_EXCEPTION_CLASS, - IllegalStateException: ILLEGAL_STATE_EXCEPTION_CLASS, - IndexOutOfBoundsException: INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS, - NegativeArraySizeException: NEGATIVE_ARRAY_SIZE_EXCEPTION_CLASS, - NullPointerException: NULL_POINTER_EXCEPTION_CLASS, - NumberFormatException: NUMBER_FORMAT_EXCEPTION_CLASS, - StringIndexOutOfBoundsException: STRING_INDEX_OUT_OF_BOUNDS_EXCEPTION_CLASS, - UnsupportedOperationException: UNSUPPORTED_OPERATION_EXCEPTION_CLASS, - SecurityException: SECURITY_EXCEPTION_CLASS, - IllegalThreadStateException: ILLEGAL_THREAD_STATE_EXCEPTION_CLASS + const factory = BUILT_IN_TYPE_FACTORIES[typeName] + const type = factory ? factory() : new ClassType(typeName) + stdlibTypeMap.set(typeName, type) + return type } +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()