From ac313061bd46d87a0a2e5a9c000af4e2ac96bf22 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 11 Mar 2026 11:17:11 +0800 Subject: [PATCH 1/5] Include current and planned features in README --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 4ec8997..a7d5229 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Open-source Implementation of the Java language in TypeScript. ( Date: Wed, 18 Mar 2026 07:37:06 +0800 Subject: [PATCH 2/5] Update compiler README --- eslint.config.mjs | 7 +++++ src/compiler/__tests__/index.ts | 15 +++-------- .../__tests__/tests/typeConversion.test.ts | 27 +++++++++++++++++++ 3 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 eslint.config.mjs create mode 100644 src/compiler/__tests__/tests/typeConversion.test.ts diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..17f865b --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,7 @@ +import markdown from "@eslint/markdown"; +import { defineConfig } from "eslint/config"; + +export default defineConfig([ + { ignores: ["**/*.js", "**/*.cjs", "**/*.mjs"] }, + { files: ["**/*.md"], plugins: { markdown }, language: "markdown/gfm" }, +]); diff --git a/src/compiler/__tests__/index.ts b/src/compiler/__tests__/index.ts index 8f31ef5..a4cec45 100644 --- a/src/compiler/__tests__/index.ts +++ b/src/compiler/__tests__/index.ts @@ -1,3 +1,4 @@ +/* import { printlnTest } from "./tests/println.test"; import { variableDeclarationTest } from "./tests/variableDeclaration.test"; import { arithmeticExpressionTest } from "./tests/arithmeticExpression.test"; @@ -9,17 +10,9 @@ 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 { typeConversionTest } from "./tests/typeConversion.test"; describe("compiler tests", () => { - printlnTest(); - variableDeclarationTest(); - arithmeticExpressionTest(); - unaryExpressionTest(); - ifElseTest(); - whileTest(); - forTest(); - methodInvocationTest(); - importTest(); - arrayTest(); - classTest(); + typeConversionTest(); }) \ No newline at end of file diff --git a/src/compiler/__tests__/tests/typeConversion.test.ts b/src/compiler/__tests__/tests/typeConversion.test.ts new file mode 100644 index 0000000..d69c7a0 --- /dev/null +++ b/src/compiler/__tests__/tests/typeConversion.test.ts @@ -0,0 +1,27 @@ +import { + runTest, + testCase, +} from "../__utils__/test-utils"; + +const testCases: testCase[] = [ + { + comment: "int to float widening type", + program: ` + public class Main { + public static void main(String[] args) { + int x = 1; + float y = x; + System.out.println(y); + } + } + `, + expectedLines: ["1.0"], + } +]; + +export const typeConversionTest = () => describe("type conversion", () => { + for (let testCase of testCases) { + const { comment: comment, program: program, expectedLines: expectedLines } = testCase; + it(comment, () => runTest(program, expectedLines)); + } +}); From 9484d200ce91fe7f9023ff05fd53e2e80368cc68 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Thu, 26 Mar 2026 03:35:07 +0800 Subject: [PATCH 3/5] implement type casting --- src/ast/types/blocks-and-statements.ts | 9 ++++- src/compiler/README.md | 25 +++++++----- src/compiler/code-generator.ts | 56 +++++++++++++++++++++++++- src/compiler/grammar.ts | 18 ++++++++- src/types/ast/utils.ts | 5 ++- src/types/checker/index.ts | 31 ++++++++++++++ 6 files changed, 130 insertions(+), 14 deletions(-) diff --git a/src/ast/types/blocks-and-statements.ts b/src/ast/types/blocks-and-statements.ts index fe5dc7a..2e501fe 100644 --- a/src/ast/types/blocks-and-statements.ts +++ b/src/ast/types/blocks-and-statements.ts @@ -259,7 +259,14 @@ export interface Assignment extends BaseNode { } export type LeftHandSide = ExpressionName | ArrayAccess; -export type UnaryExpression = PrefixExpression | PostfixExpression; +export type UnaryExpression = PrefixExpression | PostfixExpression | CastExpression; + +export interface CastExpression extends BaseNode { + kind: "CastExpression"; + castType: Identifier; + expression: Expression; + isPrimitiveCast: Boolean; +} export interface PrefixExpression extends BaseNode { kind: "PrefixExpression"; diff --git a/src/compiler/README.md b/src/compiler/README.md index a184cd4..c63de65 100644 --- a/src/compiler/README.md +++ b/src/compiler/README.md @@ -1,5 +1,7 @@ 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 @@ -12,21 +14,26 @@ This is a bookkeeping of the planned scope of the compiler. It will be updated f - Method invocation - Non-static import statements - Single dimension array declaration/initialization - -**Features that are planned to support** - -- Class fields (with `public static` access flag) - Primitive type variables +- Class fields (with `public static` access flag) - Object instantiation (with `new` keyword) +- Instance fields/methods +- Class inheritance +- Method overloading/overriding +- Type casting + -**Features that will not be supported** +**Features that can possibly be supported in the future** - Annotations - Multiple files, modules, packages -- Instance fields/methods - Interfaces -- Class inheritance -- Method overloading/overriding - Generics -- Type casting +- Exceptions + +**Testing** +Unit tests are located in the "__tests__/tests" folder. The main testing file is "__tests__/index.ts", in which the tests to be run can be specified. To run, navigate to the main java-slang folder and run: +```bash +$ yarn test src/compiler/__tests__/index.ts +``` \ No newline at end of file diff --git a/src/compiler/code-generator.ts b/src/compiler/code-generator.ts index 1b0d8c8..73ae83e 100644 --- a/src/compiler/code-generator.ts +++ b/src/compiler/code-generator.ts @@ -24,7 +24,8 @@ import { ClassInstanceCreationExpression, ExpressionStatement, TernaryExpression, - LeftHandSide + LeftHandSide, + CastExpression } from '../ast/types/blocks-and-statements' import { MethodDeclaration, UnannType } from '../ast/types/classes' import { ConstantPoolManager } from './constant-pool-manager' @@ -516,6 +517,59 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi return f(node, cg.labels[cg.labels.length - 1], false) }, + CastExpression: (node: Node, cg: CodeGenerator) => { + const { castType: ct, expression: expr, isPrimitiveCast: b } = node as CastExpression + if (b) { + const res = compile(expr, cg) + const { stackSize: size, resultType: rt } = res + switch (ct) { + case 'double': + switch (rt) { + case 'F': + cg.code.push(OPCODE.F2D) + case 'J': + cg.code.push(OPCODE.L2D) + case 'I': + cg.code.push(OPCODE.I2D) + default: + } + return { stackSize: Math.max(size, 2), resultType: 'D' } + case 'float': + switch(rt) { + case 'D': + cg.code.push(OPCODE.D2F) + case 'J': + cg.code.push(OPCODE.L2F) + case 'I': + cg.code.push(OPCODE.I2F) + default: + } + return { stackSize: Math.max(size, 1), resultType: 'F' } + case 'long': + switch(rt) { + case 'D': + cg.code.push(OPCODE.D2L) + case 'F': + cg.code.push(OPCODE.F2L) + case 'I': + cg.code.push(OPCODE.I2L) + } + return { stackSize: Math.max(size, 2), resultType: 'L' } + case 'int': + switch (rt) { + case 'D': + cg.code.push(OPCODE.D2I) + case 'F': + cg.code.push(OPCODE.F2I) + case 'J': + cg.code.push(OPCODE.L2I) + } + return { stackSize: Math.max(size, 1), resultType: 'I' } + } + } + return compile(expr, cg) + }, + ClassInstanceCreationExpression: (node: Node, cg: CodeGenerator) => { const { identifier: id, argumentList: argLst } = node as ClassInstanceCreationExpression let maxStack = 2 diff --git a/src/compiler/grammar.ts b/src/compiler/grammar.ts index 4b6f1ee..013d564 100755 --- a/src/compiler/grammar.ts +++ b/src/compiler/grammar.ts @@ -1109,8 +1109,22 @@ PostfixExpression } CastExpression - = lparen PrimitiveType rparen UnaryExpression - / lparen ReferenceType rparen (LambdaExpression / !(PlusMinus) UnaryExpression) + = lparen t:PrimitiveType rparen expr:UnaryExpression { + return addLocInfo({ + kind: "CastExpression", + castType: t, + expression: expr, + isPrimitiveCast: true, + }); + } + / lparen t:ReferenceType rparen expr:(LambdaExpression / !(PlusMinus) UnaryExpression) { + return addLocInfo({ + kind: "CastExpression", + castType: t, + expression: expr, + isPrimitiveCast: false, + }); + } SwitchExpression = SwitchStatement diff --git a/src/types/ast/utils.ts b/src/types/ast/utils.ts index 1a703b7..f00fa7f 100644 --- a/src/types/ast/utils.ts +++ b/src/types/ast/utils.ts @@ -1,5 +1,6 @@ import { IToken } from 'java-parser' import { + ArrayType, ClassOrInterfaceType, Dim, Identifier, @@ -67,9 +68,11 @@ export const isIdentifier = (object: Record): boolean => { * @deprecated */ export const unannTypeToString = ( - type: LocalVariableType | ClassOrInterfaceType | Result + type: LocalVariableType | ClassOrInterfaceType | ArrayType | Result ): string => { switch (type.kind) { + case 'ArrayType': + return unannTypeToString(type.type) + '[]'.repeat(type.dims.dims.length) case 'Boolean': return 'boolean' case 'FloatingPointType': diff --git a/src/types/checker/index.ts b/src/types/checker/index.ts index 005286e..e4c8eeb 100644 --- a/src/types/checker/index.ts +++ b/src/types/checker/index.ts @@ -192,6 +192,37 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R case 'BreakStatement': { return OK_RESULT } + case 'CastExpression': { + if ('primitiveType' in node) { + let castType = frame.getType(unannTypeToString(node.primitiveType), node.primitiveType.location) + if (castType instanceof TypeCheckerError) return newResult(null, [castType]) + const { currentType, errors } = typeCheckBody(node.unaryExpression, frame) + if (errors.length > 0) return newResult(null, errors) + if (!currentType) throw new Error('Target of cast expression should return a type.') + if (!castType.canBeAssigned(currentType) && !currentType.canBeAssigned(castType)) + return newResult(null, [new IncompatibleTypesError(node.location)]) + return newResult(castType) + } else if ('referenceType' in node && 'unaryExpressionNotPlusMinus' in node) { + let castType = frame.getType(unannTypeToString(node.referenceType), node.referenceType.location) + if (castType instanceof TypeCheckerError) return newResult(null, [castType]) + const { currentType, errors } = typeCheckBody(node.unaryExpressionNotPlusMinus, frame) + if (errors.length > 0) return newResult(null, errors) + if (!currentType) throw new Error('Target of cast expression should return a type.') + if (!castType.canBeAssigned(currentType) && !currentType.canBeAssigned(castType)) + return newResult(null, [new IncompatibleTypesError(node.location)]) + return newResult(castType) + } else if ('referenceType' in node && 'lambdaExpression' in node) { + let castType = frame.getType(unannTypeToString(node.referenceType), node.referenceType.location) + if (castType instanceof TypeCheckerError) return newResult(null, [castType]) + const { currentType, errors } = typeCheckBody(node.lambdaExpression, frame) + if (errors.length > 0) return newResult(null, errors) + if (!currentType) throw new Error('Target of cast expression should return a type.') + if (!castType.canBeAssigned(currentType) && !currentType.canBeAssigned(castType)) + return newResult(null, [new IncompatibleTypesError(node.location)]) + return newResult(castType) + } + throw new Error('Invalid typecast.') + } case 'ClassInstanceCreationExpression': { const classIdentifier = node.unqualifiedClassInstanceCreationExpression.classOrInterfaceTypeToInstantiate From c5fa5f22148ec81a2d25a6047144ce77a45fe668 Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 1 Apr 2026 06:35:36 +0800 Subject: [PATCH 4/5] bug fixes and code improvement --- README.md | 4 +-- src/ast/types/blocks-and-statements.ts | 2 +- src/compiler/code-generator.ts | 22 +++++++++++- src/types/checker/index.ts | 47 ++++++++++++-------------- 4 files changed, 46 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index a7d5229..2f498ce 100644 --- a/README.md +++ b/README.md @@ -51,14 +51,14 @@ The Java language in Source Academy currently supports a host of available featu - Implicit type widening for both primitive and non-primitive types (e.g. int to long) - Basic exception/error messages - Basic system calls e.g. System.out.println +- Explicit type conversion (type narrowing) +- Implicit type conversion for system calls (e.g. int input to System.out.println) - Single nested class ## Future Features - Multiple class declarations in the same file - Inheritance (basis for implementation exists but requires multiclass declaration to function) -- Explicit type conversion (type narrowing) -- Implicit type conversion for system calls (e.g. int input to System.out.println) - Generics - Multi-file programs diff --git a/src/ast/types/blocks-and-statements.ts b/src/ast/types/blocks-and-statements.ts index 2e501fe..b5f9e9c 100644 --- a/src/ast/types/blocks-and-statements.ts +++ b/src/ast/types/blocks-and-statements.ts @@ -265,7 +265,7 @@ export interface CastExpression extends BaseNode { kind: "CastExpression"; castType: Identifier; expression: Expression; - isPrimitiveCast: Boolean; + isPrimitiveCast: boolean; } export interface PrefixExpression extends BaseNode { diff --git a/src/compiler/code-generator.ts b/src/compiler/code-generator.ts index 73ae83e..63eb288 100644 --- a/src/compiler/code-generator.ts +++ b/src/compiler/code-generator.ts @@ -527,21 +527,28 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi switch (rt) { case 'F': cg.code.push(OPCODE.F2D) + break case 'J': cg.code.push(OPCODE.L2D) + break case 'I': cg.code.push(OPCODE.I2D) + break default: + break } return { stackSize: Math.max(size, 2), resultType: 'D' } case 'float': switch(rt) { case 'D': cg.code.push(OPCODE.D2F) + break case 'J': cg.code.push(OPCODE.L2F) + break case 'I': cg.code.push(OPCODE.I2F) + break default: } return { stackSize: Math.max(size, 1), resultType: 'F' } @@ -549,25 +556,38 @@ const codeGenerators: { [type: string]: (node: Node, cg: CodeGenerator) => Compi switch(rt) { case 'D': cg.code.push(OPCODE.D2L) + break case 'F': cg.code.push(OPCODE.F2L) + break case 'I': cg.code.push(OPCODE.I2L) + break + default: + break } return { stackSize: Math.max(size, 2), resultType: 'L' } case 'int': switch (rt) { case 'D': cg.code.push(OPCODE.D2I) + break case 'F': cg.code.push(OPCODE.F2I) + break case 'J': cg.code.push(OPCODE.L2I) + break + default: + break } return { stackSize: Math.max(size, 1), resultType: 'I' } } } - return compile(expr, cg) + const res = compile(expr, cg); + const classInfoIndex = cg.constantPoolManager.indexClassInfo(ct as string); + cg.code.push(OPCODE.CHECKCAST, 0, classInfoIndex); + return res; }, ClassInstanceCreationExpression: (node: Node, cg: CodeGenerator) => { diff --git a/src/types/checker/index.ts b/src/types/checker/index.ts index e4c8eeb..43c6ffc 100644 --- a/src/types/checker/index.ts +++ b/src/types/checker/index.ts @@ -193,35 +193,32 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R return OK_RESULT } case 'CastExpression': { + let castTypeNode, expressionNode; if ('primitiveType' in node) { - let castType = frame.getType(unannTypeToString(node.primitiveType), node.primitiveType.location) - if (castType instanceof TypeCheckerError) return newResult(null, [castType]) - const { currentType, errors } = typeCheckBody(node.unaryExpression, frame) - if (errors.length > 0) return newResult(null, errors) - if (!currentType) throw new Error('Target of cast expression should return a type.') - if (!castType.canBeAssigned(currentType) && !currentType.canBeAssigned(castType)) - return newResult(null, [new IncompatibleTypesError(node.location)]) - return newResult(castType) + castTypeNode = node.primitiveType; + expressionNode = node.unaryExpression; } else if ('referenceType' in node && 'unaryExpressionNotPlusMinus' in node) { - let castType = frame.getType(unannTypeToString(node.referenceType), node.referenceType.location) - if (castType instanceof TypeCheckerError) return newResult(null, [castType]) - const { currentType, errors } = typeCheckBody(node.unaryExpressionNotPlusMinus, frame) - if (errors.length > 0) return newResult(null, errors) - if (!currentType) throw new Error('Target of cast expression should return a type.') - if (!castType.canBeAssigned(currentType) && !currentType.canBeAssigned(castType)) - return newResult(null, [new IncompatibleTypesError(node.location)]) - return newResult(castType) + castTypeNode = node.referenceType; + expressionNode = node.unaryExpressionNotPlusMinus; } else if ('referenceType' in node && 'lambdaExpression' in node) { - let castType = frame.getType(unannTypeToString(node.referenceType), node.referenceType.location) - if (castType instanceof TypeCheckerError) return newResult(null, [castType]) - const { currentType, errors } = typeCheckBody(node.lambdaExpression, frame) - if (errors.length > 0) return newResult(null, errors) - if (!currentType) throw new Error('Target of cast expression should return a type.') - if (!castType.canBeAssigned(currentType) && !currentType.canBeAssigned(castType)) - return newResult(null, [new IncompatibleTypesError(node.location)]) - return newResult(castType) + castTypeNode = node.referenceType; + expressionNode = node.lambdaExpression; + } else { + throw new Error('Invalid typecast.'); + } + + const castType = frame.getType(unannTypeToString(castTypeNode), castTypeNode.location); + if (castType instanceof TypeCheckerError) return newResult(null, [castType]); + + const { currentType, errors } = typeCheckBody(expressionNode, frame); + if (errors.length > 0) return newResult(null, errors); + if (!currentType) throw new Error('Target of cast expression should return a type.'); + + if (!castType.canBeAssigned(currentType) && !currentType.canBeAssigned(castType)) { + return newResult(null, [new IncompatibleTypesError(node.location)]); } - throw new Error('Invalid typecast.') + + return newResult(castType); } case 'ClassInstanceCreationExpression': { const classIdentifier = From e14a512e21512bd441f302199888a08c75beabcc Mon Sep 17 00:00:00 2001 From: kjw142857 Date: Wed, 1 Apr 2026 07:32:53 +0800 Subject: [PATCH 5/5] update test suite --- src/compiler/__tests__/index.ts | 13 +++++++++++-- src/compiler/__tests__/tests/typeConversion.test.ts | 8 ++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/compiler/__tests__/index.ts b/src/compiler/__tests__/index.ts index a4cec45..c6de94a 100644 --- a/src/compiler/__tests__/index.ts +++ b/src/compiler/__tests__/index.ts @@ -1,4 +1,3 @@ -/* import { printlnTest } from "./tests/println.test"; import { variableDeclarationTest } from "./tests/variableDeclaration.test"; import { arithmeticExpressionTest } from "./tests/arithmeticExpression.test"; @@ -10,9 +9,19 @@ 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 { typeConversionTest } from "./tests/typeConversion.test"; describe("compiler tests", () => { + printlnTest(); + variableDeclarationTest(); + arithmeticExpressionTest(); + ifElseTest(); + whileTest(); + forTest(); + unaryExpressionTest(); + methodInvocationTest(); + importTest(); + arrayTest(); + classTest(); typeConversionTest(); }) \ No newline at end of file diff --git a/src/compiler/__tests__/tests/typeConversion.test.ts b/src/compiler/__tests__/tests/typeConversion.test.ts index d69c7a0..de3ae73 100644 --- a/src/compiler/__tests__/tests/typeConversion.test.ts +++ b/src/compiler/__tests__/tests/typeConversion.test.ts @@ -9,13 +9,13 @@ const testCases: testCase[] = [ program: ` public class Main { public static void main(String[] args) { - int x = 1; - float y = x; - System.out.println(y); + float f = 1.0f; + int x = (int) f; + System.out.println(x); } } `, - expectedLines: ["1.0"], + expectedLines: ["1"], } ];