Skip to content

Commit

Permalink
feat: buffered channels (#20)
Browse files Browse the repository at this point in the history
* feat: add `TypeLiteral` support in parser

* feat: implement `make` builtin function

Added ECE types for `TypeLiteral` and `make` related constructs

* feat: implement abstraction for `Channel`

Includes methods to `send` and `recv`

* refactor: make use of getters and setters for `Channel`

* refactor: force `GoStatement` to only take in `CallExpression`

This is to temporarily fix the case where the parser mistakenly parse
the a a call expression as a binary expression when a `GoStatement` is
followed by a `ReceiveExpression`

* chore: add doc to `make` (to note only buffered chans are supported now)

* feat: add `SendStatement`, `ChanSendOp` and `ChanRecvOp` ECE types

* feat: add `Channel` support in heap

* feat: implement buffered channel support in ECE

* refactor: implement circular queue for buffered channel

* refactor: remove pushing `chanAddr` on recv/send retry

* fix: expressions should not have optional line terminator in grammar

Only a `ExpressionStatement` should look out for a line/file terminator
  • Loading branch information
shenyih0ng authored Apr 4, 2024
1 parent ad70fb8 commit 0c73caf
Show file tree
Hide file tree
Showing 9 changed files with 504 additions and 188 deletions.
15 changes: 15 additions & 0 deletions src/go-slang/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ export class UnknownInstructionError extends RuntimeSourceError {
}
}

export class InvalidOperationError extends RuntimeSourceError {
private errorMessage: string

public location: NodeLocation

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

public explain(): string {
return `invalid operation: ${this.errorMessage}`
}
}

export class UndefinedError extends RuntimeSourceError {
private identifier: string

Expand Down
52 changes: 46 additions & 6 deletions src/go-slang/goroutine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { RuntimeSourceError } from '../errors/runtimeSourceError'
import {
FuncArityError,
GoExprMustBeFunctionError,
InvalidOperationError,
UndefinedError,
UnknownInstructionError
} from './error'
Expand All @@ -17,11 +18,14 @@ import {
Assignment,
BinaryExpression,
BinaryOp,
BinaryOperator,
Block,
BranchOp,
BuiltinOp,
CallExpression,
CallOp,
ChanRecv,
ChanSend,
ClosureOp,
CommandType,
EmptyStmt,
Expand All @@ -46,18 +50,22 @@ import {
PopTillMOp,
RetMarker,
ReturnStatement,
SendStatement,
SourceFile,
True,
TypeLiteral,
UnaryExpression,
UnaryOp,
VarDeclOp,
VariableDeclaration
} from './types'
import { Scheduler } from './scheduler'
import { PredeclaredFuncT } from './lib/predeclared'
import { BufferedChannel } from './lib/channel'

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

export interface Context {
C: Control
Expand Down Expand Up @@ -194,10 +202,15 @@ const Interpreter: {
)
},

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

EmptyStatement: () => void {},

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

TypeLiteral: (inst: TypeLiteral, { S }) => S.push(inst),

Identifier: ({ name, loc }: Identifier, { S, E, H }) => {
const value = E.lookup(name)
return value === null ? new UndefinedError(name, loc!) : S.push(H.alloc(value))
Expand Down Expand Up @@ -231,14 +244,18 @@ const Interpreter: {
!E.assign(id.name, H.resolve(S.pop())) ? new UndefinedError(id.name, id.loc!) : void {}
},

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

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

const operand = H.resolve(S.pop())
S.push(H.alloc(A.get<Operator>(opNodeId).op === '-' ? -operand : operand))
return S.push(H.alloc(operator === '-' ? -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)))
S.push(H.alloc(evaluateBinaryOp(A.get<Operator>(opNodeId).op as BinaryOperator, left, right)))
},

CallOp: ({ calleeNodeId, arity }: CallOp, { C, S, E, B, H, A }) => {
Expand All @@ -247,7 +264,8 @@ const Interpreter: {

// handle BuiltinOp
if (op.type === CommandType.BuiltinOp) {
return S.push(H.alloc(B.get(op.id)!(...values)))
const result = B.get(op.id)!(...values)
return result instanceof InvalidOperationError ? result : S.push(H.alloc(result))
}

// handle ClosureOp
Expand Down Expand Up @@ -290,6 +308,28 @@ const Interpreter: {
return sched.schedule(new GoRoutine({ C: _C, S: _S, E: _E, B, H, A }, sched))
},

ChanRecvOp: (_inst, { C, S, H }) => {
const chanAddr = S.peek()
const chan = H.resolve(chanAddr) as BufferedChannel

// if the channel is empty, we retry the receive operation
if (chan.isBufferEmpty()) { return C.push(ChanRecv) } // prettier-ignore

S.pop()
S.push(H.alloc(chan.recv()))
},

ChanSendOp: (_inst, { C, S, H }) => {
const chanAddr = S.peek()
const chan = H.resolve(chanAddr) as BufferedChannel

// if the channel is full, we retry the send operation
if (chan.isBufferFull()) { return C.push(ChanSend) } // prettier-ignore

const [_, valueAddr] = S.popN(2)
chan.send(H.resolve(valueAddr))
},

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

Expand Down
76 changes: 76 additions & 0 deletions src/go-slang/lib/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { WORD_SIZE } from './heap/config'

/* prettier-ignore */
class Channel {
protected memory: DataView

constructor(memory: DataView) { this.memory = memory }

protected get maxBufSize(): number { return this.memory.getUint16(5) }

protected getSlotAddr(slotIdx: number): number { return (slotIdx + 1) * WORD_SIZE }

protected getSlotValue(slotIdx: number): number { return this.memory.getFloat64(this.getSlotAddr(slotIdx)) }

protected setSlotValue(slotIdx: number, value: number): void { this.memory.setFloat64(this.getSlotAddr(slotIdx), value) }
}

export class BufferedChannel extends Channel {
static READ_IDX_OFFSET = 1
static WRITE_IDX_OFFSET = 2
static BUF_SIZE_OFFSET = 3

constructor(memory: DataView) { super(memory) } // prettier-ignore

public send(value: any): boolean {
if (this.isBufferFull()) { return false } // prettier-ignore

// enqueue
this.setSlotValue(this.writeIdx++ % this.maxBufSize, value)
this.bufSize++

return true
}

public recv(): number | null {
if (this.isBufferEmpty()) { return null } // prettier-ignore

// dequeue
const value = this.getSlotValue(this.readIdx++ % this.maxBufSize)
this.bufSize--

return value
}

public isBufferFull(): boolean {
return this.bufSize === this.maxBufSize
}

public isBufferEmpty(): boolean {
return this.bufSize === 0
}

private get readIdx(): number {
return this.memory.getUint8(BufferedChannel.READ_IDX_OFFSET)
}

private set readIdx(newReadIdx: number) {
this.memory.setUint8(BufferedChannel.READ_IDX_OFFSET, newReadIdx)
}

private get writeIdx(): number {
return this.memory.getUint8(BufferedChannel.WRITE_IDX_OFFSET)
}

private set writeIdx(newWriteIdx: number) {
this.memory.setUint8(BufferedChannel.WRITE_IDX_OFFSET, newWriteIdx)
}

private get bufSize(): number {
return this.memory.getUint8(BufferedChannel.BUF_SIZE_OFFSET)
}

private set bufSize(newBufSize: number) {
this.memory.setUint8(BufferedChannel.BUF_SIZE_OFFSET, newBufSize)
}
}
66 changes: 58 additions & 8 deletions src/go-slang/lib/heap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import {
CommandType,
EnvOp,
GoRoutineOp,
MakeChannel,
Node,
PopS,
Type,
UnaryOp,
VarDeclOp,
isCommand,
isMake,
isNode
} from '../../types'
import { AstMap } from '../astMap'
import { BufferedChannel } from '../channel'
import { DEFAULT_HEAP_SIZE, WORD_SIZE } from './config'
import { PointerTag } from './tags'

Expand Down Expand Up @@ -86,6 +90,15 @@ export class Heap {
}
}

// Make operation
if (isMake(value)) {
switch (value.type) {
case Type.Channel:
const { size: bufSize } = value as MakeChannel
return bufSize === 0 ? this.allocateUnbufferedChan() : this.allocateBufferedChan(bufSize)
}
}

return value
}

Expand Down Expand Up @@ -159,6 +172,15 @@ export class Heap {
} as EnvOp
case PointerTag.PopSOp:
return PopS
case PointerTag.BufferedChannel:
const chanMaxBufSize = this.size(heap_addr)
const chanMemRegion = new DataView(
this.memory.buffer,
mem_addr,
// +1 to include the tagged pointer
WORD_SIZE * (chanMaxBufSize + 1)
)
return new BufferedChannel(chanMemRegion)
}
}

Expand Down Expand Up @@ -276,6 +298,24 @@ export class Heap {
return ptr_heap_addr
}

public allocateUnbufferedChan(): HeapAddress {
throw new Error('allocateUnbufferedChan not implemented.')
}

/* Memory Layout of an BufferedChannel:
* [0:tag, 1:readIdx, 2:writeIdx, 3:bufSize, 4:_unused_, 5-6:bufSize, 7:_unused_] (1 + `size` words)
*/
public allocateBufferedChan(size: number): HeapAddress {
const ptr_heap_addr = this.allocateTaggedPtr(PointerTag.BufferedChannel, size)

const ptr_mem_addr = ptr_heap_addr * WORD_SIZE
this.memory.setUint8(ptr_mem_addr + 1, 0) // initialize read index to 0
this.memory.setUint8(ptr_mem_addr + 2, 0) // initialize write index to 0
this.memory.setUint8(ptr_mem_addr + 3, 0) // initialize buffer size to 0

return ptr_heap_addr
}

/**
* Allocate a tagged pointer in the heap
*
Expand Down Expand Up @@ -306,30 +346,40 @@ export class Heap {
/**
* Get the tag of the block at the given address
*
* @param heap_addr n_words to the block
* @param heap_addr
* @returns tag of the block
*/
private tag(heap_addr: HeapAddress): PointerTag {
return this.memory.getInt8(heap_addr * WORD_SIZE)
}

/**
* Get the size of the underlying data structure of the tagged pointer
*
* @param heap_addr
* @returns size of the underlying data structure
*/
private size(heap_addr: HeapAddress): number {
return this.memory.getUint16(heap_addr * WORD_SIZE + 5)
}

/**
* Get the raw word value at the given address
*
* @param address n_words to the query block
* @param heap_addr
* @returns raw word value at the given address
*/
private get(address: HeapAddress): number {
return this.memory.getFloat64(address * WORD_SIZE)
private get(heap_addr: HeapAddress): number {
return this.memory.getFloat64(heap_addr * WORD_SIZE)
}

/**
* Set word value at the given address
*
* @param address n_words to the block
* @param value value to be set
* @param heap_addr
* @param value value to be set
*/
private set(address: number, value: number): void {
this.memory.setFloat64(address * WORD_SIZE, value)
private set(heap_addr: number, value: number): void {
this.memory.setFloat64(heap_addr * WORD_SIZE, value)
}
}
3 changes: 2 additions & 1 deletion src/go-slang/lib/heap/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export enum PointerTag {
BuiltInOp,
ClosureOp,
EnvOp,
PopSOp
PopSOp,
BufferedChannel
}
Loading

0 comments on commit 0c73caf

Please sign in to comment.