https://github.com/akkartik/mu/blob/master/075channel.mu

      1 # Mu synchronizes between routines using channels rather than locks, like
      2 # Erlang and Go.
      3 #
      4 # The key property of channels: Writing to a full channel or reading from an
      5 # empty one will put the current routine in 'waiting' state until the
      6 # operation can be completed.
      7 #
      8 # Beware of addresses passed into channels. They can cause race conditions.
      9
     10 scenario channel [
     11   run [
     12     local-scope
     13     source:&:source:num, sink:&:sink:num <- new-channel 3/capacity
     14     sink <- write sink, 34
     15     10:num/raw, 11:bool/raw, source <- read source
     16   ]
     17   memory-should-contain [
     18     10 <- 34
     19     11 <- 0  # read was successful
     20   ]
     21 ]
     22
     23 container channel:_elem [
     24   lock:bool  # inefficient but simple: serialize all reads as well as writes
     25   first-full:num  # for write
     26   first-free:num  # for read
     27   # A circular buffer contains values from index first-full up to (but not
     28   # including) index first-free. The reader always modifies it at first-full,
     29   # while the writer always modifies it at first-free.
     30   data:&:@:_elem
     31 ]
     32
     33 # Since channels have two ends, and since it's an error to use either end from
     34 # multiple routines, let's distinguish the ends.
     35
     36 container source:_elem [
     37   chan:&:channel:_elem
     38 ]
     39
     40 container sink:_elem [
     41   chan:&:channel:_elem
     42 ]
     43
     44 def new-channel capacity:num -> in:&:source:_elem, out:&:sink:_elem [
     45   local-scope
     46   load-inputs
     47   result:&:channel:_elem <- new {(channel _elem): type}
     48   *result <- put *result, first-full:offset, 0
     49   *result <- put *result, first-free:offset, 0
     50   capacity <- add capacity, 1  # unused slot for 'full?' below
     51   data:&:@:_elem <- new _elem:type, capacity
     52   *result <- put *result, data:offset, data
     53   in <- new {(source _elem): type}
     54   *in <- put *in, chan:offset, result
     55   out <- new {(sink _elem): type}
     56   *out <- put *out, chan:offset, result
     57 ]
     58
     59 # write a value to a channel
     60 def write out:&:sink:_elem, val:_elem -> out:&:sink:_elem [
     61   local-scope
     62   load-inputs
     63   assert out, [write to null channel]
     64   chan:&:channel:_elem <- get *out, chan:offset
     65   <channel-write-initial>
     66   # block until lock is acquired AND queue has room
     67   lock:location <- get-location *chan, lock:offset
     68 #?   $print [write], 10/newline
     69   {
     70 #?     $print [trying to acquire lock for writing], 10/newline
     71     wait-for-reset-then-set lock
     72 #?     $print [lock acquired for writing], 10/newline
     73     full?:bool <- channel-full? chan
     74     break-unless full?
     75 #?     $print [but channel is full; relinquishing lock], 10/newline
     76     # channel is full; relinquish lock and give a reader the opportunity to
     77     # create room on it
     78     reset lock
     79     current-routine-is-blocked
     80     switch  # avoid spinlocking
     81     loop
     82   }
     83   current-routine-is-unblocked
     84 #?   $print [performing write], 10/newline
     85   # store a deep copy of val
     86   circular-buffer:&:@:_elem <- get *chan, data:offset
     87   free:num <- get *chan, first-free:offset
     88   *circular-buffer <- put-index *circular-buffer, free, val
     89   # mark its slot as filled
     90   free <- add free, 1
     91   {
     92     # wrap free around to 0 if necessary
     93     len:num <- length *circular-buffer
     94     at-end?:bool <- greater-or-equal free, len
     95     break-unless at-end?
     96     free <- copy 0
     97   }
     98   # write back
     99   *chan <- put *chan, first-free:offset, free
    100 #?   $print [relinquishing lock after writing], 10/newline
    101   reset lock
    102 ]
    103
    104 # read a value from a channel
    105 def read in:&:source:_elem -> result:_elem, eof?:bool, in:&:source:_elem [
    106   local-scope
    107   load-inputs
    108   assert in, [read on null channel]
    109   eof? <- copy false  # default result
    110   chan:&:channel:_elem <- get *in, chan:offset
    111   # block until lock is acquired AND queue has data
    112   lock:location <- get-location *chan, lock:offset
    113 #?   $print [read], 10/newline
    114   {
    115 #?     $print [trying to acquire lock for reading], 10/newline
    116     wait-for-reset-then-set lock
    117 #?     $print [lock acquired for reading], 10/newline
    118     empty?:bool <- channel-empty? chan
    119     break-unless empty?
    120 #?     $print [but channel is empty; relinquishing lock], 10/newline
    121     # channel is empty; relinquish lock and give a writer the opportunity to
    122     # add to it
    123     reset lock
    124     current-routine-is-blocked
    125     <channel-read-empty>
    126     switch  # avoid spinlocking
    127     loop
    128   }
    129   current-routine-is-unblocked
    130   # pull result off
    131   full:num <- get *chan, first-full:offset
    132   circular-buffer:&:@:_elem <- get *chan, data:offset
    133   result <- index *circular-buffer, full
    134   # clear the slot
    135   empty:&:_elem <- new _elem:type
    136   *circular-buffer <- put-index *circular-buffer, full, *empty
    137   # mark its slot as empty
    138   full <- add full, 1
    139   {
    140     # wrap full around to 0 if necessary
    141     len:num <- length *circular-buffer
    142     at-end?:bool <- greater-or-equal full, len
    143     break-unless at-end?
    144     full <- copy 0
    145   }
    146   # write back
    147   *chan <- put *chan, first-full:offset, full
    148 #?   $print [relinquishing lock after reading], 10/newline
    149   reset lock
    150 ]
    151
    152 # todo: create a notion of iterator and iterable so we can read/write whole
    153 # aggregates (arrays, lists, ..) of _elems at once.
    154
    155 scenario channel-initialization [
    156   run [
    157     local-scope
    158     source:&:source:num <- new-channel 3/capacity
    159     chan:&:channel:num <- get *source, chan:offset
    160     10:num/raw <- get *chan, first-full:offset
    161     11:num/raw <- get *chan, first-free:offset
    162   ]
    163   memory-should-contain [
    164     10 <- 0  # first-full
    165     11 <- 0  # first-free
    166   ]
    167 ]
    168
    169 scenario channel-write-increments-free [
    170   local-scope
    171   _, sink:&:sink:num <- new-channel 3/capacity
    172   run [
    173     sink <- write sink, 34
    174     chan:&:channel:num <- get *sink, chan:offset
    175     10:num/raw <- get *chan, first-full:offset
    176     11:num/raw <- get *chan, first-free:offset
    177   ]
    178   memory-should-contain [
    179     10 <- 0  # first-full
    180     11 <- 1  # first-free
    181   ]
    182 ]
    183
    184 scenario channel-read-increments-full [
    185   local-scope
    186   source:&:source:num, sink:&:sink:num <- new-channel 3/capacity
    187   sink <- write sink, 34
    188   run [
    189     _, _, source <- read source
    190     chan:&:channel:num <- get *source, chan:offset
    191     10:num/raw <- get *chan, first-full:offset
    192     11:num/raw <- get *chan, first-free:offset
    193   ]
    194   memory-should-contain [
    195     10 <- 1  # first-full
    196     11 <- 1  # first-free
    197   ]
    198 ]
    199
    200 scenario channel-wrap [
    201   local-scope
    202   # channel with just 1 slot
    203   source:&:source:num, sink:&:sink:num <- new-channel 1/capacity
    204   chan:&:channel:num <- get *source, chan:offset
    205   # write and read a value
    206   sink <- write sink, 34
    207   _, _, source <- read source
    208   run [
    209     # first-free will now be 1
    210     10:num/raw <- get *chan, first-free:offset
    211     11:num/raw <- get *chan, first-free:offset
    212     # write second value, verify that first-free wraps
    213     sink <- write sink, 34
    214     20:num/raw <- get *chan, first-free:offset
    215     # read second value, verify that first-full wraps
    216     _, _, source <- read source
    217     30:num/raw <- get *chan, first-full:offset
    218   ]
    219   memory-should-contain [
    220     10 <- 1  # first-free after first write
    221     11 <- 1  # first-full after first read
    222     20 <- 0  # first-free after second write, wrapped
    223     30 <- 0  # first-full after second read, wrapped
    224   ]
    225 ]
    226
    227 scenario channel-new-empty-not-full [
    228   run [
    229     local-scope
    230     source:&:source:num <- new-channel 3/capacity
    231     chan:&:channel:num <- get *source, chan:offset
    232     10:bool/raw <- channel-empty? chan
    233     11:bool/raw <- channel-full? chan
    234   ]
    235   memory-should-contain [
    236     10 <- 1  # empty?
    237     11 <- 0  # full?
    238   ]
    239 ]
    240
    241 scenario channel-write-not-empty [
    242   local-scope
    243   source:&:source:num, sink:&:sink:num <- new-channel 3/capacity
    244   chan:&:channel:num <- get *source, chan:offset
    245   run [
    246     sink <- write sink, 34
    247     10:bool/raw <- channel-empty? chan
    248     11:bool/raw <- channel-full? chan
    249   ]
    250   memory-should-contain [
    251     10 <- 0  # empty?
    252     11 <- 0  # full?
    253   ]
    254 ]
    255
    256 scenario channel-write-full [
    257   local-scope
    258   source:&:source:num, sink:&:sink:num <- new-channel 1/capacity
    259   chan:&:channel:num <- get *source, chan:offset
    260   run [
    261     sink <- write sink, 34
    262     10:bool/raw <- channel-empty? chan
    263     11:bool/raw <- channel-full? chan
    264   ]
    265   memory-should-contain [
    266     10 <- 0  # empty?
    267     11 <- 1  # full?
    268   ]
    269 ]
    270
    271 scenario channel-read-not-full [
    272   local-scope
    273   source:&:source:num, sink:&:sink:num <- new-channel 1/capacity
    274   chan:&:channel:num <- get *source, chan:offset
    275   sink <- write sink, 34
    276   run [
    277     _, _, source <- read source
    278     10:bool/raw <- channel-empty? chan
    279     11:bool/raw <- channel-full? chan
    280   ]
    281   memory-should-contain [
    282     10 <- 1  # empty?
    283     11 <- 0  # full?
    284   ]
    285 ]
    286
    287 scenario channel-clear [
    288   local-scope
    289   # create a channel with a few items
    290   source:&:source:num, sink:&:sink:num <- new-channel 3/capacity
    291   chan:&:channel:num <- get *sink, chan:offset
    292   write sink, 30
    293   write sink, 31
    294   write sink, 32
    295   run [
    296     clear source
    297     10:bool/raw <- channel-empty? chan
    298   ]
    299   memory-should-contain [
    300     10 <- 1  # after the call to 'clear', the channel should be empty
    301   ]
    302 ]
    303
    304 def clear in:&:source:_elem -> in:&:source:_elem [
    305   local-scope
    306   load-inputs
    307   chan:&:channel:_elem <- get *in, chan:offset
    308   {
    309     empty?:bool <- channel-empty? chan
    310     break-if empty?
    311     _, _, in <- read in
    312     loop
    313   }
    314 ]
    315
    316 ## cancelling channels
    317
    318 # every channel comes with a boolean signifying if it's been closed
    319 # initially this boolean is false
    320 container channel:_elem [
    321   closed?:bool
    322 ]
    323
    324 # a channel can be closed from either the source or the sink
    325 # both routines can modify the 'closed?' bit, but they can only ever set it, so this is a benign race
    326 def close x:&:source:_elem -> x:&:source:_elem [
    327   local-scope
    328   load-inputs
    329   chan:&:channel:_elem <- get *x, chan:offset
    330   *chan <- put *chan, closed?:offset, true
    331 ]
    332 def close x:&:sink:_elem -> x:&:sink:_elem [
    333   local-scope
    334   load-inputs
    335   chan:&:channel:_elem <- get *x, chan:offset
    336   *chan <- put *chan, closed?:offset, true
    337 ]
    338
    339 # once a channel is closed from one side, no further operations are expected from that side
    340 # if a channel is closed for reading,
    341 #   no further writes will be let through
    342 # if a channel is closed for writing,
    343 #   future reads continue until the channel empties,
    344 #   then the channel is also closed for reading
    345 after <channel-write-initial> [
    346   closed?:bool <- get *chan, closed?:offset
    347   return-if closed?
    348 ]
    349 after <channel-read-empty> [
    350   closed?:bool <- get *chan, closed?:offset
    351   {
    352     break-unless closed?
    353     empty-result:&:_elem <- new _elem:type
    354     current-routine-is-unblocked
    355     return *empty-result, true
    356   }
    357 ]
    358
    359 ## helpers
    360
    361 # An empty channel has first-free and first-full both at the same value.
    362 def channel-empty? chan:&:channel:_elem -> result:bool [
    363   local-scope
    364   load-inputs
    365   # return chan.first-full == chan.first-free
    366   full:num <- get *chan, first-full:offset
    367   free:num <- get *chan, first-free:offset
    368   result <- equal full, free
    369 ]
    370
    371 # A full channel has first-free just before first-full, wasting one slot.
    372 # (Other alternatives: https://www.snellman.net/blog/archive/2016-12-13-ring-buffers)
    373 def channel-full? chan:&:channel:_elem -> result:bool [
    374   local-scope
    375   load-inputs
    376   # tmp = chan.first-free + 1
    377   tmp:num <- get *chan, first-free:offset
    378   tmp <- add tmp, 1
    379   {
    380     # if tmp == chan.capacity, tmp = 0
    381     len:num <- capacity chan
    382     at-end?:bool <- greater-or-equal tmp, len
    383     break-unless at-end?
    384     tmp <- copy 0
    385   }
    386   # return chan.first-full == tmp
    387   full:num <- get *chan, first-full:offset
    388   result <- equal full, tmp
    389 ]
    390
    391 def capacity chan:&:channel:_elem -> result:num [
    392   local-scope
    393   load-inputs
    394   q:&:@:_elem <- get *chan, data:offset
    395   result <- length *q
    396 ]
    397
    398 ## helpers for channels of characters in particular
    399
    400 def buffer-lines in:&:source:char, buffered-out:&:sink:char -> buffered-out:&:sink:char, in:&:source:char [
    401   local-scope
    402   load-inputs
    403   # repeat forever
    404   eof?:bool <- copy false
    405   {
    406     line:&:buffer:char <- new-buffer 30
    407     # read characters from 'in' until newline, copy into line
    408     {
    409       +next-character
    410       c:char, eof?:bool, in <- read in
    411       break-if eof?
    412       # drop a character on backspace
    413       {
    414         # special-case: if it's a backspace
    415         backspace?:bool <- equal c, 8
    416         break-unless backspace?
    417         # drop previous character
    418         {
    419           buffer-length:num <- get *line, length:offset
    420           buffer-empty?:bool <- equal buffer-length, 0
    421           break-if buffer-empty?
    422           buffer-length <- subtract buffer-length, 1
    423           *line <- put *line, length:offset, buffer-length
    424         }
    425         # and don't append this one
    426         loop +next-character
    427       }
    428       # append anything else
    429       line <- append line, c
    430       line-done?:bool <- equal c, 10/newline
    431       break-if line-done?
    432       loop
    433     }
    434     # copy line into 'buffered-out'
    435     i:num <- copy 0
    436     line-contents:text <- get *line, data:offset
    437     max:num <- get *line, length:offset
    438     {
    439       done?:bool <- greater-or-equal i, max
    440       break-if done?
    441       c:char <- index *line-contents, i
    442       buffered-out <- write buffered-out, c
    443       i <- add i, 1
    444       loop
    445     }
    446     {
    447       break-unless eof?
    448       buffered-out <- close buffered-out
    449       return
    450     }
    451     loop
    452   }
    453 ]
    454
    455 scenario buffer-lines-blocks-until-newline [
    456   run [
    457     local-scope
    458     source:&:source:char, sink:&:sink:char <- new-channel 10/capacity
    459     _, buffered-stdin:&:sink:char/buffered-stdin <- new-channel 10/capacity
    460     buffered-chan:&:channel:char <- get *buffered-stdin, chan:offset
    461     empty?:bool <- channel-empty? buffered-chan
    462     assert empty?, [
    463 F buffer-lines-blocks-until-newline: channel should be empty after init]
    464     # buffer stdin into buffered-stdin, try to read from buffered-stdin
    465     buffer-routine:num <- start-running buffer-lines, source, buffered-stdin
    466     wait-for-routine-to-block buffer-routine
    467     empty? <- channel-empty? buffered-chan
    468     assert empty?:bool, [
    469 F buffer-lines-blocks-until-newline: channel should be empty after buffer-lines bring-up]
    470     # write 'a'
    471     sink <- write sink, 97/a
    472     restart buffer-routine
    473     wait-for-routine-to-block buffer-routine
    474     empty? <- channel-empty? buffered-chan
    475     assert empty?:bool, [
    476 F buffer-lines-blocks-until-newline: channel should be empty after writing 'a']
    477     # write 'b'
    478     sink <- write sink, 98/b
    479     restart buffer-routine
    480     wait-for-routine-to-block buffer-routine
    481     empty? <- channel-empty? buffered-chan
    482     assert empty?:bool, [
    483 F buffer-lines-blocks-until-newline: channel should be empty after writing 'b']
    484     # write newline
    485     sink <- write sink, 10/newline
    486     restart buffer-routine
    487     wait-for-routine-to-block buffer-routine
    488     empty? <- channel-empty? buffered-chan
    489     data-emitted?:bool <- not empty?
    490     assert data-emitted?, [
    491 F buffer-lines-blocks-until-newline: channel should contain data after writing newline]
    492     trace 1, [test], [reached end]
    493   ]
    494   trace-should-contain [
    495     test: reached end
    496   ]
    497 ]
    498
    499 def drain source:&:source:char -> result:text, source:&:source:char [
    500   local-scope
    501   load-inputs
    502   buf:&:buffer:char <- new-buffer 30
    503   {
    504     c:char, done?:bool <- read source
    505     break-if done?
    506     buf <- append buf, c
    507     loop
    508   }
    509   result <- buffer-to-array buf
    510 ]