Skip to content

Commit

Permalink
feat: mark sweep garbage collector
Browse files Browse the repository at this point in the history
  • Loading branch information
shenyih0ng committed Apr 9, 2024
1 parent 4936178 commit 417c56a
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 17 deletions.
2 changes: 1 addition & 1 deletion src/go-slang/ece.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function evaluate(program: SourceFile, slangContext: SlangContext): Value
// `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 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'
}
}
32 changes: 25 additions & 7 deletions src/go-slang/goroutine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,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 @@ -113,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 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
}
}
64 changes: 57 additions & 7 deletions src/go-slang/lib/heap/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { OutOfMemoryError } from '../../error'
import { Scheduler } from '../../scheduler'
import {
AssignOp,
BinaryOp,
Expand Down Expand Up @@ -30,23 +32,29 @@ function ceilPow2(n: number): number {
return 2 ** Math.ceil(Math.log2(n))
}

function buddyAddr(addr: number, bIdx: number): number {
function getBuddyAddr(addr: number, bIdx: number): number {
return addr ^ (1 << bIdx)
}

export type HeapAddress = number

export class Heap {
private memory: DataView
public memory: DataView
// buddyBlocks[i] stores the set of free blocks of size 2^i
// free blocks are represented by their starting address
private buddyBlocks: Array<Set<number>>
// map of heap addresses to their corresponding block size
private buddyBlockMap: Map<number, number> = new Map()

// we need to keep track of the AstMap to be able to resolve AST nodes stored in the heap
private astMap: AstMap
// keep a reference to the scheduler to be able to get active routines for garbage collection
// NOTE: this is hacky and should be refactored
private scheduler: Scheduler

constructor(astMap: AstMap, n_words?: number) {
constructor(astMap: AstMap, scheduler: Scheduler, n_words?: number) {
this.astMap = astMap
this.scheduler = scheduler

const totalBytes = (n_words ?? DEFAULT_HEAP_SIZE) * WORD_SIZE
this.memory = new DataView(new ArrayBuffer(totalBytes))
Expand All @@ -68,6 +76,7 @@ export class Heap {

const addr = this.buddyBlocks[bIdx].values().next().value
this.buddyBlocks[bIdx].delete(addr)
this.buddyBlockMap.set(addr, bIdx)
return addr
}

Expand All @@ -84,10 +93,20 @@ export class Heap {
this.buddyBlocks[idx].delete(addr)

this.buddyBlocks[idx - 1].add(addr)
this.buddyBlocks[idx - 1].add(buddyAddr(addr, idx - 1))
this.buddyBlocks[idx - 1].add(getBuddyAddr(addr, idx - 1))
return true
}

private buddyFree(addr: HeapAddress): void {
const bIdx = this.buddyBlockMap.get(addr)
if (bIdx === undefined) {
throw new Error('Free is utilized on a non-allocated memory address.')
}

this.buddyBlocks[bIdx].add(addr)
this.buddyBlockMap.delete(addr)
}

/**
* Allocate a value in the heap
*
Expand Down Expand Up @@ -165,7 +184,7 @@ export class Heap {
return heap_addr
}

const tag = this.memory.getInt8(heap_addr)
const tag = this.tag(heap_addr)
switch (tag) {
case PointerTag.False:
return false
Expand Down Expand Up @@ -352,8 +371,29 @@ export class Heap {
* @returns Address of the allocated block
*/
private allocateTaggedPtr(tag: PointerTag, size: number = 0): HeapAddress {
// TODO handle out of memory error
const alloc_heap_addr = this.buddyAlloc((size + 1) * WORD_SIZE)
let alloc_heap_addr = this.buddyAlloc((size + 1) * WORD_SIZE)

if (alloc_heap_addr === -1) {
// perform garbage collection
const activeHeapAddresses = new Set<HeapAddress>()
for (const gr of this.scheduler.activeGoRoutines) {
for (const addr of gr.activeHeapAddresses()) {
activeHeapAddresses.add(addr)
}
}

this.buddyBlockMap.forEach((_, addr) => {
if (!activeHeapAddresses.has(addr) && this.tag(addr) !== PointerTag.AstNode) {
this.buddyFree(addr)
}
})

// retry allocation
alloc_heap_addr = this.buddyAlloc((size + 1) * WORD_SIZE)
// if allocation still fails, we hard fail
if (alloc_heap_addr === -1) { throw new OutOfMemoryError() } // prettier-ignore
}

// set the tag (1 byte) of the block
this.memory.setInt8(alloc_heap_addr, tag)
// set the size (2 bytes) of the underlying data structure
Expand All @@ -362,6 +402,16 @@ export class Heap {
return alloc_heap_addr
}

/**
* Get the tag of the tagged pointer
*
* @param heap_addr
* @returns tag of the tagged pointer
*/
private tag(heap_addr: HeapAddress): PointerTag {
return this.memory.getInt8(heap_addr)
}

/**
* Get the size of the underlying data structure of the tagged pointer
*
Expand Down
15 changes: 13 additions & 2 deletions src/go-slang/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Context as SlangContext } from '..'
import { GoRoutine, GoRoutineState, Context } from './goroutine'
import { RuntimeSourceError } from '../errors/runtimeSourceError'
import { Counter, benchmark } from './lib/utils'
import { DeadLockError } from './error'
import { DeadLockError, OutOfMemoryError } from './error'

type TimeQuanta = number

Expand All @@ -15,6 +15,7 @@ export class Scheduler {

private slangContext: SlangContext
private routines: Array<[GoRoutine, TimeQuanta]> = []

constructor(slangContext: SlangContext) {
this.slangContext = slangContext
}
Expand All @@ -38,19 +39,25 @@ export class Scheduler {
let numConsecNoProgress = 0

while (this.routines.length && numConsecNoProgress < this.routines.length) {
const [routine, timeQuanta] = this.routines.shift() as [GoRoutine, TimeQuanta]
const [routine, timeQuanta] = this.routines[0] as [GoRoutine, TimeQuanta]

let remainingTime = timeQuanta
while (remainingTime--) {
const result = routine.tick()
if (result.isFailure) {
this.slangContext.errors.push(result.error as RuntimeSourceError)

// if the routine runs out of memory, we terminate the program
if (result.error instanceof OutOfMemoryError) { return } // prettier-ignore

break
}
// if the routine is no longer running we schedule it out
if (result.unwrap() !== GoRoutineState.Running) { break } // prettier-ignore
}

this.routines.shift() // remove the routine from the queue

// once main exits, the other routines are terminated and the program exits
if (routine.isMain && routine.state === GoRoutineState.Exited) { return } // prettier-ignore

Expand All @@ -66,4 +73,8 @@ export class Scheduler {
// if we reach here, all routines are blocked
this.slangContext.errors.push(new DeadLockError())
}

public get activeGoRoutines(): Array<GoRoutine> {
return this.routines.map(([routine]) => routine)
}
}

0 comments on commit 417c56a

Please sign in to comment.