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

[test] Add tentative JS API tests for Exported GC Object #355

Merged
merged 5 commits into from
Oct 5, 2023

Conversation

takikawa
Copy link
Contributor

Adds tests for the "Exported GC Object" described in the proposed JS API spec in #352.

This requires updating the wasm module builder helper library, which is now fairly de-synced with the V8 library it's based on. I've only synced a bare minimum in this PR to be able to construct some GC modules (syncing the whole thing causes other tests to fail).

The V8 version also uses a slightly different opcode numbering than the current spec document in this repo. In this PR I went with the spec numbering (in the end they will all need to be synchronized).

This doesn't have tests for all the ToWasmValue/ToJSValue cases for conversions, which would also be useful to add.

}

// GC opcodes
let kExprStructNew = 0x01;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note: the opcode numbers for struct.new and array.new differ from V8/SM currently.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's because the implementations all along the stack (engines, Binaryen, *-to-Wasm compilers) have been maintaining (incremental) backwards compatibility when adding and removing instructions as the proposal evolved, whereas the official spec is not concerned with such mundane details.

This will resolve itself when we do the large final breaking opcode reshuffling. In the meantime, see https://bit.ly/3cWcm6Q for the authoritative documentation on what toolchains and engines implement.

Copy link
Contributor

Choose a reason for hiding this comment

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

A note in the spec about this would be useful, IMO. In the meantime, it might be best to use the de facto opcode numbers in the tests, so that they can be run against implementations - not much point in them otherwise.

test(() => {
const struct = functions.makeStruct()
const array = functions.makeArray()
assert_throws_js(TypeError, () => Object.getPrototypeOf(struct));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

When I tried this in V8, this test failed because the getPrototypeOf didn't throw.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, when we made [[Get]] not throw we also had to make [[GetPrototypeOf]] not throw (but return null instead). Looks like I forgot to update the draft doc accordingly, sorry about that. Fixed it there just now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok thanks, I updated this test case to check for null instead as well. I think this actually makes it easier for JSC to implement [[Get]]'s behavior too, though I'm wondering if it'll be sufficient to future-proof for shared structs.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I'm a little worried about future-proofing for improved prototypes later (related to alignment with shared structs). Is this is the only way we can make it so that async functions can return Wasm GC objects without blowing up? If so, it seems like this is the best possible compromise.

@takikawa
Copy link
Contributor Author

Other than the points I mentioned in the tests above, I was able to run this in V8 by manually importing it into v8/test/wasm-js/tests and editing a few things like opcodes. I also ran it against a WIP implementation in JSC.

test(() => {
const struct = functions.makeStruct()
const array = functions.makeArray()
assert_throws_js(TypeError, () => { struct.foo = 5; });
Copy link
Contributor Author

@takikawa takikawa Feb 17, 2023

Choose a reason for hiding this comment

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

Right now [[Set]] is specified to always throw, which matches the test cases here.

It is arguably a bit inconsistent with the rest of JS though, because usually [[Set]] returns false on failure but doesn't throw. In the usual case, that means that in non-strict mode, struct.foo = 5 will not add/mutate the field and just silently fail without throwing. With Wasm structs/arrays, it will throw even in non-strict mode. Not sure anyone will care about this difference, but I thought it might be worth mentioning.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, returning false probably makes more sense. In any case, we should probably make sure to test these cases both in strict and loose mode

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that makes sense. I've changed the test now to have strict and non-strict versions, with no error raised in the non-strict version (and the property shouldn't be added/deleted). We'll have to change the spec to match this though.

@takikawa
Copy link
Contributor Author

takikawa commented Mar 7, 2023

Who would be the best person to review this and provide review approval? @Ms2ger would you be interested in taking a look?

@ajklein
Copy link

ajklein commented Mar 7, 2023

I can't seem to add him as a reviewer, but it would make sense for @syg to do at least an unofficial review of this given the intersection with JS Structs.

// Use these for multi-byte instructions (opcode > 0x7F needing two LEB bytes):
function GCInstr(opcode) {
if (opcode <= 0x7F) return [kGCPrefix, opcode];
return [kGCPrefix, 0x80 | (opcode & 0x7F), opcode >> 7];
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this use wasmSignedLeb?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It might be able to.

In general for these wasm-module-builder.js changes I took the upstream V8 code as-is when I could, so merging new changes would be easier without a complicated diff. Do you prefer that I make changes here anyway and let it diverge with upstream? (also applies to other comments)

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps someone from the V8 side could comment. @jakobkummerow seems to have some blame around here.

Would be a nice project for someone with time to consolidate all the versions going around.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, this probably could use wasmSignedLeb, but it also doesn't matter as it seems exceedingly unlikely that we'll have that many opcodes any time soon (if ever). From a V8 perspective, we see our wasm-module-builder.js as very frequently changing code anyway: we update it whenever we need to (typically to support some new feature), and that happens a lot. Basically the reason we have our own thing there rather than importing someone else's implementation is because we need to change it so much, and want that to have low friction and low latency.

If you want to submit a patch to improve it, I'd volunteer to review that, but I'm currently not inclined to spend my own time to make this code more polished-looking.

}

// GC opcodes
let kExprStructNew = 0x01;
Copy link
Contributor

Choose a reason for hiding this comment

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

A note in the spec about this would be useful, IMO. In the meantime, it might be best to use the de facto opcode numbers in the tests, so that they can be run against implementations - not much point in them otherwise.

}

emit_type(type) {
if ((typeof type) == 'number') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if ((typeof type) == 'number') {
if (typeof type === 'number') {

@@ -674,6 +742,35 @@ class WasmTableBuilder {
}
}

function makeField(type, mutability) {
if ((typeof mutability) != 'boolean') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if ((typeof mutability) != 'boolean') {
if (typeof mutability !== 'boolean') {

Comment on lines +829 to +832
// We use {is_final = true} so that the MVP syntax is generated for
// signatures.
Copy link
Contributor

Choose a reason for hiding this comment

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

What's this about, and should it really be controlled on a type-by-type basis?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the GC proposal type declarations for structs & arrays can be declared final or not. Since the finality is declared for particular type defs I think it has to be type-by-type.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand the MVP comment, then.

test(() => {
const struct = functions.makeStruct()
const array = functions.makeArray()
assert_throws_js(TypeError, () => { struct.foo = 5; });
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, returning false probably makes more sense. In any case, we should probably make sure to test these cases both in strict and loose mode

@takikawa
Copy link
Contributor Author

Thanks for the comments, I resolved them aside from the ones about the module builder (see comment). The main change is to allow not throwing an exception in non-strict mode (and silently failing) for adding properties and deleting.

This modified version should succeed on V8 except for the non-strict tests. On JSC it also passes (except non-strict) with some minor changes in opcodes. In JSC I implemented a quick fix for the non-strict case and got all tests to pass too.

@jakobkummerow
Copy link
Contributor

I have a concern about the change to not throw in non-strict mode.
I think this new behavior (not throwing) would make perfect sense if we aimed to specify some final state of affairs. But the idea behind the throwing was specifically to keep the door wide open to future changes in behavior here (when, post-MVP, we discuss options for richer interop stories).

That's why I'd prefer to keep throwing in both strict and non-strict mode for now.
If in the future we reach the conclusion that the "no-frills" interop is actually enough, and we want to declare it final, then we can relax the non-strict mode behavior at that point.
Whereas if we develop any approach that actually allows foo.bar notation in the future, it could very well be very important for not breaking the web with that rollout that we can be sure that nobody, not even non-strict mode users, is currently shipping any code that would break with this rollout.

Also, in general, when a PR named "add tentative tests" makes such a change to behavior, I think this needs to be explicitly called out to a wider audience (one might even argue that it should happen as a separate PR).

@Ms2ger
Copy link
Contributor

Ms2ger commented Apr 20, 2023

Also, in general, when a PR named "add tentative tests" makes such a change to behavior, I think this needs to be explicitly called out to a wider audience (one might even argue that it should happen as a separate PR).

Note that this PR doesn't change the spec.

@rossberg
Copy link
Member

What is the status of this PR?

Adds a test for the draft JS API spec from this PR:

  WebAssembly#352

This primarily tests the internal object methods described in the spec.
It doesn't yet test the ToWebAssemblyValue / ToJSValue conversions.

Also syncs a portion of the module builder code with V8 to allow
constructing a subset of GC-using modules.
  * Update opcodes to final versions in MVP document
  * Undid strict/non-strict behavior difference
@rossberg
Copy link
Member

rossberg commented Sep 9, 2023

@takikawa, ping, is this PR still relevant?

@takikawa
Copy link
Contributor Author

takikawa commented Sep 9, 2023

@takikawa, ping, is this PR still relevant?

Hi @rossberg, yes I think it's relevant but it needs a rebase to adjust for the final opcodes and to address Jakob's concerns. I have some local changes that I was planning to push after testing.

@takikawa
Copy link
Contributor Author

takikawa commented Sep 9, 2023

Had to force push due to the rebase. This should now be updated to throw in non-strict mode as well, like @jakobkummerow requested (i.e., reverted to the original test behavior for this).

It should also be updated to match the final opcodes in MVP.md. I've tested it on JSC/WebKit but haven't had a chance to run it on V8 or SpiderMonkey.

@jakobkummerow
Copy link
Contributor

@takikawa For testing this on V8 with the final binary encoding, you can patch in crrev.com/c/4756846. We're planning to land that after the Phase 4 vote.

@rossberg rossberg mentioned this pull request Sep 12, 2023
14 tasks
  * The exported-object test should run on V8 now
  * Non-GC JS API tests should run correctly too
@takikawa
Copy link
Contributor Author

takikawa commented Sep 15, 2023

I've now tested this on V8 (on commit 014e0bb5ac4691c85b1da2dac579af77d02d17b7, already updated to new opcodes) in the wasm-js test suite and on Firefox (commit f4b0e304ecebf431616ce014dd198c1d361b37c1, with patches from bug 1845374 applied) with WPT tests.

I also pushed some fixes that I found while testing this, all in the wasm-module-builder.js helper.

On Firefox, some exception handling proposal tests do fail. This is because the exception tests (that are in WPT as tentative tests) use some type code names that have changed in the upstream wasm-module-builder.js and in this PR. In this case, I think the exception tests (specifically this one) should just be changed to match as that proposal is in phase 3.

@takikawa
Copy link
Contributor Author

In any case, I think this is ready to merge if other people are happy with it. Separately, I'm planning to work on on the rest of the JS API tests (testing ToWebAssemblyValue and ToJSValue) soon.

@rossberg rossberg merged commit bdad982 into WebAssembly:main Oct 5, 2023
1 check passed
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

Successfully merging this pull request may close these issues.

7 participants