Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: yield scheduler when blocked #22

Merged
merged 4 commits into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/go-slang/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!'
}
}
76 changes: 48 additions & 28 deletions src/go-slang/goroutine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { RuntimeSourceError } from '../errors/runtimeSourceError'
import {
FuncArityError,
GoExprMustBeFunctionError,
InvalidOperationError,
UndefinedError,
UnknownInstructionError
} from './error'
Expand Down Expand Up @@ -76,36 +75,44 @@ export interface Context {
A: AstMap
}

export enum GoRoutineState {
Running,
Blocked,
Exited
}

export class GoRoutine {
static idCounter: Counter = new Counter()

private id: number
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<RuntimeSourceError> {
public tick(): Result<GoRoutineState, RuntimeSourceError> {
const { C, H } = this.context
const inst = H.resolve(C.pop()) as Instruction

if (!Interpreter.hasOwnProperty(inst.type)) {
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
}
}

Expand All @@ -115,7 +122,7 @@ const Interpreter: {
context: Context,
sched: Scheduler,
routineId: number
) => RuntimeSourceError | void
) => Result<GoRoutineState, RuntimeSourceError> | void
} = {
SourceFile: ({ topLevelDecls }: SourceFile, { C, H }) => C.pushR(...H.allocM(topLevelDecls)),

Expand Down Expand Up @@ -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,
Expand All @@ -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 }) =>
Expand Down Expand Up @@ -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
Expand All @@ -280,7 +287,9 @@ const Interpreter: {

if (paramNames.length !== values.length) {
const calleeId = A.get<Identifier>(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() }]))
Expand All @@ -300,7 +309,9 @@ const Interpreter: {

if (paramNames.length !== values.length) {
const calleeId = A.get<Identifier>(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()
Expand All @@ -310,25 +321,29 @@ 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) => {
const chan = H.resolve(S.peek()) as BufferedChannel | UnbufferedChannel

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()))
}

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))
}
Expand All @@ -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 }) =>
Expand Down
43 changes: 37 additions & 6 deletions src/go-slang/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,35 @@ export function isAny<T1, T2>(query: T1, values: T2[]): boolean {
return query ? values.some(v => v === query) : false
}

export class Result<E extends SourceError> {
export class Result<T, E extends SourceError> {
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<E extends SourceError>(): Result<E> {
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 fail<E extends SourceError>(error: E): Result<E> {
return new Result(false, error)
public static ok<T, E extends SourceError>(value?: T): Result<T, E> {
return new Result<T, E>(true, undefined, value)
}

public static fail<T, E extends SourceError>(error: E): Result<T, E> {
return new Result<T, E>(false, error)
}
}

Expand All @@ -50,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
}
38 changes: 23 additions & 15 deletions src/go-slang/scheduler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Context as SlangContext } from '..'
import { GoRoutine } from './goroutine'
import { GoRoutine, GoRoutineState } from './goroutine'
import { RuntimeSourceError } from '../errors/runtimeSourceError'
import { benchmark } from './lib/utils'
import { DeadLockError } from './error'

type TimeQuanta = number

Expand All @@ -24,30 +26,36 @@ export class Scheduler {
this.routines.push([routine, Scheduler.randTimeQuanta()])
}

@benchmark('Scheduler::run')
public run(): void {
while (this.routines.length > 0) {
let numConsecAllBlocks = 0

while (this.routines.length && numConsecAllBlocks < 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)

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())
}
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"module": "commonjs",
"declaration": true,
"target": "es2016",
"experimentalDecorators": true,
"lib": [
"es2021.string",
"es2018",
Expand Down
Loading