Expose the JavaScript Exception tag and allow importing it
I believe this is what we discussed in #202
The JS tag has type externref and is exposed on the WebAssembly object, allowing it to be imported. When such exceptions are caught in wasm, the thrown JS object is passed to the catch block as an externref. When such exceptions propagate from wasm to JS, the payload value is thrown directly, rather than being wrapped in a new Exception.
I don't have any particular concerns about the implementation here. We could pack/unpack the exception in a WA.Exception at the JS/Wasm boundary as suggested, but I think modifying the catch handler instead is also worth considering: we would only need to add a check to catch blocks where the tag type is externref. Otherwise we know statically that it can't match the JSTag. If the catch tag is the global JSTag and the reference is not a WA.Exception, we push the reference directly on the operand stack. That would remove the need for packing/unpacking at the language boundary and in the catch handler.
Yes, this basically wraps/unwraps as you said. Because of that I don't think we need special handling for catch, because it would be the same as catching an exception with any other imported tag with an externref type. The existing spec is basically written by wrapping/unwrapping at the boundary, so it's a small change I think for an implementation you could modify the catch handler as @thibaudmichaud said, and it would still be compliant with this spec. If there are no catch handlers the exception would just propagate through (as if it had been wrapped and then unwrapped). Line 1439 (in this PR) also mentions that.
I don't have any particular concerns about the implementation here. We could pack/unpack the exception in a
WA.Exceptionat the JS/Wasm boundary as suggested, but I think modifying the catch handler instead is also worth considering: we would only need to add a check to catch blocks where the tag type isexternref. Otherwise we know statically that it can't match the JSTag. If the catch tag is the global JSTag and the reference is not aWA.Exception, we push the reference directly on the operand stack. That would remove the need for packing/unpacking at the language boundary and in the catch handler.
I think this can be treated as internal implementation / optimization details, and this should be still compliant with what @dschuff wrote. Right?
On second thought I think my suggestion doesn't work. If JS explicitly creates and throws a WA.Exception(WA.JSTag, error) and this exception is not handled by wasm, it should be automatically unpacked when we unwind the export according to this spec. That would not happen with my implementation strategy.
@dschuff @thibaudmichaud As discussed earlier, we should rebase this to the new EH API because this is a requirement for our partners.
Rebasing this on the new proposal has some implications so I would like to clarify a few points.
In the old proposal, the implicit wrapping/unwrapping of the JS exception into a WA.Exception at the boundary was only observable through the catch block, using the special JS tag. This allowed us to implement this as a special case of the catch block, and avoid the implicit wrapping/unwrapping (as discussed earlier in this thread).
With the new proposal, it sounds like the WA.Exception could also be observed using catch(_all)_ref. Is that correct? E.g. catching a JS exception with catch_ref will push two references: the WA.Exception (created implicitly at the boundary) and the JS exception (the externref package).
And is it correct that without this PR, catch(_all)_ref would push the original JS exception as the exnref? This contrasts with other places where we throw a type error when we try to initialize an exnref from a JS value (import/export, globals, etc.), so I wonder if this is expected regardless of the special JS tag.
Logically, the exnref of a JS exception is a wrapped value. Internally, it could be the unwrapped JS value itself, depending on implementation choices. That is not observable in Wasm. In particular, there is no way to compare the exnref against the externref that logically represents the actual JS value. From the Wasm perspective, the exnref is opaque.
Moreover, this exnref cannot be passed pack to JS, so JS cannot observe its relation to the JS value either. (That's why we need the type error at the boundary.)
Taken together, this should imply that the previous special-casing still works. An engine is free to represent a JS-exception exnref in whatever way it sees fit, whether wrapped or unwrapped. The only requirement is that throw_ref handles it accordingly.
I've rebased this against #301 to make it easier to review. One aspect becomes trivial because #301 already handles allocating the JS tag. So this just becomes about the right way to put it on the WebAssembly namespace. In that sense it becomes just like the wasm-specific exception classes, so I refactored that part. It looks like @Ms2ger authored that, WDYT?
If the attribute works (and I don't see why it wouldn't), we should prefer it over further manual adjustments to the "create a namespace object" algorithm. You'll need to add the getter steps, but those should be trivial.
Agreed; mostly I was wondering how the "create a namespace" bit that we have for the exceptions got to be the way it was in the first place. And I also don't see why the attribute description shouldn't work; byt what exactly do we mean by "work" here? If we write the attribute that way and write a getter, it seems like this is sufficient to describe the desired behavior but I don't have much experience with this kind of spec.
Unfortunately I don't have time to follow all the changes in detail, but some comments below.
This change is now pretty trivial; But the exnref change is fairly large, so I'll wait for reviews from @aheejin and @rossberg on that, but won't block on yours.
If the attribute works (and I don't see why it wouldn't), we should prefer it over further manual adjustments to the "create a namespace object" algorithm. You'll need to add the getter steps, but those should be trivial.
Agreed; mostly I was wondering how the "create a namespace" bit that we have for the exceptions got to be the way it was in the first place. And I also don't see why the attribute description shouldn't work; byt what exactly do we mean by "work" here? If we write the attribute that way and write a getter, it seems like this is sufficient to describe the desired behavior but I don't have much experience with this kind of spec.
The reason for the custom algorithm is that the exception classes are modeled after the ones that exist in JavaScript itself, and those couldn't be specified in WebIDL. The attribute should be fine, though; I was just wondering if you discovered an issue that lead you to put in the custom algorithm instead.
The reason for the custom algorithm is that the exception classes are modeled after the ones that exist in JavaScript itself, and those couldn't be specified in WebIDL. The attribute should be fine, though; I was just wondering if you discovered an issue that lead you to put in the custom algorithm instead.
Got it. No, I didn't discover any issues; the attribute is now back, with a getter.
Continuing the discussion from https://github.com/WebAssembly/exception-handling/pull/301#discussion_r1560111669
if you created a WebAssembly.Exception(WebAssembly.JSTag, payload) and threw it into wasm, it would be treated as a wasm exception on entry and unrapped; and then when it came back out to JS it would not be rewrapped but come out as just payload
It would not be unwrapped on entry (with the optimization I was discussing), so it would come out as the WebAssembly.Exception + payload. We would have to unwrap it either on entry or on exit to ensure that only the payload comes out.
@thibaudmichaud @sjrd, continuing the discussion from https://github.com/WebAssembly/exception-handling/pull/301#discussion_r1559281361
Continuing the discussion from #301 (comment)
if you created a WebAssembly.Exception(WebAssembly.JSTag, payload) and threw it into wasm, it would be treated as a wasm exception on entry and unrapped; and then when it came back out to JS it would not be rewrapped but come out as just payload
It would not be unwrapped on entry (with the optimization I was discussing), so it would come out as the WebAssembly.Exception + payload. We would have to unwrap it either on entry or on exit to ensure that only the payload comes out.
Ok, I don't mind adding this restriction. We can always relax it later if we want.
SGTM. For the same reason (and to be consistent) the restriction should also apply to throw-ing with the JSTag in wasm.
I could imagine that being actually useful though? If you have a JS object stored in an externref in wasm, I can imagine that you might want to throw it, and have the result come out as an unwrapped JS exception. Especially if it's already an Error object or something.
For that matter, if you catch a JS exception in wasm, you'll get an exnref that you can later rethrow with throw_ref and I would certainly expect that to work. Will that cause the same issues?
SGTM. For the same reason (and to be consistency) the restriction should also apply to
throw-ing with the JSTag in wasm.
I disagree. On the Wasm side, this is the only way to throw an exception that will be caught as-is on the JS side. This is important for interoperability with JS, if the JS thing you want to talk to expects specific shapes of exceptions.
The way I see it, by disallowing new WA.Exception(JSTag, x) on the JS side, but allowing throw $jstag on the Wasm side, we establish a strong 1-to-1 correspondence between JS and Wasm exceptions:
- Wasm sees
($jstag, x)if and only if JS seesx - Wasm sees
($not_jstag, x)if and only if JS seesWA.Exception(not_jstag, x)
This is good in terms of implementation because there is a unique representation of exceptions with (by spec) JSTag: the representation that JS exceptions have.
Currently in V8, the catch $jstag must handle 2 possibilities: either no-tag (i.e., JS exception) or with-tag which is JSTag. With what I propose, only the former possibility remains. It does include one more code path for throw, but no more than disallowing throw $jstag in the first place.
Note that if we do disallow it on both sides, it is still kind-of possible for Wasm to throw a bare JS exception, but it requires cooperation from JS. You can write a JS function function throwAsIs(x) { throw x; } which you can import from Wasm. You then use call $throwAsIs and unreachable instead of throw $jstag. But that is inelegant, as it is not symmetric wrt. catch.
Edit: also I'm not only saying this in the abstract. I would definitely use throw $jstag in the Scala-to-Wasm compiler, since our spec says that we can throw JS exceptions and they will be caught as is by JS.
Ok, special-casing throw $jstag is not a big change so if there are valid reasons to use it then that's ok with me.
For that matter, if you catch a JS exception in wasm, you'll get an exnref that you can later rethrow with throw_ref and I would certainly expect that to work. Will that cause the same issues?
That will work, but that does not require any special case because exnref would already be the raw JS value and throw-ref would rethrow it without adding a tag to it.
Any more comments on this? The current version just uses the WebIDL attribute with a getter, and also contains the restriction that creating a WebAssembly.Exception object using the JS tag is disallowed.