viridia added the bug label to Issue #9848.
viridia opened issue #9848:
I'm getting this error:
types incompatible: expected type `(func (param (ref array (engine 5))))`, found type `(func (param (ref array)))`
The wasm looks like this:
(type $String (;3;) (array i8)) (type $debug.type (;4;) (func (param (ref $String)))) (import "host" "debug" (func $debug (;0;) (type $debug.type)))
And I'm registering the function like this:
let engine = wasmtime::Engine::new(&config).unwrap(); let store = wasmtime::Store::<StoreData>::new(&engine, ()); let mut linker = wasmtime::Linker::new(&engine); linker .func_wrap( "host", "debug", |caller: Caller<'_, StoreData>, param: Rooted<ArrayRef>| { // ... }, ) .unwrap();
Test Case
(module $hello.saga (type $hello.type (;0;) (func (result i32))) (type $add.type (;1;) (func (param i32) (result i32))) (type $name.type (;2;) (func)) (type $String (;3;) (array (mut i8))) (type $debug.type (;4;) (func (param (ref $String)))) (import "host" "debug" (func $debug (;0;) (type $debug.type))) (export "hello" (func $hello)) (func $hello (;1;) (type $hello.type) (result i32) i32.const 1 call $add i32.const 2 i32.add f32.const 0x1.8p+1 (;=3;) i32.trunc_f32_s i32.add return ) (func $add (;2;) (type $add.type) (param i32) (result i32) (local i32) i32.const 1 local.set 1 local.get 0 local.get 1 i32.add return ) (func $name (;3;) (type $name.type) i32.const 0 i32.const 15 array.new_data $String 0 call $debug ) (data (;0;) "Hello from SAGA") )
Steps to Reproduce
Create an Engine and Linker.
Bind a function which accepts an ArrayRef as a parameter.
Attempt to load the script.Expected Results
Script should load without error (it loads if I delete the code that calls the host func).
Actual Results
Error when I attempt to instantiate the script.
Versions and Environment
Wasmtime version or commit:
{ version = "27.0.0", features = ["gc", "gc-drc"] }
Operating system: OS X
Architecture: M2
alexcrichton assigned fitzgen to issue #9848.
fitzgen commented on issue #9848:
Thanks for filing an issue. I won't have time to look into this for a while, unfortunately, but I appreciate getting it on the record for posterity.
fitzgen added the wasmtime:ref-types label to Issue #9848.
fitzgen added the wasm-proposal:gc label to Issue #9848.
github-actions[bot] commented on issue #9848:
Subscribe to Label Action
cc @fitzgen
<details>
This issue or pull request has been labeled: "wasmtime:ref-types"Thus the following users have been cc'd because of the following labels:
- fitzgen: wasmtime:ref-types
To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.
Learn more.
</details>
fitzgen commented on issue #9848:
FWIW, this is probably related to the
WasmTy
implementation forArrayRef
fitzgen-f5 commented on issue #9848:
Hm so I converted this into a test in
tests/all/arrays.rs
:#[test] fn instantiate_with_func_that_takes_array() -> Result<()> { let mut store = gc_store()?; let module = Module::new( store.engine(), r#" (module (type $String (;3;) (array i8)) (type $debug.type (;4;) (func (param (ref $String)))) (import "host" "debug" (func $debug (;0;) (type $debug.type))) ) "#, )?; let mut linker = wasmtime::Linker::new(store.engine()); linker.func_wrap("host", "debug", |_: Caller<'_, ()>, _: Rooted<ArrayRef>| {})?; let _instance = linker.instantiate(&mut store, &module)?; Ok(()) }
And I am indeed reproducing the error:
Error: incompatible import type for `host::debug` Caused by: types incompatible: expected type `(func (param (ref array (engine 0))))`, found type `(func (param (ref array)))`
However, I'm not convinced that this is actually incorrect behavior. The type of the wrapped function is
(func (param (ref array)))
which indeed is not a subtype of(func (param (ref array $String)))
and so theMatchCx
used to typecheck instance linking is correctly yielding a type error.If we make
$debug.type
non-final
(which it implicitly was originally) and switch from usinglinker.func_wrap
toFunc::new
and explicitly provide a function whose type is a subtype of$debug.type
, then the test does indeed pass:#[test] fn instantiate_with_func_that_takes_array() -> Result<()> { let mut store = gc_store()?; let module = Module::new( store.engine(), r#" (module (type $String (;3;) (array i8)) (type $debug.type (;4;) (sub (func (param (ref $String))))) (import "host" "debug" (func $debug (;0;) (type $debug.type))) ) "#, )?; let mut linker = wasmtime::Linker::new(store.engine()); let string_ty = ArrayType::new( store.engine(), FieldType::new(Mutability::Const, StorageType::I8), ); let sup_ty = FuncType::with_finality_and_supertype( store.engine(), Finality::NonFinal, None, [ValType::Ref(RefType::new( false, HeapType::ConcreteArray(string_ty), ))], None, )?; let sub_ty = FuncType::with_finality_and_supertype( store.engine(), Finality::NonFinal, Some(&sup_ty), [ValType::Ref(RefType::new(false, HeapType::Array))], None, )?; linker.func_new("host", "debug", sub_ty, |_caller, _args, _rets| Ok(()))?; let _instance = linker.instantiate(&mut store, &module)?; Ok(()) }
test arrays::instantiate_with_func_that_takes_array ... ok
That is all technically correct, but I'm not sure if it is what we ultimately want. It seems very reasonable to want to write code like in the OP, even if it technically doesn't match subtyping rules (but would if the types involved were non-
final
and declared to be subtypes; basically those that structurally match but are not actually declared subtypes).One extra twist is that I think we actually used to allow this kind of thing, but I think I removed it when getting the GC wast tests passing, since (IIRC) there were some
assert_unlinkable
pragmas exercising this stuff. Makes sense to disallow in that context, but maybe not in the context of awasmtime::Linker
in the host? Not totally convinced either way.
alexcrichton commented on issue #9848:
I always forget the directionality of subtyping but I'm confused why this doesn't pass the subtyping check. If I ask for
(ref array)
and you give me(ref $specific_array)
isn't that ok? Shouldn't the functions be subtypes here? (or is the "final" bit coming into play, I have less of a grasp on that)Otherwise though I'd agree that we want something
Func::wrap
-related to work here. That's much more efficient, works better with bindings generators, and is generally more ergonomic. How best to thread that needle though I'm not sure either.
fitzgen commented on issue #9848:
The issue is twofold:
First, that the wrapped function's type, when fully desugared, is
(sub final (func (param (ref array))))
which does not declare itself to be a subtype of
$debug.type
and therefore is not a subtype of$debug.type
even if the underlying parameter and result types match.And second, that
(type $debug.type (func (param (ref $String)))
desugars to a
final
type(type $debug.type (sub final (param (ref $String))))
so no other function type can be a subtype of
$debug.type
as written, even if it tried.This all makes sense inside the world of pure Wasm, but from a host API and developer niceties point of view, it becomes pretty frustrating and leads to bad developer experience :-/
fitzgen edited a comment on issue #9848:
The issue is twofold:
First, that the wrapped function's type, when fully desugared, is
(sub final (func (param (ref array))))
which does not declare itself to be a subtype of
$debug.type
and therefore is not a subtype of$debug.type
even if the underlying parameter and result types match.And second, that
(type $debug.type (func (param (ref $String)))
desugars to a
final
type(type $debug.type (sub final (func (param (ref $String)))))
so no other function type can be a subtype of
$debug.type
as written, even if it tried.This all makes sense inside the world of pure Wasm, but from a host API and developer niceties point of view, it becomes pretty frustrating and leads to bad developer experience :-/
alexcrichton commented on issue #9848:
Ah I see, I was mistakenly thinking about the
(sub ...)
in the wrong place!I thought we had something though which was testing that as long as the top type was the same then the host-level type-check passed? And then we optionally included a runtime type-check if the actual type was different from the top type? I'm probably misremembering here...
Another option perhaps is to do something different for wasm gc than we do for normal scalars. Some sort of argument passed to
Func::wrap_new_name
which is acquired via constructing the type or parsing it out somehow? Maybe that's just a slightly more ergonomicFunc::new
in that case ...I suppose put another way one thing we could do is to change
Func::new
to take any closure, not just the signature today. The function type is then used to perform a runtime type-check against the closure's static type and for today's signature it'd be a noop but you could pass a more specialized signature likeFunc::wrap
. That specialized signature would have a slightly different type-check than the subtyping checks we have today?In any case I'm just musing here...
Last updated: Jan 24 2025 at 00:11 UTC