Skip to content
Draft
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
52 changes: 52 additions & 0 deletions .github/workflows/build-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Build and deploy runner

on:
push:
branches:
- main

jobs:
build:
name: Build runner
permissions:
contents: read
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"@types/lodash": "^4.14.198",
"java-parser": "^2.0.5",
"lodash": "^4.17.21",
"peggy": "^4.0.2"
"peggy": "^4.0.2",
"@sourceacademy/conductor": "^0.3.0"
},
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}
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
118 changes: 118 additions & 0 deletions src/conductor/JavaEvaluator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import setupJVM from '../jvm/index'
import parseBin, { a2ab } from '../jvm/utils/disassembler'
import BasicEvaluator, { IRunnerPlugin } from '@sourceacademy/conductor/runner'

Check failure on line 3 in src/conductor/JavaEvaluator.ts

View workflow job for this annotation

GitHub Actions / build

Cannot find module '@sourceacademy/conductor/runner' or its corresponding type declarations.

/**
* 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')

Check failure on line 17 in src/conductor/JavaEvaluator.ts

View workflow job for this annotation

GitHub Actions / build

Property 'conductor' does not exist on type 'JavaEvaluator'.
}

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
const buf = Buffer.from(fileContent, 'base64')

// Try to parse the classfile bytes. If parsing fails, fall back to the
// previous placeholder behaviour so tests that pass a minimal header
// (e.g. CAFEBABE only) continue to work.
let classFile: any | null = null
try {
const ab = a2ab(buf)
const view = new DataView(ab)
classFile = parseBin(view)
} catch (e) {
// parsing failed -> fall back to stub behaviour used previously by tests
this.conductor.sendOutput('JavaEvaluator: running class via in-memory runner is not yet implemented')

Check failure on line 36 in src/conductor/JavaEvaluator.ts

View workflow job for this annotation

GitHub Actions / build

Property 'conductor' does not exist on type 'JavaEvaluator'.
this.conductor.sendResult('')

Check failure on line 37 in src/conductor/JavaEvaluator.ts

View workflow job for this annotation

GitHub Actions / build

Property 'conductor' does not exist on type 'JavaEvaluator'.
return
}

// resolve class internal name (e.g. "com/example/Main")
let mainClassName = 'Main'
try {
const clsInfo = classFile.constantPool[classFile.thisClass]
const nameConst = classFile.constantPool[clsInfo.nameIndex]
mainClassName = nameConst.value
} catch (e) {
// ignore and use default
}

// In-memory class registry (keyed by path used by loaders)
const mem: { [path: string]: any } = {}
// the AbstractClassLoader builds paths like (classPath ? classPath + '/' + className : className) + '.class'
// we'll use an empty userDir so loaders will request '<internalName>.class'
mem[`${mainClassName}.class`] = classFile

// attempt to load prebuilt stdlib classfiles mapping if available (optional)
let prebuilt: { [k: string]: string } | null = null
try {
// try a compiled mapping that some workflows generate
// eslint-disable-next-line @typescript-eslint/no-var-requires
const maybe = require('../../dist/jvm/utils/classfiles')
prebuilt = maybe && maybe.default ? maybe.default : maybe
} catch (e) {
prebuilt = null
}

const readFileSync = (path: string) => {
// direct in-memory hit
if (mem[path]) return mem[path]

// path might be prefixed with 'stdlib/' when requesting runtime classes
if (prebuilt && path.startsWith('stdlib/')) {
const key = path.slice('stdlib/'.length)
const b64 = prebuilt[key]
if (!b64) {
throw new Error(`class not found in prebuilt stdlib: ${key}`)
}
const buf2 = Buffer.from(b64, 'base64')
const classfile = parseBin(new DataView(a2ab(buf2)))
return classfile
}

// final fallback: error -> loader will translate to ClassNotFoundException
throw new Error(`readFileSync: class not found: ${path}`)
}

const runFn = setupJVM({
mainClass: mainClassName,
userDir: '',
callbacks: {
readFileSync,
readFile: () => Promise.reject('readFile not implemented'),
stdout: (m: string) => this.conductor.sendOutput(m),

Check failure on line 94 in src/conductor/JavaEvaluator.ts

View workflow job for this annotation

GitHub Actions / build

Property 'conductor' does not exist on type 'JavaEvaluator'.
stderr: (m: string) => this.conductor.sendOutput(`ERR: ${m}`),

Check failure on line 95 in src/conductor/JavaEvaluator.ts

View workflow job for this annotation

GitHub Actions / build

Property 'conductor' does not exist on type 'JavaEvaluator'.
onFinish: () => {
// when JVM finishes we don't currently capture any return value
this.conductor.sendResult('')

Check failure on line 98 in src/conductor/JavaEvaluator.ts

View workflow job for this annotation

GitHub Actions / build

Property 'conductor' does not exist on type 'JavaEvaluator'.
}
}
})

try {
runFn()
} catch (e) {
this.conductor.sendError(`${e instanceof Error ? e.message : String(e)}`)

Check failure on line 106 in src/conductor/JavaEvaluator.ts

View workflow job for this annotation

GitHub Actions / build

Property 'conductor' does not exist on type 'JavaEvaluator'.
}
return
}

this.conductor.sendOutput('JavaEvaluator: unsupported file type')

Check failure on line 111 in src/conductor/JavaEvaluator.ts

View workflow job for this annotation

GitHub Actions / build

Property 'conductor' does not exist on type 'JavaEvaluator'.
} catch (err) {
this.conductor.sendError(`${err instanceof Error ? err.message : String(err)}`)

Check failure on line 113 in src/conductor/JavaEvaluator.ts

View workflow job for this annotation

GitHub Actions / build

Property 'conductor' does not exist on type 'JavaEvaluator'.
}
}
}

export default JavaEvaluator
43 changes: 43 additions & 0 deletions src/conductor/__tests__/JavaEvaluator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import JavaEvaluator from '../JavaEvaluator'

class MockConductor {
outputs: string[] = []
results: string[] = []
errors: string[] = []
sendOutput(message: string): void {
this.outputs.push(message)
}
sendResult(result: string): void {
this.results.push(result)
}
sendError(error: string): void {
this.errors.push(error)
}
}

describe('JavaEvaluator', () => {
test('reports unsupported file type for non-.class files', async () => {
const mock = new MockConductor()
const ev = new JavaEvaluator(mock as any)

await ev.evaluateFile('program.txt', 'ignored')

expect(mock.outputs).toContain('JavaEvaluator: unsupported file type')
expect(mock.results).toHaveLength(0)
expect(mock.errors).toHaveLength(0)
})

test('falls back when class parsing fails and reports stub behaviour', async () => {
const mock = new MockConductor()
const ev = new JavaEvaluator(mock as any)

// pass some base64 that is not a valid classfile; parseBin should throw
const invalidBytes = Buffer.from([0x00, 0x01, 0x02]).toString('base64')

await ev.evaluateFile('Main.class', invalidBytes)

expect(mock.outputs).toContain('JavaEvaluator: running class via in-memory runner is not yet implemented')
expect(mock.results).toContain('')
expect(mock.errors).toHaveLength(0)
})
})
3 changes: 3 additions & 0 deletions src/conductor/evaluator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { __EVALUATOR__ } from "./index"

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 '@sourceacademy/conductor/runner'
4 changes: 4 additions & 0 deletions src/conductor/initialise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { initialise } from '@sourceacademy/conductor/runner'
import { __EVALUATOR__ } from './index'

initialise(__EVALUATOR__)
33 changes: 0 additions & 33 deletions src/jvm/exception-table.ts

This file was deleted.

5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,11 @@
dependencies:
"@sinonjs/commons" "^3.0.0"

"@sourceacademy/conductor@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@sourceacademy/conductor/-/conductor-0.3.0.tgz#5294caeb14c5eba29fb3a6015296d262683026d2"
integrity sha512-pwzx64p22g8OM6AI73eyE8wgzBg7Iq35T6HLwcO3MXDgeLvsEV33BQVLtXqRNAJYQiVy4301Ci2WAI8eM149oA==

"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz"
Expand Down
Loading