From e941932d5e75810cc4bdcc13943ba9a2be692001 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Tue, 11 Aug 2026 06:09:53 +0800 Subject: [PATCH 01/12] tighten selector criteria --- .../__tests__/switchStatements.test.ts | 23 ++++++++++++++++++- src/types/checker/statements.ts | 4 ++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/types/checker/__tests__/switchStatements.test.ts b/src/types/checker/__tests__/switchStatements.test.ts index 8889e5bf..6344ebce 100644 --- a/src/types/checker/__tests__/switchStatements.test.ts +++ b/src/types/checker/__tests__/switchStatements.test.ts @@ -1,6 +1,6 @@ import { check } from '..' import { parse } from '../../ast' -import { IncompatibleTypesError, TypeCheckerError } from '../../errors' +import { IncompatibleTypesError, SelectorTypeNotAllowedError, TypeCheckerError } from '../../errors' import { Type } from '../../types/type' const createProgram = (statement: string) => ` @@ -27,6 +27,27 @@ const testcases: { `, result: { type: null, errors: [] } }, + { + input: ` + String selector = "Tuesday"; + switch(selector) { + case "Tuesday": { + selector = "Wednesday"; + } + default: + } + `, + result: { type: null, errors: [] } + }, + { + input: ` + Boolean selector = true; + switch(selector) { + default: {} + } + `, + result: { type: null, errors: [new SelectorTypeNotAllowedError()] } + }, { input: ` int selector = 1; diff --git a/src/types/checker/statements.ts b/src/types/checker/statements.ts index ef812dad..fe88fcef 100644 --- a/src/types/checker/statements.ts +++ b/src/types/checker/statements.ts @@ -12,7 +12,7 @@ import { isPrimitiveIntegralType, isPrimitiveLongType, isReferenceBooleanType, - isReferenceType + isStringType } from '../types/utils' export const checkDoExpression = ( @@ -28,7 +28,7 @@ export const checkSwitchExpression = ( location: Location ): null | TypeCheckerError => { if (isPrimitiveIntegralType(expressionType) && !isPrimitiveLongType(expressionType)) return null - if (isReferenceType(expressionType)) return null + if (isStringType(expressionType)) return null return new SelectorTypeNotAllowedError(location) } From 89ccf3067db10b398fdd1215b3c15d841f2ff6fd Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Tue, 11 Aug 2026 07:49:09 +0800 Subject: [PATCH 02/12] add enum support --- src/compiler/code-generator.ts | 28 +++- src/compiler/compiler-utils.ts | 3 +- .../__tests__/switchStatements.test.ts | 25 ++++ src/types/checker/environment.ts | 4 +- src/types/checker/index.ts | 108 +++++++++++++-- src/types/checker/prechecks.ts | 125 +++++++++++++++++- src/types/checker/statements.ts | 2 + src/types/types/classes.ts | 2 + 8 files changed, 278 insertions(+), 19 deletions(-) diff --git a/src/compiler/code-generator.ts b/src/compiler/code-generator.ts index 773bde90..d4d5d101 100644 --- a/src/compiler/code-generator.ts +++ b/src/compiler/code-generator.ts @@ -1,4 +1,5 @@ import { OPCODE } from '../ClassFile/constants/instructions' +import { ACCESS_FLAGS } from '../ClassFile/types' import { ExceptionHandler, AttributeInfo } from '../ClassFile/types/attributes' import { FIELD_FLAGS } from '../ClassFile/types/fields' import { METHOD_FLAGS } from '../ClassFile/types/methods' @@ -1375,6 +1376,27 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi const { stackSize: exprStackSize, resultType } = compile(expression, cg) let maxStack = exprStackSize + // If the expression is an enum type, invoke ordinal() to convert to int and then continue + let _resultType = resultType + if (_resultType && _resultType.startsWith('L') && _resultType !== 'Ljava/lang/String;') { + const clean = _resultType.replace(/^L|;$/g, '') + try { + const classInfo = cg.symbolTable.queryClass(clean) + if (classInfo.accessFlags & ACCESS_FLAGS.ACC_ENUM) { + // call java.lang.Enum.ordinal() (returns int) + cg.code.push( + OPCODE.INVOKEVIRTUAL, + 0, + cg.constantPoolManager.indexMethodrefInfo('java/lang/Enum', 'ordinal', '()I') + ) + _resultType = 'I' + maxStack = Math.max(maxStack, exprStackSize + 1) + } + } catch (e) { + // ignore: not a known class + } + } + const caseLabels: Label[] = cases.map(() => cg.generateNewLabel()) const defaultLabel = cg.generateNewLabel() const endLabel = cg.generateNewLabel() @@ -1382,7 +1404,7 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi // Track the switch statement's end label cg.switchLabels.push(endLabel) - if (['I', 'B', 'S', 'C'].includes(resultType)) { + if (['I', 'B', 'S', 'C'].includes(_resultType)) { const caseValues: number[] = [] const caseLabelMap: Map = new Map() let hasDefault = false @@ -1556,7 +1578,7 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi } endLabel.offset = cg.code.length - } else if (resultType === 'Ljava/lang/String;') { + } else if (_resultType === 'Ljava/lang/String;') { // **String Switch Handling** const hashCaseMap: Map = new Map() @@ -1708,7 +1730,7 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi endLabel.offset = cg.code.length } else { throw new Error( - `Switch statements only support byte, short, int, char, or String types. Found: ${resultType}` + `Switch statements only support byte, short, int, char, String, or enum types. Found: ${_resultType}` ) } diff --git a/src/compiler/compiler-utils.ts b/src/compiler/compiler-utils.ts index 9adcae6d..4bff9609 100644 --- a/src/compiler/compiler-utils.ts +++ b/src/compiler/compiler-utils.ts @@ -6,7 +6,8 @@ import { ClassModifier, FieldModifier, MethodModifier } from '../ast/types/class const classAccessFlagMap = new Map([ ['public', ACCESS_FLAGS.ACC_PUBLIC], ['final', ACCESS_FLAGS.ACC_FINAL], - ['abstract', ACCESS_FLAGS.ACC_ABSTRACT] + ['abstract', ACCESS_FLAGS.ACC_ABSTRACT], + ['enum', ACCESS_FLAGS.ACC_ENUM] ]) export function generateClassAccessFlags(modifiers: Array) { diff --git a/src/types/checker/__tests__/switchStatements.test.ts b/src/types/checker/__tests__/switchStatements.test.ts index 6344ebce..8fb29ca2 100644 --- a/src/types/checker/__tests__/switchStatements.test.ts +++ b/src/types/checker/__tests__/switchStatements.test.ts @@ -73,6 +73,31 @@ const testcases: { } `, result: { type: null, errors: [new IncompatibleTypesError()] } + }, + { + input: ` + enum Color { RED, BLUE } + Color selector = Color.RED; + switch(selector) { + case Color.RED: { + selector = Color.BLUE; + } + default: {} + } + `, + result: { type: null, errors: [] } + }, + { + input: ` + enum Color { RED, BLUE } + enum Other { X } + Color selector = Color.RED; + switch(selector) { + case Other.X: {} + default: {} + } + `, + result: { type: null, errors: [new IncompatibleTypesError()] } } ] diff --git a/src/types/checker/environment.ts b/src/types/checker/environment.ts index 6ef2ad2b..92e12851 100644 --- a/src/types/checker/environment.ts +++ b/src/types/checker/environment.ts @@ -41,7 +41,9 @@ const GLOBAL_TYPE_ENVIRONMENT: { [key: string]: Type } = { // Hard coded variables System: SYSTEM_CLASS, Throwable: new NonPrimitives.Throwable(), - Exception: new NonPrimitives.Exception() + Exception: new NonPrimitives.Exception(), + // enum base type + Enum: new ClassType('Enum') } export class Frame { diff --git a/src/types/checker/index.ts b/src/types/checker/index.ts index 77491719..0f3c39fe 100644 --- a/src/types/checker/index.ts +++ b/src/types/checker/index.ts @@ -69,7 +69,6 @@ const isCastCompatible = (fromType: Type, toType: Type): boolean => { const fromName = fromType.constructor.name; const toName = toType.constructor.name; - console.log(fromName, toName); return !(fromName === 'char' && toName !== 'int'); } @@ -384,7 +383,6 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R return newResult(null, errors) } case 'InstanceofExpression': { - console.log(node) return OK_RESULT } case 'BinaryLiteral': @@ -584,6 +582,88 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R } return newResult(null, errors) } + case 'EnumDeclaration': { + const errors: TypeCheckerError[] = [] + const classType = frame.getType(node.typeIdentifier.identifier, node.typeIdentifier.location) + if (classType instanceof TypeCheckerError) return newResult(null, [classType]) + if (!(classType instanceof ClassType)) throw new Error('enum type retrieved should be ClassImpl') + + const classFrame = frame.newChildFrame() + classFrame.setClass(classType) + classType.mapFields((name, type) => { + const error = classFrame.setVariable(name, type, { startLine: -1, startOffset: -1 }) + if (error) errors.push(error) + }) + if (errors.length > 0) return newResult(null, errors) + + const bodyDecls = node.enumBody.enumBodyDeclarations?.classBodyDeclaration || [] + let numFieldDeclarations = 0 + let numMethodDeclarations = 0 + for (let i = 0; i < bodyDecls.length; i++) { + const bodyDeclaration = bodyDecls[i] + switch (bodyDeclaration.kind) { + case 'ConstructorDeclaration': { + const methodFrame = classFrame.newChildFrame() + const constructor = classType.getConstructor(i - numFieldDeclarations - numMethodDeclarations) + const constructorMethodErrors: TypeCheckerError[] = [] + constructor.mapParameters((name, type, isVarargs) => { + const error = methodFrame.setVariable(name, type, { startLine: -1, startOffset: -1 }) + if (error) constructorMethodErrors.push(error) + }) + if (constructorMethodErrors.length > 0) { + errors.push(...constructorMethodErrors) + break + } + const { errors: checkErrors } = typeCheckBody(bodyDeclaration.constructorBody, methodFrame) + if (checkErrors.length > 0) errors.push(...checkErrors) + break + } + case 'FieldDeclaration': { + for (const variableDeclarator of (bodyDeclaration as any).variableDeclaratorList.variableDeclarators) { + const field = classType.accessField(variableDeclarator.variableDeclaratorId.identifier.identifier, variableDeclarator.variableDeclaratorId.identifier.location) + if (field instanceof TypeCheckerError) throw new Error('field should exist in enum') + const initializer = variableDeclarator.variableInitializer + if (initializer) { + const type = createArrayType(field, initializer, expression => { + const result = typeCheckBody(expression, frame) + if (result.errors.length > 0) return result.errors[0] + if (!result.currentType) throw new Error('array initializer expression should have a type') + return result.currentType + }) + if (type instanceof TypeCheckerError) errors.push(type) + } + } + break + } + case 'MethodDeclaration': { + const methodIdentifier = (bodyDeclaration as any).methodHeader.methodDeclarator.identifier + const methodName = methodIdentifier.identifier + const overloadIndex = bodyDecls + .filter((n: any) => n.kind === 'MethodDeclaration' && (n as any).methodHeader.methodDeclarator.identifier.identifier === methodName) + .findIndex(n => n === bodyDeclaration) + const method = classType.getMethod(methodName)[overloadIndex] + const methodFrame = classFrame.newChildFrame() + const methodErrors: TypeCheckerError[] = [] + methodFrame.setReturnType(method.getReturnType()) + method.mapParameters((name, type, isVarargs) => { + const error = methodFrame.setVariable(name, type, { startLine: -1, startOffset: -1 }) + if (error) methodErrors.push(error) + }) + if (methodErrors.length > 0) { + errors.push(...methodErrors) + break + } + const { errors: checkErrors } = typeCheckBody((bodyDeclaration as any).methodBody, methodFrame) + if (checkErrors.length > 0) errors.push(...checkErrors) + break + } + } + + if (bodyDeclaration.kind === 'FieldDeclaration') numFieldDeclarations += 1 + if (bodyDeclaration.kind === 'MethodDeclaration') numMethodDeclarations += 1 + } + return newResult(null, errors) + } case 'OrdinaryCompilationUnit': { const typeCheckErrors = node.topLevelClassOrInterfaceDeclarations .map(declaration => typeCheckBody(declaration, frame)) @@ -644,16 +724,20 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R const switchBlockFrame = frame.newChildFrame() for (const group of node.switchBlock.switchBlockStatementGroups) { for (const switchLabel of group.switchLabels) { - if ('caseConstant' in switchLabel) { - const checkResult = typeCheckBody( - switchLabel.caseConstant as CaseConstant, - switchBlockFrame - ) - if (checkResult.hasErrors) return checkResult - if (!checkResult.currentType) - throw new TypeCheckerInternalError('Switch case constant should have a type.') - if (expressionCheck.currentType.canBeAssigned(checkResult.currentType)) continue - return newResult(null, [new IncompatibleTypesError(switchLabel.location)]) + // Support both singular 'caseConstant' and plural 'caseConstants' AST shapes + const caseConstants: CaseConstant[] = [] + if ('caseConstant' in switchLabel && (switchLabel as any).caseConstant) caseConstants.push((switchLabel as any).caseConstant as CaseConstant) + if ('caseConstants' in switchLabel && (switchLabel as any).caseConstants) caseConstants.push(...((switchLabel as any).caseConstants as CaseConstant[])) + if (caseConstants.length > 0) { + for (const caseConst of caseConstants) { + const checkResult = typeCheckBody(caseConst, switchBlockFrame) + if (checkResult.hasErrors) return checkResult + if (!checkResult.currentType) + throw new TypeCheckerInternalError('Switch case constant should have a type.') + const assignable = expressionCheck.currentType.canBeAssigned(checkResult.currentType) + if (assignable) continue + return newResult(null, [new IncompatibleTypesError(switchLabel.location)]) + } } } if (group.blockStatements) { diff --git a/src/types/checker/prechecks.ts b/src/types/checker/prechecks.ts index fe7df448..9ec0a9c6 100644 --- a/src/types/checker/prechecks.ts +++ b/src/types/checker/prechecks.ts @@ -1,4 +1,4 @@ -import { Class, ClassType, ObjectClass } from '../types/classes' +import { Class, ClassType, EnumClass, ObjectClass } from '../types/classes' import { ConstructorDeclaration, MethodDeclaration, Node } from '../ast/specificationTypes' import { createClassFieldsAndMethods } from '../typeFactories/classFactory' import { createMethod } from '../typeFactories/methodFactory' @@ -15,6 +15,31 @@ export const addClasses = (node: Node, frame: Frame): Result => { const typeCheckErrors = node.topLevelClassOrInterfaceDeclarations .map(declaration => addClasses(declaration, frame)) .reduce((errors, result) => (result.hasErrors ? [...errors, ...result.errors] : errors), []) + + // Register any nested enum declarations found anywhere in the compilation unit + const registerNestedEnums = (obj: any) => { + if (!obj || typeof obj !== 'object') return + if (Array.isArray(obj)) { + obj.forEach(registerNestedEnums) + return + } + if (obj.kind === 'EnumDeclaration') { + try { + const enumType = new EnumClass(obj.typeIdentifier.identifier) + const err = frame.setType(obj.typeIdentifier.identifier, enumType, obj.typeIdentifier.location) + if (err instanceof Error) { + // duplicate class — add as error + typeCheckErrors.push(new DuplicateClassError(obj.location)) + } + } catch (e) { + // ignore + } + return + } + Object.keys(obj).forEach(k => registerNestedEnums(obj[k])) + } + node.topLevelClassOrInterfaceDeclarations.forEach(registerNestedEnums) + return newResult(null, typeCheckErrors) } case 'NormalClassDeclaration': { @@ -35,7 +60,16 @@ export const addClasses = (node: Node, frame: Frame): Result => { return newResult(classType) } case 'EnumDeclaration': { - throw new Error('Not implemented') + const enumType = new EnumClass(node.typeIdentifier.identifier) + const errors: TypeCheckerError[] = [] + if (errors.length > 0) return newResult(null, errors) + const error = frame.setType( + node.typeIdentifier.identifier, + enumType, + node.typeIdentifier.location + ) + if (error instanceof Error) return newResult(null, [new DuplicateClassError(node.location)]) + return newResult(enumType) } case 'RecordDeclaration': { throw new Error('Not implemented') @@ -54,6 +88,23 @@ export const addClassMethods = (node: Node, frame: Frame): Result => { const typeCheckErrors = node.topLevelClassOrInterfaceDeclarations .map(declaration => addClassMethods(declaration, frame)) .reduce((errors, result) => (result.hasErrors ? [...errors, ...result.errors] : errors), []) + + // Also process any nested enum declarations (e.g., enums declared inside methods) + const processNestedEnums = (obj: any) => { + if (!obj || typeof obj !== 'object') return + if (Array.isArray(obj)) { + obj.forEach(processNestedEnums) + return + } + if (obj.kind === 'EnumDeclaration') { + const res = addClassMethods(obj, frame) + if (res.hasErrors) typeCheckErrors.push(...res.errors) + return + } + Object.keys(obj).forEach(k => processNestedEnums(obj[k])) + } + node.topLevelClassOrInterfaceDeclarations.forEach(processNestedEnums) + return newResult(null, typeCheckErrors) } case 'ConstructorDeclaration': @@ -74,6 +125,64 @@ export const addClassMethods = (node: Node, frame: Frame): Result => { if (classType instanceof TypeCheckerError) return newResult(null, [classType]) return newResult(classType) } + case 'EnumDeclaration': { + const createMethodLocal = ( + node: ConstructorDeclaration | MethodDeclaration + ): Method | TypeCheckerError => { + const result = addClassMethods(node, frame) + if (result.errors.length > 0) return result.errors[0] + return result.currentType as Method + } + + // Populate enum constants and any class-body declarations (fields/methods/constructors) + const classType = frame.getType(node.typeIdentifier.identifier, node.typeIdentifier.location) + if (classType instanceof TypeCheckerError) return newResult(null, [classType]) + if (!(classType instanceof ClassType)) throw new Error('enum type should be a ClassImpl') + + // Add enum constants as fields of the enum type + const enumConstants = node.enumBody.enumConstantList?.enumConstants || [] + for (const constant of enumConstants) { + const fieldError = classType.addField(constant.identifier.identifier, classType, constant.location) + if (fieldError instanceof TypeCheckerError) return newResult(null, [fieldError]) + } + + // Process body declarations similar to class body + const bodyDecls = node.enumBody.enumBodyDeclarations?.classBodyDeclaration || [] + for (const bodyNode of bodyDecls) { + switch (bodyNode.kind) { + case 'ConstructorDeclaration': { + const constructorMethod = createMethodLocal(bodyNode as ConstructorDeclaration) + if (constructorMethod instanceof TypeCheckerError) return newResult(null, [constructorMethod]) + const error = classType.addConstructor(constructorMethod, bodyNode.location) + if (error instanceof TypeCheckerError) return newResult(null, [error]) + break + } + case 'FieldDeclaration': { + const fieldType = frame.getType( + (bodyNode as any).unannType ? (bodyNode as any).unannType : (bodyNode as any).fieldType, + bodyNode.location + ) + if (fieldType instanceof TypeCheckerError) return newResult(null, [fieldType]) + for (const declarator of (bodyNode as any).variableDeclaratorList.variableDeclarators) { + const fieldIdentifier = declarator.variableDeclaratorId.identifier + const error = classType.addField(fieldIdentifier.identifier, fieldType, fieldIdentifier.location) + if (error instanceof TypeCheckerError) return newResult(null, [error]) + } + break + } + case 'MethodDeclaration': { + const methodSignature = createMethodLocal(bodyNode as MethodDeclaration) + if (methodSignature instanceof TypeCheckerError) return newResult(null, [methodSignature]) + const methodName = (bodyNode as MethodDeclaration).methodHeader.methodDeclarator.identifier + const error = classType.addMethod(methodName.identifier, methodSignature, methodName.location) + if (error instanceof TypeCheckerError) return newResult(null, [error]) + break + } + } + } + + return newResult(classType) + } default: return OK_RESULT } @@ -111,6 +220,18 @@ export const addClassParents = (node: Node, frame: Frame): Result => { } return newResult(classType) } + case 'EnumDeclaration': { + const classType = frame.getType(node.typeIdentifier.identifier, node.typeIdentifier.location) + if (classType instanceof Error) return newResult(null, [classType]) + if (!(classType instanceof ClassType)) throw new Error('enum type should be a ClassImpl') + + // Enums implicitly extend java.lang.Enum (represented here as 'Enum' in the type environment) + const enumBase = frame.getType('Enum', node.typeIdentifier.location) + if (enumBase instanceof Error) return newResult(null, [enumBase]) + if (!(enumBase instanceof ClassType)) throw new Error('Enum base should be a ClassImpl') + classType.setParentClass(enumBase) + return newResult(classType) + } default: return OK_RESULT } diff --git a/src/types/checker/statements.ts b/src/types/checker/statements.ts index fe88fcef..3ce1e192 100644 --- a/src/types/checker/statements.ts +++ b/src/types/checker/statements.ts @@ -6,6 +6,7 @@ import { TypeCheckerError } from '../errors' import { Throwable } from '../types/references' +import { EnumClass } from '../types/classes' import { Type } from '../types/type' import { isPrimitiveBooleanType, @@ -29,6 +30,7 @@ export const checkSwitchExpression = ( ): null | TypeCheckerError => { if (isPrimitiveIntegralType(expressionType) && !isPrimitiveLongType(expressionType)) return null if (isStringType(expressionType)) return null + if (expressionType instanceof EnumClass) return null return new SelectorTypeNotAllowedError(location) } diff --git a/src/types/types/classes.ts b/src/types/types/classes.ts index 00b4ec0d..8334f2e5 100644 --- a/src/types/types/classes.ts +++ b/src/types/types/classes.ts @@ -144,6 +144,8 @@ export class ClassType extends ClassOrInterfaceType implements Class { } } +export class EnumClass extends ClassType {} + export class ObjectClass extends ClassOrInterfaceType implements Class { public readonly name: string = 'Object' public constructor() { From 4e0b4d5be3487e3824e7aac5d37b2ab3646bc6eb Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 26 Aug 2026 15:37:43 +0800 Subject: [PATCH 03/12] WIP: Add enum grammar rules and compiler tests - Updated grammar.pegjs and grammar.ts to add EnumDeclaration parsing - Added TopLevelClassOrInterfaceDeclaration and ClassMemberDeclaration alternatives for EnumDeclaration - Added EnumDeclaration, EnumBody, EnumConstantList, and EnumConstant parsing rules - Created src/compiler/__tests__/tests/enum.test.ts with 3 enum test cases - Updated src/compiler/__tests__/index.ts to import and run enum tests Remaining work: - Run enum compiler tests to verify parsing works - Implement enum code generation in compiler.ts (enum initialization, synthetic methods) - Run full test suite to validate no regressions - Verify enum runtime behavior (ordinal(), name(), values(), valueOf()) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/compiler/__tests__/index.ts | 2 + src/compiler/__tests__/tests/enum.test.ts | 107 ++++++++++++++++++++++ src/compiler/grammar.pegjs | 34 +++++++ src/compiler/grammar.ts | 34 +++++++ 4 files changed, 177 insertions(+) create mode 100644 src/compiler/__tests__/tests/enum.test.ts diff --git a/src/compiler/__tests__/index.ts b/src/compiler/__tests__/index.ts index d19bcb99..bddc2a26 100644 --- a/src/compiler/__tests__/index.ts +++ b/src/compiler/__tests__/index.ts @@ -9,6 +9,7 @@ import { methodInvocationTest } from "./tests/methodInvocation.test"; import { importTest } from "./tests/import.test"; import { arrayTest } from "./tests/array.test"; import { classTest } from "./tests/class.test"; +import { enumTest } from "./tests/enum.test"; import { typeConversionTest } from "./tests/typeConversion.test"; describe("compiler tests", () => { @@ -23,5 +24,6 @@ describe("compiler tests", () => { importTest(); arrayTest(); classTest(); + enumTest(); typeConversionTest(); }) diff --git a/src/compiler/__tests__/tests/enum.test.ts b/src/compiler/__tests__/tests/enum.test.ts new file mode 100644 index 00000000..659e017d --- /dev/null +++ b/src/compiler/__tests__/tests/enum.test.ts @@ -0,0 +1,107 @@ +import { + runTest, + testCase, +} from "../__utils__/test-utils"; + +const testCases: testCase[] = [ + { + comment: "enum switch and synthetic methods", + program: ` + public enum Color { + RED, + BLUE + } + + public class Main { + public static void main(String[] args) { + Color red = Color.valueOf("RED"); + System.out.println(Color.RED.ordinal()); + System.out.println(Color.BLUE.name()); + System.out.println(red.toString()); + + Color selector = Color.BLUE; + switch (selector) { + case Color.RED: + System.out.println("bad"); + break; + case Color.BLUE: + System.out.println("ok"); + break; + default: + System.out.println("default"); + } + } + } + `, + expectedLines: ["0", "BLUE", "RED", "ok"], + }, + { + comment: "enum values returns cloned array", + program: ` + public enum Direction { + NORTH, + SOUTH + } + + public class Main { + public static void main(String[] args) { + Direction[] copy = Direction.values(); + copy[0] = Direction.SOUTH; + Direction[] fresh = Direction.values(); + + switch (fresh[0]) { + case Direction.NORTH: + System.out.println("fresh"); + break; + default: + System.out.println("bad"); + } + + switch (copy[0]) { + case Direction.SOUTH: + System.out.println("mutated"); + break; + default: + System.out.println("bad"); + } + } + } + `, + expectedLines: ["fresh", "mutated"], + }, + { + comment: "enum constructors and instance fields", + program: ` + public enum Planet { + EARTH(1), + MARS(2); + + private int moons; + + private Planet(int moons) { + this.moons = moons; + } + + public int moons() { + return this.moons; + } + } + + public class Main { + public static void main(String[] args) { + Planet mars = Planet.valueOf("MARS"); + System.out.println(Planet.EARTH.moons()); + System.out.println(mars.moons()); + } + } + `, + expectedLines: ["1", "2"], + }, +]; + +export const enumTest = () => describe("enums", () => { + for (let testCase of testCases) { + const { comment: comment, program: program, expectedLines: expectedLines } = testCase; + it(comment, () => runTest(program, expectedLines)); + } +}); diff --git a/src/compiler/grammar.pegjs b/src/compiler/grammar.pegjs index 505f648e..a1f84bc4 100755 --- a/src/compiler/grammar.pegjs +++ b/src/compiler/grammar.pegjs @@ -475,6 +475,7 @@ TypeImportOnDemandDeclaration TopLevelClassOrInterfaceDeclaration = ClassDeclaration + / EnumDeclaration / InterfaceDeclaration / semicolon @@ -520,6 +521,38 @@ ClassModifier / non_sealed / strictfp +EnumDeclaration + = cm:ClassModifier* enum tm:TypeIdentifier ClassImplements? eb:EnumBody { + return addLocInfo({ + kind: "EnumDeclaration", + classModifier: cm, + typeIdentifier: tm, + enumBody: eb, + }) + } + +EnumBody + = lcurly ecl:EnumConstantList? ec:EnumConstant* rcurly { + const constants = ecl ? [...ecl, ...ec] : ec; + return addLocInfo({ + kind: "EnumBody", + constants: constants, + }) + } + +EnumConstantList + = @EnumConstant (comma @EnumConstant)* comma? + +EnumConstant + = name:Identifier (lparen al:ArgumentList? rparen)? cb:(lcurly ClassBodyDeclaration* rcurly)? { + return addLocInfo({ + kind: "EnumConstant", + name: name, + arguments: al || [], + classBody: cb || [], + }) + } + TypeParameters = TO_BE_ADDED @@ -551,6 +584,7 @@ ClassMemberDeclaration = FieldDeclaration / MethodDeclaration / ClassDeclaration + / EnumDeclaration / InterfaceDeclaration / semicolon diff --git a/src/compiler/grammar.ts b/src/compiler/grammar.ts index c7417294..24861abb 100755 --- a/src/compiler/grammar.ts +++ b/src/compiler/grammar.ts @@ -477,6 +477,7 @@ TypeImportOnDemandDeclaration TopLevelClassOrInterfaceDeclaration = ClassDeclaration + / EnumDeclaration / InterfaceDeclaration / semicolon @@ -522,6 +523,38 @@ ClassModifier / non_sealed / strictfp +EnumDeclaration + = cm:ClassModifier* enum tm:TypeIdentifier ClassImplements? eb:EnumBody { + return addLocInfo({ + kind: "EnumDeclaration", + classModifier: cm, + typeIdentifier: tm, + enumBody: eb, + }) + } + +EnumBody + = lcurly ecl:EnumConstantList? ec:EnumConstant* rcurly { + const constants = ecl ? [...ecl, ...ec] : ec; + return addLocInfo({ + kind: "EnumBody", + constants: constants, + }) + } + +EnumConstantList + = @EnumConstant (comma @EnumConstant)* comma? + +EnumConstant + = name:Identifier (lparen al:ArgumentList? rparen)? cb:(lcurly ClassBodyDeclaration* rcurly)? { + return addLocInfo({ + kind: "EnumConstant", + name: name, + arguments: al || [], + classBody: cb || [], + }) + } + TypeParameters = TO_BE_ADDED @@ -553,6 +586,7 @@ ClassMemberDeclaration = FieldDeclaration / MethodDeclaration / ClassDeclaration + / EnumDeclaration / InterfaceDeclaration / semicolon From 0625138f1d4e13345e0f118b080f91b9fdce54de Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 26 Aug 2026 16:38:37 +0800 Subject: [PATCH 04/12] Add enum parsing and compiler support (partial) - Updated grammar (grammar.pegjs and grammar.ts) to parse enum declarations - EnumDeclaration, EnumBody, EnumConstantList, EnumConstant rules - Support for optional semicolon after constants and enum body members - Extended AST types (src/ast/types/classes.ts) - Added EnumDeclaration, EnumBody, EnumConstant interfaces - Updated ClassDeclaration union to include EnumDeclaration - Updated ClassBodyDeclaration to include EnumDeclaration - Added EnumDeclaration to NodeMap (src/ast/types/ast.ts) - Updated compiler to handle enum declarations - Added compileEnum() method in src/compiler/compiler.ts - Updated compile() to route EnumDeclaration through compileEnum() - Fixed type signatures to handle both ClassDeclaration and EnumDeclaration - Set enum parent to java/lang/Enum and ACC_ENUM flag - Updated ast-extractor.ts and ec-evaluator/utils.ts to accept ClassDeclaration[] - Updated searchMainMtdClass() to filter out enums - Created src/compiler/__tests__/tests/enum.test.ts with 3 test cases - enum switch and synthetic methods - enum values returns cloned array - enum constructors and instance fields Status: Enums parse and compile, but synthetic methods not yet implemented. Tests failing because ordinal(), name(), values(), valueOf() missing. Next: Implement synthetic enum method generation in compiler.ts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/ast/astExtractor/ast-extractor.ts | 4 +- src/ast/types/ast.ts | 2 + src/ast/types/classes.ts | 27 +++++++++-- src/compiler/binary-writer.ts | 4 +- src/compiler/code-generator.ts | 60 ++++++++++++++---------- src/compiler/compiler.ts | 66 ++++++++++++++++++++++++--- src/compiler/error.ts | 2 +- src/compiler/grammar.pegjs | 18 ++++++-- src/compiler/grammar.ts | 18 ++++++-- src/compiler/symbol-table.ts | 18 +++++--- src/ec-evaluator/utils.ts | 12 +++-- src/jvm/exception-table.ts | 50 ++++++++++---------- src/types/checker/index.ts | 2 +- src/types/checker/prechecks.ts | 6 +-- 14 files changed, 202 insertions(+), 87 deletions(-) diff --git a/src/ast/astExtractor/ast-extractor.ts b/src/ast/astExtractor/ast-extractor.ts index 681bb7e9..7364448d 100644 --- a/src/ast/astExtractor/ast-extractor.ts +++ b/src/ast/astExtractor/ast-extractor.ts @@ -1,11 +1,11 @@ import { BaseJavaCstVisitorWithDefaults, CstNode, TypeDeclarationCtx } from "java-parser"; -import { NormalClassDeclaration } from "../types/classes"; +import { ClassDeclaration } from "../types/classes"; import { AST } from "../types/packages-and-modules"; import { ClassExtractor } from "./class-extractor"; export class ASTExtractor extends BaseJavaCstVisitorWithDefaults { - private topLevelClassOrInterfaceDeclarations: NormalClassDeclaration[] = []; + private topLevelClassOrInterfaceDeclarations: ClassDeclaration[] = []; extract(cst: CstNode): AST { this.visit(cst); diff --git a/src/ast/types/ast.ts b/src/ast/types/ast.ts index 80effeac..a0eef563 100644 --- a/src/ast/types/ast.ts +++ b/src/ast/types/ast.ts @@ -12,6 +12,7 @@ import { } from "./blocks-and-statements"; import { ConstructorDeclaration, + EnumDeclaration, FieldDeclaration, MethodDeclaration, NormalClassDeclaration, @@ -29,6 +30,7 @@ interface NodeMap { MethodInvocation: MethodInvocation; ReturnStatement: ReturnStatement; NormalClassDeclaration: NormalClassDeclaration; + EnumDeclaration: EnumDeclaration; ClassInstanceCreationExpression: ClassInstanceCreationExpression; ConstructorDeclaration: ConstructorDeclaration; ExplicitConstructorInvocation: ExplicitConstructorInvocation; diff --git a/src/ast/types/classes.ts b/src/ast/types/classes.ts index b7345e78..576d9aa4 100644 --- a/src/ast/types/classes.ts +++ b/src/ast/types/classes.ts @@ -1,7 +1,7 @@ import { BaseNode } from "./ast"; import { Block, VariableDeclarator } from "./blocks-and-statements"; -export type ClassDeclaration = NormalClassDeclaration; +export type ClassDeclaration = NormalClassDeclaration | EnumDeclaration; export interface NormalClassDeclaration extends BaseNode { kind: "NormalClassDeclaration"; @@ -11,6 +11,26 @@ export interface NormalClassDeclaration extends BaseNode { classBody: Array; } +export interface EnumDeclaration extends BaseNode { + kind: "EnumDeclaration"; + classModifier: Array; + typeIdentifier: Identifier; + enumBody: EnumBody; +} + +export interface EnumBody extends BaseNode { + kind: "EnumBody"; + constants: Array; + bodyMembers?: Array; +} + +export interface EnumConstant extends BaseNode { + kind: "EnumConstant"; + name: Identifier; + arguments?: Array; + classBody?: Array; +} + export type ClassModifier = | "public" | "protected" @@ -20,9 +40,10 @@ export type ClassModifier = | "final" | "sealed" | "non-sealed" - | "strictfp"; + | "strictfp" + | "enum"; -export type ClassBodyDeclaration = ClassMemberDeclaration | ConstructorDeclaration; +export type ClassBodyDeclaration = ClassMemberDeclaration | ConstructorDeclaration | EnumDeclaration; export type ClassMemberDeclaration = MethodDeclaration | FieldDeclaration; export interface ConstructorDeclaration extends BaseNode { diff --git a/src/compiler/binary-writer.ts b/src/compiler/binary-writer.ts index 8b97dfb0..e4f09b77 100644 --- a/src/compiler/binary-writer.ts +++ b/src/compiler/binary-writer.ts @@ -49,7 +49,9 @@ export class BinaryWriter { fs.writeFileSync(filename, binary) } - private normalizeClassFile(classFile: ClassFile | Class | Array | Array): ClassFile { + private normalizeClassFile( + classFile: ClassFile | Class | Array | Array + ): ClassFile { if (Array.isArray(classFile)) { if (classFile.length === 0) { throw new Error('BinaryWriter expected a non-empty array of classes') diff --git a/src/compiler/code-generator.ts b/src/compiler/code-generator.ts index d4d5d101..d26b638d 100644 --- a/src/compiler/code-generator.ts +++ b/src/compiler/code-generator.ts @@ -191,19 +191,19 @@ const EMPTY_TYPE: string = '' function areClassTypesCompatible(fromType: string, toType: string, cg: CodeGenerator): boolean { const cleanFrom = fromType.replace(/^L|;$/g, '') const cleanTo = toType.replace(/^L|;$/g, '') - if (cleanFrom === cleanTo) return true; + if (cleanFrom === cleanTo) return true try { - let current = cg.symbolTable.queryClass(cleanFrom); + let current = cg.symbolTable.queryClass(cleanFrom) while (current.parentClassName) { - const parentClean = current.parentClassName; - if (parentClean === cleanTo) return true; - current = cg.symbolTable.queryClass(parentClean); + const parentClean = current.parentClassName + if (parentClean === cleanTo) return true + current = cg.symbolTable.queryClass(parentClean) } } catch (e) { - return false; + return false } - return false; + return false } function handleImplicitTypeConversion(fromType: string, toType: string, cg: CodeGenerator): number { @@ -839,30 +839,30 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi // --- Handle super. calls --- if (n.identifier.startsWith('super.')) { candidateMethods = cg.symbolTable.queryMethod(n.identifier.slice(6)) as MethodInfos - candidateMethods = candidateMethods.filter(method => - method.className == cg.symbolTable.queryClass(cg.currentClass).parentClassName) - cg.code.push(OPCODE.ALOAD, 0); + candidateMethods = candidateMethods.filter( + method => method.className == cg.symbolTable.queryClass(cg.currentClass).parentClassName + ) + cg.code.push(OPCODE.ALOAD, 0) } // --- Handle qualified calls (e.g. System.out.println or p.show) --- else if (n.identifier.includes('.')) { - const lastDot = n.identifier.lastIndexOf('.'); - const receiverStr = n.identifier.slice(0, lastDot); + const lastDot = n.identifier.lastIndexOf('.') + const receiverStr = n.identifier.slice(0, lastDot) if (receiverStr === 'this') { candidateMethods = cg.symbolTable.queryMethod(n.identifier.slice(5)) as MethodInfos - candidateMethods = candidateMethods.filter(method => - method.className == cg.currentClass) - cg.code.push(OPCODE.ALOAD, 0); + candidateMethods = candidateMethods.filter(method => method.className == cg.currentClass) + cg.code.push(OPCODE.ALOAD, 0) } else { - const recvRes = compile({ kind: 'ExpressionName', name: receiverStr }, cg); - maxStack = Math.max(maxStack, recvRes.stackSize); + const recvRes = compile({ kind: 'ExpressionName', name: receiverStr }, cg) + maxStack = Math.max(maxStack, recvRes.stackSize) candidateMethods = cg.symbolTable.queryMethod(n.identifier).pop() as MethodInfos } } // --- Handle unqualified calls --- else { candidateMethods = cg.symbolTable.queryMethod(n.identifier) as MethodInfos - unqualifiedCall = true; + unqualifiedCall = true } // Filter candidate methods by matching the argument list. @@ -904,11 +904,15 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi .slice(1, methodMatches[i].typeDescriptor.indexOf(')')) .match(/(\[+[BCDFIJSZ])|(\[+L[^;]+;)|[BCDFIJSZ]|L[^;]+;/g) || [] if ( - candParams.map((p, idx) => isSubtype(p, currParams[idx], cg)).reduce((a, b) => a && b, true) + candParams + .map((p, idx) => isSubtype(p, currParams[idx], cg)) + .reduce((a, b) => a && b, true) ) { selectedMethod = methodMatches[i] } else if ( - !currParams.map((p, idx) => isSubtype(p, candParams[idx], cg)).reduce((a, b) => a && b, true) + !currParams + .map((p, idx) => isSubtype(p, candParams[idx], cg)) + .reduce((a, b) => a && b, true) ) { throw new AmbiguousMethodCallError(n.identifier + argDescs.join(',')) } @@ -1267,7 +1271,7 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi try { info = cg.symbolTable.queryVariable(name) } catch (e) { - return { stackSize: 1, resultType: 'Ljava/lang/Class;' }; + return { stackSize: 1, resultType: 'Ljava/lang/Class;' } } if (Array.isArray(info)) { const fieldInfos = info @@ -1492,7 +1496,12 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi // Generate lookup table (pairs of case values and corresponding labels) caseValues.forEach((value, index) => { // push 4-byte key - cg.code.push((value >> 24) & 0xff, (value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff) + cg.code.push( + (value >> 24) & 0xff, + (value >> 16) & 0xff, + (value >> 8) & 0xff, + value & 0xff + ) // reserve 4 bytes for the branch target cg.code.push(0, 0, 0, 0) // label offset starts after the 4-byte key @@ -1635,7 +1644,12 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi // Populate LOOKUPSWITCH const hashLabels: Label[] = [] hashCaseMap.forEach((label, hashCode) => { - cg.code.push((hashCode >> 24) & 0xff, (hashCode >> 16) & 0xff, (hashCode >> 8) & 0xff, hashCode & 0xff) + cg.code.push( + (hashCode >> 24) & 0xff, + (hashCode >> 16) & 0xff, + (hashCode >> 8) & 0xff, + hashCode & 0xff + ) // reserve 4 bytes for the branch target cg.code.push(0, 0, 0, 0) // label offset starts after the 4-byte key diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 3e116800..93e0ddae 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -56,17 +56,26 @@ export class Compiler { ast.topLevelClassOrInterfaceDeclarations.forEach(decl => { const className = decl.typeIdentifier - const parentClassName = decl.sclass ? decl.sclass : 'java/lang/Object' + const parentClassName = (decl.kind === 'EnumDeclaration' ? 'java/lang/Enum' : + ('sclass' in decl && decl.sclass) ? decl.sclass : 'java/lang/Object') const accessFlags = generateClassAccessFlags(decl.classModifier) - this.symbolTable.insertClassInfo( - { name: className, accessFlags: accessFlags, parentClassName: parentClassName }) + this.symbolTable.insertClassInfo({ + name: className, + accessFlags: accessFlags, + parentClassName: parentClassName + }) this.symbolTable.returnToRoot() }) ast.topLevelClassOrInterfaceDeclarations.forEach(decl => { this.resetClassFileState() - const classFile = this.compileClass(decl) - classFiles.push({classFile: classFile, className: this.className}) + if (decl.kind === 'EnumDeclaration') { + const classFile = this.compileEnum(decl) + classFiles.push({ classFile: classFile, className: this.className }) + } else { + const classFile = this.compileClass(decl) + classFiles.push({ classFile: classFile, className: this.className }) + } }) return classFiles @@ -74,7 +83,8 @@ export class Compiler { private compileClass(classNode: ClassDeclaration): ClassFile { this.className = classNode.typeIdentifier - this.parentClassName = classNode.sclass ? classNode.sclass : 'java/lang/Object' + const sclass = 'sclass' in classNode ? classNode.sclass : undefined + this.parentClassName = sclass ? sclass : 'java/lang/Object' const accessFlags = generateClassAccessFlags(classNode.classModifier) this.symbolTable.extend() this.symbolTable.insertClassInfo({ name: this.className, accessFlags: accessFlags }) @@ -82,8 +92,50 @@ export class Compiler { const superClassIndex = this.constantPoolManager.indexClassInfo(this.parentClassName) const thisClassIndex = this.constantPoolManager.indexClassInfo(this.className) this.constantPoolManager.indexUtf8Info('Code') - this.handleClassBody(classNode.classBody) + const classBody = 'classBody' in classNode ? classNode.classBody : [] + this.handleClassBody(classBody) + + const constantPool = this.constantPoolManager.getPool() + return { + magic: MAGIC, + minorVersion: MINOR_VERSION, + majorVersion: MAJOR_VERSION, + constantPoolCount: this.constantPoolManager.getSize(), + constantPool: constantPool, + accessFlags: accessFlags, + thisClass: thisClassIndex, + superClass: superClassIndex, + interfacesCount: this.interfaces.length, + interfaces: this.interfaces, + fieldsCount: this.fields.length, + fields: this.fields, + methodsCount: this.methods.length, + methods: this.methods, + attributesCount: this.attributes.length, + attributes: this.attributes + } + } + + private compileEnum(enumNode: any): ClassFile { + this.className = enumNode.typeIdentifier + this.parentClassName = 'java/lang/Enum' + const accessFlags = generateClassAccessFlags(enumNode.classModifier) | 0x4000 // Add ACC_ENUM + this.symbolTable.extend() + this.symbolTable.insertClassInfo({ name: this.className, accessFlags: accessFlags }) + const superClassIndex = this.constantPoolManager.indexClassInfo(this.parentClassName) + const thisClassIndex = this.constantPoolManager.indexClassInfo(this.className) + this.constantPoolManager.indexUtf8Info('Code') + + // Handle enum constants and body members + const enumBody = enumNode.enumBody + const bodyMembers = enumBody.bodyMembers || [] + this.handleClassBody(bodyMembers) + + // TODO: Add synthetic enum fields and methods + // Add $VALUES array field + // Add ordinal, name, toString, values, valueOf methods + const constantPool = this.constantPoolManager.getPool() return { magic: MAGIC, diff --git a/src/compiler/error.ts b/src/compiler/error.ts index 1044d4fd..e0355528 100644 --- a/src/compiler/error.ts +++ b/src/compiler/error.ts @@ -50,4 +50,4 @@ export class OverrideFinalMethodError extends CompileError { constructor(name: string) { super(`Cannot override final method ${name}`) } -} \ No newline at end of file +} diff --git a/src/compiler/grammar.pegjs b/src/compiler/grammar.pegjs index a1f84bc4..41a41363 100755 --- a/src/compiler/grammar.pegjs +++ b/src/compiler/grammar.pegjs @@ -532,23 +532,31 @@ EnumDeclaration } EnumBody - = lcurly ecl:EnumConstantList? ec:EnumConstant* rcurly { - const constants = ecl ? [...ecl, ...ec] : ec; + = lcurly ecl:EnumConstantList? semicolon? em:EnumBodyMembers rcurly { + const constants = ecl || []; return addLocInfo({ kind: "EnumBody", constants: constants, + bodyMembers: em, }) } +EnumBodyMembers + = members:ClassBodyDeclaration* { + return members; + } + EnumConstantList - = @EnumConstant (comma @EnumConstant)* comma? + = first:EnumConstant rest:(comma @EnumConstant)* comma? { + return [first, ...rest]; + } EnumConstant - = name:Identifier (lparen al:ArgumentList? rparen)? cb:(lcurly ClassBodyDeclaration* rcurly)? { + = name:Identifier args:(lparen al:ArgumentList? rparen)? cb:(lcurly ClassBodyDeclaration* rcurly)? { return addLocInfo({ kind: "EnumConstant", name: name, - arguments: al || [], + arguments: (args && args[1]) ? args[1] : [], classBody: cb || [], }) } diff --git a/src/compiler/grammar.ts b/src/compiler/grammar.ts index 24861abb..0caf73ae 100755 --- a/src/compiler/grammar.ts +++ b/src/compiler/grammar.ts @@ -534,23 +534,31 @@ EnumDeclaration } EnumBody - = lcurly ecl:EnumConstantList? ec:EnumConstant* rcurly { - const constants = ecl ? [...ecl, ...ec] : ec; + = lcurly ecl:EnumConstantList? semicolon? em:EnumBodyMembers rcurly { + const constants = ecl || []; return addLocInfo({ kind: "EnumBody", constants: constants, + bodyMembers: em, }) } +EnumBodyMembers + = members:ClassBodyDeclaration* { + return members; + } + EnumConstantList - = @EnumConstant (comma @EnumConstant)* comma? + = first:EnumConstant rest:(comma @EnumConstant)* comma? { + return [first, ...rest]; + } EnumConstant - = name:Identifier (lparen al:ArgumentList? rparen)? cb:(lcurly ClassBodyDeclaration* rcurly)? { + = name:Identifier args:(lparen al:ArgumentList? rparen)? cb:(lcurly ClassBodyDeclaration* rcurly)? { return addLocInfo({ kind: "EnumConstant", name: name, - arguments: al || [], + arguments: (args && args[1]) ? args[1] : [], classBody: cb || [], }) } diff --git a/src/compiler/symbol-table.ts b/src/compiler/symbol-table.ts index 394ffd34..314ebcc4 100644 --- a/src/compiler/symbol-table.ts +++ b/src/compiler/symbol-table.ts @@ -1,18 +1,19 @@ import { UnannType } from '../ast/types/classes' import { ImportDeclaration } from '../ast/types/packages-and-modules' +import { METHOD_FLAGS } from '../ClassFile/types/methods' import { generateClassAccessFlags, generateFieldAccessFlags, generateMethodAccessFlags } from './compiler-utils' import { - InvalidMethodCallError, OverrideFinalMethodError, + InvalidMethodCallError, + OverrideFinalMethodError, SymbolCannotBeResolvedError, SymbolNotFoundError, SymbolRedeclarationError } from './error' import { libraries } from './import/libs' -import { METHOD_FLAGS } from '../ClassFile/types/methods' export const typeMap = new Map([ ['byte', 'B'], @@ -209,14 +210,17 @@ export class SymbolTable { const key = generateSymbol(info.name, SymbolType.METHOD) for (let i = this.curClassIdx - 1; i > 0; i--) { - const parentTable = this.tables[i]; + const parentTable = this.tables[i] if (parentTable.has(key)) { - const parentMethods = parentTable.get(key)!.info; + const parentMethods = parentTable.get(key)!.info if (Array.isArray(parentMethods)) { for (const m of parentMethods) { - if (m.typeDescriptor === info.typeDescriptor && (m.accessFlags & METHOD_FLAGS.ACC_FINAL) - && m.className == info.parentClassName) { - throw new OverrideFinalMethodError(info.name); + if ( + m.typeDescriptor === info.typeDescriptor && + m.accessFlags & METHOD_FLAGS.ACC_FINAL && + m.className == info.parentClassName + ) { + throw new OverrideFinalMethodError(info.name) } } } diff --git a/src/ec-evaluator/utils.ts b/src/ec-evaluator/utils.ts index 650a078f..244cf79b 100644 --- a/src/ec-evaluator/utils.ts +++ b/src/ec-evaluator/utils.ts @@ -9,6 +9,7 @@ import { ReturnStatement } from '../ast/types/blocks-and-statements' import { + ClassDeclaration, ConstructorDeclaration, FieldDeclaration, MethodDeclaration, @@ -370,9 +371,12 @@ export const appendEmtpyReturn = (method: MethodDeclaration): void => { } } -export const searchMainMtdClass = (classes: NormalClassDeclaration[]) => { - return classes.find(c => - c.classBody.some( +export const searchMainMtdClass = (classes: ClassDeclaration[]) => { + return classes.find(c => { + if (c.kind === 'EnumDeclaration') { + return false // Enums can't have main method (they have bodyMembers instead of classBody) + } + return (c as NormalClassDeclaration).classBody.some( d => d.kind === 'MethodDeclaration' && d.methodModifier.includes('public') && @@ -383,7 +387,7 @@ export const searchMainMtdClass = (classes: NormalClassDeclaration[]) => { d.methodHeader.formalParameterList[0].unannType === 'String[]' && d.methodHeader.formalParameterList[0].identifier === 'args' ) - )?.typeIdentifier + })?.typeIdentifier } /** diff --git a/src/jvm/exception-table.ts b/src/jvm/exception-table.ts index 15248a87..766b4ff1 100644 --- a/src/jvm/exception-table.ts +++ b/src/jvm/exception-table.ts @@ -1,33 +1,33 @@ -import { ClassData } from "./types/class/ClassData" +import { ClassData } from './types/class/ClassData' class Entry { - from: number - to: number - target: number - type: ClassData + 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; - } + constructor(from: number, to: number, target: number, type: ClassData) { + this.from = from + this.to = to + this.target = target + this.type = type + } } export class ExceptionTable { - private entries: Entry[] + private entries: Entry[] - retrieve(line: number): Entry | null { - this.entries.forEach(entry => { - if (line >= entry.from && line <= entry.to) { - return entry - } - }) - return null - } + retrieve(line: number): Entry | null { + this.entries.forEach(entry => { + if (line >= entry.from && line <= entry.to) { + return entry + } + }) + return null + } - insert(from: number, to: number, target: number, type: ClassData): void { - var entry = new Entry(from, to, target, type) - this.entries.push(entry) - } -} \ No newline at end of file + insert(from: number, to: number, target: number, type: ClassData): void { + const entry = new Entry(from, to, target, type) + this.entries.push(entry) + } +} diff --git a/src/types/checker/index.ts b/src/types/checker/index.ts index 0f3c39fe..ec88960b 100644 --- a/src/types/checker/index.ts +++ b/src/types/checker/index.ts @@ -639,7 +639,7 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R const methodIdentifier = (bodyDeclaration as any).methodHeader.methodDeclarator.identifier const methodName = methodIdentifier.identifier const overloadIndex = bodyDecls - .filter((n: any) => n.kind === 'MethodDeclaration' && (n as any).methodHeader.methodDeclarator.identifier.identifier === methodName) + .filter((n: any) => n.kind === 'MethodDeclaration' && (n).methodHeader.methodDeclarator.identifier.identifier === methodName) .findIndex(n => n === bodyDeclaration) const method = classType.getMethod(methodName)[overloadIndex] const methodFrame = classFrame.newChildFrame() diff --git a/src/types/checker/prechecks.ts b/src/types/checker/prechecks.ts index 9ec0a9c6..cb5b5dcc 100644 --- a/src/types/checker/prechecks.ts +++ b/src/types/checker/prechecks.ts @@ -151,7 +151,7 @@ export const addClassMethods = (node: Node, frame: Frame): Result => { for (const bodyNode of bodyDecls) { switch (bodyNode.kind) { case 'ConstructorDeclaration': { - const constructorMethod = createMethodLocal(bodyNode as ConstructorDeclaration) + const constructorMethod = createMethodLocal(bodyNode) if (constructorMethod instanceof TypeCheckerError) return newResult(null, [constructorMethod]) const error = classType.addConstructor(constructorMethod, bodyNode.location) if (error instanceof TypeCheckerError) return newResult(null, [error]) @@ -171,9 +171,9 @@ export const addClassMethods = (node: Node, frame: Frame): Result => { break } case 'MethodDeclaration': { - const methodSignature = createMethodLocal(bodyNode as MethodDeclaration) + const methodSignature = createMethodLocal(bodyNode) if (methodSignature instanceof TypeCheckerError) return newResult(null, [methodSignature]) - const methodName = (bodyNode as MethodDeclaration).methodHeader.methodDeclarator.identifier + const methodName = (bodyNode).methodHeader.methodDeclarator.identifier const error = classType.addMethod(methodName.identifier, methodSignature, methodName.location) if (error instanceof TypeCheckerError) return newResult(null, [error]) break From 954c17226cbca78cb40204fa457506518e4def87 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 26 Aug 2026 16:49:17 +0800 Subject: [PATCH 05/12] Register enum synthetic methods in symbol table - Added enumOrdinals Map to track enum constant ordinals - Registered ordinal(), name(), toString(), values(), valueOf() in symbol table - Fixed FieldInfo insertion to remove invalid 'ordinal' property - Fixed generateSimpleEnumMethod to use indexFieldrefInfo() Status: Compiler builds but enum compiler tests fail with: 1. Switch statement codegen doesn't recognize enum types 2. Bytecode generation may have structural issues Next: Fix enum type detection in switch codegen, then debug bytecode generation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/compiler/compiler.ts | 327 +++++++++++++++++++++++++++++++++++++- src/ec-evaluator/utils.ts | 2 +- 2 files changed, 321 insertions(+), 8 deletions(-) diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 93e0ddae..dbb99615 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -32,6 +32,7 @@ export class Compiler { private attributes: Array private className: string private parentClassName: string + private enumOrdinals: Map constructor() { this.setup() @@ -47,6 +48,7 @@ export class Compiler { this.fields = [] this.methods = [] this.attributes = [] + this.enumOrdinals = new Map() } compile(ast: AST) { @@ -56,8 +58,12 @@ export class Compiler { ast.topLevelClassOrInterfaceDeclarations.forEach(decl => { const className = decl.typeIdentifier - const parentClassName = (decl.kind === 'EnumDeclaration' ? 'java/lang/Enum' : - ('sclass' in decl && decl.sclass) ? decl.sclass : 'java/lang/Object') + const parentClassName = + decl.kind === 'EnumDeclaration' + ? 'java/lang/Enum' + : 'sclass' in decl && decl.sclass + ? decl.sclass + : 'java/lang/Object' const accessFlags = generateClassAccessFlags(decl.classModifier) this.symbolTable.insertClassInfo({ name: className, @@ -126,16 +132,69 @@ export class Compiler { const superClassIndex = this.constantPoolManager.indexClassInfo(this.parentClassName) const thisClassIndex = this.constantPoolManager.indexClassInfo(this.className) this.constantPoolManager.indexUtf8Info('Code') - + // Handle enum constants and body members const enumBody = enumNode.enumBody + const enumConstants = enumBody.constants || [] const bodyMembers = enumBody.bodyMembers || [] - this.handleClassBody(bodyMembers) - // TODO: Add synthetic enum fields and methods - // Add $VALUES array field - // Add ordinal, name, toString, values, valueOf methods + // Add enum constants as static fields + enumConstants.forEach((constant: any, ordinal: number) => { + const fieldDescriptor = 'L' + this.className + ';' + this.fields.push({ + accessFlags: 0x0019, // public static final + nameIndex: this.constantPoolManager.indexUtf8Info(constant.name), + descriptorIndex: this.constantPoolManager.indexUtf8Info(fieldDescriptor), + attributesCount: 0, + attributes: [] + }) + this.symbolTable.insertFieldInfo({ + name: constant.name, + accessFlags: 0x0019, + parentClassName: this.className, + typeName: this.className, + typeDescriptor: fieldDescriptor + }) + this.enumOrdinals.set(constant.name, ordinal) + }) + + // Add synthetic $VALUES field (private static final) + const valuesFieldDescriptor = '[L' + this.className + ';' + this.fields.push({ + accessFlags: 0x001a, // private static final + nameIndex: this.constantPoolManager.indexUtf8Info('$VALUES'), + descriptorIndex: this.constantPoolManager.indexUtf8Info(valuesFieldDescriptor), + attributesCount: 0, + attributes: [] + }) + + // Add $name and $ordinal fields (synthetic, private final) + this.fields.push({ + accessFlags: 0x1002, // private final synthetic + nameIndex: this.constantPoolManager.indexUtf8Info('$name'), + descriptorIndex: this.constantPoolManager.indexUtf8Info('Ljava/lang/String;'), + attributesCount: 0, + attributes: [] + }) + + this.fields.push({ + accessFlags: 0x1002, // private final synthetic + nameIndex: this.constantPoolManager.indexUtf8Info('$ordinal'), + descriptorIndex: this.constantPoolManager.indexUtf8Info('I'), + attributesCount: 0, + attributes: [] + }) + this.handleClassBody(bodyMembers) + + // Add synthetic methods + this.addEnumOrdinalMethod() + this.addEnumNameMethod() + this.addEnumToStringMethod() + this.addEnumValuesMethod(enumConstants) + this.addEnumValueOfMethod(enumConstants) + this.addEnumStaticInitializer(enumConstants) + const constantPool = this.constantPoolManager.getPool() return { magic: MAGIC, @@ -157,6 +216,260 @@ export class Compiler { } } + private addEnumOrdinalMethod() { + // public int ordinal() { return this.$ordinal; } + const nameIndex = this.constantPoolManager.indexUtf8Info('ordinal') + const descriptorIndex = this.constantPoolManager.indexUtf8Info('()I') + const codeAttribute = this.generateSimpleEnumMethod('ordinal', '$ordinal', 'I') + this.methods.push({ + accessFlags: 0x0001, // public + nameIndex: nameIndex, + descriptorIndex: descriptorIndex, + attributesCount: 1, + attributes: [codeAttribute] + }) + // Register in symbol table + this.symbolTable.insertMethodInfo({ + name: 'ordinal', + accessFlags: 0x0001, // public + parentClassName: this.className, + typeDescriptor: '()I', + className: this.className + }) + } + + private addEnumNameMethod() { + // public String name() { return this.$name; } + const nameIndex = this.constantPoolManager.indexUtf8Info('name') + const descriptorIndex = this.constantPoolManager.indexUtf8Info('()Ljava/lang/String;') + const codeAttribute = this.generateSimpleEnumMethod('name', '$name', 'Ljava/lang/String;') + this.methods.push({ + accessFlags: 0x0001, // public + nameIndex: nameIndex, + descriptorIndex: descriptorIndex, + attributesCount: 1, + attributes: [codeAttribute] + }) + // Register in symbol table + this.symbolTable.insertMethodInfo({ + name: 'name', + accessFlags: 0x0001, // public + parentClassName: this.className, + typeDescriptor: '()Ljava/lang/String;', + className: this.className + }) + } + + private addEnumToStringMethod() { + // public String toString() { return this.$name; } + const nameIndex = this.constantPoolManager.indexUtf8Info('toString') + const descriptorIndex = this.constantPoolManager.indexUtf8Info('()Ljava/lang/String;') + const codeAttribute = this.generateSimpleEnumMethod('toString', '$name', 'Ljava/lang/String;') + this.methods.push({ + accessFlags: 0x0001, // public + nameIndex: nameIndex, + descriptorIndex: descriptorIndex, + attributesCount: 1, + attributes: [codeAttribute] + }) + // Register in symbol table + this.symbolTable.insertMethodInfo({ + name: 'toString', + accessFlags: 0x0001, // public + parentClassName: this.className, + typeDescriptor: '()Ljava/lang/String;', + className: this.className + }) + } + + private addEnumValuesMethod(enumConstants: any[]) { + // public static EnumClass[] values() { return $VALUES.clone(); } + const nameIndex = this.constantPoolManager.indexUtf8Info('values') + const descriptorIndex = this.constantPoolManager.indexUtf8Info('()[L' + this.className + ';') + + // Generate bytecode: getstatic $VALUES, invokevirtual clone, areturn + const bytecode: number[] = [] + + // getstatic $VALUES + bytecode.push(0xb2) // getstatic + const valuesFieldRef = this.constantPoolManager.indexFieldrefInfo(this.className, '$VALUES', '[L' + this.className + ';') + bytecode.push((valuesFieldRef >> 8) & 0xff) + bytecode.push(valuesFieldRef & 0xff) + + // invokevirtual Object.clone() + bytecode.push(0xb6) // invokevirtual + const cloneMethodRef = this.constantPoolManager.indexMethodrefInfo('java/lang/Object', 'clone', '()Ljava/lang/Object;') + bytecode.push((cloneMethodRef >> 8) & 0xff) + bytecode.push(cloneMethodRef & 0xff) + + // checkcast to array type + bytecode.push(0xc0) // checkcast + const arrayTypeRef = this.constantPoolManager.indexClassInfo('[L' + this.className + ';') + bytecode.push((arrayTypeRef >> 8) & 0xff) + bytecode.push(arrayTypeRef & 0xff) + + // areturn + bytecode.push(0xb0) + + const codeAttribute: any = { + attributeNameIndex: this.constantPoolManager.indexUtf8Info('Code'), + attributeLength: 12 + bytecode.length, + maxStack: 1, + maxLocals: 0, + codeLength: bytecode.length, + code: bytecode, + exceptionTableLength: 0, + exceptionTable: [], + attributesCount: 0, + attributes: [] + } + + this.methods.push({ + accessFlags: 0x0009, // public static + nameIndex: nameIndex, + descriptorIndex: descriptorIndex, + attributesCount: 1, + attributes: [codeAttribute] + }) + // Register in symbol table + this.symbolTable.insertMethodInfo({ + name: 'values', + accessFlags: 0x0009, // public static + parentClassName: this.className, + typeDescriptor: '()[L' + this.className + ';', + className: this.className + }) + } + + private addEnumValueOfMethod(enumConstants: any[]) { + // public static EnumClass valueOf(String name) { return (EnumClass) Enum.valueOf(EnumClass.class, name); } + const nameIndex = this.constantPoolManager.indexUtf8Info('valueOf') + const descriptorIndex = this.constantPoolManager.indexUtf8Info('(Ljava/lang/String;)L' + this.className + ';') + + const bytecode: number[] = [] + + // ldc EnumClass.class + bytecode.push(0x12) // ldc + const classRefIndex = this.constantPoolManager.indexClassInfo(this.className) + bytecode.push(classRefIndex & 0xff) + + // aload_0 (String name parameter) + bytecode.push(0x19) + bytecode.push(0x00) + + // invokestatic java/lang/Enum.valueOf(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; + bytecode.push(0xb8) // invokestatic + const valueOfRef = this.constantPoolManager.indexMethodrefInfo('java/lang/Enum', 'valueOf', '(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;') + bytecode.push((valueOfRef >> 8) & 0xff) + bytecode.push(valueOfRef & 0xff) + + // checkcast to enum type + bytecode.push(0xc0) // checkcast + bytecode.push((classRefIndex >> 8) & 0xff) + bytecode.push(classRefIndex & 0xff) + + // areturn + bytecode.push(0xb0) + + const codeAttribute: any = { + attributeNameIndex: this.constantPoolManager.indexUtf8Info('Code'), + attributeLength: 12 + bytecode.length, + maxStack: 2, + maxLocals: 1, + codeLength: bytecode.length, + code: bytecode, + exceptionTableLength: 0, + exceptionTable: [], + attributesCount: 0, + attributes: [] + } + + this.methods.push({ + accessFlags: 0x0009, // public static + nameIndex: nameIndex, + descriptorIndex: descriptorIndex, + attributesCount: 1, + attributes: [codeAttribute] + }) + // Register in symbol table + this.symbolTable.insertMethodInfo({ + name: 'valueOf', + accessFlags: 0x0009, // public static + parentClassName: this.className, + typeDescriptor: '(Ljava/lang/String;)L' + this.className + ';', + className: this.className + }) + } + + private addEnumStaticInitializer(enumConstants: any[]) { + // Simplified: just create enum constants and populate $VALUES + // Full implementation would be complex bytecode generation + const nameIndex = this.constantPoolManager.indexUtf8Info('') + const descriptorIndex = this.constantPoolManager.indexUtf8Info('()V') + + const bytecode: number[] = [] + + // For now, just return (empty ) + // The JVM will handle basic initialization + bytecode.push(0xb1) // return + + const codeAttribute: any = { + attributeNameIndex: this.constantPoolManager.indexUtf8Info('Code'), + attributeLength: 12 + bytecode.length, + maxStack: 0, + maxLocals: 0, + codeLength: bytecode.length, + code: bytecode, + exceptionTableLength: 0, + exceptionTable: [], + attributesCount: 0, + attributes: [] + } + + this.methods.push({ + accessFlags: 0x0008, // static + nameIndex: nameIndex, + descriptorIndex: descriptorIndex, + attributesCount: 1, + attributes: [codeAttribute] + }) + } + + private generateSimpleEnumMethod(methodName: string, fieldName: string, fieldType: string): any { + // Generate: aload_0, getfield fieldName, return + const bytecode: number[] = [] + + // aload_0 (this) + bytecode.push(0x19) + bytecode.push(0x00) + + // getfield + bytecode.push(0xb4) + const fieldRef = this.constantPoolManager.indexFieldrefInfo(this.className, fieldName, fieldType) + bytecode.push((fieldRef >> 8) & 0xff) + bytecode.push(fieldRef & 0xff) + + // return (areturn for objects, ireturn for int) + if (fieldType === 'I') { + bytecode.push(0xac) // ireturn + } else { + bytecode.push(0xb0) // areturn + } + + return { + attributeNameIndex: this.constantPoolManager.indexUtf8Info('Code'), + attributeLength: 12 + bytecode.length, + maxStack: 1, + maxLocals: 1, + codeLength: bytecode.length, + code: bytecode, + exceptionTableLength: 0, + exceptionTable: [], + attributesCount: 0, + attributes: [] + } + } + private handleClassBody(classBody: Array) { const staticFields: Array = [] const nonStaticFields: Array = [] diff --git a/src/ec-evaluator/utils.ts b/src/ec-evaluator/utils.ts index 244cf79b..05642a20 100644 --- a/src/ec-evaluator/utils.ts +++ b/src/ec-evaluator/utils.ts @@ -376,7 +376,7 @@ export const searchMainMtdClass = (classes: ClassDeclaration[]) => { if (c.kind === 'EnumDeclaration') { return false // Enums can't have main method (they have bodyMembers instead of classBody) } - return (c as NormalClassDeclaration).classBody.some( + return c.classBody.some( d => d.kind === 'MethodDeclaration' && d.methodModifier.includes('public') && From b9a196c2669f5d72677044b593a5426e06560b92 Mon Sep 17 00:00:00 2001 From: kjw142857 <122250318+kjw142857@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:21:25 +0800 Subject: [PATCH 06/12] Implement exception handling in java-slang (#96) * jvm changes * include try/catch/finally support in code generator * fix try statement logic * add parser and type checker integration * fix finally bug * add tests and fix syntax error * Patch grammar logic for throws keyword * Revert "Patch grammar logic for throws keyword" This reverts commit 8e933b623ec015d1d381039bd4473e9515f82d95. * Patch grammar logic for throws keyword * Add fix for execption table finally logic * Add more tests * fix finally bug and missing test imports --------- Co-authored-by: Martin Henz --- src/ast/__tests__/statement-extractor.test.ts | 114 ++++++++- src/ast/astExtractor/class-extractor.ts | 9 +- src/ast/astExtractor/statement-extractor.ts | 85 ++++++- src/ast/types/blocks-and-statements.ts | 43 +++- .../__tests__/__utils__/test-utils.ts | 30 ++- src/compiler/__tests__/try.test.ts | 202 ++++++++++++++++ src/compiler/code-generator.ts | 224 +++++++++++++++++- src/compiler/grammar.pegjs | 84 ++++++- src/compiler/grammar.ts | 84 ++++++- src/compiler/import/lib-info.ts | 6 + src/jvm/__tests__/thread.ts | 40 ++++ src/jvm/exception-table.ts | 56 +++-- src/jvm/types/class/Attributes.ts | 12 +- src/jvm/types/class/Method.ts | 3 +- .../disassembler/utils/readAttributes.ts | 2 +- .../checker/__tests__/tryStatement.test.ts | 63 ++++- src/types/checker/environment.ts | 21 ++ src/types/checker/index.ts | 108 +++++++-- src/types/errors.ts | 6 + src/types/typeFactories/methodFactory.ts | 15 ++ src/types/types/methods.ts | 9 + src/types/types/throws.ts | 15 +- 22 files changed, 1146 insertions(+), 85 deletions(-) create mode 100644 src/compiler/__tests__/try.test.ts diff --git a/src/ast/__tests__/statement-extractor.test.ts b/src/ast/__tests__/statement-extractor.test.ts index 8ef346a9..5ebfae06 100644 --- a/src/ast/__tests__/statement-extractor.test.ts +++ b/src/ast/__tests__/statement-extractor.test.ts @@ -20,6 +20,7 @@ describe("extract ExpressionStatement correctly", () => { kind: "NormalClassDeclaration", classModifier: [], typeIdentifier: "Test", + sclass: undefined, classBody: [ { kind: "MethodDeclaration", @@ -203,8 +204,7 @@ describe("extract ExpressionStatement correctly", () => { const ast = parse(programStr); expect(ast).toEqual(expectedAst); }); - - it("extract Assignment Expression simple ExpressionName correctly", () => { + it("extract Assignment LeftHandSide qualified ExpressionName correctly", () => { const programStr = ` class Test { void test() { @@ -716,6 +716,7 @@ describe("extract ReturnStatement correctly", () => { kind: "NormalClassDeclaration", classModifier: [], typeIdentifier: "Test", + sclass: undefined, classBody: [ { kind: "MethodDeclaration", @@ -804,6 +805,115 @@ describe("extract ReturnStatement correctly", () => { location: expect.anything(), }; + const ast = parse(programStr); + console.log(JSON.stringify(ast, null, 2)); + expect(ast).toEqual(expectedAst); + }); +}); + +describe("extract TryStatement and ThrowStatement correctly", () => { + it("extract ThrowStatement inside catch block correctly", () => { + const programStr = ` + class Test { + void test() { + try { + throw new Exception(); + } catch (Exception e) { + throw new Exception(); + } + } + } + `; + + const expectedAst: AST = { + kind: "CompilationUnit", + importDeclarations: [], + topLevelClassOrInterfaceDeclarations: [ + { + kind: "NormalClassDeclaration", + classModifier: [], + typeIdentifier: "Test", + classBody: [ + { + kind: "MethodDeclaration", + methodModifier: [], + methodHeader: { + result: "void", + identifier: "test", + formalParameterList: [], + }, + methodBody: { + kind: "Block", + blockStatements: [ + { + kind: "TryStatement", + block: { + kind: "Block", + blockStatements: [ + { + kind: "ThrowStatement", + expression: { + kind: "ClassInstanceCreationExpression", + identifier: "Exception", + argumentList: [], + location: expect.anything(), + }, + location: expect.anything(), + }, + ], + location: expect.anything(), + }, + catches: { + kind: "Catches", + catchClauses: [ + { + kind: "CatchClause", + catchFormalParameter: { + kind: "CatchFormalParameter", + catchType: { + kind: "CatchType", + unannClassType: "Exception", + location: expect.anything(), + }, + variableDeclaratorId: "e", + location: expect.anything(), + }, + block: { + kind: "Block", + blockStatements: [ + { + kind: "ThrowStatement", + expression: { + kind: "ClassInstanceCreationExpression", + identifier: "Exception", + argumentList: [], + location: expect.anything(), + }, + location: expect.anything(), + }, + ], + location: expect.anything(), + }, + location: expect.anything(), + }, + ], + location: expect.anything(), + }, + finally: undefined, + location: expect.anything(), + }, + ], + location: expect.anything(), + }, + location: expect.anything(), + }, + ], + location: expect.anything(), + }, + ], + location: expect.anything(), + }; + const ast = parse(programStr); expect(ast).toEqual(expectedAst); }); diff --git a/src/ast/astExtractor/class-extractor.ts b/src/ast/astExtractor/class-extractor.ts index e043e6e7..cd55cace 100644 --- a/src/ast/astExtractor/class-extractor.ts +++ b/src/ast/astExtractor/class-extractor.ts @@ -27,14 +27,17 @@ export class ClassExtractor extends BaseJavaCstVisitorWithDefaults { extract(cst: ClassDeclarationCstNode): ClassDeclaration { this.visit(cst); - return { + const result: NormalClassDeclaration = { kind: "NormalClassDeclaration", classModifier: this.modifier, typeIdentifier: this.identifier, classBody: this.body, - sclass: this.sclass, location: cst.location, - } as NormalClassDeclaration; + }; + if (this.sclass) { + result.sclass = this.sclass; + } + return result; } classModifier(ctx: ClassModifierCtx) { diff --git a/src/ast/astExtractor/statement-extractor.ts b/src/ast/astExtractor/statement-extractor.ts index 13e9c02a..309ae541 100644 --- a/src/ast/astExtractor/statement-extractor.ts +++ b/src/ast/astExtractor/statement-extractor.ts @@ -25,6 +25,13 @@ import { SwitchBlockCtx, SwitchLabelCtx, SwitchBlockStatementGroupCtx, + ThrowStatementCtx, + TryStatementCtx, + CatchClauseCtx, + CatchFormalParameterCtx, + CatchTypeCtx, + CatchesCtx, + FinallyCtx, StatementCstNode, StatementExpressionCtx, StatementWithoutTrailingSubstatementCtx, @@ -97,6 +104,10 @@ export class StatementExtractor extends BaseJavaCstVisitorWithDefaults { exp: returnStatementExp, location: ctx.returnStatement[0].location, }; + } else if (ctx.throwStatement) { + return this.visit(ctx.throwStatement); + } else if (ctx.tryStatement) { + return this.visit(ctx.tryStatement); } } @@ -356,6 +367,69 @@ export class StatementExtractor extends BaseJavaCstVisitorWithDefaults { return ctx.expression.map((e) => expressionExtractor.extract(e)); } + throwStatement(ctx: ThrowStatementCtx) { + const expressionExtractor = new ExpressionExtractor(); + return { + kind: "ThrowStatement", + expression: expressionExtractor.extract(ctx.expression[0]), + location: ctx.Throw[0], + }; + } + + tryStatement(ctx: TryStatementCtx) { + return { + kind: "TryStatement", + block: ctx.block ? this.visit(ctx.block) : { kind: "Block", blockStatements: [], location: ctx.Try![0] }, + catches: ctx.catches ? this.visit(ctx.catches) : undefined, + finally: ctx.finally ? this.visit(ctx.finally) : undefined, + location: ctx.Try![0], + }; + } + + catches(ctx: CatchesCtx) { + return { + kind: "Catches", + catchClauses: ctx.catchClause.map((catchClause) => this.visit(catchClause)), + location: ctx.catchClause[0].location, + }; + } + + catchClause(ctx: CatchClauseCtx) { + return { + kind: "CatchClause", + catchFormalParameter: this.visit(ctx.catchFormalParameter), + block: this.visit(ctx.block), + location: ctx.Catch[0], + }; + } + + catchFormalParameter(ctx: CatchFormalParameterCtx) { + return { + kind: "CatchFormalParameter", + catchType: this.visit(ctx.catchType[0]), + variableDeclaratorId: + ctx.variableDeclaratorId[0].children.Identifier[0].image, + location: ctx.catchType[0].location, + }; + } + + catchType(ctx: CatchTypeCtx) { + const result = new TypeExtractor().visit(ctx.unannClassType[0] as any); + return { + kind: "CatchType", + unannClassType: result, + location: ctx.unannClassType[0].location, + }; + } + + finally(ctx: FinallyCtx) { + return { + kind: "Finally", + block: this.visit(ctx.block), + location: ctx.Finally[0], + }; + } + fqnOrRefType(ctx: FqnOrRefTypeCtx) { // Assignment LHS, MethodInvocation identifier let { name, location } = this.visit(ctx.fqnOrRefTypePartFirst); @@ -419,8 +493,15 @@ export class StatementExtractor extends BaseJavaCstVisitorWithDefaults { } block(ctx: BlockCtx): Statement { - if (ctx.blockStatements) return this.visit(ctx.blockStatements); - return { kind: "EmptyStatement" }; + const location = + (ctx.blockStatements?.[0] as any)?.location || + (ctx.LCurly?.[0] as any)?.location || + (ctx.RCurly?.[0] as any)?.location; + if (ctx.blockStatements) { + const block = this.visit(ctx.blockStatements) as Statement; + return { ...block, location }; + } + return { kind: "EmptyStatement", location }; } blockStatements(ctx: BlockStatementsCtx): Statement { diff --git a/src/ast/types/blocks-and-statements.ts b/src/ast/types/blocks-and-statements.ts index da4c3899..440329fc 100644 --- a/src/ast/types/blocks-and-statements.ts +++ b/src/ast/types/blocks-and-statements.ts @@ -101,7 +101,48 @@ export type StatementWithoutTrailingSubstatement = | DoStatement | ReturnStatement | BreakStatement - | ContinueStatement; + | ContinueStatement + | ThrowStatement + | TryStatement; + +export interface ThrowStatement extends BaseNode { + kind: "ThrowStatement"; + expression: Expression; +} + +export interface CatchClause extends BaseNode { + kind: "CatchClause"; + catchFormalParameter: CatchFormalParameter; + block: Block; +} + +export interface Catches extends BaseNode { + kind: "Catches"; + catchClauses: Array; +} + +export interface CatchFormalParameter extends BaseNode { + kind: "CatchFormalParameter"; + catchType: CatchType; + variableDeclaratorId: Identifier; +} + +export interface CatchType extends BaseNode { + kind: "CatchType"; + unannClassType: UnannType; +} + +export interface Finally extends BaseNode { + kind: "Finally"; + block: Block; +} + +export interface TryStatement extends BaseNode { + kind: "TryStatement"; + block: Block; + catches?: Catches; + finally?: Finally; +} export interface ExpressionStatement extends BaseNode { kind: "ExpressionStatement"; diff --git a/src/compiler/__tests__/__utils__/test-utils.ts b/src/compiler/__tests__/__utils__/test-utils.ts index 382c3907..7a7ae759 100644 --- a/src/compiler/__tests__/__utils__/test-utils.ts +++ b/src/compiler/__tests__/__utils__/test-utils.ts @@ -5,6 +5,7 @@ import { AST } from '../../../ast/types/packages-and-modules' import { javaPegGrammar } from '../../grammar' import { peggyFunctions } from '../../peggy-functions' import { execSync } from 'child_process' +import * as path from 'path' import * as peggy from 'peggy' import * as fs from 'fs' @@ -16,11 +17,9 @@ export type testCase = { } const debug = false -const pathToTestDir = './src/compiler/__tests__/' const parser = peggy.generate(peggyFunctions + javaPegGrammar, { allowedStartRules: ['CompilationUnit'] }) -const binaryWriter = new BinaryWriter() export function runTest(program: string, expectedLines: string[]) { const ast = parser.parse(program) @@ -30,18 +29,23 @@ export function runTest(program: string, expectedLines: string[]) { console.log(inspect(ast, false, null, true)) } - const classes = compile(ast as AST) - for (let c of classes) { - binaryWriter.writeBinary(c.classFile, pathToTestDir) - } + // Create a temporary directory for this test run to avoid race conditions + const tempDir = fs.mkdtempSync(path.join(process.cwd(), 'test-temp-')) + try { + const binaryWriter = new BinaryWriter() + const classes = compile(ast as AST) + for (let c of classes) { + binaryWriter.writeBinary(c.classFile, tempDir + path.sep) + } - const prevDir = process.cwd() - process.chdir(pathToTestDir) - execSync('java -noverify Main > output.log 2> err.log') + execSync('java -noverify Main > output.log 2> err.log', { cwd: tempDir }) - // ignore difference between \r\n and \n - const actualLines = fs.readFileSync('./output.log', 'utf-8').split(/\r?\n/).slice(0, -1) - process.chdir(prevDir) + // ignore difference between \r?\n and \n + const actualLines = fs.readFileSync(path.join(tempDir, 'output.log'), 'utf-8').split(/\r?\n/).slice(0, -1) - expect(actualLines).toStrictEqual(expectedLines) + expect(actualLines).toStrictEqual(expectedLines) + } finally { + // Clean up temporary directory + fs.rmSync(tempDir, { recursive: true, force: true }) + } } diff --git a/src/compiler/__tests__/try.test.ts b/src/compiler/__tests__/try.test.ts new file mode 100644 index 00000000..6c9afdce --- /dev/null +++ b/src/compiler/__tests__/try.test.ts @@ -0,0 +1,202 @@ +import { runTest, testCase } from "./__utils__/test-utils"; +import { check } from "../../types/checker"; +import { parse as parseTypeChecker } from "../../types/ast"; +import { TypeCheckerError, UnhandledExceptionError } from "../../types/errors"; + +const testCases: testCase[] = [ + { + comment: "try/catch block without exception", + program: ` + public class Main { + public static void main(String[] args) { + try { + System.out.println(1); + } catch (Exception e) { + System.out.println(2); + } + System.out.println(0); + } + } + `, + expectedLines: ["1", "0"], + }, + { + comment: "try/catch/finally block with exception handled", + program: ` + public class Main { + public static void main(String[] args) { + try { + int y = 1 / 0; + } catch (Exception e) { + System.out.println(2); + } finally { + System.out.println(3); + } + System.out.println(4); + } + } + `, + expectedLines: ["2", "3", "4"], + } + , + { + comment: "static helper method throws exception and catch handles it", + program: ` + public class Main { + public static int bar(int x) throws Exception { + int z = 1 / 0; + return x; + } + + public static void main(String[] args) { + try { + int y = bar(5); + } catch (Exception e) { + System.out.println(2); + } finally { + System.out.println(3); + } + System.out.println(4); + } + } + `, + expectedLines: ["2", "3", "4"], + }, + { + comment: "instance method calls static helper that throws exception and catch handles it", + program: ` + public class Main { + public int foo(int x) throws Exception { + int z = bar(x); + return x; + } + + public static int bar(int x) throws Exception { + int z = 1 / 0; + return x; + } + + public static void main(String[] args) { + try { + Main main = new Main(); + int y = main.foo(5); + } catch (Exception e) { + System.out.println(2); + } finally { + System.out.println(3); + } + System.out.println(4); + } + } + `, + expectedLines: ["2", "3", "4"], + }, + { + comment: "static helper method does not throw and catch is skipped", + program: ` + public class Main { + public static int bar(int x) throws Exception { + int z = 1; + return x; + } + + public static void main(String[] args) { + try { + int y = bar(5); + } catch (Exception e) { + System.out.println(2); + } finally { + System.out.println(3); + } + System.out.println(4); + } + } + `, + expectedLines: ["3", "4"], + }, + { + comment: "instance method calls static helper without throwing and catch is skipped", + program: ` + public class Main { + public int foo(int x) throws Exception { + int z = bar(x); + return x; + } + + public static int bar(int x) throws Exception { + int z = 1; + return x; + } + + public static void main(String[] args) { + try { + Main main = new Main(); + int y = main.foo(5); + } catch (Exception e) { + System.out.println(2); + } finally { + System.out.println(3); + } + System.out.println(4); + } + } + `, + expectedLines: ["3", "4"], + } +]; + +describe("try/catch", () => { + for (const testCase of testCases) { + it(testCase.comment, () => runTest(testCase.program, testCase.expectedLines)); + } +}); + +const typeCheckErrorCases = [ + { + comment: "static method declares checked exception but is not handled or propagated", + program: ` + public class Main { + public static int bar(int x) throws Exception { + int z = 1; + return x; + } + + public static void main(String[] args) { + bar(5); + } + } + ` + }, + { + comment: "instance method declares checked exception but is not handled or propagated", + program: ` + public class Main { + public int foo(int x) throws Exception { + int z = bar(x); + return x; + } + + public static int bar(int x) throws Exception { + int z = 1; + return x; + } + + public static void main(String[] args) { + Main main = new Main(); + main.foo(5); + } + } + ` + } +]; + +describe("try/catch type checking errors", () => { + for (const testCase of typeCheckErrorCases) { + it(testCase.comment, () => { + const ast = parseTypeChecker(testCase.program); + if (ast instanceof TypeCheckerError) throw new Error('Program parsing returns null.'); + const result = check(ast); + expect(result.errors.some(error => error instanceof UnhandledExceptionError)).toBe(true); + }); + } +}); diff --git a/src/compiler/code-generator.ts b/src/compiler/code-generator.ts index d26b638d..b088bf99 100644 --- a/src/compiler/code-generator.ts +++ b/src/compiler/code-generator.ts @@ -38,6 +38,7 @@ import { ConstructNotSupportedError, NoMethodMatchingSignatureError } from './error' +import { unannTypeToString } from '../types/ast/utils' import { FieldInfo, MethodInfos, SymbolInfo, SymbolTable, VariableInfo } from './symbol-table' type Label = { @@ -443,6 +444,15 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi ReturnStatement: (node: Node, cg: CodeGenerator) => { const { exp: expr } = node as ReturnStatement + + // Emit finally blocks from innermost to outermost before returning + for (let i = cg.finallyBlockStack.length - 1; i >= 0; i--) { + const finallyBlock = cg.finallyBlockStack[i] as any + finallyBlock.blockStatements.forEach((stmt: any) => { + compile(stmt, cg) + }) + } + if (expr) { const { stackSize: stackSize, resultType: resultType } = compile(expr, cg) cg.code.push(resultType in returnOp ? returnOp[resultType] : OPCODE.ARETURN) @@ -454,6 +464,14 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi }, BreakStatement: (node: Node, cg: CodeGenerator) => { + // Emit finally blocks from innermost to outermost before breaking + for (let i = cg.finallyBlockStack.length - 1; i >= 0; i--) { + const finallyBlock = cg.finallyBlockStack[i] as any + finallyBlock.blockStatements.forEach((stmt: any) => { + compile(stmt, cg) + }) + } + if (cg.loopLabels.length > 0) { // If inside a loop, break jumps to the end of the loop cg.addBranchInstr(OPCODE.GOTO, cg.loopLabels[cg.loopLabels.length - 1][1]) @@ -467,6 +485,14 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi }, ContinueStatement: (node: Node, cg: CodeGenerator) => { + // Emit finally blocks from innermost to outermost before continuing + for (let i = cg.finallyBlockStack.length - 1; i >= 0; i--) { + const finallyBlock = cg.finallyBlockStack[i] as any + finallyBlock.blockStatements.forEach((stmt: any) => { + compile(stmt, cg) + }) + } + cg.addBranchInstr(OPCODE.GOTO, cg.loopLabels[cg.loopLabels.length - 1][0]) return { stackSize: 0, resultType: EMPTY_TYPE } }, @@ -577,6 +603,194 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi return { stackSize: maxStack, resultType: resType } }, + TryStatement: (node: Node, cg: CodeGenerator) => { + let maxStack = 0 + const { block, catches } = node as any + const finallyNode: any = (node as any).finally + + const hasCatches = catches && catches.catchClauses && catches.catchClauses.length > 0 + + if (!hasCatches && !finallyNode) { + return { stackSize: compile(block, cg).stackSize, resultType: EMPTY_TYPE } + } + + if (hasCatches || finallyNode) { + maxStack = Math.max(maxStack, 1) + } + + const localExceptionTable: Array<{ + startPc: number + endPc: number + handlerLabel: Label + catchType: number + }> = [] + + // Push finally block onto stack so return/break/continue can access it + if (finallyNode) { + cg.finallyBlockStack.push(finallyNode.block) + } + + try { + // mark start of protected region + const tryStart = cg.generateNewLabel() + tryStart.offset = cg.code.length + + // compile try block + maxStack = Math.max(maxStack, compile(block, cg).stackSize) + + // end of protected region (first instruction after try block) + const tryEnd = cg.generateNewLabel() + tryEnd.offset = cg.code.length + + const catchAllLabel = finallyNode ? cg.generateNewLabel() : null + + // For normal path: run finally block if it exists + if (finallyNode) { + finallyNode.block.blockStatements.forEach((stmt: any) => { + const { stackSize } = compile(stmt, cg) + maxStack = Math.max(maxStack, stackSize) + }) + } + + // jump over handlers when try completes normally + const afterHandlers = cg.generateNewLabel() + cg.addBranchInstr(OPCODE.GOTO, afterHandlers) + + // For each catch clause, emit a handler and an exception table entry + if (hasCatches) { + for (const catchClause of catches.catchClauses) { + const handlerLabel = cg.generateNewLabel() + handlerLabel.offset = cg.code.length + + // determine catch type index (constant pool) + const catchTypeNode = catchClause.catchFormalParameter.catchType + const catchTypeName = unannTypeToString(catchTypeNode.unannClassType) + let catchClassName = 'java/lang/Throwable' + try { + catchClassName = cg.symbolTable.queryClass(catchTypeName).name + } catch (e) { + catchClassName = catchTypeName.includes('/') ? catchTypeName : catchTypeName.replace(/\./g, '/') + } + const catchTypeIndex = cg.constantPoolManager.indexClassInfo(catchClassName) + + // add exception table entry (startPc, endPc, handlerPc, catchType) + localExceptionTable.push({ + startPc: tryStart.offset, + endPc: tryEnd.offset, + handlerLabel: handlerLabel, + catchType: catchTypeIndex + }) + + // create scope for catch variable + cg.symbolTable.extend() + const varName = catchClause.catchFormalParameter.variableDeclaratorId + const varTypeStr = unannTypeToString(catchTypeNode.unannClassType) + const varInfo = { + name: varName, + accessFlags: 0, + index: cg.maxLocals, + typeName: varTypeStr, + typeDescriptor: cg.symbolTable.generateFieldDescriptor(varTypeStr) + } + cg.symbolTable.insertVariableInfo(varInfo) + if (['J', 'D'].includes(varInfo.typeDescriptor)) { + cg.maxLocals += 2 + } else { + cg.maxLocals++ + } + + // at handler entry, the exception object is on the stack; store it into the local + cg.code.push(OPCODE.ASTORE, varInfo.index) + + const catchStartOffset = cg.code.length + + // compile catch block statements + const catchBlock = catchClause.block + catchBlock.blockStatements.forEach((stmt: any) => { + const { stackSize } = compile(stmt, cg) + maxStack = Math.max(maxStack, stackSize) + }) + + const catchEndOffset = cg.code.length + + // teardown catch scope + cg.symbolTable.teardown() + + // If finally exists, add catch-all entry for this catch block + if (finallyNode && catchAllLabel && catchStartOffset < catchEndOffset) { + localExceptionTable.push({ + startPc: catchStartOffset, + endPc: catchEndOffset, + handlerLabel: catchAllLabel, + catchType: 0 + }) + } + + // For caught path: run finally block if it exists + if (finallyNode) { + finallyNode.block.blockStatements.forEach((stmt: any) => { + const { stackSize } = compile(stmt, cg) + maxStack = Math.max(maxStack, stackSize) + }) + } + + // after handler, jump to afterHandlers + cg.addBranchInstr(OPCODE.GOTO, afterHandlers) + } + } + + // If finally exists, add catch-all entry for the try block after all specific catch handlers. + // This ensures the catch clauses are matched before the generic finally rethrow path. + if (finallyNode && catchAllLabel) { + localExceptionTable.push({ + startPc: tryStart.offset, + endPc: tryEnd.offset, + handlerLabel: catchAllLabel, + catchType: 0 + }) + } + + // If finally exists, add a catch-all handler that runs finally then rethrows + if (finallyNode && catchAllLabel) { + catchAllLabel.offset = cg.code.length + + // allocate temp local to store exception + const tempIndex = cg.maxLocals + cg.maxLocals += 1 + cg.code.push(OPCODE.ASTORE, tempIndex) + + // compile finally block inside catch-all + finallyNode.block.blockStatements.forEach((stmt: any) => { + const { stackSize } = compile(stmt, cg) + maxStack = Math.max(maxStack, stackSize) + }) + + // reload exception and rethrow + cg.code.push(OPCODE.ALOAD, tempIndex, OPCODE.ATHROW) + } + + // place after-handlers label + afterHandlers.offset = cg.code.length + + // Now that all labels are resolved, push to cg.exceptionTable + localExceptionTable.forEach(entry => { + cg.exceptionTable.push({ + startPc: entry.startPc, + endPc: entry.endPc, + handlerPc: entry.handlerLabel.offset, + catchType: entry.catchType + }) + }) + + return { stackSize: maxStack, resultType: EMPTY_TYPE } + } finally { + // Pop finally block from stack when exiting + if (finallyNode) { + cg.finallyBlockStack.pop() + } + } + }, + TernaryExpression: (node: Node, cg: CodeGenerator) => { let maxStack = 0 const { @@ -1759,9 +1973,11 @@ class CodeGenerator { constantPoolManager: ConstantPoolManager maxLocals: number = 0 stackSize: number = 0 + exceptionTable: Array = [] labels: Label[] = [] loopLabels: Label[][] = [] switchLabels: Label[] = [] + finallyBlockStack: Node[] = [] code: number[] = [] currentClass: string @@ -1797,6 +2013,7 @@ class CodeGenerator { generateCode(currentClass: string, methodNode: MethodDeclaration) { this.symbolTable.extend() this.currentClass = currentClass + this.exceptionTable = [] if (!methodNode.methodModifier.includes('static')) { this.maxLocals++ } @@ -1835,7 +2052,6 @@ class CodeGenerator { } this.resolveLabels() - const exceptionTable: Array = [] const attributes: Array = [] const codeBuf = new Uint8Array(this.code).buffer const dataView = new DataView(codeBuf) @@ -1844,7 +2060,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() @@ -1855,8 +2071,8 @@ class CodeGenerator { maxLocals: this.maxLocals, codeLength: this.code.length, code: dataView, - exceptionTableLength: exceptionTable.length, - exceptionTable: exceptionTable, + exceptionTableLength: this.exceptionTable.length, + exceptionTable: this.exceptionTable, attributesCount: attributes.length, attributes: attributes } diff --git a/src/compiler/grammar.pegjs b/src/compiler/grammar.pegjs index 41a41363..0c071fa1 100755 --- a/src/compiler/grammar.pegjs +++ b/src/compiler/grammar.pegjs @@ -701,7 +701,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 { @@ -896,8 +910,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 0caf73ae..6745673c 100755 --- a/src/compiler/grammar.ts +++ b/src/compiler/grammar.ts @@ -703,7 +703,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 { @@ -898,8 +912,74 @@ ThrowStatement SynchronizedStatement = synchronized lparen Expression rparen Block +Catches + = catchClauses:CatchClause+ { + return addLocInfo({ + kind: "Catches", + catchClauses, + }) + } + +CatchClause + = catch lparen catchFormalParameter:CatchFormalParameter rparen block:Block { + return addLocInfo({ + kind: "CatchClause", + catchFormalParameter, + block, + }) + } + +CatchFormalParameter + = variableModifiers:VariableModifier* catchType:CatchType variableDeclaratorId:VariableDeclaratorId { + return addLocInfo({ + kind: "CatchFormalParameter", + variableModifiers, + catchType, + variableDeclaratorId, + }) + } + +CatchType + = unannClassType:UnannClassType classTypes:( _ '|' _ c:ClassType { return c })* { + return addLocInfo({ + kind: "CatchType", + unannClassType, + classTypes: classTypes.length ? classTypes : undefined, + }) + } + +UnannClassType + = typeIdentifier:TypeIdentifier { + return addLocInfo({ + kind: "UnannClassType", + typeIdentifier: { identifier: typeIdentifier }, + }) + } + +Finally + = finally block:Block { + return addLocInfo({ + kind: "Finally", + block, + }) + } + TryStatement - = TO_BE_ADDED + = try block:Block catches:Catches finallyNode:Finally? { + return addLocInfo({ + kind: "TryStatement", + block, + catches, + finally: finallyNode, + }) + } + / try block:Block finallyNode:Finally { + return addLocInfo({ + kind: "TryStatement", + block, + finally: finallyNode, + }) + } IfStatement = if lparen expr:Expression rparen c:Statement a:(else @Statement)? { diff --git a/src/compiler/import/lib-info.ts b/src/compiler/import/lib-info.ts index 8db187d6..720ced9d 100644 --- a/src/compiler/import/lib-info.ts +++ b/src/compiler/import/lib-info.ts @@ -13,6 +13,12 @@ export const rawLibInfo = { name: 'public final java.lang.System', fields: ['public static final java.io.PrintStream out'] }, + { + name: 'public class java.lang.Throwable' + }, + { + name: 'public class java.lang.Exception' + }, { name: 'public final java.lang.Math', methods: [ diff --git a/src/jvm/__tests__/thread.ts b/src/jvm/__tests__/thread.ts index e4ae911e..07395191 100644 --- a/src/jvm/__tests__/thread.ts +++ b/src/jvm/__tests__/thread.ts @@ -4,7 +4,9 @@ import { ReferenceClassData } from '../types/class/ClassData' import { JvmObject } from '../types/reference/Object' import Thread from '../../jvm/thread' import JVM from '../../jvm/jvm' +import { JavaStackFrame } from '../../jvm/stackframe' import { setupTest, TestThreadPool } from './__utils__/test-utils' +import { METHOD_FLAGS } from '../../ClassFile/types/methods' let thread: Thread let threadClass: ReferenceClassData @@ -67,4 +69,42 @@ describe('Thread', () => { test('should manage wide (64-bit) values on the operand stack correctly', () => { // TODO }) + + test('should route an exception to a matching try-catch handler in the current method', () => { + const setup = setupTest() + const { testLoader, thread: testThread, classes } = setup + const exceptionMethodClass = testLoader.createClass({ + className: 'TryCatchTest', + loader: testLoader, + methods: [ + { + accessFlags: [METHOD_FLAGS.ACC_PUBLIC], + name: 'test0', + descriptor: '()V', + attributes: [], + code: new DataView(new ArrayBuffer(1)), + exceptionTable: [ + { + startPc: 0, + endPc: 1, + handlerPc: 0, + catchType: 'java/lang/NullPointerException' + } + ] + } + ], + }) as ReferenceClassData + + const method = exceptionMethodClass.getMethod('test0()V') + expect(method).not.toBeNull() + + testThread.invokeStackFrame( + new JavaStackFrame(exceptionMethodClass, method as any, 0, []) + ) + const exceptionObj = classes.NullPointerException.instantiate() + testThread.throwException(exceptionObj) + + expect(testThread.getPC()).toBe(0) + expect(testThread.peekStackFrame().operandStack).toEqual([exceptionObj]) + }) }) diff --git a/src/jvm/exception-table.ts b/src/jvm/exception-table.ts index 766b4ff1..7cd39d9e 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 - } +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 { - const 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 } } diff --git a/src/jvm/types/class/Attributes.ts b/src/jvm/types/class/Attributes.ts index 92b5ee33..f3f2a051 100644 --- a/src/jvm/types/class/Attributes.ts +++ b/src/jvm/types/class/Attributes.ts @@ -15,6 +15,7 @@ import { SourceFileAttribute, StackMapFrame } from '../../../ClassFile/types/attributes' +import { ExceptionTable } from '../../exception-table' import { ConstantPool } from '../../constant-pool' import { ConstantClass, @@ -45,7 +46,8 @@ export const info2Attribute = (info: AttributeInfo, constantPool: ConstantPool): case 'Code': const code = info as CodeAttribute const attr: { [attributeName: string]: IAttribute } = {} - const exceptionTable = code.exceptionTable.map(handler => { + const exceptionTable = new ExceptionTable( + code.exceptionTable.map(handler => { return { startPc: handler.startPc, endPc: handler.endPc, @@ -54,6 +56,7 @@ export const info2Attribute = (info: AttributeInfo, constantPool: ConstantPool): handler.catchType === 0 ? null : (constantPool.get(handler.catchType) as ConstantClass) } }) + ) code.attributes.forEach(element => { attr[(constantPool.get(element.attributeNameIndex) as ConstantUtf8).get()] = info2Attribute( element, @@ -244,12 +247,7 @@ export interface Code extends IAttribute { codeLength: number code: DataView exceptionTableLength: number - exceptionTable: Array<{ - startPc: number - endPc: number - handlerPc: number - catchType: ConstantClass | null - }> + exceptionTable: ExceptionTable attributes: { [attributeName: string]: IAttribute } diff --git a/src/jvm/types/class/Method.ts b/src/jvm/types/class/Method.ts index b0adb49c..8e803643 100644 --- a/src/jvm/types/class/Method.ts +++ b/src/jvm/types/class/Method.ts @@ -6,6 +6,7 @@ import { attrInfo2Interface, parseMethodDescriptor, getArgs, logger } from '../. import { ErrorResult, ImmediateResult, ResultType, SuccessResult } from '../Result' import { JavaType, JvmObject } from '../reference/Object' import { Code, Exceptions, IAttribute, NestHost, Signature } from './Attributes' +import { ExceptionTable } from '../../exception-table' import { ReferenceClassData, ArrayClassData, ClassData } from './ClassData' import { ConstantClass, ConstantMethodref, ConstantNameAndType, ConstantUtf8 } from './Constants' @@ -484,7 +485,7 @@ export class Method { codeLength: dv.buffer.byteLength, code: dv, exceptionTableLength: 0, - exceptionTable: [], + exceptionTable: new ExceptionTable(), attributes: {} } as Code }, diff --git a/src/jvm/utils/disassembler/utils/readAttributes.ts b/src/jvm/utils/disassembler/utils/readAttributes.ts index a2c624fa..0f94bdea 100644 --- a/src/jvm/utils/disassembler/utils/readAttributes.ts +++ b/src/jvm/utils/disassembler/utils/readAttributes.ts @@ -186,7 +186,7 @@ function readCodeAttribute( throw new Error('Class format error: Code attribute invalid length') } - const code = new DataView(view.buffer, offset, codeLength) + const code = new DataView(view.buffer, view.byteOffset + offset, codeLength) offset += codeLength const exceptionTableLength = view.getUint16(offset) diff --git a/src/types/checker/__tests__/tryStatement.test.ts b/src/types/checker/__tests__/tryStatement.test.ts index 975a7246..90860225 100644 --- a/src/types/checker/__tests__/tryStatement.test.ts +++ b/src/types/checker/__tests__/tryStatement.test.ts @@ -1,8 +1,8 @@ import { check } from '..' import { parse } from '../../ast' import { - ExceptionHasAlreadyBeenCaughtError, IncompatibleTypesError, + UnhandledExceptionError, TypeCheckerError } from '../../errors' import { Type } from '../../types/type' @@ -14,11 +14,17 @@ const createProgram = (statement: string) => ` } } ` +const createClass = (body: string) => ` + public class Main { + ${body} + } +` const testcases: { input: string result: { type: Type | null; errors: Error[] } only?: boolean + fullProgram?: boolean }[] = [ { input: ` @@ -30,9 +36,58 @@ const testcases: { input: ` try {} catch (Throwable e) {} - catch (Exception e) {} `, - result: { type: null, errors: [new ExceptionHasAlreadyBeenCaughtError()] } + result: { type: null, errors: [] } + }, + { + input: ` + public static void foo() throws Exception { + throw new Exception(); + } + public static void main(String args[]) { + foo(); + } + `, + fullProgram: true, + result: { type: null, errors: [new UnhandledExceptionError()] } + }, + { + input: ` + public static void foo() throws Exception { + throw new Exception(); + } + public static void main(String args[]) throws Exception { + foo(); + } + `, + fullProgram: true, + result: { type: null, errors: [] } + }, + { + input: ` + public static void foo() throws Exception { + throw new Exception(); + } + public static void main(String args[]) { + try { + foo(); + } catch (Exception e) { + } + } + `, + fullProgram: true, + result: { type: null, errors: [] } + }, + { + input: ` + try { + throw new Exception(); + } catch (Exception e) { + throw new Exception(); + } finally { + } + `, + result: { type: null, errors: [] } }, { input: ` @@ -48,7 +103,7 @@ describe('Type Checker', () => { let it = test if (testcase.only) it = test.only it(`Checking try statements for ${testcase.input}`, () => { - const program = createProgram(testcase.input) + const program = testcase.fullProgram ? createClass(testcase.input) : createProgram(testcase.input) const ast = parse(program) if (!ast) throw new Error('Program parsing returns null.') if (ast instanceof TypeCheckerError) throw new Error('Test case is invalid.') diff --git a/src/types/checker/environment.ts b/src/types/checker/environment.ts index 92e12851..0769567b 100644 --- a/src/types/checker/environment.ts +++ b/src/types/checker/environment.ts @@ -53,6 +53,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[] = [] @@ -73,6 +75,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 ec88960b..daf9aee9 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, @@ -475,14 +477,60 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R if (argumentList instanceof TypeCheckerError) return newResult(null, [...errors, argumentList]) - for (let i = 0; i < methods.length - 1; i++) { + // Resolve overload: find the first applicable method + let selectedMethod: Method | null = null + let selectedReturnType: Type | TypeCheckerError | null = null + let lastInvokeError: TypeCheckerError | null = null + for (let i = 0; i < methods.length; i++) { const result = methods[i].invoke(argumentList) - if (result instanceof TypeCheckerError) continue - return newResult(result, errors) + if (result instanceof TypeCheckerError) { + lastInvokeError = result + continue + } + selectedMethod = methods[i] + selectedReturnType = result + break + } + if (selectedMethod === null || selectedReturnType === null) { + // If there was exactly one candidate and it produced a specific + // type-check error (e.g. incompatible types), surface that error + // instead of the generic "method cannot be applied" message. + if (methods.length === 1 && lastInvokeError) return newResult(null, [...errors, lastInvokeError]) + return newResult(null, [...errors, new MethodCannotBeAppliedError(node.location)]) } - const returnType = methods[methods.length - 1].invoke(argumentList) - if (returnType instanceof TypeCheckerError) return newResult(null, [...errors, returnType]) - return newResult(returnType, errors) + + // Enforce declared exceptions from the invoked method: any checked exception + // must either be caught by an enclosing try/catch or declared by the current method. + const declaredExceptions: any[] = + (selectedMethod as any).getThrownExceptions?.() || [] + if (declaredExceptions.length > 0) { + const exceptionBase = frame.getType('Exception', node.location) + for (const declaredException of declaredExceptions) { + // If we cannot determine checkedness, be conservative and treat as checked + let isChecked = true + if (!(exceptionBase instanceof TypeCheckerError)) { + // checked if it's an Exception subtype + isChecked = (exceptionBase as any).canBeAssigned(declaredException) + } + if (!isChecked) continue + + // check if caught by any active catch in scope + const activeCaught = frame.getActiveCaughtExceptions() + const isCaught = activeCaught.some(caughtType => caughtType.canBeAssigned(declaredException)) + if (isCaught) continue + + // check if current method declares it + const declaredByCurrent = frame.getThrows() + const isDeclared = declaredByCurrent.some(declared => declared.canBeAssigned(declaredException)) + if (isDeclared) continue + + return newResult(null, [new UnhandledExceptionError(node.location)]) + } + } + + if (selectedReturnType instanceof TypeCheckerError) + return newResult(null, [...errors, selectedReturnType]) + return newResult(selectedReturnType, errors) } case 'NormalClassDeclaration': { const errors: TypeCheckerError[] = [] @@ -519,6 +567,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 @@ -563,6 +615,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) @@ -756,12 +812,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), @@ -769,7 +826,7 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R ) if (catchType instanceof TypeCheckerError) { errors.push(catchType) - return + continue } const checkCatchTypeError = checkTryCatchType( catchType, @@ -778,12 +835,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, @@ -791,16 +865,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 f2a823e5e39c4eef8a38d89b6809261da8c99b80 Mon Sep 17 00:00:00 2001 From: kjw142857 <122250318+kjw142857@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:46:43 +0800 Subject: [PATCH 07/12] Add feature details in README (#88) * Include current and planned features in README * Update compiler README * Delete src/compiler/__tests__/tests/typeConversion.test.ts * Delete eslint.config.mjs * Add files via upload * Add files via upload --------- Co-authored-by: Martin Henz --- README.md | 1 + src/compiler/README.md | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 2f498ced..e7d7f7f8 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ The Java language in Source Academy currently supports a host of available featu - Explicit type conversion (type narrowing) - Implicit type conversion for system calls (e.g. int input to System.out.println) - Single nested class +- Exceptions ## Future Features diff --git a/src/compiler/README.md b/src/compiler/README.md index c63de654..7fa0faab 100644 --- a/src/compiler/README.md +++ b/src/compiler/README.md @@ -1,7 +1,5 @@ This is a bookkeeping of the planned scope of the compiler. It will be updated from time to time to reflect the current status of the compiler and to make the scope clearer. For a more formal treatment of what features are being supported, see scope.txt for a BNF-form of the Java sub-language. -Note that the compiler is separate from the Java Playground in the online version of Source Academy, which runs in tandem with the ECE. As such, any program run in the Playground will follow the features implemented in the ECE (e.g. widening type conversions), rather than the features below. - **Features that are already supported** - Single source file, single public class, with exactly one main method From 4cfcdf1a10216762359893a1f70d16cce73098e2 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 2 Sep 2026 09:42:38 +0800 Subject: [PATCH 08/12] fix exception table merge conflict --- src/jvm/exception-table.ts | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/jvm/exception-table.ts b/src/jvm/exception-table.ts index 16eb654b..7bcf0d99 100644 --- a/src/jvm/exception-table.ts +++ b/src/jvm/exception-table.ts @@ -42,25 +42,4 @@ export class ExceptionTable implements Iterable { get length() { return this.entries.length } - return null - } - - insert(startPc: number, endPc: number, handlerPc: number, catchType: ClassData | null): void { - this.entries.push({ startPc, endPc, handlerPc, catchType }) - } - - toArray(): ExceptionTableEntry[] { - return this.entries.slice() - } - - [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 From 21c3f1fe03a7da8a05cf126f7c24b1d5a6244d3b Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 2 Sep 2026 10:12:11 +0800 Subject: [PATCH 09/12] integrate enum types into switch statements --- src/compiler/__tests__/tests/enum.test.ts | 44 ++++ src/compiler/code-generator.ts | 13 +- src/compiler/compiler.ts | 258 ++++++++++------------ src/compiler/symbol-table.ts | 1 + 4 files changed, 175 insertions(+), 141 deletions(-) diff --git a/src/compiler/__tests__/tests/enum.test.ts b/src/compiler/__tests__/tests/enum.test.ts index 659e017d..f86bf744 100644 --- a/src/compiler/__tests__/tests/enum.test.ts +++ b/src/compiler/__tests__/tests/enum.test.ts @@ -4,6 +4,50 @@ import { } from "../__utils__/test-utils"; const testCases: testCase[] = [ + { + comment: "member enum constant access", + program: ` + public class Main { + public enum Day { + SUNDAY, + MONDAY, + TUESDAY, + WEDNESDAY, + THURSDAY, + FRIDAY, + SATURDAY + } + + public static void main(String[] args) { + Day day = Day.SUNDAY; + } + } + `, + expectedLines: [], + }, + { + comment: "member enum switch selects the matching constant", + program: ` + public class Main { + public enum Light { RED, YELLOW, GREEN } + + public static void main(String[] args) { + Light light = Light.GREEN; + switch (light) { + case Light.RED: + System.out.println("stop"); + break; + case Light.GREEN: + System.out.println("go"); + break; + default: + System.out.println("wait"); + } + } + } + `, + expectedLines: ["go"], + }, { comment: "enum switch and synthetic methods", program: ` diff --git a/src/compiler/code-generator.ts b/src/compiler/code-generator.ts index b088bf99..3e932c6f 100644 --- a/src/compiler/code-generator.ts +++ b/src/compiler/code-generator.ts @@ -1596,6 +1596,7 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi // If the expression is an enum type, invoke ordinal() to convert to int and then continue let _resultType = resultType + let enumTypeName: string | null = null if (_resultType && _resultType.startsWith('L') && _resultType !== 'Ljava/lang/String;') { const clean = _resultType.replace(/^L|;$/g, '') try { @@ -1608,6 +1609,7 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi cg.constantPoolManager.indexMethodrefInfo('java/lang/Enum', 'ordinal', '()I') ) _resultType = 'I' + enumTypeName = clean maxStack = Math.max(maxStack, exprStackSize + 1) } } catch (e) { @@ -1631,7 +1633,16 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi cases.forEach((caseGroup, index) => { caseGroup.labels.forEach(label => { if (label.kind === 'CaseLabel') { - const value = parseInt((label.expression as Literal).literalType.value) + const value = + label.expression.kind === 'ExpressionName' && enumTypeName + ? (() => { + const fields = cg.symbolTable.queryField(label.expression.name) + const field = fields[fields.length - 1] as FieldInfo + if (field.parentClassName !== enumTypeName || field.ordinal === undefined) + throw new Error(`Invalid enum switch label: ${label.expression.name}`) + return field.ordinal + })() + : parseInt((label.expression as Literal).literalType.value) caseValues.push(value) caseLabelMap.set(value, caseLabels[index]) } else if (label.kind === 'DefaultLabel') { diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index dbb99615..9f02bfec 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -4,6 +4,7 @@ import { ClassBodyDeclaration, ClassDeclaration, ConstructorDeclaration, + EnumDeclaration, FieldDeclaration, MethodDeclaration } from '../ast/types/classes' @@ -55,8 +56,21 @@ export class Compiler { this.setup() this.symbolTable.handleImports(ast.importDeclarations) const classFiles: Array = [] - - ast.topLevelClassOrInterfaceDeclarations.forEach(decl => { + const declarations = [ + ...ast.topLevelClassOrInterfaceDeclarations, + ...ast.topLevelClassOrInterfaceDeclarations.flatMap(declaration => + declaration.kind === 'NormalClassDeclaration' + ? this.getMemberEnums(declaration.classBody) + : [] + ) + ] + + const compilationOrder = [ + ...declarations.filter(declaration => declaration.kind === 'EnumDeclaration'), + ...declarations.filter(declaration => declaration.kind !== 'EnumDeclaration') + ] + + declarations.forEach(decl => { const className = decl.typeIdentifier const parentClassName = decl.kind === 'EnumDeclaration' @@ -64,7 +78,9 @@ export class Compiler { : 'sclass' in decl && decl.sclass ? decl.sclass : 'java/lang/Object' - const accessFlags = generateClassAccessFlags(decl.classModifier) + const accessFlags = + generateClassAccessFlags(decl.classModifier) | + (decl.kind === 'EnumDeclaration' ? 0x4000 : 0) this.symbolTable.insertClassInfo({ name: className, accessFlags: accessFlags, @@ -73,7 +89,7 @@ export class Compiler { this.symbolTable.returnToRoot() }) - ast.topLevelClassOrInterfaceDeclarations.forEach(decl => { + compilationOrder.forEach(decl => { this.resetClassFileState() if (decl.kind === 'EnumDeclaration') { const classFile = this.compileEnum(decl) @@ -87,6 +103,16 @@ export class Compiler { return classFiles } + private getMemberEnums(classBody: Array): Array { + return classBody.flatMap(declaration => { + if (declaration.kind !== 'EnumDeclaration') return [] + return [ + declaration, + ...this.getMemberEnums(declaration.enumBody.bodyMembers || []) + ] + }) + } + private compileClass(classNode: ClassDeclaration): ClassFile { this.className = classNode.typeIdentifier const sclass = 'sclass' in classNode ? classNode.sclass : undefined @@ -153,7 +179,8 @@ export class Compiler { accessFlags: 0x0019, parentClassName: this.className, typeName: this.className, - typeDescriptor: fieldDescriptor + typeDescriptor: fieldDescriptor, + ordinal }) this.enumOrdinals.set(constant.name, ordinal) }) @@ -168,29 +195,13 @@ export class Compiler { attributes: [] }) - // Add $name and $ordinal fields (synthetic, private final) - this.fields.push({ - accessFlags: 0x1002, // private final synthetic - nameIndex: this.constantPoolManager.indexUtf8Info('$name'), - descriptorIndex: this.constantPoolManager.indexUtf8Info('Ljava/lang/String;'), - attributesCount: 0, - attributes: [] - }) - - this.fields.push({ - accessFlags: 0x1002, // private final synthetic - nameIndex: this.constantPoolManager.indexUtf8Info('$ordinal'), - descriptorIndex: this.constantPoolManager.indexUtf8Info('I'), - attributesCount: 0, - attributes: [] - }) - - this.handleClassBody(bodyMembers) + if (bodyMembers.length === 0) { + this.addEnumConstructor() + } else { + this.handleClassBody(bodyMembers) + } // Add synthetic methods - this.addEnumOrdinalMethod() - this.addEnumNameMethod() - this.addEnumToStringMethod() this.addEnumValuesMethod(enumConstants) this.addEnumValueOfMethod(enumConstants) this.addEnumStaticInitializer(enumConstants) @@ -216,70 +227,46 @@ export class Compiler { } } - private addEnumOrdinalMethod() { - // public int ordinal() { return this.$ordinal; } - const nameIndex = this.constantPoolManager.indexUtf8Info('ordinal') - const descriptorIndex = this.constantPoolManager.indexUtf8Info('()I') - const codeAttribute = this.generateSimpleEnumMethod('ordinal', '$ordinal', 'I') - this.methods.push({ - accessFlags: 0x0001, // public - nameIndex: nameIndex, - descriptorIndex: descriptorIndex, - attributesCount: 1, - attributes: [codeAttribute] - }) - // Register in symbol table - this.symbolTable.insertMethodInfo({ - name: 'ordinal', - accessFlags: 0x0001, // public - parentClassName: this.className, - typeDescriptor: '()I', - className: this.className - }) - } + private addEnumConstructor() { + const bytecode = [ + 0x19, + 0x00, + 0x19, + 0x01, + 0x15, + 0x02, + 0xb7 + ] + const constructorRef = this.constantPoolManager.indexMethodrefInfo( + 'java/lang/Enum', + '', + '(Ljava/lang/String;I)V' + ) + bytecode.push((constructorRef >> 8) & 0xff, constructorRef & 0xff, 0xb1) + const codeAttribute = this.createEnumCodeAttribute(bytecode, 3, 3) - private addEnumNameMethod() { - // public String name() { return this.$name; } - const nameIndex = this.constantPoolManager.indexUtf8Info('name') - const descriptorIndex = this.constantPoolManager.indexUtf8Info('()Ljava/lang/String;') - const codeAttribute = this.generateSimpleEnumMethod('name', '$name', 'Ljava/lang/String;') this.methods.push({ - accessFlags: 0x0001, // public - nameIndex: nameIndex, - descriptorIndex: descriptorIndex, + accessFlags: 0x0002, + nameIndex: this.constantPoolManager.indexUtf8Info(''), + descriptorIndex: this.constantPoolManager.indexUtf8Info('(Ljava/lang/String;I)V'), attributesCount: 1, attributes: [codeAttribute] }) - // Register in symbol table - this.symbolTable.insertMethodInfo({ - name: 'name', - accessFlags: 0x0001, // public - parentClassName: this.className, - typeDescriptor: '()Ljava/lang/String;', - className: this.className - }) } - private addEnumToStringMethod() { - // public String toString() { return this.$name; } - const nameIndex = this.constantPoolManager.indexUtf8Info('toString') - const descriptorIndex = this.constantPoolManager.indexUtf8Info('()Ljava/lang/String;') - const codeAttribute = this.generateSimpleEnumMethod('toString', '$name', 'Ljava/lang/String;') - this.methods.push({ - accessFlags: 0x0001, // public - nameIndex: nameIndex, - descriptorIndex: descriptorIndex, - attributesCount: 1, - attributes: [codeAttribute] - }) - // Register in symbol table - this.symbolTable.insertMethodInfo({ - name: 'toString', - accessFlags: 0x0001, // public - parentClassName: this.className, - typeDescriptor: '()Ljava/lang/String;', - className: this.className - }) + private createEnumCodeAttribute(bytecode: number[], maxStack: number, maxLocals: number): any { + return { + attributeNameIndex: this.constantPoolManager.indexUtf8Info('Code'), + attributeLength: 12 + bytecode.length, + maxStack, + maxLocals, + codeLength: bytecode.length, + code: new DataView(new Uint8Array(bytecode).buffer), + exceptionTableLength: 0, + exceptionTable: [], + attributesCount: 0, + attributes: [] + } } private addEnumValuesMethod(enumConstants: any[]) { @@ -317,7 +304,7 @@ export class Compiler { maxStack: 1, maxLocals: 0, codeLength: bytecode.length, - code: bytecode, + code: new DataView(new Uint8Array(bytecode).buffer), exceptionTableLength: 0, exceptionTable: [], attributesCount: 0, @@ -377,7 +364,7 @@ export class Compiler { maxStack: 2, maxLocals: 1, codeLength: bytecode.length, - code: bytecode, + code: new DataView(new Uint8Array(bytecode).buffer), exceptionTableLength: 0, exceptionTable: [], attributesCount: 0, @@ -402,30 +389,56 @@ export class Compiler { } private addEnumStaticInitializer(enumConstants: any[]) { - // Simplified: just create enum constants and populate $VALUES - // Full implementation would be complex bytecode generation const nameIndex = this.constantPoolManager.indexUtf8Info('') const descriptorIndex = this.constantPoolManager.indexUtf8Info('()V') - const bytecode: number[] = [] - - // For now, just return (empty ) - // The JVM will handle basic initialization - bytecode.push(0xb1) // return - - const codeAttribute: any = { - attributeNameIndex: this.constantPoolManager.indexUtf8Info('Code'), - attributeLength: 12 + bytecode.length, - maxStack: 0, - maxLocals: 0, - codeLength: bytecode.length, - code: bytecode, - exceptionTableLength: 0, - exceptionTable: [], - attributesCount: 0, - attributes: [] + const enumClassRef = this.constantPoolManager.indexClassInfo(this.className) + const constructorRef = this.constantPoolManager.indexMethodrefInfo( + this.className, + '', + '(Ljava/lang/String;I)V' + ) + const emitInteger = (value: number) => { + if (value <= 5) bytecode.push(0x03 + value) + else bytecode.push(0x10, value) } - + const emitLdc = (constantPoolIndex: number) => { + bytecode.push(0x13, (constantPoolIndex >> 8) & 0xff, constantPoolIndex & 0xff) + } + + enumConstants.forEach((constant, ordinal) => { + bytecode.push(0xbb, (enumClassRef >> 8) & 0xff, enumClassRef & 0xff, 0x59) + emitLdc(this.constantPoolManager.indexStringInfo(constant.name)) + emitInteger(ordinal) + bytecode.push(0xb7, (constructorRef >> 8) & 0xff, constructorRef & 0xff) + const fieldRef = this.constantPoolManager.indexFieldrefInfo( + this.className, + constant.name, + `L${this.className};` + ) + bytecode.push(0xb3, (fieldRef >> 8) & 0xff, fieldRef & 0xff) + }) + + emitInteger(enumConstants.length) + bytecode.push(0xbd, (enumClassRef >> 8) & 0xff, enumClassRef & 0xff) + enumConstants.forEach((constant, ordinal) => { + bytecode.push(0x59) + emitInteger(ordinal) + const fieldRef = this.constantPoolManager.indexFieldrefInfo( + this.className, + constant.name, + `L${this.className};` + ) + bytecode.push(0xb2, (fieldRef >> 8) & 0xff, fieldRef & 0xff, 0x53) + }) + const valuesFieldRef = this.constantPoolManager.indexFieldrefInfo( + this.className, + '$VALUES', + `[L${this.className};` + ) + bytecode.push(0xb3, (valuesFieldRef >> 8) & 0xff, valuesFieldRef & 0xff, 0xb1) + const codeAttribute = this.createEnumCodeAttribute(bytecode, 4, 0) + this.methods.push({ accessFlags: 0x0008, // static nameIndex: nameIndex, @@ -435,41 +448,6 @@ export class Compiler { }) } - private generateSimpleEnumMethod(methodName: string, fieldName: string, fieldType: string): any { - // Generate: aload_0, getfield fieldName, return - const bytecode: number[] = [] - - // aload_0 (this) - bytecode.push(0x19) - bytecode.push(0x00) - - // getfield - bytecode.push(0xb4) - const fieldRef = this.constantPoolManager.indexFieldrefInfo(this.className, fieldName, fieldType) - bytecode.push((fieldRef >> 8) & 0xff) - bytecode.push(fieldRef & 0xff) - - // return (areturn for objects, ireturn for int) - if (fieldType === 'I') { - bytecode.push(0xac) // ireturn - } else { - bytecode.push(0xb0) // areturn - } - - return { - attributeNameIndex: this.constantPoolManager.indexUtf8Info('Code'), - attributeLength: 12 + bytecode.length, - maxStack: 1, - maxLocals: 1, - codeLength: bytecode.length, - code: bytecode, - exceptionTableLength: 0, - exceptionTable: [], - attributesCount: 0, - attributes: [] - } - } - private handleClassBody(classBody: Array) { const staticFields: Array = [] const nonStaticFields: Array = [] diff --git a/src/compiler/symbol-table.ts b/src/compiler/symbol-table.ts index 314ebcc4..72fabeb2 100644 --- a/src/compiler/symbol-table.ts +++ b/src/compiler/symbol-table.ts @@ -56,6 +56,7 @@ export interface FieldInfo { parentClassName: string typeName: string typeDescriptor: string + ordinal?: number } export type MethodInfos = Array From d9d766520b09a8a3d4f9db3d2f05dd946b52e6c3 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 2 Sep 2026 10:37:28 +0800 Subject: [PATCH 10/12] add enum ordinals --- src/compiler/__tests__/tests/enum.test.ts | 41 +++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/compiler/__tests__/tests/enum.test.ts b/src/compiler/__tests__/tests/enum.test.ts index f86bf744..0551a110 100644 --- a/src/compiler/__tests__/tests/enum.test.ts +++ b/src/compiler/__tests__/tests/enum.test.ts @@ -48,6 +48,47 @@ const testCases: testCase[] = [ `, expectedLines: ["go"], }, + { + comment: "member enum switch matches the first of seven constants", + program: ` + class Main { + public enum Day { + SUNDAY, MONDAY, TUESDAY, WEDNESDAY, + THURSDAY, FRIDAY, SATURDAY + } + + public static void main(String[] args) { + Day day = Day.SUNDAY; + switch (day) { + case Day.SUNDAY: + System.out.println(0); + break; + case Day.MONDAY: + System.out.println(1); + break; + case Day.TUESDAY: + System.out.println(2); + break; + case Day.WEDNESDAY: + System.out.println(3); + break; + case Day.THURSDAY: + System.out.println(4); + break; + case Day.FRIDAY: + System.out.println(5); + break; + case Day.SATURDAY: + System.out.println(6); + break; + default: + break; + } + } + } + `, + expectedLines: ["0"], + }, { comment: "enum switch and synthetic methods", program: ` From 9d3e723e93ced434c251ab0fa71926235c00f7ad Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 2 Sep 2026 10:58:07 +0800 Subject: [PATCH 11/12] correct enum grammar --- src/compiler/__tests__/tests/enum.test.ts | 48 ++++++++++----- src/compiler/code-generator.ts | 11 ++-- src/compiler/compiler.ts | 72 +++++++++++++++++------ src/compiler/grammar.pegjs | 2 +- src/compiler/grammar.ts | 2 +- src/compiler/symbol-table.ts | 1 + 6 files changed, 98 insertions(+), 38 deletions(-) diff --git a/src/compiler/__tests__/tests/enum.test.ts b/src/compiler/__tests__/tests/enum.test.ts index 0551a110..683baf74 100644 --- a/src/compiler/__tests__/tests/enum.test.ts +++ b/src/compiler/__tests__/tests/enum.test.ts @@ -2,6 +2,7 @@ import { runTest, testCase, } from "../__utils__/test-utils"; +import { compileFromSource } from "../../index"; const testCases: testCase[] = [ { @@ -34,10 +35,10 @@ const testCases: testCase[] = [ public static void main(String[] args) { Light light = Light.GREEN; switch (light) { - case Light.RED: + case RED: System.out.println("stop"); break; - case Light.GREEN: + case GREEN: System.out.println("go"); break; default: @@ -59,26 +60,27 @@ const testCases: testCase[] = [ public static void main(String[] args) { Day day = Day.SUNDAY; + System.out.println(10); switch (day) { - case Day.SUNDAY: + case SUNDAY: System.out.println(0); break; - case Day.MONDAY: + case MONDAY: System.out.println(1); break; - case Day.TUESDAY: + case TUESDAY: System.out.println(2); break; - case Day.WEDNESDAY: + case WEDNESDAY: System.out.println(3); break; - case Day.THURSDAY: + case THURSDAY: System.out.println(4); break; - case Day.FRIDAY: + case FRIDAY: System.out.println(5); break; - case Day.SATURDAY: + case SATURDAY: System.out.println(6); break; default: @@ -87,7 +89,7 @@ const testCases: testCase[] = [ } } `, - expectedLines: ["0"], + expectedLines: ["10", "0"], }, { comment: "enum switch and synthetic methods", @@ -106,10 +108,10 @@ const testCases: testCase[] = [ Color selector = Color.BLUE; switch (selector) { - case Color.RED: + case RED: System.out.println("bad"); break; - case Color.BLUE: + case BLUE: System.out.println("ok"); break; default: @@ -135,7 +137,7 @@ const testCases: testCase[] = [ Direction[] fresh = Direction.values(); switch (fresh[0]) { - case Direction.NORTH: + case NORTH: System.out.println("fresh"); break; default: @@ -143,7 +145,7 @@ const testCases: testCase[] = [ } switch (copy[0]) { - case Direction.SOUTH: + case SOUTH: System.out.println("mutated"); break; default: @@ -189,4 +191,22 @@ export const enumTest = () => describe("enums", () => { const { comment: comment, program: program, expectedLines: expectedLines } = testCase; it(comment, () => runTest(program, expectedLines)); } + + it("rejects qualified enum switch labels", () => { + expect(() => + compileFromSource(` + class Main { + enum Day { SUNDAY } + + public static void main(String[] args) { + Day day = Day.SUNDAY; + switch (day) { + case Day.SUNDAY: + break; + } + } + } + `) + ).toThrow(SyntaxError); + }); }); diff --git a/src/compiler/code-generator.ts b/src/compiler/code-generator.ts index 3e932c6f..52c72f3d 100644 --- a/src/compiler/code-generator.ts +++ b/src/compiler/code-generator.ts @@ -1,5 +1,4 @@ import { OPCODE } from '../ClassFile/constants/instructions' -import { ACCESS_FLAGS } from '../ClassFile/types' import { ExceptionHandler, AttributeInfo } from '../ClassFile/types/attributes' import { FIELD_FLAGS } from '../ClassFile/types/fields' import { METHOD_FLAGS } from '../ClassFile/types/methods' @@ -1601,12 +1600,12 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi const clean = _resultType.replace(/^L|;$/g, '') try { const classInfo = cg.symbolTable.queryClass(clean) - if (classInfo.accessFlags & ACCESS_FLAGS.ACC_ENUM) { - // call java.lang.Enum.ordinal() (returns int) + if (classInfo.isEnum) { + // Generated enums provide their own ordinal() method so they run without java.lang.Enum. cg.code.push( OPCODE.INVOKEVIRTUAL, 0, - cg.constantPoolManager.indexMethodrefInfo('java/lang/Enum', 'ordinal', '()I') + cg.constantPoolManager.indexMethodrefInfo(clean, 'ordinal', '()I') ) _resultType = 'I' enumTypeName = clean @@ -1636,7 +1635,9 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi const value = label.expression.kind === 'ExpressionName' && enumTypeName ? (() => { - const fields = cg.symbolTable.queryField(label.expression.name) + const fields = cg.symbolTable.queryField( + `${enumTypeName}.${label.expression.name}` + ) const field = fields[fields.length - 1] as FieldInfo if (field.parentClassName !== enumTypeName || field.ordinal === undefined) throw new Error(`Invalid enum switch label: ${label.expression.name}`) diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 9f02bfec..44d90825 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -73,18 +73,15 @@ export class Compiler { declarations.forEach(decl => { const className = decl.typeIdentifier const parentClassName = - decl.kind === 'EnumDeclaration' - ? 'java/lang/Enum' - : 'sclass' in decl && decl.sclass + 'sclass' in decl && decl.sclass ? decl.sclass : 'java/lang/Object' - const accessFlags = - generateClassAccessFlags(decl.classModifier) | - (decl.kind === 'EnumDeclaration' ? 0x4000 : 0) + const accessFlags = generateClassAccessFlags(decl.classModifier) this.symbolTable.insertClassInfo({ name: className, accessFlags: accessFlags, - parentClassName: parentClassName + parentClassName: parentClassName, + isEnum: decl.kind === 'EnumDeclaration' }) this.symbolTable.returnToRoot() }) @@ -150,8 +147,8 @@ export class Compiler { private compileEnum(enumNode: any): ClassFile { this.className = enumNode.typeIdentifier - this.parentClassName = 'java/lang/Enum' - const accessFlags = generateClassAccessFlags(enumNode.classModifier) | 0x4000 // Add ACC_ENUM + this.parentClassName = 'java/lang/Object' + const accessFlags = generateClassAccessFlags(enumNode.classModifier) this.symbolTable.extend() this.symbolTable.insertClassInfo({ name: this.className, accessFlags: accessFlags }) @@ -194,9 +191,18 @@ export class Compiler { attributesCount: 0, attributes: [] }) + + this.fields.push({ + accessFlags: 0x1002, + nameIndex: this.constantPoolManager.indexUtf8Info('$ordinal'), + descriptorIndex: this.constantPoolManager.indexUtf8Info('I'), + attributesCount: 0, + attributes: [] + }) if (bodyMembers.length === 0) { this.addEnumConstructor() + this.addEnumOrdinalMethod() } else { this.handleClassBody(bodyMembers) } @@ -231,19 +237,31 @@ export class Compiler { const bytecode = [ 0x19, 0x00, - 0x19, - 0x01, - 0x15, - 0x02, 0xb7 ] const constructorRef = this.constantPoolManager.indexMethodrefInfo( - 'java/lang/Enum', + 'java/lang/Object', '', - '(Ljava/lang/String;I)V' + '()V' + ) + const ordinalFieldRef = this.constantPoolManager.indexFieldrefInfo( + this.className, + '$ordinal', + 'I' ) - bytecode.push((constructorRef >> 8) & 0xff, constructorRef & 0xff, 0xb1) - const codeAttribute = this.createEnumCodeAttribute(bytecode, 3, 3) + bytecode.push( + (constructorRef >> 8) & 0xff, + constructorRef & 0xff, + 0x19, + 0x00, + 0x15, + 0x02, + 0xb5, + (ordinalFieldRef >> 8) & 0xff, + ordinalFieldRef & 0xff, + 0xb1 + ) + const codeAttribute = this.createEnumCodeAttribute(bytecode, 2, 3) this.methods.push({ accessFlags: 0x0002, @@ -254,6 +272,26 @@ export class Compiler { }) } + private addEnumOrdinalMethod() { + const fieldRef = this.constantPoolManager.indexFieldrefInfo(this.className, '$ordinal', 'I') + const codeAttribute = this.createEnumCodeAttribute([0x19, 0x00, 0xb4, fieldRef >> 8, fieldRef & 0xff, 0xac], 1, 1) + + this.methods.push({ + accessFlags: 0x0001, + nameIndex: this.constantPoolManager.indexUtf8Info('ordinal'), + descriptorIndex: this.constantPoolManager.indexUtf8Info('()I'), + attributesCount: 1, + attributes: [codeAttribute] + }) + this.symbolTable.insertMethodInfo({ + name: 'ordinal', + accessFlags: 0x0001, + parentClassName: this.className, + typeDescriptor: '()I', + className: this.className + }) + } + private createEnumCodeAttribute(bytecode: number[], maxStack: number, maxLocals: number): any { return { attributeNameIndex: this.constantPoolManager.indexUtf8Info('Code'), diff --git a/src/compiler/grammar.pegjs b/src/compiler/grammar.pegjs index 0c071fa1..5b6647be 100755 --- a/src/compiler/grammar.pegjs +++ b/src/compiler/grammar.pegjs @@ -855,7 +855,7 @@ SwitchBlockStatementGroup } SwitchLabel - = case expr:Expression colon { + = case expr:(Literal / id:Identifier { return addLocInfo({ kind: "ExpressionName", name: id }) }) colon { return { kind: "CaseLabel", expression: expr, diff --git a/src/compiler/grammar.ts b/src/compiler/grammar.ts index 6745673c..645f71ab 100755 --- a/src/compiler/grammar.ts +++ b/src/compiler/grammar.ts @@ -857,7 +857,7 @@ SwitchBlockStatementGroup } SwitchLabel - = case expr:Expression colon { + = case expr:(Literal / id:Identifier { return addLocInfo({ kind: "ExpressionName", name: id }) }) colon { return { kind: "CaseLabel", expression: expr, diff --git a/src/compiler/symbol-table.ts b/src/compiler/symbol-table.ts index 72fabeb2..c0374648 100644 --- a/src/compiler/symbol-table.ts +++ b/src/compiler/symbol-table.ts @@ -48,6 +48,7 @@ export interface ClassInfo { name: string accessFlags: number parentClassName?: string + isEnum?: boolean } export interface FieldInfo { From 52781ca31342b110a075fb10db06715cbac90d45 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 2 Sep 2026 12:03:17 +0800 Subject: [PATCH 12/12] remove excessively long logging statement --- src/ast/__tests__/switch-statement-extractor.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ast/__tests__/switch-statement-extractor.test.ts b/src/ast/__tests__/switch-statement-extractor.test.ts index 00ae21ea..0e20f487 100644 --- a/src/ast/__tests__/switch-statement-extractor.test.ts +++ b/src/ast/__tests__/switch-statement-extractor.test.ts @@ -429,7 +429,6 @@ describe("extract SwitchStatement correctly", () => { }; const ast = parse(programStr); - console.log(JSON.stringify(ast, null, 2)); expect(ast).toEqual(expectedAst); }); });