@@ -1422,48 +1422,64 @@ using blocking or completion-based I/O, no buffering is necessary. This
1422
1422
buffering is analogous to the buffering performed in kernel memory by a
1423
1423
` pipe() ` .
1424
1424
1425
- Given the above, we can define the ` {Readable,Writable}StreamEnd ` classes that
1426
- are actually stored in the component instance table. The classes are almost
1427
- entirely symmetric, with the only difference being whether the polymorphic
1428
- ` copy ` method (used below) calls ` read ` or ` write ` :
1425
+ The two ends of a stream are stored as separate elements in the component
1426
+ instance's table and each end has a separate ` CopyState ` that reflects what
1427
+ * that end * is currently doing or has done. This ` state ` field is factored
1428
+ out into the ` CopyEnd ` class that is derived below :
1429
1429
``` python
1430
- class StreamEnd ( Waitable ):
1431
- shared: ReadableStream | WritableStream
1432
- copying: bool
1433
- done: bool
1430
+ class CopyState ( Enum ):
1431
+ IDLE = 1
1432
+ COPYING = 2
1433
+ DONE = 3
1434
1434
1435
- def __init__ (self , shared ):
1435
+ class CopyEnd (Waitable ):
1436
+ state: CopyState
1437
+
1438
+ def __init__ (self ):
1436
1439
Waitable.__init__ (self )
1437
- self .shared = shared
1438
- self .copying = False
1439
- self .done = False
1440
+ self .state = CopyState.IDLE
1440
1441
1441
1442
def drop (self ):
1442
- trap_if(self .copying)
1443
- self .shared.drop()
1443
+ trap_if(self .state == CopyState.COPYING )
1444
1444
Waitable.drop(self )
1445
+ ```
1446
+ As shown in ` drop ` , attempting to drop a readable or writable end while a copy
1447
+ is in progress traps. This means that client code must take care to wait for
1448
+ these operations to finish (potentially cancelling them via
1449
+ ` stream.cancel-{read,write} ` ) before dropping.
1450
+
1451
+ Given the above, we can define the concrete ` {Readable,Writable}StreamEnd `
1452
+ classes which are almost entirely symmetric, with the only difference being
1453
+ whether the polymorphic ` copy ` method (used below) calls ` read ` or ` write ` :
1454
+ ``` python
1455
+ class ReadableStreamEnd (CopyEnd ):
1456
+ shared: ReadableStream
1457
+
1458
+ def __init__ (self , shared ):
1459
+ CopyEnd.__init__ (self )
1460
+ self .shared = shared
1445
1461
1446
- class ReadableStreamEnd (StreamEnd ):
1447
1462
def copy (self , inst , dst , on_copy , on_copy_done ):
1448
1463
self .shared.read(inst, dst, on_copy, on_copy_done)
1449
1464
1450
- class WritableStreamEnd (StreamEnd ):
1465
+ def drop (self ):
1466
+ self .shared.drop()
1467
+ CopyEnd.drop(self )
1468
+
1469
+ class WritableStreamEnd (CopyEnd ):
1470
+ shared: WritableStream
1471
+
1472
+ def __init__ (self , shared ):
1473
+ CopyEnd.__init__ (self )
1474
+ self .shared = shared
1475
+
1451
1476
def copy (self , inst , src , on_copy , on_copy_done ):
1452
1477
self .shared.write(inst, src, on_copy, on_copy_done)
1453
- ```
1454
- The ` copying ` field tracks whether there is an asynchronous read or write in
1455
- progress and is maintained by the definitions of ` stream.{read,write} ` below.
1456
- The ` done ` field tracks whether this end has been notified that the other end
1457
- was dropped (via ` CopyResult.DROPPED ` ) and thus no further read/write
1458
- operations are allowed. Importantly, ` copying ` and ` done ` are per-* end* , not
1459
- per-* stream* (unlike the fields of ` SharedStreamImpl ` shown above, which are
1460
- per-stream and shared by both ends via their ` shared ` field).
1461
1478
1462
- Dropping a stream end while an asynchronous read or write is in progress traps
1463
- since the async read or write cannot be cancelled without blocking and ` drop `
1464
- (called by ` stream.drop-{readable,writable} ` ) is synchronous and non-blocking.
1465
- This means that client code must take care to wait for these operations to
1466
- finish before dropping.
1479
+ def drop (self ):
1480
+ self .shared.drop()
1481
+ CopyEnd.drop(self )
1482
+ ```
1467
1483
1468
1484
1469
1485
#### Future State
@@ -1569,39 +1585,34 @@ Lastly, the `{Readable,Writable}FutureEnd` classes are mostly symmetric with
1569
1585
` WritableFutureEnd.drop ` traps if the writer hasn't successfully written a
1570
1586
value or been notified of the reader dropping their end:
1571
1587
``` python
1572
- class FutureEnd (Waitable ):
1573
- shared: ReadableFuture| WritableFuture
1574
- copying: bool
1575
- done: bool
1588
+ class ReadableFutureEnd (CopyEnd ):
1589
+ shared: ReadableFuture
1576
1590
1577
1591
def __init__ (self , shared ):
1578
- Waitable .__init__ (self )
1592
+ CopyEnd .__init__ (self )
1579
1593
self .shared = shared
1580
- self .copying = False
1581
- self .done = False
1582
-
1583
- def drop (self ):
1584
- trap_if(self .copying)
1585
- Waitable.drop(self )
1586
1594
1587
- class ReadableFutureEnd (FutureEnd ):
1588
1595
def copy (self , inst , src_buffer , on_copy_done ):
1589
1596
self .shared.read(inst, src_buffer, on_copy_done)
1590
1597
1591
1598
def drop (self ):
1592
1599
self .shared.drop()
1593
- FutureEnd.drop(self )
1600
+ CopyEnd.drop(self )
1601
+
1602
+ class WritableFutureEnd (CopyEnd ):
1603
+ shared: WritableFuture
1604
+
1605
+ def __init__ (self , shared ):
1606
+ CopyEnd.__init__ (self )
1607
+ self .shared = shared
1594
1608
1595
- class WritableFutureEnd (FutureEnd ):
1596
1609
def copy (self , inst , dst_buffer , on_copy_done ):
1597
1610
self .shared.write(inst, dst_buffer, on_copy_done)
1598
1611
1599
1612
def drop (self ):
1600
- trap_if(not self .done )
1601
- FutureEnd .drop(self )
1613
+ trap_if(self .state != CopyState. DONE )
1614
+ CopyEnd .drop(self )
1602
1615
```
1603
- The ` copying ` and ` done ` fields are maintained by the ` future ` built-ins
1604
- defined below.
1605
1616
1606
1617
1607
1618
### Despecialization
@@ -2066,8 +2077,8 @@ transitively-borrowed handle.
2066
2077
2067
2078
Streams and futures are entirely symmetric, transferring ownership of the
2068
2079
readable end from the lifting component to the host or lowering component and
2069
- trapping if the readable end is in the middle of ` copying ` (which would create
2070
- a dangling-pointer situation) or is already ` done ` (in which case the only
2080
+ trapping if the readable end is in the middle of copying (which would create
2081
+ a dangling-pointer situation) or is in the ` DONE ` state (in which case the only
2071
2082
valid operation is ` {stream,future}.drop-{readable,writable} ` ).
2072
2083
``` python
2073
2084
def lift_stream (cx , i , t ):
@@ -2081,8 +2092,7 @@ def lift_async_value(ReadableEndT, cx, i, t):
2081
2092
e = cx.inst.table.remove(i)
2082
2093
trap_if(not isinstance (e, ReadableEndT))
2083
2094
trap_if(e.shared.t != t)
2084
- trap_if(e.copying)
2085
- trap_if(e.done)
2095
+ trap_if(e.state != CopyState.IDLE )
2086
2096
return e.shared
2087
2097
```
2088
2098
@@ -3953,16 +3963,16 @@ def canon_stream_write(stream_t, opts, task, i, ptr, n):
3953
3963
```
3954
3964
3955
3965
Introducing the ` stream_copy ` function in chunks, ` stream_copy ` first checks
3956
- that the element at index ` i ` is of the right type, not already ` copying ` , and
3957
- not already ` done ` (as defined next) . (In the future, the ` copying ` trap could
3958
- be relaxed, allowing a finite number of pipelined reads or writes.)
3966
+ that the element at index ` i ` is of the right type and allowed to start a new
3967
+ copy . (In the future, the "trap if not ` IDLE ` " condition could be relaxed to
3968
+ allow multiple pipelined reads or writes.)
3959
3969
``` python
3960
3970
def stream_copy (EndT , BufferT , event_code , stream_t , opts , task , i , ptr , n ):
3961
3971
trap_if(not task.inst.may_leave)
3962
3972
e = task.inst.table.get(i)
3963
3973
trap_if(not isinstance (e, EndT))
3964
3974
trap_if(e.shared.t != stream_t.t)
3965
- trap_if(e.copying or e.done )
3975
+ trap_if(e.state != CopyState. IDLE )
3966
3976
```
3967
3977
Then a readable or writable buffer is created which (in ` Buffer ` 's constructor)
3968
3978
eagerly checks the alignment and bounds of (` i ` , ` n ` ). (In the future, the
@@ -3982,18 +3992,19 @@ event is delivered to core wasm. `stream_event` first calls `reclaim_buffer` to
3982
3992
regain ownership of ` buffer ` and prevent any further partial reads/writes.
3983
3993
Thus, up until event delivery, the other end of the stream is free to
3984
3994
repeatedly read/write from/to ` buffer ` , ideally filling it up and minimizing
3985
- context switches. Next, ` copying ` is cleared to reenable future
3986
- ` stream.{read,write} ` calls. However, if the ` CopyResult ` is ` DROPPED ` , ` done `
3987
- is set to disallow all future use of this stream end. Lastly, ` stream_event `
3988
- packs the ` CopyResult ` and number of elements copied up until this point into a
3989
- single ` i32 ` payload for core wasm.
3995
+ context switches. Next, the stream's ` state ` is updated based on the result
3996
+ being delivered to core wasm so that, once a stream end has been notified that
3997
+ the other end dropped, calling anything other than ` stream.drop-* ` traps.
3998
+ Lastly, ` stream_event ` packs the ` CopyResult ` and number of elements copied up
3999
+ until this point into a single ` i32 ` payload for core wasm.
3990
4000
``` python
3991
4001
def stream_event (result , reclaim_buffer ):
3992
4002
reclaim_buffer()
3993
- assert (e.copying)
3994
- e.copying = False
4003
+ assert (e.state == CopyState.COPYING )
3995
4004
if result == CopyResult.DROPPED :
3996
- e.done = True
4005
+ e.state = CopyState.DONE
4006
+ else :
4007
+ e.state = CopyState.IDLE
3997
4008
assert (0 <= result < 2 ** 4 )
3998
4009
assert (buffer.progress <= Buffer.MAX_LENGTH < 2 ** 28 )
3999
4010
packed_result = result | (buffer.progress << 4 )
@@ -4005,7 +4016,7 @@ single `i32` payload for core wasm.
4005
4016
def on_copy_done (result ):
4006
4017
e.set_pending_event(partial(stream_event, result, reclaim_buffer = lambda :()))
4007
4018
4008
- e.copying = True
4019
+ e.state = CopyState. COPYING
4009
4020
e.copy(task.inst, buffer, on_copy, on_copy_done)
4010
4021
```
4011
4022
@@ -4063,7 +4074,7 @@ def future_copy(EndT, BufferT, event_code, future_t, opts, task, i, ptr):
4063
4074
e = task.inst.table.get(i)
4064
4075
trap_if(not isinstance (e, EndT))
4065
4076
trap_if(e.shared.t != future_t.t)
4066
- trap_if(e.copying or e.done )
4077
+ trap_if(e.state != CopyState. IDLE )
4067
4078
4068
4079
assert (not contains_borrow(future_t))
4069
4080
cx = LiftLowerContext(opts, task.inst, borrow_scope = None )
@@ -4072,27 +4083,28 @@ def future_copy(EndT, BufferT, event_code, future_t, opts, task, i, ptr):
4072
4083
Next, the ` copy ` method of ` {Readable,Writable}FutureEnd.copy ` is called to
4073
4084
perform the actual read/write. Other than the simplifications allowed by the
4074
4085
absence of repeated partial copies, the main difference in the following code
4075
- from the stream code is that ` future_event ` sets the ` done ` flag for * both * the
4076
- ` DROPPED ` and ` COMPLETED ` results, whereas ` stream_event ` sets ` done ` only for
4077
- ` DROPPED ` . This ensures that futures are read/written at most once and futures
4078
- are only passed to other components in a state where they are ready to be
4079
- read/written. Another important difference is that, since the buffer length is
4080
- always implied by the ` CopyResult ` , the number of elements copied is not packed
4081
- in the high 28 bits; they're always zero.
4086
+ from the stream code is that ` future_event ` transitions the end to the ` DONE `
4087
+ state (in which the only valid operation is to call ` future.drop-* ` ) on
4088
+ * either * the ` DROPPED ` and ` COMPLETED ` results . This ensures that futures are
4089
+ read/written at most once and futures are only passed to other components in a
4090
+ state where they are ready to be read/written. Another important difference is
4091
+ that, since the buffer length is always implied by the ` CopyResult ` , the number
4092
+ of elements copied is not packed in the high 28 bits; they're always zero.
4082
4093
``` python
4083
4094
def future_event (result ):
4084
4095
assert ((buffer.remain() == 0 ) == (result == CopyResult.COMPLETED ))
4085
- assert (e.copying)
4086
- e.copying = False
4096
+ assert (e.state == CopyState.COPYING )
4087
4097
if result == CopyResult.DROPPED or result == CopyResult.COMPLETED :
4088
- e.done = True
4098
+ e.state = CopyState.DONE
4099
+ else :
4100
+ e.state = CopyState.IDLE
4089
4101
return (event_code, i, result)
4090
4102
4091
4103
def on_copy_done (result ):
4092
4104
assert (result != CopyResult.DROPPED or event_code == EventCode.FUTURE_WRITE )
4093
4105
e.set_pending_event(partial(future_event, result))
4094
4106
4095
- e.copying = True
4107
+ e.state = CopyState. COPYING
4096
4108
e.copy(task.inst, buffer, on_copy_done)
4097
4109
```
4098
4110
@@ -4144,7 +4156,7 @@ def cancel_copy(EndT, event_code, stream_or_future_t, sync, task, i):
4144
4156
e = task.inst.table.get(i)
4145
4157
trap_if(not isinstance (e, EndT))
4146
4158
trap_if(e.shared.t != stream_or_future_t.t)
4147
- trap_if(not e.copying )
4159
+ trap_if(e.state != CopyState. COPYING )
4148
4160
if not e.has_pending_event():
4149
4161
e.shared.cancel()
4150
4162
if not e.has_pending_event():
@@ -4153,7 +4165,7 @@ def cancel_copy(EndT, event_code, stream_or_future_t, sync, task, i):
4153
4165
else :
4154
4166
return [BLOCKED ]
4155
4167
code,index,payload = e.get_pending_event()
4156
- assert (not e.copying and code == event_code and index == i)
4168
+ assert (e.state != CopyState. COPYING and code == event_code and index == i)
4157
4169
return [payload]
4158
4170
```
4159
4171
The * first* check for ` e.has_pending_event() ` catches the case where the copy has
0 commit comments