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

[spec] Normative: Add i64<->BigInt conversion in JS API #707

Merged
merged 2 commits into from
Jun 9, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
31 changes: 16 additions & 15 deletions document/js-api/index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ urlPrefix: https://tc39.github.io/ecma262/; spec: ECMASCRIPT
text: CreateArrayFromList; url: sec-createarrayfromlist
text: GetMethod; url: sec-getmethod
text: IterableToList; url: sec-iterabletolist
text: ToBigInt64; url: #sec-tobigint64
text: BigInt; url: #sec-ecmascript-language-types-bigint-type
type: abstract-op
text: CreateMethodProperty; url: sec-createmethodproperty
urlPrefix: https://webassembly.github.io/spec/core/; spec: WebAssembly; type: dfn
Expand Down Expand Up @@ -172,6 +174,7 @@ urlPrefix: https://webassembly.github.io/spec/core/; spec: WebAssembly; type: df
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
text: signed_64; url: exec/numerics.html#aux-signed
text: sequence; url: syntax/conventions.html#grammar-notation
urlPrefix: https://heycam.github.io/webidl/; spec: WebIDL
type: dfn
Expand Down Expand Up @@ -348,8 +351,11 @@ A {{Module}} object represents a single WebAssembly module. Each {{Module}} obje
1. Let |externfunc| be the [=external value=] [=external value|func=] |funcaddr|.
1. [=list/Append=] |externfunc| to |imports|.
1. If |externtype| is of the form [=global=] <var ignore>mut</var> |valtype|,
1. If [=Type=](|v|) is Number,
1. If |valtype| is [=i64=], throw a {{LinkError}} exception.
1. If [=Type=](|v|) is Number or BigInt,
1. If |valtype| is [=i64=] and [=Type=](|v|) is Number,
Copy link

Choose a reason for hiding this comment

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

This seem unnecessarily restrictive. Why not allow a Number to be passed here? The global gets allocated const regardless of the import's declared mutability.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This has been discussed in WebAssembly/JS-BigInt-integration#12; allowing Numbers to be passed is likely to lead to rounding errors. We can revisit if it turns out there is an actual problem in practice.

1. Throw a {{LinkError}} exception.
1. If |valtype| is not [=i64=] and [=Type=](|v|) is BigInt,
1. Throw a {{LinkError}} exception.
1. Let |value| be [=ToWebAssemblyValue=](|v|, |valtype|).
1. Let |store| be the [=surrounding agent=]'s [=associated store=].
1. Let (|store|, |globaladdr|) be [=global_alloc=](|store|, [=const=] |valtype|, |value|).
Expand Down Expand Up @@ -871,7 +877,6 @@ which can be simultaneously referenced by multiple {{Instance}} objects. Each
1. If |v| is undefined,
1. Let |value| be [=DefaultValue=](|valuetype|).
1. Otherwise,
1. If |valuetype| is [=i64=], throw a {{TypeError}} exception.
1. Let |value| be [=ToWebAssemblyValue=](|v|, |valuetype|).
1. If |mutable| is true, let |globaltype| be [=var=] |valuetype|; otherwise, let |globaltype| be [=const=] |valuetype|.
1. Let |store| be the current agent's [=associated store=].
Expand All @@ -884,8 +889,6 @@ which can be simultaneously referenced by multiple {{Instance}} objects. Each
The algorithm <dfn>GetGlobalValue</dfn>({{Global}} |global|) performs the following steps:
1. Let |store| be the current agent's [=associated store=].
1. Let |globaladdr| be |global|.\[[Global]].
1. Let |globaltype| be [=global_type=](|store|, |globaladdr|).
1. If |globaltype| is of the form <var ignore>mut</var> [=i64=], throw a {{TypeError}}.
1. Let |value| be [=global_read=](|store|, |globaladdr|).
1. Return [=ToJSValue=](|value|).
</div>
Expand All @@ -899,7 +902,6 @@ which can be simultaneously referenced by multiple {{Instance}} objects. Each
1. Let |globaladdr| be **this**.\[[Global]].
1. Let |mut| |valuetype| be [=global_type=](|store|, |globaladdr|).
1. If |mut| is [=const=], throw a {{TypeError}}.
1. If |valuetype| is [=i64=], throw a {{TypeError}}.
1. Let |value| be [=ToWebAssemblyValue=](**the given value**, |valuetype|).
1. Let |store| be [=global_write=](|store|, |globaladdr|, |value|).
1. If |store| is [=error=], throw a {{RangeError}} exception.
Expand Down Expand Up @@ -958,11 +960,7 @@ This slot holds a [=function address=] relative to the [=surrounding agent=]'s [

1. Let |store| be the [=surrounding agent=]'s [=associated store=].
1. Let |functype| be [=func_type=](|store|, |funcaddr|).
1. Let [|parameters|] → [|results|] be |functype|.
1. If |parameters| or |results| contains an [=i64=], throw a {{TypeError}}.

Note: the above error is thrown each time the \[[Call]] method is invoked.

1. Let [|parameters|] → [<var ignore>results</var>] be |functype|.
1. Let |args| be an empty [=list=] of WebAssembly values.
1. Let |i| be 0.
1. [=list/iterate|For each=] |t| of |parameters|,
Expand Down Expand Up @@ -990,8 +988,7 @@ Note: Exported Functions do not have a \[[Construct]] method and thus it is not
<div algorithm>
To <dfn>run a host function</dfn> from the JavaScript object |func|, type |functype|, and [=list=] of [=WebAssembly values=] |arguments|, perform the following steps:

1. Let [|parameters|] → [|results|] be |functype|.
1. If either |parameters| or |results| contains [=i64=], throw a {{TypeError}}.
1. Let [<var ignore>parameters</var>] → [|results|] be |functype|.
1. Let |jsArguments| be an empty [=list=].
1. [=list/iterate|For each=] |arg| of |arguments|,
1. [=list/Append=] ! [=ToJSValue=](|arg|) to |jsArguments|.
Expand Down Expand Up @@ -1028,7 +1025,9 @@ Note: Exported Functions do not have a \[[Construct]] method and thus it is not
<div algorithm>
The algorithm <dfn>ToJSValue</dfn>(|w|) coerces a [=WebAssembly value=] to a JavaScript value by performing the following steps:

1. Assert: |w| is not of the form [=i64.const=] <var ignore>i64</var>.
1. If |w| is of the form [=i64.const=] |i64|,
1. Let |v| be [=signed_64=](|i64|).
1. Return a [=BigInt=] representing the mathematical value |v|.
Copy link
Contributor

@xtuc xtuc Feb 20, 2018

Choose a reason for hiding this comment

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

Does "mathematical value" have a special meaning here? I don't think we need to mention math, It's just a value?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

"Mathematical value" is a phrase used in the ECMAScript spec. It's discussed in this section but not really linkable. I'd be fine with deleting the word "mathematical" here if it's more confusing than helpful.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, it's fine for me then. Let's maybe wait for another thought on this.

Copy link

Choose a reason for hiding this comment

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

I think it's fine, but it should probably be "... of |v|". The link is https://tc39.es/ecma262/#mathematical-value, if you want to linkify it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think that's right: ES explicitly says that in the phrase "the mathematical value of x", x is a Number (i.e., a IEEE double) - which it definitely can't be at this point, because then we've lost precision.

Note that the numbers coming from wasm are already treated as "mathematical values" below, as they're passed to "the Number value for x", where x is expected to be a MV. We might want to formalize this a little more, but I'm not quite sure how to best do that.

Unless you feel strongly, I'd like to defer the linkifying until after this is merged.

1. If |w| is of the form [=i32.const=] |i32|, return [=the Number value=] for [=signed_32=](|i32|).
1. If |w| is of the form [=f32.const=] |f32|, return [=the Number value=] for |f32|.
1. If |w| is of the form [=f64.const=] |f64|, return [=the Number value=] for |f64|.
Expand All @@ -1042,7 +1041,9 @@ Note: Number values which are equal to NaN may have various observable NaN paylo
The algorithm <dfn>ToWebAssemblyValue</dfn>(|v|, |type|) coerce a JavaScript value to a [=WebAssembly value=] performs the following steps:


1. Assert: |type| is not [=i64=].
1. If |type| is [=i64=],
1. Let |i64| be ? [=ToBigInt64=](|v|).
1. Return [=i64.const=] |i64|.
1. If |type| is [=i32=],
1. Let |i32| be ? [=ToInt32=](|v|).
1. Return [=i32.const=] |i32|.
Expand Down
58 changes: 44 additions & 14 deletions test/js-api/bad-imports.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@
* (if any) yields the correct error.
*/
function test_bad_imports(t) {
function value_type(type) {
switch (type) {
case "i32": return kWasmI32;
case "i64": return kWasmI64;
case "f32": return kWasmF32;
case "f64": return kWasmF64;
default: throw new TypeError(`Unexpected type ${type}`);
}
}

for (const value of [null, true, "", Symbol(), 1, 0.1, NaN]) {
t(`Non-object imports argument: ${format_value(value)}`,
TypeError,
Expand Down Expand Up @@ -45,17 +55,6 @@ function test_bad_imports(t) {
value);
}

t(`Importing an i64 global`,
WebAssembly.LinkError,
builder => {
builder.addImportedGlobal("module", "global", kWasmI64);
},
{
"module": {
"global": 0,
},
});

for (const value of [undefined, null, true, "", Symbol(), 1, 0.1, NaN, {}]) {
t(`Importing a function with an incorrectly-typed value: ${format_value(value)}`,
WebAssembly.LinkError,
Expand All @@ -82,17 +81,48 @@ function test_bad_imports(t) {
[new WebAssembly.Global({value: "f32"}), "WebAssembly.Global object (wrong value type)"],
];

for (const [value, name = format_value(value)] of nonGlobals) {
t(`Importing a global with an incorrectly-typed value: ${name}`,
for (const type of ["i32", "i64", "f32", "f64"]) {
const extendedNonGlobals = nonGlobals.concat([
type === "i64" ? [0, "Number"] : [0n, "BigInt"]
]);
for (const [value, name = format_value(value)] of extendedNonGlobals) {
t(`Importing an ${type} global with an incorrectly-typed value: ${name}`,
WebAssembly.LinkError,
builder => {
builder.addImportedGlobal("module", "global", value_type(type));
},
{
"module": {
"global": value,
},
});
}
}

for (const type of ["i32", "i64", "f32", "f64"]) {
const value = type === "i64" ? 0n : 0;
t(`Importing an ${type} mutable global with a primitive value`,
WebAssembly.LinkError,
builder => {
builder.addImportedGlobal("module", "global", kWasmI32);
builder.addImportedGlobal("module", "global", value_type(type), true);
},
{
"module": {
"global": value,
},
});

const global = new WebAssembly.Global({ "value": type }, value);
t(`Importing an ${type} mutable global with an immutable Global object`,
WebAssembly.LinkError,
builder => {
builder.addImportedGlobal("module", "global", value_type(type), true);
},
{
"module": {
"global": global,
},
});
}

const nonMemories = [
Expand Down
47 changes: 43 additions & 4 deletions test/js-api/global/constructor.any.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ test(() => {
test(() => {
const argument = { "value": "i64" };
const global = new WebAssembly.Global(argument);
assert_throws_js(TypeError, () => global.value);
assert_throws_js(TypeError, () => global.valueOf());
assert_Global(global, 0n);
}, "i64 with default");

for (const type of ["i32", "f32", "f64"]) {
Expand All @@ -108,8 +107,10 @@ for (const type of ["i32", "f32", "f64"]) {
[false, 0],
[2, 2],
["3", 3],
[{ toString() { return "5" } }, 5, "object with toString"],
[{ valueOf() { return "8" } }, 8, "object with valueOf"],
[{ toString() { return "5" } }, 5, "object with toString returning string"],
[{ valueOf() { return "8" } }, 8, "object with valueOf returning string"],
[{ toString() { return 6 } }, 6, "object with toString returning number"],
[{ valueOf() { return 9 } }, 9, "object with valueOf returning number"],
];
for (const [value, expected, name = format_value(value)] of valueArguments) {
test(() => {
Expand All @@ -118,6 +119,44 @@ for (const type of ["i32", "f32", "f64"]) {
assert_Global(global, expected);
}, `Explicit value ${name} for type ${type}`);
}

test(() => {
const argument = { "value": type };
assert_throws_js(TypeError, () => new WebAssembly.Global(argument, 0n));
}, `BigInt value for type ${type}`);
}

const valueArguments = [
[undefined, 0n],
[true, 1n],
[false, 0n],
["3", 3n],
[123n, 123n],
[{ toString() { return "5" } }, 5n, "object with toString returning string"],
[{ valueOf() { return "8" } }, 8n, "object with valueOf returning string"],
[{ toString() { return 6n } }, 6n, "object with toString returning bigint"],
[{ valueOf() { return 9n } }, 9n, "object with valueOf returning bigint"],
];
for (const [value, expected, name = format_value(value)] of valueArguments) {
test(() => {
const argument = { "value": "i64" };
const global = new WebAssembly.Global(argument, value);
assert_Global(global, expected);
}, `Explicit value ${name} for type i64`);
}

const invalidBigints = [
null,
666,
{ toString() { return 5 } },
{ valueOf() { return 8 } },
Symbol(),
];
for (const invalidBigint of invalidBigints) {
test(() => {
var argument = { "value": "i64" };
assert_throws_js(TypeError, () => new WebAssembly.Global(argument, invalidBigint));
}, `Pass non-bigint as i64 Global value: ${format_value(invalidBigint)}`);
}

test(() => {
Expand Down
72 changes: 44 additions & 28 deletions test/js-api/global/value-get-set.any.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ test(() => {
}
}, "Branding");

for (const type of ["i32", "f32", "f64"]) {
for (const type of ["i32", "i64", "f32", "f64"]) {
const [initial, value, invalid] = type === "i64" ? [0n, 1n, 2] : [0, 1, 2n];
const immutableOptions = [
[{}, "missing"],
[{ "mutable": undefined }, "undefined"],
Expand All @@ -41,29 +42,29 @@ for (const type of ["i32", "f32", "f64"]) {
test(() => {
opts.value = type;
const global = new WebAssembly.Global(opts);
assert_equals(global.value, 0, "initial value");
assert_equals(global.valueOf(), 0, "initial valueOf");
assert_equals(global.value, initial, "initial value");
assert_equals(global.valueOf(), initial, "initial valueOf");

assert_throws_js(TypeError, () => global.value = 1);
assert_throws_js(TypeError, () => global.value = value);

assert_equals(global.value, 0, "post-set value");
assert_equals(global.valueOf(), 0, "post-set valueOf");
assert_equals(global.value, initial, "post-set value");
assert_equals(global.valueOf(), initial, "post-set valueOf");
}, `Immutable ${type} (${name})`);

test(t => {
opts.value = type;
const global = new WebAssembly.Global(opts);
assert_equals(global.value, 0, "initial value");
assert_equals(global.valueOf(), 0, "initial valueOf");
assert_equals(global.value, initial, "initial value");
assert_equals(global.valueOf(), initial, "initial valueOf");

const value = {
valueOf: t.unreached_func("should not call valueOf"),
toString: t.unreached_func("should not call toString"),
};
assert_throws_js(TypeError, () => global.value = value);

assert_equals(global.value, 0, "post-set value");
assert_equals(global.valueOf(), 0, "post-set valueOf");
assert_equals(global.value, initial, "post-set value");
assert_equals(global.valueOf(), initial, "post-set valueOf");
}, `Immutable ${type} with ToNumber side-effects (${name})`);
}

Expand All @@ -77,34 +78,49 @@ for (const type of ["i32", "f32", "f64"]) {
test(() => {
opts.value = type;
const global = new WebAssembly.Global(opts);
assert_equals(global.value, 0, "initial value");
assert_equals(global.valueOf(), 0, "initial valueOf");
assert_equals(global.value, initial, "initial value");
assert_equals(global.valueOf(), initial, "initial valueOf");

global.value = value;

global.value = 1;
assert_throws_js(TypeError, () => global.value = invalid);

assert_equals(global.value, 1, "post-set value");
assert_equals(global.valueOf(), 1, "post-set valueOf");
assert_equals(global.value, value, "post-set value");
assert_equals(global.valueOf(), value, "post-set valueOf");
}, `Mutable ${type} (${name})`);
}
}

test(() => {
const argument = { "value": "i64", "mutable": true };
const global = new WebAssembly.Global(argument);
assert_throws_js(TypeError, () => global.value);
assert_throws_js(TypeError, () => global.value = 0);
assert_throws_js(TypeError, () => global.valueOf());
}, "i64 with default");

test(t => {
const argument = { "value": "i64", "mutable": true };
const global = new WebAssembly.Global(argument);
const value = {
valueOf: t.unreached_func("should not call valueOf"),
toString: t.unreached_func("should not call toString"),
};
assert_throws_js(TypeError, () => global.value = value);
}, "i64 with ToNumber side-effects");
assert_equals(global.value, 0n, "initial value using ToJSValue");

const valid = [
[123n, 123n],
[2n ** 63n, - (2n ** 63n)],
[true, 1n],
[false, 0n],
["456", 456n],
];
for (const [input, output] of valid) {
global.value = input;
assert_equals(global.valueOf(), output, "post-set valueOf");
}

const invalid = [
undefined,
null,
0,
1,
4.2,
Symbol(),
];
for (const input of invalid) {
assert_throws_js(TypeError, () => global.value = input);
}
}, "i64 mutability");

test(() => {
const argument = { "value": "i32", "mutable": true };
Expand Down
Loading