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: garbage collection #26

Merged
merged 5 commits into from
Apr 9, 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
16 changes: 5 additions & 11 deletions src/go-slang/ece.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ import { PREDECLARED_FUNCTIONS, PREDECLARED_IDENTIFIERS } from './lib/predeclare
import { Scheduler } from './scheduler'
import { BuiltinOp, CallExpression, Instruction, NodeType, SourceFile } from './types'

function initMainGoRoutineCtx(program: SourceFile, slangContext: SlangContext): Context {
export function evaluate(program: SourceFile, slangContext: SlangContext): Value {
const scheduler = new Scheduler(slangContext)

const C = new Stack<Instruction | HeapAddress>()
const S = new Stack<any>()
const E = new Environment({ ...PREDECLARED_IDENTIFIERS })

// `SourceFile` is the root node of the AST which has latest (monotonically increasing) uid of all AST nodes
// Therefore, the next uid to be used to track AST nodes is the uid of SourceFile + 1
const A = new AstMap((program.uid as number) + 1)
const H = new Heap(A)
const H = new Heap(A, scheduler)

// inject predeclared functions into the global environment
const B = new Map<number, (...args: any[]) => any>()
Expand All @@ -41,14 +42,7 @@ function initMainGoRoutineCtx(program: SourceFile, slangContext: SlangContext):
}
C.pushR(H.alloc(program), H.alloc(CALL_MAIN))

return { C, S, E, B, H, A } as Context
}

export function evaluate(program: SourceFile, slangContext: SlangContext): Value {
const scheduler = new Scheduler(slangContext)
const mainRoutineCtx = initMainGoRoutineCtx(program, slangContext)

scheduler.spawn(mainRoutineCtx, true)
scheduler.spawn({ C, S, E, B, H, A } as Context, true)
scheduler.run()

return 'Program exited'
Expand Down
6 changes: 6 additions & 0 deletions src/go-slang/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,9 @@ export class DeadLockError extends RuntimeSourceError {
return 'all goroutines are asleep - deadlock!'
}
}

export class OutOfMemoryError extends RuntimeSourceError {
public explain() {
return 'runtime: out of memory'
}
}
92 changes: 58 additions & 34 deletions src/go-slang/goroutine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import {
CallExpression,
CallOp,
ChanRecv,
ChanRecvOp,
ChanSend,
ChanSendOp,
ClosureOp,
CommandType,
EmptyStmt,
Expand Down Expand Up @@ -102,6 +104,19 @@ export class GoRoutine {
this.isMain = isMain
}

public activeHeapAddresses(): Set<HeapAddress> {
const activeAddrSet = new Set<HeapAddress>()

// roots: Control, Stash, Environment
const { C, S, E } = this.context

const isHeapAddr = (addr: any): addr is HeapAddress => typeof addr === 'number'
C.getStack().filter(isHeapAddr).forEach(addr => activeAddrSet.add(addr)) // prettier-ignore
S.getStack().filter(isHeapAddr).forEach(addr => activeAddrSet.add(addr)) // prettier-ignore

return new Set([...activeAddrSet, ...E.activeHeapAddresses()])
}

public tick(): Result<GoRoutineState, RuntimeSourceError> {
const { C, H } = this.context
const inst = H.resolve(C.pop()) as Instruction
Expand All @@ -111,15 +126,20 @@ export class GoRoutine {
return Result.fail(new UnknownInstructionError(inst.type))
}

const nextState =
Interpreter[inst.type](inst, this.context, this.scheduler, this.id) ??
Result.ok(C.isEmpty() ? GoRoutineState.Exited : GoRoutineState.Running)
try {
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
this.progress = this.prevInst !== inst
this.prevInst = inst
this.state = nextState.isSuccess ? nextState.unwrap() : GoRoutineState.Exited
this.progress = this.prevInst !== inst
this.prevInst = inst

return nextState
return nextState
} catch (error) {
this.state = GoRoutineState.Exited
return Result.fail(error)
}
}
}

Expand All @@ -133,20 +153,23 @@ const Interpreter: {
} = {
SourceFile: ({ topLevelDecls }: SourceFile, { C, H }) => C.pushR(...H.allocM(topLevelDecls)),

FunctionDeclaration: (funcDeclNode: FunctionDeclaration, { E, A }) =>
E.declare(funcDeclNode.id.name, {
type: CommandType.ClosureOp,
funcDeclNodeUid: A.track(funcDeclNode).uid,
envId: E.id()
} as ClosureOp),
FunctionDeclaration: (funcDeclNode: FunctionDeclaration, { E, H, A }) =>
E.declare(
funcDeclNode.id.name,
H.alloc({
type: CommandType.ClosureOp,
funcDeclNodeUid: A.track(funcDeclNode).uid,
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))),
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 }
Expand All @@ -156,14 +179,14 @@ const Interpreter: {
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) }
const branch = { type: CommandType.BranchOp, cons: forBlock, alt: PopTillM(ForEndMarker()) }
C.pushR(
...H.allocM([
form ? form.expression : True,
branch as BranchOp,
ForStartMarker,
ForStartMarker(),
inst,
ForEndMarker
ForEndMarker()
])
)
} else if (form.type === ForFormType.ForClause) {
Expand All @@ -174,7 +197,7 @@ const Interpreter: {
block: {
type: NodeType.Block,
statements: [
{ ...forBlock, statements: forBlock.statements.concat(ForPostMarker) },
{ ...forBlock, statements: forBlock.statements.concat(ForPostMarker()) },
post ?? EmptyStmt
]
}
Expand All @@ -183,9 +206,10 @@ const Interpreter: {
}
},

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

ContinueStatement: (_inst, { C, H }) => C.push(H.alloc(PopTillM(ForPostMarker, ForStartMarker))),
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 }))
Expand Down Expand Up @@ -222,7 +246,7 @@ const Interpreter: {
},

SendStatement: ({ channel, value }: SendStatement, { C, H }) =>
C.pushR(...H.allocM([value, channel, ChanSend])),
C.pushR(...H.allocM([value, channel, ChanSend()])),

EmptyStatement: () => void {},

Expand All @@ -241,7 +265,7 @@ const Interpreter: {

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

UnaryExpression: ({ argument, operator: op }: UnaryExpression, { C, H, A }) =>
Expand All @@ -264,18 +288,18 @@ const Interpreter: {

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()))
zeroValue ? E.declareZeroValue(name) : E.declare(name, S.pop())
},

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

UnaryOp: ({ opNodeId }: UnaryOp, { C, S, H, A }) => {
const operator = A.get<Operator>(opNodeId).op

if (operator === '<-') { return C.push(ChanRecv) } // prettier-ignore
if (operator === '<-') { return C.push(ChanRecv()) } // prettier-ignore

const operand = H.resolve(S.pop())
return S.push(H.alloc(operator === '-' ? -operand : operand))
Expand Down Expand Up @@ -308,9 +332,9 @@ const Interpreter: {
)
}

C.pushR(...H.allocM([body, RetMarker, { type: CommandType.EnvOp, envId: E.id() }]))
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)))
E.setId(envId).extend(Object.fromEntries(zip(paramNames, H.allocM(values))))
},

// TODO: should we combine it with CallOp? there is a couple of duplicated logic
Expand All @@ -335,18 +359,18 @@ const Interpreter: {
const _S: Stash = new Stack()
const _E = E.copy()
.setId(envId)
.extend(Object.fromEntries(zip(paramNames, values)))
.extend(Object.fromEntries(zip(paramNames, H.allocM(values))))

return void sched.spawn({ C: _C, S: _S, E: _E, B, H, A } as Context)
},

ChanRecvOp: (_inst, { C, S, H }, _sched, routineId: number) => {
ChanRecvOp: (inst: ChanRecvOp, { 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()) {
C.push(ChanRecv)
C.push(inst)
return Result.ok(GoRoutineState.Blocked)
}
S.pop() // pop the channel address
Expand All @@ -357,21 +381,21 @@ const Interpreter: {
const recvValue = chan.recv(routineId)
// if we cannot receive, we retry the receive operation
if (recvValue === null) {
C.push(ChanRecv)
C.push(inst)
return Result.ok(GoRoutineState.Blocked)
}
S.pop() // pop the channel address
return S.push(H.alloc(recvValue))
}
},

ChanSendOp: (_inst, { C, S, H }, _sched, routineId: number) => {
ChanSendOp: (inst: ChanSendOp, { C, S, H }, _sched, routineId: number) => {
const [chan, sendValue] = H.resolveM(S.peekN(2)!) as [BufferedChannel | UnbufferedChannel, any]

if (chan instanceof BufferedChannel) {
// if the channel is full, we retry the send operation
if (chan.isBufferFull()) {
C.push(ChanSend)
C.push(inst)
return Result.ok(GoRoutineState.Blocked)
}
S.popN(2) // pop the channel address and the value address
Expand All @@ -381,7 +405,7 @@ const Interpreter: {
if (chan instanceof UnbufferedChannel) {
// if we cannot send, we retry the send operation
if (!chan.send(routineId, sendValue)) {
C.push(ChanSend)
C.push(inst)
return Result.ok(GoRoutineState.Blocked)
}
return void S.popN(2) // pop the channel address and the value address
Expand Down
19 changes: 19 additions & 0 deletions src/go-slang/lib/env.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HeapAddress } from './heap'
import { Counter } from './utils'

type Maybe<T> = T | null
Expand Down Expand Up @@ -75,4 +76,22 @@ export class Environment {
newEnv.frameIdCounter = this.frameIdCounter
return newEnv
}

public activeHeapAddresses(): Set<HeapAddress> {
const activeAddrSet = new Set<HeapAddress>()

this.frameMap.forEach(frame => {
let curr = frame
while (curr) {
frame.bindings.forEach(value => {
if (typeof value === 'number') {
activeAddrSet.add(value)
}
})
curr = curr.parent as Frame
}
})

return activeAddrSet
}
}
3 changes: 3 additions & 0 deletions src/go-slang/lib/heap/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ export const DEFAULT_HEAP_SIZE = 4096 // in words
// The smallest addressable unit in the heap
// We can think of it as the heap containing N number of words, each of size WORD_SIZE
export const WORD_SIZE = 8 // in bytes

// The byte offset to the size of a heap object within a tagged pointer
export const SIZE_OFFSET = 5 // in bytes
Loading
Loading