diff --git a/src/ast/__tests__/class-extractor.test.ts b/src/ast/__tests__/class-extractor.test.ts index 16df694..8f930c5 100644 --- a/src/ast/__tests__/class-extractor.test.ts +++ b/src/ast/__tests__/class-extractor.test.ts @@ -124,3 +124,39 @@ describe("extract multiple NormalClassDeclaration correctly", () => { expect(ast).toEqual(expectedAst); }); }); + +describe("extract nested NormalClassDeclaration correctly", () => { + it("extract a static nested class inside a class body correctly", () => { + const programStr = ` + class Outer { + static class Inner {} + } + `; + + const expectedAst: AST = { + kind: "CompilationUnit", + importDeclarations: [], + topLevelClassOrInterfaceDeclarations: [ + { + kind: "NormalClassDeclaration", + classModifier: [], + typeIdentifier: "Outer", + classBody: [ + { + kind: "NormalClassDeclaration", + classModifier: ["static"], + typeIdentifier: "Inner", + classBody: [], + 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 cd55cac..3896aa2 100644 --- a/src/ast/astExtractor/class-extractor.ts +++ b/src/ast/astExtractor/class-extractor.ts @@ -91,5 +91,12 @@ export class ClassExtractor extends BaseJavaCstVisitorWithDefaults { this.body.push(methodNode); }) } + if (ctx.classDeclaration) { + ctx.classDeclaration.forEach(x => { + const classExtractor = new ClassExtractor(); + const classNode = classExtractor.extract(x); + this.body.push(classNode); + }) + } } } diff --git a/src/ast/types/classes.ts b/src/ast/types/classes.ts index 576d9aa..38df2ef 100644 --- a/src/ast/types/classes.ts +++ b/src/ast/types/classes.ts @@ -43,7 +43,7 @@ export type ClassModifier = | "strictfp" | "enum"; -export type ClassBodyDeclaration = ClassMemberDeclaration | ConstructorDeclaration | EnumDeclaration; +export type ClassBodyDeclaration = ClassMemberDeclaration | ConstructorDeclaration | EnumDeclaration | NormalClassDeclaration; export type ClassMemberDeclaration = MethodDeclaration | FieldDeclaration; export interface ConstructorDeclaration extends BaseNode { diff --git a/src/compiler/__tests__/classOrdering.test.ts b/src/compiler/__tests__/classOrdering.test.ts index 838e548..22c51a5 100644 --- a/src/compiler/__tests__/classOrdering.test.ts +++ b/src/compiler/__tests__/classOrdering.test.ts @@ -11,7 +11,7 @@ describe('compiled class ordering', () => { } ` const classes = compileFromSource(program) - expect(classes.map(c => c.className)).toEqual(['Main', 'Day']) + expect(classes.map(c => c.className)).toEqual(['Main', 'Main$Day']) }) it('keeps top-level declaration order, with member enums appended', () => { @@ -24,7 +24,9 @@ describe('compiled class ordering', () => { ` const classes = compileFromSource(program) expect(classes[0].className).toBe('Main') - expect(new Set(classes.map(c => c.className))).toEqual(new Set(['Main', 'A', 'B'])) + expect(new Set(classes.map(c => c.className))).toEqual( + new Set(['Main', 'Main$A', 'Main$B']) + ) }) it('is unchanged when there is no enum', () => { diff --git a/src/compiler/__tests__/index.ts b/src/compiler/__tests__/index.ts index bddc2a2..eefc9b3 100644 --- a/src/compiler/__tests__/index.ts +++ b/src/compiler/__tests__/index.ts @@ -10,6 +10,7 @@ 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 { nestedClassesTest } from "./tests/nestedClasses.test"; import { typeConversionTest } from "./tests/typeConversion.test"; describe("compiler tests", () => { @@ -25,5 +26,6 @@ describe("compiler tests", () => { arrayTest(); classTest(); enumTest(); + nestedClassesTest(); typeConversionTest(); }) diff --git a/src/compiler/__tests__/tests/class.test.ts b/src/compiler/__tests__/tests/class.test.ts index 4ec2a9b..4a34d8c 100644 --- a/src/compiler/__tests__/tests/class.test.ts +++ b/src/compiler/__tests__/tests/class.test.ts @@ -103,6 +103,19 @@ const testCases: testCase[] = [ `, expectedLines: ["in f"], }, + { + comment: "instance field with an inline initializer", + program: ` + public class Main { + public int one = 1; + public static void main(String[] args) { + Main m = new Main(); + System.out.println(m.one); + } + } + `, + expectedLines: ["1"], + }, { comment: "instance field", program: ` diff --git a/src/compiler/__tests__/tests/nestedClasses.test.ts b/src/compiler/__tests__/tests/nestedClasses.test.ts new file mode 100644 index 0000000..6f48b7a --- /dev/null +++ b/src/compiler/__tests__/tests/nestedClasses.test.ts @@ -0,0 +1,265 @@ +import { + runTest, + testCase, +} from "../__utils__/test-utils"; +import { compileFromSource } from "../../index"; + +const testCases: testCase[] = [ + { + comment: "basic static nested class instantiation and usage", + program: ` + public class Main { + public static class Inner { + public int value; + public Inner(int v) { + this.value = v; + } + public int getValue() { + return this.value; + } + } + + public static void main(String[] args) { + Inner inner = new Inner(42); + System.out.println(inner.getValue()); + } + } + `, + expectedLines: ["42"], + }, + { + comment: "one nested class field typed as a sibling nested class", + program: ` + public class Main { + public static class Box { + public int value; + public Box(int v) { + this.value = v; + } + } + + public static class Holder { + public Box box; + public Holder(Box b) { + this.box = b; + } + } + + public static void main(String[] args) { + Box box = new Box(9); + Holder holder = new Holder(box); + System.out.println(holder.box.value); + } + } + `, + expectedLines: ["9"], + }, + { + comment: "nested class instance field with an inline initializer", + program: ` + public class Main { + static class Inner1 { + public int one = 1; + } + public static void main(String[] args) { + Inner1 inner1 = new Inner1(); + int n = inner1.one; + System.out.println(n); + } + } + `, + expectedLines: ["1"], + }, + { + comment: "nested class forward-references a sibling class declared after it", + program: ` + public class Main { + static class A { + int get() { return B.code; } + } + static class B { + static int code = 9; + } + public static void main(String[] args) { + A a = new A(); + System.out.println(a.get()); + } + } + `, + expectedLines: ["9"], + }, + { + comment: "nested class forward-references a sibling enum declared after it", + program: ` + public class Main { + static class Box { + Color get() { return Color.RED; } + } + enum Color { RED, BLUE } + public static void main(String[] args) { + Box box = new Box(); + Color c = box.get(); + System.out.println(c.name()); + } + } + `, + expectedLines: ["RED"], + }, + { + comment: "two levels of static nested class", + program: ` + public class Main { + public static class Middle { + public static class Inner { + public String greet() { + return "hello"; + } + } + } + + public static void main(String[] args) { + Inner inner = new Inner(); + System.out.println(inner.greet()); + } + } + `, + expectedLines: ["hello"], + }, + { + comment: "two unrelated top-level classes each have a nested class with the same simple name", + program: ` + class A { + static class Item {} + } + class B { + static class Item {} + } + public class Main { + public static void main(String[] args) { + System.out.println(0); + } + } + `, + expectedLines: ["0"], + }, + { + comment: "nested class extends a sibling nested class", + program: ` + public class Main { + static class Base {} + static class Child extends Base { + public int get() { return 1; } + } + + public static void main(String[] args) { + Child child = new Child(); + System.out.println(child.get()); + } + } + `, + expectedLines: ["1"], + }, + { + comment: "two levels deep: nested class extends a sibling nested class", + program: ` + public class Main { + static class Middle { + static class Base {} + static class Child extends Base { + public int get() { return 1; } + } + } + + public static void main(String[] args) { + Child child = new Child(); + System.out.println(child.get()); + } + } + `, + expectedLines: ["1"], + }, + { + comment: "nested class extends its own (nested) enclosing class", + program: ` + public class Main { + static class Middle { + static class Child extends Middle { + public int get() { return 1; } + } + } + + public static void main(String[] args) { + Child child = new Child(); + System.out.println(child.get()); + } + } + `, + expectedLines: ["1"], + }, + { + comment: "unqualified declaration, qualified instantiation (new Outer.Inner())", + program: ` + public class Main { + static class Inner { + public int get() { return 1; } + } + public static void main(String[] args) { + Inner inner = new Main.Inner(); + System.out.println(inner.get()); + } + } + `, + expectedLines: ["1"], + }, + { + comment: "qualified declaration and instantiation (Outer.Inner on both sides)", + program: ` + public class Main { + static class Inner { + public int get() { return 1; } + } + public static void main(String[] args) { + Main.Inner inner = new Main.Inner(); + System.out.println(inner.get()); + } + } + `, + expectedLines: ["1"], + }, + { + comment: "two levels deep, fully qualified (Outer.Middle.Inner)", + program: ` + public class Main { + static class Middle { + static class Inner { + public int get() { return 1; } + } + } + public static void main(String[] args) { + Main.Middle.Inner inner = new Main.Middle.Inner(); + System.out.println(inner.get()); + } + } + `, + expectedLines: ["1"], + }, +]; + +export const nestedClassesTest = () => describe("static nested classes", () => { + for (let testCase of testCases) { + const { comment: comment, program: program, expectedLines: expectedLines } = testCase; + it(comment, () => runTest(program, expectedLines)); + } + + it("still rejects two top-level classes sharing the same name", () => { + expect(() => + compileFromSource(` + class Item {} + class Item {} + public class Main { + public static void main(String[] args) {} + } + `) + ).toThrow('"Item" has already been declared') + }) +}); diff --git a/src/compiler/code-generator.ts b/src/compiler/code-generator.ts index 9937a0d..5cc812d 100644 --- a/src/compiler/code-generator.ts +++ b/src/compiler/code-generator.ts @@ -1098,7 +1098,13 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi } } const res = compile(expr, cg) - const classInfoIndex = cg.constantPoolManager.indexClassInfo(ct) + let castClassName = ct + try { + castClassName = cg.symbolTable.queryClass(ct).name + } catch (e) { + castClassName = ct.includes('/') ? ct : ct.replace(/\./g, '/') + } + const classInfoIndex = cg.constantPoolManager.indexClassInfo(castClassName) cg.code.push(OPCODE.CHECKCAST, 0, classInfoIndex) return res }, @@ -1107,7 +1113,18 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi const { identifier: id, argumentList: argLst } = node as ClassInstanceCreationExpression let maxStack = 2 - cg.code.push(OPCODE.NEW, 0, cg.constantPoolManager.indexClassInfo(id), OPCODE.DUP) + let instantiatedClassName = id + try { + instantiatedClassName = cg.symbolTable.queryClass(id).name + } catch (e) { + instantiatedClassName = id.includes('/') ? id : id.replace(/\./g, '/') + } + cg.code.push( + OPCODE.NEW, + 0, + cg.constantPoolManager.indexClassInfo(instantiatedClassName), + OPCODE.DUP + ) const argTypes: Array = [] argLst.forEach((x, i) => { @@ -1120,7 +1137,10 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi const methodInfos = cg.symbolTable.queryMethod('') as MethodInfos for (let i = 0; i < methodInfos.length; i++) { const methodInfo = methodInfos[i] - if (methodInfo.typeDescriptor.includes(argDescriptor) && methodInfo.className == id) { + if ( + methodInfo.typeDescriptor.includes(argDescriptor) && + methodInfo.className == instantiatedClassName + ) { const method = cg.constantPoolManager.indexMethodrefInfo( methodInfo.className, methodInfo.name, @@ -1131,7 +1151,7 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi } } - return { stackSize: maxStack, resultType: id } + return { stackSize: maxStack, resultType: instantiatedClassName } }, ArrayAccess: (node: Node, cg: CodeGenerator) => { diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index ee65704..5d18473 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -6,18 +6,20 @@ import { ConstructorDeclaration, EnumDeclaration, FieldDeclaration, - MethodDeclaration + MethodDeclaration, + NormalClassDeclaration } from '../ast/types/classes' import { AttributeInfo } from '../ClassFile/types/attributes' import { FieldInfo } from '../ClassFile/types/fields' import { MethodInfo } from '../ClassFile/types/methods' +import { ConstructNotSupportedError } from './error' import { ConstantPoolManager } from './constant-pool-manager' import { generateClassAccessFlags, generateFieldAccessFlags, generateMethodAccessFlags } from './compiler-utils' -import { SymbolTable } from './symbol-table' +import { SymbolTable, Table } from './symbol-table' import { generateCode } from './code-generator' const MAGIC = 0xcafebabe @@ -33,7 +35,6 @@ export class Compiler { private attributes: Array private className: string private parentClassName: string - private enumOrdinals: Map constructor() { this.setup() @@ -49,46 +50,68 @@ export class Compiler { this.fields = [] this.methods = [] this.attributes = [] - this.enumOrdinals = new Map() } compile(ast: AST) { this.setup() this.symbolTable.handleImports(ast.importDeclarations) - const declarations = [ - ...ast.topLevelClassOrInterfaceDeclarations, - ...ast.topLevelClassOrInterfaceDeclarations.flatMap(declaration => - declaration.kind === 'NormalClassDeclaration' - ? this.getMemberEnums(declaration.classBody) - : [] - ) - ] - - // Enums are compiled first so their synthetic members are in the symbol - // table before the classes that reference them are compiled. - const compilationOrder = [ - ...declarations.filter(declaration => declaration.kind === 'EnumDeclaration'), - ...declarations.filter(declaration => declaration.kind !== 'EnumDeclaration') - ] + const topLevelDeclarations = ast.topLevelClassOrInterfaceDeclarations + const memberDeclarations = topLevelDeclarations.flatMap(declaration => + declaration.kind === 'NormalClassDeclaration' + ? this.getMemberTypes(declaration.classBody, declaration.typeIdentifier) + : [] + ) + const declarations = [...topLevelDeclarations, ...memberDeclarations] + const compilationOrder = [...memberDeclarations, ...topLevelDeclarations] + // Pass 0: register every class's identity (name/parent/isEnum) up + // front, so a type name resolves via queryClass(...) regardless of + // declaration order. declarations.forEach(decl => { const className = decl.typeIdentifier const parentClassName = 'sclass' in decl && decl.sclass ? decl.sclass : 'java/lang/Object' const accessFlags = generateClassAccessFlags(decl.classModifier) - this.symbolTable.insertClassInfo({ - name: className, - accessFlags: accessFlags, - parentClassName: parentClassName, - isEnum: decl.kind === 'EnumDeclaration' - }) + // Nested types are registered under both their simple name (so + // unqualified references from within the compilation unit resolve) and + // their qualified binary name (so a descriptor like `LOuter$Inner;` + // can be resolved back to this class's info via queryClass(...)). + const simpleName = className.split('$').pop() as string + this.symbolTable.insertClassInfo( + { + name: className, + accessFlags: accessFlags, + parentClassName: parentClassName, + isEnum: decl.kind === 'EnumDeclaration' + }, + [simpleName, className] + ) this.symbolTable.returnToRoot() }) + // Pass 1: register every class's own members (fields, methods, + // constructors, enum constants and synthetic members) with no bytecode + // generated yet. This makes member resolution order-independent: a + // sibling declared later in the same scope is already fully visible by + // the time pass 2 generates any bytecode that might reference it. + const classScopes = new Map<(typeof declarations)[number], Table>() + compilationOrder.forEach(decl => { + const scope = + decl.kind === 'EnumDeclaration' + ? this.registerEnumSignature(decl) + : this.registerClassSignature(decl) + classScopes.set(decl, scope) + }) + + // Pass 2: generate the actual bytecode for every class, re-entering + // each class's own member scope registered in pass 1. const compiled = new Map<(typeof declarations)[number], Class>() compilationOrder.forEach(decl => { this.resetClassFileState() + this.symbolTable.restoreClassScope(classScopes.get(decl) as Table) const classFile = - decl.kind === 'EnumDeclaration' ? this.compileEnum(decl) : this.compileClass(decl) + decl.kind === 'EnumDeclaration' + ? this.generateEnumClassFile(decl) + : this.generateClassFile(decl) compiled.set(decl, { classFile: classFile, className: this.className }) }) @@ -97,26 +120,108 @@ export class Compiler { return declarations.map(decl => compiled.get(decl) as Class) } - private getMemberEnums(classBody: Array): Array { + /** + * Flattens nested enum and static nested class declarations out of a class + * body, rewriting each `typeIdentifier` to its JVM binary name + * (`Outer$Inner`, `Outer$Middle$Inner`, ...) so the rest of the compiler can + * treat them exactly like top-level declarations. + * + * `ancestorBinaryNames` maps the simple name of every enclosing class (and + * its siblings, transitively) to its binary name. It lets an `extends` + * clause that targets a sibling nested type - or the enclosing class + * itself, if that's also nested - get rewritten to the right binary name + * too (see the `sclass` rewrite below); without it the emitted class file + * would reference a nonexistent top-level class. + */ + private getMemberTypes( + classBody: Array, + enclosingBinaryName: string, + ancestorBinaryNames: Map = new Map() + ): Array { + // Built before flat-mapping (so a forward-declared sibling's `extends` + // target still resolves) and passed down to every child's recursive + // call, so deeper nesting levels also see this level's siblings. + const siblingBinaryNames = new Map(ancestorBinaryNames) + for (const declaration of classBody) { + if (declaration.kind === 'EnumDeclaration' || declaration.kind === 'NormalClassDeclaration') { + siblingBinaryNames.set( + declaration.typeIdentifier, + enclosingBinaryName + '$' + declaration.typeIdentifier + ) + } + } + return classBody.flatMap(declaration => { - if (declaration.kind !== 'EnumDeclaration') return [] - return [declaration, ...this.getMemberEnums(declaration.enumBody.bodyMembers || [])] + if (declaration.kind === 'EnumDeclaration') { + const qualified = { + ...declaration, + typeIdentifier: enclosingBinaryName + '$' + declaration.typeIdentifier + } + return [ + qualified, + ...this.getMemberTypes( + qualified.enumBody.bodyMembers || [], + qualified.typeIdentifier, + siblingBinaryNames + ) + ] + } + if (declaration.kind === 'NormalClassDeclaration') { + if (!declaration.classModifier.includes('static')) { + throw new ConstructNotSupportedError('non-static nested class') + } + const qualified = { + ...declaration, + typeIdentifier: enclosingBinaryName + '$' + declaration.typeIdentifier, + ...(declaration.sclass && siblingBinaryNames.has(declaration.sclass) + ? { sclass: siblingBinaryNames.get(declaration.sclass) } + : {}) + } + return [ + qualified, + ...this.getMemberTypes(qualified.classBody, qualified.typeIdentifier, siblingBinaryNames) + ] + } + return [] }) } - private compileClass(classNode: ClassDeclaration): ClassFile { + /** + * Pass 1 for a class: registers its own member signatures (fields, + * methods, constructors) in a fresh scope, with no bytecode generated. + * Returns that scope so pass 2 can re-enter it via restoreClassScope. + */ + private registerClassSignature(classNode: ClassDeclaration): Table { this.className = classNode.typeIdentifier 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 }) + const scope = this.symbolTable.insertClassInfo({ name: this.className, accessFlags }, [ + this.className.split('$').pop() as string, + this.className + ]) + const classBody = 'classBody' in classNode ? classNode.classBody : [] + this.registerClassBodyMembers(classBody) + return scope + } + + /** + * Pass 2 for a class: generates its bytecode, assuming its own scope has + * already been restored (via restoreClassScope) and every other class's + * members are already registered, regardless of declaration order. + */ + private generateClassFile(classNode: ClassDeclaration): ClassFile { + this.className = classNode.typeIdentifier + const sclass = 'sclass' in classNode ? classNode.sclass : undefined + this.parentClassName = sclass ? sclass : 'java/lang/Object' + const accessFlags = generateClassAccessFlags(classNode.classModifier) const superClassIndex = this.constantPoolManager.indexClassInfo(this.parentClassName) const thisClassIndex = this.constantPoolManager.indexClassInfo(this.className) this.constantPoolManager.indexUtf8Info('Code') const classBody = 'classBody' in classNode ? classNode.classBody : [] - this.handleClassBody(classBody) + this.generateClassBody(classBody) const constantPool = this.constantPoolManager.getPool() return { @@ -139,45 +244,65 @@ export class Compiler { } } - private compileEnum(enumNode: any): ClassFile { + /** + * Pass 1 for an enum: registers its constants, inherited/synthetic Enum + * methods, and any explicit body members, with no bytecode generated. + */ + private registerEnumSignature(enumNode: any): Table { this.className = enumNode.typeIdentifier - // Generated enums genuinely extend java.lang.Enum: the constructor chains to - // Enum.(String, int) and name()/ordinal()/compareTo() are inherited. this.parentClassName = 'java/lang/Enum' const accessFlags = generateClassAccessFlags(enumNode.classModifier) | 0x4000 // 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 enumConstants = enumBody.constants || [] - const bodyMembers = enumBody.bodyMembers || [] + const scope = this.symbolTable.insertClassInfo({ name: this.className, accessFlags }, [ + this.className.split('$').pop() as string, + this.className + ]) - // Add enum constants as static fields + const enumConstants = enumNode.enumBody.constants || [] 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, + typeDescriptor: 'L' + this.className + ';', ordinal }) - this.enumOrdinals.set(constant.name, ordinal) }) + // name(), ordinal(), compareTo() etc. are inherited from java.lang.Enum; + // register the ones reachable from Source programs so calls resolve. + this.registerInheritedEnumMethods() + this.registerEnumSyntheticMethodSignatures() + + const bodyMembers = enumNode.enumBody.bodyMembers || [] + if (bodyMembers.length > 0) { + this.registerClassBodyMembers(bodyMembers) + } + return scope + } + + /** + * Pass 2 for an enum: generates its bytecode, assuming its own scope has + * already been restored and every other class's members are registered. + */ + private generateEnumClassFile(enumNode: any): ClassFile { + this.className = enumNode.typeIdentifier + // Generated enums genuinely extend java.lang.Enum: the constructor chains to + // Enum.(String, int) and name()/ordinal()/compareTo() are inherited. + this.parentClassName = 'java/lang/Enum' + const accessFlags = generateClassAccessFlags(enumNode.classModifier) | 0x4000 // ACC_ENUM + + const superClassIndex = this.constantPoolManager.indexClassInfo(this.parentClassName) + const thisClassIndex = this.constantPoolManager.indexClassInfo(this.className) + this.constantPoolManager.indexUtf8Info('Code') + + const enumBody = enumNode.enumBody + const enumConstants = enumBody.constants || [] + const bodyMembers = enumBody.bodyMembers || [] + + this.buildEnumConstantFields(enumConstants) + // Add synthetic $VALUES field (private static final) const valuesFieldDescriptor = '[L' + this.className + ';' this.fields.push({ @@ -188,19 +313,15 @@ export class Compiler { attributes: [] }) - // name(), ordinal(), compareTo() etc. are inherited from java.lang.Enum; - // register the ones reachable from Source programs so calls resolve. - this.registerInheritedEnumMethods() - if (bodyMembers.length === 0) { this.addEnumConstructor() } else { - this.handleClassBody(bodyMembers) + this.generateClassBody(bodyMembers) } // Add synthetic methods - this.addEnumValuesMethod(enumConstants) - this.addEnumValueOfMethod(enumConstants) + this.addEnumValuesMethod() + this.addEnumValueOfMethod() this.addEnumStaticInitializer(enumConstants) const constantPool = this.constantPoolManager.getPool() @@ -224,6 +345,37 @@ export class Compiler { } } + /** Builds the enum constants' field-table entries (pass 2; no symbol-table work - that's pass 1's job). */ + private buildEnumConstantFields(enumConstants: any[]) { + enumConstants.forEach((constant: any) => { + this.fields.push({ + accessFlags: 0x0019, // public static final + nameIndex: this.constantPoolManager.indexUtf8Info(constant.name), + descriptorIndex: this.constantPoolManager.indexUtf8Info('L' + this.className + ';'), + attributesCount: 0, + attributes: [] + }) + }) + } + + /** Registers values()/valueOf()'s signatures (pass 1); their bytecode is emitted in pass 2. */ + private registerEnumSyntheticMethodSignatures() { + this.symbolTable.insertMethodInfo({ + name: 'values', + accessFlags: 0x0009, // public static + parentClassName: this.className, + typeDescriptor: '()[L' + this.className + ';', + className: this.className + }) + this.symbolTable.insertMethodInfo({ + name: 'valueOf', + accessFlags: 0x0009, // public static + parentClassName: this.className, + typeDescriptor: '(Ljava/lang/String;)L' + this.className + ';', + className: this.className + }) + } + private addEnumConstructor() { // (String name, int ordinal) { super(name, ordinal); } const superInitRef = this.constantPoolManager.indexMethodrefInfo( @@ -293,8 +445,9 @@ export class Compiler { } } - private addEnumValuesMethod(enumConstants: any[]) { + private addEnumValuesMethod() { // public static EnumClass[] values() { return $VALUES.clone(); } + // (its signature is already registered in pass 1, by registerEnumSyntheticMethodSignatures) const nameIndex = this.constantPoolManager.indexUtf8Info('values') const descriptorIndex = this.constantPoolManager.indexUtf8Info('()[L' + this.className + ';') @@ -350,18 +503,11 @@ export class Compiler { 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[]) { + private addEnumValueOfMethod() { // public static EnumClass valueOf(String name) { return (EnumClass) Enum.valueOf(EnumClass.class, name); } + // (its signature is already registered in pass 1, by registerEnumSyntheticMethodSignatures) const nameIndex = this.constantPoolManager.indexUtf8Info('valueOf') const descriptorIndex = this.constantPoolManager.indexUtf8Info( '(Ljava/lang/String;)L' + this.className + ';' @@ -416,14 +562,6 @@ export class Compiler { 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[]) { @@ -486,7 +624,13 @@ export class Compiler { }) } - private handleClassBody(classBody: Array) { + /** + * Splits a class body into its member kinds, synthesising the default + * constructor if none was declared. Shared by both passes so they agree + * on exactly which members exist (in particular, the same synthetic + * default constructor) without either pass depending on the other. + */ + private partitionClassBody(classBody: Array) { const staticFields: Array = [] const nonStaticFields: Array = [] const staticMethods: Array = [] @@ -527,38 +671,72 @@ export class Compiler { }) } + return { staticFields, nonStaticFields, staticMethods, nonStaticMethods, constructors } + } + + /** Pass 1 for a class body: registers every member's signature, no bytecode. */ + private registerClassBodyMembers(classBody: Array) { + const { staticFields, nonStaticFields, staticMethods, nonStaticMethods, constructors } = + this.partitionClassBody(classBody) + constructors.forEach(c => this.recordConstructorInfo(c)) - staticFields.forEach(f => this.recordFieldInfo(f)) + staticFields.forEach(f => this.registerFieldSignature(f)) + nonStaticFields.forEach(f => this.registerFieldSignature(f)) staticMethods.forEach(m => this.recordMethodInfo(m)) - - nonStaticFields.forEach(f => this.recordFieldInfo(f)) nonStaticMethods.forEach(m => this.recordMethodInfo(m)) + } + + /** Pass 2 for a class body: generates every member's bytecode. */ + private generateClassBody(classBody: Array) { + const { staticFields, nonStaticFields, staticMethods, nonStaticMethods, constructors } = + this.partitionClassBody(classBody) + + staticFields.forEach(f => this.buildFieldEntry(f)) + nonStaticFields.forEach(f => this.buildFieldEntry(f)) nonStaticMethods.forEach(m => this.compileMethod(m)) staticMethods.forEach(m => this.compileMethod(m)) this.compileStaticFieldInitializers(staticFields) - constructors.forEach(c => this.compileConstructor(c)) + constructors.forEach(c => this.compileConstructor(c, nonStaticFields)) } /** - * Emits a `` that runs the initialiser expression of each static - * field, in declaration order (`static T f = expr;` -> `f = expr;`). + * Builds synthetic `f = expr;` assignment statements from each field's + * initialiser, in declaration order. Instance-field targets are qualified + * as `this.f` - the Assignment code generator only emits the ALOAD_0 + * needed before PUTFIELD when it sees that prefix; a bare name silently + * skips loading the receiver. */ - private compileStaticFieldInitializers(staticFields: Array) { + private buildFieldInitializerStatements( + fields: Array, + qualifyWithThis: boolean = false + ): any[] { const blockStatements: any[] = [] - for (const field of staticFields) { + for (const field of fields) { for (const declarator of field.variableDeclaratorList) { if (declarator.variableInitializer === undefined) continue + const name = qualifyWithThis + ? 'this.' + declarator.variableDeclaratorId + : declarator.variableDeclaratorId blockStatements.push({ kind: 'ExpressionStatement', stmtExp: { kind: 'Assignment', - left: { kind: 'ExpressionName', name: declarator.variableDeclaratorId }, + left: { kind: 'ExpressionName', name }, operator: '=', right: declarator.variableInitializer } }) } } + return blockStatements + } + + /** + * Emits a `` that runs the initialiser expression of each static + * field, in declaration order (`static T f = expr;` -> `f = expr;`). + */ + private compileStaticFieldInitializers(staticFields: Array) { + const blockStatements = this.buildFieldInitializerStatements(staticFields) if (blockStatements.length === 0) return this.compileMethod({ @@ -569,19 +747,13 @@ export class Compiler { } as unknown as MethodDeclaration) } - private recordFieldInfo(fieldNode: FieldDeclaration) { + /** Pass 1: registers a field's signature in the symbol table only. */ + private registerFieldSignature(fieldNode: FieldDeclaration) { const accessFlags = generateFieldAccessFlags(fieldNode.fieldModifier) const type = fieldNode.fieldType fieldNode.variableDeclaratorList.forEach(v => { const fullType = type + (v.dims ?? '') const typeDescriptor = this.symbolTable.generateFieldDescriptor(fullType) - this.fields.push({ - accessFlags: accessFlags, - nameIndex: this.constantPoolManager.indexUtf8Info(v.variableDeclaratorId), - descriptorIndex: this.constantPoolManager.indexUtf8Info(typeDescriptor), - attributesCount: 0, - attributes: [] - }) this.symbolTable.insertFieldInfo({ name: v.variableDeclaratorId, accessFlags: accessFlags, @@ -592,6 +764,23 @@ export class Compiler { }) } + /** Pass 2: builds a field's class-file field-table entry. */ + private buildFieldEntry(fieldNode: FieldDeclaration) { + const accessFlags = generateFieldAccessFlags(fieldNode.fieldModifier) + const type = fieldNode.fieldType + fieldNode.variableDeclaratorList.forEach(v => { + const fullType = type + (v.dims ?? '') + const typeDescriptor = this.symbolTable.generateFieldDescriptor(fullType) + this.fields.push({ + accessFlags: accessFlags, + nameIndex: this.constantPoolManager.indexUtf8Info(v.variableDeclaratorId), + descriptorIndex: this.constantPoolManager.indexUtf8Info(typeDescriptor), + attributesCount: 0, + attributes: [] + }) + }) + } + private recordMethodInfo(methodNode: MethodDeclaration) { const header = methodNode.methodHeader const methodName = header.identifier @@ -646,7 +835,14 @@ export class Compiler { }) } - private compileConstructor(constructor: ConstructorDeclaration) { + private compileConstructor( + constructor: ConstructorDeclaration, + instanceFields: Array = [] + ) { + // Instance field initialisers run at the start of every constructor body + // (right after the implicit super() call that generateCode() emits), + // mirroring how compileStaticFieldInitializers seeds . + const fieldInitializers = this.buildFieldInitializerStatements(instanceFields, true) const methodNode: MethodDeclaration = { kind: 'MethodDeclaration', methodModifier: constructor.constructorModifier, @@ -655,7 +851,10 @@ export class Compiler { formalParameterList: constructor.constructorDeclarator.formalParameterList, result: 'void' }, - methodBody: constructor.constructorBody + methodBody: { + kind: 'Block', + blockStatements: [...fieldInitializers, ...constructor.constructorBody.blockStatements] + } } this.compileMethod(methodNode) diff --git a/src/compiler/grammar.pegjs b/src/compiler/grammar.pegjs index 5b6647b..8d72acb 100755 --- a/src/compiler/grammar.pegjs +++ b/src/compiler/grammar.pegjs @@ -379,9 +379,9 @@ ReferenceType ClassType = head:(pn:(PackageName dot Annotation*)? id:TypeIdentifier TypeArguments? - { return (pn ?? "") + id; }) - tail:(dot Annotation* @TypeIdentifier TypeArguments?)* { - return head + tail.join(); + { return (pn ? pn + "." : "") + id; }) + tail:(dot Annotation* i:TypeIdentifier TypeArguments? { return "." + i; })* { + return head + tail.join(""); } Dims diff --git a/src/compiler/grammar.ts b/src/compiler/grammar.ts index 645f71a..a864b85 100755 --- a/src/compiler/grammar.ts +++ b/src/compiler/grammar.ts @@ -381,9 +381,9 @@ ReferenceType ClassType = head:(pn:(PackageName dot Annotation*)? id:TypeIdentifier TypeArguments? - { return (pn ?? "") + id; }) - tail:(dot Annotation* @TypeIdentifier TypeArguments?)* { - return head + tail.join(); + { return (pn ? pn + "." : "") + id; }) + tail:(dot Annotation* i:TypeIdentifier TypeArguments? { return "." + i; })* { + return head + tail.join(""); } Dims diff --git a/src/compiler/symbol-table.ts b/src/compiler/symbol-table.ts index d1442e9..c7b6c99 100644 --- a/src/compiler/symbol-table.ts +++ b/src/compiler/symbol-table.ts @@ -29,8 +29,8 @@ export const typeMap = new Map([ ['void', 'V'] ]) -type Symbol = string -type Table = Map +export type Symbol = string +export type Table = Map export type SymbolNode = { info: SymbolInfo @@ -107,6 +107,12 @@ export class SymbolTable { private curClassIdx: number private importedPackages: Array private importedClassMap: Map + // Class-name keys that were ambiguous (shared by two unrelated + // declarations - e.g. two different enclosing classes each with a nested + // class of the same simple name) and have been evicted. Tracked + // separately from the tables themselves so a later declaration can't + // silently reclaim the bare name once it's known to be ambiguous. + private poisonedClassKeys: Set constructor() { this.tables = [this.getNewTable()] @@ -114,6 +120,7 @@ export class SymbolTable { this.curIdx = 0 this.importedPackages = [] this.importedClassMap = new Map() + this.poisonedClassKeys = new Set() } private setup() { @@ -245,22 +252,59 @@ export class SymbolTable { this.curTable = this.tables[this.curIdx] } - insertClassInfo(info: ClassInfo) { - const key = generateSymbol(info.name, SymbolType.CLASS) - - if (this.curTable.has(key)) { - throw new SymbolRedeclarationError(info.name) - } - + // A nested class is registered under both its simple name (so unqualified + // source references like `Inner` resolve) and its qualified binary name + // (so a `Ljava/lang/Outer$Inner;`-style descriptor can be resolved back to + // its member table) - both keys share the same SymbolNode/children table. + // Returns the class's own member table (the `children` scope), so a caller + // compiling in two passes can register every class's signature up front + // (pass 1) and later re-enter each class's scope to generate bytecode + // (pass 2) via restoreClassScope, once every sibling is already visible. + insertClassInfo(info: ClassInfo, lookupNames: Array = [info.name]): Table { + const names = [...new Set(lookupNames)] const symbolNode: SymbolNode = { info: info, children: this.getNewTable() } - this.curTable.set(key, symbolNode) + + for (const name of names) { + const key = generateSymbol(name, SymbolType.CLASS) + const isPrimaryKey = name === info.name + if (this.curTable.has(key) || this.poisonedClassKeys.has(key)) { + if (isPrimaryKey) { + // The qualified binary name is built from a unique nesting path, + // so a collision here is a genuine redeclaration, not ambiguity. + throw new SymbolRedeclarationError(info.name) + } + // A secondary alias (e.g. a nested class's bare simple name) shared + // by two unrelated declarations is permanently ambiguous - evict it + // rather than letting whichever registered first silently win every + // future unqualified lookup. Each declaration stays fully reachable + // by its own unique qualified binary name; only the bare alias is + // lost, for both the earlier and any later registrant. + this.curTable.delete(key) + this.poisonedClassKeys.add(key) + continue + } + this.curTable.set(key, symbolNode) + } this.tables[++this.curIdx] = symbolNode.children this.curTable = this.tables[this.curIdx] this.curClassIdx = this.curIdx + return symbolNode.children + } + + // Re-enters a class's own member scope (previously returned by + // insertClassInfo) so its method bodies can be compiled in a later pass, + // after every class's signature has already been registered. This always + // appends a fresh slot rather than rewinding to the scope's original + // index, so classes registered afterwards (whose tables occupy higher + // indices) remain reachable via getClassTable's scan from the new curIdx. + restoreClassScope(classScope: Table): void { + this.tables[++this.curIdx] = classScope + this.curTable = classScope + this.curClassIdx = this.curIdx } insertFieldInfo(info: FieldInfo) { @@ -344,6 +388,17 @@ export class SymbolTable { return root.get(key)!.info as ClassInfo } + // A dotted name (e.g. "Outer.Inner") may be a source-qualified reference + // to a nested type, whose registered binary name joins segments with + // '$' instead of '.' - try that before falling through to import/package + // resolution (which treats dots as package separators instead). + if (name.includes('.')) { + key = generateSymbol(name.replace(/\./g, '$'), SymbolType.CLASS) + if (root.has(key)) { + return root.get(key)!.info as ClassInfo + } + } + if (this.importedClassMap.has(name)) { const fullName = this.importedClassMap.get(name)! key = generateSymbol(fullName, SymbolType.CLASS) @@ -373,6 +428,22 @@ export class SymbolTable { } } + // A dotted name (e.g. "Outer.Inner") may be a source-qualified reference + // to a nested type, whose registered binary name joins segments with + // '$' instead of '.' - try that before falling through to import/package + // resolution (which treats dots as package separators instead). This is + // also how a qualified-typed local variable/field resolves its own + // member table when dispatching a method/field access on it. + if (name.includes('.')) { + key = generateSymbol(name.replace(/\./g, '$'), SymbolType.CLASS) + for (let i = this.curIdx; i >= 0; i--) { + const table = this.tables[i] + if (table.has(key)) { + return table.get(key)!.children + } + } + } + const root = this.tables[0] if (this.importedClassMap.has(name)) { const fullName = this.importedClassMap.get(name)! diff --git a/src/types/checker/__tests__/nestedClasses.test.ts b/src/types/checker/__tests__/nestedClasses.test.ts new file mode 100644 index 0000000..e294c38 --- /dev/null +++ b/src/types/checker/__tests__/nestedClasses.test.ts @@ -0,0 +1,99 @@ +import { check } from '..' +import { parse } from '../../ast' +import { TypeCheckerError, UnsupportedNestedClassError } from '../../errors' +import { Type } from '../../types/type' + +const testcases: { + input: string + result: { type: Type | null; errors: Error[] } + only?: boolean +}[] = [ + { + input: ` + class Outer { + static class Inner { + int value; + Inner(int v) { value = v; } + int getValue() { return value; } + } + + public static void main(String[] args) { + Inner inner = new Inner(5); + inner.getValue(); + } + } + `, + result: { type: null, errors: [] } + }, + { + input: ` + class Outer { + static int counter = 1; + + static class Inner { + int read() { return counter; } + } + + public static void main(String[] args) { + Inner inner = new Inner(); + inner.read(); + } + } + `, + result: { type: null, errors: [] } + }, + { + input: ` + class Outer { + static class Middle { + static class Inner { + void hello() {} + } + } + + public static void main(String[] args) { + Inner inner = new Inner(); + inner.hello(); + } + } + `, + result: { type: null, errors: [] } + }, + { + input: ` + class Outer { + class Inner {} + + public static void main(String[] args) {} + } + `, + result: { type: null, errors: [new UnsupportedNestedClassError()] } + } +] + +describe('Type Checker', () => { + testcases.map(testcase => { + let it = test + if (testcase.only) it = test.only + it(`Checking nested classes for '${testcase.input}'`, () => { + const program = 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.') + const result = check(ast) + if (result.currentType === null) expect(result.currentType).toBe(testcase.result.type) + else expect(result.currentType).toBeInstanceOf(testcase.result.type) + if (testcase.result.errors.length > result.errors.length) { + testcase.result.errors.forEach((error, index) => { + if (!result.errors[index]) expect('').toBe(error.message) + expect(result.errors[index].message).toBe(error.message) + }) + } else { + result.errors.forEach((error, index) => { + if (!testcase.result.errors[index]) expect(error.message).toBe('') + expect(error.message).toBe(testcase.result.errors[index].message) + }) + } + }) + }) +}) diff --git a/src/types/checker/index.ts b/src/types/checker/index.ts index d154890..21cb5e6 100644 --- a/src/types/checker/index.ts +++ b/src/types/checker/index.ts @@ -554,6 +554,7 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R let numFieldDeclarations = 0 let numMethodDeclarations = 0 + let numNestedClassDeclarations = 0 for (let i = 0; i < node.classBody.classBodyDeclarations.length; i++) { const bodyDeclaration = node.classBody.classBodyDeclarations[i] @@ -561,7 +562,7 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R case 'ConstructorDeclaration': { const methodFrame = classFrame.newChildFrame() const constructor = classType.getConstructor( - i - numFieldDeclarations - numMethodDeclarations + i - numFieldDeclarations - numMethodDeclarations - numNestedClassDeclarations ) const constructorMethodErrors: TypeCheckerError[] = [] constructor.mapParameters((name, type, isVarargs) => { @@ -651,10 +652,16 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R if (checkErrors.length > 0) errors.push(...checkErrors) break } + case 'NormalClassDeclaration': { + const { errors: checkErrors } = typeCheckBody(bodyDeclaration, classFrame) + if (checkErrors.length > 0) errors.push(...checkErrors) + break + } } if (bodyDeclaration.kind === 'FieldDeclaration') numFieldDeclarations += 1 if (bodyDeclaration.kind === 'MethodDeclaration') numMethodDeclarations += 1 + if (bodyDeclaration.kind === 'NormalClassDeclaration') numNestedClassDeclarations += 1 } return newResult(null, errors) } diff --git a/src/types/checker/prechecks.ts b/src/types/checker/prechecks.ts index 49424cc..83d6e26 100644 --- a/src/types/checker/prechecks.ts +++ b/src/types/checker/prechecks.ts @@ -2,7 +2,12 @@ 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' -import { CyclicInheritanceError, DuplicateClassError, TypeCheckerError } from '../errors' +import { + CyclicInheritanceError, + DuplicateClassError, + TypeCheckerError, + UnsupportedNestedClassError +} from '../errors' import { Method } from '../types/methods' import { Frame } from './environment' import { newResult, OK_RESULT, Result } from '.' @@ -57,6 +62,24 @@ export const addClasses = (node: Node, frame: Frame): Result => { node.typeIdentifier.location ) if (error instanceof Error) return newResult(null, [new DuplicateClassError(node.location)]) + + // Register static nested class declarations found directly in this class's body. + for (const bodyDeclaration of node.classBody.classBodyDeclarations) { + if (bodyDeclaration.kind !== 'NormalClassDeclaration') continue + const isStatic = bodyDeclaration.classModifiers.some( + modifier => modifier.identifier === 'static' + ) + if (!isStatic) { + errors.push(new UnsupportedNestedClassError(bodyDeclaration.location)) + continue + } + const nestedResult = addClasses(bodyDeclaration, frame) + if (nestedResult.hasErrors) errors.push(...nestedResult.errors) + else if (nestedResult.currentType instanceof ClassType) + nestedResult.currentType.setEnclosingClass(classType) + } + if (errors.length > 0) return newResult(null, errors) + return newResult(classType) } case 'EnumDeclaration': { @@ -123,6 +146,15 @@ export const addClassMethods = (node: Node, frame: Frame): Result => { } const classType = createClassFieldsAndMethods(node, frame, createMethod, createMethod) if (classType instanceof TypeCheckerError) return newResult(null, [classType]) + + const errors: TypeCheckerError[] = [] + for (const bodyDeclaration of node.classBody.classBodyDeclarations) { + if (bodyDeclaration.kind !== 'NormalClassDeclaration') continue + const nestedResult = addClassMethods(bodyDeclaration, frame) + if (nestedResult.hasErrors) errors.push(...nestedResult.errors) + } + if (errors.length > 0) return newResult(null, errors) + return newResult(classType) } case 'EnumDeclaration': { @@ -241,6 +273,14 @@ export const addClassParents = (node: Node, frame: Frame): Result => { } } + const errors: TypeCheckerError[] = [] + for (const bodyDeclaration of node.classBody.classBodyDeclarations) { + if (bodyDeclaration.kind !== 'NormalClassDeclaration') continue + const nestedResult = addClassParents(bodyDeclaration, frame) + if (nestedResult.hasErrors) errors.push(...nestedResult.errors) + } + if (errors.length > 0) return newResult(null, errors) + return newResult(classType) } case 'EnumDeclaration': { diff --git a/src/types/errors.ts b/src/types/errors.ts index da235cd..beeefff 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -183,3 +183,9 @@ export class UnhandledExceptionError extends TypeCheckerError { super('unhandled exception', location) } } + +export class UnsupportedNestedClassError extends TypeCheckerError { + constructor(location?: Location) { + super('only static nested classes are supported', location) + } +} diff --git a/src/types/types/classes.ts b/src/types/types/classes.ts index 8334f2e..7bc237f 100644 --- a/src/types/types/classes.ts +++ b/src/types/types/classes.ts @@ -26,6 +26,7 @@ export class ClassType extends ClassOrInterfaceType implements Class { public readonly name: string private _modifiers = new Modifiers() private _parent: Class = new ObjectClass() + private _enclosingClass: Class | null = null private _constructors: Method[] = [] private _fields = new Map() @@ -39,13 +40,27 @@ export class ClassType extends ClassOrInterfaceType implements Class { public accessField(_name: string, location: Location): Type | TypeCheckerError { const field = this._fields.get(_name) if (field) return field - return this._parent.accessField(_name, location) + const parentResult = this._parent.accessField(_name, location) + if (!(parentResult instanceof TypeCheckerError)) return parentResult + if (this._enclosingClass) return this._enclosingClass.accessField(_name, location) + return parentResult } public accessMethod(name: string, location: Location): Method[] | TypeCheckerError { const method = this._methods.get(name) if (method) return method - return this._parent.accessMethod(name, location) + const parentResult = this._parent.accessMethod(name, location) + if (!(parentResult instanceof TypeCheckerError)) return parentResult + if (this._enclosingClass) return this._enclosingClass.accessMethod(name, location) + return parentResult + } + + public getEnclosingClass(): Class | null { + return this._enclosingClass + } + + public setEnclosingClass(enclosingClass: Class): void { + this._enclosingClass = enclosingClass } public addConstructor(method: Method, location: Location): void | TypeCheckerError {