diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml new file mode 100644 index 0000000..4d12001 --- /dev/null +++ b/.github/workflows/build-deploy.yml @@ -0,0 +1,50 @@ +name: Build and deploy runner + +on: + push: + branches: + - main + +jobs: + build: + name: Build runner + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Enable corepack + run: corepack enable + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: yarn + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Build + run: yarn run build --all || yarn build + + - name: Upload artifacts + id: deployment + uses: actions/upload-pages-artifact@v3 + with: + path: dist/ + + deploy: + needs: build + name: Deploy runner + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/src/compiler/symbol-table.ts b/src/compiler/symbol-table.ts index 394ffd3..127ac47 100644 --- a/src/compiler/symbol-table.ts +++ b/src/compiler/symbol-table.ts @@ -1,5 +1,6 @@ import { UnannType } from '../ast/types/classes' import { ImportDeclaration } from '../ast/types/packages-and-modules' +import { METHOD_FLAGS } from '../ClassFile/types/methods' import { generateClassAccessFlags, generateFieldAccessFlags, @@ -12,7 +13,6 @@ import { SymbolRedeclarationError } from './error' import { libraries } from './import/libs' -import { METHOD_FLAGS } from '../ClassFile/types/methods' export const typeMap = new Map([ ['byte', 'B'], diff --git a/src/conductor/JavaEvaluator.ts b/src/conductor/JavaEvaluator.ts new file mode 100644 index 0000000..6709ab7 --- /dev/null +++ b/src/conductor/JavaEvaluator.ts @@ -0,0 +1,51 @@ +import setupJVM from '../jvm/index' +import BasicEvaluator, { IRunnerPlugin } from './runner' + +/** + * Minimal Java conductor evaluator stub. + * Currently this evaluator is a placeholder that demonstrates how to + * integrate with the local JVM runner. It expects class file bytes + * encoded as a base64 string when used via conductor channels. + */ +export class JavaEvaluator extends BasicEvaluator { + constructor(conductor: IRunnerPlugin) { + super(conductor) + } + + async evaluateChunk(_chunk: string): Promise { + this.conductor.sendOutput('JavaEvaluator: evaluateChunk not supported; use evaluateFile with a .class file encoded as base64') + } + + async evaluateFile(fileName: string, fileContent: string): Promise { + try { + if (fileName.endsWith('.class')) { + // Expect class file content as base64 to allow conductor transport via JSON + Buffer.from(fileContent, 'base64') + // NOTE: we intentionally do not parse the class bytes here in the stub + // to avoid coupling the evaluator to the full classfile parser. A future + // implementation may call `parseBin` and then load into the JVM. + // create a JVM and run the Main class if present (not implemented in this stub) + setupJVM({ + callbacks: { + readFileSync: (path: string) => { + throw new Error('readFileSync not available in conductor JavaEvaluator') + }, + stdout: (m: string) => this.conductor.sendOutput(m), + stderr: (m: string) => this.conductor.sendOutput(`ERR: ${m}`) + } + }) + // Register a readFileSync that returns our in-memory class when requested + // The JVM runner currently expects classes on disk; for now we run using a minimal approach + this.conductor.sendOutput('JavaEvaluator: running class via in-memory runner is not yet implemented') + this.conductor.sendResult('') + return + } + + this.conductor.sendOutput('JavaEvaluator: unsupported file type') + } catch (err) { + this.conductor.sendError(`${err instanceof Error ? err.message : String(err)}`) + } + } +} + +export default JavaEvaluator diff --git a/src/conductor/__tests__/JavaEvaluator.test.ts b/src/conductor/__tests__/JavaEvaluator.test.ts new file mode 100644 index 0000000..353f659 --- /dev/null +++ b/src/conductor/__tests__/JavaEvaluator.test.ts @@ -0,0 +1,44 @@ +import { JavaEvaluator } from '../JavaEvaluator' +import { IRunnerPlugin } from '../runner' + +class MockConductor implements IRunnerPlugin { + outputs: string[] = [] + results: string[] = [] + errors: string[] = [] + statuses: Array<{ s: string; a: boolean }> = [] + sendOutput(message: string): void { + this.outputs.push(message) + } + sendResult(result: string): void { + this.results.push(result) + } + sendError(error: string): void { + this.errors.push(error) + } + updateStatus(status: any, isActive: boolean): void { + this.statuses.push({ s: String(status), a: isActive }) + } +} + +test('evaluateFile with unsupported file emits message', async () => { + const mock = new MockConductor() + const ev = new JavaEvaluator(mock) + await ev.evaluateFile('hello.txt', 'console') + expect(mock.outputs.length).toBeGreaterThan(0) + expect(mock.outputs[0]).toMatch(/unsupported file type/) + expect(mock.errors.length).toBe(0) +}) + +test('evaluateFile with .class base64 emits placeholder run message and result', async () => { + const mock = new MockConductor() + const ev = new JavaEvaluator(mock) + // Use a minimal dummy classfile header (CAFEBABE) as base64; the evaluator + // stub only inspects bytes and currently does not run them. + const buf = Buffer.from([0xca, 0xfe, 0xba, 0xbe]) + const b64 = buf.toString('base64') + await ev.evaluateFile('Main.class', b64) + // JavaEvaluator currently emits a placeholder message then a result string + expect(mock.outputs.some(o => /running class via in-memory runner is not yet implemented/.test(o))).toBe(true) + // sendResult is called with an empty string in the stub + expect(mock.results.length).toBeGreaterThanOrEqual(1) +}) diff --git a/src/conductor/evaluator.ts b/src/conductor/evaluator.ts new file mode 100644 index 0000000..a18cd23 --- /dev/null +++ b/src/conductor/evaluator.ts @@ -0,0 +1,5 @@ +// `__EVALUATOR__` is replaced at build time by bundlers in other projects. +// We declare it here for type compatibility when bundle replacement is used. +declare const __EVALUATOR__: any + +export default __EVALUATOR__ diff --git a/src/conductor/index.ts b/src/conductor/index.ts new file mode 100644 index 0000000..f0065c5 --- /dev/null +++ b/src/conductor/index.ts @@ -0,0 +1,2 @@ +export { JavaEvaluator } from './JavaEvaluator' +export { default as BasicEvaluator } from './runner' diff --git a/src/conductor/initialise.ts b/src/conductor/initialise.ts new file mode 100644 index 0000000..b1b47ed --- /dev/null +++ b/src/conductor/initialise.ts @@ -0,0 +1,5 @@ +import { initialise } from './runner' +// @ts-expect-error — __EVALUATOR__ is replaced at build time by bundlers +import { __EVALUATOR__ } from './index' + +initialise(__EVALUATOR__) diff --git a/src/conductor/runner.ts b/src/conductor/runner.ts new file mode 100644 index 0000000..3f906dc --- /dev/null +++ b/src/conductor/runner.ts @@ -0,0 +1,30 @@ +export type RunnerStatus = 'IDLE' | 'RUNNING' | 'ERROR' | 'FINISHED' + +export interface IRunnerPlugin { + sendOutput(message: string): void + sendResult(result: string): void + sendError(error: string): void + updateStatus(status: RunnerStatus, isActive: boolean): void +} + +export abstract class BasicEvaluator { + protected conductor: IRunnerPlugin + constructor(conductor: IRunnerPlugin) { + this.conductor = conductor + } + + abstract evaluateChunk(chunk: string): Promise + + async evaluateFile(fileName: string, fileContent: string): Promise { + // default: treat file content as a single chunk + await this.evaluateChunk(fileContent) + } +} + +export function initialise(_evaluator: any) { + // This is a minimal shim for initialise used in bundling entrypoints. + // Real conductor runner initialisation is out of scope for this repo. + return +} + +export default BasicEvaluator diff --git a/src/jvm/exception-table.ts b/src/jvm/exception-table.ts index 15248a8..46a1573 100644 --- a/src/jvm/exception-table.ts +++ b/src/jvm/exception-table.ts @@ -27,7 +27,7 @@ export class ExceptionTable { } insert(from: number, to: number, target: number, type: ClassData): void { - var entry = new Entry(from, to, target, type) + const entry = new Entry(from, to, target, type) this.entries.push(entry) } } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index f67a700..eff5375 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1299,6 +1299,10 @@ concat-map@0.0.1: resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +"conductor@https://github.com/source-academy/conductor.git#0.3.0": + version "0.3.0" + resolved "https://github.com/source-academy/conductor.git#56e4a421005b909d0fef55ba8119a736b9bd1e0c" + convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz"