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

Update JS API for exnref #301

Merged
merged 22 commits into from
Apr 11, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 75 additions & 54 deletions document/js-api/index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ urlPrefix: https://webassembly.github.io/exception-handling/core/; spec: WebAsse
text: ref.null
text: ref.func
text: ref.extern
text: ref.exn
text: function index; url: syntax/modules.html#syntax-funcidx
text: function instance; url: exec/runtime.html#function-instances
text: store_init; url: appendix/embedding.html#embed-store-init
Expand Down Expand Up @@ -101,6 +102,12 @@ urlPrefix: https://webassembly.github.io/exception-handling/core/; spec: WebAsse
text: global address; url: exec/runtime.html#syntax-globaladdr
text: extern address; url: exec/runtime.html#syntax-externaddr
text: tag address; url: exec/runtime.html#syntax-tagaddr
text: tag_alloc; url: appendix/embedding.html#embed-tag-alloc
text: tag_type; url: appendix/embedding.html#embed-tag-type
text: exception address; url: exec/runtime.html#syntax-exnaddr
text: exn_alloc; url: appendix/embedding.html#embed-exn-alloc
text: exn_read; url: appendix/embedding.html#embed-exn-read
text: tag type; url: syntax/types.html#syntax-tagtype
url: syntax/types.html#syntax-numtype
text: i32
text: i64
Expand Down Expand Up @@ -145,6 +152,7 @@ urlPrefix: https://webassembly.github.io/exception-handling/core/; spec: WebAsse
text: address; url: exec/runtime.html#addresses
text: signed_32; url: exec/numerics.html#aux-signed
text: memory.grow; url: exec/instructions.html#exec-memory-grow
text: throw_ref; url: exec/instructions.html#exec-throw-ref
text: current frame; url: exec/conventions.html#exec-notation-textual
text: module; for: frame; url: exec/runtime.html#syntax-frame
text: memaddrs; for: moduleinst; url: exec/runtime.html#syntax-moduleinst
Expand Down Expand Up @@ -238,6 +246,8 @@ Each [=agent=] is associated with the following [=ordered map=]s:
* The <dfn>Global object cache</dfn>, mapping [=global address=]es to {{Global}} objects.
* The <dfn>Extern value cache</dfn>, mapping [=extern address=]es to values.
* The <dfn>Tag object cache</dfn>, mapping [=tag addresses=] to {{Tag}} objects.
* The <dfn>Exception object cache</dfn>, mapping [=exception address=]es to {{Exception}} objects.


<h2 id="webassembly-namespace">The WebAssembly Namespace</h2>

Expand Down Expand Up @@ -760,7 +770,7 @@ Each {{Table}} object has a \[[Table]] internal slot, which is a [=table address
The <dfn constructor for="Table">Table(|descriptor|, |value|)</dfn> constructor, when invoked, performs the following steps:
1. Let |elementType| be [=ToValueType=](|descriptor|["element"]).
1. If |elementType| is not a [=reftype=],
1. [=Throw=] a {{TypeError}} exception.
1. Throw a {{TypeError}} exception.
dschuff marked this conversation as resolved.
Show resolved Hide resolved
1. Let |initial| be |descriptor|["initial"].
1. If |descriptor|["maximum"] [=map/exists=], let |maximum| be |descriptor|["maximum"]; otherwise, let |maximum| be empty.
1. If |maximum| is not empty and |maximum| &lt; |initial|, throw a {{RangeError}} exception.
Expand Down Expand Up @@ -807,7 +817,7 @@ Each {{Table}} object has a \[[Table]] internal slot, which is a [=table address
1. Let |store| be the [=surrounding agent=]'s [=associated store=].
1. Let (<var ignore>limits</var>, |elementType|) be [=table_type=](|store|, |tableaddr|).
1. If |elementType| is [=exnref=],
1. [=Throw=] a {{TypeError}} exception.
1. Throw a {{TypeError}} exception.
1. Let |result| be [=table_read=](|store|, |tableaddr|, |index|).
1. If |result| is [=error=], throw a {{RangeError}} exception.
1. Return [=ToJSValue=](|result|).
Expand All @@ -819,7 +829,7 @@ Each {{Table}} object has a \[[Table]] internal slot, which is a [=table address
1. Let |store| be the [=surrounding agent=]'s [=associated store=].
1. Let (<var ignore>limits</var>, |elementType|) be [=table_type=](|store|, |tableaddr|).
1. If |elementType| is [=exnref=],
1. [=Throw=] a {{TypeError}} exception.
1. Throw a {{TypeError}} exception.
1. If |value| is missing,
1. Let |ref| be [=DefaultValue=](|elementType|).
1. Otherwise,
Expand Down Expand Up @@ -1011,15 +1021,16 @@ This slot holds a [=function address=] relative to the [=surrounding agent=]'s [
1. [=list/Append=] [=ToWebAssemblyValue=](|arg|, |t|) to |args|.
1. Set |i| to |i| + 1.
1. Let (|store|, |ret|) be the result of [=func_invoke=](|store|, |funcaddr|, |args|).
1. Note: The expectation is that [=func_invoke=] will be updated to return (|store|, <var ignore>val</var>* | [=error=] | (exception |exntag| |payload| |opaqueData|)).
1. Set the [=surrounding agent=]'s [=associated store=] to |store|.
1. If |ret| is [=error=], throw an exception. This exception should be a WebAssembly {{RuntimeError}} exception, unless otherwise indicated by <a href="#errors">the WebAssembly error mapping</a>.
1. If |ret| is exception |exntag| |payload| |opaqueData|, then
1. If |opaqueData| is not [=ref.null=] [=externref=],
1. Let « [=ref.extern=] |externaddr| » be |opaqueData|.
1. Throw the result of [=retrieving an extern value=] from |externaddr|.
1. Let |exception| be [=create an Exception object|a new Exception=] for |exntag| and |payload|.
1. Throw |exception|.
1. If |ret| is [=THROW=] [=ref.exn=] |exnaddr|, then
1. Let (|tagaddr|, |payload|) be [=exn_read=](|store|, |exnaddr|).
1. Let |jsTagAddr| be the result of [=get the JavaScript exception tag |getting the JavaScript exception tag=].
1. If |tagaddr| is equal to |jsTagAddr|,
1. Throw the result of [=retrieving an extern value=] from |payload|[0].
Comment on lines +1029 to +1030
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an optimization that was discussed before where the engine does not wrap/unwrap the exception at the boundary, and instead implements the JSTag behavior as a special-case in the catch handler. But with that optimization, if the user code creates a WebAssembly.Exception(WebAssembly.JSTag, ...) object explicitly and throws it, it would not get unwrapped automatically at the export, which would violate this spec, right?
We can still benefit from the optimization and add a check when the object crosses the JS/Wasm boundary to cover this special case, but I just wanted to make sure that this is indeed what this spec requires us to do.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about that issue as well while porting the JSTag behavior to the exnref implementation in V8. I was wondering if we could disallow creating a WebAssembly.Exception(JSTag, ...) in the first place? As in, throw a RangeError or some other exception from the constructor of WA.Exception. That would probably simplify a number of things, both at the spec and implementation level.

Would that behavior be acceptable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would describe the spec as currently written as saying that regular JS exceptions just never get wrapped or unwrapped, the payload just passes through (although like all exceptions the payload is paired with a tag, even if that tag is not exposed to the user). I think as written today, 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. If I'm understanding the optimization you describe, it would naturally have exactly this behavior, so there wouldn't need to be special cases.

Having said that, that behavior is a little weird and may be unexpected since it's asymmetric. I don't really see a use case for an explicitly-wrapped WebAssembly.Exception with the JS tag, so maybe it does make sense to disallow creation of such an object.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also realized that this discussion should probably go in #269 since that PR is where the JS Tag object actually gets exposed to JS explicitly and makes that behavior possible.

1. Otherwise,
1. Let |exception| be [=create an Exception object|a new Exception=] created from |exnaddr|.
1. Throw |exception|.
1. Let |outArity| be the [=list/size=] of |ret|.
1. If |outArity| is 0, return undefined.
1. Otherwise, if |outArity| is 1, return [=ToJSValue=](|ret|[0]).
Expand Down Expand Up @@ -1048,7 +1059,7 @@ Note: Exported Functions do not have a \[[Construct]] method and thus it is not
1. Otherwise, if |resultsSize| is 1, return « [=?=] [=ToWebAssemblyValue=](|ret|, |results|[0]) ».
1. Otherwise,
1. Let |method| be [=?=] [$GetMethod$](|ret|, {{@@iterator}}).
1. If |method| is undefined, [=throw=] a {{TypeError}}.
1. If |method| is undefined, throw a {{TypeError}}.
1. Let |values| be [=?=] [$IterableToList$](|ret|, |method|).
1. Let |wasmValues| be a new, empty [=list=].
1. If |values|'s [=list/size=] is not |resultsSize|, throw a {{TypeError}} exception.
Expand All @@ -1071,18 +1082,18 @@ Note: Exported Functions do not have a \[[Construct]] method and thus it is not
1. [=Clean up after running a callback=] with |stored settings|.
1. [=Clean up after running script=] with |relevant settings|.
1. Assert: |result|.\[[Type]] is <emu-const>throw</emu-const> or <emu-const>normal</emu-const>.
1. Let |store| be the [=surrounding agent=]'s [=associated store=].
1. If |result|.\[[Type]] is <emu-const>throw</emu-const>, then:
1. Let |v| be |result|.\[[Value]].
1. If |v| [=implements=] {{Exception}},
1. Let |type| be |v|.\[[Type]].
1. Let |payload| be |v|.\[[Payload]].
1. Let |address| be |v|.\[[Address]].
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does a WebAssembly.Exception object have Address?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, Address is initialized in "initialize an Exception object". Exception is now just like all the other JS-exposed objects which also have their wasm Addresses stored in an internal slot in the JS object. (Although I just noticed that the naming convention isn't consistent right now; The Exception and Tag objects have the address in a field named Address whereas the rest have that field named after the object type, e.g. Global. Maybe I'll fix that in a followup change).

1. Otherwise,
1. Let |type| be the [=JavaScript exception tag=].
1. Let |payload| be « ».
1. Let |opaqueData| be [=ToWebAssemblyValue=](|v|, [=externref=])
1. [=WebAssembly/Throw=] with |type|, |payload| and |opaqueData|.
1. Let |type| be the result of [=get the JavaScript exception tag |getting the JavaScript exception tag=].
1. Let |payload| be [=!=] [=ToWebAssemblyValue=](|v|, [=externref=]).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks v contains Type and Value. Shouldn't we only convert the Value part, rather than the whole v?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Type and Value fields come from ECMAScript completion record, and result is the completion record.
On lines 1086 - 1087 above, result.Type is THROW and then v is result.Value, i.e. the value returned by the JS host function called on line 1081, so it's already a value. So I think that's the thing that we need to convert to a wasm externref and throw back into wasm.

... actually though, I just noticed that the handling of completion records between 'run a host function' and 'create a host function' wasn't quite right even before the EH spec. I just filed WebAssembly/spec#1743 about that.

1. Let (|store|, |address|) be [=exn_alloc=](|store|, |type|, « |payload| »).
1. Set the [=surrounding agent=]'s [=associated store=] to |store|.
1. Execute the WebAssembly instructions ([=ref.exn=] |address|) ([=throw_ref=]).
1. Otherwise, return |result|.\[[Value]].
1. Let |store| be the [=surrounding agent=]'s [=associated store=].
1. Let (|store|, |funcaddr|) be [=func_alloc=](|store|, |functype|, |hostfunc|).
1. Set the [=surrounding agent=]'s [=associated store=] to |store|.
1. Return |funcaddr|.
Expand Down Expand Up @@ -1171,10 +1182,6 @@ The algorithm <dfn>ToWebAssemblyValue</dfn>(|v|, |type|) coerces a JavaScript va

<h3 id="tags">Tags</h3>

The <dfn>tag_alloc</dfn>(|store|, |parameters|) algorithm creates a new [=tag address=] for |parameters| in |store| and returns the updated store and the [=tag address=].

The <dfn>tag_parameters</dfn>(|store|, |tagAddress|) algorithm returns the [=list=] of types for |tagAddress| in |store|.

<h4 id="exceptions-types">Exception types</h4>

<pre class="idl">
Expand Down Expand Up @@ -1235,7 +1242,7 @@ The <dfn constructor for="Tag" lt="Tag(type)">new Tag(|type|)</dfn> constructor
The <dfn method for="Tag">type()</dfn> method steps are:

1. Let |store| be the [=surrounding agent=]'s [=associated store=].
1. Let |parameters| be [=tag_parameters=](|store|, **this**.\[[Address]]).
1. Let [|parameters|] → [] be [=tag_type=](|store|, **this**.\[[Address]]).
1. Let |idlParameters| be «».
1. [=list/iterate|For each=] |type| of |parameters|,
1. [=list/Append=] [$FromValueType$](|type|) to |idlParameters|.
Expand All @@ -1255,7 +1262,7 @@ dictionary ExceptionOptions {
[LegacyNamespace=WebAssembly, Exposed=(Window,Worker,Worklet)]
interface Exception {
constructor(Tag exceptionTag, sequence&lt;any> payload, optional ExceptionOptions options = {});
any getArg(Tag exceptionTag, [EnforceRange] unsigned long index);
any getArg([EnforceRange] unsigned long index);
boolean is(Tag exceptionTag);
readonly attribute (DOMString or undefined) stack;
};
Expand All @@ -1265,19 +1272,31 @@ An {{Exception}} value represents an exception.

<div algorithm>

To <dfn>create an Exception object</dfn> from a [=tag address=] |tagAddress| and a [=list=] of
WebAssembly values |payload|, perform the following steps:
To <dfn>initialize an Exception object</dfn> |exn| from an [=Exception address=] |exnAddress|, perform the following steps:

1. Let |map| be the [=surrounding agent=]'s associated [=Exception object cache=].
1. Assert: |map|[|exnAddress|] doesn't [=map/exist=].
1. Set |exn|.\[[Address]] to |exnAddress|.
1. [=map/Set=] |map|[|exnAddress|] to |exn|.
1. Let |store| be the [=surrounding agent=]'s [=associated store=].
1. Let |types| be [=tag_parameters=](|store|, |tagAddress|).
1. Assert: |types|'s [=list/size=] is |payload|'s [=list/size=].
1. [=list/iterate|For each=] |value| and |resultType| of |payload| and |types|, paired linearly,
1. Assert: |value|'s type matches |resultType|.
1. Let |exception| be a [=new=] {{Exception}}.
1. Set |exception|.\[[Type]] to |tagAddress|.
1. Set |exception|.\[[Payload]] to |payload|.
1. Set |exception|.\[[Stack]] to undefined.
1. Return |exception|.
1. Let (|tagaddr|, |payload|) be [=exn_read=](|store|, |exnAddress|).
1. Set |exn|.\[[Type]] to |tagaddr|.
1. Set |exn|.\[[Payload]] to |payload|.
1. Set |exn|.\[[Stack]] to undefined.

</div>

<div algorithm>

To <dfn>create an Exception object</dfn> from a [=exception address=] |exnAddress|, perform the following steps:

1. Let |map| be the [=surrounding agent=]'s associated [=Exception object cache=].
1. If |map|[|exnAddress|] [=map/exists=],
1. Return |map|[|exnAddress|].
1. Let |exn| be a [=new=] {{Exception}}.
1. [=initialize an Exception object|Initialize=] |exn| from |exnAddress|.
1. Return |exn|.


</div>

Expand All @@ -1288,28 +1307,28 @@ lt="Exception(exceptionTag, payload, options)">new Exception(|exceptionTag|, |pa
constructor steps are:

1. Let |store| be the [=surrounding agent=]'s [=associated store=].
1. Let |types| be [=tag_parameters=](|store|, |exceptionTag|.\[[Address]]).
1. Let [|types|] → [] be [=tag_type=](|store|, |exceptionTag|.\[[Address]]).
1. If |types|'s [=list/size=] is not |payload|'s [=list/size=],
1. Throw a {{TypeError}}.
1. Let |wasmPayload| be « ».
1. [=list/iterate|For each=] |value| and |resultType| of |payload| and |types|, paired linearly,
1. [=list/Append=] ? [=ToWebAssemblyValue=](|value|, |resultType|) to |wasmPayload|.
1. Set **this**.\[[Type]] to |exceptionTag|.\[[Address]].
1. Set **this**.\[[Payload]] to |wasmPayload|.
1. [=list/Append=] [=?=] [=ToWebAssemblyValue=](|value|, |resultType|) to |wasmPayload|.
1. Let (|store|, |exceptionAddr|) be [=exn_alloc=](|store|, |exceptionTag|.\[[Address]], |wasmPayload|)
dschuff marked this conversation as resolved.
Show resolved Hide resolved
1. Set the [=surrounding agent=]'s [=associated store=] to |store|.
1. [=initialize an Exception object|Initialize=] **this** from |exceptionAddr|.
dschuff marked this conversation as resolved.
Show resolved Hide resolved
1. If |options|["traceStack"] is true,
1. Set **this**.\[[Stack]] to either a {{DOMString}} representation of the current call stack or undefined.
1. Otherwise,
1. Set **this**.\[[Stack]] to undefined.
dschuff marked this conversation as resolved.
Show resolved Hide resolved


</div>

<div algorithm>

The <dfn method for="Exception">getArg(|exceptionTag|, |index|)</dfn> method steps are:
The <dfn method for="Exception">getArg(|index|)</dfn> method steps are:

1. If **this**.\[[Type]] is not equal to |exceptionTag|.\[[Address]],
1. Throw a {{TypeError}}.
1. Let |payload| be **this**.\[[Payload]].
1. Let |store| be the [=surrounding agent=]'s [=associated store=].
1. Let (|tagaddr|, |payload|) be [=exn_read=](|store|, **this**.\[[Address]]).
1. Assert: |tagaddr| is equal to **this**.\[[Type]].
1. If |index| ≥ |payload|'s [=list/size=],
1. Throw a {{RangeError}}.
1. Return [=ToJSValue=](|payload|[|index|]).
Expand All @@ -1336,20 +1355,22 @@ The <dfn attribute for="Exception">stack</dfn> getter steps are:

<h4 id="js-exceptions">JavaScript exceptions</h4>

The <dfn>JavaScript exception tag</dfn> is a [=tag address=] reserved by this
specification to distinguish exceptions originating from JavaScript.
The <dfn>JavaScript exception tag</dfn> is a [=tag address=] associated with
the surrounding agent. It is allocated in the agent's [=associated store=] on
first use and cached. It always has the [=tag type=] « [=externref=] » → « ».

For any [=associated store=] |store|, the result of
[=tag_parameters=](|store|, [=JavaScript exception tag=]) must be « ».

<div algorithm>

To <dfn for=WebAssembly>throw</dfn> with a [=tag address=] |type|, a matching [=list=] of WebAssembly values |payload|, and an [=externref=] |opaqueData|, perform the following steps:

1. Unwind the stack until reaching the *catching try block* given |type|.
1. Invoke the catch block with |payload| and |opaqueData|.
To <dfn>get the JavaScript exception tag</dfn>, perform the following steps:

Note: This algorithm is expected to be moved into the core specification.
1. If the [=surrounding agent=]'s associated [=JavaScript exception tag=] has been initialized,
1. return the [=surrounding agent=]'s associated [=JavaScript exception tag=]
1. Let |store| be the [=surrounding agent=]'s [=associated store=].
1. Let (|store|, |tagAddress|) be [=tag_alloc=](|store|, « [=externref=] » → « »).
1. Set the current agent's [=associated store=] to |store|.
1. Set the current agent's associated [=JavaScript exception tag=] to |tagAddress|.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean tag == tagAddress? Somewhere above in this doc contains

Set map[tagAddress] to tag.

So they seem to be different.

And this PR doesn't include how Wasm should handle the JS exception tag, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 1208 is where you're referring to. There tag and tagAddress are both basically function parameters, and tag is a JS object and tagAddress is an address as defined in the wasm runtime spec. Here tagAddress is basically a local variable (a wasm address), a return value of tag_alloc; and tag isn't a variable at all. This function just returns the address.

And yes, #269 is the PR that allows actually exposing the JS exception tag (which by extension allows it to be imported into a wasm module).

1. return |tagAddress|.

</div>

Expand Down
Loading