From eee88001dd89c6fa07e7698ec27fcbc41fce788d Mon Sep 17 00:00:00 2001 From: Levi Redlin Date: Sun, 12 Oct 2025 11:20:47 -0500 Subject: [PATCH] Refactor select to not create objects --- model/channel/channel.go | 430 ++++++++++++++++------------------ model/channel/channel_test.go | 322 +++++++------------------ 2 files changed, 287 insertions(+), 465 deletions(-) diff --git a/model/channel/channel.go b/model/channel/channel.go index fdc55a47..33998a0f 100644 --- a/model/channel/channel.go +++ b/model/channel/channel.go @@ -286,261 +286,229 @@ const ( SelectRecv SelectDir = 1 // case <-Chan: ) -// value is used for the value the sender will send and also used to return the received value by -// reference. -type SelectCase[T any] struct { - channel *Channel[T] - dir SelectDir - Value T - Ok bool -} - -func NewSendCase[T any](channel *Channel[T], value T) *SelectCase[T] { - return &SelectCase[T]{ - channel: channel, - dir: SelectSend, - Value: value, - } -} - -func NewRecvCase[T any](channel *Channel[T]) *SelectCase[T] { - return &SelectCase[T]{ - channel: channel, - dir: SelectRecv, +// Non-blocking select with 1 case (send or receive) +// For receive: value parameter is ignored +// Returns (selected, received_value, ok) +func NonBlockingSelect1[T any](ch *Channel[T], dir SelectDir, value T) (bool, T, bool) { + var zero T + + if dir == SelectSend { + selected := ch.TrySend(value, false) + return selected, zero, false + } else { // SelectRecv + selected, recv_val, ok := ch.TryReceive(false) + return selected, recv_val, ok } } -// Uses the applicable Try function on the select case's channel. Default is always -// selectable so simply returns true. -func TrySelect[T any](select_case *SelectCase[T], blocking bool) bool { - var channel *Channel[T] = select_case.channel - if channel == nil { - return false - } - if select_case.dir == SelectSend { - return channel.TrySend(select_case.Value, blocking) - } - if select_case.dir == SelectRecv { - var item T - var ok bool - var selected bool - selected, item, ok = channel.TryReceive(blocking) - // We can use these values for return by reference and they will be implicitly kept alive - // by the garbage collector so we can use value here for both the send and receive - // variants. What a miracle it is to not be using C++. - select_case.Value = item - select_case.Ok = ok - return selected +// Blocking select with 2 cases +// Returns (caseIndex, received_value1, received_value2, ok) +func BlockingSelect2[T1, T2 any]( + ch1 *Channel[T1], dir1 SelectDir, val1 T1, + ch2 *Channel[T2], dir2 SelectDir, val2 T2) (uint64, T1, T2, bool) { - } - return false -} + var zero1 T1 + var zero2 T2 -// Select1 performs a select operation on 1 case. This is used for Send and -// Receive as well, since these channel operations in Go are equivalent to -// a single case select statement with no default. -func Select1[T1 any]( - case1 *SelectCase[T1], - blocking bool) bool { - var selected bool for { - selected = TrySelect(case1, blocking) - if selected || !blocking { - break + // Flip coin each iteration + if primitive.RandomUint64()%2 == 0 { + // Try case 1 + if dir1 == SelectSend { + if ch1.TrySend(val1, true) { + return 0, zero1, zero2, false + } + } else { + selected, recv_val, ok := ch1.TryReceive(true) + if selected { + return 0, recv_val, zero2, ok + } + } + } else { + // Try case 2 + if dir2 == SelectSend { + if ch2.TrySend(val2, true) { + return 1, zero1, zero2, false + } + } else { + selected, recv_val, ok := ch2.TryReceive(true) + if selected { + return 1, zero1, recv_val, ok + } + } } } - return selected -} - -func TrySelectCase2[T1, T2 any]( - index uint64, - case1 *SelectCase[T1], - case2 *SelectCase[T2], blocking bool) bool { - if index == 0 { - return TrySelect(case1, blocking) - } - if index == 1 { - return TrySelect(case2, blocking) - } - panic("index needs to be 0 or 1") } -func Select2[T1, T2 any]( - case1 *SelectCase[T1], - case2 *SelectCase[T2], - blocking bool) uint64 { - - i := primitive.RandomUint64() % uint64(2) - if TrySelectCase2(i, case1, case2, blocking) { - return i - } - - // If nothing was selected and we're blocking, try in a loop - for { - if TrySelect(case1, blocking) { - return 0 - } - if TrySelect(case2, blocking) { - return 1 - } - if !blocking { - return 2 +// Non-blocking select with 2 cases +// Returns (caseIndex, received_value1, received_value2, ok) +// caseIndex = 2 means no selection +func NonBlockingSelect2[T1, T2 any]( + ch1 *Channel[T1], dir1 SelectDir, val1 T1, + ch2 *Channel[T2], dir2 SelectDir, val2 T2) (uint64, T1, T2, bool) { + + var zero1 T1 + var zero2 T2 + + // Randomize which case to try first + if primitive.RandomUint64()%2 == 0 { + // Try case 1 first + if dir1 == SelectSend { + if ch1.TrySend(val1, false) { + return 0, zero1, zero2, false + } + } else { + selected, recv_val, ok := ch1.TryReceive(false) + if selected { + return 0, recv_val, zero2, ok + } } - } -} -func TrySelectCase3[T1, T2, T3 any]( - index uint64, - case1 *SelectCase[T1], - case2 *SelectCase[T2], - case3 *SelectCase[T3], blocking bool) bool { - if index == 0 { - return TrySelect(case1, blocking) - } - if index == 1 { - return TrySelect(case2, blocking) - } - if index == 2 { - return TrySelect(case3, blocking) - } - panic("index needs to be 0, 1 or 2") -} - -func Select3[T1, T2, T3 any]( - case1 *SelectCase[T1], - case2 *SelectCase[T2], - case3 *SelectCase[T3], - blocking bool) uint64 { - - i := primitive.RandomUint64() % uint64(3) - if TrySelectCase3(i, case1, case2, case3, blocking) { - return i - } - - for { - if TrySelect(case1, blocking) { - return 0 - } - if TrySelect(case2, blocking) { - return 1 + // Try case 2 + if dir2 == SelectSend { + if ch2.TrySend(val2, false) { + return 1, zero1, zero2, false + } + } else { + selected, recv_val, ok := ch2.TryReceive(false) + if selected { + return 1, zero1, recv_val, ok + } } - if TrySelect(case3, blocking) { - return 2 + } else { + // Try case 2 first + if dir2 == SelectSend { + if ch2.TrySend(val2, false) { + return 1, zero1, zero2, false + } + } else { + selected, recv_val, ok := ch2.TryReceive(false) + if selected { + return 1, zero1, recv_val, ok + } } - if !blocking { - return 3 + + // Try case 1 + if dir1 == SelectSend { + if ch1.TrySend(val1, false) { + return 0, zero1, zero2, false + } + } else { + selected, recv_val, ok := ch1.TryReceive(false) + if selected { + return 0, recv_val, zero2, ok + } } } -} -func TrySelectCase4[T1, T2, T3, T4 any]( - index uint64, - case1 *SelectCase[T1], - case2 *SelectCase[T2], - case3 *SelectCase[T3], - case4 *SelectCase[T4], blocking bool) bool { - if index == 0 { - return TrySelect(case1, blocking) - } - if index == 1 { - return TrySelect(case2, blocking) - } - if index == 2 { - return TrySelect(case3, blocking) - } - if index == 3 { - return TrySelect(case4, blocking) - } - panic("index needs to be 0, 1, 2 or 3") + // Nothing selected + return 2, zero1, zero2, false } -func Select4[T1, T2, T3, T4 any]( - case1 *SelectCase[T1], - case2 *SelectCase[T2], - case3 *SelectCase[T3], - case4 *SelectCase[T4], - blocking bool) uint64 { - - i := primitive.RandomUint64() % uint64(4) - if TrySelectCase4(i, case1, case2, case3, case4, blocking) { - return i - } - +func BlockingSelect3[T1, T2, T3 any]( + ch1 *Channel[T1], dir1 SelectDir, val1 T1, + ch2 *Channel[T2], dir2 SelectDir, val2 T2, + ch3 *Channel[T3], dir3 SelectDir, val3 T3) (uint64, T1, T2, T3, bool) { + var zero1 T1 + var zero2 T2 + var zero3 T3 for { - if TrySelect(case1, blocking) { - return 0 - } - if TrySelect(case2, blocking) { - return 1 - } - if TrySelect(case3, blocking) { - return 2 - } - if TrySelect(case4, blocking) { - return 3 - } - if !blocking { - return 4 + // Randomly pick one of 3 cases + r := primitive.RandomUint64() % 3 + if r == 0 { + // Try case 1 + if dir1 == SelectSend { + if ch1.TrySend(val1, true) { + return 0, zero1, zero2, zero3, false + } + } else { + selected, recv_val, ok := ch1.TryReceive(true) + if selected { + return 0, recv_val, zero2, zero3, ok + } + } + } else if r == 1 { + // Try case 2 + if dir2 == SelectSend { + if ch2.TrySend(val2, true) { + return 1, zero1, zero2, zero3, false + } + } else { + selected, recv_val, ok := ch2.TryReceive(true) + if selected { + return 1, zero1, recv_val, zero3, ok + } + } + } else { + // Try case 3 + if dir3 == SelectSend { + if ch3.TrySend(val3, true) { + return 2, zero1, zero2, zero3, false + } + } else { + selected, recv_val, ok := ch3.TryReceive(true) + if selected { + return 2, zero1, zero2, recv_val, ok + } + } } } } -func TrySelectCase5[T1, T2, T3, T4, T5 any]( - index uint64, - case1 *SelectCase[T1], - case2 *SelectCase[T2], - case3 *SelectCase[T3], - case4 *SelectCase[T4], - case5 *SelectCase[T5], blocking bool) bool { - if index == 0 { - return TrySelect(case1, blocking) - } - if index == 1 { - return TrySelect(case2, blocking) - } - if index == 2 { - return TrySelect(case3, blocking) - } - if index == 3 { - return TrySelect(case4, blocking) - } - if index == 4 { - return TrySelect(case5, blocking) - } - panic("index needs to be 0, 1, 2, 3 or 4") -} - -func Select5[T1, T2, T3, T4, T5 any]( - case1 *SelectCase[T1], - case2 *SelectCase[T2], - case3 *SelectCase[T3], - case4 *SelectCase[T4], - case5 *SelectCase[T5], - blocking bool) uint64 { - - i := primitive.RandomUint64() % uint64(5) - if TrySelectCase5(i, case1, case2, case3, case4, case5, blocking) { - return i - } - - for { - if TrySelect(case1, blocking) { - return 0 - } - if TrySelect(case2, blocking) { - return 1 - } - if TrySelect(case3, blocking) { - return 2 - } - if TrySelect(case4, blocking) { - return 3 - } - if TrySelect(case5, blocking) { - return 4 - } - if !blocking { - return 5 +// Non-blocking select with 3 cases +// Returns (caseIndex, received_value1, received_value2, received_value3, ok) +// caseIndex = 3 means no selection +func NonBlockingSelect3[T1, T2, T3 any]( + ch1 *Channel[T1], dir1 SelectDir, val1 T1, + ch2 *Channel[T2], dir2 SelectDir, val2 T2, + ch3 *Channel[T3], dir3 SelectDir, val3 T3) (uint64, T1, T2, T3, bool) { + var zero1 T1 + var zero2 T2 + var zero3 T3 + + // Start with a random case + start := primitive.RandomUint64() % 3 + + // Try all 3 cases starting from the random one + for i := uint64(0); i < 3; i++ { + caseIdx := (start + i) % 3 + + if caseIdx == 0 { + if dir1 == SelectSend { + if ch1.TrySend(val1, false) { + return 0, zero1, zero2, zero3, false + } + } else { + selected, recv_val, ok := ch1.TryReceive(false) + if selected { + return 0, recv_val, zero2, zero3, ok + } + } + } else if caseIdx == 1 { + if dir2 == SelectSend { + if ch2.TrySend(val2, false) { + return 1, zero1, zero2, zero3, false + } + } else { + selected, recv_val, ok := ch2.TryReceive(false) + if selected { + return 1, zero1, recv_val, zero3, ok + } + } + } else { // caseIdx == 2 + if dir3 == SelectSend { + if ch3.TrySend(val3, false) { + return 2, zero1, zero2, zero3, false + } + } else { + selected, recv_val, ok := ch3.TryReceive(false) + if selected { + return 2, zero1, zero2, recv_val, ok + } + } } } + + // Nothing selected + return 3, zero1, zero2, zero3, false } diff --git a/model/channel/channel_test.go b/model/channel/channel_test.go index 777584f7..be42b476 100644 --- a/model/channel/channel_test.go +++ b/model/channel/channel_test.go @@ -73,7 +73,7 @@ func TestChan(t *testing.T) { if atomic.LoadUint32(&sent) != 0 { t.Fatalf("chan[%d]: send to full chan", chanCap) } - selected := c.TrySend(0, false) + selected, _, _ := channel.NonBlockingSelect1(c, channel.SelectSend, 0) if selected { t.Fatalf("chan[%d]: send to full chan", chanCap) } @@ -629,7 +629,7 @@ func TestNonblockRecvRace(t *testing.T) { c := channel.NewChannelRef[uint64](1) c.Send(1) go func() { - selected, _, _ := c.TryReceive(false) + selected, _, _ := channel.NonBlockingSelect1(c, channel.SelectRecv, 0) if !selected { t.Error("chan is not ready") } @@ -721,9 +721,7 @@ func doRequest(useSelect bool) (*response, error) { if useSelect { go func() { - case_1 := channel.NewSendCase[*async](ch, &async{resp: nil, err: myError{}}) - case_2 := channel.NewRecvCase[struct{}](done) - selected_case := channel.Select2(case_1, case_2, true) + selected_case, _, _, _ := channel.BlockingSelect2(ch, channel.SelectSend, &async{resp: nil, err: myError{}}, done, channel.SelectRecv, struct{}{}) // These cases don't actually do anything but wanted to stick with the intended // translation throughout this file. if selected_case == 0 { @@ -803,9 +801,7 @@ func TestNonblockSelectRace(t *testing.T) { c2 := channel.NewChannelRef[int](1) c1.Send(1) go func() { - case_1 := channel.NewRecvCase(c1) - case_2 := channel.NewRecvCase(c2) - selected_case := channel.Select2(case_1, case_2, false) + selected_case, _, _, _ := channel.NonBlockingSelect2(c1, channel.SelectRecv, 0, c2, channel.SelectRecv, 0) if selected_case == 0 { } if selected_case == 1 { @@ -835,13 +831,13 @@ func TestNonblockSelectRace2(t *testing.T) { c2 := channel.NewChannelRef[int](1) c1.Send(1) go func() { - case_1 := channel.NewRecvCase(c1) - case_2 := channel.NewRecvCase(c2) - selected_case := channel.Select2(case_1, case_2, false) + selected_case, _, _, _ := channel.NonBlockingSelect2( + c1, channel.SelectRecv, 0, + c2, channel.SelectRecv, 0) + if selected_case == 0 { } if selected_case == 1 { - } if selected_case == 2 { done.Send(false) @@ -850,7 +846,7 @@ func TestNonblockSelectRace2(t *testing.T) { done.Send(true) }() c2.Close() - c1.TryReceive(false) + channel.NonBlockingSelect1(c1, channel.SelectRecv, 0) val := done.ReceiveDiscardOk() if !val { t.Fatal("no chan is ready") @@ -874,24 +870,24 @@ func TestSelfSelect(t *testing.T) { defer wg.Done() for i := uint64(0); i < 1000; i++ { if p == 0 || i%2 == 0 { - case_1 := channel.NewSendCase(c, p) - case_2 := channel.NewRecvCase(c) - selected_case := channel.Select2(case_1, case_2, true) + selected_case, _, recv_val, _ := channel.BlockingSelect2( + c, channel.SelectSend, p, + c, channel.SelectRecv, 0) if selected_case == 0 { break } else if selected_case == 1 { - if chanCap == 0 && case_2.Value == p { + if chanCap == 0 && recv_val == p { t.Errorf("self receive") return } break } } else { - case_1 := channel.NewRecvCase(c) - case_2 := channel.NewSendCase(c, p) - selected_case := channel.Select2(case_1, case_2, true) + selected_case, recv_val, _, _ := channel.BlockingSelect2( + c, channel.SelectRecv, 0, + c, channel.SelectSend, p) if selected_case == 0 { - if chanCap == 0 && case_1.Value == p { + if chanCap == 0 && recv_val == p { t.Errorf("self receive") return } @@ -914,14 +910,12 @@ func TestSelectLivenessOrder1(t *testing.T) { c2 := channel.NewChannelRef[uint64](uint64(2)) c1.Close() c2.Send(0) - - case_1 := channel.NewRecvCase(c1) - case_2 := channel.NewRecvCase(c2) - c1_selected := false c2_selected := false for { - selected_case := channel.Select2(case_1, case_2, false) + selected_case, _, _, _ := channel.NonBlockingSelect2( + c1, channel.SelectRecv, 0, + c2, channel.SelectRecv, 0) // Make sure we eventually hit the second case if selected_case == 0 { c1_selected = true @@ -931,9 +925,7 @@ func TestSelectLivenessOrder1(t *testing.T) { if c1_selected && c2_selected { break } - } - } // Same as above but swap the case order to make sure it works symmetrically i.e. the @@ -941,27 +933,24 @@ func TestSelectLivenessOrder1(t *testing.T) { func TestSelectLivenessOrder2(t *testing.T) { c1 := channel.NewChannelRef[uint64](uint64(0)) c2 := channel.NewChannelRef[uint64](uint64(1)) - case_1 := channel.NewRecvCase(c1) - case_2 := channel.NewRecvCase(c2) - c1.Close() c2.Send(0) c1_selected := false c2_selected := false for { - selected_case := channel.Select2(case_2, case_1, false) + selected_case, _, _, _ := channel.NonBlockingSelect2( + c2, channel.SelectRecv, 0, + c1, channel.SelectRecv, 0) // Make sure we eventually hit the second case if selected_case == 0 { - c1_selected = true - } else if selected_case == 1 { c2_selected = true + } else if selected_case == 1 { + c1_selected = true } if c1_selected && c2_selected { break } - } - } // Make sure if we keep selecting and 1 case is immediately selectable we still can choose a case @@ -969,20 +958,19 @@ func TestSelectLivenessOrder2(t *testing.T) { func TestSelectLivenessNotImmediatelySelectable(t *testing.T) { c1 := channel.NewChannelRef[uint64](uint64(0)) c2 := channel.NewChannelRef[uint64](uint64(0)) - case_1 := channel.NewRecvCase(c1) - case_2 := channel.NewRecvCase(c2) - c1.Close() c1_selected := false c2_selected := false go func() { for { - selected_case := channel.Select2(case_2, case_1, false) + selected_case, _, _, _ := channel.NonBlockingSelect2( + c2, channel.SelectRecv, 0, + c1, channel.SelectRecv, 0) // Make sure we eventually hit the second case if selected_case == 0 { - c1_selected = true - } else if selected_case == 1 { c2_selected = true + } else if selected_case == 1 { + c1_selected = true } if c1_selected && c2_selected { break @@ -991,7 +979,6 @@ func TestSelectLivenessNotImmediatelySelectable(t *testing.T) { }() time.Sleep(time.Millisecond * 10) c2.Send(0) - } // Make sure a selectable buffered channel case isn't selected every time if it @@ -1001,24 +988,20 @@ func TestSelectFairnessWithBufferedChannel(t *testing.T) { c1 := channel.NewChannelRef[int](1) // Buffered (capacity 1) c2 := channel.NewChannelRef[int](0) // Unbuffered - // Create select cases - buffered channel first - case1 := channel.NewRecvCase(c1) - case2 := channel.NewRecvCase(c2) - // Put data in the buffered channel to make it immediately ready c1.Send(42) // Channel to signal test completion done := channel.NewChannelRef[bool](0) - buffered_selected := false unbuffered_selected := false // Start a goroutine that selects until both channels have been chosen go func() { for { - selected_case := channel.Select2(case1, case2, true) - + selected_case, _, _, _ := channel.BlockingSelect2( + c1, channel.SelectRecv, 0, + c2, channel.SelectRecv, 0) if selected_case == 0 { buffered_selected = true // Refill the buffered channel @@ -1026,7 +1009,6 @@ func TestSelectFairnessWithBufferedChannel(t *testing.T) { } else if selected_case == 1 { unbuffered_selected = true } - if buffered_selected && unbuffered_selected { done.Send(true) return @@ -1051,44 +1033,40 @@ func TestSelect1(t *testing.T) { // preload c1 c1.Send(66) - // build the case (generic used explicitly) - case1 := channel.NewRecvCase(c1) - // non-blocking: should pick the first case (index 0) - selected := channel.Select1(case1, false) + selected, recv_val, ok := channel.NonBlockingSelect1(c1, channel.SelectRecv, 0) if !selected { t.Error("expected selected") } - // and confirm the SelectCase struct was populated - if case1.Value != 66 { - t.Errorf("expected case1.Value=66, got %v", case1.Value) + // and confirm the values were returned correctly + if recv_val != 66 { + t.Errorf("expected recv_val=66, got %v", recv_val) } - if !case1.Ok { - t.Error("expected case1.Ok=true, got false") + if !ok { + t.Error("expected ok=true, got false") } // Create a new empty channel for testing non-blocking behavior emptyC1 := channel.NewChannelRef[uint64](1) - emptyCase1 := channel.NewRecvCase(emptyC1) - // With blocking=false and no selectable statement, should return DefaultCase - selected = channel.Select1(emptyCase1, false) + // With blocking=false and no selectable statement, should return selected=false + selected, _, _ = channel.NonBlockingSelect1(emptyC1, channel.SelectRecv, 0) if selected { t.Error("expected !selected when non-blocking with no available case") } // Close the channel and test receive on closed channel emptyC1.Close() - selected = channel.Select1(emptyCase1, true) + selected, recv_val, ok = channel.NonBlockingSelect1(emptyC1, channel.SelectRecv, 0) if !selected { t.Error("expected selected for closed channel") } - if emptyCase1.Ok { - t.Error("expected emptyCase1.Ok=false for closed channel, got true") + if ok { + t.Error("expected ok=false for closed channel, got true") } - if emptyCase1.Value != 0 { - t.Errorf("expected emptyCase1.Value=0 (zero value) for closed channel, got %v", emptyCase1.Value) + if recv_val != 0 { + t.Errorf("expected recv_val=0 (zero value) for closed channel, got %v", recv_val) } } @@ -1099,47 +1077,47 @@ func TestSelect2(t *testing.T) { // preload c2 c2.Send(77) - // build the cases (generic used explicitly) - case1 := channel.NewRecvCase(c1) - case2 := channel.NewRecvCase(c2) - // non-blocking: should pick the second case (index 1) - idx := channel.Select2(case1, case2, false) + idx, val1, val2, ok := channel.NonBlockingSelect2( + c1, channel.SelectRecv, 0, + c2, channel.SelectRecv, 0) if idx != 1 { t.Errorf("expected selected index=1, got %d", idx) } - // and confirm the SelectCase struct was populated - if case2.Value != 77 { - t.Errorf("expected case2.Value=77, got %v", case2.Value) + // and confirm the values were returned correctly + if val2 != 77 { + t.Errorf("expected val2=77, got %v", val2) } - if !case2.Ok { - t.Error("expected case2.Ok=true, got false") + if !ok { + t.Error("expected ok=true, got false") } // Create new empty channels for testing non-blocking behavior emptyC1 := channel.NewChannelRef[uint64](1) emptyC2 := channel.NewChannelRef[uint64](1) - emptyCase1 := channel.NewRecvCase(emptyC1) - emptyCase2 := channel.NewRecvCase(emptyC2) // With blocking=false and no selectable statement, should return DefaultCase - idx = channel.Select2(emptyCase1, emptyCase2, false) + idx, _, _, _ = channel.NonBlockingSelect2( + emptyC1, channel.SelectRecv, 0, + emptyC2, channel.SelectRecv, 0) if idx != 2 { - t.Errorf("expected selected index=3 when non-blocking with no available case, got %d", idx) + t.Errorf("expected selected index=2 when non-blocking with no available case, got %d", idx) } // Close a channel and test receive on closed channel emptyC1.Close() - idx = channel.Select2(emptyCase1, emptyCase2, true) + idx, val1, _, ok = channel.BlockingSelect2( + emptyC1, channel.SelectRecv, 0, + emptyC2, channel.SelectRecv, 0) if idx != 0 { t.Errorf("expected selected index=0 for closed channel, got %d", idx) } - if emptyCase1.Ok { - t.Error("expected emptyCase1.Ok=false for closed channel, got true") + if ok { + t.Error("expected ok=false for closed channel, got true") } - if emptyCase1.Value != 0 { - t.Errorf("expected emptyCase1.Value=0 (zero value) for closed channel, got %v", emptyCase1.Value) + if val1 != 0 { + t.Errorf("expected val1=0 (zero value) for closed channel, got %v", val1) } } @@ -1151,186 +1129,63 @@ func TestSelect3(t *testing.T) { // preload c3 c3.Send(88) - // build the cases (generic used explicitly) - case1 := channel.NewRecvCase(c1) - case2 := channel.NewRecvCase(c2) - case3 := channel.NewRecvCase(c3) - // non-blocking: should pick the third case (index 2) - idx := channel.Select3(case1, case2, case3, false) + idx, _, val2, val3, ok := channel.NonBlockingSelect3( + c1, channel.SelectRecv, 0, + c2, channel.SelectRecv, 0, + c3, channel.SelectRecv, 0) if idx != 2 { t.Errorf("expected selected index=2, got %d", idx) } - // and confirm the SelectCase struct was populated - if case3.Value != 88 { - t.Errorf("expected case3.Value=88, got %v", case3.Value) + // and confirm the values were returned correctly + if val3 != 88 { + t.Errorf("expected val3=88, got %v", val3) } - if !case3.Ok { - t.Error("expected case3.Ok=true, got false") + if !ok { + t.Error("expected ok=true, got false") } // Create new empty channels for testing non-blocking behavior emptyC1 := channel.NewChannelRef[uint64](1) emptyC2 := channel.NewChannelRef[uint64](1) emptyC3 := channel.NewChannelRef[uint64](1) - emptyCase1 := channel.NewRecvCase(emptyC1) - emptyCase2 := channel.NewRecvCase(emptyC2) - emptyCase3 := channel.NewRecvCase(emptyC3) // With blocking=false and no selectable statement, should return DefaultCase - idx = channel.Select3(emptyCase1, emptyCase2, emptyCase3, false) + idx, _, _, _, _ = channel.NonBlockingSelect3( + emptyC1, channel.SelectRecv, 0, + emptyC2, channel.SelectRecv, 0, + emptyC3, channel.SelectRecv, 0) if idx != 3 { t.Errorf("expected selected index=3 when non-blocking with no available case, got %d", idx) } // Close a channel and test receive on closed channel emptyC2.Close() - idx = channel.Select3(emptyCase1, emptyCase2, emptyCase3, true) + idx, _, val2, _, ok = channel.BlockingSelect3( + emptyC1, channel.SelectRecv, 0, + emptyC2, channel.SelectRecv, 0, + emptyC3, channel.SelectRecv, 0) if idx != 1 { t.Errorf("expected selected index=1 for closed channel, got %d", idx) } - if emptyCase2.Ok { - t.Error("expected emptyCase2.Ok=false for closed channel, got true") - } - if emptyCase2.Value != 0 { - t.Errorf("expected emptyCase2.Value=0 (zero value) for closed channel, got %v", emptyCase2.Value) - } -} - -func TestSelect4(t *testing.T) { - // Four buffered channels so we can preload one without blocking - c1 := channel.NewChannelRef[uint64](1) // capacity=1 - c2 := channel.NewChannelRef[uint64](1) // capacity=1 - c3 := channel.NewChannelRef[uint64](1) // capacity=1 - c4 := channel.NewChannelRef[uint64](1) // capacity=1 - // preload c4 - c4.Send(99) - - // build the cases (generic used explicitly) - case1 := channel.NewRecvCase(c1) - case2 := channel.NewRecvCase(c2) - case3 := channel.NewRecvCase(c3) - case4 := channel.NewRecvCase(c4) - - // non-blocking: should pick the fourth case (index 3) - idx := channel.Select4(case1, case2, case3, case4, false) - if idx != 3 { - t.Errorf("expected selected index=3, got %d", idx) - } - - // and confirm the SelectCase struct was populated - if case4.Value != 99 { - t.Errorf("expected case4.Value=99, got %v", case4.Value) - } - if !case4.Ok { - t.Error("expected case4.Ok=true, got false") - } - - // Create new empty channels for testing non-blocking behavior - emptyC1 := channel.NewChannelRef[uint64](1) - emptyC2 := channel.NewChannelRef[uint64](1) - emptyC3 := channel.NewChannelRef[uint64](1) - emptyC4 := channel.NewChannelRef[uint64](1) - emptyCase1 := channel.NewRecvCase(emptyC1) - emptyCase2 := channel.NewRecvCase(emptyC2) - emptyCase3 := channel.NewRecvCase(emptyC3) - emptyCase4 := channel.NewRecvCase(emptyC4) - - // With blocking=false and no selectable statement, should return DefaultCase - idx = channel.Select4(emptyCase1, emptyCase2, emptyCase3, emptyCase4, false) - if idx != 4 { - t.Errorf("expected selected index=4 when non-blocking with no available case, got %d", idx) + if ok { + t.Error("expected ok=false for closed channel, got true") } - - // Close a channel and test receive on closed channel - emptyC3.Close() - idx = channel.Select4(emptyCase1, emptyCase2, emptyCase3, emptyCase4, true) - if idx != 2 { - t.Errorf("expected selected index=2 for closed channel, got %d", idx) - } - if emptyCase3.Ok { - t.Error("expected emptyCase3.Ok=false for closed channel, got true") - } - if emptyCase3.Value != 0 { - t.Errorf("expected emptyCase3.Value=0 (zero value) for closed channel, got %v", emptyCase3.Value) - } -} - -func TestSelect5(t *testing.T) { - // Five buffered channels so we can preload one without blocking - c1 := channel.NewChannelRef[uint64](1) // capacity=1 - c2 := channel.NewChannelRef[uint64](1) // capacity=1 - c3 := channel.NewChannelRef[uint64](1) // capacity=1 - c4 := channel.NewChannelRef[uint64](1) // capacity=1 - c5 := channel.NewChannelRef[uint64](1) // capacity=1 - // preload c5 - c5.Send(111) - - // build the cases (generic used explicitly) - case1 := channel.NewRecvCase(c1) - case2 := channel.NewRecvCase(c2) - case3 := channel.NewRecvCase(c3) - case4 := channel.NewRecvCase(c4) - case5 := channel.NewRecvCase(c5) - - // non-blocking: should pick the fifth case (index 4) - idx := channel.Select5(case1, case2, case3, case4, case5, false) - if idx != 4 { - t.Errorf("expected selected index=4, got %d", idx) - } - - // and confirm the SelectCase struct was populated - if case5.Value != 111 { - t.Errorf("expected case5.Value=111, got %v", case5.Value) - } - if !case5.Ok { - t.Error("expected case5.Ok=true, got false") - } - - // Create new empty channels for testing non-blocking behavior - emptyC1 := channel.NewChannelRef[uint64](1) - emptyC2 := channel.NewChannelRef[uint64](1) - emptyC3 := channel.NewChannelRef[uint64](1) - emptyC4 := channel.NewChannelRef[uint64](1) - emptyC5 := channel.NewChannelRef[uint64](1) - emptyCase1 := channel.NewRecvCase(emptyC1) - emptyCase2 := channel.NewRecvCase(emptyC2) - emptyCase3 := channel.NewRecvCase(emptyC3) - emptyCase4 := channel.NewRecvCase(emptyC4) - emptyCase5 := channel.NewRecvCase(emptyC5) - - // With blocking=false and no selectable statement, should return DefaultCase - idx = channel.Select5(emptyCase1, emptyCase2, emptyCase3, emptyCase4, emptyCase5, false) - if idx != 5 { - t.Errorf("expected selected index=5 when non-blocking with no available case, got %d", idx) - } - - // Close a channel and test receive on closed channel - emptyC4.Close() - idx = channel.Select5(emptyCase1, emptyCase2, emptyCase3, emptyCase4, emptyCase5, true) - if idx != 3 { - t.Errorf("expected selected index=3 for closed channel, got %d", idx) - } - if emptyCase4.Ok { - t.Error("expected emptyCase4.Ok=false for closed channel, got true") - } - if emptyCase4.Value != 0 { - t.Errorf("expected emptyCase4.Value=0 (zero value) for closed channel, got %v", emptyCase4.Value) + if val2 != 0 { + t.Errorf("expected val2=0 (zero value) for closed channel, got %v", val2) } } // Two non blocking selects should not match up. func Test2NBSelectNoProgress(t *testing.T) { c1 := channel.NewChannelRef[uint64](uint64(0)) - case_1 := channel.NewRecvCase(c1) - case_2 := channel.NewSendCase(c1, 0) // Run the receiver loop in a goroutine doneRecv := make(chan struct{}) go func() { for { - selected := channel.Select1(case_1, false) + selected, _, _ := channel.NonBlockingSelect1(c1, channel.SelectRecv, 0) if selected { break } @@ -1342,7 +1197,7 @@ func Test2NBSelectNoProgress(t *testing.T) { doneSend := make(chan struct{}) go func() { for { - selected := channel.Select1(case_2, false) + selected, _, _ := channel.NonBlockingSelect1(c1, channel.SelectSend, 0) if selected { break } @@ -1352,7 +1207,6 @@ func Test2NBSelectNoProgress(t *testing.T) { // Use a timeout: test passes if both loops never make progress timeout := time.After(2 * time.Second) - select { case <-doneRecv: t.Fatal("unexpected progress in recv loop: expected to block")