Skip to content

Commit 9fee148

Browse files
Chang-CHmartin-henzRichDom2185
authored
Java slang (#2895)
* add java-slang support * merge master * remove lib import * fix library import path * remove fixme comment * use java-slang package * use js-slang enums * revert yarn lock * use cdn * clean up code structure * read input as json * fix editor suggestions * refactor async load * move url to constants * fix typo * fix buffer, JSON error handling * update js-slang * specify module name --------- Co-authored-by: Martin Henz <henz@comp.nus.edu.sg> Co-authored-by: Richard Dominick <34370238+RichDom2185@users.noreply.github.com>
1 parent 3098e60 commit 9fee148

File tree

9 files changed

+268
-4
lines changed

9 files changed

+268
-4
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"flexboxgrid": "^6.3.1",
5151
"flexboxgrid-helpers": "^1.1.3",
5252
"hastscript": "^9.0.0",
53+
"java-slang": "^1.0.1",
5354
"js-slang": "^1.0.62",
5455
"js-yaml": "^4.1.0",
5556
"konva": "^9.2.0",

src/commons/application/ApplicationTypes.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,15 @@ export enum SupportedLanguage {
122122
JAVASCRIPT = 'JavaScript',
123123
SCHEME = 'Scheme',
124124
PYTHON = 'Python',
125+
JAVA = 'Java',
125126
C = 'C'
126127
}
127128

128129
export const SUPPORTED_LANGUAGES = [
129130
SupportedLanguage.JAVASCRIPT,
130131
SupportedLanguage.SCHEME,
131132
SupportedLanguage.PYTHON,
133+
SupportedLanguage.JAVA,
132134
SupportedLanguage.C
133135
];
134136

@@ -211,6 +213,15 @@ export const pyLanguages: SALanguage[] = pySubLanguages.map(sublang => {
211213
return { ...sublang, mainLanguage: SupportedLanguage.PYTHON, supports: { repl: true } };
212214
});
213215

216+
export const javaLanguages: SALanguage[] = [
217+
{
218+
chapter: Chapter.FULL_JAVA,
219+
variant: Variant.DEFAULT,
220+
displayName: 'Java',
221+
mainLanguage: SupportedLanguage.JAVA,
222+
supports: {}
223+
}
224+
];
214225
export const cLanguages: SALanguage[] = [
215226
{
216227
chapter: Chapter.FULL_C,
@@ -290,6 +301,7 @@ export const ALL_LANGUAGES: readonly SALanguage[] = [
290301
htmlLanguage,
291302
...schemeLanguages,
292303
...pyLanguages,
304+
...javaLanguages,
293305
...cLanguages
294306
];
295307
// TODO: Remove this function once logic has been fully migrated

src/commons/editor/Editor.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -435,8 +435,8 @@ const EditorBase = React.memo((props: EditorProps & LocalStateProps) => {
435435
session.on('changeAnnotation' as any, makeHandleAnnotationChange(session));
436436

437437
// Start autocompletion
438-
if (props.sourceChapter === Chapter.FULL_C) {
439-
// for C language, use the default autocomplete provided by ace editor
438+
if (props.sourceChapter === Chapter.FULL_C || props.sourceChapter === Chapter.FULL_JAVA) {
439+
// for C, Java language, use the default autocomplete provided by ace editor
440440
const { textCompleter, keyWordCompleter, setCompleters } = acequire('ace/ext/language_tools');
441441
setCompleters([textCompleter, keyWordCompleter]);
442442
} else {

src/commons/navigationBar/subcomponents/NavigationBarLangSelectButton.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useDispatch } from 'react-redux';
44
import {
55
cLanguages,
66
getLanguageConfig,
7+
javaLanguages,
78
pyLanguages,
89
SALanguage,
910
schemeLanguages,
@@ -23,6 +24,7 @@ const defaultSublanguages: {
2324
[SupportedLanguage.JAVASCRIPT]: sourceLanguages[0],
2425
[SupportedLanguage.PYTHON]: pyLanguages[0],
2526
[SupportedLanguage.SCHEME]: schemeLanguages[0],
27+
[SupportedLanguage.JAVA]: javaLanguages[0],
2628
[SupportedLanguage.C]: cLanguages[0]
2729
};
2830

src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { SagaIterator } from 'redux-saga';
99
import { call, put, race, select, take } from 'redux-saga/effects';
1010
import * as Sourceror from 'sourceror';
1111
import { makeCCompilerConfig, specialCReturnObject } from 'src/commons/utils/CToWasmHelper';
12+
import { javaRun } from 'src/commons/utils/JavaHelper';
1213
import { notifyStoriesEvaluated } from 'src/features/stories/StoriesActions';
1314
import { EVAL_STORY } from 'src/features/stories/StoriesTypes';
1415

@@ -244,6 +245,7 @@ export function* evalCode(
244245
const isLazy: boolean = context.variant === Variant.LAZY;
245246
const isWasm: boolean = context.variant === Variant.WASM;
246247
const isC: boolean = context.chapter === Chapter.FULL_C;
248+
const isJava: boolean = context.chapter === Chapter.FULL_JAVA;
247249

248250
let lastDebuggerResult = yield select(
249251
(state: OverallState) => state.workspaces[workspaceLocation].lastDebuggerResult
@@ -263,6 +265,8 @@ export function* evalCode(
263265
? call_variant(context.variant)
264266
: isC
265267
? call(cCompileAndRun, entrypointCode, context)
268+
: isJava
269+
? call(javaRun, entrypointCode, context)
266270
: call(
267271
runFilesInContext,
268272
isFolderModeEnabled

src/commons/utils/AceHelper.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export const getModeString = (chapter: Chapter, variant: Variant, library: strin
5050
case Chapter.SCHEME_4:
5151
case Chapter.FULL_SCHEME:
5252
return 'scheme';
53+
case Chapter.FULL_JAVA:
54+
return 'java';
5355
case Chapter.FULL_C:
5456
return 'c_cpp';
5557
default:

src/commons/utils/Constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const githubClientId = process.env.REACT_APP_GITHUB_CLIENT_ID || '';
3939
const githubOAuthProxyUrl = process.env.REACT_APP_GITHUB_OAUTH_PROXY_URL || '';
4040
const sicpBackendUrl =
4141
process.env.REACT_APP_SICPJS_BACKEND_URL || 'https://sicp.sourceacademy.org/';
42+
const javaPackagesUrl = 'https://source-academy.github.io/modules/java/java-packages/src/';
4243
const workspaceSettingsLocalStorageKey = 'workspace-settings';
4344

4445
// For achievements feature (CA - Continual Assessment)
@@ -136,6 +137,7 @@ const Constants = {
136137
sharedbBackendUrl,
137138
cadetLoggerInterval,
138139
sicpBackendUrl,
140+
javaPackagesUrl,
139141
workspaceSettingsLocalStorageKey,
140142
caFulfillmentLevel
141143
};

src/commons/utils/JavaHelper.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import setupJVM, { parseBin } from 'java-slang/dist/jvm';
2+
import { createModuleProxy, loadCachedFiles } from 'java-slang/dist/jvm/utils/integration';
3+
import { Context } from 'js-slang';
4+
import loadSourceModules from 'js-slang/dist/modules/loader';
5+
6+
import Constants from './Constants';
7+
import DisplayBufferService from './DisplayBufferService';
8+
9+
export async function javaRun(javaCode: string, context: Context) {
10+
let compiled = {};
11+
12+
let files = {};
13+
let buffer: string[] = [];
14+
15+
const readClassFiles = (path: string) => {
16+
let item = files[path as keyof typeof files];
17+
18+
// not found: attempt to fetch from CDN
19+
if (!item && path) {
20+
const splits = path.split('/');
21+
splits.pop(); // classname.class
22+
const pkg = splits.join('_');
23+
24+
const request = new XMLHttpRequest();
25+
request.open('GET', `${Constants.javaPackagesUrl}${pkg}.json`, false);
26+
request.send(null);
27+
if (request.status !== 200) {
28+
throw new Error('File not found: ' + path);
29+
}
30+
const json = JSON.parse(request.responseText);
31+
// we might want to cache the files in IndexedDB here
32+
files = { ...files, ...json };
33+
34+
if (!files[path as keyof typeof files]) {
35+
throw new Error('File not found: ' + path);
36+
}
37+
38+
item = files[path as keyof typeof files];
39+
}
40+
41+
// convert base64 to classfile object
42+
const binaryString = atob(item);
43+
const bytes = new Uint8Array(binaryString.length);
44+
for (let i = 0; i < binaryString.length; i++) {
45+
bytes[i] = binaryString.charCodeAt(i);
46+
}
47+
return parseBin(new DataView(bytes.buffer));
48+
};
49+
const loadNatives = async (path: string) => {
50+
// dynamic load modules
51+
if (path.startsWith('modules')) {
52+
const module = path.split('/')[1] as string;
53+
const moduleFuncs = await loadSourceModules(new Set([module]), context, true);
54+
const { proxy } = createModuleProxy(module, moduleFuncs[module]);
55+
return { default: proxy };
56+
}
57+
return await import(`java-slang/dist/jvm/stdlib/${path}.js`);
58+
};
59+
const stdout = (str: string) => {
60+
if (str.endsWith('\n')) {
61+
buffer.push(str);
62+
const lines = buffer.join('').split('\n');
63+
lines.pop();
64+
lines.forEach(line => DisplayBufferService.push(line, context.externalContext));
65+
buffer = [];
66+
} else {
67+
buffer.push(str);
68+
}
69+
};
70+
const stderr = (msg: string) => {
71+
context.errors.push({
72+
type: 'Runtime' as any,
73+
severity: 'Error' as any,
74+
location: {
75+
start: {
76+
line: -1,
77+
column: -1
78+
},
79+
end: {
80+
line: -1,
81+
column: -1
82+
}
83+
},
84+
explain: () => msg,
85+
elaborate: () => msg
86+
});
87+
};
88+
89+
// FIXME: Remove when the compiler is working
90+
try {
91+
const json = JSON.parse(javaCode);
92+
compiled = json;
93+
} catch (e) {
94+
stderr(e);
95+
return Promise.resolve({ status: 'error' });
96+
}
97+
98+
// load cached classfiles from IndexedDB
99+
return loadCachedFiles(() =>
100+
// Initial loader to fetch commonly used classfiles
101+
fetch(Constants.javaPackagesUrl + '_base.json')
102+
.then(res => res.json())
103+
.then((obj: { [key: string]: string }) => {
104+
return obj;
105+
})
106+
)
107+
.then(stdlib => {
108+
files = {
109+
...stdlib,
110+
...compiled
111+
};
112+
113+
// run the JVM
114+
return new Promise((resolve, reject) => {
115+
setupJVM({
116+
javaClassPath: '',
117+
nativesPath: '',
118+
callbacks: {
119+
readFileSync: readClassFiles,
120+
readFile: loadNatives,
121+
stdout,
122+
stderr,
123+
onFinish: () => {
124+
resolve(
125+
context.errors.length
126+
? { status: 'error' }
127+
: { status: 'finished', context, value: '' }
128+
);
129+
}
130+
},
131+
natives: {}
132+
})();
133+
});
134+
})
135+
.catch(() => {
136+
return { status: 'error' };
137+
});
138+
}

0 commit comments

Comments
 (0)