Skip to content

Commit

Permalink
feat: goroutines (#19)
Browse files Browse the repository at this point in the history
* feat: naive time quanta based scheduler

Abstracted the current program as a `main` go routine in preparation for
`GoStatement` implementation

* chore: fix eslint errors

* refactor: use `Counter` for frame id generation

* refactor: add `GoStatement` to ECE types

* feat: implement `GoStatement` support in ECE

Right now we only support non-builtin calls to spawn a goroutine. We
need to consider if its even worth it spawn a new goroutine for a
builtin since its only a "1 step/tick" op. Additionally, I introduced
`GoRoutineOp` to handle spawning of new goroutines. Its payload is
same as `CallOp` so we can consider combining them if there is no
special cases to handle later on

* feat: add `StringLit` support in parser

This is mainly so that we can `println` strings to differentiate between
goroutines. Note that this is purely a convenience feature, the ECE does
not allocate strings in the heap/string pool right now.

* feat: implement `GoRoutineOp` support in heap

* feat: randomize goroutine time quanta
  • Loading branch information
shenyih0ng authored Apr 2, 2024
1 parent 983a274 commit ce87563
Show file tree
Hide file tree
Showing 12 changed files with 1,051 additions and 733 deletions.
252 changes: 17 additions & 235 deletions src/go-slang/ece.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,19 @@
import { Context as SlangContext } from '..'
import { Stack } from '../cse-machine/utils'
import { RuntimeSourceError } from '../errors/runtimeSourceError'
import { Value } from '../types'
import { FuncArityError, UndefinedError, UnknownInstructionError } from './error'
import { Context, GoRoutine } from './goroutine'
import { AstMap } from './lib/astMap'
import { evaluateBinaryOp } from './lib/binaryOp'
import { Environment } from './lib/env'
import { Heap, HeapAddress } from './lib/heap'
import { PREDECLARED_FUNCTIONS, PREDECLARED_IDENTIFIERS } from './lib/predeclared'
import { zip, isAny } from './lib/utils'
import {
AssignOp,
Assignment,
BinaryExpression,
BinaryOp,
Block,
BranchOp,
BuiltinOp,
CallExpression,
CallOp,
ClosureOp,
CommandType,
EmptyStmt,
EnvOp,
ExpressionStatement,
ForEndMarker,
ForFormType,
ForPostMarker,
ForStartMarker,
ForStatement,
FunctionDeclaration,
Identifier,
IfStatement,
Instruction,
Literal,
NodeType,
Operator,
PopS,
PopTillM,
PopTillMOp,
RetMarker,
ReturnStatement,
SourceFile,
True,
UnaryExpression,
UnaryOp,
VarDeclOp,
VariableDeclaration
} from './types'

type Control = Stack<Instruction | HeapAddress>
type Stash = Stack<HeapAddress>
type Builtins = Map<number, (...args: any[]) => any>

interface Context {
C: Control
S: Stash
E: Environment
B: Builtins
H: Heap
A: AstMap
}

export function evaluate(program: SourceFile, slangContext: SlangContext): Value {
import { Scheduler } from './scheduler'
import { BuiltinOp, CallExpression, Instruction, NodeType, SourceFile } from './types'

function initMainGoRoutine(
program: SourceFile,
slangContext: SlangContext,
scheduler: Scheduler
): GoRoutine {
const C = new Stack<Instruction | HeapAddress>()
const S = new Stack<any>()
const E = new Environment({ ...PREDECLARED_IDENTIFIERS })
Expand All @@ -86,192 +37,23 @@ export function evaluate(program: SourceFile, slangContext: SlangContext): Value
B.set(id, func)
})

const Context = { C, S, E, B, H, A }

// start the program by calling `main`
// seed the `main` go routine with the program's `main` function
const CALL_MAIN: CallExpression = {
type: NodeType.CallExpression,
callee: { type: NodeType.Identifier, name: 'main' },
args: []
}
C.pushR(H.alloc(program), H.alloc(CALL_MAIN))

while (!C.isEmpty()) {
const inst = H.resolve(C.pop()) as Instruction

if (!interpreter.hasOwnProperty(inst.type)) {
slangContext.errors.push(new UnknownInstructionError(inst.type))
return undefined
}

const runtimeError = interpreter[inst.type](inst, Context)
if (runtimeError) {
slangContext.errors.push(runtimeError as RuntimeSourceError)
return undefined
}
}

return 'Program exited'
return new GoRoutine({ C, S, E, B, H, A } as Context, scheduler, true)
}

const interpreter: {
[key: string]: (inst: Instruction, context: Context) => RuntimeSourceError | void
} = {
SourceFile: ({ topLevelDecls }: SourceFile, { C, H }) => C.pushR(...H.allocM(topLevelDecls)),

FunctionDeclaration: (funcDeclNode: FunctionDeclaration, { E, A }) => {
const { id, uid: funcDeclNodeUid } = funcDeclNode
A.set(funcDeclNodeUid as number, funcDeclNode)
E.declare(id.name, { type: CommandType.ClosureOp, funcDeclNodeUid, envId: E.id() } as ClosureOp)
},

Block: ({ statements }: Block, { C, E, H }) => {
C.pushR(...H.allocM([...statements, { type: CommandType.EnvOp, envId: E.id() }]))
E.extend({})
},

ReturnStatement: ({ expression }: ReturnStatement, { C, H }) =>
C.pushR(H.alloc(expression), H.alloc(PopTillM(RetMarker))),

IfStatement: ({ stmt, cond, cons, alt }: IfStatement, { C, H }) => {
const branchOp: BranchOp = { type: CommandType.BranchOp, cons, alt }
stmt ? C.pushR(...H.allocM([stmt, cond, branchOp])) : C.pushR(...H.allocM([cond, branchOp]))
},

ForStatement: (inst: ForStatement, { C, H }) => {
const { form, block: forBlock } = inst
if (form === null || form.type === ForFormType.ForCondition) {
const branch = { type: CommandType.BranchOp, cons: forBlock, alt: PopTillM(ForEndMarker) }
C.pushR(
...H.allocM([
form ? form.expression : True,
branch as BranchOp,
ForStartMarker,
inst,
ForEndMarker
])
)
} else if (form.type === ForFormType.ForClause) {
const { init, cond, post } = form
const forCond = {
type: NodeType.ForStatement,
form: { type: ForFormType.ForCondition, expression: cond ?? True },
block: {
type: NodeType.Block,
statements: [
{ ...forBlock, statements: forBlock.statements.concat(ForPostMarker) },
post ?? EmptyStmt
]
}
} as ForStatement
C.push(H.alloc({ type: NodeType.Block, statements: [init ?? EmptyStmt, forCond] }))
}
},

BreakStatement: (_inst, { C, H }) => C.push(H.alloc(PopTillM(ForEndMarker))),

ContinueStatement: (_inst, { C, H }) => C.push(H.alloc(PopTillM(ForPostMarker, ForStartMarker))),

VariableDeclaration: ({ left, right }: VariableDeclaration, { C, H, A }) => {
const decls = A.trackM(left).map(({ uid }) => ({ type: CommandType.VarDeclOp, idNodeUid: uid }))
return right.length === 0
? // if there is no right side, we push zero value for each declaration
C.pushR(...H.allocM(decls.map(decl => ({ ...decl, zeroValue: true }))))
: // assume: left.length === right.length
C.pushR(...H.allocM(right), ...H.allocM(decls.reverse()))
},

Assignment: ({ left, right }: Assignment, { C, H, A }) => {
const ids = left as Identifier[] // assume: left is always an array of identifiers
const asgmts = ids.map(id => ({ type: CommandType.AssignOp, idNodeUid: A.track(id).uid }))
C.pushR(...H.allocM(right), ...H.allocM(asgmts.reverse()))
},

EmptyStatement: () => void {},

Literal: (inst: Literal, { S, H }) => S.push(H.alloc(inst.value)),

Identifier: ({ name }: Identifier, { S, E, H }) => {
const value = E.lookup(name)
return value === null ? new UndefinedError(name) : S.push(H.alloc(value))
},

UnaryExpression: ({ argument, operator: op }: UnaryExpression, { C, H, A }) =>
C.pushR(...H.allocM([argument, { type: CommandType.UnaryOp, opNodeId: A.track(op).uid }])),

BinaryExpression: ({ left, right, operator: op }: BinaryExpression, { C, H, A }) =>
C.pushR(...H.allocM([left, right, { type: CommandType.BinaryOp, opNodeId: A.track(op).uid }])),

CallExpression: ({ callee, args }: CallExpression, { C, H, A }) =>
C.pushR(
...H.allocM([
callee,
...args,
{ type: CommandType.CallOp, calleeNodeId: A.track(callee).uid, arity: args.length }
])
),

ExpressionStatement: ({ expression }: ExpressionStatement, { C, H }) =>
C.pushR(...H.allocM([expression, PopS])),

VarDeclOp: ({ idNodeUid, zeroValue }: VarDeclOp, { S, E, H, A }) => {
const name = A.get<Identifier>(idNodeUid).name
zeroValue ? E.declareZeroValue(name) : E.declare(name, H.resolve(S.pop()))
},

AssignOp: ({ idNodeUid }: AssignOp, { S, E, H, A }) => {
const name = A.get<Identifier>(idNodeUid).name
!E.assign(name, H.resolve(S.pop())) ? new UndefinedError(name) : void {}
},

UnaryOp: ({ opNodeId }: UnaryOp, { S, H, A }) => {
const operand = H.resolve(S.pop())
S.push(H.alloc(A.get<Operator>(opNodeId).op === '-' ? -operand : operand))
},

BinaryOp: ({ opNodeId }: BinaryOp, { S, H, A }) => {
const [left, right] = H.resolveM(S.popNR(2))
S.push(H.alloc(evaluateBinaryOp(A.get<Operator>(opNodeId).op, left, right)))
},

CallOp: ({ calleeNodeId, arity }: CallOp, { C, S, E, B, H, A }) => {
const values = H.resolveM(S.popNR(arity))
const op = H.resolve(S.pop()) as ClosureOp | BuiltinOp

// handle BuiltinOp
if (op.type === CommandType.BuiltinOp) {
return S.push(H.alloc(B.get(op.id)!(...values)))
}

// handle ClosureOp
const { funcDeclNodeUid, envId } = op
const { params, body } = A.get<FunctionDeclaration>(funcDeclNodeUid)
const paramNames = params.map(({ name }) => name)

if (paramNames.length !== values.length) {
const calleeId = A.get<Identifier>(calleeNodeId)
return new FuncArityError(calleeId.name, values.length, params.length)
}

C.pushR(...H.allocM([body, RetMarker, { type: CommandType.EnvOp, envId: E.id() }]))
// set the environment to the closure's environment
E.setId(envId).extend(Object.fromEntries(zip(paramNames, values)))
},

BranchOp: ({ cons, alt }: BranchOp, { S, C, H }) =>
void (H.resolve(S.pop()) ? C.pushR(H.alloc(cons)) : alt && C.pushR(H.alloc(alt))),

EnvOp: ({ envId }: EnvOp, { E }) => void E.setId(envId),

PopSOp: (_inst, { S }) => void S.pop(),

PopTillMOp: ({ markers }: PopTillMOp, { C }) => {
while (!C.isEmpty() && !isAny(C.pop(), markers)) {}
},
export function evaluate(program: SourceFile, slangContext: SlangContext): Value {
const scheduler = new Scheduler(slangContext)
const mainR = initMainGoRoutine(program, slangContext, scheduler)

RetMarker: () => void {},
scheduler.schedule(mainR)
scheduler.run()

ForStartMarker: () => void {},
ForPostMarker: () => void {},
ForEndMarker: () => void {}
return 'Program exited'
}
13 changes: 13 additions & 0 deletions src/go-slang/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,16 @@ export class FuncArityError extends RuntimeSourceError {
)
}
}

export class GoExprMustBeFunctionError extends RuntimeSourceError {
private expr: string

constructor(expr: string) {
super()
this.expr = expr
}

public explain() {
return `expression in go statement must be function call, not ${this.expr}`
}
}
Loading

0 comments on commit ce87563

Please sign in to comment.