Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposed exnref value type semantics #282

Closed
eqrion opened this issue Sep 19, 2023 · 15 comments
Closed

Proposed exnref value type semantics #282

eqrion opened this issue Sep 19, 2023 · 15 comments

Comments

@eqrion
Copy link
Contributor

eqrion commented Sep 19, 2023

I don't believe this was discussed elsewhere, apologies if I missed it.

Here's a proposal for the semantics of the new exnref value type.

  • Contains any reference that a wasm catch or catch_all may receive
    • In browsers this is a union of any JS value (primitives included) and any wasm exception reference
    • On other hosts this may just be a limited exception object hierarchy and not included primitives
  • New top type, creating an independent type hierarchy
  • JS-API ToWebAssemblyValue(value, 'exnref') succeeds for any value (like externref)
    • This is due to every JS value being a possible thrown value in JS hosts
  • JS-API ToJSValue(value, 'exnref') succeeds for any value (like externref)
  • Add an extern.convert_exn instruction for getting an externref from an exnref
  • Add an any.convert_exn instruction for getting an anyref from an exnref
  • With GC: introduce a noexn heap type to mirror the other bottom types
  • With GC: introduce a nullexnref alias for (ref null noexn)
  • With GC: ref.cast exnref/nullexnref are valid instructions similar, but do very little like in the externref hierarchy.

Note, there is no exn.convert_any or exn.convert_extern as we don't want to assume in core wasm that exnref can hold any host value. That's true on JS hosts though, which is why the JS-API allows conversions on the boundary.

Did I miss anything?

There is a world where we do something more conservative and don't allow exnref in ToWebAssemblyValue ToJSValue or introduce the conversion instructions. But I don't think they are much work, and would be useful.

@conrad-watt
Copy link
Contributor

Note, there is no exn.convert_any or exn.convert_extern as we don't want to assume in core wasm that exnref can hold any host value. That's true on JS hosts though, which is why the JS-API allows conversions on the boundary.

Can you walk me through why this is true on JS hosts? I wouldn't necessarily expect that a exception object already materialised on the JS side could be passed to Wasm as an exnref through a function argument (I'd expect it to appear as an externref); even if such an object, when thrown in JS, could be caught in Wasm as an exnref.

@eqrion
Copy link
Contributor Author

eqrion commented Sep 19, 2023

So JS code can do things like:

throw 'JS string primitive';

and wasm can catch that and receive an exnref:

block $l (result exnref)
  try (catch_all $l)
    call $JS
  end
end
;; exnref which contains a JS string primitive

And in JS this thrown value doesn't have any identity or stack trace associated with it (AFAIK), it's simply just the primitive without any wrapper exception object.

So that's why I believe we need to allow for exnref to contain any host value on JS hosts, which is why I think the JS-API ToWebAssembly/ToJSValue semantics I propose are safe.

However, other hosts could have (simpler) semantics that only objects in an exception class hierarchy can be thrown. So we wouldn't want to force those hosts to be able to convert anyref to exnref inside core wasm. It seems safe to allow the other direction though.

@conrad-watt
Copy link
Contributor

Ah ok, I misunderstood. This makes sense, thanks! Do we expect that such an exnref is catchable with tag externref (to immediately get at the externref view of the thrown JS value) or would the Wasm code need to rely on catch_all and extern.convert_exn?

@keithw
Copy link
Member

keithw commented Sep 19, 2023

Unfortunately I think extern.convert_exn would be a problem for wasm2c and other consumers who want to implement exception-handling without a dependency on GC. :-(

Per #280 (comment), my understanding had been that exnref would be representable as a sum type (big enough to contain any plausible set of tag values) that can reasonably be stored on the stack and copied when needed.

If Wasm code has the ability to convert an exnref into an externref (which for us is basically a void*), that would throw a wrench into that plan -- the lifetime of the tag values would become independent of any exnref value.

@eqrion
Copy link
Contributor Author

eqrion commented Sep 19, 2023

@keithw Ah, good point. I did not know that wasm2c implemented externref, is that a GC or ref-counted value? What is it used for?

@eqrion
Copy link
Contributor Author

eqrion commented Sep 19, 2023

Ah ok, I misunderstood. This makes sense, thanks! Do we expect that such an exnref is catchable with tag externref (to immediately get at the externref view of the thrown JS value) or would the Wasm code need to rely on catch_all and extern.convert_exn?

I think that would be really tricky to specify, as tags are generative and so we'd need to have the tag for 'host values' that is importable. And then engines would need to know that there's a special tag that non-wasm exception objects all implicitly have. I think the catch_all with conversion would be cleaner.

@keithw
Copy link
Member

keithw commented Sep 19, 2023

@eqrion It's not GCed or ref-counted -- for wasm2c, externref is just an opaque type (by default, a void*). I think it's up to the host API to define the purpose (other than "so wasm2c can pass the spec testsuite"), but in our own work we've been using externref to pass arbitrary capabilities/handles to a Wasm module.

@rossberg
Copy link
Member

I think we need to distinguish exnref values from host values thrown as exceptions. When you catch a host value, then the exnref you get conceptually carries that value, but it isn't the same as that value — I think of exnref as more like an instance of WebAssembly.Exception and the value is its arg. It may happen to work in JS to identify the two (and an implementation may do so), but I doubt it necessarily works for other embeddings to make that observable and allow converting between extern and exnref as if that maintained identity.

If we want to allow Wasm to extract a JS value as an externref when catching a JS exception, then we should rather materialise JS exceptions with a special tag defined by the JS API. Then you can do:

(tag $JSexn (import "JS" "JSexn") (param externref))

(block $on_JSexn (result externref exnref)
   (try (catch_all $JSexn $on_JSexn)
       call $JS
   )
   ...
)
(drop)  ;; ignore exnref
;; JS value on top of stack as externref

With that there is no reason to allow converting exnref to externref or anyref, or to pass it to JS. If we don't, then I think an engine choosing to identify JS exn and its argument as mentioned above would become an unobservable optimisation, which seems preferable to hardwiring it into the semantics.

@eqrion
Copy link
Contributor Author

eqrion commented Sep 19, 2023

@rossberg I think that could work, but I'll need to think about it more. I'm a bit nervous that long-term the implicit WebAssembly.Exception wrapping of JS values could become accidentally observable in a future extension. But maybe it's fine.

@keithw Okay, that makes sense. In that case we should not have the conversion instructions I listed.

@dschuff
Copy link
Member

dschuff commented Sep 19, 2023

We discussed exposing a special exception tag for JavaScript and allowing importing it (the payload would be an externref that represents the thrown object): #269
I think something like that would still make sense in an exnref world.

@aheejin
Copy link
Member

aheejin commented Sep 23, 2023

+1 on what @rossberg and @dschuff said.

In the current JS API, we have WebAssembly.Exception in JS side, which is an object containing a tag, thrown values, and possibly other metadata like stack traces. The current Phase 3 JS API spec was hard to write because there was no direct corresponding object for it in the Wasm side, so we had to do some handwaving. When we have exnref, many things in the current JS API can be simplified.

So the drift of that is, I think exnref should correspond 1:1 with WebAssembly.Exception, not the thrown object (without a tag).

To support catching of JS exceptions, #269 proposed a special "JS tag" so that the web engine can recognize it and treat it as a special case. So when wasm code uses try-catch with the JS tag, even though a random JS object thrown from JS side doesn't have a "tag" attached to it when thrown, the engine treats it like a WebAssembly.Exception thrown with a JS tag. AFAIK this has been already implemented in v8:
v8/v8@16f0553
v8/v8@4e79015

@eqrion
Copy link
Contributor Author

eqrion commented Sep 28, 2023

Another reason in favor of having JS values auto-boxed in a WebAssembly.Exception is around null.

In JS it's valid to throw/catch null:

try {
  throw null;
} catch (value);
  assertEq(value, null);
}

While it also seems desirable to have throw_ref trap if it receives a ref.null exn. If JS null is boxed in a WebAssembly.Exception when in WebAssembly, that allows the rethrow semantics you'd expect.

This all may have been discussed before, I'm just catching up :)

@dschuff
Copy link
Member

dschuff commented Apr 11, 2024

Now the #301 has landed I did want to briefly bring back up the question of passing exnrefs into JS via function return, global getters, etc. @rossberg mentioned there and above that we don't want exnref to be convertible to anyref or externref and I agree that makes sense.
But could still allow exnrefs to go to JS via this route if we wrap them in WebAssembly.Exception at each of the points where they go to JS (perhaps in ToJSValue) and unwrap them going the other direction. This would not cause any of the aforementioned issues with the type system in wasm, nor would it force non-JS engines to use full GC instead of refcounting; but it would still allow JS users to store an exnref or a JS-wrapped equivalent wherever they wanted.
In fact, it's of course already possible to do this; you just have to throw the exnref out of wasm instead of passing it some other way, and it's exactly equivalent, just awkward. So I don't think the current restriction really buys much compared to just allowing the wrapping and unwrapping in more places.

@rossberg
Copy link
Member

I think that would still be problematic, because ToWasmValue with target type exnref would then have to convert non-WA.Exception values to exnrefs with the JS tag.

But doing so it wouldn't be able to distinguish a WA.Exception that was originally an exnref (and hence needs to be converted back into one, using the original tag and throw context) from a random object manually created via new WA.Exception (which would need the JS tag and a payload that is this object as an externref).

So AFAICS, this would still lead to the conflation of different value domains: exnrefs and their payloads. The former represent thrown values, including the context of the throw, not the values themselves.

Unless you somehow want to treat manually created WA.Exception objects as actual exnrefs with some dummy throw context. But I'm not convinced that's desirable or useful.

(Note that this conflation does not occur when merely throwing a manual WA.Exception, because that explicitly turns it into a thrown value, producing an actual throw context.)

@dschuff
Copy link
Member

dschuff commented Jun 14, 2024

I think the issues here have been resolved and the JS API spec now reflects what we discussed here. Let's open specific new issues if there's anything new.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants