From cefe268e50ef6336972f8c8f363e6fcb7f9ecdaf Mon Sep 17 00:00:00 2001 From: Luke Wagner Date: Thu, 22 Oct 2026 16:55:56 -0500 Subject: [PATCH] Fix memory64 feature to pass (rep i64) when lowering borrow --- design/mvp/Binary.md | 4 +- design/mvp/CanonicalABI.md | 391 ++++++++++++++---------- design/mvp/canonical-abi/definitions.py | 319 ++++++++++--------- design/mvp/canonical-abi/run_tests.py | 69 ++++- 4 files changed, 479 insertions(+), 304 deletions(-) diff --git a/design/mvp/Binary.md b/design/mvp/Binary.md index 42a268d2..622fdfb9 100644 --- a/design/mvp/Binary.md +++ b/design/mvp/Binary.md @@ -292,8 +292,8 @@ Notes: `none` case of an optional immediate.) * 🔧 for fixed-sized lists the length of the list must be larger than 0 to pass validation. -* Validation requires that, for every `defvaltype` `t`, `elem_size(t, 'i64')` is - less than 228, as defined by the +* Validation requires that, for every `defvaltype` `t`, the worst-case in-memory + element size is less than 228, as defined by `elem_size` in the [Canonical ABI](CanonicalABI.md#element-size). diff --git a/design/mvp/CanonicalABI.md b/design/mvp/CanonicalABI.md index c2c5887a..c070b38b 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -1027,20 +1027,23 @@ class CanonicalOptions(LiftLowerOptions): Most Canonical ABI definitions depend on some ambient information which is established by the `canon lift`- or `canon lower`-defined function that is being called: +* whether values are being lifted out of or lowered into core wasm * the ABI options supplied via [`canonopt`] * the containing component instance * the `Task` or `Subtask` used to lower or lift, resp., `borrow` handles -These three pieces of ambient information are stored in an `LiftLowerContext` +These four pieces of ambient information are stored in an `LiftLowerContext` object that is threaded through all the Python functions below as the `cx` parameter/field. ```python class LiftLowerContext: + direction: Literal['lift', 'lower'] opts: LiftLowerOptions inst: ComponentInstance borrow_scope: Optional[Task|Subtask] - def __init__(self, opts, inst, borrow_scope = None): + def __init__(self, direction, opts, inst, borrow_scope = None): + self.direction = direction self.opts = opts self.inst = inst self.borrow_scope = borrow_scope @@ -1048,6 +1051,16 @@ class LiftLowerContext: The `borrow_scope` field may be `None` if the types being lifted/lowered are known to not contain `borrow`. +The `direction` field indicates which of lifting or lowering (of values or whole +functions) is being performed. The `reverse_direction` method flips the +direction, which is used to specify how the parameters/arguments of a function +are to be lifted or lowered relative to the function as a whole: +```python + def reverse_direction(self): + new_direction = 'lift' if self.direction == 'lower' else 'lower' + return LiftLowerContext(new_direction, self.opts, self.inst, self.borrow_scope) +``` + The `LiftLowerContext.reallocate` and `LiftLowerContext.allocate` methods define how to dynamically allocate linear memory using the `realloc` `canonopt` when lowering non-flattened values. The definition below specifies this call as-if @@ -1183,8 +1196,8 @@ class ResourceHandle: self.num_lends = 0 ``` The `rt` and `rep` fields of `ResourceHandle` store the resource type and -representation of the resource. The `rep` field is currently fixed to be an -`i32`, but will be generalized in the future to other types. +representation of the resource, where `rep` is a core value of type `rt.rep` +(defined below). The `own` field indicates whether this element was created from an `own` type (or, if false, a `borrow` type). @@ -1207,12 +1220,16 @@ the `rt` field of `ResourceHandle` (above) and thus resource type equality is ```python class ResourceType(Type): impl: ComponentInstance + rep: Literal['i32', 'i64'] dtor: Optional[Callable] - def __init__(self, impl, dtor = None): + def __init__(self, impl, rep, dtor = None): self.impl = impl + self.rep = rep self.dtor = dtor ``` +The `rep` field stores the core value type given by the `(rep ...)` immediate +of the `resource` type definition. ### Waitable State @@ -1552,8 +1569,8 @@ class GuestBuffer(Buffer): def __init__(self, t, cx, ptr, length): trap_if(length > Buffer.MAX_LENGTH) if t and length > 0: - trap_if(ptr != align_to(ptr, alignment(t, cx.opts.memory.ptr_type()))) - trap_if(ptr + length * elem_size(t, cx.opts.memory.ptr_type()) > len(cx.opts.memory)) + trap_if(ptr != align_to(ptr, alignment(cx, t))) + trap_if(ptr + length * elem_size(cx, t) > len(cx.opts.memory)) self.cx = cx self.t = t self.ptr = ptr @@ -1568,7 +1585,7 @@ class ReadableGuestBuffer(GuestBuffer, ReadableBuffer): assert(n <= self.remain()) if self.t: vs = load_list_from_valid_range(self.cx, self.ptr, n, self.t) - self.ptr += n * elem_size(self.t, self.cx.opts.memory.ptr_type()) + self.ptr += n * elem_size(self.cx, self.t) else: vs = n * [()] self.progress += n @@ -1579,7 +1596,7 @@ class WritableGuestBuffer(GuestBuffer, WritableBuffer): assert(len(vs) <= self.remain()) if self.t: store_list_into_valid_range(self.cx, vs, self.ptr, self.t) - self.ptr += len(vs) * elem_size(self.t, self.cx.opts.memory.ptr_type()) + self.ptr += len(vs) * elem_size(self.cx, self.t) else: assert(all(v == () for v in vs)) self.progress += len(vs) @@ -1988,7 +2005,7 @@ Each value type is assigned an [alignment] which is used by subsequent Canonical ABI definitions. Presenting the definition of `alignment` piecewise, we start with the top-level case analysis: ```python -def alignment(t, ptr_type): +def alignment(cx, t): match despecialize(t): case BoolType() : return 1 case S8Type() | U8Type() : return 1 @@ -1998,31 +2015,33 @@ def alignment(t, ptr_type): case F32Type() : return 4 case F64Type() : return 8 case CharType() : return 4 - case StringType() : return ptr_size(ptr_type) + case StringType() : return cx.opts.memory.ptr_size() case ErrorContextType() : return 4 - case ListType(t, l) : return alignment_list(t, l, ptr_type) - case RecordType(fields) : return alignment_record(fields, ptr_type) - case VariantType(cases) : return alignment_variant(cases, ptr_type) + case ListType(t, l) : return alignment_list(cx, t, l) + case RecordType(fields) : return alignment_record(cx, fields) + case VariantType(cases) : return alignment_variant(cx, cases) case FlagsType(labels) : return alignment_flags(labels) - case OwnType() | BorrowType() : return 4 + case OwnType() : return 4 + case BorrowType() : return alignment_borrow(cx, t) case StreamType() | FutureType() : return 4 ``` List alignment is the same as tuple alignment when the length is fixed and otherwise uses the alignment of pointers. ```python -def alignment_list(elem_type, maybe_length, ptr_type): +def alignment_list(cx, elem_type, maybe_length): if maybe_length is not None: - return alignment(elem_type, ptr_type) - return ptr_size(ptr_type) + return alignment(cx, elem_type) + else: + return cx.opts.memory.ptr_size() ``` Record alignment is tuple alignment, with the definitions split for reuse below: ```python -def alignment_record(fields, ptr_type): +def alignment_record(cx, fields): a = 1 for f in fields: - a = max(a, alignment(f.t, ptr_type)) + a = max(a, alignment(cx, f.t)) return a ``` @@ -2032,8 +2051,8 @@ covering the number of cases in the variant (with cases numbered in order from compact representations of variants in memory. This smallest integer type is selected by the following function, used above and below: ```python -def alignment_variant(cases, ptr_type): - return max(alignment(discriminant_type(cases), ptr_type), max_case_alignment(cases, ptr_type)) +def alignment_variant(cx, cases): + return max(alignment(cx, discriminant_type(cases)), max_case_alignment(cx, cases)) def discriminant_type(cases): n = len(cases) @@ -2044,11 +2063,11 @@ def discriminant_type(cases): case 2: return U16Type() case 3: return U32Type() -def max_case_alignment(cases, ptr_type): +def max_case_alignment(cx, cases): a = 1 for c in cases: if c.t is not None: - a = max(a, alignment(c.t, ptr_type)) + a = max(a, alignment(cx, c.t)) return a ``` @@ -2063,6 +2082,18 @@ def alignment_flags(labels): return 4 ``` +As an optimization in `store_borrow` and `lower_flat_borrow` (defined below), +when a `(borrow $R)` handle is lowered into the component that defined `$R`, the +resource's representation value is passed instead of the `i32` handle index and +thus alignment depends on the resource's `(rep $t)`. +```python +def alignment_borrow(cx, t): + if cx.inst is t.rt.impl and cx.direction == 'lower': + return ptr_size(t.rt.rep) + else: + return 4 +``` + ## Element Size @@ -2074,10 +2105,10 @@ maps well to languages which represent `list`s as random-access arrays. Empty types, such as records with no fields, are not permitted, to avoid complications in source languages. To prevent integer overflow in obscure corner cases, component validation rules require that for every value type `t` defined -by a component, `elem_size(t, 'i64')` is less than 228 (the same -upper bound as `MAX_LIST_BYTE_LENGTH`). +by a component and every possible `cx`, `elem_size(cx, t)` is less than +228 (the same upper bound as `MAX_LIST_BYTE_LENGTH`). ```python -def elem_size(t, ptr_type): +def elem_size(cx, t): match despecialize(t): case BoolType() : return 1 case S8Type() | U8Type() : return 1 @@ -2087,40 +2118,42 @@ def elem_size(t, ptr_type): case F32Type() : return 4 case F64Type() : return 8 case CharType() : return 4 - case StringType() : return 2 * ptr_size(ptr_type) + case StringType() : return 2 * cx.opts.memory.ptr_size() case ErrorContextType() : return 4 - case ListType(t, l) : return elem_size_list(t, l, ptr_type) - case RecordType(fields) : return elem_size_record(fields, ptr_type) - case VariantType(cases) : return elem_size_variant(cases, ptr_type) + case ListType(t, l) : return elem_size_list(cx, t, l) + case RecordType(fields) : return elem_size_record(cx, fields) + case VariantType(cases) : return elem_size_variant(cx, cases) case FlagsType(labels) : return elem_size_flags(labels) - case OwnType() | BorrowType() : return 4 + case OwnType() : return 4 + case BorrowType() : return elem_size_borrow(cx, t) case StreamType() | FutureType() : return 4 -def elem_size_list(elem_type, maybe_length, ptr_type): +def elem_size_list(cx, elem_type, maybe_length): if maybe_length is not None: - return maybe_length * elem_size(elem_type, ptr_type) - return 2 * ptr_size(ptr_type) + return maybe_length * elem_size(cx, elem_type) + else: + return 2 * cx.opts.memory.ptr_size() -def elem_size_record(fields, ptr_type): +def elem_size_record(cx, fields): s = 0 for f in fields: - s = align_to(s, alignment(f.t, ptr_type)) - s += elem_size(f.t, ptr_type) + s = align_to(s, alignment(cx, f.t)) + s += elem_size(cx, f.t) assert(s > 0) - return align_to(s, alignment_record(fields, ptr_type)) + return align_to(s, alignment_record(cx, fields)) def align_to(ptr, alignment): return math.ceil(ptr / alignment) * alignment -def elem_size_variant(cases, ptr_type): - s = elem_size(discriminant_type(cases), ptr_type) - s = align_to(s, max_case_alignment(cases, ptr_type)) +def elem_size_variant(cx, cases): + s = elem_size(cx, discriminant_type(cases)) + s = align_to(s, max_case_alignment(cx, cases)) cs = 0 for c in cases: if c.t is not None: - cs = max(cs, elem_size(c.t, ptr_type)) + cs = max(cs, elem_size(cx, c.t)) s += cs - return align_to(s, alignment_variant(cases, ptr_type)) + return align_to(s, alignment_variant(cx, cases)) def elem_size_flags(labels): n = len(labels) @@ -2128,6 +2161,12 @@ def elem_size_flags(labels): if n <= 8: return 1 if n <= 16: return 2 return 4 + +def elem_size_borrow(cx, t): + if cx.inst is t.rt.impl and cx.direction == 'lower': + return ptr_size(t.rt.rep) + else: + return 4 ``` ## Loading @@ -2138,8 +2177,8 @@ as a Python value. Presenting the definition of `load` piecewise, we start with the top-level case analysis: ```python def load(cx, ptr, t): - assert(ptr == align_to(ptr, alignment(t, cx.opts.memory.ptr_type()))) - assert(ptr + elem_size(t, cx.opts.memory.ptr_type()) <= len(cx.opts.memory)) + assert(ptr == align_to(ptr, alignment(cx, t))) + assert(ptr + elem_size(cx, t) <= len(cx.opts.memory)) match despecialize(t): case BoolType() : return convert_int_to_bool(load_int(cx, ptr, 1)) case U8Type() : return load_int(cx, ptr, 1) @@ -2251,19 +2290,20 @@ from [UTF-8] to [UTF-16]). String = tuple[str, str, int] def load_string(cx, ptr) -> String: - begin = load_int(cx, ptr, cx.opts.memory.ptr_size()) - tagged_code_units = load_int(cx, ptr + cx.opts.memory.ptr_size(), cx.opts.memory.ptr_size()) + ptr_size = cx.opts.memory.ptr_size() + begin = load_int(cx, ptr, ptr_size) + tagged_code_units = load_int(cx, ptr + ptr_size, ptr_size) return load_string_from_range(cx, begin, tagged_code_units) -def utf16_tag(ptr_type): - return 1 << (ptr_size(ptr_type) * 8 - 1) +def utf16_tag(cx): + return 1 << (cx.opts.memory.ptr_size() * 8 - 1) REALLOC_I32_MAX = 2**32 - 1 MAX_STRING_BYTE_LENGTH = (1 << 28) - 1 assert(REALLOC_I32_MAX > 2 * MAX_STRING_BYTE_LENGTH) def load_string_from_range(cx, ptr, tagged_code_units) -> String: - tag = utf16_tag(cx.opts.memory.ptr_type()) + tag = utf16_tag(cx) match cx.opts.string_encoding: case 'utf8': alignment = 1 @@ -2316,28 +2356,29 @@ assert(REALLOC_I32_MAX > 2 * MAX_LIST_BYTE_LENGTH) def load_list(cx, ptr, elem_type, maybe_length): if maybe_length is not None: return load_list_from_valid_range(cx, ptr, maybe_length, elem_type) - begin = load_int(cx, ptr, cx.opts.memory.ptr_size()) - length = load_int(cx, ptr + cx.opts.memory.ptr_size(), cx.opts.memory.ptr_size()) + ptr_size = cx.opts.memory.ptr_size() + begin = load_int(cx, ptr, ptr_size) + length = load_int(cx, ptr + ptr_size, ptr_size) return load_list_from_range(cx, begin, length, elem_type) def load_list_from_range(cx, ptr, length, elem_type): - trap_if(length * elem_size(elem_type, cx.opts.memory.ptr_type()) > MAX_LIST_BYTE_LENGTH) - trap_if(ptr != align_to(ptr, alignment(elem_type, cx.opts.memory.ptr_type()))) - trap_if(ptr + length * elem_size(elem_type, cx.opts.memory.ptr_type()) > len(cx.opts.memory)) + trap_if(length * elem_size(cx, elem_type) > MAX_LIST_BYTE_LENGTH) + trap_if(ptr != align_to(ptr, alignment(cx, elem_type))) + trap_if(ptr + length * elem_size(cx, elem_type) > len(cx.opts.memory)) return load_list_from_valid_range(cx, ptr, length, elem_type) def load_list_from_valid_range(cx, ptr, length, elem_type): a = [] for i in range(length): - a.append(load(cx, ptr + i * elem_size(elem_type, cx.opts.memory.ptr_type()), elem_type)) + a.append(load(cx, ptr + i * elem_size(cx, elem_type), elem_type)) return a def load_record(cx, ptr, fields): record = {} for field in fields: - ptr = align_to(ptr, alignment(field.t, cx.opts.memory.ptr_type())) + ptr = align_to(ptr, alignment(cx, field.t)) record[field.label] = load(cx, ptr, field.t) - ptr += elem_size(field.t, cx.opts.memory.ptr_type()) + ptr += elem_size(cx, field.t) return record ``` As a technical detail: the `align_to` in the loop in `load_record` is @@ -2351,12 +2392,12 @@ implementation can build the appropriate index tables at compile-time so that variant-passing is always O(1) and not involving string operations. ```python def load_variant(cx, ptr, cases): - disc_size = elem_size(discriminant_type(cases), cx.opts.memory.ptr_type()) + disc_size = elem_size(cx, discriminant_type(cases)) case_index = load_int(cx, ptr, disc_size) ptr += disc_size trap_if(case_index >= len(cases)) c = cases[case_index] - ptr = align_to(ptr, max_case_alignment(cases, cx.opts.memory.ptr_type())) + ptr = align_to(ptr, max_case_alignment(cx, cases)) if c.t is None: return { c.label: None } return { c.label: load(cx, ptr, c.t) } @@ -2456,8 +2497,8 @@ The `store` function defines how to write a value `v` of a given value type `store` piecewise, we start with the top-level case analysis: ```python def store(cx, v, t, ptr): - assert(ptr == align_to(ptr, alignment(t, cx.opts.memory.ptr_type()))) - assert(ptr + elem_size(t, cx.opts.memory.ptr_type()) <= len(cx.opts.memory)) + assert(ptr == align_to(ptr, alignment(cx, t))) + assert(ptr + elem_size(cx, t) <= len(cx.opts.memory)) match despecialize(t): case BoolType() : store_int(cx, int(bool(v)), ptr, 1) case U8Type() : store_int(cx, v, ptr, 1) @@ -2478,7 +2519,7 @@ def store(cx, v, t, ptr): case VariantType(cases) : store_variant(cx, v, ptr, cases) case FlagsType(labels) : store_flags(cx, v, ptr, labels) case OwnType() : store_int(cx, lower_own(cx, v, t), ptr, 4) - case BorrowType() : store_int(cx, lower_borrow(cx, v, t), ptr, 4) + case BorrowType() : store_borrow(cx, v, ptr, t) case StreamType(t) : store_int(cx, lower_stream(cx, v, t), ptr, 4) case FutureType(t) : store_int(cx, lower_future(cx, v, t), ptr, 4) ``` @@ -2574,12 +2615,13 @@ combinations, subdividing the `latin1+utf16` encoding into either `latin1` or ```python def store_string(cx, v: String, ptr): begin, tagged_code_units = store_string_into_range(cx, v) - store_int(cx, begin, ptr, cx.opts.memory.ptr_size()) - store_int(cx, tagged_code_units, ptr + cx.opts.memory.ptr_size(), cx.opts.memory.ptr_size()) + ptr_size = cx.opts.memory.ptr_size() + store_int(cx, begin, ptr, ptr_size) + store_int(cx, tagged_code_units, ptr + ptr_size, ptr_size) def store_string_into_range(cx, v: String): src, src_encoding, src_tagged_code_units = v - tag = utf16_tag(cx.opts.memory.ptr_type()) + tag = utf16_tag(cx) if src_encoding == 'latin1+utf16': if bool(src_tagged_code_units & tag): @@ -2701,7 +2743,7 @@ def store_string_to_latin1_or_utf16(cx, src, src_code_units): cx.opts.memory[ptr+2*dst_byte_length : ptr+len(encoded)] = encoded[2*dst_byte_length : ] if worst_case_size > len(encoded): ptr = cx.reallocate(ptr, worst_case_size, 2, len(encoded)) - tagged_code_units = int(len(encoded) / 2) | utf16_tag(cx.opts.memory.ptr_type()) + tagged_code_units = int(len(encoded) / 2) | utf16_tag(cx) return (ptr, tagged_code_units) if dst_byte_length < src_code_units: ptr = cx.reallocate(ptr, src_code_units, 2, dst_byte_length) @@ -2726,7 +2768,7 @@ def store_probably_utf16_to_latin1_or_utf16(cx, src, src_code_units): encoded = src.encode('utf-16-le') cx.opts.memory[ptr : ptr+len(encoded)] = encoded if any(ord(c) >= (1 << 8) for c in src): - tagged_code_units = int(len(encoded) / 2) | utf16_tag(cx.opts.memory.ptr_type()) + tagged_code_units = int(len(encoded) / 2) | utf16_tag(cx) return (ptr, tagged_code_units) latin1_size = int(len(encoded) / 2) for i in range(latin1_size): @@ -2755,25 +2797,26 @@ def store_list(cx, v, ptr, elem_type, maybe_length): store_list_into_valid_range(cx, v, ptr, elem_type) return begin, length = store_list_into_range(cx, v, elem_type) - store_int(cx, begin, ptr, cx.opts.memory.ptr_size()) - store_int(cx, length, ptr + cx.opts.memory.ptr_size(), cx.opts.memory.ptr_size()) + ptr_size = cx.opts.memory.ptr_size() + store_int(cx, begin, ptr, ptr_size) + store_int(cx, length, ptr + ptr_size, ptr_size) def store_list_into_range(cx, v, elem_type): - byte_length = len(v) * elem_size(elem_type, cx.opts.memory.ptr_type()) + byte_length = len(v) * elem_size(cx, elem_type) assert(byte_length <= REALLOC_I32_MAX) - ptr = cx.allocate(alignment(elem_type, cx.opts.memory.ptr_type()), byte_length) + ptr = cx.allocate(alignment(cx, elem_type), byte_length) store_list_into_valid_range(cx, v, ptr, elem_type) return (ptr, len(v)) def store_list_into_valid_range(cx, v, ptr, elem_type): for i,e in enumerate(v): - store(cx, e, elem_type, ptr + i * elem_size(elem_type, cx.opts.memory.ptr_type())) + store(cx, e, elem_type, ptr + i * elem_size(cx, elem_type)) def store_record(cx, v, ptr, fields): for f in fields: - ptr = align_to(ptr, alignment(f.t, cx.opts.memory.ptr_type())) + ptr = align_to(ptr, alignment(cx, f.t)) store(cx, v[f.label], f.t, ptr) - ptr += elem_size(f.t, cx.opts.memory.ptr_type()) + ptr += elem_size(cx, f.t) ``` Variant values are represented as Python dictionaries containing exactly one @@ -2786,10 +2829,10 @@ indices. ```python def store_variant(cx, v, ptr, cases): case_index, case_value = match_case(v, cases) - disc_size = elem_size(discriminant_type(cases), cx.opts.memory.ptr_type()) + disc_size = elem_size(cx, discriminant_type(cases)) store_int(cx, case_index, ptr, disc_size) ptr += disc_size - ptr = align_to(ptr, max_case_alignment(cases, cx.opts.memory.ptr_type())) + ptr = align_to(ptr, max_case_alignment(cx, cases)) c = cases[case_index] if c.t is not None: store(cx, case_value, c.t, ptr) @@ -2830,19 +2873,24 @@ def lower_own(cx, rep, t): h = ResourceHandle(t.rt, rep, own = True) return cx.inst.handles.add(h) -def lower_borrow(cx, rep, t): - assert(isinstance(cx.borrow_scope, Task)) +def store_borrow(cx, rep, ptr, t): if cx.inst is t.rt.impl: - return rep + store_int(cx, rep, ptr, ptr_size(t.rt.rep)) + else: + store_int(cx, add_borrow(cx, rep, t), ptr, 4) + +def add_borrow(cx, rep, t): + assert(isinstance(cx.borrow_scope, Task)) h = ResourceHandle(t.rt, rep, own = False, borrow_scope = cx.borrow_scope) h.borrow_scope.num_borrows += 1 return cx.inst.handles.add(h) ``` -The special case in `lower_borrow` is an optimization, recognizing that, when +The special case in `store_borrow` is an optimization, recognizing that, when a borrowed handle is passed to the component that implemented the resource type, the only thing the borrowed handle is good for is calling `resource.rep`, so lowering might as well avoid the overhead of creating an -intermediate borrow handle. +intermediate borrow handle and instead pass the `rep` directly, using the size +of the resource's representation type. Lowering a `stream` or `future` simply adds the given readable end to the current component instance's `handles` table, establishing unique ownership of @@ -2891,45 +2939,47 @@ have already allocated space for the return value (e.g., efficiently on the stack), passing in an `i32` pointer as an parameter instead of returning an `i32` as a return value. -Given all this, the top-level definition of `flatten_functype` is: +Given all this, the top-level definition of `flatten_functype` is as follows, +where `cx.direction` indicates whether `ft` is being lifted (by `canon lift`) or +lowered (by `canon lower`): ```python MAX_FLAT_PARAMS = 16 MAX_FLAT_ASYNC_PARAMS = 4 MAX_FLAT_RESULTS = 1 -def flatten_functype(opts, ft, context): - flat_params = flatten_types(ft.param_types(), opts) - flat_results = flatten_types(ft.result_type(), opts) - if not opts.async_: +def flatten_functype(cx, ft): + flat_params = flatten_types(cx.reverse_direction(), ft.param_types()) + flat_results = flatten_types(cx, ft.result_type()) + if not cx.opts.async_: if len(flat_params) > MAX_FLAT_PARAMS: - flat_params = [opts.memory.ptr_type()] + flat_params = [cx.opts.memory.ptr_type()] if len(flat_results) > MAX_FLAT_RESULTS: - match context: + match cx.direction: case 'lift': - flat_results = [opts.memory.ptr_type()] + flat_results = [cx.opts.memory.ptr_type()] case 'lower': - flat_params += [opts.memory.ptr_type()] + flat_params += [cx.opts.memory.ptr_type()] flat_results = [] return CoreFuncType(flat_params, flat_results) else: - match context: + match cx.direction: case 'lift': if len(flat_params) > MAX_FLAT_PARAMS: - flat_params = [opts.memory.ptr_type()] - if opts.callback: + flat_params = [cx.opts.memory.ptr_type()] + if cx.opts.callback: flat_results = ['i32'] else: flat_results = [] case 'lower': if len(flat_params) > MAX_FLAT_ASYNC_PARAMS: - flat_params = [opts.memory.ptr_type()] + flat_params = [cx.opts.memory.ptr_type()] if len(flat_results) > 0: - flat_params += [opts.memory.ptr_type()] + flat_params += [cx.opts.memory.ptr_type()] flat_results = ['i32'] return CoreFuncType(flat_params, flat_results) -def flatten_types(ts, opts): - return [ft for t in ts for ft in flatten_type(t, opts)] +def flatten_types(cx, ts): + return [ft for t in ts for ft in flatten_type(cx, t)] ``` As shown here, the core signatures of `async`-lowered functions use a lower limit on the maximum number of parameters (4) and results (0) passed as scalars @@ -2938,7 +2988,7 @@ before falling back to passing through memory. Presenting the definition of `flatten_type` piecewise, we start with the top-level case analysis: ```python -def flatten_type(t, opts): +def flatten_type(cx, t): match despecialize(t): case BoolType() : return ['i32'] case U8Type() | U16Type() | U32Type() : return ['i32'] @@ -2947,31 +2997,32 @@ def flatten_type(t, opts): case F32Type() : return ['f32'] case F64Type() : return ['f64'] case CharType() : return ['i32'] - case StringType() : return [opts.memory.ptr_type(), opts.memory.ptr_type()] + case StringType() : return 2 * [cx.opts.memory.ptr_type()] case ErrorContextType() : return ['i32'] - case ListType(t, l) : return flatten_list(t, l, opts) - case RecordType(fields) : return flatten_record(fields, opts) - case VariantType(cases) : return flatten_variant(cases, opts) + case ListType(t, l) : return flatten_list(cx, t, l) + case RecordType(fields) : return flatten_record(cx, fields) + case VariantType(cases) : return flatten_variant(cx, cases) case FlagsType(labels) : return ['i32'] - case OwnType() | BorrowType() : return ['i32'] + case OwnType() : return ['i32'] + case BorrowType() : return flatten_borrow(cx, t) case StreamType() | FutureType() : return ['i32'] ``` List flattening of a fixed-length list uses the same flattening as a tuple (via `flatten_record` below). ```python -def flatten_list(elem_type, maybe_length, opts): +def flatten_list(cx, elem_type, maybe_length): if maybe_length is not None: - return flatten_type(elem_type, opts) * maybe_length - return [opts.memory.ptr_type(), opts.memory.ptr_type()] + return flatten_type(cx, elem_type) * maybe_length + return 2 * [cx.opts.memory.ptr_type()] ``` Record flattening simply flattens each field in sequence. ```python -def flatten_record(fields, opts): +def flatten_record(cx, fields): flat = [] for f in fields: - flat += flatten_type(f.t, opts) + flat += flatten_type(cx, f.t) return flat ``` @@ -2984,16 +3035,16 @@ case, all flattened variants are passed with the same static set of core types, which may involve, e.g., reinterpreting an `f32` as an `i32` or zero-extending an `i32` into an `i64`. ```python -def flatten_variant(cases, opts): +def flatten_variant(cx, cases): flat = [] for c in cases: if c.t is not None: - for i,ft in enumerate(flatten_type(c.t, opts)): + for i,ft in enumerate(flatten_type(cx, c.t)): if i < len(flat): flat[i] = join(flat[i], ft) else: flat.append(ft) - return flatten_type(discriminant_type(cases), opts) + flat + return flatten_type(cx, discriminant_type(cases)) + flat def join(a, b): if a == b: return a @@ -3001,6 +3052,16 @@ def join(a, b): return 'i64' ``` +Lastly, matching `store_borrow` above, a `borrow` lowered into the component +that defined its resource type is passed as its `rep`: +```python +def flatten_borrow(cx, t): + if cx.inst is t.rt.impl and cx.direction == 'lower': + return [t.rt.rep] + else: + return ['i32'] +``` + ## Flat Lifting Values are lifted by iterating over a list of parameter or result Core @@ -3118,7 +3179,7 @@ reinterprets between the different types appropriately and also wraps `i64` values to 32-bit when needed: ```python def lift_flat_variant(cx, vi, cases): - flat_types = flatten_variant(cases, cx.opts) + flat_types = flatten_variant(cx, cases) assert(flat_types.pop(0) == 'i32') case_index = vi.next('i32') trap_if(case_index >= len(cases)) @@ -3182,7 +3243,7 @@ def lower_flat(cx, v, t): case VariantType(cases) : return lower_flat_variant(cx, v, cases) case FlagsType(labels) : return lower_flat_flags(v, labels) case OwnType() : return [lower_own(cx, v, t)] - case BorrowType() : return [lower_borrow(cx, v, t)] + case BorrowType() : return lower_flat_borrow(cx, v, t) case StreamType(t) : return [lower_stream(cx, v, t)] case FutureType(t) : return [lower_future(cx, v, t)] ``` @@ -3234,14 +3295,14 @@ manually coercing the otherwise-incompatible type pairings allowed by `join`: ```python def lower_flat_variant(cx, v, cases): case_index, case_value = match_case(v, cases) - flat_types = flatten_variant(cases, cx.opts) + flat_types = flatten_variant(cx, cases) assert(flat_types.pop(0) == 'i32') c = cases[case_index] if c.t is None: payload = [] else: payload = lower_flat(cx, case_value, c.t) - for i,(fv,have) in enumerate(zip(payload, flatten_type(c.t, cx.opts))): + for i,(fv,have) in enumerate(zip(payload, flatten_type(cx, c.t))): want = flat_types.pop(0) match (have, want): case ('f32', 'i32') : payload[i] = encode_float_as_i32(fv) @@ -3254,13 +3315,23 @@ def lower_flat_variant(cx, v, cases): return [case_index] + payload ``` -Finally, flags are lowered by packing the flags into an `i32` bitvector. +Flags are lowered by packing the flags into an `i32` bitvector. ```python def lower_flat_flags(v, labels): assert(0 < len(labels) <= 32) return [pack_flags_into_int(v, labels)] ``` +Finally, `borrow` handles are lowered with the same optimization as +`store_borrow` above: +```python +def lower_flat_borrow(cx, rep, t): + if cx.inst is t.rt.impl: + return [rep] + else: + return [add_borrow(cx, rep, t)] +``` + ## Lifting and Lowering Values The `lift_flat_values` function defines how to lift a list of core @@ -3268,12 +3339,12 @@ parameters or results (given by the `CoreValueIter` `vi`) into a tuple of component-level values with types `ts`. ```python def lift_flat_values(cx, max_flat, vi, ts): - flat_types = flatten_types(ts, cx.opts) + flat_types = flatten_types(cx, ts) if len(flat_types) > max_flat: ptr = vi.next(cx.opts.memory.ptr_type()) tuple_type = TupleType(ts) - trap_if(ptr != align_to(ptr, alignment(tuple_type, cx.opts.memory.ptr_type()))) - trap_if(ptr + elem_size(tuple_type, cx.opts.memory.ptr_type()) > len(cx.opts.memory)) + trap_if(ptr != align_to(ptr, alignment(cx, tuple_type))) + trap_if(ptr + elem_size(cx, tuple_type) > len(cx.opts.memory)) return list(load(cx, ptr, tuple_type).values()) else: return [ lift_flat(cx, vi, t) for t in ts ] @@ -3287,18 +3358,17 @@ storage with `realloc` or accepting a caller-allocated buffer as an out-param: ```python def lower_flat_values(cx, max_flat, vs, ts, out_param = None): - flat_types = flatten_types(ts, cx.opts) + flat_types = flatten_types(cx, ts) if len(flat_types) > max_flat: tuple_type = TupleType(ts) tuple_value = {str(i): v for i,v in enumerate(vs)} if out_param is None: - ptr_type = cx.opts.memory.ptr_type() - ptr = cx.allocate(alignment(tuple_type, ptr_type), elem_size(tuple_type, ptr_type)) + ptr = cx.allocate(alignment(cx, tuple_type), elem_size(cx, tuple_type)) flat_vals = [ptr] else: ptr = out_param.next(cx.opts.memory.ptr_type()) - trap_if(ptr != align_to(ptr, alignment(tuple_type, cx.opts.memory.ptr_type()))) - trap_if(ptr + elem_size(tuple_type, cx.opts.memory.ptr_type()) > len(cx.opts.memory)) + trap_if(ptr != align_to(ptr, alignment(cx, tuple_type))) + trap_if(ptr + elem_size(cx, tuple_type) > len(cx.opts.memory)) flat_vals = [] store(cx, tuple_value, tuple_type, ptr) else: @@ -3360,11 +3430,12 @@ For a canonical definition: ``` In addition to [general validation of `$opts`](#canonopt-validation) the additional -validation is performed: +validation is performed with `cx = LiftLowerContext('lift', $opts, inst)` where +`inst` is the component instance containing this lifting definition: -* `$callee` must have type `flatten_functype($opts, $ft, 'lift')` +* `$callee` must have type `flatten_functype(cx, $ft)` * `$f` is given type `$ft` -* if a `post-return` is present, it has type `(func (param flatten_functype($opts, $ft, 'lift').results))` +* if a `post-return` is present, it has type `(func (param flatten_functype(cx, $ft).results))` * requires options based on [`lift(param)`](#canonopt-validation) for all parameters in `ft` * requires options based on [`lower(result)`](#canonopt-validation) if `ft` has a result * if `len(flatten_types(ft.param_types())) > MAX_FLAT_PARAMS`, `realloc` is required @@ -3401,10 +3472,10 @@ to the callee. The `CANCELLED_BEFORE_{STARTED,RETURNED}` return codes from `subtask.cancel` are thus necessary for the caller to distinguish whether their arguments were lowered or not. ```python - cx = LiftLowerContext(opts, inst, task) + cx = LiftLowerContext('lift', opts, inst, task) + flat_ft = flatten_functype(cx, ft) args = task.start() - flat_args = lower_flat_values(cx, MAX_FLAT_PARAMS, args, ft.param_types()) - flat_ft = flatten_functype(opts, ft, 'lift') + flat_args = lower_flat_values(cx.reverse_direction(), MAX_FLAT_PARAMS, args, ft.param_types()) assert(types_match_values(flat_ft.params, flat_args)) ``` @@ -3597,19 +3668,21 @@ For a canonical definition: ``` In addition to [general validation of `$opts`](#canonopt-validation), additional -validation is performed where `$callee` has type `$ft`: +validation is performed where `$callee` has type `ft`, +`cx = LiftLowerContext('lower', $opts, inst)`, and `inst` is the component +instance containing this lowering definition: -* `$f` is given type `flatten_functype($opts, $ft, 'lower')` +* `$f` is given type `flatten_functype(cx, ft)` * requires options [based on `lower(param)`](#canonopt-validation) for all parameters in `ft` * requires options [based on `lift(result)`](#canonopt-validation) if `ft` has a result * if `len(flatten_types(ft.param_types())) > max_flat_params`, `memory` is required * if `len(flatten_types(ft.result_type())) > max_flat_results`, `memory` is required When instantiating a component instance, the runtime calls `Store.lower` (defined -above) to capture the `$callee`, `$ft` and `$opts` immediates of `canon lower` -along with the component instance being instantiated. These are then passed into -`canon_lower` every time the generated `CoreFuncInst` is called, along with the -runtime Core WebAssembly arguments. +above) to capture the `$callee` and `$opts` immediates of `canon lower` along +with `ft` (the type of `$callee`) and the component instance being instantiated. +These are then passed into `canon_lower` every time the generated `CoreFuncInst` +is called, along with the runtime Core WebAssembly arguments. Based on this, `canon_lower` is defined in chunks as follows. First, like most Canonical ABI functions callable from Core WebAssembly, lowered imports may not @@ -3620,12 +3693,15 @@ def canon_lower(callee, ft, opts, flat_args: list[CoreValType]) -> list[CoreValT trap_if(not thread.task.inst.may_leave) ``` -The component-level function type maps to a Core WebAssembly function type using +After creating the `Subtask` and `LiftLowerContext` for the call, the +component-level function type maps to a Core WebAssembly function type using different flattening constants based on whether the sync or async ABI is used. The values of `max_flat_{params,results}` mirror the flattening logic in `flatten_functype` above: ```python - flat_ft = flatten_functype(opts, ft, 'lower') + subtask = Subtask() + cx = LiftLowerContext('lower', opts, thread.task.inst, subtask) + flat_ft = flatten_functype(cx, ft) assert(types_match_values(flat_ft.params, flat_args)) flat_args = CoreValueIter(flat_args) @@ -3644,15 +3720,13 @@ no-op but is switched to `on_progress` (defined below) in the `async` case if `callee` [blocks], so that progress updates are asynchronously delivered to the caller. ```python - subtask = Subtask() - cx = LiftLowerContext(opts, thread.task.inst, subtask) maybe_on_progress = lambda:() def on_start(): maybe_on_progress() assert(subtask.state == Subtask.State.STARTING) subtask.state = Subtask.State.STARTED - return lift_flat_values(cx, max_flat_params, flat_args, ft.param_types()) + return lift_flat_values(cx.reverse_direction(), max_flat_params, flat_args, ft.param_types()) def on_resolve(result): maybe_on_progress() @@ -3789,7 +3863,8 @@ def canon_resource_drop(rt, i): if h.own: assert(h.borrow_scope is None) opts = CanonicalOptions(async_ = False) - ft = FuncType([U32Type()], [], async_ = False) + rep_type = U32Type() if rt.rep == 'i32' else U64Type() + ft = FuncType([rep_type], [], async_ = False) dtor = rt.dtor or (lambda rep: []) callee = inst.store.lift(dtor, ft, opts, rt.impl) caller = inst.store.lower(callee, ft, opts, inst) @@ -3798,7 +3873,7 @@ def canon_resource_drop(rt, i): h.borrow_scope.num_borrows -= 1 return [] ``` -The call to a resource's destructor passes the `i32` representation value that +The call to a resource's destructor passes the representation value that was previously supplied to `resource.new`. The call works like a normal non-`async` cross-component call which means that destructors may not block. However, the destructor may spawn an explicit thread that blocks as long as @@ -3926,7 +4001,7 @@ For a canonical definition: In addition to [general validation of `$opts`](#canonopt-validation) validation specifies: -* `$f` is given type `flatten_functype($opts, (func (param $t)?), 'lower')` +* `$f` is given type `flatten_functype(('lower', $opts), (func (param $t)?))` * `$t` must not transitively contain `borrow` * `$opts` may only contain `memory` and `string-encoding` * [`lift($f.result)` above](#canonopt-validation) defines required options @@ -3940,7 +4015,7 @@ def canon_task_return(result_type, opts: LiftOptions, flat_args): trap_if(not task.opts.async_) trap_if(result_type != task.ft.result) trap_if(not LiftOptions.equal(opts, task.opts)) - cx = LiftLowerContext(opts, task.inst, task) + cx = LiftLowerContext('lift', opts, task.inst, task) result = lift_flat_values(cx, MAX_FLAT_PARAMS, CoreValueIter(flat_args), task.ft.result_type()) task.return_(result) return [] @@ -4042,7 +4117,7 @@ def canon_waitable_set_wait(mem, si, ptr): def unpack_event(mem, inst, ptr, e: EventTuple): event, p1, p2 = e - cx = LiftLowerContext(LiftLowerOptions(memory = mem), inst) + cx = LiftLowerContext('lower', LiftLowerOptions(memory = mem), inst) trap_if(ptr != align_to(ptr, 4)) trap_if(ptr + 8 > len(cx.opts.memory)) store(cx, p1, U32Type(), ptr) @@ -4303,18 +4378,18 @@ The implementations of these 4 built-ins all funnel down to a single parameterized `copy` function: ```python def canon_stream_read(stream_t, opts, i, ptr, length): - return copy(ReadableStreamEnd, WritableGuestBuffer, stream_t, opts, i, ptr, length) + return copy(ReadableStreamEnd, WritableGuestBuffer, 'lower', stream_t, opts, i, ptr, length) def canon_stream_write(stream_t, opts, i, ptr, length): - return copy(WritableStreamEnd, ReadableGuestBuffer, stream_t, opts, i, ptr, length) + return copy(WritableStreamEnd, ReadableGuestBuffer, 'lift', stream_t, opts, i, ptr, length) def canon_future_read(future_t, opts, i, ptr): - return copy(ReadableFutureEnd, WritableGuestBuffer, future_t, opts, i, ptr, 1) + return copy(ReadableFutureEnd, WritableGuestBuffer, 'lower', future_t, opts, i, ptr, 1) def canon_future_write(future_t, opts, i, ptr): - return copy(WritableFutureEnd, ReadableGuestBuffer, future_t, opts, i, ptr, 1) + return copy(WritableFutureEnd, ReadableGuestBuffer, 'lift', future_t, opts, i, ptr, 1) -def copy(EndT, BufferT, stream_or_future_t, opts, i, ptr, length): +def copy(EndT, BufferT, direction, stream_or_future_t, opts, i, ptr, length): thread = current_thread() trap_if(not thread.task.inst.may_leave) end = thread.task.inst.handles.get(i) @@ -4322,7 +4397,7 @@ def copy(EndT, BufferT, stream_or_future_t, opts, i, ptr, length): trap_if(end.t != stream_or_future_t.t) trap_if(end.state != End.State.IDLE) trap_if(end.in_waitable_set() and not opts.async_) - cx = LiftLowerContext(opts, thread.task.inst, borrow_scope = None) + cx = LiftLowerContext(direction, opts, thread.task.inst, borrow_scope = None) buffer = BufferT(end.t, cx, ptr, length) end.copy(buffer) if not end.has_pending_event(): @@ -4795,7 +4870,7 @@ def canon_error_context_new(opts, ptr, tagged_code_units): if DETERMINISTIC_PROFILE or random.randint(0,1): s = String(('', 'utf8', 0)) else: - cx = LiftLowerContext(opts, inst) + cx = LiftLowerContext('lift', opts, inst) s = load_string_from_range(cx, ptr, tagged_code_units) s = host_defined_transformation(s) i = inst.handles.add(ErrorContext(s)) @@ -4836,9 +4911,9 @@ def canon_error_context_debug_message(opts, i, ptr): trap_if(not inst.may_leave) errctx = inst.handles.get(i) trap_if(not isinstance(errctx, ErrorContext)) - cx = LiftLowerContext(opts, inst) - trap_if(ptr != align_to(ptr, alignment(StringType(), cx.opts.memory.ptr_type()))) - trap_if(ptr + elem_size(StringType(), cx.opts.memory.ptr_type()) > len(cx.opts.memory)) + cx = LiftLowerContext('lower', opts, inst) + trap_if(ptr != align_to(ptr, alignment(cx, StringType()))) + trap_if(ptr + elem_size(cx, StringType()) > len(cx.opts.memory)) store(cx, errctx.debug_message, StringType(), ptr) return [] ``` diff --git a/design/mvp/canonical-abi/definitions.py b/design/mvp/canonical-abi/definitions.py index 38046e5a..1c1d59bf 100644 --- a/design/mvp/canonical-abi/definitions.py +++ b/design/mvp/canonical-abi/definitions.py @@ -613,15 +613,21 @@ class CanonicalOptions(LiftLowerOptions): ## Lifting and Lowering Context class LiftLowerContext: + direction: Literal['lift', 'lower'] opts: LiftLowerOptions inst: ComponentInstance borrow_scope: Optional[Task|Subtask] - def __init__(self, opts, inst, borrow_scope = None): + def __init__(self, direction, opts, inst, borrow_scope = None): + self.direction = direction self.opts = opts self.inst = inst self.borrow_scope = borrow_scope + def reverse_direction(self): + new_direction = 'lift' if self.direction == 'lower' else 'lower' + return LiftLowerContext(new_direction, self.opts, self.inst, self.borrow_scope) + def reallocate(self, old, old_byte_length, alignment, new_byte_length): ptrt = U32Type() if self.opts.memory.ptr_type() == 'i32' else U64Type() ft = FuncType([ptrt, ptrt, ptrt, ptrt], [ptrt], async_ = False) @@ -704,10 +710,12 @@ def __init__(self, rt, rep, own, borrow_scope = None): class ResourceType(Type): impl: ComponentInstance + rep: Literal['i32', 'i64'] dtor: Optional[Callable] - def __init__(self, impl, dtor = None): + def __init__(self, impl, rep, dtor = None): self.impl = impl + self.rep = rep self.dtor = dtor ### Waitable State @@ -896,8 +904,8 @@ class GuestBuffer(Buffer): def __init__(self, t, cx, ptr, length): trap_if(length > Buffer.MAX_LENGTH) if t and length > 0: - trap_if(ptr != align_to(ptr, alignment(t, cx.opts.memory.ptr_type()))) - trap_if(ptr + length * elem_size(t, cx.opts.memory.ptr_type()) > len(cx.opts.memory)) + trap_if(ptr != align_to(ptr, alignment(cx, t))) + trap_if(ptr + length * elem_size(cx, t) > len(cx.opts.memory)) self.cx = cx self.t = t self.ptr = ptr @@ -912,7 +920,7 @@ def read(self, n): assert(n <= self.remain()) if self.t: vs = load_list_from_valid_range(self.cx, self.ptr, n, self.t) - self.ptr += n * elem_size(self.t, self.cx.opts.memory.ptr_type()) + self.ptr += n * elem_size(self.cx, self.t) else: vs = n * [()] self.progress += n @@ -923,7 +931,7 @@ def write(self, vs): assert(len(vs) <= self.remain()) if self.t: store_list_into_valid_range(self.cx, vs, self.ptr, self.t) - self.ptr += len(vs) * elem_size(self.t, self.cx.opts.memory.ptr_type()) + self.ptr += len(vs) * elem_size(self.cx, self.t) else: assert(all(v == () for v in vs)) self.progress += len(vs) @@ -1143,7 +1151,7 @@ def none_or_number_type(t): ## Alignment -def alignment(t, ptr_type): +def alignment(cx, t): match despecialize(t): case BoolType() : return 1 case S8Type() | U8Type() : return 1 @@ -1153,28 +1161,30 @@ def alignment(t, ptr_type): case F32Type() : return 4 case F64Type() : return 8 case CharType() : return 4 - case StringType() : return ptr_size(ptr_type) + case StringType() : return cx.opts.memory.ptr_size() case ErrorContextType() : return 4 - case ListType(t, l) : return alignment_list(t, l, ptr_type) - case RecordType(fields) : return alignment_record(fields, ptr_type) - case VariantType(cases) : return alignment_variant(cases, ptr_type) + case ListType(t, l) : return alignment_list(cx, t, l) + case RecordType(fields) : return alignment_record(cx, fields) + case VariantType(cases) : return alignment_variant(cx, cases) case FlagsType(labels) : return alignment_flags(labels) - case OwnType() | BorrowType() : return 4 + case OwnType() : return 4 + case BorrowType() : return alignment_borrow(cx, t) case StreamType() | FutureType() : return 4 -def alignment_list(elem_type, maybe_length, ptr_type): +def alignment_list(cx, elem_type, maybe_length): if maybe_length is not None: - return alignment(elem_type, ptr_type) - return ptr_size(ptr_type) + return alignment(cx, elem_type) + else: + return cx.opts.memory.ptr_size() -def alignment_record(fields, ptr_type): +def alignment_record(cx, fields): a = 1 for f in fields: - a = max(a, alignment(f.t, ptr_type)) + a = max(a, alignment(cx, f.t)) return a -def alignment_variant(cases, ptr_type): - return max(alignment(discriminant_type(cases), ptr_type), max_case_alignment(cases, ptr_type)) +def alignment_variant(cx, cases): + return max(alignment(cx, discriminant_type(cases)), max_case_alignment(cx, cases)) def discriminant_type(cases): n = len(cases) @@ -1185,11 +1195,11 @@ def discriminant_type(cases): case 2: return U16Type() case 3: return U32Type() -def max_case_alignment(cases, ptr_type): +def max_case_alignment(cx, cases): a = 1 for c in cases: if c.t is not None: - a = max(a, alignment(c.t, ptr_type)) + a = max(a, alignment(cx, c.t)) return a def alignment_flags(labels): @@ -1199,9 +1209,15 @@ def alignment_flags(labels): if n <= 16: return 2 return 4 +def alignment_borrow(cx, t): + if cx.inst is t.rt.impl and cx.direction == 'lower': + return ptr_size(t.rt.rep) + else: + return 4 + ## Element Size -def elem_size(t, ptr_type): +def elem_size(cx, t): match despecialize(t): case BoolType() : return 1 case S8Type() | U8Type() : return 1 @@ -1211,40 +1227,42 @@ def elem_size(t, ptr_type): case F32Type() : return 4 case F64Type() : return 8 case CharType() : return 4 - case StringType() : return 2 * ptr_size(ptr_type) + case StringType() : return 2 * cx.opts.memory.ptr_size() case ErrorContextType() : return 4 - case ListType(t, l) : return elem_size_list(t, l, ptr_type) - case RecordType(fields) : return elem_size_record(fields, ptr_type) - case VariantType(cases) : return elem_size_variant(cases, ptr_type) + case ListType(t, l) : return elem_size_list(cx, t, l) + case RecordType(fields) : return elem_size_record(cx, fields) + case VariantType(cases) : return elem_size_variant(cx, cases) case FlagsType(labels) : return elem_size_flags(labels) - case OwnType() | BorrowType() : return 4 + case OwnType() : return 4 + case BorrowType() : return elem_size_borrow(cx, t) case StreamType() | FutureType() : return 4 -def elem_size_list(elem_type, maybe_length, ptr_type): +def elem_size_list(cx, elem_type, maybe_length): if maybe_length is not None: - return maybe_length * elem_size(elem_type, ptr_type) - return 2 * ptr_size(ptr_type) + return maybe_length * elem_size(cx, elem_type) + else: + return 2 * cx.opts.memory.ptr_size() -def elem_size_record(fields, ptr_type): +def elem_size_record(cx, fields): s = 0 for f in fields: - s = align_to(s, alignment(f.t, ptr_type)) - s += elem_size(f.t, ptr_type) + s = align_to(s, alignment(cx, f.t)) + s += elem_size(cx, f.t) assert(s > 0) - return align_to(s, alignment_record(fields, ptr_type)) + return align_to(s, alignment_record(cx, fields)) def align_to(ptr, alignment): return math.ceil(ptr / alignment) * alignment -def elem_size_variant(cases, ptr_type): - s = elem_size(discriminant_type(cases), ptr_type) - s = align_to(s, max_case_alignment(cases, ptr_type)) +def elem_size_variant(cx, cases): + s = elem_size(cx, discriminant_type(cases)) + s = align_to(s, max_case_alignment(cx, cases)) cs = 0 for c in cases: if c.t is not None: - cs = max(cs, elem_size(c.t, ptr_type)) + cs = max(cs, elem_size(cx, c.t)) s += cs - return align_to(s, alignment_variant(cases, ptr_type)) + return align_to(s, alignment_variant(cx, cases)) def elem_size_flags(labels): n = len(labels) @@ -1253,11 +1271,17 @@ def elem_size_flags(labels): if n <= 16: return 2 return 4 +def elem_size_borrow(cx, t): + if cx.inst is t.rt.impl and cx.direction == 'lower': + return ptr_size(t.rt.rep) + else: + return 4 + ## Loading def load(cx, ptr, t): - assert(ptr == align_to(ptr, alignment(t, cx.opts.memory.ptr_type()))) - assert(ptr + elem_size(t, cx.opts.memory.ptr_type()) <= len(cx.opts.memory)) + assert(ptr == align_to(ptr, alignment(cx, t))) + assert(ptr + elem_size(cx, t) <= len(cx.opts.memory)) match despecialize(t): case BoolType() : return convert_int_to_bool(load_int(cx, ptr, 1)) case U8Type() : return load_int(cx, ptr, 1) @@ -1326,19 +1350,20 @@ def convert_i32_to_char(cx, i): String = tuple[str, str, int] def load_string(cx, ptr) -> String: - begin = load_int(cx, ptr, cx.opts.memory.ptr_size()) - tagged_code_units = load_int(cx, ptr + cx.opts.memory.ptr_size(), cx.opts.memory.ptr_size()) + ptr_size = cx.opts.memory.ptr_size() + begin = load_int(cx, ptr, ptr_size) + tagged_code_units = load_int(cx, ptr + ptr_size, ptr_size) return load_string_from_range(cx, begin, tagged_code_units) -def utf16_tag(ptr_type): - return 1 << (ptr_size(ptr_type) * 8 - 1) +def utf16_tag(cx): + return 1 << (cx.opts.memory.ptr_size() * 8 - 1) REALLOC_I32_MAX = 2**32 - 1 MAX_STRING_BYTE_LENGTH = (1 << 28) - 1 assert(REALLOC_I32_MAX > 2 * MAX_STRING_BYTE_LENGTH) def load_string_from_range(cx, ptr, tagged_code_units) -> String: - tag = utf16_tag(cx.opts.memory.ptr_type()) + tag = utf16_tag(cx) match cx.opts.string_encoding: case 'utf8': alignment = 1 @@ -1378,37 +1403,38 @@ def lift_error_context(cx, i): def load_list(cx, ptr, elem_type, maybe_length): if maybe_length is not None: return load_list_from_valid_range(cx, ptr, maybe_length, elem_type) - begin = load_int(cx, ptr, cx.opts.memory.ptr_size()) - length = load_int(cx, ptr + cx.opts.memory.ptr_size(), cx.opts.memory.ptr_size()) + ptr_size = cx.opts.memory.ptr_size() + begin = load_int(cx, ptr, ptr_size) + length = load_int(cx, ptr + ptr_size, ptr_size) return load_list_from_range(cx, begin, length, elem_type) def load_list_from_range(cx, ptr, length, elem_type): - trap_if(length * elem_size(elem_type, cx.opts.memory.ptr_type()) > MAX_LIST_BYTE_LENGTH) - trap_if(ptr != align_to(ptr, alignment(elem_type, cx.opts.memory.ptr_type()))) - trap_if(ptr + length * elem_size(elem_type, cx.opts.memory.ptr_type()) > len(cx.opts.memory)) + trap_if(length * elem_size(cx, elem_type) > MAX_LIST_BYTE_LENGTH) + trap_if(ptr != align_to(ptr, alignment(cx, elem_type))) + trap_if(ptr + length * elem_size(cx, elem_type) > len(cx.opts.memory)) return load_list_from_valid_range(cx, ptr, length, elem_type) def load_list_from_valid_range(cx, ptr, length, elem_type): a = [] for i in range(length): - a.append(load(cx, ptr + i * elem_size(elem_type, cx.opts.memory.ptr_type()), elem_type)) + a.append(load(cx, ptr + i * elem_size(cx, elem_type), elem_type)) return a def load_record(cx, ptr, fields): record = {} for field in fields: - ptr = align_to(ptr, alignment(field.t, cx.opts.memory.ptr_type())) + ptr = align_to(ptr, alignment(cx, field.t)) record[field.label] = load(cx, ptr, field.t) - ptr += elem_size(field.t, cx.opts.memory.ptr_type()) + ptr += elem_size(cx, field.t) return record def load_variant(cx, ptr, cases): - disc_size = elem_size(discriminant_type(cases), cx.opts.memory.ptr_type()) + disc_size = elem_size(cx, discriminant_type(cases)) case_index = load_int(cx, ptr, disc_size) ptr += disc_size trap_if(case_index >= len(cases)) c = cases[case_index] - ptr = align_to(ptr, max_case_alignment(cases, cx.opts.memory.ptr_type())) + ptr = align_to(ptr, max_case_alignment(cx, cases)) if c.t is None: return { c.label: None } return { c.label: load(cx, ptr, c.t) } @@ -1461,8 +1487,8 @@ def lift_async_value(ReadableEndT, cx, i, t): ## Storing def store(cx, v, t, ptr): - assert(ptr == align_to(ptr, alignment(t, cx.opts.memory.ptr_type()))) - assert(ptr + elem_size(t, cx.opts.memory.ptr_type()) <= len(cx.opts.memory)) + assert(ptr == align_to(ptr, alignment(cx, t))) + assert(ptr + elem_size(cx, t) <= len(cx.opts.memory)) match despecialize(t): case BoolType() : store_int(cx, int(bool(v)), ptr, 1) case U8Type() : store_int(cx, v, ptr, 1) @@ -1483,7 +1509,7 @@ def store(cx, v, t, ptr): case VariantType(cases) : store_variant(cx, v, ptr, cases) case FlagsType(labels) : store_flags(cx, v, ptr, labels) case OwnType() : store_int(cx, lower_own(cx, v, t), ptr, 4) - case BorrowType() : store_int(cx, lower_borrow(cx, v, t), ptr, 4) + case BorrowType() : store_borrow(cx, v, ptr, t) case StreamType(t) : store_int(cx, lower_stream(cx, v, t), ptr, 4) case FutureType(t) : store_int(cx, lower_future(cx, v, t), ptr, 4) @@ -1534,12 +1560,13 @@ def char_to_i32(c): def store_string(cx, v: String, ptr): begin, tagged_code_units = store_string_into_range(cx, v) - store_int(cx, begin, ptr, cx.opts.memory.ptr_size()) - store_int(cx, tagged_code_units, ptr + cx.opts.memory.ptr_size(), cx.opts.memory.ptr_size()) + ptr_size = cx.opts.memory.ptr_size() + store_int(cx, begin, ptr, ptr_size) + store_int(cx, tagged_code_units, ptr + ptr_size, ptr_size) def store_string_into_range(cx, v: String): src, src_encoding, src_tagged_code_units = v - tag = utf16_tag(cx.opts.memory.ptr_type()) + tag = utf16_tag(cx) if src_encoding == 'latin1+utf16': if bool(src_tagged_code_units & tag): @@ -1635,7 +1662,7 @@ def store_string_to_latin1_or_utf16(cx, src, src_code_units): cx.opts.memory[ptr+2*dst_byte_length : ptr+len(encoded)] = encoded[2*dst_byte_length : ] if worst_case_size > len(encoded): ptr = cx.reallocate(ptr, worst_case_size, 2, len(encoded)) - tagged_code_units = int(len(encoded) / 2) | utf16_tag(cx.opts.memory.ptr_type()) + tagged_code_units = int(len(encoded) / 2) | utf16_tag(cx) return (ptr, tagged_code_units) if dst_byte_length < src_code_units: ptr = cx.reallocate(ptr, src_code_units, 2, dst_byte_length) @@ -1648,7 +1675,7 @@ def store_probably_utf16_to_latin1_or_utf16(cx, src, src_code_units): encoded = src.encode('utf-16-le') cx.opts.memory[ptr : ptr+len(encoded)] = encoded if any(ord(c) >= (1 << 8) for c in src): - tagged_code_units = int(len(encoded) / 2) | utf16_tag(cx.opts.memory.ptr_type()) + tagged_code_units = int(len(encoded) / 2) | utf16_tag(cx) return (ptr, tagged_code_units) latin1_size = int(len(encoded) / 2) for i in range(latin1_size): @@ -1665,32 +1692,33 @@ def store_list(cx, v, ptr, elem_type, maybe_length): store_list_into_valid_range(cx, v, ptr, elem_type) return begin, length = store_list_into_range(cx, v, elem_type) - store_int(cx, begin, ptr, cx.opts.memory.ptr_size()) - store_int(cx, length, ptr + cx.opts.memory.ptr_size(), cx.opts.memory.ptr_size()) + ptr_size = cx.opts.memory.ptr_size() + store_int(cx, begin, ptr, ptr_size) + store_int(cx, length, ptr + ptr_size, ptr_size) def store_list_into_range(cx, v, elem_type): - byte_length = len(v) * elem_size(elem_type, cx.opts.memory.ptr_type()) + byte_length = len(v) * elem_size(cx, elem_type) assert(byte_length <= REALLOC_I32_MAX) - ptr = cx.allocate(alignment(elem_type, cx.opts.memory.ptr_type()), byte_length) + ptr = cx.allocate(alignment(cx, elem_type), byte_length) store_list_into_valid_range(cx, v, ptr, elem_type) return (ptr, len(v)) def store_list_into_valid_range(cx, v, ptr, elem_type): for i,e in enumerate(v): - store(cx, e, elem_type, ptr + i * elem_size(elem_type, cx.opts.memory.ptr_type())) + store(cx, e, elem_type, ptr + i * elem_size(cx, elem_type)) def store_record(cx, v, ptr, fields): for f in fields: - ptr = align_to(ptr, alignment(f.t, cx.opts.memory.ptr_type())) + ptr = align_to(ptr, alignment(cx, f.t)) store(cx, v[f.label], f.t, ptr) - ptr += elem_size(f.t, cx.opts.memory.ptr_type()) + ptr += elem_size(cx, f.t) def store_variant(cx, v, ptr, cases): case_index, case_value = match_case(v, cases) - disc_size = elem_size(discriminant_type(cases), cx.opts.memory.ptr_type()) + disc_size = elem_size(cx, discriminant_type(cases)) store_int(cx, case_index, ptr, disc_size) ptr += disc_size - ptr = align_to(ptr, max_case_alignment(cases, cx.opts.memory.ptr_type())) + ptr = align_to(ptr, max_case_alignment(cx, cases)) c = cases[case_index] if c.t is not None: store(cx, case_value, c.t, ptr) @@ -1717,10 +1745,14 @@ def lower_own(cx, rep, t): h = ResourceHandle(t.rt, rep, own = True) return cx.inst.handles.add(h) -def lower_borrow(cx, rep, t): - assert(isinstance(cx.borrow_scope, Task)) +def store_borrow(cx, rep, ptr, t): if cx.inst is t.rt.impl: - return rep + store_int(cx, rep, ptr, ptr_size(t.rt.rep)) + else: + store_int(cx, add_borrow(cx, rep, t), ptr, 4) + +def add_borrow(cx, rep, t): + assert(isinstance(cx.borrow_scope, Task)) h = ResourceHandle(t.rt, rep, own = False, borrow_scope = cx.borrow_scope) h.borrow_scope.num_borrows += 1 return cx.inst.handles.add(h) @@ -1746,41 +1778,41 @@ def lower_async_value(ReadableEndT, cx, end, t): MAX_FLAT_ASYNC_PARAMS = 4 MAX_FLAT_RESULTS = 1 -def flatten_functype(opts, ft, context): - flat_params = flatten_types(ft.param_types(), opts) - flat_results = flatten_types(ft.result_type(), opts) - if not opts.async_: +def flatten_functype(cx, ft): + flat_params = flatten_types(cx.reverse_direction(), ft.param_types()) + flat_results = flatten_types(cx, ft.result_type()) + if not cx.opts.async_: if len(flat_params) > MAX_FLAT_PARAMS: - flat_params = [opts.memory.ptr_type()] + flat_params = [cx.opts.memory.ptr_type()] if len(flat_results) > MAX_FLAT_RESULTS: - match context: + match cx.direction: case 'lift': - flat_results = [opts.memory.ptr_type()] + flat_results = [cx.opts.memory.ptr_type()] case 'lower': - flat_params += [opts.memory.ptr_type()] + flat_params += [cx.opts.memory.ptr_type()] flat_results = [] return CoreFuncType(flat_params, flat_results) else: - match context: + match cx.direction: case 'lift': if len(flat_params) > MAX_FLAT_PARAMS: - flat_params = [opts.memory.ptr_type()] - if opts.callback: + flat_params = [cx.opts.memory.ptr_type()] + if cx.opts.callback: flat_results = ['i32'] else: flat_results = [] case 'lower': if len(flat_params) > MAX_FLAT_ASYNC_PARAMS: - flat_params = [opts.memory.ptr_type()] + flat_params = [cx.opts.memory.ptr_type()] if len(flat_results) > 0: - flat_params += [opts.memory.ptr_type()] + flat_params += [cx.opts.memory.ptr_type()] flat_results = ['i32'] return CoreFuncType(flat_params, flat_results) -def flatten_types(ts, opts): - return [ft for t in ts for ft in flatten_type(t, opts)] +def flatten_types(cx, ts): + return [ft for t in ts for ft in flatten_type(cx, t)] -def flatten_type(t, opts): +def flatten_type(cx, t): match despecialize(t): case BoolType() : return ['i32'] case U8Type() | U16Type() | U32Type() : return ['i32'] @@ -1789,42 +1821,49 @@ def flatten_type(t, opts): case F32Type() : return ['f32'] case F64Type() : return ['f64'] case CharType() : return ['i32'] - case StringType() : return [opts.memory.ptr_type(), opts.memory.ptr_type()] + case StringType() : return 2 * [cx.opts.memory.ptr_type()] case ErrorContextType() : return ['i32'] - case ListType(t, l) : return flatten_list(t, l, opts) - case RecordType(fields) : return flatten_record(fields, opts) - case VariantType(cases) : return flatten_variant(cases, opts) + case ListType(t, l) : return flatten_list(cx, t, l) + case RecordType(fields) : return flatten_record(cx, fields) + case VariantType(cases) : return flatten_variant(cx, cases) case FlagsType(labels) : return ['i32'] - case OwnType() | BorrowType() : return ['i32'] + case OwnType() : return ['i32'] + case BorrowType() : return flatten_borrow(cx, t) case StreamType() | FutureType() : return ['i32'] -def flatten_list(elem_type, maybe_length, opts): +def flatten_list(cx, elem_type, maybe_length): if maybe_length is not None: - return flatten_type(elem_type, opts) * maybe_length - return [opts.memory.ptr_type(), opts.memory.ptr_type()] + return flatten_type(cx, elem_type) * maybe_length + return 2 * [cx.opts.memory.ptr_type()] -def flatten_record(fields, opts): +def flatten_record(cx, fields): flat = [] for f in fields: - flat += flatten_type(f.t, opts) + flat += flatten_type(cx, f.t) return flat -def flatten_variant(cases, opts): +def flatten_variant(cx, cases): flat = [] for c in cases: if c.t is not None: - for i,ft in enumerate(flatten_type(c.t, opts)): + for i,ft in enumerate(flatten_type(cx, c.t)): if i < len(flat): flat[i] = join(flat[i], ft) else: flat.append(ft) - return flatten_type(discriminant_type(cases), opts) + flat + return flatten_type(cx, discriminant_type(cases)) + flat def join(a, b): if a == b: return a if (a == 'i32' and b == 'f32') or (a == 'f32' and b == 'i32'): return 'i32' return 'i64' +def flatten_borrow(cx, t): + if cx.inst is t.rt.impl and cx.direction == 'lower': + return [t.rt.rep] + else: + return ['i32'] + ## Flat Lifting class CoreValueIter: @@ -1906,7 +1945,7 @@ def lift_flat_record(cx, vi, fields): return record def lift_flat_variant(cx, vi, cases): - flat_types = flatten_variant(cases, cx.opts) + flat_types = flatten_variant(cx, cases) assert(flat_types.pop(0) == 'i32') case_index = vi.next('i32') trap_if(case_index >= len(cases)) @@ -1961,7 +2000,7 @@ def lower_flat(cx, v, t): case VariantType(cases) : return lower_flat_variant(cx, v, cases) case FlagsType(labels) : return lower_flat_flags(v, labels) case OwnType() : return [lower_own(cx, v, t)] - case BorrowType() : return [lower_borrow(cx, v, t)] + case BorrowType() : return lower_flat_borrow(cx, v, t) case StreamType(t) : return [lower_stream(cx, v, t)] case FutureType(t) : return [lower_future(cx, v, t)] @@ -1992,14 +2031,14 @@ def lower_flat_record(cx, v, fields): def lower_flat_variant(cx, v, cases): case_index, case_value = match_case(v, cases) - flat_types = flatten_variant(cases, cx.opts) + flat_types = flatten_variant(cx, cases) assert(flat_types.pop(0) == 'i32') c = cases[case_index] if c.t is None: payload = [] else: payload = lower_flat(cx, case_value, c.t) - for i,(fv,have) in enumerate(zip(payload, flatten_type(c.t, cx.opts))): + for i,(fv,have) in enumerate(zip(payload, flatten_type(cx, c.t))): want = flat_types.pop(0) match (have, want): case ('f32', 'i32') : payload[i] = encode_float_as_i32(fv) @@ -2015,32 +2054,37 @@ def lower_flat_flags(v, labels): assert(0 < len(labels) <= 32) return [pack_flags_into_int(v, labels)] +def lower_flat_borrow(cx, rep, t): + if cx.inst is t.rt.impl: + return [rep] + else: + return [add_borrow(cx, rep, t)] + ## Lifting and Lowering Values def lift_flat_values(cx, max_flat, vi, ts): - flat_types = flatten_types(ts, cx.opts) + flat_types = flatten_types(cx, ts) if len(flat_types) > max_flat: ptr = vi.next(cx.opts.memory.ptr_type()) tuple_type = TupleType(ts) - trap_if(ptr != align_to(ptr, alignment(tuple_type, cx.opts.memory.ptr_type()))) - trap_if(ptr + elem_size(tuple_type, cx.opts.memory.ptr_type()) > len(cx.opts.memory)) + trap_if(ptr != align_to(ptr, alignment(cx, tuple_type))) + trap_if(ptr + elem_size(cx, tuple_type) > len(cx.opts.memory)) return list(load(cx, ptr, tuple_type).values()) else: return [ lift_flat(cx, vi, t) for t in ts ] def lower_flat_values(cx, max_flat, vs, ts, out_param = None): - flat_types = flatten_types(ts, cx.opts) + flat_types = flatten_types(cx, ts) if len(flat_types) > max_flat: tuple_type = TupleType(ts) tuple_value = {str(i): v for i,v in enumerate(vs)} if out_param is None: - ptr_type = cx.opts.memory.ptr_type() - ptr = cx.allocate(alignment(tuple_type, ptr_type), elem_size(tuple_type, ptr_type)) + ptr = cx.allocate(alignment(cx, tuple_type), elem_size(cx, tuple_type)) flat_vals = [ptr] else: ptr = out_param.next(cx.opts.memory.ptr_type()) - trap_if(ptr != align_to(ptr, alignment(tuple_type, cx.opts.memory.ptr_type()))) - trap_if(ptr + elem_size(tuple_type, cx.opts.memory.ptr_type()) > len(cx.opts.memory)) + trap_if(ptr != align_to(ptr, alignment(cx, tuple_type))) + trap_if(ptr + elem_size(cx, tuple_type) > len(cx.opts.memory)) flat_vals = [] store(cx, tuple_value, tuple_type, ptr) else: @@ -2058,10 +2102,10 @@ def thread_func(): if not task.enter_implicit_thread(): return - cx = LiftLowerContext(opts, inst, task) + cx = LiftLowerContext('lift', opts, inst, task) + flat_ft = flatten_functype(cx, ft) args = task.start() - flat_args = lower_flat_values(cx, MAX_FLAT_PARAMS, args, ft.param_types()) - flat_ft = flatten_functype(opts, ft, 'lift') + flat_args = lower_flat_values(cx.reverse_direction(), MAX_FLAT_PARAMS, args, ft.param_types()) assert(types_match_values(flat_ft.params, flat_args)) if not opts.async_: @@ -2150,7 +2194,9 @@ def canon_lower(callee, ft, opts, flat_args: list[CoreValType]) -> list[CoreValT thread = current_thread() trap_if(not thread.task.inst.may_leave) - flat_ft = flatten_functype(opts, ft, 'lower') + subtask = Subtask() + cx = LiftLowerContext('lower', opts, thread.task.inst, subtask) + flat_ft = flatten_functype(cx, ft) assert(types_match_values(flat_ft.params, flat_args)) flat_args = CoreValueIter(flat_args) @@ -2161,15 +2207,13 @@ def canon_lower(callee, ft, opts, flat_args: list[CoreValType]) -> list[CoreValT max_flat_params = MAX_FLAT_ASYNC_PARAMS max_flat_results = 0 - subtask = Subtask() - cx = LiftLowerContext(opts, thread.task.inst, subtask) maybe_on_progress = lambda:() def on_start(): maybe_on_progress() assert(subtask.state == Subtask.State.STARTING) subtask.state = Subtask.State.STARTED - return lift_flat_values(cx, max_flat_params, flat_args, ft.param_types()) + return lift_flat_values(cx.reverse_direction(), max_flat_params, flat_args, ft.param_types()) def on_resolve(result): maybe_on_progress() @@ -2233,7 +2277,8 @@ def canon_resource_drop(rt, i): if h.own: assert(h.borrow_scope is None) opts = CanonicalOptions(async_ = False) - ft = FuncType([U32Type()], [], async_ = False) + rep_type = U32Type() if rt.rep == 'i32' else U64Type() + ft = FuncType([rep_type], [], async_ = False) dtor = rt.dtor or (lambda rep: []) callee = inst.store.lift(dtor, ft, opts, rt.impl) caller = inst.store.lower(callee, ft, opts, inst) @@ -2294,7 +2339,7 @@ def canon_task_return(result_type, opts: LiftOptions, flat_args): trap_if(not task.opts.async_) trap_if(result_type != task.ft.result) trap_if(not LiftOptions.equal(opts, task.opts)) - cx = LiftLowerContext(opts, task.inst, task) + cx = LiftLowerContext('lift', opts, task.inst, task) result = lift_flat_values(cx, MAX_FLAT_PARAMS, CoreValueIter(flat_args), task.ft.result_type()) task.return_(result) return [] @@ -2328,7 +2373,7 @@ def canon_waitable_set_wait(mem, si, ptr): def unpack_event(mem, inst, ptr, e: EventTuple): event, p1, p2 = e - cx = LiftLowerContext(LiftLowerOptions(memory = mem), inst) + cx = LiftLowerContext('lower', LiftLowerOptions(memory = mem), inst) trap_if(ptr != align_to(ptr, 4)) trap_if(ptr + 8 > len(cx.opts.memory)) store(cx, p1, U32Type(), ptr) @@ -2428,18 +2473,18 @@ def canon_future_new(future_t): ### 🔀 `canon stream.{read,write}` def canon_stream_read(stream_t, opts, i, ptr, length): - return copy(ReadableStreamEnd, WritableGuestBuffer, stream_t, opts, i, ptr, length) + return copy(ReadableStreamEnd, WritableGuestBuffer, 'lower', stream_t, opts, i, ptr, length) def canon_stream_write(stream_t, opts, i, ptr, length): - return copy(WritableStreamEnd, ReadableGuestBuffer, stream_t, opts, i, ptr, length) + return copy(WritableStreamEnd, ReadableGuestBuffer, 'lift', stream_t, opts, i, ptr, length) def canon_future_read(future_t, opts, i, ptr): - return copy(ReadableFutureEnd, WritableGuestBuffer, future_t, opts, i, ptr, 1) + return copy(ReadableFutureEnd, WritableGuestBuffer, 'lower', future_t, opts, i, ptr, 1) def canon_future_write(future_t, opts, i, ptr): - return copy(WritableFutureEnd, ReadableGuestBuffer, future_t, opts, i, ptr, 1) + return copy(WritableFutureEnd, ReadableGuestBuffer, 'lift', future_t, opts, i, ptr, 1) -def copy(EndT, BufferT, stream_or_future_t, opts, i, ptr, length): +def copy(EndT, BufferT, direction, stream_or_future_t, opts, i, ptr, length): thread = current_thread() trap_if(not thread.task.inst.may_leave) end = thread.task.inst.handles.get(i) @@ -2447,7 +2492,7 @@ def copy(EndT, BufferT, stream_or_future_t, opts, i, ptr, length): trap_if(end.t != stream_or_future_t.t) trap_if(end.state != End.State.IDLE) trap_if(end.in_waitable_set() and not opts.async_) - cx = LiftLowerContext(opts, thread.task.inst, borrow_scope = None) + cx = LiftLowerContext(direction, opts, thread.task.inst, borrow_scope = None) buffer = BufferT(end.t, cx, ptr, length) end.copy(buffer) if not end.has_pending_event(): @@ -2652,7 +2697,7 @@ def canon_error_context_new(opts, ptr, tagged_code_units): if DETERMINISTIC_PROFILE or random.randint(0,1): s = String(('', 'utf8', 0)) else: - cx = LiftLowerContext(opts, inst) + cx = LiftLowerContext('lift', opts, inst) s = load_string_from_range(cx, ptr, tagged_code_units) s = host_defined_transformation(s) i = inst.handles.add(ErrorContext(s)) @@ -2665,9 +2710,9 @@ def canon_error_context_debug_message(opts, i, ptr): trap_if(not inst.may_leave) errctx = inst.handles.get(i) trap_if(not isinstance(errctx, ErrorContext)) - cx = LiftLowerContext(opts, inst) - trap_if(ptr != align_to(ptr, alignment(StringType(), cx.opts.memory.ptr_type()))) - trap_if(ptr + elem_size(StringType(), cx.opts.memory.ptr_type()) > len(cx.opts.memory)) + cx = LiftLowerContext('lower', opts, inst) + trap_if(ptr != align_to(ptr, alignment(cx, StringType()))) + trap_if(ptr + elem_size(cx, StringType()) > len(cx.opts.memory)) store(cx, errctx.debug_message, StringType(), ptr) return [] diff --git a/design/mvp/canonical-abi/run_tests.py b/design/mvp/canonical-abi/run_tests.py index d007eba8..a81c5961 100644 --- a/design/mvp/canonical-abi/run_tests.py +++ b/design/mvp/canonical-abi/run_tests.py @@ -49,7 +49,7 @@ def mk_opts(memory = MemInst(bytearray(), 'i32'), encoding = 'utf8', realloc = N def mk_cx(memory = MemInst(bytearray(), 'i32'), encoding = 'utf8', realloc = None, post_return = None): opts = mk_opts(memory, encoding, realloc, post_return) inst = ComponentInstance(Store()) - return LiftLowerContext(opts, inst) + return LiftLowerContext('lift', opts, inst) def lift_and_run(opts, inst, ft, callee, on_start, on_resolve): func_inst = inst.store.lift(callee, ft, opts, inst) @@ -290,7 +290,7 @@ def test_string(src_encoding, dst_encoding, s, addr_type='i32'): except UnicodeEncodeError: pass encoded = s.encode('utf-16-le') - tagged_code_units = int(len(encoded) / 2) | utf16_tag(addr_type) + tagged_code_units = int(len(encoded) / 2) | (1 << (ptr_size(addr_type) * 8 - 1)) test_string_internal(src_encoding, dst_encoding, s, encoded, tagged_code_units, addr_type) encodings = ['utf8', 'utf16', 'latin1+utf16'] @@ -416,7 +416,7 @@ def test_heap(t, expect, args, byte_array, addr_type='i32'): [42,0xff,83,0, 43,0xff,84,0]) def test_flatten(t, params, results, addr_type='i32'): - opts = mk_opts(MemInst(bytearray(), addr_type)) + cx = mk_cx(MemInst(bytearray(), addr_type)) expect = CoreFuncType(params, results) if len(params) > definitions.MAX_FLAT_PARAMS: @@ -424,13 +424,15 @@ def test_flatten(t, params, results, addr_type='i32'): if len(results) > definitions.MAX_FLAT_RESULTS: expect.results = [addr_type] - got = flatten_functype(opts, t, 'lift') + cx.direction = 'lift' + got = flatten_functype(cx, t) assert(got == expect) if len(results) > definitions.MAX_FLAT_RESULTS: expect.params += [addr_type] expect.results = [] - got = flatten_functype(opts, t, 'lower') + cx.direction = 'lower' + got = flatten_functype(cx, t) assert(got == expect) test_flatten(FuncType([U8Type(),F32Type(),F64Type()],[]), ['i32','f32','f64'], []) @@ -584,9 +586,9 @@ def dtor(args): store = Store() producer_inst = ComponentInstance(store) - rt = ResourceType(producer_inst, dtor) # usable in imports and exports + rt = ResourceType(producer_inst, 'i32', dtor) # usable in imports and exports inst = ComponentInstance(store) - rt2 = ResourceType(inst, dtor) # only usable in exports + rt2 = ResourceType(inst, 'i32', dtor) # only usable in exports opts = mk_opts() host_ft = FuncType([ @@ -679,6 +681,58 @@ def on_resolve(results): definitions.MAX_FLAT_RESULTS = before +def test_i64_borrows(): + store = Store() + impl_inst = ComponentInstance(store) + rt = ResourceType(impl_inst, 'i64') + other_inst = ComponentInstance(store) + + ft = FuncType([ + BorrowType(rt), + ListType(TupleType([BorrowType(rt), U8Type()])) + ],[]) + + def flat_params(direction, inst): + return flatten_functype(LiftLowerContext(direction, mk_opts(), inst), ft).params + assert(flat_params('lift', impl_inst) == ['i64', 'i32', 'i32']) + assert(flat_params('lower', impl_inst) == ['i32', 'i32', 'i32']) + assert(flat_params('lift', other_inst) == ['i32', 'i32', 'i32']) + cx = LiftLowerContext('lift', mk_opts(), impl_inst) + assert(elem_size(cx, BorrowType(rt)) == alignment(cx, BorrowType(rt)) == 4) + + reps = [2**32 + 1, 2**32 + 2, 2**32 + 3] + def on_start(): + return [reps[0], [mk_tup(reps[1], 7), mk_tup(reps[2], 8)]] + + impl_heap = Heap(100) + impl_opts = mk_opts(MemInst(impl_heap.memory, 'i32'), realloc = impl_heap.realloc) + def core_impl(args): + [rep, ptr, length] = args + assert(rep == reps[0]) + assert(length == 2) + assert(int.from_bytes(impl_heap.memory[ptr : ptr+8], 'little') == reps[1]) + assert(impl_heap.memory[ptr+8] == 7) + assert(int.from_bytes(impl_heap.memory[ptr+16 : ptr+24], 'little') == reps[2]) + assert(impl_heap.memory[ptr+24] == 8) + return [] + lift_and_run(impl_opts, impl_inst, ft, core_impl, on_start, lambda _: ()) + + other_heap = Heap(100) + other_opts = mk_opts(MemInst(other_heap.memory, 'i32'), realloc = other_heap.realloc) + def core_other(args): + [h0, ptr, length] = args + assert(length == 2) + h1 = int.from_bytes(other_heap.memory[ptr : ptr+4], 'little') + assert(other_heap.memory[ptr+4] == 7) + h2 = int.from_bytes(other_heap.memory[ptr+8 : ptr+12], 'little') + assert(other_heap.memory[ptr+12] == 8) + for h,rep in zip([h0, h1, h2], reps): + assert(other_inst.handles.get(h).rep == rep) + [] = canon_resource_drop(rt, h) + return [] + lift_and_run(other_opts, other_inst, ft, core_other, on_start, lambda _: ()) + + def test_async_to_async(): producer_heap = Heap(10) producer_opts = mk_opts(MemInst(producer_heap.memory, 'i32')) @@ -3125,6 +3179,7 @@ def on_resolve(v): test_trap_propagation() test_cross_component_realloc() test_handles() +test_i64_borrows() test_async_to_async() test_async_callback() test_callback_interleaving()