@@ -123,6 +123,7 @@ class ComponentInstance:
123123 parent: Optional[ComponentInstance]
124124 handles: Table[ResourceHandle | Waitable | WaitableSet | ErrorContext]
125125 threads: Table[Thread]
126+ may_enter: bool
126127 may_leave: bool
127128 backpressure: int
128129 num_waiting_to_enter: int
@@ -134,6 +135,7 @@ class ComponentInstance:
134135 self .parent = parent
135136 self .handles = Table()
136137 self .threads = Table()
138+ self .may_enter = True
137139 self .may_leave = True
138140 self .backpressure = 0
139141 self .num_waiting_to_enter = 0
@@ -155,26 +157,37 @@ by the host, `None`, in the `parent` field. Thus, the set of component instances
155157in a store forms a forest rooted by the component instances that were
156158instantiated directly by the host.
157159
158- Based on this, the "reflexive ancestors" of a component instance (i.e., itself
159- and all parent component instances up to the root component instance) can be
160- enumerated and tested via these two helper functions:
160+ TODO
161161``` python
162- def reflexive_ancestors (self ) -> set[ComponentInstance]:
162+ def enter_from (self , caller : Optional[ComponentInstance]):
163+ for inst in self .entering(caller):
164+ trap_if(not inst.may_enter)
165+ inst.may_enter = False
166+
167+ def leave_to (self , caller : Optional[ComponentInstance]):
168+ for inst in self .entering(caller):
169+ assert (not inst.may_enter)
170+ inst.may_enter = True
171+
172+ def entering (self , caller : Optional[ComponentInstance]):
173+ if caller:
174+ return self .self_and_ancestors() - caller.self_and_ancestors()
175+ else :
176+ return self .self_and_ancestors()
177+
178+ def self_and_ancestors (self ) -> set[ComponentInstance]:
163179 s = set ()
164180 inst = self
165181 while inst is not None :
166182 s.add(inst)
167183 inst = inst.parent
168184 return s
169-
170- def is_reflexive_ancestor_of (self , other ):
171- while other is not None :
172- if self is other:
173- return True
174- other = other.parent
175- return False
176185```
177186
187+ Based on this, the "reflexive ancestors" of a component instance (i.e., itself
188+ and all parent component instances up to the root component instance) can be
189+ enumerated and tested via these two helper functions:
190+ ...
178191How the host instantiates and invokes root components is up to the host and not
179192specified by the Component Model. Exports of previously-instantiated root
180193components * may* be supplied as the imports of subsequently-instantiated root
@@ -206,76 +219,7 @@ cases are prevented via trap for several reasons:
206219 to link recursive calls and this requires opting in via some
207220 [ TBD] ( Concurrency.md#TODO ) function effect type or canonical ABI option.
208221
209- To detect and prevent recursive calls, the runtime tracks the dynamic call
210- stack via a linked list of ` Supertask ` nodes. The ` inst ` field of ` Supertask `
211- either points to the ` ComponentInstance ` of the calling component or, if
212- ` None ` , indicates that the caller is the host.
213- ``` python
214- class Supertask :
215- inst: Optional[ComponentInstance]
216- supertask: Optional[Supertask]
217- ```
218-
219- The ` call_might_be_recursive ` predicate is used by ` Store.lift ` to
220- conservatively detect recursive reentrance and subsequently trap.
221- ``` python
222- def call_might_be_recursive (caller : Supertask, callee_inst : ComponentInstance):
223- if caller.inst is None :
224- while caller is not None :
225- if caller.inst and caller.inst.reflexive_ancestors() & callee_inst.reflexive_ancestors():
226- return True
227- caller = caller.supertask
228- return False
229- else :
230- return (caller.inst.is_reflexive_ancestor_of(callee_inst) or
231- callee_inst.is_reflexive_ancestor_of(caller.inst))
232- ```
233- The first case (where ` caller.inst ` is ` None ` ) covers host-to-component calls.
234- By testing whether any of the callers' reflexive anecestor sets intersect the
235- callee's ancestor set, the following case is considered recursive:
236- ```
237- +-------+
238- | A |<-.
239- | +---+ | |
240- host-->| B |-->host
241- | +---+ |
242- +-------+
243- ```
244- Here, when attempting to recursively call back into ` A ` , ` caller ` points to the
245- following stack:
246- ```
247- |inst=None| --supertask--> |inst=B| --supertask--> |inst=None| --supertask--> None
248- ```
249- while ` A ` does not appear as the ` inst ` of any ` Supertask ` on this stack,
250- ` B.reflexive_ancestors() ` is ` { B, A } ` , so the loop correctly determines that
251- ` A ` is being reentered. This ensures that child components are kept an
252- encapsulated detail of the parent.
253-
254- The second case (where ` caller.inst ` is not ` None ` ) covers component-to-
255- component calls by conservatively rejecting any call from a component to its
256- anecestor or descendant (thereby preventing any possible recursion via ancestor
257- ` funcref ` ). Thus, the following sibling-to-sibling component call is allowed:
258- ```
259- +----------------+
260- | P |
261- | +----+ +----+ |
262- host-->| C1 |->| C2 | |
263- | +----+ +----+ |
264- +----------------+
265- ```
266- while the following child-to-parent and parent-to-child calls are disallowed:
267- ```
268- +----------+ +----------+
269- | +---+ | | +---+ |
270- host-->| C |->P | host->| P->| C | |
271- | +---+ | | +---+ |
272- +----------+ +----------+
273- ```
274- This conservative approximation allows ` call_might_be_recursive ` to be computed
275- ahead-of-time when compiling a fused component-to-component adapter (where both
276- caller and callee intances and their relationship are statically known). In the
277- future this check will be relaxed and more sophisticated optimizations can be
278- used to statically eliminate the check in common cases.
222+ TODO
279223
280224The other fields of ` ComponentInstance ` are described below as they are used.
281225
@@ -720,16 +664,14 @@ spec-level function type, where the host can be the caller, the callee or even
720664OnStart = Callable[[], list[any ]]
721665OnResolve = Callable[[Optional[list[any ]]], None ]
722666OnCancel = Callable[[], None ]
723- FuncInst = Callable[[Supertask, OnStart, OnResolve], OnCancel]
667+ FuncInst = Callable[[OnStart, OnResolve, Optional[ComponentInstance] ], OnCancel]
724668```
725669The three parameters of ` FuncInst ` are:
726- * an optional caller ` Supertask ` which is used to maintain the
727- [ async callstack] [ Structured Concurrency ] and enforce the
728- non-reentrance [ component invariant] ;
729670* an ` OnStart ` callback that is called by the callee when it is ready to
730671 receive its arguments after waiting for any [ backpressure] to subside;
731672* an ` OnResolve ` callback that is called by the callee when it is ready to
732673 return its value or, if cancellation has been requested, ` None ` .
674+ * the caller's ` ComponentInstance ` , if the caller is not the host
733675
734676Critically, if the callee [ blocks] at the wasm level, the spec-level ` FuncInst `
735677returns immediately to the caller while continuing to execute the callee in a
@@ -745,7 +687,7 @@ call creates a `Task` object to track the state of the call and ensure that the
745687wasm guest code adheres to the above ` FuncInst ` calling convention (or else
746688traps). ` Task ` is introduced in chunks, starting with fields and initialization:
747689``` python
748- class Task ( Supertask ) :
690+ class Task :
749691 class State (Enum ):
750692 INITIAL = 1
751693 STARTED = 2
@@ -756,19 +698,17 @@ class Task(Supertask):
756698 ft: FuncType
757699 opts: CanonicalOptions
758700 inst: ComponentInstance
759- supertask: Supertask
760701 on_start: OnStart
761702 on_resolve: OnResolve
762703 state: State
763704 num_borrows: int
764705 waiting_to_enter: Optional[Thread]
765706 threads: list[Thread]
766707
767- def __init__ (self , ft , opts , inst , supertask , on_start , on_resolve ):
708+ def __init__ (self , ft , opts , inst , on_start , on_resolve ):
768709 self .ft = ft
769710 self .opts = opts
770711 self .inst = inst
771- self .supertask = supertask
772712 self .on_start = on_start
773713 self .on_resolve = on_resolve
774714 self .state = Task.State.INITIAL
@@ -1013,44 +953,56 @@ CoreFuncInst = Callable[[list[CoreValType]], list[CoreValType]]
1013953
1014954class Store :
1015955 waiting: list[Thread]
956+ nesting_depth: int
1016957
1017958 def __init__ (self ):
1018959 self .waiting = []
960+ self .nesting_depth = 0
1019961
1020- def invoke (self , f : FuncInst, caller : Optional[Supertask], on_start , on_resolve ) -> OnCancel:
1021- host_caller = Supertask()
1022- host_caller.inst = None
1023- host_caller.supertask = caller
1024- return f(host_caller, on_start, on_resolve)
962+ def invoke (self , f : FuncInst, on_start : OnStart, on_resolve : OnResolve ) -> OnCancel:
963+ self .nesting_depth += 1
964+ on_cancel = f(on_start, on_resolve, caller = None )
965+ self .nesting_depth -= 1
966+ return on_cancel
1025967
1026968 def lift (self , f : CoreFuncInst, ft : FuncType, opts : CanonicalOptions, inst : ComponentInstance) -> FuncInst:
1027- def func_inst (caller : Supertask, on_start : OnStart, on_resolve : OnResolve) -> OnCancel:
1028- trap_if(call_might_be_recursive(caller, inst))
1029- return canon_lift(f, ft, opts, inst, caller, on_start, on_resolve)
969+ def func_inst (on_start : OnStart, on_resolve : OnResolve, caller : Optional[ComponentInstance]) -> OnCancel:
970+ inst.enter_from(caller)
971+ on_cancel = canon_lift(f, ft, opts, inst, on_start, on_resolve)
972+ inst.leave_to(caller)
973+ return on_cancel
1030974 return func_inst
1031975
1032976 def lower (self , f : FuncInst, ft : FuncType, opts : CanonicalOptions, inst : ComponentInstance) -> CoreFuncInst:
1033977 def core_func_inst (args : list[CoreValType]) -> list[CoreValType]:
1034- assert (current_instance() is inst)
978+ assert (current_instance() is inst and self .nesting_depth > 0 )
1035979 return canon_lower(f, ft, opts, args)
1036980 return core_func_inst
1037981
1038982 def tick (self ):
1039- random.shuffle(self .waiting)
1040- for thread in self .waiting:
1041- if thread.ready():
1042- thread.resume()
1043- return
1044- ```
983+ assert (self .nesting_depth == 0 )
984+ candidates = { t for t in self .waiting if t.ready() }
985+ if candidates:
986+ thread = random.choice(list (candidates))
987+ self .nesting_depth += 1
988+ thread.task.inst.enter_from(None )
989+ thread.resume()
990+ thread.task.inst.leave_to(None )
991+ self .nesting_depth -= 1
992+ ```
993+ TODO: ` nesting_depth `
994+
1045995The ` FuncInst ` passed to ` Store.invoke ` is described above and can represent
1046996either a guest function (produced by ` Store.lift ` ) or (in the special case of a
1047997component re-export of a host import) a host function. ` Store.invoke ` describes
1048998how a ` FuncInst ` is invoked by the host, but ` FuncInst ` s can also be invoked by
1049999guest code that calls the ` CoreFuncInst ` produced by ` Store.lower ` .
10501000
1001+ TODO: update
10511002When a ` FuncInst ` is produced by lifting core wasm guest code, it is guarded
10521003by a call to ` call_might_be_recursive ` , which is described above.
10531004
1005+ TODO: ` nesting_depth == 0 `
10541006The ` Store.tick ` method does not have an analogue in Core WebAssembly and
10551007enables [ native concurrency support] ( Concurrency.md ) in the Component Model. The
10561008expectation is that the host will interleave calls to ` invoke ` with calls to
@@ -3503,7 +3455,7 @@ executes in a new *implicit thread* defined here by `thread_func`. The first
35033455thing this implicit thread does is to wait for any backpressure, as defined by
35043456` Task.enter_implicit_thread ` above:
35053457``` python
3506- def canon_lift (callee , ft , opts , inst , caller , on_start , on_resolve ) -> OnCancel:
3458+ def canon_lift (callee , ft , opts , inst , on_start , on_resolve ) -> OnCancel:
35073459 def thread_func ():
35083460 if not task.enter_implicit_thread():
35093461 return
@@ -3635,7 +3587,7 @@ required by the `FuncInst` calling contract. Lastly, `canon_lift` returns
36353587` Task.request_cancellation ` , bound to the call's new task, as the required
36363588` OnCancel ` value.
36373589``` python
3638- task = Task(ft, opts, inst, caller, on_start, on_resolve)
3590+ task = Task(ft, opts, inst, on_start, on_resolve)
36393591 thread = Thread(task, thread_func)
36403592 thread.resume()
36413593 return task.request_cancellation
@@ -3775,7 +3727,7 @@ above).
37753727 nonlocal flat_results
37763728 flat_results = lower_flat_values(cx, max_flat_results, result, ft.result_type(), flat_args)
37773729
3778- subtask.on_cancel = callee(thread.task, on_start, on_resolve)
3730+ subtask.on_cancel = callee(on_start, on_resolve, caller = thread.task.inst )
37793731 assert (ft.async_ or subtask.state == Subtask.State.RETURNED )
37803732```
37813733The ` Subtask.state ` field is updated by the callbacks to keep track of the
@@ -3899,17 +3851,13 @@ def canon_resource_drop(rt, i):
38993851 trap_if(h.num_lends != 0 )
39003852 if h.own:
39013853 assert (h.borrow_scope is None )
3902- if inst is rt.impl:
3903- if rt.dtor:
3904- rt.dtor(h.rep)
3905- else :
3906- caller_opts = CanonicalOptions(async_ = False )
3907- callee_opts = CanonicalOptions(async_ = rt.dtor_async, callback = rt.dtor_callback)
3908- ft = FuncType([U32Type()],[], async_ = False )
3909- dtor = rt.dtor or (lambda rep : [])
3910- callee = inst.store.lift(dtor, ft, callee_opts, rt.impl)
3911- caller = inst.store.lower(callee, ft, caller_opts, inst)
3912- caller([h.rep])
3854+ caller_opts = CanonicalOptions(async_ = False )
3855+ callee_opts = CanonicalOptions(async_ = rt.dtor_async, callback = rt.dtor_callback)
3856+ ft = FuncType([U32Type()], [], async_ = False )
3857+ dtor = rt.dtor or (lambda rep : [])
3858+ callee = inst.store.lift(dtor, ft, callee_opts, rt.impl)
3859+ caller = inst.store.lower(callee, ft, caller_opts, inst)
3860+ caller([h.rep])
39133861 else :
39143862 h.borrow_scope.num_borrows -= 1
39153863 return []
@@ -3920,15 +3868,12 @@ the private `i32` representation as a parameter. Thus, destructors *may* block
39203868on I/O, but only after they ` task.return ` , ensuring that ` resource.drop ` never
39213869blocks.
39223870
3923- Since there are valid reasons to call ` resource.drop ` in the same component
3924- instance that defined the resource, which would otherwise trap at the
3925- reentrance guard of ` Store.lift ` , an exception is made when the resource type's
3926- implementation-instance is the same as the current instance (which is
3927- statically known for any given ` canon resource.drop ` ).
3928-
3929- When a destructor isn't present, there is still a trap on recursive reentrance
3930- since this is the caller's responsibility and the presence or absence of a
3931- destructor is an encapsulated implementation detail of the resource type.
3871+ In particular, the ` ComponentIntance.enter_from ` method called by ` Store.lift `
3872+ may triger a trap if the call to the destructor would reenter the destructor's
3873+ instance. In the special case where the ` current_instance ` is the * same* as the
3874+ destructor's instance, ` ComponentInstance.enter_from ` is defined to * not* trap
3875+ and thus, as one might expect, component instances can always ` resource.drop `
3876+ the owned handles that they created (via ` resource.new ` ).
39323877
39333878
39343879### ` canon resource.rep `
0 commit comments