Browse Source

Reserve handle index 0 in the component model (#7661)

This commit updates the allocation scheme for resources in the component
model to start at 1 instead of 0 when communicating with components.
This is an implementation of WebAssembly/component-model#284.

While this broke a number of tests we have this shouldn't actually break
any components in practice. The broken tests were all overly-precise in
their assertions and error messages and this shouldn't idiomatically
come up in any guest language, so this should not be a practically
breaking change.

This change additionally places an upper limit on the maximum
allocatable index at `1 << 30` which is also specified in the above PR.
pull/7977/head
Alex Crichton 9 months ago
committed by GitHub
parent
commit
93f17e3c0f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      crates/runtime/src/component.rs
  2. 4
      crates/runtime/src/component/libcalls.rs
  3. 70
      crates/runtime/src/component/resources.rs
  4. 20
      crates/wasmtime/src/runtime/component/func/options.rs
  5. 48
      crates/wasmtime/src/runtime/component/resources.rs
  6. 12
      tests/all/component_model/resources.rs
  7. 54
      tests/misc_testsuite/component-model/resources.wast

6
crates/runtime/src/component.rs

@ -531,7 +531,7 @@ impl ComponentInstance {
/// Implementation of the `resource.new` intrinsic for `i32`
/// representations.
pub fn resource_new32(&mut self, resource: TypeResourceTableIndex, rep: u32) -> u32 {
pub fn resource_new32(&mut self, resource: TypeResourceTableIndex, rep: u32) -> Result<u32> {
self.resource_tables().resource_new(Some(resource), rep)
}
@ -600,7 +600,7 @@ impl ComponentInstance {
) -> Result<u32> {
let mut tables = self.resource_tables();
let rep = tables.resource_lift_own(Some(src), idx)?;
Ok(tables.resource_lower_own(Some(dst), rep))
tables.resource_lower_own(Some(dst), rep)
}
pub(crate) fn resource_transfer_borrow(
@ -623,7 +623,7 @@ impl ComponentInstance {
if dst_owns_resource {
return Ok(rep);
}
Ok(tables.resource_lower_borrow(Some(dst), rep))
tables.resource_lower_borrow(Some(dst), rep)
}
pub(crate) fn resource_enter_call(&mut self) {

4
crates/runtime/src/component/libcalls.rs

@ -519,9 +519,7 @@ fn inflate_latin1_bytes(dst: &mut [u16], latin1_bytes_so_far: usize) -> &mut [u1
unsafe fn resource_new32(vmctx: *mut VMComponentContext, resource: u32, rep: u32) -> Result<u32> {
let resource = TypeResourceTableIndex::from_u32(resource);
Ok(ComponentInstance::from_vmctx(vmctx, |instance| {
instance.resource_new32(resource, rep)
}))
ComponentInstance::from_vmctx(vmctx, |instance| instance.resource_new32(resource, rep))
}
unsafe fn resource_rep32(vmctx: *mut VMComponentContext, resource: u32, idx: u32) -> Result<u32> {

70
crates/runtime/src/component/resources.rs

@ -28,6 +28,11 @@ use std::mem;
use wasmtime_environ::component::TypeResourceTableIndex;
use wasmtime_environ::PrimaryMap;
/// The maximum handle value is specified in
/// <https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md>
/// currently and keeps the upper bit free for use in the component.
const MAX_RESOURCE_HANDLE: u32 = 1 << 30;
/// Contextual state necessary to perform resource-related operations.
///
/// This state a bit odd since it has a few optional bits, but the idea is that
@ -125,7 +130,7 @@ impl ResourceTables<'_> {
/// Implementation of the `resource.new` canonical intrinsic.
///
/// Note that this is the same as `resource_lower_own`.
pub fn resource_new(&mut self, ty: Option<TypeResourceTableIndex>, rep: u32) -> u32 {
pub fn resource_new(&mut self, ty: Option<TypeResourceTableIndex>, rep: u32) -> Result<u32> {
self.table(ty).insert(Slot::Own { rep, lend_count: 0 })
}
@ -173,7 +178,11 @@ impl ResourceTables<'_> {
/// the same as `resource_new` implementation-wise.
///
/// This is an implementation of the canonical ABI `lower_own` function.
pub fn resource_lower_own(&mut self, ty: Option<TypeResourceTableIndex>, rep: u32) -> u32 {
pub fn resource_lower_own(
&mut self,
ty: Option<TypeResourceTableIndex>,
rep: u32,
) -> Result<u32> {
self.table(ty).insert(Slot::Own { rep, lend_count: 0 })
}
@ -237,7 +246,11 @@ impl ResourceTables<'_> {
/// function. The other half of this implementation is located on
/// `VMComponentContext` which handles the special case of avoiding borrow
/// tracking entirely.
pub fn resource_lower_borrow(&mut self, ty: Option<TypeResourceTableIndex>, rep: u32) -> u32 {
pub fn resource_lower_borrow(
&mut self,
ty: Option<TypeResourceTableIndex>,
rep: u32,
) -> Result<u32> {
let scope = self.calls.scopes.len() - 1;
let borrow_count = &mut self.calls.scopes.last_mut().unwrap().borrow_count;
*borrow_count = borrow_count.checked_add(1).unwrap();
@ -278,12 +291,8 @@ impl ResourceTables<'_> {
}
impl ResourceTable {
fn next(&self) -> usize {
self.next as usize
}
fn insert(&mut self, new: Slot) -> u32 {
let next = self.next();
fn insert(&mut self, new: Slot) -> Result<u32> {
let next = self.next as usize;
if next == self.slots.len() {
self.slots.push(Slot::Free {
next: self.next.checked_add(1).unwrap(),
@ -294,36 +303,49 @@ impl ResourceTable {
Slot::Free { next } => next,
_ => unreachable!(),
};
u32::try_from(ret).unwrap()
// The component model reserves index 0 as never allocatable so add one
// to the table index to start the numbering at 1 instead. Also note
// that the component model places an upper-limit per-table on the
// maximum allowed index.
let ret = ret + 1;
if ret >= MAX_RESOURCE_HANDLE {
bail!("cannot allocate another handle: index overflow");
}
Ok(ret)
}
fn handle_index_to_table_index(&self, idx: u32) -> Option<usize> {
// NB: `idx` is decremented by one to account for the `+1` above during
// allocation.
let idx = idx.checked_sub(1)?;
usize::try_from(idx).ok()
}
fn rep(&self, idx: u32) -> Result<u32> {
match usize::try_from(idx).ok().and_then(|i| self.slots.get(i)) {
let slot = self
.handle_index_to_table_index(idx)
.and_then(|i| self.slots.get(i));
match slot {
None | Some(Slot::Free { .. }) => bail!("unknown handle index {idx}"),
Some(Slot::Own { rep, .. } | Slot::Borrow { rep, .. }) => Ok(*rep),
}
}
fn get_mut(&mut self, idx: u32) -> Result<&mut Slot> {
match usize::try_from(idx)
.ok()
.and_then(|i| self.slots.get_mut(i))
{
let slot = self
.handle_index_to_table_index(idx)
.and_then(|i| self.slots.get_mut(i));
match slot {
None | Some(Slot::Free { .. }) => bail!("unknown handle index {idx}"),
Some(other) => Ok(other),
}
}
fn remove(&mut self, idx: u32) -> Result<Slot> {
match usize::try_from(idx).ok().and_then(|i| self.slots.get(i)) {
Some(Slot::Own { .. }) | Some(Slot::Borrow { .. }) => {}
_ => bail!("unknown handle index {idx}"),
};
let ret = mem::replace(
&mut self.slots[idx as usize],
Slot::Free { next: self.next },
);
self.next = idx;
let to_fill = Slot::Free { next: self.next };
let ret = mem::replace(self.get_mut(idx)?, to_fill);
self.next = idx - 1;
Ok(ret)
}
}

20
crates/wasmtime/src/runtime/component/func/options.rs

@ -289,13 +289,21 @@ impl<'a, T> LowerContext<'a, T> {
/// into a guest-local index.
///
/// The `ty` provided is which table to put this into.
pub fn guest_resource_lower_own(&mut self, ty: TypeResourceTableIndex, rep: u32) -> u32 {
pub fn guest_resource_lower_own(
&mut self,
ty: TypeResourceTableIndex,
rep: u32,
) -> Result<u32> {
self.resource_tables().guest_resource_lower_own(rep, ty)
}
/// Lowers a `borrow` resource into the guest, converting the `rep` to a
/// guest-local index in the `ty` table specified.
pub fn guest_resource_lower_borrow(&mut self, ty: TypeResourceTableIndex, rep: u32) -> u32 {
pub fn guest_resource_lower_borrow(
&mut self,
ty: TypeResourceTableIndex,
rep: u32,
) -> Result<u32> {
// Implement `lower_borrow`'s special case here where if a borrow is
// inserted into a table owned by the instance which implemented the
// original resource then no borrow tracking is employed and instead the
@ -308,7 +316,7 @@ impl<'a, T> LowerContext<'a, T> {
// Note that the unsafety here should be valid given the contract of
// `LowerContext::new`.
if unsafe { (*self.instance).resource_owned_by_own_instance(ty) } {
return rep;
return Ok(rep);
}
self.resource_tables().guest_resource_lower_borrow(rep, ty)
}
@ -330,7 +338,7 @@ impl<'a, T> LowerContext<'a, T> {
///
/// Note that this is a special case for `Resource<T>`. Most of the time a
/// host value shouldn't be lowered with a lowering context.
pub fn host_resource_lower_own(&mut self, rep: u32) -> HostResourceIndex {
pub fn host_resource_lower_own(&mut self, rep: u32) -> Result<HostResourceIndex> {
self.resource_tables().host_resource_lower_own(rep)
}
@ -483,13 +491,13 @@ impl<'a> LiftContext<'a> {
/// Lowers a resource into the host-owned table, returning the index it was
/// inserted at.
pub fn host_resource_lower_own(&mut self, rep: u32) -> HostResourceIndex {
pub fn host_resource_lower_own(&mut self, rep: u32) -> Result<HostResourceIndex> {
self.resource_tables().host_resource_lower_own(rep)
}
/// Lowers a resource into the host-owned table, returning the index it was
/// inserted at.
pub fn host_resource_lower_borrow(&mut self, rep: u32) -> HostResourceIndex {
pub fn host_resource_lower_borrow(&mut self, rep: u32) -> Result<HostResourceIndex> {
self.resource_tables().host_resource_lower_borrow(rep)
}

48
crates/wasmtime/src/runtime/component/resources.rs

@ -397,15 +397,15 @@ impl<'a> HostResourceTables<'a> {
/// will point to the `rep` specified as well as recording that it has the
/// `ty` specified. The returned index is suitable for conversion into
/// either [`Resource`] or [`ResourceAny`].
pub fn host_resource_lower_own(&mut self, rep: u32) -> HostResourceIndex {
let idx = self.tables.resource_lower_own(None, rep);
self.new_host_index(idx)
pub fn host_resource_lower_own(&mut self, rep: u32) -> Result<HostResourceIndex> {
let idx = self.tables.resource_lower_own(None, rep)?;
Ok(self.new_host_index(idx))
}
/// See [`HostResourceTables::host_resource_lower_own`].
pub fn host_resource_lower_borrow(&mut self, rep: u32) -> HostResourceIndex {
let idx = self.tables.resource_lower_borrow(None, rep);
self.new_host_index(idx)
pub fn host_resource_lower_borrow(&mut self, rep: u32) -> Result<HostResourceIndex> {
let idx = self.tables.resource_lower_borrow(None, rep)?;
Ok(self.new_host_index(idx))
}
/// Validates that `idx` is still valid for the host tables, notably
@ -453,6 +453,12 @@ impl<'a> HostResourceTables<'a> {
match list.get_mut(idx as usize) {
Some(slot) => *slot = gen,
None => {
// Resource handles start at 1, not zero, so push two elements
// for the first resource handle.
if list.is_empty() {
assert_eq!(idx, 1);
list.push(0);
}
assert_eq!(idx as usize, list.len());
list.push(gen);
}
@ -482,7 +488,11 @@ impl<'a> HostResourceTables<'a> {
/// into a guest-local index.
///
/// The `ty` provided is which table to put this into.
pub fn guest_resource_lower_own(&mut self, rep: u32, ty: TypeResourceTableIndex) -> u32 {
pub fn guest_resource_lower_own(
&mut self,
rep: u32,
ty: TypeResourceTableIndex,
) -> Result<u32> {
self.tables.resource_lower_own(Some(ty), rep)
}
@ -495,7 +505,11 @@ impl<'a> HostResourceTables<'a> {
/// into a guest has a special case where `rep` is returned directly if `ty`
/// belongs to the component being lowered into. That property must be
/// handled by the caller of this function.
pub fn guest_resource_lower_borrow(&mut self, rep: u32, ty: TypeResourceTableIndex) -> u32 {
pub fn guest_resource_lower_borrow(
&mut self,
rep: u32,
ty: TypeResourceTableIndex,
) -> Result<u32> {
self.tables.resource_lower_borrow(Some(ty), rep)
}
@ -612,7 +626,7 @@ where
// can move the rep into the guest table.
ResourceState::Index(idx) => cx.host_resource_lift_own(idx)?,
};
Ok(cx.guest_resource_lower_own(t, rep))
cx.guest_resource_lower_own(t, rep)
}
InterfaceType::Borrow(t) => {
let rep = match self.state.get() {
@ -632,7 +646,7 @@ where
//
// Afterwards this is the same as the `idx` case below.
ResourceState::NotInTable => {
let idx = cx.host_resource_lower_own(self.rep);
let idx = cx.host_resource_lower_own(self.rep)?;
let prev = self.state.swap(ResourceState::Index(idx));
assert_eq!(prev, ResourceState::NotInTable);
cx.host_resource_lift_borrow(idx)?
@ -642,7 +656,7 @@ where
// out of the table with borrow-tracking employed.
ResourceState::Index(idx) => cx.host_resource_lift_borrow(idx)?,
};
Ok(cx.guest_resource_lower_borrow(t, rep))
cx.guest_resource_lower_borrow(t, rep)
}
_ => bad_type_info(),
}
@ -879,9 +893,9 @@ impl ResourceAny {
let mut tables = HostResourceTables::new_host(store.0);
let (idx, own_state) = match state.get() {
ResourceState::Borrow => (tables.host_resource_lower_borrow(rep), None),
ResourceState::Borrow => (tables.host_resource_lower_borrow(rep)?, None),
ResourceState::NotInTable => {
let idx = tables.host_resource_lower_own(rep);
let idx = tables.host_resource_lower_own(rep)?;
(
idx,
Some(OwnState {
@ -1025,14 +1039,14 @@ impl ResourceAny {
bail!("mismatched resource types");
}
let rep = cx.host_resource_lift_own(self.idx)?;
Ok(cx.guest_resource_lower_own(t, rep))
cx.guest_resource_lower_own(t, rep)
}
InterfaceType::Borrow(t) => {
if cx.resource_type(t) != self.ty {
bail!("mismatched resource types");
}
let rep = cx.host_resource_lift_borrow(self.idx)?;
Ok(cx.guest_resource_lower_borrow(t, rep))
cx.guest_resource_lower_borrow(t, rep)
}
_ => bad_type_info(),
}
@ -1043,7 +1057,7 @@ impl ResourceAny {
InterfaceType::Own(t) => {
let ty = cx.resource_type(t);
let (rep, dtor, flags) = cx.guest_resource_lift_own(t, index)?;
let idx = cx.host_resource_lower_own(rep);
let idx = cx.host_resource_lower_own(rep)?;
Ok(ResourceAny {
idx,
ty,
@ -1057,7 +1071,7 @@ impl ResourceAny {
InterfaceType::Borrow(t) => {
let ty = cx.resource_type(t);
let rep = cx.guest_resource_lift_borrow(t, index)?;
let idx = cx.host_resource_lower_borrow(rep);
let idx = cx.host_resource_lower_borrow(rep)?;
Ok(ResourceAny {
idx,
ty,

12
tests/all/component_model/resources.rs

@ -233,7 +233,7 @@ fn mismatch_intrinsics() -> Result<()> {
let ctor = i.get_typed_func::<(u32,), (ResourceAny,)>(&mut store, "ctor")?;
assert_eq!(
ctor.call(&mut store, (100,)).unwrap_err().to_string(),
"unknown handle index 0"
"unknown handle index 1"
);
Ok(())
@ -371,7 +371,7 @@ fn drop_guest_twice() -> Result<()> {
assert_eq!(
dtor.call(&mut store, (&t,)).unwrap_err().to_string(),
"unknown handle index 0"
"unknown handle index 1"
);
Ok(())
@ -1250,7 +1250,7 @@ fn pass_guest_back_as_borrow() -> Result<()> {
// Should not be valid to use `resource` again
let err = take.call(&mut store, (&resource,)).unwrap_err();
assert_eq!(err.to_string(), "unknown handle index 0");
assert_eq!(err.to_string(), "unknown handle index 1");
Ok(())
}
@ -1412,9 +1412,9 @@ fn guest_different_host_same() -> Result<()> {
(import "" "drop2" (func $drop2 (param i32)))
(func (export "f") (param i32 i32)
;; separate tables both have initial index of 0
(if (i32.ne (local.get 0) (i32.const 0)) (then (unreachable)))
(if (i32.ne (local.get 1) (i32.const 0)) (then (unreachable)))
;; separate tables both have initial index of 1
(if (i32.ne (local.get 0) (i32.const 1)) (then (unreachable)))
(if (i32.ne (local.get 1) (i32.const 1)) (then (unreachable)))
;; host should end up getting the same resource
(call $f (local.get 0) (local.get 1))

54
tests/misc_testsuite/component-model/resources.wast

@ -14,7 +14,7 @@
(local $r i32)
(local.set $r (call $new (i32.const 100)))
(if (i32.ne (local.get $r) (i32.const 0)) (then (unreachable)))
(if (i32.ne (local.get $r) (i32.const 1)) (then (unreachable)))
(if (i32.ne (call $rep (local.get $r)) (i32.const 100)) (then (unreachable)))
(call $drop (local.get $r))
@ -95,13 +95,13 @@
;; resources assigned sequentially
(local.set $r1 (call $new (i32.const 100)))
(if (i32.ne (local.get $r1) (i32.const 0)) (then (unreachable)))
(if (i32.ne (local.get $r1) (i32.const 1)) (then (unreachable)))
(local.set $r2 (call $new (i32.const 200)))
(if (i32.ne (local.get $r2) (i32.const 1)) (then (unreachable)))
(if (i32.ne (local.get $r2) (i32.const 2)) (then (unreachable)))
(local.set $r3 (call $new (i32.const 300)))
(if (i32.ne (local.get $r3) (i32.const 2)) (then (unreachable)))
(if (i32.ne (local.get $r3) (i32.const 3)) (then (unreachable)))
;; representations all look good
(if (i32.ne (call $rep (local.get $r1)) (i32.const 100)) (then (unreachable)))
@ -113,7 +113,7 @@
(local.set $r2 (call $new (i32.const 400)))
;; should have reused index 1
(if (i32.ne (local.get $r2) (i32.const 1)) (then (unreachable)))
(if (i32.ne (local.get $r2) (i32.const 2)) (then (unreachable)))
;; representations all look good
(if (i32.ne (call $rep (local.get $r1)) (i32.const 100)) (then (unreachable)))
@ -135,13 +135,13 @@
(if (i32.ne (call $rep (local.get $r3)) (i32.const 700)) (then (unreachable)))
;; indices should be lifo
(if (i32.ne (local.get $r1) (i32.const 2)) (then (unreachable)))
(if (i32.ne (local.get $r2) (i32.const 1)) (then (unreachable)))
(if (i32.ne (local.get $r3) (i32.const 0)) (then (unreachable)))
(if (i32.ne (local.get $r1) (i32.const 3)) (then (unreachable)))
(if (i32.ne (local.get $r2) (i32.const 2)) (then (unreachable)))
(if (i32.ne (local.get $r3) (i32.const 1)) (then (unreachable)))
;; bump one more time
(local.set $r4 (call $new (i32.const 800)))
(if (i32.ne (local.get $r4) (i32.const 3)) (then (unreachable)))
(if (i32.ne (local.get $r4) (i32.const 4)) (then (unreachable)))
;; deallocate everything
(call $drop (local.get $r1))
@ -241,13 +241,13 @@
(local.set $r2 (call $ctor (i32.const 200)))
;; assert r1/r2 are sequential
(if (i32.ne (local.get $r1) (i32.const 0)) (then (unreachable)))
(if (i32.ne (local.get $r2) (i32.const 1)) (then (unreachable)))
(if (i32.ne (local.get $r1) (i32.const 1)) (then (unreachable)))
(if (i32.ne (local.get $r2) (i32.const 2)) (then (unreachable)))
;; reallocate r1 and it should be reassigned the same index
(call $drop (local.get $r1))
(local.set $r1 (call $ctor (i32.const 300)))
(if (i32.ne (local.get $r1) (i32.const 0)) (then (unreachable)))
(if (i32.ne (local.get $r1) (i32.const 1)) (then (unreachable)))
;; internal values should match
(call $assert (local.get $r1) (i32.const 300))
@ -443,7 +443,7 @@
(import "" "ctor" (func $ctor (param i32) (result i32)))
(func $start
(if (i32.ne (call $ctor (i32.const 100)) (i32.const 0)) (then (unreachable)))
(if (i32.ne (call $ctor (i32.const 100)) (i32.const 1)) (then (unreachable)))
)
(start $start)
)
@ -464,7 +464,7 @@
(import "" "drop" (func $drop (param i32)))
(func (export "f")
(call $drop (i32.const 0))
(call $drop (i32.const 1))
)
)
(core instance $i (instantiate $m
@ -492,10 +492,10 @@
(import "" "drop" (func $drop (param i32)))
(func (export "alloc")
(if (i32.ne (call $ctor (i32.const 100)) (i32.const 0)) (then (unreachable)))
(if (i32.ne (call $ctor (i32.const 100)) (i32.const 1)) (then (unreachable)))
)
(func (export "dealloc")
(call $drop (i32.const 0))
(call $drop (i32.const 1))
)
)
(core instance $i (instantiate $m
@ -552,10 +552,10 @@
(import "" "drop" (func $drop (param i32)))
(func (export "alloc")
(if (i32.ne (call $ctor (i32.const 100)) (i32.const 0)) (then (unreachable)))
(if (i32.ne (call $ctor (i32.const 100)) (i32.const 1)) (then (unreachable)))
)
(func (export "dealloc")
(call $drop (i32.const 0))
(call $drop (i32.const 1))
)
)
(core instance $i (instantiate $m
@ -617,12 +617,12 @@
(call $drop2 (call $new2 (i32.const 104)))
;; should be referencing the same namespace
(if (i32.ne (call $new1 (i32.const 105)) (i32.const 0)) (then (unreachable)))
(if (i32.ne (call $new2 (i32.const 105)) (i32.const 1)) (then (unreachable)))
(if (i32.ne (call $new1 (i32.const 105)) (i32.const 1)) (then (unreachable)))
(if (i32.ne (call $new2 (i32.const 105)) (i32.const 2)) (then (unreachable)))
;; use different drops out of order
(call $drop2 (i32.const 0))
(call $drop1 (i32.const 1))
(call $drop2 (i32.const 1))
(call $drop1 (i32.const 2))
)
(start $start)
@ -701,8 +701,8 @@
(local.set $r2 (call $new2 (i32.const 200)))
;; both should be index 0
(if (i32.ne (local.get $r1) (i32.const 0)) (then (unreachable)))
(if (i32.ne (local.get $r2) (i32.const 0)) (then (unreachable)))
(if (i32.ne (local.get $r1) (i32.const 1)) (then (unreachable)))
(if (i32.ne (local.get $r2) (i32.const 1)) (then (unreachable)))
;; nothing should be dropped yet
(if (i32.ne (call $drops) (i32.const 0)) (then (unreachable)))
@ -866,7 +866,7 @@
;; table should be empty at this point, so a fresh allocation should get
;; index 0
(if (i32.ne (call $ctor (i32.const 600)) (i32.const 0)) (then (unreachable)))
(if (i32.ne (call $ctor (i32.const 600)) (i32.const 1)) (then (unreachable)))
)
(start $start)
@ -1024,8 +1024,8 @@
(local.set $r1 (call $ctor (i32.const 100)))
(local.set $r2 (call $ctor (i32.const 200)))
(if (i32.ne (local.get $r1) (i32.const 0)) (then (unreachable)))
(if (i32.ne (local.get $r2) (i32.const 1)) (then (unreachable)))
(if (i32.ne (local.get $r1) (i32.const 1)) (then (unreachable)))
(if (i32.ne (local.get $r2) (i32.const 2)) (then (unreachable)))
(call $assert-borrow (local.get $r2) (i32.const 200))
(call $assert-borrow (local.get $r1) (i32.const 100))

Loading…
Cancel
Save