From d349c534d07e9f7d54c1a92013e316c3f56ff035 Mon Sep 17 00:00:00 2001 From: Shen Yi Hong Date: Sat, 6 Apr 2024 19:06:16 +0800 Subject: [PATCH 1/4] refactor: add value type to `Result` util class --- src/go-slang/lib/utils.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/go-slang/lib/utils.ts b/src/go-slang/lib/utils.ts index 66ac36ae1..5e51dea12 100644 --- a/src/go-slang/lib/utils.ts +++ b/src/go-slang/lib/utils.ts @@ -22,24 +22,35 @@ export function isAny(query: T1, values: T2[]): boolean { return query ? values.some(v => v === query) : false } -export class Result { +export class Result { + private value: T | undefined + public isSuccess: boolean public isFailure: boolean public error: E | undefined - private constructor(isSuccess: boolean, error?: E) { + private constructor(isSuccess: boolean, error?: E, value?: T) { this.isSuccess = isSuccess this.isFailure = !isSuccess this.error = error + this.value = value + Object.freeze(this) } - public static ok(): Result { - return new Result(true) + public unwrap(): T { + if (this.isFailure) { + throw new Error('called `unwrap` on a failed result') + } + return this.value as T + } + + public static ok(value?: T): Result { + return new Result(true, undefined, value) } - public static fail(error: E): Result { - return new Result(false, error) + public static fail(error: E): Result { + return new Result(false, error) } } From 3ecc9f2ebffe5f8aecfbd263740afbe95e02f1fe Mon Sep 17 00:00:00 2001 From: Shen Yi Hong Date: Sat, 6 Apr 2024 21:38:22 +0800 Subject: [PATCH 2/4] feat: inject state into `GoRoutine` Having goroutine states help simplify the implementation of the scheduler as well --- src/go-slang/goroutine.ts | 76 ++++++++++++++++++++++++--------------- src/go-slang/scheduler.ts | 25 ++++++------- 2 files changed, 58 insertions(+), 43 deletions(-) diff --git a/src/go-slang/goroutine.ts b/src/go-slang/goroutine.ts index e572b4d15..28c58ab5e 100644 --- a/src/go-slang/goroutine.ts +++ b/src/go-slang/goroutine.ts @@ -4,7 +4,6 @@ import { RuntimeSourceError } from '../errors/runtimeSourceError' import { FuncArityError, GoExprMustBeFunctionError, - InvalidOperationError, UndefinedError, UnknownInstructionError } from './error' @@ -76,6 +75,12 @@ export interface Context { A: AstMap } +export enum GoRoutineState { + Running, + Blocked, + Exited +} + export class GoRoutine { static idCounter: Counter = new Counter() @@ -83,20 +88,18 @@ export class GoRoutine { private context: Context private scheduler: Scheduler + public state: GoRoutineState public isMain: boolean constructor(context: Context, scheduler: Scheduler, isMain: boolean = false) { this.id = GoRoutine.idCounter.next() + this.state = GoRoutineState.Running this.context = context this.scheduler = scheduler this.isMain = isMain } - public finished(): boolean { - return this.context.C.isEmpty() - } - - public tick(): Result { + public tick(): Result { const { C, H } = this.context const inst = H.resolve(C.pop()) as Instruction @@ -104,8 +107,12 @@ export class GoRoutine { return Result.fail(new UnknownInstructionError(inst.type)) } - const runtimeError = Interpreter[inst.type](inst, this.context, this.scheduler, this.id) - return runtimeError ? Result.fail(runtimeError) : Result.ok() + const nextState = + Interpreter[inst.type](inst, this.context, this.scheduler, this.id) ?? + Result.ok(C.isEmpty() ? GoRoutineState.Exited : GoRoutineState.Running) + + this.state = nextState.isSuccess ? nextState.unwrap() : GoRoutineState.Exited + return nextState } } @@ -115,7 +122,7 @@ const Interpreter: { context: Context, sched: Scheduler, routineId: number - ) => RuntimeSourceError | void + ) => Result | void } = { SourceFile: ({ topLevelDecls }: SourceFile, { C, H }) => C.pushR(...H.allocM(topLevelDecls)), @@ -190,11 +197,11 @@ const Interpreter: { GoStatement: ({ call, loc }: GoStatement, { C, H, A }) => { if (call.type !== NodeType.CallExpression) { - return new GoExprMustBeFunctionError(call.type, loc!) + return Result.fail(new GoExprMustBeFunctionError(call.type, loc!)) } const { callee, args } = call as CallExpression - return C.pushR( + return void C.pushR( ...H.allocM([ callee, ...args, @@ -218,7 +225,7 @@ const Interpreter: { Identifier: ({ name, loc }: Identifier, { S, E, H }) => { const value = E.lookup(name) - return value === null ? new UndefinedError(name, loc!) : S.push(H.alloc(value)) + return value === null ? Result.fail(new UndefinedError(name, loc!)) : S.push(H.alloc(value)) }, UnaryExpression: ({ argument, operator: op }: UnaryExpression, { C, H, A }) => @@ -270,7 +277,7 @@ const Interpreter: { // handle BuiltinOp if (op.type === CommandType.BuiltinOp) { const result = B.get(op.id)!(...values) - return result instanceof InvalidOperationError ? result : S.push(H.alloc(result)) + return result instanceof RuntimeSourceError ? Result.fail(result) : S.push(H.alloc(result)) } // handle ClosureOp @@ -280,7 +287,9 @@ const Interpreter: { if (paramNames.length !== values.length) { const calleeId = A.get(calleeNodeId) - return new FuncArityError(calleeId.name, values.length, params.length, calleeId.loc!) + return Result.fail( + new FuncArityError(calleeId.name, values.length, params.length, calleeId.loc!) + ) } C.pushR(...H.allocM([body, RetMarker, { type: CommandType.EnvOp, envId: E.id() }])) @@ -300,7 +309,9 @@ const Interpreter: { if (paramNames.length !== values.length) { const calleeId = A.get(calleeNodeId) - return new FuncArityError(calleeId.name, values.length, params.length, calleeId.loc!) + return Result.fail( + new FuncArityError(calleeId.name, values.length, params.length, calleeId.loc!) + ) } const _C: Control = new Stack() @@ -310,7 +321,7 @@ const Interpreter: { .setId(envId) .extend(Object.fromEntries(zip(paramNames, values))) - return sched.schedule(new GoRoutine({ C: _C, S: _S, E: _E, B, H, A }, sched)) + return void sched.schedule(new GoRoutine({ C: _C, S: _S, E: _E, B, H, A }, sched)) }, ChanRecvOp: (_inst, { C, S, H }, _sched, routineId: number) => { @@ -318,8 +329,10 @@ const Interpreter: { if (chan instanceof BufferedChannel) { // if the channel is empty, we retry the receive operation - if (chan.isBufferEmpty()) { return C.push(ChanRecv) } // prettier-ignore - + if (chan.isBufferEmpty()) { + C.push(ChanRecv) + return Result.ok(GoRoutineState.Blocked) + } S.pop() // pop the channel address return S.push(H.alloc(chan.recv())) } @@ -327,8 +340,10 @@ const Interpreter: { if (chan instanceof UnbufferedChannel) { const recvValue = chan.recv(routineId) // if we cannot receive, we retry the receive operation - if (recvValue === null) { return C.push(ChanRecv) } // prettier-ignore - + if (recvValue === null) { + C.push(ChanRecv) + return Result.ok(GoRoutineState.Blocked) + } S.pop() // pop the channel address return S.push(H.alloc(recvValue)) } @@ -339,20 +354,25 @@ const Interpreter: { if (chan instanceof BufferedChannel) { // if the channel is full, we retry the send operation - if (chan.isBufferFull()) { return C.push(ChanSend) } // prettier-ignore - + if (chan.isBufferFull()) { + C.push(ChanSend) + return Result.ok(GoRoutineState.Blocked) + } S.popN(2) // pop the channel address and the value address - chan.send(sendValue) - return + return void chan.send(sendValue) } if (chan instanceof UnbufferedChannel) { // if we cannot send, we retry the send operation - if (!chan.send(routineId, sendValue)) { return C.push(ChanSend) } // prettier-ignore - - S.popN(2) // pop the channel address and the value address - return + if (!chan.send(routineId, sendValue)) { + C.push(ChanSend) + return Result.ok(GoRoutineState.Blocked) + } + return void S.popN(2) // pop the channel address and the value address } + + // NOTE: this should be unreachable + return }, BranchOp: ({ cons, alt }: BranchOp, { S, C, H }) => diff --git a/src/go-slang/scheduler.ts b/src/go-slang/scheduler.ts index 6c7622ea6..4375ede85 100644 --- a/src/go-slang/scheduler.ts +++ b/src/go-slang/scheduler.ts @@ -1,5 +1,5 @@ import { Context as SlangContext } from '..' -import { GoRoutine } from './goroutine' +import { GoRoutine, GoRoutineState } from './goroutine' import { RuntimeSourceError } from '../errors/runtimeSourceError' type TimeQuanta = number @@ -25,29 +25,24 @@ export class Scheduler { } public run(): void { - while (this.routines.length > 0) { + while (this.routines.length) { const [routine, timeQuanta] = this.routines.shift() as [GoRoutine, TimeQuanta] - let hasError: boolean = false let remainingTime = timeQuanta - while (remainingTime--) { - if (routine.finished()) { break } // prettier-ignore - const result = routine.tick() - if (result.isSuccess) { continue } // prettier-ignore - - hasError = true - this.slangContext.errors.push(result.error as RuntimeSourceError) - break + if (result.isFailure) { this.slangContext.errors.push(result.error as RuntimeSourceError) } // prettier-ignore + // if the routine is no longer running we schedule it out + if (result.unwrap() !== GoRoutineState.Running) { break } // prettier-ignore } // once main exits, the other routines are terminated and the program exits - if (routine.isMain && (routine.finished() || hasError)) { return } // prettier-ignore + if (routine.isMain && routine.state === GoRoutineState.Exited) { return } // prettier-ignore - if (!routine.finished() && !hasError) { - this.routines.push([routine, Scheduler.randTimeQuanta()]) - } + // if the routine exits, we don't schedule it back + if (routine.state === GoRoutineState.Exited) { continue } // prettier-ignore + + this.schedule(routine) } } } From 78ff667e0cbcb08174a8abb744d7c28cc36201bf Mon Sep 17 00:00:00 2001 From: Shen Yi Hong Date: Sat, 6 Apr 2024 22:07:15 +0800 Subject: [PATCH 3/4] test: implement `benchmark` decorator for scheduler --- src/go-slang/lib/utils.ts | 20 ++++++++++++++++++++ src/go-slang/scheduler.ts | 2 ++ tsconfig.json | 1 + 3 files changed, 23 insertions(+) diff --git a/src/go-slang/lib/utils.ts b/src/go-slang/lib/utils.ts index 5e51dea12..972d2b9a6 100644 --- a/src/go-slang/lib/utils.ts +++ b/src/go-slang/lib/utils.ts @@ -61,3 +61,23 @@ export class Counter { constructor (start: number = 0) { this.count = start } public next(): number { return this.count++ } } + +export function benchmark(label: string) { + function _benchmark( + _target: any, + _propertyKey: string, + descriptor: PropertyDescriptor + ): PropertyDescriptor { + const originalMethod = descriptor.value + + descriptor.value = function (...args: any[]) { + const start = performance.now() + const result = originalMethod.apply(this, args) + console.log(`[${label}] exec time: ${(performance.now() - start).toFixed(2)}ms`) + return result + } + + return descriptor + } + return _benchmark +} diff --git a/src/go-slang/scheduler.ts b/src/go-slang/scheduler.ts index 4375ede85..b7ee4dd3f 100644 --- a/src/go-slang/scheduler.ts +++ b/src/go-slang/scheduler.ts @@ -1,6 +1,7 @@ import { Context as SlangContext } from '..' import { GoRoutine, GoRoutineState } from './goroutine' import { RuntimeSourceError } from '../errors/runtimeSourceError' +import { benchmark } from './lib/utils' type TimeQuanta = number @@ -24,6 +25,7 @@ export class Scheduler { this.routines.push([routine, Scheduler.randTimeQuanta()]) } + @benchmark('Scheduler.run') public run(): void { while (this.routines.length) { const [routine, timeQuanta] = this.routines.shift() as [GoRoutine, TimeQuanta] diff --git a/tsconfig.json b/tsconfig.json index 6f59f3ebe..cf7c50443 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "module": "commonjs", "declaration": true, "target": "es2016", + "experimentalDecorators": true, "lib": [ "es2021.string", "es2018", From dd0ea8dcce772f33b5709d0c1faeef447d7389bf Mon Sep 17 00:00:00 2001 From: Shen Yi Hong Date: Sat, 6 Apr 2024 23:07:02 +0800 Subject: [PATCH 4/4] feat: naive deadlock detection --- src/go-slang/error.ts | 6 ++++++ src/go-slang/scheduler.ts | 15 +++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/go-slang/error.ts b/src/go-slang/error.ts index c1299d1c0..8eee8c415 100644 --- a/src/go-slang/error.ts +++ b/src/go-slang/error.ts @@ -88,3 +88,9 @@ export class GoExprMustBeFunctionError extends RuntimeSourceError { return `expression in go statement must be function call, not ${this.expr}` } } + +export class DeadLockError extends RuntimeSourceError { + public explain() { + return 'all goroutines are asleep - deadlock!' + } +} diff --git a/src/go-slang/scheduler.ts b/src/go-slang/scheduler.ts index b7ee4dd3f..b69e28b5c 100644 --- a/src/go-slang/scheduler.ts +++ b/src/go-slang/scheduler.ts @@ -2,6 +2,7 @@ import { Context as SlangContext } from '..' import { GoRoutine, GoRoutineState } from './goroutine' import { RuntimeSourceError } from '../errors/runtimeSourceError' import { benchmark } from './lib/utils' +import { DeadLockError } from './error' type TimeQuanta = number @@ -25,9 +26,11 @@ export class Scheduler { this.routines.push([routine, Scheduler.randTimeQuanta()]) } - @benchmark('Scheduler.run') + @benchmark('Scheduler::run') public run(): void { - while (this.routines.length) { + let numConsecAllBlocks = 0 + + while (this.routines.length && numConsecAllBlocks < this.routines.length) { const [routine, timeQuanta] = this.routines.shift() as [GoRoutine, TimeQuanta] let remainingTime = timeQuanta @@ -45,6 +48,14 @@ export class Scheduler { if (routine.state === GoRoutineState.Exited) { continue } // prettier-ignore this.schedule(routine) + + const hasRunningRoutines = this.routines.some( + ([{ state }]) => state === GoRoutineState.Running + ) + numConsecAllBlocks = hasRunningRoutines ? 0 : numConsecAllBlocks + 1 } + + // if we reach here, all routines are blocked + this.slangContext.errors.push(new DeadLockError()) } }