Skip to content
This repository was archived by the owner on Apr 25, 2025. It is now read-only.
This repository was archived by the owner on Apr 25, 2025. It is now read-only.

Should we reverse the externref <: anyref relation? #256

@jakobkummerow

Description

@jakobkummerow

Maybe this is a bad idea... it just occurred to me while thinking about issue #254, so I thought I'd write it down.

The current design is (skipping the ...ref parts of names for brevity):

        any
      /  |   \
extern   eq   func
        /  \
     i31   data

Which causes at least the following difficulties:

  • Given an externref, one can (implicitly) upcast to anyref, and check ref.{is_func,is_data,is_i31} and even downcast accordingly, which violates the notion that externrefs are opaque.
  • By using externref on its interface (e.g. to consume function arguments), a module can demand to be passed objects that can't be constructed by another module, as there is no Wasm way to construct externref values (they can only come from the host). While we could argue that it is each module's responsibility not to be silly, people have also argued that it's a key requirement that a module's functions that assumed to be called with host-provided externref values can also be called from another module with fake/polyfilled/wrapped/layered, i.e. module-constructed values.

I think one way to address these difficulties could be to swap the subtyping relationship, and make the intended-to-be-opaque type the top type:

 extern (maybe better named "opaqueref" then?)
   |
  any  (maybe better named... uh... "bikeshedref" then?)
  / \
eq  func
| \
...

Notably, ref.is_* and ref.cast would continue to consume anyref values, which addresses the first difficulty mentioned above. Semantics of externref is "I don't know or care what this is, and that implies I won't try to inspect it" (i.e. closer to void* in C than to Object in Java, though of course both of those comparisons are rather imperfect because Wasm is neither C nor Java).

The second difficulty mentioned above is addressed rather trivially by continuing to allow implicit upcasts to supertypes. That means to call a function that consumes an externref, the caller can pass any anyref it happens to have handy, or a struct/array it created, etc.

This wouldn't make fat pointers or other non-unified representations any more feasible than they were before, but as discussed elsewhere any dreams of doing that are very unlikely to become reality in engines anyway, even if the type system allowed it.

(Side note: whether funcref should really be non-eq is a question that might deserve separate discussion. I'm totally on board with not wanting JavaScript-style distinguishable-object semantics for distinct references to the same function. But the opposite, i.e. requiring canonicalization, aka requiring that (ref.eq (ref.func $f0) (ref.func $f0)) always returns true/1, could provide interesting expressiveness (specifically, I'm thinking of toolchain-side optimizations such as building caching/fast-path schemes in userspace), and shouldn't break any scenario that was possible when funcrefs were not ref.eq-comparable. But let's open a new issue if anyone wants to discuss that. It's related here because if we did make that change, then eqref could take the place of anyref, and then we'd at least have a name for the thing.)

Feel free to tell me why that can't work, and close this issue :-)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions