diff --git a/design/mvp/CanonicalABI.md b/design/mvp/CanonicalABI.md index dd2141bf..acd88ea3 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -210,31 +210,39 @@ The subsequent definitions of loading and storing a value from linear memory require additional context, which is threaded through most subsequent definitions via the `cx` parameter: ```python +class Context: + opts: CanonicalOptions + inst: ComponentInstance + called_as_export: bool +``` + +The `opts` field represents the [`canonopt`] values supplied to +currently-executing `canon lift` or `canon lower`: +```python class CanonicalOptions: memory: bytearray string_encoding: str realloc: Callable[[int,int,int,int],int] post_return: Callable[[],None] +``` +The `inst` field represents the component instance that the currently-executing +canonical definition is defined to execute inside. The `may_enter` and +`may_leave` fields are used to enforce the [component invariants]: `may_leave` +indicates whether the instance may call out to an import and the `may_enter` +state indicates whether the instance may be called from the outside world +through an export. +```python class ComponentInstance: may_leave = True may_enter = True # ... - -class Context: - opts: CanonicalOptions - inst: ComponentInstance ``` -Going through the fields of `Context`: - -The `opts` field represents the [`canonopt`] values supplied to -currently-executing `canon lift` or `canon lower`. -The `inst` field represents the component instance that the currently-executing -canonical definition is closed over. The `may_enter` and `may_leave` fields are -used to enforce the [component invariants]: `may_leave` indicates whether the -instance may call out to an import and the `may_enter` state indicates whether -the instance may be called from the outside world through an export. +Lastly, the `called_as_export` field indicates whether the lifted function is +being called through a component export or whether this is an internal call, +(for example, when a child component calls an import that is defined by its +parent component). ### Loading @@ -1204,29 +1212,29 @@ component*. Given the above closure arguments, `canon_lift` is defined: ```python -def canon_lift(callee_cx, callee, ft, args, called_as_export): - if called_as_export: - trap_if(not callee_cx.inst.may_enter) - callee_cx.inst.may_enter = False +def canon_lift(cx, callee, ft, args): + if cx.called_as_export: + trap_if(not cx.inst.may_enter) + cx.inst.may_enter = False else: - assert(not callee_cx.inst.may_enter) + assert(not cx.inst.may_enter) - assert(callee_cx.inst.may_leave) - callee_cx.inst.may_leave = False - flat_args = lower_values(callee_cx, MAX_FLAT_PARAMS, args, ft.param_types()) - callee_cx.inst.may_leave = True + assert(cx.inst.may_leave) + cx.inst.may_leave = False + flat_args = lower_values(cx, MAX_FLAT_PARAMS, args, ft.param_types()) + cx.inst.may_leave = True try: flat_results = callee(flat_args) except CoreWebAssemblyException: trap() - results = lift_values(callee_cx, MAX_FLAT_RESULTS, ValueIter(flat_results), ft.result_types()) + results = lift_values(cx, MAX_FLAT_RESULTS, ValueIter(flat_results), ft.result_types()) def post_return(): - if callee_cx.opts.post_return is not None: - callee_cx.opts.post_return(flat_results) - if called_as_export: - callee_cx.inst.may_enter = True + if cx.opts.post_return is not None: + cx.opts.post_return(flat_results) + if cx.called_as_export: + cx.inst.may_enter = True return (results, post_return) ``` @@ -1237,15 +1245,13 @@ boundaries. Thus, if a component wishes to signal an error, it must use some sort of explicit type such as `result` (whose `error` case particular language bindings may choose to map to and from exceptions). -The `called_as_export` parameter indicates whether `canon_lift` is being called -as part of a component export or whether this `canon_lift` is being called -internally (for example, by a child component instance). By clearing -`may_enter` for the duration of `canon_lift` when called as an export, the -dynamic traps ensure that components cannot be reentered, which is a [component -invariant]. Furthermore, because `may_enter` is not cleared on the exceptional -exit path taken by `trap()`, if there is a trap during Core WebAssembly -execution or lifting/lowering, the component is left permanently un-enterable, -ensuring the lockdown-after-trap [component invariant]. +By clearing `may_enter` for the duration of `canon_lift` when the function is +called as an export, the dynamic traps ensure that components cannot be +reentered, ensuring the non-reentrance [component invariant]. Furthermore, +because `may_enter` is not cleared on the exceptional exit path taken by +`trap()`, if there is a trap during Core WebAssembly execution of lifting or +lowering, the component is left permanently un-enterable, ensuring the +lockdown-after-trap [component invariant]. The contract assumed by `canon_lift` (and ensured by `canon_lower` below) is that the caller of `canon_lift` *must* call `post_return` right after lowering @@ -1272,17 +1278,17 @@ Thus, from the perspective of Core WebAssembly, `$f` is a [function instance] containing a `hostfunc` that closes over `$opts`, `$inst`, `$callee` and `$ft` and, when called from Core WebAssembly code, calls `canon_lower`, which is defined as: ```python -def canon_lower(caller_cx, callee, ft, flat_args): - trap_if(not caller_cx.inst.may_leave) +def canon_lower(cx, callee, ft, flat_args): + trap_if(not cx.inst.may_leave) flat_args = ValueIter(flat_args) - args = lift_values(caller_cx, MAX_FLAT_PARAMS, flat_args, ft.param_types()) + args = lift_values(cx, MAX_FLAT_PARAMS, flat_args, ft.param_types()) results, post_return = callee(args) - caller_cx.inst.may_leave = False - flat_results = lower_values(caller_cx, MAX_FLAT_RESULTS, results, ft.result_types(), flat_args) - caller_cx.inst.may_leave = True + cx.inst.may_leave = False + flat_results = lower_values(cx, MAX_FLAT_RESULTS, results, ft.result_types(), flat_args) + cx.inst.may_leave = True post_return() diff --git a/design/mvp/canonical-abi/definitions.py b/design/mvp/canonical-abi/definitions.py index 4cf18af9..b9ab43eb 100644 --- a/design/mvp/canonical-abi/definitions.py +++ b/design/mvp/canonical-abi/definitions.py @@ -4,6 +4,7 @@ ### Boilerplate +from __future__ import annotations import math import struct from dataclasses import dataclass @@ -271,21 +272,26 @@ def num_i32_flags(labels): ### Context +class Context: + opts: CanonicalOptions + inst: ComponentInstance + called_as_export: bool + +# + class CanonicalOptions: memory: bytearray string_encoding: str realloc: Callable[[int,int,int,int],int] post_return: Callable[[],None] +# + class ComponentInstance: may_leave = True may_enter = True # ... -class Context: - opts: CanonicalOptions - inst: ComponentInstance - ### Loading def load(cx, ptr, t): @@ -1002,45 +1008,45 @@ def lower_values(cx, max_flat, vs, ts, out_param = None): ### `lift` -def canon_lift(callee_cx, callee, ft, args, called_as_export): - if called_as_export: - trap_if(not callee_cx.inst.may_enter) - callee_cx.inst.may_enter = False +def canon_lift(cx, callee, ft, args): + if cx.called_as_export: + trap_if(not cx.inst.may_enter) + cx.inst.may_enter = False else: - assert(not callee_cx.inst.may_enter) + assert(not cx.inst.may_enter) - assert(callee_cx.inst.may_leave) - callee_cx.inst.may_leave = False - flat_args = lower_values(callee_cx, MAX_FLAT_PARAMS, args, ft.param_types()) - callee_cx.inst.may_leave = True + assert(cx.inst.may_leave) + cx.inst.may_leave = False + flat_args = lower_values(cx, MAX_FLAT_PARAMS, args, ft.param_types()) + cx.inst.may_leave = True try: flat_results = callee(flat_args) except CoreWebAssemblyException: trap() - results = lift_values(callee_cx, MAX_FLAT_RESULTS, ValueIter(flat_results), ft.result_types()) + results = lift_values(cx, MAX_FLAT_RESULTS, ValueIter(flat_results), ft.result_types()) def post_return(): - if callee_cx.opts.post_return is not None: - callee_cx.opts.post_return(flat_results) - if called_as_export: - callee_cx.inst.may_enter = True + if cx.opts.post_return is not None: + cx.opts.post_return(flat_results) + if cx.called_as_export: + cx.inst.may_enter = True return (results, post_return) ### `lower` -def canon_lower(caller_cx, callee, ft, flat_args): - trap_if(not caller_cx.inst.may_leave) +def canon_lower(cx, callee, ft, flat_args): + trap_if(not cx.inst.may_leave) flat_args = ValueIter(flat_args) - args = lift_values(caller_cx, MAX_FLAT_PARAMS, flat_args, ft.param_types()) + args = lift_values(cx, MAX_FLAT_PARAMS, flat_args, ft.param_types()) results, post_return = callee(args) - caller_cx.inst.may_leave = False - flat_results = lower_values(caller_cx, MAX_FLAT_RESULTS, results, ft.result_types(), flat_args) - caller_cx.inst.may_leave = True + cx.inst.may_leave = False + flat_results = lower_values(cx, MAX_FLAT_RESULTS, results, ft.result_types(), flat_args) + cx.inst.may_leave = True post_return() diff --git a/design/mvp/canonical-abi/run_tests.py b/design/mvp/canonical-abi/run_tests.py index 51fddf23..1500943f 100644 --- a/design/mvp/canonical-abi/run_tests.py +++ b/design/mvp/canonical-abi/run_tests.py @@ -32,7 +32,7 @@ def realloc(self, original_ptr, original_size, alignment, new_size): self.memory[ret : ret + original_size] = self.memory[original_ptr : original_ptr + original_size] return ret -def mk_cx(memory, encoding = None, realloc = None, post_return = None): +def mk_cx(memory = bytearray(), encoding = 'utf8', realloc = None, post_return = None): cx = Context() cx.opts = CanonicalOptions() cx.opts.memory = memory @@ -40,6 +40,7 @@ def mk_cx(memory, encoding = None, realloc = None, post_return = None): cx.opts.realloc = realloc cx.opts.post_return = post_return cx.inst = ComponentInstance() + cx.called_as_export = True return cx def mk_str(s): @@ -56,7 +57,7 @@ def fail(msg): raise BaseException(msg) def test(t, vals_to_lift, v, - cx = mk_cx(bytearray(), 'utf8', None, None), + cx = mk_cx(), dst_encoding = None, lower_t = None, lower_v = None): @@ -85,7 +86,7 @@ def test_name(): heap = Heap(5*len(cx.opts.memory)) if dst_encoding is None: dst_encoding = cx.opts.string_encoding - cx = mk_cx(heap.memory, dst_encoding, heap.realloc, None) + cx = mk_cx(heap.memory, dst_encoding, heap.realloc) lowered_vals = lower_flat(cx, v, lower_t) assert(flatten_type(lower_t) == list(map(lambda v: v.t, lowered_vals))) @@ -200,7 +201,7 @@ def test_nan64(inbits, outbits): def test_string_internal(src_encoding, dst_encoding, s, encoded, tagged_code_units): heap = Heap(len(encoded)) heap.memory[:] = encoded[:] - cx = mk_cx(heap.memory, src_encoding, None, None) + cx = mk_cx(heap.memory, src_encoding) v = (s, src_encoding, tagged_code_units) test(String(), [0, tagged_code_units], v, cx, dst_encoding) @@ -237,7 +238,7 @@ def test_string(src_encoding, dst_encoding, s): def test_heap(t, expect, args, byte_array): heap = Heap(byte_array) - cx = mk_cx(heap.memory, 'utf8', None, None) + cx = mk_cx(heap.memory) test(t, args, expect, cx) test_heap(List(Record([])), [{},{},{}], [0,3], []) @@ -348,10 +349,10 @@ def test_roundtrip(t, v): callee_heap = Heap(1000) callee_cx = mk_cx(callee_heap.memory, 'utf8', callee_heap.realloc, lambda x: () ) - lifted_callee = lambda args: canon_lift(callee_cx, callee, ft, args, True) + lifted_callee = lambda args: canon_lift(callee_cx, callee, ft, args) caller_heap = Heap(1000) - caller_cx = mk_cx(caller_heap.memory, 'utf8', caller_heap.realloc, None) + caller_cx = mk_cx(caller_heap.memory, 'utf8', caller_heap.realloc) flat_args = lower_flat(caller_cx, v, t) flat_results = canon_lower(caller_cx, lifted_callee, ft, flat_args)