-
Notifications
You must be signed in to change notification settings - Fork 84
Should we reverse the externref <: anyref
relation? #256
Description
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 toanyref
, and checkref.{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 :-)