Closed bakkot closed 9 months ago
I think we've been evaluating builtins as more like a collection of additional importable Wasm operations, instead of as JS functions (I'm not saying we should be doing this, just trying to explicitly surface what I think have been our assumptions). We don't normally expect that newly introduced Wasm operations are polyfillable inline - a Web site wanting to support old browsers generally needs to just provide a version of the Wasm module without the unsupported operations.
The analogy here isn't totally precise, as a Wasm module attempting to import builtins could still be used in an older browser if the imports were polyfilled (with the caveats about postMessage
above), but I think we would consider this situation "better" than the normal expectations of polyfillability. Also, we're slowly working towards a proposal for Wasm to get shared
functions, which if successful might allow a more seamless polyfill in future situations like this.
Two immediate thoughts:
Maybe we would want to restrict built-ins from being directly callable from JS, since they're intended to be compiled to inline machine code in Wasm as opposed to a full function call?
Within the Wasm module, do we want to explicitly mark these import types as shared
(e.g. in the binary)? This would seem to better fit the direction of travel if we eventually aim to have polyfills using shared
Wasm functions.
Ah, part of my concern stemmed from a misunderstanding that these would be marking existing things (like String.fromCodePoints
) as shareable, rather than providing new things which are intended only to be used from wasm. I agree this isn't that much of a concern if these are wasm-only.
Maybe we would want to restrict built-ins from being directly callable from JS, since they're intended to be compiled to inline machine code in Wasm as opposed to a full function call?
As builtins are intended to be instances of WebAssembly.Function
, it'd be a bit challenging to prevent them from being called by JS. I would guess it'd be strictly more work for an engine to do?
Within the Wasm module, do we want to explicitly mark these import types as
shared
(e.g. in the binary)? This would seem to better fit the direction of travel if we eventually aim to have polyfills usingshared
Wasm functions.
What's the motivation for this? Allowing the imported funcs to have a shareable type in the future? My assumption was that shared
<: noshared
and so imports could request a non-shared func and receive a shared func.
My assumption was that
shared <: noshared
~Hmm - this definitely can't be true for anything mutable that can store refs like a global,~
EDIT: no longer sure about the above, so re-writing my justification :)
Currently this isn't true for shared
memories since engines might want to compile shared
and noshared
memories with different bounds checking strategies, but maybe we could get away with it for functions? I'd need to think through all the implications. By default I think the assumption should be that this doesn't hold in the Wasm type system.
After thinking things through further though, I think it's ok for a pre-import provided at compile time to satisfy both shared
and noshared
imports, even if we don't have shared <: noshared
when type-checking imports provided at instantiation-time, so long as pre-imports are whitelisted builtins rather than arbitrary user-defined functions. The idea is essentially that because of the whitelist, and the fact that builtins are restricted in the way they can mess with ambient state, we know that a builtin can be typed both as shared
and noshared
from the Wasm pov, even if user-defined functions have to pick one world or the other.
EDIT2:
or put another way, we could think of the "type hierarchy" for sharedness as like this
shared noshared
| |
-----------------
|
either
and currently only builtins can be typed with either
, although maybe one day we could interpret Wasm functions with a shared
annotation/bit set as typeable with either
as well if it's clear this wouldn't damage implementation strategies. I think it would be hard for us to allow an either
-typed memory with current implementations.
as opposed to
noshared
|
shared
which I think can't be true in general right now
The latest version of this proposal (after #8 landed) no longer has compile-time imports but uses opt-in host provided imports.
In this world, compiling a module with js-string-builtins enabled means that the host will provide the js-string imports. The resulting module is shareable, because any host provided import is reasonably expected to be available on all threads.
If you want to polyfill js-string-builtins for browser that don't support it, you would not opt-in to js-string-builtins, and instead provide your polyfill module during instantiation. For a shared module (i.e. postMessaged), you can provide a polyfill specific to the web worker when instantiating it.
If I'm reading this right, this would make certain built-in functions
postMessage
able.That makes those functions impossible to polyfill. Existing features sometimes need to be polyfilled when their behavior is updated - e.g. when
AbortController
was added,fetch
needed to be polyfilled on old browsers to support the new behavior.User-defined functions cannot be
postMessage
'd. So making any functionpostMessage
able means it cannot ever be fully polyfilled. Also, the existence of a polyfill for something with a compile-time import would either cause a LinkError or preventpostMessage
ing the module. That seems bad.It also means such functions can't be virtualized, which is also bad, though for pure computation it matters less.