Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Open-source Implementation of the Java language in TypeScript. (<https://docs.or
- [Table of Contents](#table-of-contents)
- [Prerequisites](#prerequisites)
- [Usage](#usage)
- [Current Features](#current-features)
- [Future Features](#future-features)
- [Testing](#testing)
- [Using your java-slang in Source Academy](#using-your-java-slang-in-source-academy)
- [Using your java-slang in your local Source Academy](#using-your-java-slang-in-your-local-source-academy)
Expand Down Expand Up @@ -39,6 +41,26 @@ $ git submodule update --init --recursive
$ git submodule update --recursive --remote
```

## Current Features

The Java language in Source Academy currently supports a host of available features, including:

- Primitive classes: boolean, byte, short, int, long, char, float and double.
- Custom (non-primitive) classes
- Method overloading
- 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)
- Generics
- Multi-file programs

## Testing

Expand Down
7 changes: 7 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -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" },
]);
9 changes: 8 additions & 1 deletion src/ast/types/blocks-and-statements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
25 changes: 16 additions & 9 deletions src/compiler/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
```
4 changes: 3 additions & 1 deletion src/compiler/__tests__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +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();
unaryExpressionTest();
ifElseTest();
whileTest();
forTest();
unaryExpressionTest();
methodInvocationTest();
importTest();
arrayTest();
classTest();
typeConversionTest();
})
27 changes: 27 additions & 0 deletions src/compiler/__tests__/tests/typeConversion.test.ts
Original file line number Diff line number Diff line change
@@ -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) {
float f = 1.0f;
int x = (int) f;
System.out.println(x);
}
}
`,
expectedLines: ["1"],
}
];

export const typeConversionTest = () => describe("type conversion", () => {
for (let testCase of testCases) {
const { comment: comment, program: program, expectedLines: expectedLines } = testCase;
Comment thread
kjw142857 marked this conversation as resolved.
it(comment, () => runTest(program, expectedLines));
}
});
76 changes: 75 additions & 1 deletion src/compiler/code-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -516,6 +517,79 @@ 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)
break
case 'J':
cg.code.push(OPCODE.L2D)
break
case 'I':
cg.code.push(OPCODE.I2D)
break
default:
break
}
Comment thread
kjw142857 marked this conversation as resolved.
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' }
case 'long':
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' }
}
}
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) => {
const { identifier: id, argumentList: argLst } = node as ClassInstanceCreationExpression
let maxStack = 2
Expand Down
18 changes: 16 additions & 2 deletions src/compiler/grammar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion src/types/ast/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IToken } from 'java-parser'
import {
ArrayType,
ClassOrInterfaceType,
Dim,
Identifier,
Expand Down Expand Up @@ -67,9 +68,11 @@ export const isIdentifier = (object: Record<string, any>): 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':
Expand Down
28 changes: 28 additions & 0 deletions src/types/checker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,34 @@ export const typeCheckBody = (node: Node, frame: Frame = Frame.globalFrame()): R
case 'BreakStatement': {
return OK_RESULT
}
case 'CastExpression': {
let castTypeNode, expressionNode;
if ('primitiveType' in node) {
castTypeNode = node.primitiveType;
expressionNode = node.unaryExpression;
} else if ('referenceType' in node && 'unaryExpressionNotPlusMinus' in node) {
castTypeNode = node.referenceType;
expressionNode = node.unaryExpressionNotPlusMinus;
} else if ('referenceType' in node && 'lambdaExpression' in node) {
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)]);
}

return newResult(castType);
}
Comment thread
kjw142857 marked this conversation as resolved.
case 'ClassInstanceCreationExpression': {
const classIdentifier =
node.unqualifiedClassInstanceCreationExpression.classOrInterfaceTypeToInstantiate
Expand Down
Loading