Skip to content

Commit fef280b

Browse files
committed
Tidy CABI: use SuspendResult everywhere instead of bool for clarity
1 parent c6ba212 commit fef280b

File tree

3 files changed

+183
-177
lines changed

3 files changed

+183
-177
lines changed

design/mvp/CanonicalABI.md

Lines changed: 102 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,16 @@ ability to switch stacks. In any case, the use of `threading.Thread` is
436436
encapsulated by the `Thread` class so that the rest of the Canonical ABI can
437437
simply use `suspend`, `resume`, etc.
438438

439+
When a `Thread` is suspended and then resumed, it receives a `SuspendResult`
440+
value indicating whether the caller has cooperatively requested that the thread
441+
cancel itself which is communicated to Core WebAssembly with the following
442+
integer values:
443+
```python
444+
class SuspendResult(IntEnum):
445+
NOT_CANCELLED = 0
446+
CANCELLED = 1
447+
```
448+
439449
Introducing the `Thread` class in chunks, a `Thread` has the following fields
440450
and can be in one of the following 3 states based on these fields:
441451
* `running`: actively executing with a "parent" thread that is waiting
@@ -451,7 +461,7 @@ class Thread:
451461
parent_lock: Optional[threading.Lock]
452462
ready_func: Optional[Callable[[], bool]]
453463
cancellable: bool
454-
cancelled: bool
464+
suspend_result: Optional[SuspendResult]
455465
in_event_loop: bool
456466
index: Optional[int]
457467
context: list[int]
@@ -490,13 +500,14 @@ immediately blocked `acquire()`ing `fiber_lock` (which will be `release()`ed by
490500
self.parent_lock = None
491501
self.ready_func = None
492502
self.cancellable = False
493-
self.cancelled = False
503+
self.suspend_result = None
494504
self.in_event_loop = False
495505
self.index = None
496506
self.context = [0] * Thread.CONTEXT_LENGTH
497507
def fiber_func():
498508
self.fiber_lock.acquire()
499-
assert(self.running())
509+
assert(self.running() and self.suspend_result == SuspendResult.NOT_CANCELLED)
510+
self.suspend_result = None
500511
thread_func(self)
501512
assert(self.running())
502513
self.task.thread_stop(self)
@@ -529,11 +540,10 @@ instruction in the Core WebAssembly [stack-switching] proposal). This waiting
529540
is accomplished using the `parent_lock` field of the resumed thread, which the
530541
resumed thread will `release()` when it suspends or exits.
531542

532-
One extra boolean value communicated from `resume` to `suspend` is requests for
533-
cancellation. When a thread calls `Thread.suspend`, it indicates whether it is
534-
able to handle cancellation. This information is stored in the `cancellable`
535-
field which is used by `Task.request_cancellation` (defined below) to only
536-
`resume` with `cancel = True` when the thread expects it.
543+
When a thread calls `Thread.suspend`, it indicates whether it is able to handle
544+
cancellation. This information is stored in the `cancellable` field which is
545+
used by `Task.request_cancellation` (defined below) to only `resume` with
546+
`SuspendResult.CANCELLED` when the thread expects it.
537547

538548
Lastly, several `Thread` methods below will set the `ready_func` and add the
539549
`Thread` to the `Store.pending` list so that `Store.tick` will call `resume`
@@ -544,32 +554,33 @@ when the `ready_func` returns `True`. Once `Thread.resume` is called, the
544554
Given the above, `Thread.resume` and `Thread.suspend` can be defined
545555
complementarily using `parent_lock` and `fiber_lock` as follows:
546556
```python
547-
def resume(self, cancel = False):
548-
assert(not self.running() and not self.cancelled)
557+
def resume(self, suspend_result = SuspendResult.NOT_CANCELLED):
558+
assert(not self.running() and self.suspend_result is None)
549559
if self.ready_func:
550-
assert(cancel or self.ready_func())
560+
assert(suspend_result == SuspendResult.CANCELLED or self.ready_func())
551561
self.ready_func = None
552562
self.task.inst.store.pending.remove(self)
553-
assert(self.cancellable or not cancel)
554-
self.cancelled = cancel
563+
assert(self.cancellable or suspend_result == SuspendResult.NOT_CANCELLED)
564+
self.suspend_result = suspend_result
555565
self.parent_lock = threading.Lock()
556566
self.parent_lock.acquire()
557567
self.fiber_lock.release()
558568
self.parent_lock.acquire()
559569
self.parent_lock = None
560570
assert(not self.running())
561571

562-
def suspend(self, cancellable) -> bool:
563-
assert(self.running() and not self.cancellable and not self.cancelled)
572+
def suspend(self, cancellable) -> SuspendResult:
573+
assert(self.running() and not self.cancellable and self.suspend_result is None)
564574
self.cancellable = cancellable
565575
self.parent_lock.release()
566576
self.fiber_lock.acquire()
567577
assert(self.running())
568578
self.cancellable = False
569-
completed = not self.cancelled
570-
self.cancelled = False
571-
assert(cancellable or completed)
572-
return completed
579+
suspend_result = self.suspend_result
580+
self.suspend_result = None
581+
assert(suspend_result is not None)
582+
assert(cancellable or suspend_result == SuspendResult.NOT_CANCELLED)
583+
return suspend_result
573584
```
574585

575586
The `Thread.resume_later` method is called by `canon_thread_resume_later` below
@@ -586,10 +597,10 @@ in the near future:
586597
The `Thread.suspend_until` method is used by a multiple internal callers below
587598
to specify a custom `ready_func` that is polled by `Store.tick`:
588599
```python
589-
def suspend_until(self, ready_func, cancellable = False) -> bool:
600+
def suspend_until(self, ready_func, cancellable = False) -> SuspendResult:
590601
assert(self.running())
591602
if ready_func() and not DETERMINISTIC_PROFILE and random.randint(0,1):
592-
return True
603+
return SuspendResult.NOT_CANCELLED
593604
self.ready_func = ready_func
594605
self.task.inst.store.pending.append(self)
595606
return self.suspend(cancellable)
@@ -610,10 +621,11 @@ of internal `thread.switch-to`s before suspending, the `async`-lowered caller
610621
resumes execution immediately (as if there were no `thread.switch-to` and
611622
[Asyncify] was used to emulate stack switching instead).
612623
```python
613-
def switch_to(self, cancellable, other: Thread) -> bool:
614-
assert(self.running() and other.suspended())
615-
assert(not self.cancellable)
624+
def switch_to(self, cancellable, other: Thread) -> SuspendResult:
625+
assert(self.running() and not self.cancellable and self.suspend_result is None)
626+
assert(other.suspended() and other.suspend_result is None)
616627
self.cancellable = cancellable
628+
other.suspend_result = SuspendResult.NOT_CANCELLED
617629
assert(self.parent_lock and not other.parent_lock)
618630
other.parent_lock = self.parent_lock
619631
self.parent_lock = None
@@ -622,16 +634,18 @@ resumes execution immediately (as if there were no `thread.switch-to` and
622634
self.fiber_lock.acquire()
623635
assert(self.running())
624636
self.cancellable = False
625-
completed = not self.cancelled
626-
self.cancelled = False
627-
return completed
637+
suspend_result = self.suspend_result
638+
self.suspend_result = None
639+
assert(suspend_result is not None)
640+
assert(cancellable or suspend_result == SuspendResult.NOT_CANCELLED)
641+
return suspend_result
628642
```
629643

630644
Lastly, the `Thread.yield_to` method is used by `canon_thread_yield_to` below
631645
to switch execution to some other thread (like `Thread.switch_to`), but leave
632646
the current thread `ready` instead of `suspended`.
633647
```python
634-
def yield_to(self, cancellable, other: Thread) -> bool:
648+
def yield_to(self, cancellable, other: Thread) -> SuspendResult:
635649
assert(not self.ready_func)
636650
self.ready_func = lambda: True
637651
self.task.inst.store.pending.append(self)
@@ -889,9 +903,9 @@ backpressure is disabled. There are three sources of backpressure:
889903
return self.inst.backpressure > 0 or (self.needs_exclusive() and self.inst.exclusive)
890904
if has_backpressure() or self.inst.num_waiting_to_enter > 0:
891905
self.inst.num_waiting_to_enter += 1
892-
completed = thread.suspend_until(lambda: not has_backpressure(), cancellable = True)
906+
result = thread.suspend_until(lambda: not has_backpressure(), cancellable = True)
893907
self.inst.num_waiting_to_enter -= 1
894-
if not completed:
908+
if result == SuspendResult.CANCELLED:
895909
self.cancel()
896910
return False
897911
if self.needs_exclusive():
@@ -940,7 +954,7 @@ that it can be delivered in the future by `Task.deliver_pending_cancel`.
940954
for thread in self.threads:
941955
if thread.cancellable and not (thread.in_event_loop and self.inst.exclusive):
942956
self.state = Task.State.CANCEL_DELIVERED
943-
thread.resume(cancel = True)
957+
thread.resume(SuspendResult.CANCELLED)
944958
return
945959
self.state = Task.State.PENDING_CANCEL
946960

@@ -963,28 +977,28 @@ and `needs_exclusive`).
963977
The following `Task` methods wrap corresponding `Thread` methods after first
964978
delivering any pending cancellations set by `Task.request_cancellation`:
965979
```python
966-
def suspend(self, thread, cancellable) -> bool:
980+
def suspend(self, thread, cancellable) -> SuspendResult:
967981
assert(thread in self.threads and thread.task is self)
968982
if self.deliver_pending_cancel(cancellable):
969-
return False
983+
return SuspendResult.CANCELLED
970984
return thread.suspend(cancellable)
971985

972-
def suspend_until(self, ready_func, thread, cancellable) -> bool:
986+
def suspend_until(self, ready_func, thread, cancellable) -> SuspendResult:
973987
assert(thread in self.threads and thread.task is self)
974988
if self.deliver_pending_cancel(cancellable):
975-
return False
989+
return SuspendResult.CANCELLED
976990
return thread.suspend_until(ready_func, cancellable)
977991

978-
def switch_to(self, thread, cancellable, other_thread) -> bool:
992+
def switch_to(self, thread, cancellable, other_thread) -> SuspendResult:
979993
assert(thread in self.threads and thread.task is self)
980994
if self.deliver_pending_cancel(cancellable):
981-
return False
995+
return SuspendResult.CANCELLED
982996
return thread.switch_to(cancellable, other_thread)
983997

984-
def yield_to(self, thread, cancellable, other_thread) -> bool:
998+
def yield_to(self, thread, cancellable, other_thread) -> SuspendResult:
985999
assert(thread in self.threads and thread.task is self)
9861000
if self.deliver_pending_cancel(cancellable):
987-
return False
1001+
return SuspendResult.CANCELLED
9881002
return thread.yield_to(cancellable, other_thread)
9891003
```
9901004

@@ -1000,10 +1014,11 @@ trap if another task tries to drop the waitable set being used.
10001014
wset.num_waiting += 1
10011015
def ready_and_has_event():
10021016
return ready_func() and wset.has_pending_event()
1003-
if not self.suspend_until(ready_and_has_event, thread, cancellable):
1004-
event = (EventCode.TASK_CANCELLED, 0, 0)
1005-
else:
1006-
event = wset.get_pending_event()
1017+
match self.suspend_until(ready_and_has_event, thread, cancellable):
1018+
case SuspendResult.CANCELLED:
1019+
event = (EventCode.TASK_CANCELLED, 0, 0)
1020+
case SuspendResult.NOT_CANCELLED:
1021+
event = wset.get_pending_event()
10071022
wset.num_waiting -= 1
10081023
return event
10091024
```
@@ -1018,12 +1033,14 @@ nondeterministically switch to another task (or not).
10181033
def poll_until(self, ready_func, thread, wset, cancellable) -> Optional[EventTuple]:
10191034
assert(thread in self.threads and thread.task is self)
10201035
wset.num_waiting += 1
1021-
if not self.suspend_until(ready_func, thread, cancellable):
1022-
event = (EventCode.TASK_CANCELLED, 0, 0)
1023-
elif wset.has_pending_event():
1024-
event = wset.get_pending_event()
1025-
else:
1026-
event = (EventCode.NONE, 0, 0)
1036+
match self.suspend_until(ready_func, thread, cancellable):
1037+
case SuspendResult.CANCELLED:
1038+
event = (EventCode.TASK_CANCELLED, 0, 0)
1039+
case SuspendResult.NOT_CANCELLED:
1040+
if wset.has_pending_event():
1041+
event = wset.get_pending_event()
1042+
else:
1043+
event = (EventCode.NONE, 0, 0)
10271044
wset.num_waiting -= 1
10281045
return event
10291046
```
@@ -1034,10 +1051,11 @@ the event loop in `canon_lift` when `CallbackCode.YIELD` is returned.
10341051
```python
10351052
def yield_until(self, ready_func, thread, cancellable) -> EventTuple:
10361053
assert(thread in self.threads and thread.task is self)
1037-
if not self.suspend_until(ready_func, thread, cancellable):
1038-
return (EventCode.TASK_CANCELLED, 0, 0)
1039-
else:
1040-
return (EventCode.NONE, 0, 0)
1054+
match self.suspend_until(ready_func, thread, cancellable):
1055+
case SuspendResult.CANCELLED:
1056+
return (EventCode.TASK_CANCELLED, 0, 0)
1057+
case SuspendResult.NOT_CANCELLED:
1058+
return (EventCode.NONE, 0, 0)
10411059
```
10421060

10431061
The `Task.return_` method is called by `canon_task_return` and `canon_lift` to
@@ -4386,27 +4404,20 @@ index `$i` from the current component instance's table, traps if it's not
43864404
[suspended], and then switches to that thread, leaving the [current thread]
43874405
suspended.
43884406
```python
4389-
class SuspendResult(IntEnum):
4390-
COMPLETED = 0
4391-
CANCELLED = 1
4392-
43934407
def canon_thread_switch_to(cancellable, thread, i):
43944408
trap_if(not thread.task.inst.may_leave)
43954409
other_thread = thread.task.inst.table.get(i)
43964410
trap_if(not isinstance(other_thread, Thread))
43974411
trap_if(not other_thread.suspended())
4398-
if not thread.task.switch_to(thread, cancellable, other_thread):
4399-
assert(cancellable)
4400-
return [SuspendResult.CANCELLED]
4401-
else:
4402-
return [SuspendResult.COMPLETED]
4412+
suspend_result = thread.task.switch_to(thread, cancellable, other_thread)
4413+
return [suspend_result]
44034414
```
4404-
If `cancellable` is set, then `thread.switch-to` will return whether the
4405-
supertask has already or concurrently requested cancellation. `thread.switch-to`
4406-
(and other cancellable operations) will only indicate cancellation once and
4407-
thus, if a caller is not prepared to propagate cancellation, they can omit
4408-
`cancellable` so that cancellation is instead delivered at a later
4409-
`cancellable` call.
4415+
If `cancellable` is set, then `thread.switch-to` will return a `SuspendResult`
4416+
value to indicate whether the supertask has already or concurrently requested
4417+
cancellation. `thread.switch-to` (and other cancellable operations) will only
4418+
indicate cancellation once and thus, if a caller is not prepared to propagate
4419+
cancellation, they can omit `cancellable` so that cancellation is instead
4420+
delivered at a later `cancellable` call.
44104421

44114422

44124423
### 🧵 `canon thread.suspend`
@@ -4424,18 +4435,15 @@ calling component.
44244435
```python
44254436
def canon_thread_suspend(cancellable, thread):
44264437
trap_if(not thread.task.inst.may_leave)
4427-
if not thread.task.suspend(thread, cancellable):
4428-
assert(cancellable)
4429-
return [SuspendResult.CANCELLED]
4430-
else:
4431-
return [SuspendResult.COMPLETED]
4438+
suspend_result = thread.task.suspend(thread, cancellable)
4439+
return [suspend_result]
44324440
```
4433-
If `cancellable` is set, then `thread.suspend` will return whether the
4434-
supertask has already or concurrently requested cancellation. `thread.suspend`
4435-
(and other cancellable operations) will only indicate cancellation once and
4436-
thus, if a caller is not prepared to propagate cancellation, they can omit
4437-
`cancellable` so that cancellation is instead delivered at a later
4438-
`cancellable` call.
4441+
If `cancellable` is set, then `thread.suspend` will return a `SuspendResult`
4442+
value to indicate whether the supertask has already or concurrently requested
4443+
cancellation. `thread.suspend` (and other cancellable operations) will only
4444+
indicate cancellation once and thus, if a caller is not prepared to propagate
4445+
cancellation, they can omit `cancellable` so that cancellation is instead
4446+
delivered at a later `cancellable` call.
44394447

44404448

44414449
### 🧵 `canon thread.resume-later`
@@ -4485,18 +4493,15 @@ def canon_thread_yield_to(cancellable, thread, i):
44854493
other_thread = thread.task.inst.table.get(i)
44864494
trap_if(not isinstance(other_thread, Thread))
44874495
trap_if(not other_thread.suspended())
4488-
if not thread.task.yield_to(thread, cancellable, other_thread):
4489-
assert(cancellable)
4490-
return [SuspendResult.CANCELLED]
4491-
else:
4492-
return [SuspendResult.COMPLETED]
4496+
suspend_result = thread.task.yield_to(thread, cancellable, other_thread)
4497+
return [suspend_result]
44934498
```
4494-
If `cancellable` is set, then `thread.yield-to` will return whether the
4495-
supertask has already or concurrently requested cancellation. `thread.yield-to`
4496-
(and other cancellable operations) will only indicate cancellation once and
4497-
thus, if a caller is not prepared to propagate cancellation, they can omit
4498-
`cancellable` so that cancellation is instead delivered at a later
4499-
`cancellable` call.
4499+
If `cancellable` is set, then `thread.yield-to` will return a `SuspendResult`
4500+
value indicating whether the supertask has already or concurrently requested
4501+
cancellation. `thread.yield-to` (and other cancellable operations) will only
4502+
indicate cancellation once and thus, if a caller is not prepared to propagate
4503+
cancellation, they can omit `cancellable` so that cancellation is instead
4504+
delivered at a later `cancellable` call.
45004505

45014506

45024507
### 🧵 `canon thread.yield`
@@ -4519,19 +4524,20 @@ def canon_thread_yield(cancellable, thread):
45194524
event_code,_,_ = thread.task.yield_until(lambda: True, thread, cancellable)
45204525
match event_code:
45214526
case EventCode.NONE:
4522-
return [SuspendResult.COMPLETED]
4527+
return [SuspendResult.NOT_CANCELLED]
45234528
case EventCode.TASK_CANCELLED:
45244529
return [SuspendResult.CANCELLED]
45254530
```
45264531
Even though `yield_until` passes `lambda: True` as the condition it is waiting
45274532
for, `yield_until` does transitively peform a `Thread.suspend` which allows
45284533
the embedder to nondeterministically switch to executing another thread.
45294534

4530-
If `cancellable` is set, then `thread.yield` will return whether the supertask
4531-
has already or concurrently requested cancellation. `thread.yield` (and other
4532-
cancellable operations) will only indicate cancellation once and thus, if a
4533-
caller is not prepared to propagate cancellation, they can omit `cancellable`
4534-
so that cancellation is instead delivered at a later `cancellable` call.
4535+
If `cancellable` is set, then `thread.yield` will return a `SuspendResult`
4536+
value indicating whether the supertask has already or concurrently requested
4537+
cancellation. `thread.yield` (and other cancellable operations) will only
4538+
indicate cancellation once and thus, if a caller is not prepared to propagate
4539+
cancellation, they can omit `cancellable` so that cancellation is instead
4540+
delivered at a later `cancellable` call.
45354541

45364542

45374543
### 📝 `canon error-context.new`

0 commit comments

Comments
 (0)