chan.close does not unblock subsequent chan.receive (blocks forever instead of returning false) #499

Closed
opened 2026-05-20 03:14:39 +00:00 by navicore · 1 comment
Owner

Summary

A chan.receive on a closed and fully-drained channel blocks indefinitely instead of returning ( default false ). This contradicts the behavior documented in the seqlings curriculum (exercises/24-channels/README.md and exercises/24-channels/04-close.seq).

Behavior reproduces both in single-strand code and in the standard CSP cross-strand pattern (producer strand sends, closes, exits; consumer reads past end).

Affects: seqc 7.4.0

Documented behavior

exercises/24-channels/README.md:

Closing Channels

chan chan.close   # Signal no more values will be sent

After closing:

  • Sends fail
  • Receives return remaining values, then fail

exercises/24-channels/04-close.seq:

After closing:

  • Remaining values can still be received
  • New sends will return false
  • After all values received, receives return false

Reproducer 1 — single strand

: test-min-single ( -- )
    chan.make
    dup 42 swap chan.send drop
    dup chan.close
    dup chan.receive drop drop   # receive 42 — succeeds
    chan.receive                 # expected ( default false ); actually blocks
    [ drop false test.assert ]
    [ drop true  test.assert ] if
;

seqc test test-min-single.seq never returns; aborted by 4-second timeout.

Reproducer 2 — cross-strand (canonical CSP pattern)

: producer ( Channel -- )
    dup 10 swap chan.send drop
    dup 20 swap chan.send drop
    chan.close                    # consumes channel; producer strand exits
;

: test-min-repro ( -- )
    chan.make
    [ producer ] strand.spawn drop

    dup chan.receive drop drop    # receive 10 — succeeds
    dup chan.receive drop drop    # receive 20 — succeeds

    chan.receive                  # expected ( default false ); actually blocks
    [ drop false test.assert ]
    [ drop true  test.assert ] if
;

Same outcome: hangs at the third chan.receive. The producer strand has fully exited after chan.close, so no sender-side references remain.

Expected behavior

Per the documented contract, the third chan.receive should return ( default false ) once the channel is closed and drained. The if would then take the else branch and the test would pass.

Actual behavior

The third chan.receive blocks forever — no other strand will ever send to the channel, but the runtime does not detect the closed-and-drained condition.

Impact

This blocks the canonical CSP "producer closes, consumer loops on success-flag" pattern in any user code. In the seqlings curriculum it blocks a planned ch 35 (Little's Law) redesign that needs recursive worker strands terminating on a closed work channel; we're pivoting to a fixed K-workers-for-K-items pattern as a workaround. The README at exercises/24-channels/README.md should also be updated to match whichever behavior is intended.

Probable area

crates/runtime channel implementation — specifically the path that decides whether a blocked chan.receive should wake with false when the queue is empty and the close flag is set.

## Summary A `chan.receive` on a closed and fully-drained channel blocks indefinitely instead of returning `( default false )`. This contradicts the behavior documented in the seqlings curriculum (`exercises/24-channels/README.md` and `exercises/24-channels/04-close.seq`). Behavior reproduces both in single-strand code and in the standard CSP cross-strand pattern (producer strand sends, closes, exits; consumer reads past end). **Affects:** `seqc 7.4.0` ## Documented behavior `exercises/24-channels/README.md`: > ## Closing Channels > ```seq > chan chan.close # Signal no more values will be sent > ``` > After closing: > - Sends fail > - Receives return remaining values, then fail `exercises/24-channels/04-close.seq`: > After closing: > - Remaining values can still be received > - New sends will return false > - After all values received, receives return false ## Reproducer 1 — single strand ```seq : test-min-single ( -- ) chan.make dup 42 swap chan.send drop dup chan.close dup chan.receive drop drop # receive 42 — succeeds chan.receive # expected ( default false ); actually blocks [ drop false test.assert ] [ drop true test.assert ] if ; ``` `seqc test test-min-single.seq` never returns; aborted by 4-second timeout. ## Reproducer 2 — cross-strand (canonical CSP pattern) ```seq : producer ( Channel -- ) dup 10 swap chan.send drop dup 20 swap chan.send drop chan.close # consumes channel; producer strand exits ; : test-min-repro ( -- ) chan.make [ producer ] strand.spawn drop dup chan.receive drop drop # receive 10 — succeeds dup chan.receive drop drop # receive 20 — succeeds chan.receive # expected ( default false ); actually blocks [ drop false test.assert ] [ drop true test.assert ] if ; ``` Same outcome: hangs at the third `chan.receive`. The producer strand has fully exited after `chan.close`, so no sender-side references remain. ## Expected behavior Per the documented contract, the third `chan.receive` should return `( default false )` once the channel is closed and drained. The `if` would then take the else branch and the test would pass. ## Actual behavior The third `chan.receive` blocks forever — no other strand will ever send to the channel, but the runtime does not detect the closed-and-drained condition. ## Impact This blocks the canonical CSP "producer closes, consumer loops on success-flag" pattern in any user code. In the seqlings curriculum it blocks a planned ch 35 (Little's Law) redesign that needs recursive worker strands terminating on a closed work channel; we're pivoting to a fixed K-workers-for-K-items pattern as a workaround. The README at `exercises/24-channels/README.md` should also be updated to match whichever behavior is intended. ## Probable area `crates/runtime` channel implementation — specifically the path that decides whether a blocked `chan.receive` should wake with `false` when the queue is empty and the close flag is set.
navicore referenced this issue from a commit 2026-05-20 17:22:17 +00:00
Author
Owner
https://git.navicore.tech/navicore/patch-seq/pulls/502
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
navicore/patch-seq#499
No description provided.