WebAssembly / design

WebAssembly Design Documents
http://webassembly.org
Apache License 2.0
11.4k stars 696 forks source link

[js-api] JS-Wasm Interchangeability #1353

Open RossTate opened 4 years ago

RossTate commented 4 years ago

A question that will come up over and over again is how interchangeable do we want JS and wasm to be. That is, for programs that could just as easily be implemented in either language, how easy do we want it to be for someone to swap an existing JS implementation with an analogous wasm implementation. It would be useful to establish some high-level guidance here, but there's a lot of fuzziness in the previous statement, so I thought it'd be best to start off with something very concrete and yet still quite informative.

Consider the following copycat wasm module:

(module
  (func $cat (import "js" "cat") (param externref) (result externref))
  (func (export "copy") (param externref) (result externref)
    call $cat (local.get 0)))

copycat just imports a unary function and exports a unary function that simply calls the imported function.

Now suppose we have a JS functional value cat, and suppose copy is the JS value of the exported function resulting from instantiating copycat with cat (and, for simplicity, suppose it is the only live reference to the module instance). With all that setup, here's my question:

Should (x) => cat(x) and (x) => copy(x) be semantically equivalent JS expressions, given that both are lambda expressions that just feed their input (eventually) to cat and (eventually) return the resulting output?

My answer would be "yes": if we can't guarantee equivalence in even this very simple case, then it will be difficult for people to reliably migrate their much-more-complex JS programs to wasm. Simple gotchas here will become complex gotchas for them, and ideally people don't have to be wasm experts to migrate their code reliably.

But that's just my own thinking. What would be helpful to know is what the broader group believes, so please share your thoughts!

backes commented 4 years ago

Should (x) => cat(x) and (x) => copy(x) be semantically equivalent JS expressions, given that both are lambda expressions that just feed their input (eventually) to cat and (eventually) return the resulting output?

Yes, this should be semantically equivalent, for this particular example. I think this follows from the fact that externref does not cause any coercion when transferred between JS and Wasm.

For most other types the answer will be different.

fgmccabe commented 4 years ago

As with distributed computing, we should not attempt to make inter-language and inter-module interoperation 'invisible'. There are simply too many 'gotchas' to be able to do this in all cases.

If you are crossing a language boundary you have to know it.

I think that that your 'migration principle' needs to be qualified: someone who wishes to migrate a function from one language/module/owner to another needs to prove that that preserves semantics.

What that means, IMO, is that we need clarity on what can and cannot be expected to happen.

Interface Types draws a line in the sand here: you can communicate certain types of values; but there are no expectations of being able to transmit arbitrary values. Additionally, those values you can communicate will be copied (not copying is an optimization that cannot always be guaranteed).

Interface Types also has something to say about exceptions: only those exceptions that are catchable by the adapters will be communicated; again communicated by coercion - not just passed through.

Finally, Interface Types also has something to say about state: the only shared state sanctioned is that by sharing resources (technically by communicating resource identifiers). There is no implicit sharing of state. Two modules can only affect each other's state by invoking functions on each other.

Perhaps these add up to a set of criteria for permissible forms of code migration ... although I can think of other criteria that we have not factored into - such as performance related constraints.

RossTate commented 4 years ago

@fgmccabe, I understand the high-level points you are making, but what I don't know is what you think about this particular example. I'm not trying to generalize at this point (e.g. to other types, like @backes points out). This example alone will already be informative.

fgmccabe commented 4 years ago

depends on what you want to do about exceptions, performance, any implicit shared state etc. etc. I prefer to design top-down, implement bottom-up.

RossTate commented 4 years ago

Okay, got it. So it sounds like your opinion is that, even though these two programs are semantically equivalent currently (as @backes points out), you do not believe developers should expect them to remain equivalent. (I'm talking about semantic equivalence, i.e. internally observable behavior disregarding timing channels and the like, which generally disregards performance.)

fgmccabe commented 4 years ago

TBH, I am not certain that they are equivalent today. What happens if cat throws? What about the arguments to the function itself? Today, they are only equivalent if the underlying function takes integers or floats.

RossTate commented 4 years ago

Sorry, by currently I meant with the Reference Types proposal (so that externref is defined).

What about the arguments to the function itself? Today, they are only equivalent if the underlying function takes integers or floats.

I believe the use of externref addresses this by treating JS values as opaque references in wasm.

What happens if cat throws?

From what I can tell of the JS API (create a host function), if copy calls cat and cat throws, then I believe the effect is that copy will throw the same value cat threw. So still equivalent behavior.

rossberg commented 4 years ago

I am concerned that the idea of not allowing control flow transfer across interface boundaries (which is the what catching and coercing exceptions really means) would prevent most interesting uses of coroutines/effect handlers and thus any control abstractions implemented with them in a mixed language setting. This is a desirable feature to use cross-language, as Alon's proposal for await demonstrated. If you can't "throw" an effect across an interface boundary, then comparable scenarios simply wouldn't work.

taralx commented 4 years ago

I think in a world with externref and exnref we are good with interchangeability. Can anyone think of a scenario where you couldn't translate JS code into wasm code and get equivalent execution?

RossTate commented 4 years ago

@rossberg, am I correct in interpreting your words to be suggestive that the two should be equivalent?

@taralx, there's a separate question of whether wasm can express all of JS. At a high level, my question here is whether the two should be interchangeable whenever they overlap. But in order to avoid the question of what does it mean in general to overlap (which @fgmccabe points out is itself non-trivial), I'm focusing on just this concrete example.

fgmccabe commented 4 years ago

@rossberg I do not follow the logic of "which is the what catching and coercing exceptions really means". If you catch, coerce and rethrow, how have you not communicated the exceptional nature of your control flow. OTOH, why make exceptions special? Everything else is being coerced (arguments and return values).

RossTate commented 4 years ago

Part of the reason I'm asking this question is because I realized it would help inform how to design the JS API for exception handling. That API has not been designed, though, so we'll be going in circles if we discuss its (not-yet-known) specifics here.

Everything else is being coerced (arguments and return values).

Nothing is being coerced with externref.

If you catch, coerce and rethrow, how have you not communicated the exceptional nature of your control flow. OTOH, why make exceptions special?

There are ways to design inter-language coercions such that the code bases on the two sides of a composition boundary cannot tell which language the code base on the other side is implemented in. One thing I am trying to get guidance on is whether people would like to strive for such designs so that JS and wasm can be interchangeable in various circumstances (which, right now, is just the concrete copycat example).

rossberg commented 4 years ago

@RossTate, not necessarily in the concrete, but abstractly they are clearly related. If we already disallow exceptions, then obviously it is hard to justify allowing the more general construct (or vice versa, if we want to allow effect invocation, then why forbid exceptions, which are mostly a special case?).

@fgmccabe, the problem is that this only works easily for the one-way case, which is exceptions. In the generalised case of bidirectional transfer, as with effects or resumable exception, you would lose the continuation if you were to intercept the exception in the middle. You could try to propagate manually in both directions, but that would be rather inefficient and cumbersome, possibly rendering the use of such abstractions unattractive.

Another problem is that translating exceptions/effects at the boundary requires exposing all exceptions/effects, and both sides understanding them. This may or may not be desirable in different cases. One appeal of exceptions and non-local control transfer is that it can tunnel through other code without that code requiring knowledge of their definition. A simple example would be an iterator function that the invoking code wants to complete early in some cases (or alternatively, suspend). You can use an exception/effect for this without the iterator function having to know that exception.

RossTate commented 4 years ago

@rossberg, I am having a hard time understanding you here, and I worry it's because of some underlying miscommunication. If it helps to clarify things, I believe these two programs are equivalent even with the hypothetical extensions to WebAssembly in WebAssembly/exception-handling#105. That is, the equivalence doesn't rely in disallowing exceptions or continuations or the like.

rossberg commented 4 years ago

@RossTate, sorry for the confusion, my comment is tangential to the OP. It was merely a follow-up on this paragraph about exceptions from @fgmccabe's comment above:

Interface Types also has something to say about exceptions: only those exceptions that are catchable by the adapters will be communicated; again communicated by coercion - not just passed through.