Stream: git-wasmtime

Topic: wasmtime / issue #9848 Error linking to function that tak...


view this post on Zulip Wasmtime GitHub notifications bot (Dec 18 2024 at 01:08):

viridia added the bug label to Issue #9848.

view this post on Zulip Wasmtime GitHub notifications bot (Dec 18 2024 at 01:08):

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

view this post on Zulip Wasmtime GitHub notifications bot (Dec 18 2024 at 15:54):

alexcrichton assigned fitzgen to issue #9848.

view this post on Zulip Wasmtime GitHub notifications bot (Dec 18 2024 at 17:16):

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.

view this post on Zulip Wasmtime GitHub notifications bot (Dec 19 2024 at 17:51):

fitzgen added the wasmtime:ref-types label to Issue #9848.

view this post on Zulip Wasmtime GitHub notifications bot (Dec 19 2024 at 17:51):

fitzgen added the wasm-proposal:gc label to Issue #9848.

view this post on Zulip Wasmtime GitHub notifications bot (Dec 19 2024 at 17:52):

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:

To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.

Learn more.
</details>

view this post on Zulip Wasmtime GitHub notifications bot (Dec 19 2024 at 17:53):

fitzgen commented on issue #9848:

FWIW, this is probably related to the WasmTy implementation for ArrayRef

view this post on Zulip Wasmtime GitHub notifications bot (Jan 23 2025 at 01:04):

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 the MatchCx 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 using linker.func_wrap to Func::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 a wasmtime::Linker in the host? Not totally convinced either way.

view this post on Zulip Wasmtime GitHub notifications bot (Jan 23 2025 at 17:58):

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.

view this post on Zulip Wasmtime GitHub notifications bot (Jan 23 2025 at 18:10):

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 :-/

view this post on Zulip Wasmtime GitHub notifications bot (Jan 23 2025 at 18:11):

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 :-/

view this post on Zulip Wasmtime GitHub notifications bot (Jan 23 2025 at 20:22):

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 ergonomic Func::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 like Func::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