Skip to content
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
23 changes: 16 additions & 7 deletions gs/builtin/channel.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GoPanic, RuntimeError, runtimePanic } from './panic.js'
import { queueTask } from './scheduler.js'

/**
Expand Down Expand Up @@ -371,6 +372,12 @@ export async function chanRecvWithOk<T>(
* @param direction Optional direction for the channel. Default is 'both' (bidirectional).
* @returns A new channel instance or channel reference.
*/
// closedSendPanic returns the Go runtime panic of a send on a closed channel,
// to throw or to reject a waiting send with.
function closedSendPanic(): GoPanic {
return new GoPanic(new RuntimeError('send on closed channel'))
}

export function makeChannel<T>(
bufferSize: number,
zeroValue: T,
Expand All @@ -391,6 +398,10 @@ export function makeChannel<T>(
zeroValue: T,
direction: 'send' | 'receive' | 'both' = 'both',
): Channel<T> | ChannelRef<T> {
// A negative size panics as in Go, so recover sees a runtime.Error.
if (bufferSize < 0) {
runtimePanic('makechan: size out of range')
}
const channel = new BufferedChannel<T>(bufferSize, zeroValue)

if (direction === 'send') {
Expand Down Expand Up @@ -439,7 +450,7 @@ class BufferedChannel<T> implements Channel<T> {

async send(value: T): Promise<void> {
if (this.closed) {
throw new Error('send on closed channel')
throw closedSendPanic()
}

// Attempt to hand off to a waiting receiver (rendezvous)
Expand Down Expand Up @@ -627,7 +638,7 @@ class BufferedChannel<T> implements Channel<T> {

trySelectSend(value: T, id: number): SelectResult<boolean> | undefined {
if (this.closed) {
throw new Error('send on closed channel')
throw closedSendPanic()
}
// Sends must hand off to already waiting receivers before using spare
// buffer capacity, including through the select fast path.
Expand Down Expand Up @@ -662,7 +673,7 @@ class BufferedChannel<T> implements Channel<T> {
onCommit?.()
// A select case sending on a closed channel panics in Go.
// This will cause Promise.race in selectStatement to reject.
throw new Error('send on closed channel')
throw closedSendPanic()
}

if (this.receivers.length > 0) {
Expand Down Expand Up @@ -736,16 +747,14 @@ class BufferedChannel<T> implements Channel<T> {

close(): void {
if (this.closed) {
throw new Error('close of closed channel')
runtimePanic('close of closed channel')
}
this.closed = true

const sendersToNotify = [...this.senders] // Shallow copy for iteration
this.senders = []
for (const senderTask of sendersToNotify) {
scheduleClosedChannelWake(() =>
senderTask.rejectSend(new Error('send on closed channel')),
)
scheduleClosedChannelWake(() => senderTask.rejectSend(closedSendPanic()))
}

const receiversToNotify = [...this.receivers]
Expand Down
3 changes: 2 additions & 1 deletion gs/builtin/map.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { comparableEqual } from './builtin.js'
import { runtimePanic } from './panic.js'
import {
canonicalPointerIdentity,
isStructObject,
Expand Down Expand Up @@ -230,7 +231,7 @@ export function mapGet<K, V, D>(
// mapSet preserves the existing equal key when replacing its value.
export const mapSet = <K, V>(map: Map<K, V> | null, key: K, value: V): void => {
if (!map) {
throw new Error('assign to nil map')
runtimePanic('assignment to entry in nil map')
}
const canonical = goMapStructKey(map, key)
if (canonical !== undefined) {
Expand Down
46 changes: 46 additions & 0 deletions gs/builtin/panic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
recovered,
runtimePanic,
} from './panic.js'
import { makeChannel } from './channel.js'
import { mapSet } from './map.js'
import { goSlice, index, makeSlice } from './slice.js'
import { DisposableStack } from './defer.js'

Expand Down Expand Up @@ -92,6 +94,50 @@ describe('runtime panics route through GoPanic', () => {
)
})

it('makechan with negative size panics with the Go message', () => {
const caught = captureThrow(() => makeChannel(-1, 0, 'both'))

expect(caught).toBeInstanceOf(GoPanic)
expect(((caught as GoPanic).value as RuntimeError).Error()).toBe(
'makechan: size out of range',
)
})

it('assignment to a nil map panics with the Go message', () => {
const caught = captureThrow(() => mapSet(null, 'k', 1))

expect(caught).toBeInstanceOf(GoPanic)
expect(((caught as GoPanic).value as RuntimeError).Error()).toBe(
'assignment to entry in nil map',
)
})

it('close of a closed channel panics with the Go message', () => {
const ch = makeChannel(0, 0, 'both')
ch.close()
const caught = captureThrow(() => ch.close())

expect(caught).toBeInstanceOf(GoPanic)
expect(((caught as GoPanic).value as RuntimeError).Error()).toBe(
'close of closed channel',
)
})

it('send on a closed channel panics with the Go message', async () => {
const ch = makeChannel(1, 0, 'both')
ch.close()
const caught = await ch.send(1).then(
() => undefined,
(e: unknown) => e,
)

expect(caught).toBeInstanceOf(GoPanic)
expect(((caught as GoPanic).value as RuntimeError).Error()).toBe(
'send on closed channel',
)
recovered(caught as GoPanic)
})

it('panic carries an arbitrary value that panicValue unwraps', () => {
const caught = captureThrow(() => panic('boom'))

Expand Down
2 changes: 1 addition & 1 deletion gs/net/http/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ describe('net/http override', () => {
expect(written.Len()).toBe(0)
Header_Del(null, 'Content-Type')
expect(() => Header_Set(null, 'Content-Type', 'text/plain')).toThrow(
'assign to nil map',
'assignment to entry in nil map',
)
})

Expand Down
Loading