diff --git a/README.md b/README.md index 167e95b..afa2afb 100644 --- a/README.md +++ b/README.md @@ -38,4 +38,16 @@ The AST types need to be regenerated after changing the AST type definitions in `generate-ast.ts`. ```shell npm run regen -``` \ No newline at end of file +``` + +## Acknowledgements + +This project adapts the `Conductor Interface` from [source-academy/conductor](https://github.com/source-academy/conductor), which is part of the Source Academy ecosystem. + +Specifically, all files under the following folders are derived from the conductor repository: + +- `src/conductor/` +- `src/common/` +- `src/conduit/` + +All credits go to the original authors of the Source Academy Conductor Interface. diff --git a/src/common/Constant.ts b/src/common/Constant.ts new file mode 100644 index 0000000..3c3f24c --- /dev/null +++ b/src/common/Constant.ts @@ -0,0 +1,8 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export const enum Constant { + PROTOCOL_VERSION = 0, + PROTOCOL_MIN_VERSION = 0, +} diff --git a/src/common/ds/MessageQueue.ts b/src/common/ds/MessageQueue.ts new file mode 100644 index 0000000..221020b --- /dev/null +++ b/src/common/ds/MessageQueue.ts @@ -0,0 +1,31 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { Queue } from "./Queue"; + +export class MessageQueue { + private readonly __inputQueue: Queue = new Queue(); + private readonly __promiseQueue: Queue = new Queue(); + + push(item: T) { + if (this.__promiseQueue.length !== 0) this.__promiseQueue.pop()(item); + else this.__inputQueue.push(item); + } + + async pop(): Promise { + if (this.__inputQueue.length !== 0) return this.__inputQueue.pop(); + return new Promise((resolve, _reject) => { + this.__promiseQueue.push(resolve); + }); + } + + tryPop(): T | undefined { + if (this.__inputQueue.length !== 0) return this.__inputQueue.pop(); + return undefined; + } + + constructor() { + this.push = this.push.bind(this); + } +} diff --git a/src/common/ds/Queue.ts b/src/common/ds/Queue.ts new file mode 100644 index 0000000..0558776 --- /dev/null +++ b/src/common/ds/Queue.ts @@ -0,0 +1,55 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +/** + * A stack-based queue implementation. + * `push` and `pop` run in amortized constant time. + */ +export class Queue { + /** The output stack. */ + private __s1: T[] = []; + /** The input stack. */ + private __s2: T[] = []; + + /** + * Adds an item to the queue. + * @param item The item to be added to the queue. + */ + push(item: T) { + this.__s2.push(item); + } + + /** + * Removes an item from the queue. + * @returns The item removed from the queue. + * @throws If the queue is empty. + */ + pop(): T { + if (this.__s1.length === 0) { + if (this.__s2.length === 0) throw new Error("queue is empty"); + let temp = this.__s1; + this.__s1 = this.__s2.reverse(); + this.__s2 = temp; + } + return this.__s1.pop()!; // as the length is nonzero + } + + /** + * The length of the queue. + */ + get length() { + return this.__s1.length + this.__s2.length; + } + + /** + * Makes a copy of the queue. + * @returns A copy of the queue. + */ + clone(): Queue { + const newQueue = new Queue(); + newQueue.__s1 = [...this.__s1]; + newQueue.__s2 = [...this.__s2]; + return newQueue; + } +} diff --git a/src/common/ds/index.ts b/src/common/ds/index.ts new file mode 100644 index 0000000..abe0900 --- /dev/null +++ b/src/common/ds/index.ts @@ -0,0 +1,6 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export { MessageQueue } from "./MessageQueue"; +export { Queue } from "./Queue"; diff --git a/src/common/errors/ConductorError.ts b/src/common/errors/ConductorError.ts new file mode 100644 index 0000000..4c559ac --- /dev/null +++ b/src/common/errors/ConductorError.ts @@ -0,0 +1,17 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { ErrorType } from "./ErrorType"; + +/** + * Generic Conductor Error. + */ +export class ConductorError extends Error { + override name = "ConductorError"; + readonly errorType: ErrorType | string = ErrorType.UNKNOWN; + + constructor(message: string) { + super(message); + } +} diff --git a/src/common/errors/ConductorInternalError.ts b/src/common/errors/ConductorInternalError.ts new file mode 100644 index 0000000..d1c7256 --- /dev/null +++ b/src/common/errors/ConductorInternalError.ts @@ -0,0 +1,18 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { ConductorError } from "./ConductorError"; +import { ErrorType } from "./ErrorType"; + +/** + * Conductor internal error, probably caused by developer oversight. + */ +export class ConductorInternalError extends ConductorError { + override name = "ConductorInternalError"; + override readonly errorType: ErrorType | string = ErrorType.INTERNAL; + + constructor(message: string) { + super(message); + } +} diff --git a/src/common/errors/ErrorType.ts b/src/common/errors/ErrorType.ts new file mode 100644 index 0000000..7e6503d --- /dev/null +++ b/src/common/errors/ErrorType.ts @@ -0,0 +1,12 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export const enum ErrorType { + UNKNOWN = "__unknown", + INTERNAL = "__internal", + EVALUATOR = "__evaluator", + EVALUATOR_SYNTAX = "__evaluator_syntax", + EVALUATOR_TYPE = "__evaluator_type", + EVALUATOR_RUNTIME = "__evaluator_runtime", +} diff --git a/src/common/errors/EvaluatorError.ts b/src/common/errors/EvaluatorError.ts new file mode 100644 index 0000000..e4579b6 --- /dev/null +++ b/src/common/errors/EvaluatorError.ts @@ -0,0 +1,30 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { ConductorError } from "./ConductorError"; +import { ErrorType } from "./ErrorType"; + +/** + * Generic evaluation error, caused by a problem in user code. + */ +export class EvaluatorError extends ConductorError { + override name = "EvaluatorError"; + override readonly errorType: ErrorType | string = ErrorType.EVALUATOR; + + readonly rawMessage: string; + readonly line?: number; + readonly column?: number; + readonly fileName?: string + + constructor(message: string, line?: number, column?: number, fileName?: string) { + const location = line !== undefined + ? `${fileName ? fileName + ":" : ""}${line}${column !== undefined ? ":" + column : ""}: ` + : ""; + super(`${location}${message}`); + this.rawMessage = message; + this.line = line; + this.column = column; + this.fileName = fileName + } +} diff --git a/src/common/errors/EvaluatorRuntimeError.ts b/src/common/errors/EvaluatorRuntimeError.ts new file mode 100644 index 0000000..20d91c7 --- /dev/null +++ b/src/common/errors/EvaluatorRuntimeError.ts @@ -0,0 +1,14 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { ErrorType } from "./ErrorType"; +import { EvaluatorError } from "./EvaluatorError"; + +/** + * Evaluator runtime error - some problem occurred while running the user code. + */ +export class EvaluatorRuntimeError extends EvaluatorError { + override name = "EvaluatorRuntimeError"; + override readonly errorType: ErrorType | string = ErrorType.EVALUATOR_RUNTIME; +} diff --git a/src/common/errors/EvaluatorSyntaxError.ts b/src/common/errors/EvaluatorSyntaxError.ts new file mode 100644 index 0000000..02dffb4 --- /dev/null +++ b/src/common/errors/EvaluatorSyntaxError.ts @@ -0,0 +1,14 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { ErrorType } from "./ErrorType"; +import { EvaluatorError } from "./EvaluatorError"; + +/** + * Evaluator syntax error - the user code does not follow the evaluator's prescribed syntax. + */ +export class EvaluatorSyntaxError extends EvaluatorError { + override name = "EvaluatorSyntaxError"; + override readonly errorType: ErrorType | string = ErrorType.EVALUATOR_SYNTAX; +} diff --git a/src/common/errors/EvaluatorTypeError.ts b/src/common/errors/EvaluatorTypeError.ts new file mode 100644 index 0000000..0cc7331 --- /dev/null +++ b/src/common/errors/EvaluatorTypeError.ts @@ -0,0 +1,26 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { ConductorError } from "./ConductorError"; +import { ErrorType } from "./ErrorType"; +import { EvaluatorError } from "./EvaluatorError"; + +/** + * Evaluator type error - the user code is not well typed or provides values of incorrect type to external functions. + */ +export class EvaluatorTypeError extends EvaluatorError { + override name = "EvaluatorTypeError"; + override readonly errorType: ErrorType | string = ErrorType.EVALUATOR_TYPE; + + override readonly rawMessage: string; + readonly expected: string; + readonly actual: string; + + constructor(message: string, expected: string, actual: string, line?: number, column?: number, fileName?: string) { + super(`${message} (expected ${expected}, got ${actual})`, line, column, fileName); + this.rawMessage = message; + this.expected = expected; + this.actual = actual; + } +} diff --git a/src/common/errors/index.ts b/src/common/errors/index.ts new file mode 100644 index 0000000..4ad6ad2 --- /dev/null +++ b/src/common/errors/index.ts @@ -0,0 +1,8 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export { ConductorError } from "./ConductorError"; +export { ConductorInternalError } from "./ConductorInternalError"; +export { ErrorType } from "./ErrorType"; +export { EvaluatorTypeError } from "./EvaluatorTypeError"; diff --git a/src/common/util/InvalidModuleError.ts b/src/common/util/InvalidModuleError.ts new file mode 100644 index 0000000..ae6af0d --- /dev/null +++ b/src/common/util/InvalidModuleError.ts @@ -0,0 +1,14 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { ConductorError } from "../errors"; + +export class InvalidModuleError extends ConductorError { + override name = "InvalidModuleError"; + override readonly errorType = "__invalidmodule"; + + constructor() { + super("Not a module"); + } +} diff --git a/src/common/util/importExternalModule.ts b/src/common/util/importExternalModule.ts new file mode 100644 index 0000000..6a72211 --- /dev/null +++ b/src/common/util/importExternalModule.ts @@ -0,0 +1,18 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { IModulePlugin } from "../../conductor/module"; +import { PluginClass } from "../../conduit/types"; +import { importExternalPlugin } from "./importExternalPlugin"; + +/** + * Imports an external module from a given location. + * @param location Where to find the external module. + * @returns A promise resolving to the imported module. + */ +export async function importExternalModule(location: string): Promise> { + const plugin = await importExternalPlugin(location) as PluginClass; + // TODO: additional verification it is a module + return plugin; +} diff --git a/src/common/util/importExternalPlugin.ts b/src/common/util/importExternalPlugin.ts new file mode 100644 index 0000000..508cba8 --- /dev/null +++ b/src/common/util/importExternalPlugin.ts @@ -0,0 +1,16 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { PluginClass } from "../../conduit/types"; + +/** + * Imports an external plugin from a given location. + * @param location Where to find the external plugin. + * @returns A promise resolving to the imported plugin. + */ +export async function importExternalPlugin(location: string): Promise { + const plugin = (await import(/* webpackIgnore: true */ location)).plugin as PluginClass; + // TODO: verify it is actually a plugin + return plugin; +} diff --git a/src/common/util/index.ts b/src/common/util/index.ts new file mode 100644 index 0000000..f459b58 --- /dev/null +++ b/src/common/util/index.ts @@ -0,0 +1,7 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export { importExternalModule } from "./importExternalModule"; +export { importExternalPlugin } from "./importExternalPlugin"; +export { InvalidModuleError } from "./InvalidModuleError"; diff --git a/src/conductor/host/BasicHostPlugin.ts b/src/conductor/host/BasicHostPlugin.ts new file mode 100644 index 0000000..c54c722 --- /dev/null +++ b/src/conductor/host/BasicHostPlugin.ts @@ -0,0 +1,116 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { Constant } from "../../common/Constant"; +import type { ConductorError } from "../../common/errors"; +import { importExternalPlugin } from "../../common/util"; +import { IChannel, IConduit, IPlugin } from "../../conduit"; +import { makeRpc } from "../../conduit/rpc"; +import { PluginClass } from "../../conduit/types"; +import { checkIsPluginClass } from "../../conduit/util"; +import { InternalChannelName, InternalPluginName } from "../strings"; +import { AbortServiceMessage, Chunk, EntryServiceMessage, HelloServiceMessage, IChunkMessage, IErrorMessage, IIOMessage, IServiceMessage, IStatusMessage, PluginServiceMessage, RunnerStatus } from "../types"; +import { ServiceMessageType } from "../types"; +import { IHostFileRpc, IHostPlugin } from "./types"; + +@checkIsPluginClass +export abstract class BasicHostPlugin implements IHostPlugin { + name = InternalPluginName.HOST_MAIN; + + private readonly __conduit: IConduit; + private readonly __chunkChannel: IChannel; + private readonly __serviceChannel: IChannel; + private readonly __ioChannel: IChannel; + + private readonly __status = new Map(); + + private __chunkCount: number = 0; + + // @ts-expect-error TODO: figure proper way to typecheck this + private readonly __serviceHandlers = new Map void>([ + [ServiceMessageType.HELLO, function helloServiceHandler(this: BasicHostPlugin, message: HelloServiceMessage) { + if (message.data.version < Constant.PROTOCOL_MIN_VERSION) { + this.__serviceChannel.send(new AbortServiceMessage(Constant.PROTOCOL_MIN_VERSION)); + console.error(`Runner's protocol version (${message.data.version}) must be at least ${Constant.PROTOCOL_MIN_VERSION}`); + } else { + console.log(`Runner is using protocol version ${message.data.version}`); + } + }], + [ServiceMessageType.ABORT, function abortServiceHandler(this: BasicHostPlugin, message: AbortServiceMessage) { + console.error(`Runner expects at least protocol version ${message.data.minVersion}, but we are on version ${Constant.PROTOCOL_VERSION}`); + this.__conduit.terminate(); + }], + [ServiceMessageType.PLUGIN, function pluginServiceHandler(this: BasicHostPlugin, message: PluginServiceMessage) { + const pluginName = message.data; + this.requestLoadPlugin(pluginName); + }] + ]); + + abstract requestFile(fileName: string): Promise; + + abstract requestLoadPlugin(pluginName: string): void; + + startEvaluator(entryPoint: string): void { + this.__serviceChannel.send(new EntryServiceMessage(entryPoint)); + } + + sendChunk(chunk: Chunk): void { + this.__chunkChannel.send({ id: this.__chunkCount++, chunk }); + } + + sendInput(message: string): void { + this.__ioChannel.send({ message }); + } + + receiveOutput?(message: string): void; + + receiveError?(message: ConductorError): void; + + isStatusActive(status: RunnerStatus): boolean { + return this.__status.get(status) ?? false; + } + + receiveStatusUpdate?(status: RunnerStatus, isActive: boolean): void; + + registerPlugin(pluginClass: PluginClass, ...arg: Arg): NoInfer { + return this.__conduit.registerPlugin(pluginClass, ...arg); + } + + unregisterPlugin(plugin: IPlugin): void { + this.__conduit.unregisterPlugin(plugin); + } + + async importAndRegisterExternalPlugin(location: string, ...arg: any[]): Promise { + const pluginClass = await importExternalPlugin(location); + return this.registerPlugin(pluginClass as any, ...arg); + } + + static readonly channelAttach = [InternalChannelName.FILE, InternalChannelName.CHUNK, InternalChannelName.SERVICE, InternalChannelName.STANDARD_IO, InternalChannelName.ERROR, InternalChannelName.STATUS]; + constructor(conduit: IConduit, [fileChannel, chunkChannel, serviceChannel, ioChannel, errorChannel, statusChannel]: IChannel[]) { + this.__conduit = conduit; + + makeRpc(fileChannel, { + requestFile: this.requestFile.bind(this) + }); + + this.__chunkChannel = chunkChannel; + this.__serviceChannel = serviceChannel; + + this.__ioChannel = ioChannel; + ioChannel.subscribe((ioMessage: IIOMessage) => this.receiveOutput?.(ioMessage.message)); + + errorChannel.subscribe((errorMessage: IErrorMessage) => this.receiveError?.(errorMessage.error)); + + statusChannel.subscribe((statusMessage: IStatusMessage) => { + const {status, isActive} = statusMessage; + this.__status.set(status, isActive); + this.receiveStatusUpdate?.(status, isActive); + }); + + this.__serviceChannel.send(new HelloServiceMessage()); + this.__serviceChannel.subscribe(message => { + this.__serviceHandlers.get(message.type)?.call(this, message); + }); + } +} diff --git a/src/conductor/host/index.ts b/src/conductor/host/index.ts new file mode 100644 index 0000000..25b32a0 --- /dev/null +++ b/src/conductor/host/index.ts @@ -0,0 +1,6 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export type { IHostPlugin } from "./types"; +export { BasicHostPlugin } from "./BasicHostPlugin"; diff --git a/src/conductor/host/types/IHostFileRpc.ts b/src/conductor/host/types/IHostFileRpc.ts new file mode 100644 index 0000000..034dee6 --- /dev/null +++ b/src/conductor/host/types/IHostFileRpc.ts @@ -0,0 +1,7 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export interface IHostFileRpc { + requestFile(fileName: string): Promise; +} diff --git a/src/conductor/host/types/IHostPlugin.ts b/src/conductor/host/types/IHostPlugin.ts new file mode 100644 index 0000000..03b2524 --- /dev/null +++ b/src/conductor/host/types/IHostPlugin.ts @@ -0,0 +1,113 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { ConductorError } from "../../../common/errors"; +import type { IPlugin } from "../../../conduit"; +import { PluginClass } from "../../../conduit/types"; +import type { Chunk, RunnerStatus } from "../../types"; + +export interface IHostPlugin extends IPlugin { + /** + * Request a file's contents. + * @param fileName The name of the file to request. + * @returns A promise resolving to the content of the requested file. + */ + requestFile(fileName: string): Promise; + + /** + * Request to load a plugin. + * @param pluginName The name of the plugin to request loading. + */ + requestLoadPlugin(pluginName: string): void; + + /** + * Starts the evaluator. + * @param entryPoint The entry point file to start running from. + */ + startEvaluator(entryPoint: string): void; + + /** + * Send the next chunk to be run. + * @param chunk The next chunk to be run. + */ + sendChunk(chunk: Chunk): void; + + /** + * Send an input on standard-input. + * @param input The input to be sent on standard-input. + */ + sendInput(input: string): void; + + // /** + // * Request for some output on standard-output. + // * @returns A promise resolving to the output received. + // */ + // requestOutput(): Promise; + + // /** + // * Try to request for some output on standard-output. + // * @returns The output received, or undefined if there is currently no output. + // */ + // tryRequestOutput(): string | undefined; + + /** + * An event handler called when an output is received. + * @param message The output received. + */ + receiveOutput?(message: string): void; + + // /** + // * Request for some output on standard-error. + // * @returns A promise resolving to the error received. + // */ + // requestError(): Promise; + + // /** + // * Try to request for some output on standard-error. + // * @returns The error received, or undefined if there is currently no error. + // */ + // tryRequestError(): ConductorError | undefined; + + /** + * An event handler called when an error is received. + * @param message The error received. + */ + receiveError?(message: ConductorError): void; + + /** + * Checks if a runner status is active. + * @param status The runner status to check. + * @returns true if the given status is active. + */ + isStatusActive(status: RunnerStatus): boolean; + + /** + * An event handler called when a status update is received. + * @param message The status update received. + * @param isActive Is the specified status currently active? + */ + receiveStatusUpdate?(status: RunnerStatus, isActive: boolean): void; + + /** + * Registers a plugin with the conduit. + * @param pluginClass The plugin class to be registered. + * @param arg Arguments to be passed to pluginClass' constructor. + * @returns The registered plugin. + */ + registerPlugin(pluginClass: PluginClass, ...arg: Arg): NoInfer; + + /** + * Unregister a plugin from the conduit. + * @param plugin The plugin to be unregistered. + */ + unregisterPlugin(plugin: IPlugin): void; + + /** + * Imports an external plugin and registers it with the conduit. + * @param location The location of the external plugin. + * @param arg Arguments to be passed to the external plugin's constructor. + * @returns The imported plugin. + */ + importAndRegisterExternalPlugin(location: string, ...arg: any[]): Promise; +} diff --git a/src/conductor/host/types/index.ts b/src/conductor/host/types/index.ts new file mode 100644 index 0000000..04a2fd4 --- /dev/null +++ b/src/conductor/host/types/index.ts @@ -0,0 +1,6 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export type { IHostFileRpc } from "./IHostFileRpc"; +export type { IHostPlugin } from "./IHostPlugin"; diff --git a/src/conductor/module/BaseModulePlugin.ts b/src/conductor/module/BaseModulePlugin.ts new file mode 100644 index 0000000..79442c9 --- /dev/null +++ b/src/conductor/module/BaseModulePlugin.ts @@ -0,0 +1,32 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { ConductorInternalError } from "../../common/errors/ConductorInternalError"; +import { IChannel, IConduit } from "../../conduit"; +import { checkIsPluginClass } from "../../conduit/util"; +import { IInterfacableEvaluator } from "../runner/types"; +import { ExternCallable, IDataHandler, IFunctionSignature } from "../types"; +import { IModulePlugin, IModuleExport } from "./types"; + +@checkIsPluginClass +export abstract class BaseModulePlugin implements IModulePlugin { + readonly exports: IModuleExport[] = []; + readonly exportedNames: readonly (keyof this)[] = []; + + readonly evaluator: IDataHandler; + + static readonly channelAttach: string[]; + constructor(_conduit: IConduit, _channels: IChannel[], evaluator: IInterfacableEvaluator) { + this.evaluator = evaluator; + for (const name of this.exportedNames) { + const m = this[name] as ExternCallable & {signature?: IFunctionSignature}; + if (!m.signature || typeof m !== "function" || typeof name !== "string") throw new ConductorInternalError(`'${String(name)}' is not an exportable method`); + this.exports.push({ + symbol: name, + value: m, + signature: m.signature + }); + } + } +} diff --git a/src/conductor/module/index.ts b/src/conductor/module/index.ts new file mode 100644 index 0000000..b3bb29b --- /dev/null +++ b/src/conductor/module/index.ts @@ -0,0 +1,6 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export type { IModuleExport, IModulePlugin } from "./types"; +export { BaseModulePlugin } from "./BaseModulePlugin"; diff --git a/src/conductor/module/types/IModuleExport.ts b/src/conductor/module/types/IModuleExport.ts new file mode 100644 index 0000000..3236815 --- /dev/null +++ b/src/conductor/module/types/IModuleExport.ts @@ -0,0 +1,16 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { ExternCallable, IFunctionSignature, NativeValue } from "../../types"; + +export interface IModuleExport { + /** The symbol referencing the export. */ + symbol: string; + + /** The exported value. Can be JS-native values or a function. */ + value: NativeValue | ExternCallable; + + /** If value is a function, provides its function signature. */ + signature?: IFunctionSignature; // TODO: allow richer typing somehow? +} diff --git a/src/conductor/module/types/IModulePlugin.ts b/src/conductor/module/types/IModulePlugin.ts new file mode 100644 index 0000000..42bec24 --- /dev/null +++ b/src/conductor/module/types/IModulePlugin.ts @@ -0,0 +1,13 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { IPlugin } from "../../../conduit"; +import type { IDataHandler } from "../../types"; +import type { IModuleExport } from "./IModuleExport"; + +export interface IModulePlugin extends IPlugin { + readonly exports: IModuleExport[]; + + readonly evaluator: IDataHandler; +} diff --git a/src/conductor/module/types/ModuleClass.ts b/src/conductor/module/types/ModuleClass.ts new file mode 100644 index 0000000..cdd22dc --- /dev/null +++ b/src/conductor/module/types/ModuleClass.ts @@ -0,0 +1,9 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { PluginClass } from "../../../conduit/types"; +import { IEvaluator } from "../../runner/types"; +import { IModulePlugin } from "./IModulePlugin"; + +export type ModuleClass = PluginClass<[IEvaluator], T>; diff --git a/src/conductor/module/types/index.ts b/src/conductor/module/types/index.ts new file mode 100644 index 0000000..8f4fe03 --- /dev/null +++ b/src/conductor/module/types/index.ts @@ -0,0 +1,6 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export type { IModuleExport } from "./IModuleExport"; +export type { IModulePlugin } from "./IModulePlugin"; diff --git a/src/conductor/module/util/index.ts b/src/conductor/module/util/index.ts new file mode 100644 index 0000000..f108c0d --- /dev/null +++ b/src/conductor/module/util/index.ts @@ -0,0 +1,5 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export { moduleMethod } from "./moduleMethod"; diff --git a/src/conductor/module/util/moduleMethod.ts b/src/conductor/module/util/moduleMethod.ts new file mode 100644 index 0000000..3e330f4 --- /dev/null +++ b/src/conductor/module/util/moduleMethod.ts @@ -0,0 +1,13 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { DataType, ExternCallable, IFunctionSignature } from "../../types"; + +export function moduleMethod(args: Args, returnType: Ret) { + const signature = {args, returnType} as const satisfies IFunctionSignature; + function externalClosureDecorator(method: ExternCallable & {signature?: IFunctionSignature}, _context: ClassMemberDecoratorContext) { + method.signature = signature; + } + return externalClosureDecorator; +} diff --git a/src/conductor/runner/BasicEvaluator.ts b/src/conductor/runner/BasicEvaluator.ts new file mode 100644 index 0000000..5331a1e --- /dev/null +++ b/src/conductor/runner/BasicEvaluator.ts @@ -0,0 +1,41 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { ConductorInternalError } from "../../common/errors"; +import { IEvaluator, IRunnerPlugin } from "./types"; + +export abstract class BasicEvaluator implements IEvaluator { + readonly conductor: IRunnerPlugin; + + async startEvaluator(entryPoint: string): Promise { + const initialChunk = await this.conductor.requestFile(entryPoint); + if (!initialChunk) throw new ConductorInternalError("Cannot load entrypoint file"); + await this.evaluateFile(entryPoint, initialChunk); + while (true) { + const chunk = await this.conductor.requestChunk(); + await this.evaluateChunk(chunk); + } + } + + /** + * Evaluates a file. + * @param fileName The name of the file to be evaluated. + * @param fileContent The content of the file to be evaluated. + * @returns A promise that resolves when the evaluation is complete. + */ + async evaluateFile(fileName: string, fileContent: string): Promise { + return this.evaluateChunk(fileContent); + } + + /** + * Evaluates a chunk. + * @param chunk The chunk to be evaluated. + * @returns A promise that resolves when the evaluation is complete. + */ + abstract evaluateChunk(chunk: string): Promise; + + constructor(conductor: IRunnerPlugin) { + this.conductor = conductor; + } +} diff --git a/src/conductor/runner/RunnerPlugin.ts b/src/conductor/runner/RunnerPlugin.ts new file mode 100644 index 0000000..1f6fc9a --- /dev/null +++ b/src/conductor/runner/RunnerPlugin.ts @@ -0,0 +1,137 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { Constant } from "../../common/Constant"; +import type { ConductorError } from "../../common/errors"; +import { ConductorInternalError } from "../../common/errors/ConductorInternalError"; +import { importExternalModule, importExternalPlugin } from "../../common/util"; +import { IConduit, IChannelQueue, IChannel, ChannelQueue, IPlugin } from "../../conduit"; +import { makeRpc } from "../../conduit/rpc"; +import { Remote } from "../../conduit/rpc/types"; +import { PluginClass } from "../../conduit/types"; +import { checkIsPluginClass } from "../../conduit/util"; +import { IHostFileRpc } from "../host/types"; +import { IModulePlugin } from "../module"; +import { ModuleClass } from "../module/types/ModuleClass"; +import { InternalChannelName, InternalPluginName } from "../strings"; +import { Chunk, IChunkMessage, IServiceMessage, IIOMessage, IStatusMessage, RunnerStatus, ServiceMessageType, HelloServiceMessage, AbortServiceMessage, type EntryServiceMessage, IErrorMessage, PluginServiceMessage } from "../types"; +import { IRunnerPlugin, IEvaluator, IInterfacableEvaluator, EvaluatorClass } from "./types"; + +@checkIsPluginClass +export class RunnerPlugin implements IRunnerPlugin { + name = InternalPluginName.RUNNER_MAIN; + + private readonly __evaluator: IEvaluator | IInterfacableEvaluator; + private readonly __isCompatibleWithModules: boolean; + private readonly __conduit: IConduit; + private readonly __fileRpc: Remote; + private readonly __chunkQueue: IChannelQueue; + private readonly __serviceChannel: IChannel; + private readonly __ioQueue: IChannelQueue; + private readonly __errorChannel: IChannel; + private readonly __statusChannel: IChannel; + + // @ts-expect-error TODO: figure proper way to typecheck this + private readonly __serviceHandlers = new Map void>([ + [ServiceMessageType.HELLO, function helloServiceHandler(this: RunnerPlugin, message: HelloServiceMessage) { + if (message.data.version < Constant.PROTOCOL_MIN_VERSION) { + this.__serviceChannel.send(new AbortServiceMessage(Constant.PROTOCOL_MIN_VERSION)); + console.error(`Host's protocol version (${message.data.version}) must be at least ${Constant.PROTOCOL_MIN_VERSION}`); + } else { + console.log(`Host is using protocol version ${message.data.version}`); + } + }], + [ServiceMessageType.ABORT, function abortServiceHandler(this: RunnerPlugin, message: AbortServiceMessage) { + console.error(`Host expects at least protocol version ${message.data.minVersion}, but we are on version ${Constant.PROTOCOL_VERSION}`); + this.__conduit.terminate(); + }], + [ServiceMessageType.ENTRY, function entryServiceHandler(this: RunnerPlugin, message: EntryServiceMessage) { + this.__evaluator.startEvaluator(message.data); + }] + ]); + + requestFile(fileName: string): Promise { + return this.__fileRpc.requestFile(fileName); + } + + async requestChunk(): Promise { + return (await this.__chunkQueue.receive()).chunk; + } + + async requestInput(): Promise { + const { message } = await this.__ioQueue.receive(); + return message; + } + + tryRequestInput(): string | undefined { + const out = this.__ioQueue.tryReceive(); + return out?.message; + } + + sendOutput(message: string): void { + this.__ioQueue.send({ message }); + } + + sendError(error: ConductorError): void { + this.__errorChannel.send({ error }); + } + + updateStatus(status: RunnerStatus, isActive: boolean): void { + this.__statusChannel.send({ status, isActive }); + } + + hostLoadPlugin(pluginName: string): void { + this.__serviceChannel.send(new PluginServiceMessage(pluginName)); + } + + registerPlugin(pluginClass: PluginClass, ...arg: Arg): NoInfer { + return this.__conduit.registerPlugin(pluginClass, ...arg); + } + + unregisterPlugin(plugin: IPlugin): void { + this.__conduit.unregisterPlugin(plugin); + } + + registerModule(moduleClass: ModuleClass): NoInfer { + if (!this.__isCompatibleWithModules) throw new ConductorInternalError("Evaluator has no data interface"); + return this.registerPlugin(moduleClass, this.__evaluator as IInterfacableEvaluator); + } + + unregisterModule(module: IModulePlugin): void { + this.unregisterPlugin(module); + } + + async importAndRegisterExternalPlugin(location: string, ...arg: any[]): Promise { + const pluginClass = await importExternalPlugin(location); + return this.registerPlugin(pluginClass as any, ...arg); + } + + async importAndRegisterExternalModule(location: string): Promise { + const moduleClass = await importExternalModule(location); + return this.registerModule(moduleClass); + } + + static readonly channelAttach = [InternalChannelName.FILE, InternalChannelName.CHUNK, InternalChannelName.SERVICE, InternalChannelName.STANDARD_IO, InternalChannelName.ERROR, InternalChannelName.STATUS]; + constructor( + conduit: IConduit, + [fileChannel, chunkChannel, serviceChannel, ioChannel, errorChannel, statusChannel]: IChannel[], + evaluatorClass: EvaluatorClass + ) { + this.__conduit = conduit; + this.__fileRpc = makeRpc<{}, IHostFileRpc>(fileChannel, {}); + this.__chunkQueue = new ChannelQueue(chunkChannel); + this.__serviceChannel = serviceChannel; + this.__ioQueue = new ChannelQueue(ioChannel); + this.__errorChannel = errorChannel; + this.__statusChannel = statusChannel; + + this.__serviceChannel.send(new HelloServiceMessage()); + this.__serviceChannel.subscribe(message => { + this.__serviceHandlers.get(message.type)?.call(this, message); + }); + + this.__evaluator = new evaluatorClass(this); + this.__isCompatibleWithModules = (this.__evaluator as IInterfacableEvaluator).hasDataInterface ?? false; + } +} diff --git a/src/conductor/runner/index.ts b/src/conductor/runner/index.ts new file mode 100644 index 0000000..419d4e7 --- /dev/null +++ b/src/conductor/runner/index.ts @@ -0,0 +1,6 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export { BasicEvaluator } from "./BasicEvaluator"; +export { RunnerPlugin } from "./RunnerPlugin"; diff --git a/src/conductor/runner/types/EvaluatorClass.ts b/src/conductor/runner/types/EvaluatorClass.ts new file mode 100644 index 0000000..ffeb673 --- /dev/null +++ b/src/conductor/runner/types/EvaluatorClass.ts @@ -0,0 +1,9 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { IEvaluator } from "./IEvaluator"; +import { IInterfacableEvaluator } from "./IInterfacableEvaluator"; +import { IRunnerPlugin } from "./IRunnerPlugin"; + +export type EvaluatorClass = new (conductor: IRunnerPlugin, ...arg: Arg) => IEvaluator | IInterfacableEvaluator; diff --git a/src/conductor/runner/types/IEvaluator.ts b/src/conductor/runner/types/IEvaluator.ts new file mode 100644 index 0000000..29a91ee --- /dev/null +++ b/src/conductor/runner/types/IEvaluator.ts @@ -0,0 +1,15 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +/** + * The IEvaluator interface exposes methods used by Conductor to interact with evaluators. + */ +export interface IEvaluator { + /** + * Starts this evaluator. + * @param entryPoint The entry point file to start running from. + * @returns A promise that resolves when the evaluator has terminated. + */ + startEvaluator(entryPoint: string): Promise; +} diff --git a/src/conductor/runner/types/IInterfacableEvaluator.ts b/src/conductor/runner/types/IInterfacableEvaluator.ts new file mode 100644 index 0000000..71572ae --- /dev/null +++ b/src/conductor/runner/types/IInterfacableEvaluator.ts @@ -0,0 +1,8 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { IDataHandler } from "../../types"; +import type { IEvaluator } from "./IEvaluator"; + +export type IInterfacableEvaluator = IEvaluator & IDataHandler; diff --git a/src/conductor/runner/types/IRunnerPlugin.ts b/src/conductor/runner/types/IRunnerPlugin.ts new file mode 100644 index 0000000..efa776b --- /dev/null +++ b/src/conductor/runner/types/IRunnerPlugin.ts @@ -0,0 +1,104 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { ConductorError } from "../../../common/errors"; +import type { IPlugin } from "../../../conduit"; +import { PluginClass } from "../../../conduit/types"; +import type { IModulePlugin } from "../../module"; +import { ModuleClass } from "../../module/types/ModuleClass"; +import type { Chunk, RunnerStatus } from "../../types"; + +export interface IRunnerPlugin extends IPlugin { + /** + * Request a file's contents. + * @param fileName The name of the file to request. + * @returns A promise resolving to the content of the requested file. + */ + requestFile(fileName: string): Promise; + + /** + * Request the next chunk to run. + * @returns A promise resolving to the next chunk. + */ + requestChunk(): Promise; + + /** + * Request for some input on standard-input. + * @returns A promise resolving to the input received. + */ + requestInput(): Promise; + + /** + * Try to request for some input on standard-input. + * @returns The input received, or undefined if there is currently no input. + */ + tryRequestInput(): string | undefined; + + /** + * Sends a message on standard-output. + * @param message The output message to send. + */ + sendOutput(message: string): void; + + /** + * Sends an error. + * @param error The error to send. + */ + sendError(error: ConductorError): void; + + /** + * Provide a status update of the runner. + * @param status The status to update. + * @param isActive Is the specified status currently active? + */ + updateStatus(status: RunnerStatus, isActive: boolean): void; + + /** + * Informs the host to load a plugin. + * @param pluginName The name of the plugin to load. + */ + hostLoadPlugin(pluginName: string): void; + + /** + * Registers a plugin with the conduit. + * @param pluginClass The plugin class to be registered. + * @param arg Arguments to be passed to pluginClass' constructor. + * @returns The registered plugin. + */ + registerPlugin(pluginClass: PluginClass, ...arg: Arg): NoInfer; + + /** + * Unregister a plugin from the conduit. + * @param plugin The plugin to be unregistered. + */ + unregisterPlugin(plugin: IPlugin): void; + + /** + * Registers an external module with the conduit, and links it with the evaluator. + * @param moduleClass The module class to be registered. + * @returns The registered module. + */ + registerModule(moduleClass: ModuleClass): T; + + /** + * Unregisters an external module from the conduit, and unlinks it from the evaluator. + * @param module The module to be unregistered. + */ + unregisterModule(module: IModulePlugin): void; + + /** + * Imports an external plugin and registers it with the conduit. + * @param location The location of the external plugin. + * @param arg Arguments to be passed to the external plugin's constructor. + * @returns The imported plugin. + */ + importAndRegisterExternalPlugin(location: string, ...arg: any[]): Promise; + + /** + * Imports an external module and registers it with the conduit. + * @param location The location of the external module. + * @returns The imported module. + */ + importAndRegisterExternalModule(location: string): Promise; +} diff --git a/src/conductor/runner/types/PyEvaluator.ts b/src/conductor/runner/types/PyEvaluator.ts new file mode 100644 index 0000000..2c08d6a --- /dev/null +++ b/src/conductor/runner/types/PyEvaluator.ts @@ -0,0 +1,47 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { runInContext } from "../../../"; +import { Context } from "../../../cse-machine/context"; +import { BasicEvaluator } from "../BasicEvaluator"; +import { IRunnerPlugin } from "./IRunnerPlugin"; +import { IOptions } from "../../../"; +import { Finished } from "../../../types"; + +const defaultContext = new Context(); +const defaultOptions: IOptions = { + isPrelude: false, + envSteps: 100000, + stepLimit: 100000 +}; + +export class PyEvaluator extends BasicEvaluator { + private context: Context; + private options: IOptions; + + constructor(conductor: IRunnerPlugin) { + super(conductor); + this.context = defaultContext; + this.options = defaultOptions; + } + + async evaluateChunk(chunk: string): Promise { + try { + const result = await runInContext( + chunk, // Code + this.context, + this.options + ); + this.conductor.sendOutput(`${(result as Finished).representation.toString((result as Finished).value)}`); + } catch (error) { + this.conductor.sendOutput(`Error: ${error instanceof Error ? error.message : error}`); + } + } +} + +// runInContext +// IOptions +// Context +// BasicEvaluator; +// IRunnerPlugin \ No newline at end of file diff --git a/src/conductor/runner/types/index.ts b/src/conductor/runner/types/index.ts new file mode 100644 index 0000000..042469c --- /dev/null +++ b/src/conductor/runner/types/index.ts @@ -0,0 +1,8 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export type { EvaluatorClass } from "./EvaluatorClass"; +export type { IEvaluator } from "./IEvaluator"; +export type { IInterfacableEvaluator } from "./IInterfacableEvaluator"; +export type { IRunnerPlugin } from "./IRunnerPlugin"; diff --git a/src/conductor/runner/util/index.ts b/src/conductor/runner/util/index.ts new file mode 100644 index 0000000..a7e1ca2 --- /dev/null +++ b/src/conductor/runner/util/index.ts @@ -0,0 +1 @@ +export { initialise } from "./initialise"; diff --git a/src/conductor/runner/util/initialise.ts b/src/conductor/runner/util/initialise.ts new file mode 100644 index 0000000..1b478a6 --- /dev/null +++ b/src/conductor/runner/util/initialise.ts @@ -0,0 +1,15 @@ +import { RunnerPlugin } from ".."; +import { Conduit, IConduit, ILink } from "../../../conduit"; +import { EvaluatorClass, IRunnerPlugin } from "../types"; + +/** + * Initialise this runner with the evaluator to be used. + * @param evaluatorClass The Evaluator to be used on this runner. + * @param link The underlying communication link. + * @returns The initialised `runnerPlugin` and `conduit`. + */ +export function initialise(evaluatorClass: EvaluatorClass, link: ILink = self as ILink): { runnerPlugin: IRunnerPlugin, conduit: IConduit } { + const conduit = new Conduit(link, false); + const runnerPlugin = conduit.registerPlugin(RunnerPlugin, evaluatorClass); + return { runnerPlugin, conduit }; +} diff --git a/src/conductor/stdlib/index.ts b/src/conductor/stdlib/index.ts new file mode 100644 index 0000000..717a06c --- /dev/null +++ b/src/conductor/stdlib/index.ts @@ -0,0 +1,14 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { StdlibFunction } from "../types"; +import { accumulate, is_list, length } from "./list"; + +export const stdlib = { + is_list: is_list, + accumulate: accumulate, + length: length +} satisfies Record>; + +export { accumulate }; diff --git a/src/conductor/stdlib/list/accumulate.ts b/src/conductor/stdlib/list/accumulate.ts new file mode 100644 index 0000000..317ec38 --- /dev/null +++ b/src/conductor/stdlib/list/accumulate.ts @@ -0,0 +1,26 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { ClosureIdentifier, DataType, IDataHandler, TypedValue, List } from "../../types" + +/** + * Accumulates a Closure over a List. + * + * The Closure is applied in a right-to-left order - the first application + * will be on the last element of the list and the given initial value. + * @param op The Closure to use as an accumulator over the List. + * @param initial The initial typed value (that is, the result of accumulating an empty List). + * @param sequence The List to be accumulated over. + * @param resultType The (expected) type of the result. + * @returns A Promise resolving to the result of accumulating the Closure over the List. + */ +export async function accumulate>(this: IDataHandler, op: ClosureIdentifier, initial: TypedValue, sequence: List, resultType: T): Promise> { + const vec = this.list_to_vec(sequence); + let result = initial; + for (let i = vec.length - 1; i >= 0; --i) { + result = await this.closure_call(op, [vec[i], result], resultType); + } + + return result; +} diff --git a/src/conductor/stdlib/list/index.ts b/src/conductor/stdlib/list/index.ts new file mode 100644 index 0000000..86cfee5 --- /dev/null +++ b/src/conductor/stdlib/list/index.ts @@ -0,0 +1,9 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export { accumulate } from "./accumulate"; +export { is_list } from "./is_list"; +export { length } from "./length"; +export { list_to_vec } from "./list_to_vec"; +export { list } from "./list"; diff --git a/src/conductor/stdlib/list/is_list.ts b/src/conductor/stdlib/list/is_list.ts new file mode 100644 index 0000000..f48f411 --- /dev/null +++ b/src/conductor/stdlib/list/is_list.ts @@ -0,0 +1,20 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { DataType, IDataHandler, List } from "../../types" + +/** + * Checks if a List is a true list (`tail(tail...(xs))` is empty-list). + * @param xs The List to check. + * @returns true if the provided List is a true list. + */ +export function is_list(this: IDataHandler, xs: List): boolean { + if (xs === null) return true; // TODO: figure out some way to avoid JS value comparison + while (true) { + const tail = this.pair_tail(xs); + if (tail.type === DataType.EMPTY_LIST) return true; + if (tail.type !== DataType.PAIR) return false; + xs = tail.value; + } +} diff --git a/src/conductor/stdlib/list/length.ts b/src/conductor/stdlib/list/length.ts new file mode 100644 index 0000000..ff6cc30 --- /dev/null +++ b/src/conductor/stdlib/list/length.ts @@ -0,0 +1,23 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { EvaluatorTypeError } from "../../../common/errors"; +import { DataType, IDataHandler, List } from "../../types" + +/** + * Gets the length of a List. + * @param xs The List to get the length of. + * @returns The length of the List. + */ +export function length(this: IDataHandler, xs: List): number { + let length = 0; + if (xs === null) return length; // TODO: figure out some way to avoid JS value comparison + while (true) { + length++; + const tail = this.pair_tail(xs); + if (tail.type === DataType.EMPTY_LIST) return length; + if (tail.type !== DataType.PAIR) throw new EvaluatorTypeError("Input is not a list", DataType[DataType.LIST], DataType[tail.type]); + xs = tail.value; + } +} diff --git a/src/conductor/stdlib/list/list.ts b/src/conductor/stdlib/list/list.ts new file mode 100644 index 0000000..11ef921 --- /dev/null +++ b/src/conductor/stdlib/list/list.ts @@ -0,0 +1,20 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { DataType, IDataHandler, TypedValue } from "../../types"; +import { mList } from "../../util/mList"; + +/** + * Creates a new List from given elements. + * @param elements The elements of the List, given as typed values. + * @returns The newly created List. + */ +export function list(this: IDataHandler, ...elements: TypedValue[]): TypedValue { + let theList: TypedValue = mList(null); + for (let i = elements.length - 1; i >= 0; --i) { + const p = mList(this.pair_make(elements[i], theList)); + theList = p; + } + return theList; +} diff --git a/src/conductor/stdlib/list/list_to_vec.ts b/src/conductor/stdlib/list/list_to_vec.ts new file mode 100644 index 0000000..6240866 --- /dev/null +++ b/src/conductor/stdlib/list/list_to_vec.ts @@ -0,0 +1,18 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { EvaluatorTypeError } from "../../../common/errors"; +import { DataType, IDataHandler, List, TypedValue } from "../../types"; + +export function list_to_vec(this: IDataHandler, xs: List): TypedValue[] { + const vec: TypedValue[] = []; + if (xs === null) return vec; + while (true) { + vec.push(this.pair_head(xs)); + const tail = this.pair_tail(xs); + if (tail.type === DataType.EMPTY_LIST) return vec; + if (tail.type !== DataType.PAIR) throw new EvaluatorTypeError("Input is not a list", DataType[DataType.LIST], DataType[tail.type]); + xs = tail.value; + } +} diff --git a/src/conductor/stdlib/util/array_assert.ts b/src/conductor/stdlib/util/array_assert.ts new file mode 100644 index 0000000..8b5cb70 --- /dev/null +++ b/src/conductor/stdlib/util/array_assert.ts @@ -0,0 +1,18 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { EvaluatorTypeError } from "../../../common/errors"; +import { ArrayIdentifier, DataType, IDataHandler } from "../../types"; +import { isSameType } from "../../util"; + +export function array_assert(this: IDataHandler, a: ArrayIdentifier, type?: T, length?: number): asserts a is ArrayIdentifier { + if (type) { + const t = this.array_type(a); + if (!isSameType(t, type)) throw new EvaluatorTypeError("Array type assertion failure", DataType[type], DataType[t]); + } + if (length) { + const l = this.array_length(a); + if (l !== length) throw new EvaluatorTypeError("Array length assertion failure", String(length), String(l)); + } +} diff --git a/src/conductor/stdlib/util/closure_arity_assert.ts b/src/conductor/stdlib/util/closure_arity_assert.ts new file mode 100644 index 0000000..e87cfdf --- /dev/null +++ b/src/conductor/stdlib/util/closure_arity_assert.ts @@ -0,0 +1,13 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { EvaluatorTypeError } from "../../../common/errors"; +import { IDataHandler, ClosureIdentifier, DataType } from "../../types"; + +export function closure_arity_assert(this: IDataHandler, c: ClosureIdentifier, arity: number): void { + const a = this.closure_arity(c); + if (this.closure_is_vararg(c) ? arity < a : arity !== a) { + throw new EvaluatorTypeError("Closure arity assertion failure", String(arity), String(a)); + } +} diff --git a/src/conductor/stdlib/util/index.ts b/src/conductor/stdlib/util/index.ts new file mode 100644 index 0000000..8081b9c --- /dev/null +++ b/src/conductor/stdlib/util/index.ts @@ -0,0 +1,7 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export { array_assert } from "./array_assert"; +export { closure_arity_assert } from "./closure_arity_assert"; +export { pair_assert } from "./pair_assert"; diff --git a/src/conductor/stdlib/util/pair_assert.ts b/src/conductor/stdlib/util/pair_assert.ts new file mode 100644 index 0000000..880795a --- /dev/null +++ b/src/conductor/stdlib/util/pair_assert.ts @@ -0,0 +1,18 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { EvaluatorTypeError } from "../../../common/errors"; +import { DataType, IDataHandler, PairIdentifier } from "../../types"; +import { isSameType } from "../../util"; + +export function pair_assert(this: IDataHandler, p: PairIdentifier, headType?: DataType, tailType?: DataType): void { + if (headType) { + const head = this.pair_head(p); + if (!isSameType(head.type, headType)) throw new EvaluatorTypeError("Pair head assertion failure", DataType[headType], DataType[head.type]); + } + if (tailType) { + const tail = this.pair_tail(p); + if (!isSameType(tail.type, tailType)) throw new EvaluatorTypeError("Pair tail assertion failure", DataType[tailType], DataType[tail.type]); + } +} diff --git a/src/conductor/strings/InternalChannelName.ts b/src/conductor/strings/InternalChannelName.ts new file mode 100644 index 0000000..ee4f736 --- /dev/null +++ b/src/conductor/strings/InternalChannelName.ts @@ -0,0 +1,12 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export const enum InternalChannelName { + CHUNK = "__chunk", + FILE = "__file_rpc", + SERVICE = "__service", + STANDARD_IO = "__stdio", + ERROR = "__error", + STATUS = "__status", +}; diff --git a/src/conductor/strings/InternalPluginName.ts b/src/conductor/strings/InternalPluginName.ts new file mode 100644 index 0000000..752e8d9 --- /dev/null +++ b/src/conductor/strings/InternalPluginName.ts @@ -0,0 +1,8 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export const enum InternalPluginName { + HOST_MAIN = "__host_main", + RUNNER_MAIN = "__runner_main" +}; diff --git a/src/conductor/strings/index.ts b/src/conductor/strings/index.ts new file mode 100644 index 0000000..4554c5c --- /dev/null +++ b/src/conductor/strings/index.ts @@ -0,0 +1,6 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export { InternalChannelName } from "./InternalChannelName"; +export { InternalPluginName } from "./InternalPluginName"; diff --git a/src/conductor/types/Chunk.ts b/src/conductor/types/Chunk.ts new file mode 100644 index 0000000..548224d --- /dev/null +++ b/src/conductor/types/Chunk.ts @@ -0,0 +1,6 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +/** A chunk of code. */ +export type Chunk = string; diff --git a/src/conductor/types/IChunkMessage.ts b/src/conductor/types/IChunkMessage.ts new file mode 100644 index 0000000..1cb6db6 --- /dev/null +++ b/src/conductor/types/IChunkMessage.ts @@ -0,0 +1,10 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { Chunk } from "./Chunk"; + +export interface IChunkMessage { + id: number; + chunk: Chunk; +} diff --git a/src/conductor/types/IErrorMessage.ts b/src/conductor/types/IErrorMessage.ts new file mode 100644 index 0000000..d05d80e --- /dev/null +++ b/src/conductor/types/IErrorMessage.ts @@ -0,0 +1,9 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { ConductorError } from "../../common/errors"; + +export interface IErrorMessage { + error: ConductorError; +} diff --git a/src/conductor/types/IIOMessage.ts b/src/conductor/types/IIOMessage.ts new file mode 100644 index 0000000..55de019 --- /dev/null +++ b/src/conductor/types/IIOMessage.ts @@ -0,0 +1,8 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export interface IIOMessage { + // stream: number; + message: string; +} diff --git a/src/conductor/types/IServiceMessage.ts b/src/conductor/types/IServiceMessage.ts new file mode 100644 index 0000000..93dbaf3 --- /dev/null +++ b/src/conductor/types/IServiceMessage.ts @@ -0,0 +1,10 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { ServiceMessageType } from "./ServiceMessageType"; + +export interface IServiceMessage { + readonly type: ServiceMessageType; + readonly data?: any; +} diff --git a/src/conductor/types/IStatusMessage.ts b/src/conductor/types/IStatusMessage.ts new file mode 100644 index 0000000..a2144da --- /dev/null +++ b/src/conductor/types/IStatusMessage.ts @@ -0,0 +1,10 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { RunnerStatus } from "./RunnerStatus"; + +export interface IStatusMessage { + status: RunnerStatus; + isActive: boolean; +} diff --git a/src/conductor/types/RunnerStatus.ts b/src/conductor/types/RunnerStatus.ts new file mode 100644 index 0000000..886523d --- /dev/null +++ b/src/conductor/types/RunnerStatus.ts @@ -0,0 +1,13 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export const enum RunnerStatus { + ONLINE, // Runner is online + EVAL_READY, // Evaluator is ready + RUNNING, // I am running some code + WAITING, // I am waiting for inputs + BREAKPOINT, // I have reached a debug breakpoint + STOPPED, // I have exited, crashed, etc.; the environment is no longer valid + ERROR, // I have stopped unexpectedly +}; diff --git a/src/conductor/types/ServiceMessageType.ts b/src/conductor/types/ServiceMessageType.ts new file mode 100644 index 0000000..f8ad4d7 --- /dev/null +++ b/src/conductor/types/ServiceMessageType.ts @@ -0,0 +1,17 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export const enum ServiceMessageType { + /** A handshake message. See `HelloServiceMessage`. */ + HELLO = 0, + + /** Abort the connection, due to incompatible protocol versions. See `AbortServiceMessage`. */ + ABORT = 1, + + /** The evaluation entry point, sent from the host. See `EntryServiceMessage`. */ + ENTRY = 2, + + /** Plugin advisory sent from the runner so the host may load a corresponding plugin. See `PluginServiceMessage`. */ + PLUGIN = 3, +}; diff --git a/src/conductor/types/index.ts b/src/conductor/types/index.ts new file mode 100644 index 0000000..b6e0037 --- /dev/null +++ b/src/conductor/types/index.ts @@ -0,0 +1,14 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export type { Chunk } from "./Chunk"; +export type { IChunkMessage } from "./IChunkMessage"; +export type { IErrorMessage } from "./IErrorMessage"; +export type { IIOMessage } from "./IIOMessage"; +export type { IServiceMessage } from "./IServiceMessage"; +export type { IStatusMessage } from "./IStatusMessage"; +export { RunnerStatus } from "./RunnerStatus"; +export { ServiceMessageType } from "./ServiceMessageType"; +export * from "./moduleInterface"; +export * from "./serviceMessages"; diff --git a/src/conductor/types/moduleInterface/ArrayIdentifier.ts b/src/conductor/types/moduleInterface/ArrayIdentifier.ts new file mode 100644 index 0000000..675d265 --- /dev/null +++ b/src/conductor/types/moduleInterface/ArrayIdentifier.ts @@ -0,0 +1,9 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { DataType } from "./DataType"; +import type { Identifier } from "./Identifier"; + +/** An identifier for an extern array. */ +export type ArrayIdentifier = Identifier & { __brand: "array", __type: T }; // apply branding so it's harder to mix identifiers up diff --git a/src/conductor/types/moduleInterface/ClosureIdentifier.ts b/src/conductor/types/moduleInterface/ClosureIdentifier.ts new file mode 100644 index 0000000..ef986f7 --- /dev/null +++ b/src/conductor/types/moduleInterface/ClosureIdentifier.ts @@ -0,0 +1,9 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { DataType } from "./DataType"; +import type { Identifier } from "./Identifier"; + +/** An identifier for an extern closure. */ +export type ClosureIdentifier = Identifier & { __brand: "closure", __ret: T }; // apply branding so it's harder to mix identifiers up diff --git a/src/conductor/types/moduleInterface/DataType.ts b/src/conductor/types/moduleInterface/DataType.ts new file mode 100644 index 0000000..761c975 --- /dev/null +++ b/src/conductor/types/moduleInterface/DataType.ts @@ -0,0 +1,35 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export enum DataType { + /** The return type of functions with no returned value. As a convention, the associated JS value is undefined. */ + VOID = 0, + + /** A Boolean value. */ + BOOLEAN = 1, + + /** A numerical value. */ + NUMBER = 2, + + /** An immutable string of characters. */ + CONST_STRING = 3, + + /** The empty list. As a convention, the associated JS value is null. */ + EMPTY_LIST = 4, + + /** A pair of values. Reference type. */ + PAIR = 5, + + /** An array of values of a single type. Reference type. */ + ARRAY = 6, + + /** A value that can be called with fixed arity. Reference type. */ + CLOSURE = 7, + + /** An opaque value that cannot be manipulated from user code. */ + OPAQUE = 8, + + /** A list (either a pair or the empty list). */ + LIST = 9, +}; diff --git a/src/conductor/types/moduleInterface/ExternCallable.ts b/src/conductor/types/moduleInterface/ExternCallable.ts new file mode 100644 index 0000000..55458fc --- /dev/null +++ b/src/conductor/types/moduleInterface/ExternCallable.ts @@ -0,0 +1,14 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { DataType } from "./DataType"; +import type { ExternTypeOf } from "./ExternTypeOf"; +import type { IFunctionSignature } from "./IFunctionSignature"; + +type DataTypeMap = { + [Idx in keyof T]: ExternTypeOf +}; + +/** The expected function type based on an IFunctionSignature. */ +export type ExternCallable = (...args: DataTypeMap) => ExternTypeOf | Promise>; diff --git a/src/conductor/types/moduleInterface/ExternTypeOf.ts b/src/conductor/types/moduleInterface/ExternTypeOf.ts new file mode 100644 index 0000000..b2bc835 --- /dev/null +++ b/src/conductor/types/moduleInterface/ExternTypeOf.ts @@ -0,0 +1,26 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { ArrayIdentifier } from "./ArrayIdentifier"; +import type { ClosureIdentifier } from "./ClosureIdentifier"; +import { DataType } from "./DataType"; +import { List } from "./List"; +import type { OpaqueIdentifier } from "./OpaqueIdentifier"; +import type { PairIdentifier } from "./PairIdentifier"; + +type typeMap = { + [DataType.VOID]: void; + [DataType.BOOLEAN]: boolean; + [DataType.NUMBER]: number; + [DataType.CONST_STRING]: string; + [DataType.EMPTY_LIST]: null; + [DataType.PAIR]: PairIdentifier; + [DataType.ARRAY]: ArrayIdentifier; + [DataType.CLOSURE]: ClosureIdentifier; + [DataType.OPAQUE]: OpaqueIdentifier; + [DataType.LIST]: List; +} + +/** Maps the Conductor DataTypes to their corresponding native types. */ +export type ExternTypeOf = T extends DataType ? typeMap[T] : never; diff --git a/src/conductor/types/moduleInterface/ExternValue.ts b/src/conductor/types/moduleInterface/ExternValue.ts new file mode 100644 index 0000000..99c3161 --- /dev/null +++ b/src/conductor/types/moduleInterface/ExternValue.ts @@ -0,0 +1,8 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { ArrayIdentifier, ClosureIdentifier, DataType, Identifier, NativeValue, OpaqueIdentifier, PairIdentifier } from "."; + +/** A valid extern value. */ +export type ExternValue = NativeValue | Identifier | PairIdentifier | ArrayIdentifier | ClosureIdentifier | OpaqueIdentifier; diff --git a/src/conductor/types/moduleInterface/IDataHandler.ts b/src/conductor/types/moduleInterface/IDataHandler.ts new file mode 100644 index 0000000..8e7aff4 --- /dev/null +++ b/src/conductor/types/moduleInterface/IDataHandler.ts @@ -0,0 +1,207 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { ArrayIdentifier, ClosureIdentifier, DataType, ExternCallable, Identifier, IFunctionSignature, List, OpaqueIdentifier, PairIdentifier, TypedValue } from "."; + +export interface IDataHandler { + readonly hasDataInterface: true; + + ///// Data Handling Functions + + /** + * Makes a new Pair. + * @param head The typed value to be the head of the new Pair. + * @param tail The typed value to be the tail of the new Pair. + * @returns An identifier to the new Pair. + */ + pair_make(head: TypedValue, tail: TypedValue): PairIdentifier; + + /** + * Gets the typed value in the head of a Pair. + * @param p The Pair to retrieve the head of. + * @returns The typed value in the head of the Pair. + */ + pair_head(p: PairIdentifier): TypedValue; + + /** + * Sets the head of a Pair. + * @param p The Pair to set the head of. + * @param tv The typed value to set the head of the Pair to. + */ + pair_sethead(p: PairIdentifier, tv: TypedValue): void; + + /** + * Gets the typed value in the tail of a Pair. + * @param p The Pair to retrieve the tail of. + * @returns The typed value in the tail of the Pair. + */ + pair_tail(p: PairIdentifier): TypedValue; + + /** + * Sets the tail of a Pair. + * @param p The Pair to set the tail of. + * @param tv The typed value to set the tail of the Pair to. + */ + pair_settail(p: PairIdentifier, tv: TypedValue): void; + + /** + * Asserts the type of a Pair. + * @param p The Pair to assert the type of. + * @param headType The expected type of the head of the Pair. + * @param tailType The expected type of the tail of the Pair. + * @throws If the Pair's type is not as expected. + */ + pair_assert(p: PairIdentifier, headType?: DataType, tailType?: DataType): void; + + /** + * Makes a new Array. + * + * Creation of untyped arrays (with type `VOID`) should be avoided. + * @param t The type of the elements of the Array + * @param len The length of the Array + * @param init An optional initial typed value for the elements of the Array + * @returns An identifier to the new Array. + */ + array_make(t: T, len: number, init?: TypedValue>): ArrayIdentifier>; + + /** + * Gets the length of an Array. + * @param a The Array to retrieve the length of. + * @returns The length of the given Array. + */ + array_length(a: ArrayIdentifier): number; + + /** + * Gets the typed value at a specific index of an Array. + * Arrays are 0-indexed. + * @param a The Array to retrieve the value from. + * @param idx The index of the value wanted. + * @returns The typed value at the given index of the given Array. + */ + array_get(a: ArrayIdentifier, idx: number): TypedValue; + array_get(a: ArrayIdentifier, idx: number): TypedValue>; + + /** + * Gets the type of the elements of an Array. + * + * If the Array is untyped, `VOID` is returned. + * @param a The Array to retrieve the element type of. + * @returns The type of the elements of the Array. + */ + array_type(a: ArrayIdentifier): NoInfer; + + /** + * Sets a value at a specific index of an Array. + * Arrays are 0-indexed. + * @param a The Array to be modified. + * @param idx The index to be modified. + * @param tv The new typed value at the given index of the given Array. + * @throws If the array is typed and v's type does not match the Array's type. + */ + array_set(a: ArrayIdentifier, idx: number, tv: TypedValue): void; + array_set(a: ArrayIdentifier, idx: number, tv: TypedValue>): void; + + /** + * Asserts the type and/or length of an Array. + * @param a The Array to assert. + * @param type The expected type of the elements of the Array. + * @param length The expected length of the Array. + * @throws If the Array's type is not as expected. + */ + array_assert(a: ArrayIdentifier, type?: T, length?: number): asserts a is ArrayIdentifier>; + + /** + * Makes a new Closure. + * @param sig The signature of the new Closure. + * @param func A callback to be called when the Closure is called. + * @param dependsOn An optional array of Identifiers the Closure will depend on. + * @returns An identifier to the new Closure. + */ + closure_make(sig: T, func: ExternCallable, dependsOn?: (Identifier | null)[]): ClosureIdentifier; + + /** + * Checks if a Closure accepts variable number of arguments. + * @param c The Closure to check. + * @returns `true` if the Closure accepts variable number of arguments. + */ + closure_is_vararg(c: ClosureIdentifier): boolean; + + /** + * Gets the arity (number of parameters) of a Closure. + * For vararg Closures, the arity is the minimum number of parameters required. + * @param c The Closure to get the arity of. + * @returns The arity of the Closure. + */ + closure_arity(c: ClosureIdentifier): number; + + /** + * Calls a Closure and checks the type of the returned value. + * @param c The Closure to be called. + * @param args An array of typed arguments to be passed to the Closure. + * @param returnType The expected type of the returned value. + * @returns The returned typed value. + */ + closure_call(c: ClosureIdentifier, args: TypedValue[], returnType: T): Promise>>; + + /** + * Calls a Closure of known return type. + * @param c The Closure to be called. + * @param args An array of typed arguments to be passed to the Closure. + * @returns The returned typed value. + */ + closure_call_unchecked(c: ClosureIdentifier, args: TypedValue[]): Promise>>; + + /** + * Asserts the arity of a Closure. + * @param c The Closure to assert the arity of. + * @param arity The expected arity of the Closure. + * @throws If the Closure's arity is not as expected. + */ + closure_arity_assert(c: ClosureIdentifier, arity: number): void; + + /** + * Makes a new Opaque object. + * @param v The value to be stored under this Opaque object. + * @param immutable Mark this Opaque object as immutable. Mutable Opaque objects are not rollback-friendly, + * and evaluators should disable any rollback functionality upon receiving such an object. + * @returns An identifier to the new Opaque object. + */ + opaque_make(v: any, immutable?: boolean): OpaqueIdentifier; + + /** + * Gets the value stored under an Opaque object. + * @param o The identifier to the Opaque object. + * @returns The value stored under this new Opaque object. + */ + opaque_get(o: OpaqueIdentifier): any; + + /** + * Update the value stored under an Opaque object. + * @param o The identifier to the Opaque object. + * @param v The new value to store under this Opaque object. + */ + opaque_update(o: OpaqueIdentifier, v: any): void; + + /** + * Ties the lifetime of the dependee to the dependent. + * @param dependent The object that requires the existence of the dependee. + * @param dependee The object whose existence is required by the dependent. + */ + tie(dependent: Identifier, dependee: Identifier | null): void; + + /** + * Unties the lifetime of the dependee from the dependent. + * @param dependent The tied dependent object. + * @param dependee The tied dependee object. + */ + untie(dependent: Identifier, dependee: Identifier | null): void; + + ///// Standard library functions + + list(...elements: TypedValue[]): TypedValue; + is_list(xs: List): boolean; + list_to_vec(xs: List): TypedValue[]; + accumulate>(op: ClosureIdentifier, initial: TypedValue, sequence: List, resultType: T): Promise>; + length(xs: List): number; +} diff --git a/src/conductor/types/moduleInterface/IFunctionSignature.ts b/src/conductor/types/moduleInterface/IFunctionSignature.ts new file mode 100644 index 0000000..fc552f9 --- /dev/null +++ b/src/conductor/types/moduleInterface/IFunctionSignature.ts @@ -0,0 +1,16 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { DataType } from "./DataType"; + +export interface IFunctionSignature { + /** The name of this function or closure. */ + name?: string; + + /** The parameter types of this function or closure. */ + args: readonly DataType[]; + + /** The type of the return value from this function or closure. */ + returnType: DataType; +} diff --git a/src/conductor/types/moduleInterface/Identifier.ts b/src/conductor/types/moduleInterface/Identifier.ts new file mode 100644 index 0000000..3b43b28 --- /dev/null +++ b/src/conductor/types/moduleInterface/Identifier.ts @@ -0,0 +1,6 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +/** An identifier to an extern value. */ +export type Identifier = number; // we want number here so evaluators do not have to specifically cast to it diff --git a/src/conductor/types/moduleInterface/List.ts b/src/conductor/types/moduleInterface/List.ts new file mode 100644 index 0000000..14b52fb --- /dev/null +++ b/src/conductor/types/moduleInterface/List.ts @@ -0,0 +1,8 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { PairIdentifier } from "./PairIdentifier"; + +/** Either an identifier for a extern pair, or a null (empty-list) value. */ +export type List = PairIdentifier | null; diff --git a/src/conductor/types/moduleInterface/NativeValue.ts b/src/conductor/types/moduleInterface/NativeValue.ts new file mode 100644 index 0000000..6990bb2 --- /dev/null +++ b/src/conductor/types/moduleInterface/NativeValue.ts @@ -0,0 +1,6 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +/** A value that can be expressed with JS primitives. */ +export type NativeValue = undefined | boolean | number | string | null; diff --git a/src/conductor/types/moduleInterface/OpaqueIdentifier.ts b/src/conductor/types/moduleInterface/OpaqueIdentifier.ts new file mode 100644 index 0000000..f6e9f25 --- /dev/null +++ b/src/conductor/types/moduleInterface/OpaqueIdentifier.ts @@ -0,0 +1,8 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { Identifier } from "./Identifier"; + +/** An identifier for an extern opaque value. */ +export type OpaqueIdentifier = Identifier & { __brand: "opaque" }; // apply branding so it's harder to mix identifiers up diff --git a/src/conductor/types/moduleInterface/PairIdentifier.ts b/src/conductor/types/moduleInterface/PairIdentifier.ts new file mode 100644 index 0000000..df34e47 --- /dev/null +++ b/src/conductor/types/moduleInterface/PairIdentifier.ts @@ -0,0 +1,8 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { Identifier } from "./Identifier"; + +/** An identifier for an extern pair. */ +export type PairIdentifier = Identifier & { __brand: "pair" }; // apply branding so it's harder to mix identifiers up diff --git a/src/conductor/types/moduleInterface/StdlibFunction.ts b/src/conductor/types/moduleInterface/StdlibFunction.ts new file mode 100644 index 0000000..7b85daf --- /dev/null +++ b/src/conductor/types/moduleInterface/StdlibFunction.ts @@ -0,0 +1,7 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { IDataHandler } from "./IDataHandler"; + +export type StdlibFunction = (this: IDataHandler, ...args: Arg) => Ret; diff --git a/src/conductor/types/moduleInterface/TypedValue.ts b/src/conductor/types/moduleInterface/TypedValue.ts new file mode 100644 index 0000000..965a13a --- /dev/null +++ b/src/conductor/types/moduleInterface/TypedValue.ts @@ -0,0 +1,14 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { DataType } from "./DataType"; +import type { ExternTypeOf } from "./ExternTypeOf"; + +interface ITypedValue { + type: T; + value: ExternTypeOf; +} + +// export a type instead to benefit from distributive conditional type +export type TypedValue = T extends DataType ? ITypedValue : never; diff --git a/src/conductor/types/moduleInterface/index.ts b/src/conductor/types/moduleInterface/index.ts new file mode 100644 index 0000000..a937716 --- /dev/null +++ b/src/conductor/types/moduleInterface/index.ts @@ -0,0 +1,19 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export type { ArrayIdentifier } from "./ArrayIdentifier"; +export type { ClosureIdentifier } from "./ClosureIdentifier"; +export { DataType } from "./DataType"; +export type { ExternCallable } from "./ExternCallable"; +export type { ExternTypeOf } from "./ExternTypeOf"; +export type { ExternValue } from "./ExternValue"; +export type { IDataHandler } from "./IDataHandler"; +export type { Identifier } from "./Identifier"; +export type { IFunctionSignature } from "./IFunctionSignature"; +export type { List } from "./List"; +export type { NativeValue } from "./NativeValue"; +export type { OpaqueIdentifier } from "./OpaqueIdentifier"; +export type { PairIdentifier } from "./PairIdentifier"; +export type { StdlibFunction } from "./StdlibFunction"; +export type { TypedValue } from "./TypedValue"; diff --git a/src/conductor/types/serviceMessages/AbortServiceMessage.ts b/src/conductor/types/serviceMessages/AbortServiceMessage.ts new file mode 100644 index 0000000..60548fa --- /dev/null +++ b/src/conductor/types/serviceMessages/AbortServiceMessage.ts @@ -0,0 +1,14 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { IServiceMessage } from "../IServiceMessage"; +import { ServiceMessageType } from "../ServiceMessageType"; + +export class AbortServiceMessage implements IServiceMessage { + readonly type = ServiceMessageType.ABORT; + readonly data: {minVersion: number}; + constructor(minVersion: number) { + this.data = {minVersion: minVersion}; + } +} diff --git a/src/conductor/types/serviceMessages/EntryServiceMessage.ts b/src/conductor/types/serviceMessages/EntryServiceMessage.ts new file mode 100644 index 0000000..55cc19e --- /dev/null +++ b/src/conductor/types/serviceMessages/EntryServiceMessage.ts @@ -0,0 +1,14 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { IServiceMessage } from "../IServiceMessage"; +import { ServiceMessageType } from "../ServiceMessageType"; + +export class EntryServiceMessage implements IServiceMessage { + readonly type = ServiceMessageType.ENTRY; + readonly data: string; + constructor(entryPoint: string) { + this.data = entryPoint; + } +} diff --git a/src/conductor/types/serviceMessages/HelloServiceMessage.ts b/src/conductor/types/serviceMessages/HelloServiceMessage.ts new file mode 100644 index 0000000..fab2bc0 --- /dev/null +++ b/src/conductor/types/serviceMessages/HelloServiceMessage.ts @@ -0,0 +1,12 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { Constant } from "../../../common/Constant"; +import type { IServiceMessage } from "../IServiceMessage"; +import { ServiceMessageType } from "../ServiceMessageType"; + +export class HelloServiceMessage implements IServiceMessage { + readonly type = ServiceMessageType.HELLO; + readonly data = { version: Constant.PROTOCOL_VERSION }; +} diff --git a/src/conductor/types/serviceMessages/PluginServiceMessage.ts b/src/conductor/types/serviceMessages/PluginServiceMessage.ts new file mode 100644 index 0000000..02a3f14 --- /dev/null +++ b/src/conductor/types/serviceMessages/PluginServiceMessage.ts @@ -0,0 +1,14 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { IServiceMessage } from "../IServiceMessage"; +import { ServiceMessageType } from "../ServiceMessageType"; + +export class PluginServiceMessage implements IServiceMessage { + readonly type = ServiceMessageType.PLUGIN; + readonly data: string; + constructor(pluginName: string) { + this.data = pluginName; + } +} diff --git a/src/conductor/types/serviceMessages/index.ts b/src/conductor/types/serviceMessages/index.ts new file mode 100644 index 0000000..de1b776 --- /dev/null +++ b/src/conductor/types/serviceMessages/index.ts @@ -0,0 +1,8 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export { AbortServiceMessage } from "./AbortServiceMessage"; +export { EntryServiceMessage } from "./EntryServiceMessage"; +export { HelloServiceMessage } from "./HelloServiceMessage"; +export { PluginServiceMessage } from "./PluginServiceMessage"; diff --git a/src/conductor/util/index.ts b/src/conductor/util/index.ts new file mode 100644 index 0000000..91da694 --- /dev/null +++ b/src/conductor/util/index.ts @@ -0,0 +1,16 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export { isReferenceType } from "./isReferenceType"; +export { isSameType } from "./isSameType"; +export { mArray } from "./mArray"; +export { mBoolean } from "./mBoolean"; +export { mClosure } from "./mClosure"; +export { mEmptyList } from "./mEmptyList"; +export { mList } from "./mList"; +export { mNumber } from "./mNumber"; +export { mOpaque } from "./mOpaque"; +export { mPair } from "./mPair"; +export { mString } from "./mString"; +export { mVoid } from "./mVoid"; diff --git a/src/conductor/util/isReferenceType.ts b/src/conductor/util/isReferenceType.ts new file mode 100644 index 0000000..e261af2 --- /dev/null +++ b/src/conductor/util/isReferenceType.ts @@ -0,0 +1,22 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { DataType } from "../types"; + +const lookupTable = { + [DataType.VOID]: false, + [DataType.BOOLEAN]: false, + [DataType.NUMBER]: false, + [DataType.CONST_STRING]: false, + [DataType.EMPTY_LIST]: true, // technically not; see list + [DataType.PAIR]: true, + [DataType.ARRAY]: true, + [DataType.CLOSURE]: true, + [DataType.OPAQUE]: true, + [DataType.LIST]: true, // technically not, but easier to do this due to pair being so +} + +export function isReferenceType(type: DataType): boolean { + return lookupTable[type]; +} diff --git a/src/conductor/util/isSameType.ts b/src/conductor/util/isSameType.ts new file mode 100644 index 0000000..d83e0b7 --- /dev/null +++ b/src/conductor/util/isSameType.ts @@ -0,0 +1,12 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { DataType } from "../types"; + +export function isSameType(t1: DataType, t2: DataType): boolean { + if (t1 === t2) return true; + if (t1 === DataType.LIST && (t2 === DataType.PAIR || t2 === DataType.EMPTY_LIST)) return true; + if (t2 === DataType.LIST && (t1 === DataType.PAIR || t1 === DataType.EMPTY_LIST)) return true; + return false; +} diff --git a/src/conductor/util/mArray.ts b/src/conductor/util/mArray.ts new file mode 100644 index 0000000..a4feeb4 --- /dev/null +++ b/src/conductor/util/mArray.ts @@ -0,0 +1,12 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { ArrayIdentifier, DataType, TypedValue } from "../types"; + +export function mArray(value: ArrayIdentifier): TypedValue { + return { + type: DataType.ARRAY, + value + }; +} diff --git a/src/conductor/util/mBoolean.ts b/src/conductor/util/mBoolean.ts new file mode 100644 index 0000000..15ffaa1 --- /dev/null +++ b/src/conductor/util/mBoolean.ts @@ -0,0 +1,12 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { DataType, TypedValue } from "../types"; + +export function mBoolean(value: boolean): TypedValue { + return { + type: DataType.BOOLEAN, + value + }; +} diff --git a/src/conductor/util/mClosure.ts b/src/conductor/util/mClosure.ts new file mode 100644 index 0000000..e72d5e1 --- /dev/null +++ b/src/conductor/util/mClosure.ts @@ -0,0 +1,12 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { ClosureIdentifier, DataType, TypedValue } from "../types"; + +export function mClosure(value: ClosureIdentifier): TypedValue { + return { + type: DataType.CLOSURE, + value + }; +} diff --git a/src/conductor/util/mEmptyList.ts b/src/conductor/util/mEmptyList.ts new file mode 100644 index 0000000..a157ff1 --- /dev/null +++ b/src/conductor/util/mEmptyList.ts @@ -0,0 +1,12 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { DataType, TypedValue } from "../types"; + +export function mEmptyList(value: null = null): TypedValue { + return { + type: DataType.EMPTY_LIST, + value + }; +} diff --git a/src/conductor/util/mList.ts b/src/conductor/util/mList.ts new file mode 100644 index 0000000..d4dd3ba --- /dev/null +++ b/src/conductor/util/mList.ts @@ -0,0 +1,12 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { DataType, TypedValue, PairIdentifier } from "../types"; + +export function mList(value: PairIdentifier | null): TypedValue { + return { + type: DataType.LIST, + value + }; +} diff --git a/src/conductor/util/mNumber.ts b/src/conductor/util/mNumber.ts new file mode 100644 index 0000000..36e2bc8 --- /dev/null +++ b/src/conductor/util/mNumber.ts @@ -0,0 +1,12 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { DataType, TypedValue } from "../types"; + +export function mNumber(value: number): TypedValue { + return { + type: DataType.NUMBER, + value + }; +} diff --git a/src/conductor/util/mOpaque.ts b/src/conductor/util/mOpaque.ts new file mode 100644 index 0000000..41b00b4 --- /dev/null +++ b/src/conductor/util/mOpaque.ts @@ -0,0 +1,12 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { DataType, TypedValue, OpaqueIdentifier } from "../types"; + +export function mOpaque(value: OpaqueIdentifier): TypedValue { + return { + type: DataType.OPAQUE, + value + }; +} diff --git a/src/conductor/util/mPair.ts b/src/conductor/util/mPair.ts new file mode 100644 index 0000000..87291c1 --- /dev/null +++ b/src/conductor/util/mPair.ts @@ -0,0 +1,12 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { DataType, TypedValue, PairIdentifier } from "../types"; + +export function mPair(value: PairIdentifier): TypedValue { + return { + type: DataType.PAIR, + value + }; +} diff --git a/src/conductor/util/mString.ts b/src/conductor/util/mString.ts new file mode 100644 index 0000000..ec33a46 --- /dev/null +++ b/src/conductor/util/mString.ts @@ -0,0 +1,12 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { DataType, TypedValue } from "../types"; + +export function mString(value: string): TypedValue { + return { + type: DataType.CONST_STRING, + value + }; +} diff --git a/src/conductor/util/mVoid.ts b/src/conductor/util/mVoid.ts new file mode 100644 index 0000000..df15971 --- /dev/null +++ b/src/conductor/util/mVoid.ts @@ -0,0 +1,12 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { DataType, TypedValue } from "../types"; + +export function mVoid(value: void = undefined): TypedValue { + return { + type: DataType.VOID, + value + }; +} diff --git a/src/conduit/Channel.ts b/src/conduit/Channel.ts new file mode 100644 index 0000000..85fa730 --- /dev/null +++ b/src/conduit/Channel.ts @@ -0,0 +1,94 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { ConductorInternalError } from "../common/errors/ConductorInternalError"; +import { IChannel, Subscriber } from "./types"; + +export class Channel implements IChannel { + readonly name: string; + + /** The underlying MessagePort of this Channel. */ + private __port!: MessagePort; // replacePort assigns this in the constructor + + /** The callbacks subscribed to this Channel. */ + private readonly __subscribers: Set> = new Set(); // TODO: use WeakRef? but callbacks tend to be thrown away and leaking is better than incorrect behaviour + + /** Is the Channel allowed to be used? */ + private __isAlive: boolean = true; + + private __waitingMessages?: T[] = []; + + send(message: T, transfer?: Transferable[]): void { + this.__verifyAlive(); + this.__port.postMessage(message, transfer ?? []); + } + subscribe(subscriber: Subscriber): void { + this.__verifyAlive(); + this.__subscribers.add(subscriber); + if (this.__waitingMessages) { + for (const data of this.__waitingMessages) { + subscriber(data); + } + delete this.__waitingMessages; + } + } + unsubscribe(subscriber: Subscriber): void { + this.__verifyAlive(); + this.__subscribers.delete(subscriber); + } + close(): void { + this.__verifyAlive(); + this.__isAlive = false; + this.__port?.close(); + } + + /** + * Check if this Channel is allowed to be used. + * @throws Throws an error if the Channel has been closed. + */ + private __verifyAlive() { + if (!this.__isAlive) throw new ConductorInternalError(`Channel ${this.name} has been closed`); + } + + /** + * Dispatch some data to subscribers. + * @param data The data to be dispatched to subscribers. + */ + private __dispatch(data: T): void { + this.__verifyAlive(); + if (this.__waitingMessages) { + this.__waitingMessages.push(data); + } else { + for (const subscriber of this.__subscribers) { + subscriber(data); + } + } + } + + /** + * Listens to the port's message event, and starts the port. + * Messages will be buffered until the first subscriber listens to the Channel. + * @param port The MessagePort to listen to. + */ + listenToPort(port: MessagePort): void { + port.addEventListener("message", e => this.__dispatch(e.data)); + port.start(); + } + + /** + * Replaces the underlying MessagePort of this Channel and closes it, and starts the new port. + * @param port The new port to use. + */ + replacePort(port: MessagePort): void { + this.__verifyAlive(); + this.__port?.close(); + this.__port = port; + this.listenToPort(port); + } + + constructor(name: string, port: MessagePort) { + this.name = name; + this.replacePort(port); + } +} diff --git a/src/conduit/ChannelQueue.ts b/src/conduit/ChannelQueue.ts new file mode 100644 index 0000000..4d84f0b --- /dev/null +++ b/src/conduit/ChannelQueue.ts @@ -0,0 +1,30 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { MessageQueue } from "../common/ds"; +import { IChannelQueue, IChannel } from "./types"; + +export class ChannelQueue implements IChannelQueue { + readonly name: string; + private __channel: IChannel; + private __messageQueue: MessageQueue = new MessageQueue(); + + async receive(): Promise { + return this.__messageQueue.pop(); + } + tryReceive(): T | undefined { + return this.__messageQueue.tryPop(); + } + send(message: T, transfer?: Transferable[]): void { + this.__channel.send(message, transfer); + } + close(): void { + this.__channel.unsubscribe(this.__messageQueue.push); + } + constructor(channel: IChannel) { + this.name = channel.name; + this.__channel = channel; + this.__channel.subscribe(this.__messageQueue.push); + } +} diff --git a/src/conduit/Conduit.ts b/src/conduit/Conduit.ts new file mode 100644 index 0000000..e97d46d --- /dev/null +++ b/src/conduit/Conduit.ts @@ -0,0 +1,91 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { ConductorInternalError } from "../common/errors/ConductorInternalError"; +import { Channel } from "./Channel"; +import { IConduit, ILink, IPlugin, IChannel, PluginClass } from "./types"; + +export class Conduit implements IConduit { + private __alive: boolean = true; + private readonly __link: ILink; + private readonly __parent: boolean; + private readonly __channels: Map> = new Map(); + private readonly __pluginMap: Map = new Map(); + private readonly __plugins: IPlugin[] = []; + private __negotiateChannel(channelName: string): void { + const { port1, port2 } = new MessageChannel(); + const channel = new Channel(channelName, port1); + this.__link.postMessage([channelName, port2], [port2]); // TODO: update communication protocol? + this.__channels.set(channelName, channel); + } + private __verifyAlive() { + if (!this.__alive) throw new ConductorInternalError("Conduit already terminated"); + } + registerPlugin(pluginClass: PluginClass, ...arg: Arg): NoInfer { + this.__verifyAlive(); + const attachedChannels: IChannel[] = []; + for (const channelName of pluginClass.channelAttach) { + if (!this.__channels.has(channelName)) this.__negotiateChannel(channelName); + attachedChannels.push(this.__channels.get(channelName)!); // as the Channel has been negotiated + } + const plugin = new pluginClass(this, attachedChannels, ...arg); + + if (plugin.name !== undefined) { + if (this.__pluginMap.has(plugin.name)) throw new ConductorInternalError(`Plugin ${plugin.name} already registered`); + this.__pluginMap.set(plugin.name, plugin); + } + + this.__plugins.push(plugin); + + return plugin; + } + unregisterPlugin(plugin: IPlugin): void { + this.__verifyAlive(); + let p = 0; + for (let i = 0; i < this.__plugins.length; ++i) { + if (this.__plugins[p] === plugin) ++p; + this.__plugins[i] = this.__plugins[i + p]; + } + for (let i = this.__plugins.length - 1, e = this.__plugins.length - p; i >= e; --i) { + delete this.__plugins[i]; + } + if (plugin.name) { + this.__pluginMap.delete(plugin.name); + } + plugin.destroy?.(); + } + lookupPlugin(pluginName: string): IPlugin { + this.__verifyAlive(); + if (!this.__pluginMap.has(pluginName)) throw new ConductorInternalError(`Plugin ${pluginName} not registered`); + return this.__pluginMap.get(pluginName)!; // as the map has been checked + } + terminate(): void { + this.__verifyAlive(); + for (const plugin of this.__plugins) { + //this.unregisterPlugin(plugin); + plugin.destroy?.(); + } + this.__link.terminate?.(); + this.__alive = false; + } + private __handlePort(data: [string, MessagePort]) { // TODO: update communication protocol? + const [channelName, port] = data; + if (this.__channels.has(channelName)) { // uh-oh, we already have a port for this channel + const channel = this.__channels.get(channelName)!; // as the map has been checked + if (this.__parent) { // extract the data and discard the messageport; child's Channel will close it + channel.listenToPort(port); + } else { // replace our messageport; Channel will close it + channel.replacePort(port); + } + } else { // register the new channel + const channel = new Channel(channelName, port); + this.__channels.set(channelName, channel); + } + } + constructor(link: ILink, parent: boolean = false) { + this.__link = link; + link.addEventListener("message", e => this.__handlePort(e.data)); + this.__parent = parent; + } +} diff --git a/src/conduit/index.ts b/src/conduit/index.ts new file mode 100644 index 0000000..f721fc6 --- /dev/null +++ b/src/conduit/index.ts @@ -0,0 +1,8 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export type { IChannel, IConduit, ILink, IChannelQueue, IPlugin, Subscriber } from "./types"; +export { Channel } from "./Channel"; +export { ChannelQueue } from "./ChannelQueue"; +export { Conduit } from "./Conduit"; diff --git a/src/conduit/rpc/index.ts b/src/conduit/rpc/index.ts new file mode 100644 index 0000000..17f0a0a --- /dev/null +++ b/src/conduit/rpc/index.ts @@ -0,0 +1,6 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export { Remote } from "./types"; +export { makeRpc } from "./makeRpc"; diff --git a/src/conduit/rpc/makeRpc.ts b/src/conduit/rpc/makeRpc.ts new file mode 100644 index 0000000..db4f7e1 --- /dev/null +++ b/src/conduit/rpc/makeRpc.ts @@ -0,0 +1,63 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { IChannel } from "../types"; +import { IRpcMessage, Remote, RpcCallMessage, RpcErrorMessage, RpcMessageType, RpcReturnMessage } from "./types"; + +export function makeRpc(channel: IChannel, self: ISelf): Remote { + const waiting: [Function, Function][] = []; + let invocations = 0; + const otherCallbacks: Partial Promise>> = {}; + + channel.subscribe(async rpcMessage => { + switch (rpcMessage.type) { + case RpcMessageType.CALL: + { + const {fn, args, invokeId} = (rpcMessage as RpcCallMessage).data; + try { + // @ts-expect-error + const res = await self[fn as keyof ISelf](...args); + if (invokeId > 0) channel.send(new RpcReturnMessage(invokeId, res)); + } catch (err) { + if (invokeId > 0) channel.send(new RpcErrorMessage(invokeId, err)); + } + break; + } + case RpcMessageType.RETURN: + { + const {invokeId, res} = (rpcMessage as RpcReturnMessage).data; + waiting[invokeId]?.[0]?.(res); + delete waiting[invokeId]; + break; + } + case RpcMessageType.RETURN_ERR: + { + const {invokeId, err} = (rpcMessage as RpcErrorMessage).data; + waiting[invokeId]?.[1]?.(err); + delete waiting[invokeId]; + break; + } + } + }); + + return new Proxy(otherCallbacks, { // TODO: transferring functions + get(target, p, receiver) { + const cb = Reflect.get(target, p, receiver); + if (cb) return cb; + const newCallback = typeof p === "string" && p.charAt(0) === "$" + ? (...args: any[]) => { + channel.send(new RpcCallMessage(p, args, 0)); + } + : (...args: any[]) => { + const invokeId = ++invocations; + channel.send(new RpcCallMessage(p, args, invokeId)); + return new Promise((resolve, reject) => { + waiting[invokeId] = [resolve, reject]; + }); + } + Reflect.set(target, p, newCallback, receiver); + return newCallback; + }, + }) as Remote; +} diff --git a/src/conduit/rpc/types/IRpcMessage.ts b/src/conduit/rpc/types/IRpcMessage.ts new file mode 100644 index 0000000..57acbb2 --- /dev/null +++ b/src/conduit/rpc/types/IRpcMessage.ts @@ -0,0 +1,10 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { RpcMessageType } from "./RpcMessageType"; + +export interface IRpcMessage { + type: RpcMessageType; + data?: any; +} diff --git a/src/conduit/rpc/types/Remote.ts b/src/conduit/rpc/types/Remote.ts new file mode 100644 index 0000000..92954f2 --- /dev/null +++ b/src/conduit/rpc/types/Remote.ts @@ -0,0 +1,15 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export type Remote = { + [K in keyof IOther]: IOther[K] extends (...args: infer Args) => infer Ret + ? K extends `$${infer _N}` + ? Ret extends void + ? IOther[K] + : (...args: Args) => void + : Ret extends Promise + ? IOther[K] + : (...args: Args) => Promise + : never +} diff --git a/src/conduit/rpc/types/RpcCallMessage.ts b/src/conduit/rpc/types/RpcCallMessage.ts new file mode 100644 index 0000000..ec04d91 --- /dev/null +++ b/src/conduit/rpc/types/RpcCallMessage.ts @@ -0,0 +1,15 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { IRpcMessage } from "./IRpcMessage"; +import { RpcMessageType } from "./RpcMessageType"; + +export class RpcCallMessage implements IRpcMessage { + type = RpcMessageType.CALL; + readonly data: {fn: string | symbol, args: any[], invokeId: number}; + + constructor(fn: string | symbol, args: any[], invokeId: number) { + this.data = {fn, args, invokeId}; + } +} diff --git a/src/conduit/rpc/types/RpcErrorMessage.ts b/src/conduit/rpc/types/RpcErrorMessage.ts new file mode 100644 index 0000000..ffb2f72 --- /dev/null +++ b/src/conduit/rpc/types/RpcErrorMessage.ts @@ -0,0 +1,15 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { IRpcMessage } from "./IRpcMessage"; +import { RpcMessageType } from "./RpcMessageType"; + +export class RpcErrorMessage implements IRpcMessage { + type = RpcMessageType.RETURN_ERR; + readonly data: {invokeId: number, err: any}; + + constructor(invokeId: number, err: any) { + this.data = {invokeId, err}; + } +} diff --git a/src/conduit/rpc/types/RpcMessageType.ts b/src/conduit/rpc/types/RpcMessageType.ts new file mode 100644 index 0000000..b75bc4c --- /dev/null +++ b/src/conduit/rpc/types/RpcMessageType.ts @@ -0,0 +1,11 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +const enum RpcMessageType { + CALL, + RETURN, + RETURN_ERR +} + +export { RpcMessageType }; diff --git a/src/conduit/rpc/types/RpcReturnMessage.ts b/src/conduit/rpc/types/RpcReturnMessage.ts new file mode 100644 index 0000000..420a857 --- /dev/null +++ b/src/conduit/rpc/types/RpcReturnMessage.ts @@ -0,0 +1,15 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { IRpcMessage } from "./IRpcMessage"; +import { RpcMessageType } from "./RpcMessageType"; + +export class RpcReturnMessage implements IRpcMessage { + type = RpcMessageType.RETURN; + readonly data: {invokeId: number, res: any}; + + constructor(invokeId: number, res: any) { + this.data = {invokeId, res}; + } +} diff --git a/src/conduit/rpc/types/index.ts b/src/conduit/rpc/types/index.ts new file mode 100644 index 0000000..23d74c6 --- /dev/null +++ b/src/conduit/rpc/types/index.ts @@ -0,0 +1,10 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export type { IRpcMessage } from "./IRpcMessage"; +export type { Remote } from "./Remote"; +export { RpcCallMessage } from "./RpcCallMessage"; +export { RpcErrorMessage } from "./RpcErrorMessage"; +export { RpcMessageType } from "./RpcMessageType"; +export { RpcReturnMessage } from "./RpcReturnMessage"; diff --git a/src/conduit/types/AbstractPluginClass.ts b/src/conduit/types/AbstractPluginClass.ts new file mode 100644 index 0000000..15f1343 --- /dev/null +++ b/src/conduit/types/AbstractPluginClass.ts @@ -0,0 +1,11 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { IChannel } from "./IChannel"; +import { IConduit } from "./IConduit"; +import { IPlugin } from "./IPlugin"; + +export type AbstractPluginClass = { + readonly channelAttach: string[]; +} & (abstract new (conduit: IConduit, channels: IChannel[], ...arg: Arg) => T); diff --git a/src/conduit/types/IChannel.ts b/src/conduit/types/IChannel.ts new file mode 100644 index 0000000..b8e383a --- /dev/null +++ b/src/conduit/types/IChannel.ts @@ -0,0 +1,34 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { Subscriber } from "./Subscriber"; + +export interface IChannel { + /** The name of the channel. */ + readonly name: string; + + /** + * Send a message through this channel. + * @param message The message to be sent. + * @param transfer An array of transferable objects to be sent with the message. + */ + send(message: T, transfer?: Transferable[]): void; + + /** + * Subscribe to messages on this channel. + * @param subscriber The function to be called when a message is received. + */ + subscribe(subscriber: Subscriber): void; + + /** + * Unsubscribe from messages on this channel. + * @param subscriber The function that was called when a message is received. + */ + unsubscribe(subscriber: Subscriber): void; + + /** + * Closes the channel, and frees any held resources. + */ + close(): void; +} diff --git a/src/conduit/types/IChannelQueue.ts b/src/conduit/types/IChannelQueue.ts new file mode 100644 index 0000000..d56009b --- /dev/null +++ b/src/conduit/types/IChannelQueue.ts @@ -0,0 +1,33 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export interface IChannelQueue { + /** The name of the message queue. */ + readonly name: string; + + /** + * Send a message through the underlying channel. + * @param message The message to be sent. + * @param transfer An array of transferable objects to be sent with the message. + */ + send(message: T, transfer?: Transferable[]): void; + + /** + * Receives a queued message, or waits until one arrives. + * @returns A promise resolving to the received message. + */ + receive(): Promise; + + /** + * Tries to receive a queued message. + * Does not wait for a message if the queue is empty. + * @returns The received message, or undefined if the queue is empty. + */ + tryReceive(): T | undefined; + + /** + * Closes the message queue. + */ + close(): void; +} diff --git a/src/conduit/types/IConduit.ts b/src/conduit/types/IConduit.ts new file mode 100644 index 0000000..1ff3c52 --- /dev/null +++ b/src/conduit/types/IConduit.ts @@ -0,0 +1,31 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import type { IPlugin } from "./IPlugin"; +import type { PluginClass } from "./PluginClass"; + +export interface IConduit { + /** + * Register a plugin with the conduit. + * @param pluginClass The plugin to be registered. + */ + registerPlugin(pluginClass: PluginClass, ...arg: Arg): NoInfer; + + /** + * Unregister a plugin from the conduit. + * @param plugin The plugin to be unregistered. + */ + unregisterPlugin(plugin: IPlugin): void; + + /** + * Look for a plugin with the given name. + * @param pluginName The name of the plugin to be searched for. + */ + lookupPlugin(pluginName: string): IPlugin; + + /** + * Shuts down the conduit. + */ + terminate(): void; +} diff --git a/src/conduit/types/ILink.ts b/src/conduit/types/ILink.ts new file mode 100644 index 0000000..7c2db3f --- /dev/null +++ b/src/conduit/types/ILink.ts @@ -0,0 +1,9 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export interface ILink { + postMessage: typeof Worker.prototype.postMessage; + addEventListener: typeof Worker.prototype.addEventListener; + terminate?: typeof Worker.prototype.terminate; +} diff --git a/src/conduit/types/IPlugin.ts b/src/conduit/types/IPlugin.ts new file mode 100644 index 0000000..5804d04 --- /dev/null +++ b/src/conduit/types/IPlugin.ts @@ -0,0 +1,13 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export interface IPlugin { + /** The name of the plugin. Can be undefined for an unnamed plugin. */ + readonly name?: string; + + /** + * Perform any cleanup of the plugin (e.g. closing message queues). + */ + destroy?(): void; +} diff --git a/src/conduit/types/PluginClass.ts b/src/conduit/types/PluginClass.ts new file mode 100644 index 0000000..b01dd7f --- /dev/null +++ b/src/conduit/types/PluginClass.ts @@ -0,0 +1,11 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { IChannel } from "./IChannel"; +import { IConduit } from "./IConduit"; +import type { IPlugin } from "./IPlugin"; + +export type PluginClass = { + readonly channelAttach: string[]; +} & (new (conduit: IConduit, channels: IChannel[], ...arg: Arg) => T); diff --git a/src/conduit/types/Subscriber.ts b/src/conduit/types/Subscriber.ts new file mode 100644 index 0000000..2e8a16c --- /dev/null +++ b/src/conduit/types/Subscriber.ts @@ -0,0 +1,6 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +/** A subscriber of a channel. */ +export type Subscriber = (data: T) => void; diff --git a/src/conduit/types/index.ts b/src/conduit/types/index.ts new file mode 100644 index 0000000..1a12428 --- /dev/null +++ b/src/conduit/types/index.ts @@ -0,0 +1,12 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export type { AbstractPluginClass } from "./AbstractPluginClass"; +export type { IChannel } from "./IChannel"; +export type { IChannelQueue } from "./IChannelQueue"; +export type { IConduit } from "./IConduit"; +export type { ILink } from "./ILink"; +export type { IPlugin } from "./IPlugin"; +export type { PluginClass } from "./PluginClass"; +export type { Subscriber } from "./Subscriber"; diff --git a/src/conduit/util/checkIsPluginClass.ts b/src/conduit/util/checkIsPluginClass.ts new file mode 100644 index 0000000..ede4fd3 --- /dev/null +++ b/src/conduit/util/checkIsPluginClass.ts @@ -0,0 +1,16 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +import { IPlugin } from ".."; +import { AbstractPluginClass, PluginClass } from "../types"; + +/** + * Typechecking utility decorator. + * It is recommended that usage of this decorator is removed + * before or during the build process, as some tools + * (e.g. terser) do not have good support for class decorators. + * @param _pluginClass The Class to be typechecked. + */ +export function checkIsPluginClass(_pluginClass: PluginClass | AbstractPluginClass) { +} diff --git a/src/conduit/util/index.ts b/src/conduit/util/index.ts new file mode 100644 index 0000000..1bc76af --- /dev/null +++ b/src/conduit/util/index.ts @@ -0,0 +1,5 @@ +// This file is adapted from: +// https://github.com/source-academy/conductor +// Original author(s): Source Academy Team + +export { checkIsPluginClass } from "./checkIsPluginClass"; diff --git a/src/cse-machine/interpreter.ts b/src/cse-machine/interpreter.ts index 7e1e2ee..24ab772 100644 --- a/src/cse-machine/interpreter.ts +++ b/src/cse-machine/interpreter.ts @@ -26,7 +26,6 @@ import { IOptions } from '..'; import { CseError } from './error'; import { filterImportDeclarations } from './dict'; import { RuntimeSourceError } from '../errors/runtimeSourceError'; -// import { Identifier } from '../conductor/types'; type CmdEvaluator = ( command: ControlItem, diff --git a/src/index.ts b/src/index.ts index 251660c..48eaf32 100644 --- a/src/index.ts +++ b/src/index.ts @@ -134,6 +134,13 @@ import { Parser } from "./parser"; import { Translator } from "./translator"; import { Program } from "estree"; import { Resolver } from "./resolver"; +import { Context } from './cse-machine/context'; +export * from './errors'; +import { Finished, RecursivePartial, Result } from "./types"; +import { runCSEMachine } from "./runner/pyRunner"; +import { initialise } from "./conductor/runner/util/initialise"; +import { PyEvaluator } from "./conductor/runner/types/PyEvaluator"; +export * from './errors'; export function parsePythonToEstreeAst(code: string, variant: number = 1, @@ -150,9 +157,6 @@ export function parsePythonToEstreeAst(code: string, return translator.resolve(ast) as unknown as Program } - -export * from './errors'; - // import {ParserErrors, ResolverErrors, TokenizerErrors} from "./errors"; // import fs from "fs"; // const BaseParserError = ParserErrors.BaseParserError; @@ -190,3 +194,15 @@ export interface IOptions { envSteps: number, stepLimit: number }; + +export async function runInContext( + code: string, + context: Context, + options: RecursivePartial = {} +): Promise { + const estreeAst = parsePythonToEstreeAst(code, 1, true); + const result = runCSEMachine(code, estreeAst, context, options); + return result; +} + +const {runnerPlugin, conduit} = initialise(PyEvaluator); diff --git a/src/runner/pyRunner.ts b/src/runner/pyRunner.ts new file mode 100644 index 0000000..4e6ac0f --- /dev/null +++ b/src/runner/pyRunner.ts @@ -0,0 +1,10 @@ +import { IOptions } from ".." +import { Context } from "../cse-machine/context" +import { CSEResultPromise, evaluate } from "../cse-machine/interpreter" +import { RecursivePartial, Result } from "../types" +import * as es from 'estree' + +export function runCSEMachine(code: string, program: es.Program, context: Context, options: RecursivePartial = {}): Promise { + const result = evaluate(code, program, context, options); + return CSEResultPromise(context, result); +} \ No newline at end of file