Skip to content

Commit

Permalink
CABI: put all resources into a single table, instead of per-resource-…
Browse files Browse the repository at this point in the history
…type

Resolves #395
  • Loading branch information
lukewagner committed Dec 5, 2024
1 parent ee4822a commit 58a3d89
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 109 deletions.
99 changes: 36 additions & 63 deletions design/mvp/CanonicalABI.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ The `ComponentInstance` class contains all the relevant per-component-instance
state that `canon`-generated functions use to maintain component invariants.
```python
class ComponentInstance:
resources: ResourceTables
resources: Table[ResourceHandle]
waitables: Table[Subtask|StreamHandle|FutureHandle]
error_contexts: Table[ErrorContext]
num_tasks: int
Expand All @@ -161,7 +161,7 @@ class ComponentInstance:
starting_pending_task: bool

def __init__(self):
self.resources = ResourceTables()
self.resources = Table[ResourceHandle]()
self.waitables = Table[Subtask|StreamHandle|FutureHandle]()
self.error_contexts = Table[ErrorContext]()
self.num_tasks = 0
Expand Down Expand Up @@ -233,54 +233,27 @@ sentinel values).

#### Resource State

The `ResourceTables` stored in the `resources` field maps `ResourceType`s to
`Table`s of `ResourceHandle`s (defined next), establishing a separate
`i32`-indexed array per resource type:
```python
class ResourceTables:
rt_to_table: MutableMapping[ResourceType, Table[ResourceHandle]]

def __init__(self):
self.rt_to_table = dict()

def table(self, rt):
if rt not in self.rt_to_table:
self.rt_to_table[rt] = Table[ResourceHandle]()
return self.rt_to_table[rt]

def get(self, rt, i):
return self.table(rt).get(i)
def add(self, rt, h):
return self.table(rt).add(h)
def remove(self, rt, i):
return self.table(rt).remove(i)
```
While this Python code performs a dynamic hash-table lookup on each handle
table access, as we'll see below, the `rt` parameter is always statically known
such that a normal implementation can statically enumerate all `Table` objects
at compile time and then route the calls to `get`, `add` and `remove` to the
correct `Table` at the callsite. The net result is that each component instance
will contain one handle table per resource type used by the component, with
each compiled adapter function accessing the correct handle table as-if it were
a global variable.

The `ResourceHandle` class defines the elements of the per-resource-type
`Table`s stored in `ResourceTables`:
The `ResourceHandle` class defines the elements of the `resources` field of
`ComponentInstance`:
```python
class ResourceHandle:
rt: ResourceType
rep: int
own: bool
borrow_scope: Optional[Task]
lend_count: int

def __init__(self, rep, own, borrow_scope = None):
def __init__(self, rt, rep, own, borrow_scope = None):
self.rt = rt
self.rep = rep
self.own = own
self.borrow_scope = borrow_scope
self.lend_count = 0
```
The `rep` field of `ResourceHandle` stores the resource representation
(currently fixed to be an `i32`) passed to `resource.new`.
The `rt` and `rep` fields of `ResourceHandle` store the `rt` and `rep`
parameters passed to the `resource.new` call that created this handle. The
`rep` field is currently fixed to be an `i32`, but will be generalized in the
future to other types.

The `own` field indicates whether this element was created from an `own` type
(or, if false, a `borrow` type).
Expand All @@ -296,16 +269,12 @@ live handles that were lent from this `own` handle (by calls to `borrow`-taking
functions). This count is maintained by the `ImportCall` bookkeeping functions
(above) and is ensured to be zero when an `own` handle is dropped.

An optimizing implementation can enumerate the canonical definitions present
in a component to statically determine that a given resource type's handle
table only contains `own` or `borrow` handles and then, based on this,
statically eliminate the `own` and the `lend_count` xor `borrow_scope` fields,
and guards thereof.

The `ResourceType` class represents a concrete resource type that has been
created by the component instance `impl`. `ResourceType` objects are used as
keys by `ResourceTables` above and thus we assume that Python object identity
corresponds to resource type equality, as defined by [type checking] rules.
The `ResourceType` class represents a runtime instance of a resource type that
has been created either by the host or a component instance (where multiple
component instances of the same static component create multiple `ResourceType`
instances). `ResourceType` Python object identity is used by trapping guards on
the `rt` field of `ResourceHandle` (above) and thus resource type equality is
*not* defined structurally (on the contents of `ResourceType`).
```python
class ResourceType(Type):
impl: ComponentInstance
Expand Down Expand Up @@ -1583,7 +1552,8 @@ possible, for example if the index was actually a `borrow` or if the `own`
handle is currently being lent out as borrows.
```python
def lift_own(cx, i, t):
h = cx.inst.resources.remove(t.rt, i)
h = cx.inst.resources.remove(i)
trap_if(h.rt is not t.rt)
trap_if(h.lend_count != 0)
trap_if(not h.own)
return h.rep
Expand All @@ -1601,7 +1571,8 @@ component instance's handle table:
```python
def lift_borrow(cx, i, t):
assert(isinstance(cx.borrow_scope, Subtask))
h = cx.inst.resources.get(t.rt, i)
h = cx.inst.resources.get(i)
trap_if(h.rt is not t.rt)
if h.own:
cx.borrow_scope.add_lender(h)
else:
Expand Down Expand Up @@ -2056,21 +2027,21 @@ def pack_flags_into_int(v, labels):
```

Finally, `own` and `borrow` handles are lowered by initializing new handle
elements in the current component instance's handle table. The increment of
`borrow_scope.todo` is complemented by a decrement in `canon_resource_drop`
elements in the current component instance's `resources` table. The increment
of `borrow_scope.todo` is complemented by a decrement in `canon_resource_drop`
and ensures that all borrowed handles are dropped before the end of the task.
```python
def lower_own(cx, rep, t):
h = ResourceHandle(rep, own = True)
return cx.inst.resources.add(t.rt, h)
h = ResourceHandle(t.rt, rep, own = True)
return cx.inst.resources.add(h)

def lower_borrow(cx, rep, t):
assert(isinstance(cx.borrow_scope, Task))
if cx.inst is t.rt.impl:
return rep
h = ResourceHandle(rep, own = False, borrow_scope = cx.borrow_scope)
h = ResourceHandle(t.rt, rep, own = False, borrow_scope = cx.borrow_scope)
h.borrow_scope.todo += 1
return cx.inst.resources.add(t.rt, h)
return cx.inst.resources.add(h)
```
The special case in `lower_borrow` is an optimization, recognizing that, when
a borrowed handle is passed to the component that implemented the resource
Expand Down Expand Up @@ -2796,12 +2767,12 @@ validation specifies:

Calling `$f` invokes the following function, which adds an owning handle
containing the given resource representation in the current component
instance's handle table:
instance's `resources` table:
```python
async def canon_resource_new(rt, task, rep):
trap_if(not task.inst.may_leave)
h = ResourceHandle(rep, own = True)
i = task.inst.resources.add(rt, h)
h = ResourceHandle(rt, rep, own = True)
i = task.inst.resources.add(h)
return [i]
```

Expand All @@ -2816,13 +2787,14 @@ validation specifies:
* `$f` is given type `(func (param i32))`

Calling `$f` invokes the following function, which removes the handle from the
current component instance's handle table and, if the handle was owning, calls
the resource's destructor.
current component instance's `resources` table and, if the handle was owning,
calls the resource's destructor.
```python
async def canon_resource_drop(rt, sync, task, i):
trap_if(not task.inst.may_leave)
inst = task.inst
h = inst.resources.remove(rt, i)
h = inst.resources.remove(i)
trap_if(h.rt is not rt)
flat_results = [] if sync else [0]
if h.own:
assert(h.borrow_scope is None)
Expand Down Expand Up @@ -2872,7 +2844,8 @@ Calling `$f` invokes the following function, which extracts the resource
representation from the handle.
```python
async def canon_resource_rep(rt, task, i):
h = task.inst.resources.get(rt, i)
h = task.inst.resources.get(i)
trap_if(h.rt is not rt)
return [h.rep]
```
Note that the "locally-defined" requirement above ensures that only the
Expand Down
52 changes: 20 additions & 32 deletions design/mvp/canonical-abi/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from __future__ import annotations
from dataclasses import dataclass
from functools import partial
from typing import Any, Optional, Callable, Awaitable, Literal, MutableMapping, TypeVar, Generic
from typing import Any, Optional, Callable, Awaitable, TypeVar, Generic
from enum import IntEnum
from copy import copy
import math
Expand Down Expand Up @@ -205,7 +205,7 @@ class CanonicalOptions:
### Runtime State

class ComponentInstance:
resources: ResourceTables
resources: Table[ResourceHandle]
waitables: Table[Subtask|StreamHandle|FutureHandle]
error_contexts: Table[ErrorContext]
num_tasks: int
Expand All @@ -216,7 +216,7 @@ class ComponentInstance:
starting_pending_task: bool

def __init__(self):
self.resources = ResourceTables()
self.resources = Table[ResourceHandle]()
self.waitables = Table[Subtask|StreamHandle|FutureHandle]()
self.error_contexts = Table[ErrorContext]()
self.num_tasks = 0
Expand Down Expand Up @@ -264,31 +264,15 @@ def remove(self, i):

#### Resource State

class ResourceTables:
rt_to_table: MutableMapping[ResourceType, Table[ResourceHandle]]

def __init__(self):
self.rt_to_table = dict()

def table(self, rt):
if rt not in self.rt_to_table:
self.rt_to_table[rt] = Table[ResourceHandle]()
return self.rt_to_table[rt]

def get(self, rt, i):
return self.table(rt).get(i)
def add(self, rt, h):
return self.table(rt).add(h)
def remove(self, rt, i):
return self.table(rt).remove(i)

class ResourceHandle:
rt: ResourceType
rep: int
own: bool
borrow_scope: Optional[Task]
lend_count: int

def __init__(self, rep, own, borrow_scope = None):
def __init__(self, rt, rep, own, borrow_scope = None):
self.rt = rt
self.rep = rep
self.own = own
self.borrow_scope = borrow_scope
Expand Down Expand Up @@ -1083,14 +1067,16 @@ def unpack_flags_from_int(i, labels):
return record

def lift_own(cx, i, t):
h = cx.inst.resources.remove(t.rt, i)
h = cx.inst.resources.remove(i)
trap_if(h.rt is not t.rt)
trap_if(h.lend_count != 0)
trap_if(not h.own)
return h.rep

def lift_borrow(cx, i, t):
assert(isinstance(cx.borrow_scope, Subtask))
h = cx.inst.resources.get(t.rt, i)
h = cx.inst.resources.get(i)
trap_if(h.rt is not t.rt)
if h.own:
cx.borrow_scope.add_lender(h)
else:
Expand Down Expand Up @@ -1403,16 +1389,16 @@ def pack_flags_into_int(v, labels):
return i

def lower_own(cx, rep, t):
h = ResourceHandle(rep, own = True)
return cx.inst.resources.add(t.rt, h)
h = ResourceHandle(t.rt, rep, own = True)
return cx.inst.resources.add(h)

def lower_borrow(cx, rep, t):
assert(isinstance(cx.borrow_scope, Task))
if cx.inst is t.rt.impl:
return rep
h = ResourceHandle(rep, own = False, borrow_scope = cx.borrow_scope)
h = ResourceHandle(t.rt, rep, own = False, borrow_scope = cx.borrow_scope)
h.borrow_scope.todo += 1
return cx.inst.resources.add(t.rt, h)
return cx.inst.resources.add(h)

def lower_stream(cx, v, t):
return lower_async_value(ReadableStreamHandle, WritableStreamHandle, cx, v, t)
Expand Down Expand Up @@ -1820,16 +1806,17 @@ async def do_call(on_block):

async def canon_resource_new(rt, task, rep):
trap_if(not task.inst.may_leave)
h = ResourceHandle(rep, own = True)
i = task.inst.resources.add(rt, h)
h = ResourceHandle(rt, rep, own = True)
i = task.inst.resources.add(h)
return [i]

### `canon resource.drop`

async def canon_resource_drop(rt, sync, task, i):
trap_if(not task.inst.may_leave)
inst = task.inst
h = inst.resources.remove(rt, i)
h = inst.resources.remove(i)
trap_if(h.rt is not rt)
flat_results = [] if sync else [0]
if h.own:
assert(h.borrow_scope is None)
Expand All @@ -1853,7 +1840,8 @@ async def canon_resource_drop(rt, sync, task, i):
### `canon resource.rep`

async def canon_resource_rep(rt, task, i):
h = task.inst.resources.get(rt, i)
h = task.inst.resources.get(i)
trap_if(h.rt is not rt)
return [h.rep]

### 🔀 `canon task.backpressure`
Expand Down
28 changes: 14 additions & 14 deletions design/mvp/canonical-abi/run_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,8 +438,8 @@ async def core_wasm(task, args):
nonlocal dtor_value

assert(len(args) == 4)
assert(len(inst.resources.table(rt).array) == 4)
assert(inst.resources.table(rt).array[0] is None)
assert(len(inst.resources.array) == 4)
assert(inst.resources.array[0] is None)
assert(args[0] == 1)
assert(args[1] == 2)
assert(args[2] == 3)
Expand All @@ -466,22 +466,22 @@ async def core_wasm(task, args):
dtor_value = None
await canon_resource_drop(rt, True, task, 1)
assert(dtor_value == 42)
assert(len(inst.resources.table(rt).array) == 5)
assert(inst.resources.table(rt).array[1] is None)
assert(len(inst.resources.table(rt).free) == 1)
assert(len(inst.resources.array) == 5)
assert(inst.resources.array[1] is None)
assert(len(inst.resources.free) == 1)

h = (await canon_resource_new(rt, task, 46))[0]
assert(h == 1)
assert(len(inst.resources.table(rt).array) == 5)
assert(inst.resources.table(rt).array[1] is not None)
assert(len(inst.resources.table(rt).free) == 0)
assert(len(inst.resources.array) == 5)
assert(inst.resources.array[1] is not None)
assert(len(inst.resources.free) == 0)

dtor_value = None
await canon_resource_drop(rt, True, task, 3)
assert(dtor_value is None)
assert(len(inst.resources.table(rt).array) == 5)
assert(inst.resources.table(rt).array[3] is None)
assert(len(inst.resources.table(rt).free) == 1)
assert(len(inst.resources.array) == 5)
assert(inst.resources.array[3] is None)
assert(len(inst.resources.free) == 1)

return [1, 2, 4]

Expand Down Expand Up @@ -510,9 +510,9 @@ def on_return(results):
assert(got[0] == 46)
assert(got[1] == 43)
assert(got[2] == 45)
assert(len(inst.resources.table(rt).array) == 5)
assert(all(inst.resources.table(rt).array[i] is None for i in range(4)))
assert(len(inst.resources.table(rt).free) == 4)
assert(len(inst.resources.array) == 5)
assert(all(inst.resources.array[i] is None for i in range(4)))
assert(len(inst.resources.free) == 4)
definitions.MAX_FLAT_RESULTS = before


Expand Down

0 comments on commit 58a3d89

Please sign in to comment.