Skip to content
Open
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
50 changes: 50 additions & 0 deletions .github/workflows/build-deploy.yml
Original file line number Diff line number Diff line change
@@ -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:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
Comment on lines +10 to +37
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
2 changes: 1 addition & 1 deletion src/compiler/symbol-table.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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'],
Expand Down
51 changes: 51 additions & 0 deletions src/conductor/JavaEvaluator.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
this.conductor.sendOutput('JavaEvaluator: evaluateChunk not supported; use evaluateFile with a .class file encoded as base64')
}

async evaluateFile(fileName: string, fileContent: string): Promise<void> {
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}`)
}
})
Comment on lines +28 to +36

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

setupJVM returns a runner function that must be executed to actually start the JVM. Currently, the returned function is discarded, meaning the JVM is never run. Since running the class via the in-memory runner is not yet implemented, this call is dead code and can be removed or commented out to avoid unnecessary instantiation overhead.

        // setupJVM call removed as it is currently unused dead code

// 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
44 changes: 44 additions & 0 deletions src/conductor/__tests__/JavaEvaluator.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
5 changes: 5 additions & 0 deletions src/conductor/evaluator.ts
Original file line number Diff line number Diff line change
@@ -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__
2 changes: 2 additions & 0 deletions src/conductor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { JavaEvaluator } from './JavaEvaluator'
export { default as BasicEvaluator } from './runner'
5 changes: 5 additions & 0 deletions src/conductor/initialise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { initialise } from './runner'
// @ts-expect-error — __EVALUATOR__ is replaced at build time by bundlers
import { __EVALUATOR__ } from './index'
Comment on lines +2 to +3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of importing __EVALUATOR__ from ./index (which does not export it) and using @ts-expect-error, you can import it directly from ./evaluator where it is declared and exported as default. This provides full type safety and avoids compilation/bundling workarounds.

Suggested change
// @ts-expect-error — __EVALUATOR__ is replaced at build time by bundlers
import { __EVALUATOR__ } from './index'
import __EVALUATOR__ from './evaluator'


initialise(__EVALUATOR__)
30 changes: 30 additions & 0 deletions src/conductor/runner.ts
Original file line number Diff line number Diff line change
@@ -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<void>

async evaluateFile(fileName: string, fileContent: string): Promise<void> {
// 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
2 changes: 1 addition & 1 deletion src/jvm/exception-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The this.entries array is declared but never initialized (e.g., to []), which will cause a runtime TypeError when push is called. Please initialize this.entries to an empty array. Note that the retrieve method also has an issue where returning from inside forEach does not return from the outer function; consider refactoring it to use find or a for...of loop.

        if (!this.entries) {
            this.entries = []
        }
        const entry = new Entry(from, to, target, type)

this.entries.push(entry)
}
}
4 changes: 4 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading