@@ -130,8 +130,11 @@ class Store:
130130 def __init__ (self ):
131131 self .pending = []
132132
133- def invoke (self , f : FuncInst, caller , on_start , on_resolve ) -> Call:
134- return f(caller, on_start, on_resolve)
133+ def invoke (self , f : FuncInst, caller : Optional[Supertask], on_start , on_resolve ) -> Call:
134+ host_caller = Supertask()
135+ host_caller.inst = None
136+ host_caller.supertask = caller
137+ return f(host_caller, on_start, on_resolve)
135138
136139 def tick (self ):
137140 random.shuffle(self .pending)
@@ -167,7 +170,7 @@ OnStart = Callable[[], list[any]]
167170OnResolve = Callable[[Optional[list[any ]]], None ]
168171
169172class Supertask :
170- inst: ComponentInstance
173+ inst: Optional[ ComponentInstance]
171174 supertask: Optional[Supertask]
172175
173176class Call :
@@ -190,6 +193,14 @@ However, as described in the [concurrency explainer], an async call's
190193(currently) that the caller can know or do about it (hence there are
191194currently no other methods on ` Call ` ).
192195
196+ The optional ` Supertask.inst ` field either points to the ` ComponentInstance `
197+ containing the supertask or, if ` None ` , indicates that the supertask is a host
198+ function. Because ` Store.invoke ` unconditionally appends a host ` Supertask ` ,
199+ every callstack is rooted by a host ` Supertask ` . There is no prohibition on
200+ component-to-host-to-component calls (as long as the conditions checked by
201+ ` call_might_be_recursive ` are satisfied) and thus host ` Supertask ` s may also
202+ appear anywhere else in the callstack.
203+
193204
194205## Supporting definitions
195206
@@ -280,24 +291,147 @@ behavior and enforce invariants.
280291``` python
281292class ComponentInstance :
282293 store: Store
294+ parent: Optional[ComponentInstance]
283295 table: Table
284296 may_leave: bool
285297 backpressure: int
286298 exclusive: bool
287299 num_waiting_to_enter: int
288300
289- def __init__ (self , store ):
301+ def __init__ (self , store , parent = None ):
302+ assert (parent is None or parent.store is store)
290303 self .store = store
304+ self .parent = parent
291305 self .table = Table()
292306 self .may_leave = True
293307 self .backpressure = 0
294308 self .exclusive = False
295309 self .num_waiting_to_enter = 0
296310```
297311Components are always instantiated in the context of a ` Store ` which is saved
298- immutably in the ` store ` field. The other fields are described below as they
299- are used.
312+ immutably in the ` store ` field.
313+
314+ If a component is instantiated by an ` instantiate ` expression in a "parent"
315+ component, the parent's ` ComponentInstance ` is immutably saved in the ` parent `
316+ field of the child's ` ComponentInstance ` . If instead a component is
317+ instantiated directly by the host, the ` parent ` field is ` None ` . Thus, the set
318+ of component instances in a store forms a forest rooted by the component
319+ instances that were instantiated directly by the host.
320+
321+ Based on this, the "reflexive ancestors" of a component (i.e., all parent
322+ component instances up to the root component including the component itself) can
323+ be enumerated and tested via these two helper functions:
324+ ``` python
325+ def reflexive_ancestors (self ) -> set[ComponentInstance]:
326+ s = set ()
327+ inst = self
328+ while inst is not None :
329+ s.add(inst)
330+ inst = inst.parent
331+ return s
332+
333+ def is_reflexive_ancestor_of (self , other ):
334+ while other is not None :
335+ if self is other:
336+ return True
337+ other = other.parent
338+ return False
339+ ```
300340
341+ How the host instantiates and invokes root components is up to the host and not
342+ specified by the Component Model. Exports of previously-instantiated root
343+ components * may* be supplied as the imports of subsequently-instantiated root
344+ components. Due to the ordered nature of instantiation, root components cannot
345+ directly import each others' exports in cyclic manner. However, the host * may*
346+ attempt to perform cyclic component-to-host-to-component calls using host
347+ powers.
348+
349+ Because a child component is fully encapsulated by its parent component (with
350+ all child imports specified by the parent's ` instantiate ` expression and access
351+ to all child exports controlled by the parent through its private instance index
352+ space), the host does not have direct control over how a child component is
353+ instantiated or invoked. However, if a child's ancestors transitively forward
354+ the root component's host-supplied imports to the child, direct child-to-host
355+ calls are possible. Symmetrically, if a child's ancestors transitively
356+ re-export the child's exports from the root component, direct host-to-child
357+ calls are possible.
358+
359+ Recursive component calls are technically possible using either host powers (as
360+ mentioned above) or via a parent component lowering a child component's export
361+ to a ` funcref ` and then recursively calling this ` funcref ` from a lifted parent
362+ function passed as an import to the child. However, for the time being, both
363+ cases are prevented via trap for several reasons:
364+ * automatic [ backpressure] would otherwise deadlock in unpredictable and
365+ surprising ways;
366+ * by default, most code does not expect [ recursive reentrance] and will break
367+ in subtle and potentially security sensitive ways if allowed;
368+ * to properly handle recursive reentrance, an extra ABI parameter is required
369+ to link recursive calls and this requires opting in via some
370+ [ TBD] ( Concurrency.md#TODO ) function effect type or canonical ABI option.
371+
372+ The ` call_might_be_recursive ` predicate is used by ` canon_lift ` and
373+ ` canon_resource_drop ` (defined below) to conservatively detect recursive
374+ reentrance and subsequently trap.
375+ ``` python
376+ def call_might_be_recursive (caller : Supertask, callee_inst : ComponentInstance):
377+ if caller.inst is None :
378+ while caller is not None :
379+ if caller.inst and caller.inst.reflexive_ancestors() & callee_inst.reflexive_ancestors():
380+ return True
381+ caller = caller.supertask
382+ return False
383+ else :
384+ return (caller.inst.is_reflexive_ancestor_of(callee_inst) or
385+ callee_inst.is_reflexive_ancestor_of(caller.inst))
386+ ```
387+ The first case (where ` caller.inst ` is ` None ` ) covers host-to-component calls.
388+ By testing whether any of the callers' reflexive anecestor sets intersect the
389+ callee's ancestor set, the following case is considered recursive:
390+ ```
391+ +-------+
392+ | A |<-.
393+ | +---+ | |
394+ host-->| B |-->host
395+ | +---+ |
396+ +-------+
397+ ```
398+ Here, when attempting to recursively call back into ` A ` , ` caller ` points to the
399+ following stack:
400+ ```
401+ |inst=None| --supertask--> |inst=B| --supertask--> |inst=None| --supertask--> None
402+ ```
403+ while ` A ` does not appear as the ` inst ` of any ` Supertask ` on this stack,
404+ ` B.reflexive_ancestors() ` is ` { B, A } ` , so the loop correctly determines that
405+ ` A ` is being reentered. This ensures that child components are kept an
406+ encapsulated detail of the parent.
407+
408+ The second case (where ` caller.inst ` is not ` None ` ) covers component-to-
409+ component calls by conservatively rejecting any call from a component to its
410+ anecestor or descendant (thereby preventing any possible recursion via ancestor
411+ ` funcref ` ). Thus, the following sibling-to-sibling component call is allowed:
412+ ```
413+ +----------------+
414+ | P |
415+ | +----+ +----+ |
416+ host-->| C1 |->| C2 | |
417+ | +----+ +----+ |
418+ +----------------+
419+ ```
420+ while the following child-to-parent and parent-to-child calls are disallowed:
421+ ```
422+ +----------+ +----------+
423+ | +---+ | | +---+ |
424+ host-->| C |->P | host->| P->| C | |
425+ | +---+ | | +---+ |
426+ +----------+ +----------+
427+ ```
428+ This conservative approximation allows ` call_might_be_recursive ` to be computed
429+ ahead-of-time when compiling a fused component-to-component adapter (where both
430+ caller and callee intances and their relationship are statically known). In the
431+ future this check will be relaxed and more sophisticated optimizations can be
432+ used to statically eliminate the check in common cases.
433+
434+ The other fields of ` ComponentInstance ` are described below as they are used.
301435
302436#### Table State
303437
@@ -804,7 +938,7 @@ class Task(Call, Supertask):
804938 opts: CanonicalOptions
805939 inst: ComponentInstance
806940 ft: FuncType
807- supertask: Optional[Task]
941+ supertask: Supertask
808942 on_resolve: OnResolve
809943 num_borrows: int
810944 threads: list[Thread]
@@ -838,37 +972,6 @@ called (by the `Task.return_` and `Task.cancel` methods, defined below).
838972 assert (self .num_borrows == 0 )
839973```
840974
841- The ` Task.trap_if_on_the_stack ` method checks for unintended reentrance,
842- enforcing a [ component invariant] . This guard uses the ` Supertask ` defined by
843- the [ Embedding] ( #embedding ) interface to walk up the async call tree defined as
844- part of [ structured concurrency] . The async call tree is necessary to
845- distinguish between the deadlock-hazardous kind of reentrance (where the new
846- task is a transitive subtask of a task already running in the same component
847- instance) and the normal kind of async reentrance (where the new task is just a
848- sibling of any existing tasks running in the component instance). Note that, in
849- the [ future] ( Concurrency.md#TODO ) , there will be a way for a function to opt in
850- (via function type attribute) to the hazardous kind of reentrance, which will
851- nuance this test.
852- ``` python
853- def trap_if_on_the_stack (self , inst ):
854- c = self .supertask
855- while c is not None :
856- trap_if(c.inst is inst)
857- c = c.supertask
858- ```
859- An optimizing implementation can avoid the O(n) loop in ` trap_if_on_the_stack `
860- in several ways:
861- * Reentrance by a child component can (often) be statically ruled out when the
862- parent component doesn't both lift and lower the child's imports and exports
863- (i.e., "donut wrapping").
864- * Reentrance of the root component by the host can either be asserted not to
865- happen or be tracked in a per-root-component-instance flag.
866- * When a potentially-reenterable child component only lifts and lowers
867- synchronously, reentrance can be tracked in a per-component-instance flag.
868- * For the remaining cases, the live instances on the stack can be maintained in
869- a packed bit-vector (assigning each potentially-reenterable async component
870- instance a static bit position) that is passed by copy from caller to callee.
871-
872975The ` Task.needs_exclusive ` predicate returns whether the Canonical ABI options
873976indicate that the core wasm being executed does not expect to be reentered
874977(e.g., because the code is using a single global linear memory shadow stack).
@@ -3132,8 +3235,8 @@ Based on this, `canon_lift` is defined in chunks as follows, starting with how
31323235a ` lift ` ed function starts executing:
31333236``` python
31343237def canon_lift (opts , inst , ft , callee , caller , on_start , on_resolve ) -> Call:
3238+ trap_if(call_might_be_recursive(caller, inst))
31353239 task = Task(opts, inst, ft, caller, on_resolve)
3136- task.trap_if_on_the_stack(inst)
31373240 def thread_func (thread ):
31383241 if not task.enter(thread):
31393242 return
@@ -3147,16 +3250,16 @@ def canon_lift(opts, inst, ft, callee, caller, on_start, on_resolve) -> Call:
31473250 flat_ft = flatten_functype(opts, ft, ' lift' )
31483251 assert (types_match_values(flat_ft.params, flat_args))
31493252```
3150- Each call starts by immediately checking for unexpected reentrance using
3151- ` Task.trap_if_on_the_stack ` .
3253+ Each lifted function call starts by immediately trapping on possible recursive
3254+ reentrance (as defined by ` call_might_be_recursive ` above) .
31523255
31533256The ` thread_func ` is immediately called from a new ` Thread ` created and resumed
3154- at the end of ` canon_lift ` and so control flow proceeds directly from the
3155- ` trap_if_on_stack ` to the ` enter ` . ` Task.enter ` (defined above) suspends the
3156- newly-created ` Thread ` if there is backpressure until the backpressure is
3157- resolved. If the caller cancels the new ` Task ` while the ` Task ` is still
3158- waiting to ` enter ` , the call is aborted before the arguments are lowered (which
3159- means that owned-handle arguments are not transferred).
3257+ at the end of ` canon_lift ` and so control flow proceeds directly to the ` enter ` .
3258+ ` Task.enter ` (defined above) suspends the newly-created ` Thread ` if there is
3259+ backpressure until the backpressure is resolved. If the caller cancels the new
3260+ ` Task ` while the ` Task ` is still waiting to ` enter ` , the call is aborted before
3261+ the arguments are lowered (which means that owned-handle arguments are not
3262+ transferred).
31603263
31613264Once the backpressure gate is cleared, the ` Thread ` is added to the callee's
31623265component instance's table (storing the index for later retrieval by the
@@ -3541,7 +3644,7 @@ def canon_resource_drop(rt, thread, i):
35413644 callee = partial(canon_lift, callee_opts, rt.impl, ft, rt.dtor)
35423645 [] = canon_lower(caller_opts, ft, callee, thread, [h.rep])
35433646 else :
3544- thread.task.trap_if_on_the_stack( rt.impl)
3647+ trap_if(call_might_be_recursive( thread.task, rt.impl) )
35453648 else :
35463649 h.borrow_scope.num_borrows -= 1
35473650 return []
@@ -3558,9 +3661,9 @@ reentrance guard of `Task.enter`, an exception is made when the resource type's
35583661implementation-instance is the same as the current instance (which is
35593662statically known for any given ` canon resource.drop ` ).
35603663
3561- When a destructor isn't present, the rules still perform a reentrance check
3664+ When a destructor isn't present, there is still a trap on recursive reentrance
35623665since this is the caller's responsibility and the presence or absence of a
3563- destructor is an encapsualted implementation detail of the resource type.
3666+ destructor is an encapsulated implementation detail of the resource type.
35643667
35653668
35663669### ` canon resource.rep `
@@ -4780,6 +4883,7 @@ def canon_thread_available_parallelism():
47804883[ Concurrency Explainer ] : Concurrency.md
47814884[ Suspended ] : Concurrency#thread-built-ins
47824885[ Structured Concurrency ] : Concurrency.md#subtasks-and-supertasks
4886+ [ Recursive Reentrance ] : Concurrency.md#subtasks-and-supertasks
47834887[ Backpressure ] : Concurrency.md#backpressure
47844888[ Current Thread ] : Concurrency.md#current-thread-and-task
47854889[ Current Task ] : Concurrency.md#current-thread-and-task
0 commit comments