diff --git a/document/js-api/index.bs b/document/js-api/index.bs index 7d172c81c5..26f4665706 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -88,6 +88,14 @@ urlPrefix: https://tc39.github.io/ecma262/; spec: ECMASCRIPT text: ℤ; url: #ℤ text: mathematical value; url: #mathematical-value text: SameValue; url: sec-samevalue + text: IsStrictlyEqual; url: sec-isstrictlyequal + text: IsLessThan; url: sec-islessthan + text: String.fromCharCode; url: sec-string.fromcharcode + text: String.fromCodePoint; url: sec-string.fromcodepoint + text: String.prototype.charCodeAt; url: sec-string.prototype.charcodeat + text: String.prototype.codePointAt; url: sec-string.prototype.codepointat + text: String.prototype.concat; url: sec-string.prototype.concat + text: String.prototype.substring; url: sec-string.prototype.substring text: Array; url: sec-array-exotic-objects text: BigInt; url: sec-ecmascript-language-types-bigint-type urlPrefix: https://webassembly.github.io/spec/core/; spec: WebAssembly; type: dfn @@ -152,6 +160,7 @@ urlPrefix: https://webassembly.github.io/spec/core/; spec: WebAssembly; type: df text: ref_type; url: appendix/embedding.html#embed-ref-type text: val_default; url: appendix/embedding.html#embed-val-default text: match_valtype; url: appendix/embedding.html#embed-match-valtype + text: match_externtype; url: appendix/embedding.html#embed-match-externtype text: error; url: appendix/embedding.html#embed-error text: store; url: exec/runtime.html#syntax-store text: address type; url: syntax/types.html#syntax-addrtype @@ -351,13 +360,18 @@ dictionary WebAssemblyInstantiatedSource { required Instance instance; }; +dictionary WebAssemblyCompileOptions { + USVString? importedStringConstants; + sequence<USVString> builtins; +}; + [Exposed=*] namespace WebAssembly { - boolean validate(BufferSource bytes); - Promise<Module> compile(BufferSource bytes); + boolean validate(BufferSource bytes, optional WebAssemblyCompileOptions options = {}); + Promise<Module> compile(BufferSource bytes, optional WebAssemblyCompileOptions options = {}); Promise<WebAssemblyInstantiatedSource> instantiate( - BufferSource bytes, optional object importObject); + BufferSource bytes, optional object importObject, optional WebAssemblyCompileOptions options = {}); Promise<Instance> instantiate( Module moduleObject, optional object importObject); @@ -384,10 +398,26 @@ Note:
- The validate(|bytes|) method, when invoked, performs the following steps: + To validate builtins and imported string for a WebAssembly module from module |module|, enabled builtins |builtinSetNames|, and |importedStringModule|, perform the following steps: + 1. If [=validate builtin set names|validating builtin set names=] for |builtinSetNames| is false, return false. + 1. [=list/iterate|For each=] |import| of [=module_imports=](|module|), + 1. If |importedStringModule| is not null and |import|[0] equals |importedStringModule|, + 1. Let |importExternType| be |import|[2] + 1. Let |stringExternType| be `global const (ref extern)` + 1. If [=match_externtype=](|stringExternType|, |importExternType|) is false, return false + 1. Else, + 1. If [=validate an import for builtins|validating a import for builtin=] with |import| and |builtinSetNames| is false, return false. + 1. Return true. +
+ +
+ The validate(|bytes|, |options|) method, when invoked, performs the following steps: 1. Let |stableBytes| be a [=get a copy of the buffer source|copy of the bytes held by the buffer=] |bytes|. 1. [=Compile a WebAssembly module|Compile=] |stableBytes| as a WebAssembly module and store the results as |module|. 1. If |module| is [=error=], return false. + 1. Let |builtinSetNames| be |options|["builtins"] + 1. Let |importedStringModule| be |options|["importedStringConstants"] + 1. If [=validate builtins and imported string for a WebAssembly module|validating builtins and imported strings=] for |module| with |builtinSetNames| and |importedStringModule| returns false, return false. 1. Return true.
@@ -395,42 +425,77 @@ A {{Module}} object represents a single WebAssembly module. Each {{Module}} obje * \[[Module]] : a WebAssembly [=/module=] * \[[Bytes]] : the source bytes of \[[Module]]. + * \[[BuiltinSets]] : an ordered set of names of builtin sets + * \[[ImportedStringModule]] : an optional module specifier string where any string constant can be imported from.
- To construct a WebAssembly module object from a module |module| and source bytes |bytes|, perform the following steps: + To construct a WebAssembly module object from a module |module|, source bytes |bytes|, enabled builtins |builtinSetNames|, and |importedStringModule|, perform the following steps: 1. Let |moduleObject| be a new {{Module}} object. 1. Set |moduleObject|.\[[Module]] to |module|. 1. Set |moduleObject|.\[[Bytes]] to |bytes|. + 1. Set |moduleObject|.\[[BuiltinSets]] to |builtinSetNames|. + 1. Set |moduleObject|.\[[ImportedStringModule]] to |importedStringModule|. 1. Return |moduleObject|.
- To asynchronously compile a WebAssembly module from source bytes |bytes|, using optional [=task source=] |taskSource|, perform the following steps: + To asynchronously compile a WebAssembly module from source bytes |bytes| and {{WebAssemblyCompileOptions}} |options| using optional [=task source=] |taskSource|, perform the following steps: 1. Let |promise| be [=a new promise=]. 1. Run the following steps [=in parallel=]: 1. [=compile a WebAssembly module|Compile the WebAssembly module=] |bytes| and store the result as |module|. 1. [=Queue a task=] to perform the following steps. If |taskSource| was provided, queue the task on that task source. 1. If |module| is [=error=], reject |promise| with a {{CompileError}} exception. + 1. Let |builtinSetNames| be |options|["builtins"] + 1. Let |importedStringModule| be |options|["importedStringConstants"] + 1. If [=validate builtins and imported string for a WebAssembly module|validating builtins and imported strings=] for |module| with |builtinSetNames| and |importedStringModule| is false, reject |promise| with a {{CompileError}} exception. 1. Otherwise, - 1. [=Construct a WebAssembly module object=] from |module| and |bytes|, and let |moduleObject| be the result. + 1. [=Construct a WebAssembly module object=] from |module|, |bytes|, |builtinSetNames|, |importedStringModule|, and let |moduleObject| be the result. 1. [=Resolve=] |promise| with |moduleObject|. 1. Return |promise|.
- The compile(|bytes|) method, when invoked, performs the following steps: + The compile(|bytes|, |options|) method, when invoked, performs the following steps: 1. Let |stableBytes| be a [=get a copy of the buffer source|copy of the bytes held by the buffer=] |bytes|. - 1. [=Asynchronously compile a WebAssembly module=] from |stableBytes| and return the result. + 1. [=Asynchronously compile a WebAssembly module=] from |stableBytes| using |options| and return the result. +
+ +
+To instantiate imported strings with module |module| and |importedStringModule|, perform the following steps: +1. Assert: |importedStringModule| is not null. +1. Let |exportsObject| be [=!=] [$OrdinaryObjectCreate$](null). +1. [=list/iterate|For each=] (|moduleName|, |componentName|, |externtype|) of [=module_imports=](|module|), + 1. If |moduleName| does not equal |importedStringModule|, then [=iteration/continue=]. + 1. Let |stringConstant| be |componentName|. + 1. Let |status| be [=!=] [$CreateDataProperty$](|exportsObject|, |stringConstant|, |stringConstant|). + 1. Assert: |status| is true. +1. Return |exportsObject|. +
- To read the imports from a WebAssembly module |module| from imports object |importObject|, perform the following steps: + To read the imports from a WebAssembly module |module| from imports object |importObject|, enabled builtins |builtinSetNames|, and |importedStringModule|, perform the following steps: 1. If |module|.[=imports=] [=list/is empty|is not empty=], and |importObject| is undefined, throw a {{TypeError}} exception. + 1. Let |builtinOrStringImports| be the ordered map « ». + 1. [=list/iterate|For each=] |builtinSetName| of |builtinSetNames|, + 1. Assert: |builtinOrStringImports| does not contain |builtinSetName| + 1. If |builtinSetName| does not refer to a builtin set, then [=iteration/continue=]. + 1. Let |exportsObject| be the result of [=instantiate a builtin set=] with |builtinSetName| + 1. Let |builtinSetQualifiedName| be |builtinSetName| prefixed with "wasm:" + 1. [=map/set|Set=] |builtinOrStringImports|[|builtinSetQualifiedName|] to |exportsObject| + 1. If |importedStringModule| is not null, + 1. Let |exportsObject| be the result of [=instantiate imported strings=] with |module| and |importedStringModule| + 1. [=map/set|Set=] |builtinOrStringImports|[|importedStringModule|] to |exportsObject| 1. Let |imports| be « ». 1. [=list/iterate|For each=] (|moduleName|, |componentName|, |externtype|) of [=module_imports=](|module|), - 1. Let |o| be [=?=] [$Get$](|importObject|, |moduleName|). + 1. If |builtinOrStringImports| [=map/exist|contains=] |moduleName|, + 1. Let |o| be |builtinOrStringImports|[|moduleName|]. + 1. If |o| [=is not an Object=] or if |o| [=map/exist|does not contain=] |componentName|, + 1. Set |o| to [=?=] [$Get$](|importObject|, |moduleName|). + 1. Else, + 1. Let |o| be [=?=] [$Get$](|importObject|, |moduleName|). 1. If |o| [=is not an Object=], throw a {{TypeError}} exception. 1. Let |v| be [=?=] [$Get$](|o|, |componentName|). 1. If |externtype| is of the form [=external-type/func=] |functype|, @@ -446,9 +511,9 @@ A {{Module}} object represents a single WebAssembly module. Each {{Module}} obje 1. If |v| [=implements=] {{Global}}, 1. Let |globaladdr| be |v|.\[[Global]]. 1. Otherwise, - 1. If |valtype| is [=i64=] and [=Type=](|v|) is not BigInt, + 1. If |valtype| is [=i64=] and |v| [=is not a BigInt=], 1. Throw a {{LinkError}} exception. - 1. If |valtype| is one of [=i32=], [=f32=] or [=f64=] and [=Type=](|v|) is not Number, + 1. If |valtype| is one of [=i32=], [=f32=] or [=f64=] and |v| [=is not a Number=], 1. Throw a {{LinkError}} exception. 1. If |valtype| is [=v128=], 1. Throw a {{LinkError}} exception. @@ -545,7 +610,9 @@ The verification of WebAssembly type requirements is deferred to the To asynchronously instantiate a WebAssembly module from a {{Module}} |moduleObject| and imports |importObject|, perform the following steps: 1. Let |promise| be [=a new promise=]. 1. Let |module| be |moduleObject|.\[[Module]]. - 1. [=Read the imports=] of |module| with imports |importObject|, and let |imports| be the result. + 1. Let |builtinSetNames| be |moduleObject|.\[[BuiltinSets]]. + 1. Let |importedStringModule| be |moduleObject|.\[[ImportedStringModule]]. + 1. [=Read the imports=] of |module| with imports |importObject|, |builtinSetNames| and |importedStringModule|, and let |imports| be the result. If this operation throws an exception, catch it, [=reject=] |promise| with the exception, and return |promise|. 1. Run the following steps [=in parallel=]: 1. [=Queue a task=] to perform the following steps: @@ -576,9 +643,9 @@ The verification of WebAssembly type requirements is deferred to the
- The instantiate(|bytes|, |importObject|) method, when invoked, performs the following steps: + The instantiate(|bytes|, |importObject|, |options|) method, when invoked, performs the following steps: 1. Let |stableBytes| be a [=get a copy of the buffer source|copy of the bytes held by the buffer=] |bytes|. - 1. [=Asynchronously compile a WebAssembly module=] from |stableBytes| and let |promiseOfModule| be the result. + 1. [=Asynchronously compile a WebAssembly module=] from |stableBytes| using |options| and let |promiseOfModule| be the result. 1. [=Instantiate a promise of a module|Instantiate=] |promiseOfModule| with imports |importObject| and return the result.
@@ -626,7 +693,7 @@ dictionary ModuleImportDescriptor { [LegacyNamespace=WebAssembly, Exposed=*] interface Module { - constructor(BufferSource bytes); + constructor(BufferSource bytes, optional WebAssemblyCompileOptions options = {}); static sequence<ModuleExportDescriptor> exports(Module moduleObject); static sequence<ModuleImportDescriptor> imports(Module moduleObject); static sequence<ArrayBuffer> customSections(Module moduleObject, DOMString sectionName); @@ -656,8 +723,12 @@ interface Module {
The imports(|moduleObject|) method, when invoked, performs the following steps: 1. Let |module| be |moduleObject|.\[[Module]]. + 1. Let |builtinSetNames| be |moduleObject|.\[[BuiltinSets]]. + 1. Let |importedStringModule| be |moduleObject|.\[[ImportedStringModule]]. 1. Let |imports| be « ». 1. [=list/iterate|For each=] (|moduleName|, |name|, |type|) of [=module_imports=](|module|), + 1. If [=find a builtin=] for (|moduleName|, |name|, |type|) and |builtinSetNames| is not null, then [=iteration/continue=] + 1. If |importedStringModule| is not null and |moduleName| equals |importedStringModule|, then [=iteration/continue=] 1. Let |kind| be the [=string value of the extern type=] |type|. 1. Let |obj| be «[ "{{ModuleImportDescriptor/module}}" → |moduleName|, "{{ModuleImportDescriptor/name}}" → |name|, "{{ModuleImportDescriptor/kind}}" → |kind| ]». 1. [=list/Append=] |obj| to |imports|. @@ -677,13 +748,18 @@ interface Module {
- The Module(|bytes|) constructor, when invoked, performs the following steps: + The Module(|bytes|, |options|) constructor, when invoked, performs the following steps: 1. Let |stableBytes| be a [=get a copy of the buffer source|copy of the bytes held by the buffer=] |bytes|. 1. [=Compile a WebAssembly module|Compile the WebAssembly module=] |stableBytes| and store the result as |module|. 1. If |module| is [=error=], throw a {{CompileError}} exception. + 1. Let |builtinSetNames| be |options|["builtins"] + 1. Let |importedStringModule| be |options|["importedStringConstants"] + 1. If [=validate builtins and imported string for a WebAssembly module|validating builtins and imported strings=] for |module| with |builtinSetNames| and |importedStringModule| returns false, throw a {{CompileError}} exception. 1. Set **this**.\[[Module]] to |module|. 1. Set **this**.\[[Bytes]] to |stableBytes|. + 1. Set **this**.\[[BuiltinSets]] to |builtinSetNames|. + 1. Set **this**.\[[ImportedStringModule]] to |importedStringModule|. Note: Some implementations enforce a size limitation on |bytes|. Use of this API is discouraged, in favor of asynchronous APIs.
@@ -701,7 +777,9 @@ interface Instance {
The Instance(|module|, |importObject|) constructor, when invoked, runs the following steps: 1. Let |module| be |module|.\[[Module]]. - 1. [=Read the imports=] of |module| with imports |importObject|, and let |imports| be the result. + 1. Let |builtinSetNames| be |module|.\[[BuiltinSets]]. + 1. Let |importedStringModule| be |module|.\[[ImportedStringModule]]. + 1. [=Read the imports=] of |module| with imports |importObject|, |builtinSetNames|, and |importedStringModule|, and let |imports| be the result. 1. [=Instantiate the core of a WebAssembly module=] |module| with |imports|, and let |instance| be the result. 1. [=initialize an instance object|Initialize=] **this** from |module| and |instance|. @@ -1739,6 +1817,327 @@ They expose the same interface as native JavaScript errors like {{TypeError}} an Note: It is not currently possible to define this behavior using Web IDL. +

Builtins

+ +The JS-API defines sets of builtin functions which can be imported through {{WebAssemblyCompileOptions|options}} when compiling a module. WebAssembly builtin functions mirror existing JavaScript builtins, but adapt them to be useable directly as WebAssembly functions with minimal overhead. + +All builtin functions are grouped into sets. Every builtin set has a |name| that is used in {{WebAssemblyCompileOptions}}, and a |qualified name| with a `wasm:` prefix that [=read the imports|is used during import lookup=]. + +
+To get the builtins for a builtin set with |builtinSetName|, perform the following steps: + +1. Return a list of (|name|, |funcType|, |steps|) for the set with name |builtinSetName| defined within this section. + +
+ +
+To find a builtin with |import| and enabled builtins |builtinSetNames|, perform the following steps: + +1. Assert: [=validate builtin set names=] |builtinSetNames| is true. +1. Let |importModuleName| be |import|[0] +1. Let |importName| be |import|[1] +1. [=list/iterate|For each=] |builtinSetName| of |builtinSetNames|, + 1. If |builtinSetName| does not refer to a builtin set, then [=iteration/continue=]. + 1. Let |builtinSetQualifiedName| be |builtinSetName| prefixed with "wasm:" + 1. If |importModuleName| equals |builtinSetQualifiedName| + 1. Let |builtins| be the result of [=get the builtins for a builtin set=] |builtinSetName| + 1. [=list/iterate|For each=] |builtin| of |builtins| + 1. Let |builtinName| be |builtin|[0] + 1. If |importName| equals |builtinName|, return (|builtinSetName|, |builtin|). +1. Return null. + +
+ +
+To validate builtin set names with |builtinSetNames|, perform the following steps: + +1. If |builtinSetNames| contains any duplicates, return false. +1. Return true. + +
+ +
+ To create a builtin function from type |funcType| and execution steps |steps|, perform the following steps: + + 1. Let |stored settings| be the incumbent settings object. + 1. Let |hostfunc| be a [=host function=] which executes |steps| when called. + 1. Let (|store|, |funcaddr|) be [=func_alloc=](|store|, |functype|, |hostfunc|). + 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. + 1. Return |funcaddr|. + +
+ +
+To instantiate a builtin set with name |builtinSetName|, perform the following steps: + +1. Let |builtins| be the result of [=get the builtins for a builtin set=] |builtinSetName| +1. Let |exportsObject| be [=!=] [$OrdinaryObjectCreate$](null). +1. [=list/iterate|For each=] (|name|, |funcType|, |steps|) of |builtins|, + 1. Let |funcaddr| be the result fo [=create a builtin function=] with |funcType| and |steps| + 1. Let |func| be the result of creating [=a new Exported Function=] from |funcaddr|. + 1. Let |value| be |func|. + 1. Let |status| be [=!=] [$CreateDataProperty$](|exportsObject|, |name|, |value|). + 1. Assert: |status| is true. +1. Return |exportsObject|. + +
+ +
+To validate an import for builtins with |import|, enabled builtins |builtinSetNames|, perform the following steps: + +1. Assert: [=validate builtin set names=] |builtinSetNames| is true. +1. Let |maybeBuiltin| be the result of [=find a builtin|finding a builtin=] for |import| and |builtinSetNames| +1. If |maybeBuiltin| is null, return true. +1. Let |importExternType| be |import|[2] +1. Let |builtinFuncType| be |maybeBuiltin|[0][1] +1. Let |builtinExternType| be `func |builtinFuncType|` +1. Return [=match_externtype=](|builtinExternType|, |importExternType|) + +
+ + +

String Builtins

+ +String builtins adapt the interface of the [=String=] builtin object. The |name| for this set is `js-string`. + +Note: The algorithms in this section refer to JS builtins defined on [=String=]. These refer to the actual builtin and do not perform a dynamic lookup on the [=String=] object. + +

Abstract operations

+
+ +The UnwrapString(|v|) abstract operation, when invoked, performs the following steps: + +1. If |v| [=is not a String=] + 1. Throw a {{RuntimeError}} exception as if a [=trap=] was executed. +1. Return |v| + +
+ +
+ +The FromCharCode(|v|) abstract operation, when invoked, performs the following steps: + +1. Assert: |v| is of type [=i32=]. +1. Return [=!=] [$Call$]([=String.fromCharCode=], undefined, « [=ToJSValue=](|v|) »). + +
+ +
+ +The CharCodeAt(|string|, |index|) abstract operation, when invoked, performs the following steps: + +1. Assert: |index| is of type [=i32=]. +1. Return [=!=] [$Call$]([=String.prototype.charCodeAt=], |string|, « [=ToJSValue=](|index|) »). + +
+ +

cast

+ +The |funcType| of this builtin is `(func (param externref) (result externref))`. + +
+When this builtin is invoked with parameter |v|, the following steps must be run: + +1. Return [=?=] [$UnwrapString$](|v|) + +
+ +

test

+ +The |funcType| of this builtin is `(func (param externref) (result i32))`. + +
+When this builtin is invoked with parameter |v|, the following steps must be run: + +1. If |v| [=is not a String=] + 1. Return 0 +1. Return 1 + +
+ +

fromCharCodeArray

+ +The |funcType| of this builtin is `(func (param (ref null (array (mut i16))) i32 i32) (result externref))`. + +Note: This function only takes a mutable i16 array defined in its own recursion group. + +
+When this builtin is invoked with parameters |array|, |start|, and |end|, the following steps must be run: + +1. If |array| is null + 1. Throw a {{RuntimeError}} exception as if a [=trap=] was executed. +1. Let |length| be the number of elements in |array|. +1. If |start| > |end| or |end| > |length| + 1. Throw a {{RuntimeError}} exception as if a [=trap=] was executed. +1. Let |result| be the empty string. +1. Let |i| be |start|. +1. While |i| < |end|: + 1. Let |charCode| be the value of the element stored at index |i| in |array|. + 1. Let |charCodeString| be [$FromCharCode$](|charCode|). + 1. Let |result| be the concatenation of |result| and |charCodeString|. + 1. Set |i| to |i| + 1. +1. Return |result|. + +
+ +

intoCharCodeArray

+ +The |funcType| of this builtin is `(func (param externref (ref null (array (mut i16))) i32) (result i32))`. + +Note: This function only takes a mutable i16 array defined in its own recursion group. + +
+When this builtin is invoked with parameters |string|, |array|, and |start|, the following steps must be run: + +1. If |array| is null + 1. Throw a {{RuntimeError}} exception as if a [=trap=] was executed. +1. Let |string| be [=?=] [$UnwrapString$](|string|). +1. Let |stringLength| be the [=string/length=] of |string|. +1. Let |arrayLength| be the number of elements in |array|. +1. If |start| + |stringLength| > |arrayLength| + 1. Throw a {{RuntimeError}} exception as if a [=trap=] was executed. +1. Let |i| be 0. +1. While |i| < |stringLength|: + 1. Let |charCode| be [$CharCodeAt$](|string|, |i|). + 1. Set the element at index |start| + |i| in |array| to [=ToWebAssemblyValue=](|charCode|). + 1. Set |i| to |i| + 1. +1. Return |stringLength|. + +
+ + +

fromCharCode

+ +The |funcType| of this builtin is `(func (param i32) (result externref))`. + +
+When this builtin is invoked with parameter |v|, the following steps must be run: + +1. Return [$FromCharCode$](|v|). + +
+ +

fromCodePoint

+ +The |funcType| of this builtin is `(func (param i32) (result externref))`. + +
+When this builtin is invoked with parameter |v|, the following steps must be run: + +1. If |v| > 0x10ffff + 1. Throw a {{RuntimeError}} exception as if a [=trap=] was executed. +1. Return [=!=] [$Call$]([=String.fromCodePoint=], undefined, « [=ToJSValue=](|v|) »). + +
+ +

charCodeAt

+ +The type of this function is `(func (param externref i32) (result i32))`. + +
+When this builtin is invoked with parameters |string| and |index|, the following steps must be run: + +1. Let |string| be [=?=] [$UnwrapString$](|string|). +1. Let |length| be the [=string/length=] of |string|. +1. If |index| >= |length| + 1. Throw a {{RuntimeError}} exception as if a [=trap=] was executed. +1. Return [$CharCodeAt$](|string|, |index|). + +
+ +

codePointAt

+ +The type of this function is `(func (param externref i32) (result i32))`. + +
+When this builtin is invoked with parameters |string| and |index|, the following steps must be run: + +1. Let |string| be [=?=] [$UnwrapString$](|string|). +1. Let |length| be the [=string/length=] of |string|. +1. If |index| >= |length| + 1. Throw a {{RuntimeError}} exception as if a [=trap=] was executed. +1. Return [=!=] [$Call$]([=String.prototype.codePointAt=], |string|, « [=ToJSValue=](|index|) »). + +
+ +

length

+ +The |funcType| of this builtin is `(func (param externref) (result i32))`. + +
+When this builtin is invoked with parameter |v|, the following steps must be run: + +1. Let |string| be [=?=] [$UnwrapString$](|v|). +1. Return the [=string/length=] of |string|. + +
+ +

concat

+ +The |funcType| of this builtin is `(func (param externref externref) (result externref))`. + +
+When this builtin is invoked with parameters |first| and |second|, the following steps must be run: + +1. Let |first| be [=?=] [$UnwrapString$](|first|). +1. Let |second| be [=?=] [$UnwrapString$](|second|). +1. Return [=!=] [$Call$]([=String.prototype.concat=], |first|, « |second| »). + +
+ +

substring

+ +The |funcType| of this builtin is `(func (param externref i32 i32) (result externref))`. + +
+When this builtin is invoked with parameters |string|, |start|, and |end|, the following steps must be run: + +1. Let |string| be [=?=] [$UnwrapString$](|string|). +1. Let |length| be the [=string/length=] of |string|. +1. If |start| > |end| or |start| > |length| + 1. Return the empty string. +1. Return [=!=] [$Call$]([=String.prototype.substring=], |string|, « [=ToJSValue=](|start|), [=ToJSValue=](|end|) »). + +
+ +

equals

+ +The |funcType| of this builtin is `(func (param externref externref) (result i32))`. + +Note: Explicitly allow null strings to be compared for equality as that is meaningful. + +
+ +When this builtin is invoked with parameters |first| and |second|, the following steps must be run: + +1. If |first| is not null and |first| [=is not a String=] + 1. Throw a {{RuntimeError}} exception as if a [=trap=] was executed. +1. If |second| is not null and |second| [=is not a String=] + 1. Throw a {{RuntimeError}} exception as if a [=trap=] was executed. +1. If [=!=] [=IsStrictlyEqual=](|first|, |second|) is true + 1. Return 1. +1. Return 0. + +
+ +

compare

+ +The |funcType| of this builtin is `(func (param externref externref) (result i32))`. + +
+ +When this builtin is invoked with parameters |first| and |second|, the following steps must be run: + +1. Let |first| be [=?=] [$UnwrapString$](|first|). +1. Let |second| be [=?=] [$UnwrapString$](|second|). +1. If [=!=] [=IsStrictlyEqual=](|first|, |second|) is true + 1. Return 0. +1. If [=!=] [=IsLessThan=](|first|, |second|, true) is true + 1. Return -1. +1. Return 1. + +
+

Error Condition Mappings to JavaScript

Running WebAssembly programs encounter certain events which halt execution of the WebAssembly code. diff --git a/document/web-api/index.bs b/document/web-api/index.bs index a63ee60bbc..faa044e3e0 100644 --- a/document/web-api/index.bs +++ b/document/web-api/index.bs @@ -88,25 +88,25 @@ additional APIs that are implemented by Web user agents but are outside the scop
 [Exposed=(Window,Worker)]
 partial namespace WebAssembly {
-  Promise<Module> compileStreaming(Promise<Response> source);
+  Promise<Module> compileStreaming(Promise<Response> source, optional WebAssemblyCompileOptions options);
   Promise<WebAssemblyInstantiatedSource> instantiateStreaming(
-      Promise<Response> source, optional object importObject);
+      Promise<Response> source, optional object importObject, optional WebAssemblyCompileOptions options);
 };
 
-The compileStreaming(|source|) method, when invoked, returns the result of [=compile a potential WebAssembly response|compiling a potential WebAssembly response=] with |source|. +The compileStreaming(|source|, |options|) method, when invoked, returns the result of [=compile a potential WebAssembly response|compiling a potential WebAssembly response=] with |source| using |options|.
-The instantiateStreaming(|source|, |importObject|) method, when invoked, performs the following steps: +The instantiateStreaming(|source|, |importObject|, |options|) method, when invoked, performs the following steps: - 1. Let |promiseOfModule| be the result of [=compile a potential WebAssembly response|compiling a potential WebAssembly response=] with |source|. + 1. Let |promiseOfModule| be the result of [=compile a potential WebAssembly response|compiling a potential WebAssembly response=] with |source| using |options|. 1. Return the result of [=instantiate a promise of a module|instantiating the promise of a module=] |promiseOfModule| with imports |importObject|.
-To compile a potential WebAssembly response with a promise of a {{Response}} |source|, perform the following steps: +To compile a potential WebAssembly response with a promise of a {{Response}} |source| and {{WebAssemblyCompileOptions}} |options|, perform the following steps: Note: This algorithm accepts a {{Response}} object, or a promise for one, and compiles and instantiates the resulting bytes of the response. This compilation @@ -135,7 +135,7 @@ Note: This algorithm accepts a {{Response}} object, or a 1. [=Upon fulfillment=] of |bodyPromise| with value |bodyArrayBuffer|: 1. Let |stableBytes| be a [=get a copy of the buffer source|copy of the bytes held by the buffer=] |bodyArrayBuffer|. - 1. [=Asynchronously compile a WebAssembly module|Asynchronously compile the WebAssembly module=] |stableBytes| using the [=networking task source=] and [=resolve=] |returnValue| with the result. + 1. [=Asynchronously compile a WebAssembly module|Asynchronously compile the WebAssembly module=] |stableBytes| using the [=networking task source=] and |options| and [=resolve=] |returnValue| with the result. 1. [=Upon rejection=] of |bodyPromise| with reason |reason|: 1. [=Reject=] |returnValue| with |reason|. 1. [=Upon rejection=] of |source| with reason |reason|: diff --git a/proposals/js-string-builtins/Overview.md b/proposals/js-string-builtins/Overview.md new file mode 100644 index 0000000000..6528301602 --- /dev/null +++ b/proposals/js-string-builtins/Overview.md @@ -0,0 +1,801 @@ +# JS String Builtins + +## Motivation + +JavaScript runtimes have a rich set of [builtin objects and primitives](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects). Some languages targeting WebAssembly may have compatible primitives and would benefit from being able to use the equivalent JavaScript primitive for their implementation. The most pressing use-case here is for languages who would like to use the JavaScript [`String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) type to implement their strings. + +It is already possible to use any JavaScript or Web API from WebAssembly by importing JavaScript 'glue code' which adapts between JavaScript and WebAssembly values and calling conventions. Usually, this has a negligible performance impact and work has been done to [optimize this in runtimes when we can](https://hacks.mozilla.org/2018/10/calls-between-javascript-and-webassembly-are-finally-fast-%F0%9F%8E%89/). + +However, the overhead of importing glue code is prohibitive for primitives such as [`String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String), [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer), [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp), [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), and [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) where the desired overhead of operations is a tight sequence of inline instructions, not an indirect function call (which is typical of imported functions). + +## Overview + +This proposal aims to provide a minimal and general mechanism for importing specific JavaScript primitives for efficient usage in WebAssembly code. + +This is done by first adding a set of Wasm builtin functions for performing JavaScript String operations. These builtin functions mirror a subset of the [JavaScript String API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) and adapt it to be efficiently callable without JavaScript glue code. + +Then a mechanism for importing these Wasm builtin functions is added to the WebAssembly JS-API. These builtins are grouped in modules and exist in a new reserved import namespace `wasm:` that is enabled at compile-time with a flag. + +These two pieces in combination allow runtimes to reliably emit optimal code sequences for JavaScript string operations within WebAssembly modules. In the future, other JS builtin objects or JS primitives can be exposed through new Wasm builtins. + +## Do we need new Wasm builtin functions? + +It is already possible today to import JS builtin functions (such as [`String.prototoype.charCodeAt()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt)) from Wasm modules. Instead of defining new Wasm specific-builtins, we could just re-use those directly. + +There are several problems with this approach. + +The first problem is that existing APIs require a calling convention conversion to handle differences around the `this` value, which WebAssembly function import calls leave as `undefined`. The second problem is that certain primitives use JS operators such as `===` and `<` that cannot be imported. A third problem is that most JS builtins are extremely permissive of the types of values they accept, and it's desirable to leverage Wasm's type system to remove those checks and coercions wherever we can. + +It seems that creating new importable definitions that adapt existing JS primitives to WebAssembly is simpler and more flexible in the future. + +## Do we need a new import mechanism for Wasm builtin functions? + +There is a variety of execution techniques for WebAssembly. Some WebAssembly engines compile modules eagerly (at `WebAssembly.compile()`), some use interpreters and dynamic tiering, and some use on-demand compilation (after instantiation) and dynamic tiering. + +If we just have builtin functions, it would be possible to normally import them through instantiation. However this would prevent engines from using eager compilation when builtins are in use. + +It seems desirable to support a variety of execution techniques, especially because engines may support multiple depending on heuristics or change them over time. + +By adding builtins that are in a reserved and known namespace (`wasm:`), engines can know that these builtin functions are being used at `WebAssembly.compile()` time and generate optimal code for them. + +## Goals for builtins + +Builtins should not provide any new abilities to WebAssembly that JS doesn't already have. They are intended to just wrap existing primitives in such a manner that WebAssembly can efficiently use them. In the cases the primitive already has a name, we should re-use it and not invent a new one. + +Most builtins should be simple and do little work outside of calling into the JS functionality to do the operation. The one exception is for operations that convert between a JS primitive and a Wasm primitive, such as between JS strings/arrays/linear memory. In this case, the builtin may need some non-trivial code to perform the operation and it's still expected that the operation is just semantically copying information and not substantially transforming it into a new interpretation. + +The standardization of Wasm builtins will be governed by the WebAssembly standards process and would exist in the JS-API document. + +The bar for adding a new builtin would be that it enables significantly better code generation for an important use-case beyond what is possible with a normal import. We don't want to add a new builtin for every existing API, only ones where adapting the JavaScript API to WebAssembly and allowing inline code generation results in significantly better codegen than a plain function call. + +## Function builtins + +Function builtins are defined with an external Wasm function type, and internal JS-defined behavior. They have the same semantics as following ['create a host function'](https://webassembly.github.io/spec/js-api/#create-a-host-function) for the Wasm function type and JS code given to get a wasm `funcaddr` that can be imported. + +There are several implications of this: + - Calling a function builtin from Wasm will have the Wasm parameters converted to JS values, and JS results converted back to Wasm values. + - Exported function builtins are wrapped using ['create a new Exported function'](https://webassembly.github.io/spec/js-api/#a-new-exported-function). + - Function builtins must be imported with the correct type. + - Function builtins may become `funcref`, stored in tables, etc. + +The `name of the WebAssembly function` JS-API procedure is extended to return the import field name for builtin functions, not an index value. + +## Type builtins + +Type builtins could be an instance of the `WebAssembly.Type` interface provided by the [type-imports](https://github.com/WebAssembly/proposal-type-imports) proposal. The values contained in a type builtin would be specified with a predicate. + +This proposal does not add any type builtins, as the design around type-imports is in flux. + +## Using builtins + +Every builtin has a name, and builtins are grouped into collections with a name that matches the interface they are mirroring. + +An example import specifier could therefore be `(import "wasm:js-string" "equals" ...)`. + +The JS-API does not reserve a `wasm:` namespace today, so modules theoretically could already be using this namespace. Additionally, some users may wish to disable this feature for modules they compile so they could polyfill it. This feature is therefore opt-in via flags for each interface. + +To just enabled `js-string` builtins, a user would compile with: + +```js +WebAssembly.compile(bytes, { builtins: ['js-string'] }); +``` + +The full extension to the JS-API WebIDL is: + +```idl +dictionary WebAssemblyCompileOptions { + optional sequence builtins; +} + +[LegacyNamespace=WebAssembly, Exposed=*] +interface Module { + constructor(BufferSource bytes, optional WebAssemblyCompileOptions options); + ... +} + +[Exposed=*] +namespace WebAssembly { + # Validate accepts compile options for feature detection. + # See below for details. + boolean validate( + BufferSource bytes, + optional WebAssemblyCompileOptions options); + + # Async compile accepts compile options. + Promise compile( + BufferSource bytes, + optional WebAssemblyCompileOptions options); + + # Async instantiate overload with bytes parameters does accept compile + # options. + Promise instantiate( + BufferSource bytes, + optional object importObject, + optional WebAssemblyCompileOptions options + ); + + # Async instantiate overload with module parameter does not accept compile + # options and remains the same. + Promise instantiate( + Module moduleObject, + optional object importObject + ); + + # Async streaming compile accepts compile options. + Promise compileStreaming( + Promise source, + optional WebAssemblyCompileOptions options); + + # Async streaming compile and instantiate accepts compile options after + # imports. + Promise instantiateStreaming( + Promise source, + optional object importObject, + optional WebAssemblyCompileOptions options); +}; +``` + +A Wasm module that has enabled builtins will have the specific import specifier, such as `wasm:js-string` for that interface available and eagerly applied. + +Concretely this means that imports that refer to that specifier will be eagerly checked for link errors at compile time, those imports will not show up in `WebAssembly.Module.imports()`, and those imports will not need to be provided at instantiation time. No property lookup on the instantiation imports object will be done for those imports. + +When the module is instantiated, a unique instantiation of the builtins is created. This means that re-exports of builtin functions will have different identities if they come from different instances. This is a useful property for future extensions to bind memory to builtins or evolve the types as things like type-imports or a core stringref type are added (see below). + +## Progressive enhancement + +For engines that don't support builtins, any compile options passed to the JS-API will be ignored (due to WebIDL rules for extra parameters). For engines that do support builtins, any imports that refer to a builtin are not looked up on the instantiation import object. + +Together this means that it's safe for users to request builtins while still providing a polyfill for backup behavior and the optimal path will be chosen. + +## Feature detection + +Users may wish to detect if a specific builtin is available in their system. + +For this purpose, `WebAssembly.validate()` is extended to take a list of builtins to enable, like `compile()` does. After validating the module, the eager link checking that `compile()` does is also performed. Users can validate a module that deliberately imports a builtin operation with an incorrect signature and infer support for that particular builtin if validation reports a link error. + +## Polyfilling + +If a user wishes to polyfill these imports for some reason, or is running on a system without a builtin, these imports may be provided as normal through instantiation. + +## String constants + +String constants may be defined in JS and made available to Wasm through a variety of means. + +The simplest way is to have a module import each string as an immutable global. This can work for small amounts of strings, but has a high cost for when the number of string constants is very large. + +This proposal adds an extension to the JS-API compile routine to support optimized 'imported string constants' to address this use-case. + +The `WebAssemblyCompileOptions` dictionary is extended with a `USVString? importedStringConstants` flag. + +``` +partial dictionary WebAssemblyCompileOptions { + USVString? importedStringConstants; +} +``` + +When this is set to a non-null value, the module may import globals of the form `(import "%importedStringConstants%" "%stringConstant%"" (global ...))`, and the JS-API will use the provided `%stringConstant%` import field name to be the value of the global. This allows for any UTF-8 string to be imported with minimal overhead. + +### Example + +```wasm +(module + (global (import "strings" "my string constant") (ref extern)) + (export "constant" (global 0)) +) +``` + +```js +let instance = WebAssembly.instantiate(bytes, {importedStringConstants: "strings"}); + +// The global is automatically populated with the string constant +assertEq(instance.exports.constant.value, "my string constant"); +``` + +### Details + +When `importedStringConstants` is non-null, the specified string becomes the `imported string namespace`. + +During the ['compile a module'](https://webassembly.github.io/spec/js-api/index.html#compile-a-webassembly-module) step of the JS-API, the imports of the module are examined to see which refer to the imported string namespace. If an import refers to the imported string namespace, then the import type is [matched](https://webassembly.github.io/spec/core/valid/types.html#globals) against an extern type of `(global (ref extern))`. If an import fails to match, then 'compile a module' fails. The resulting module is associated with the imported string namespace for use during instantiation. + +During the ['read the imports'](https://webassembly.github.io/spec/js-api/index.html#read-the-imports) step of the JS-API, if the module has an imported string namespace, then every import that refers to this namespace has a global created to hold the string constant specified in the import field. This global is added to the imports object. If all imports in a module are from the imported string namespace, no import object needs to be provided. + +When the imports object is used during ['instantiate a module'](https://webassembly.github.io/spec/js-api/index.html#instantiate-the-core-of-a-webassembly-module), these implicitly created globals should never cause a link error due to the eager matching done in 'compile a module'. + +## JS String Builtin API + +The following is an initial set of function builtins for JavaScript String. The builtins are exposed under `wasm:js-string`. + +All below references to builtins on the Global object (e.g., `String.fromCharCode()`) refer to the original version on the Global object before any modifications by user code. + +The following internal helpers are defined in Wasm and are used by the below definitions: + +```wasm +(module + (type $array_i16 (array (mut i16))) + + (func (export "trap") + unreachable + ) + (func (export "array_len") (param arrayref) (result i32) + local.get 0 + array.len + ) + (func (export "array_i16_get") (param (ref $array_i16) i32) (result i32) + local.get 0 + local.get 1 + array.get_u $array_i16 + ) + (func (export "array_i16_set") (param (ref $array_i16) i32 i32) + local.get 0 + local.get 1 + local.get 2 + array.set $array_i16 + ) +) +``` + +### "wasm:js-string" "cast" + +``` +func cast( + string: externref +) -> (ref extern) { + // Technically a partially redundant test, but want to be clear the null is + // not allowed. + if (string === null || + typeof string !== "string") + trap(); + + return string; +} +``` + +### "wasm:js-string" "test" + +``` +func test( + string: externref +) -> i32 { + // Technically a partially redundant test, but want to be clear the null is + // not allowed. + if (string === null || + typeof string !== "string") + return 0; + return 1; +} +``` + +### "wasm:js-string" "fromCharCodeArray" + +``` +/// Convert the specified range of a mutable i16 array into a String, +/// treating each i16 as an unsigned 16-bit char code. +/// +/// The range is given by [start, end). This function traps if the range is +/// outside the bounds of the array. +/// +/// NOTE: This function only takes a mutable i16 array defined in its own +/// recursion group. +/// +/// If this is an issue for toolchains, we can look into how to relax the +/// function type while still maintaining good performance. +func fromCharCodeArray( + array: (ref null (array (mut i16))), + start: i32, + end: i32 +) -> (ref extern) +{ + // NOTE: `start` and `end` are interpreted as signed 32-bit integers when + // converted to JS values using standard conversions. Reinterpret them as + // unsigned here. + start >>>= 0; + end >>>= 0; + + if (array === null) + trap(); + + if (start > end || + end > array_len(array)) + trap(); + + let result = ""; + for(let i = start; i < end; i++) { + let charCode = array_i16_get(array, i); + result += String.fromCharCode(charCode); + } + return result; +} +``` + +### "wasm:js-string" "intoCharCodeArray" + +``` +/// Copy a string into a pre-allocated mutable i16 array at `start` index. +/// +/// Returns the number of char codes written, which is equal to the length of +/// the string. +/// +/// Traps if the string doesn't fit into the array. +func intoCharCodeArray( + string: externref, + array: (ref null (array (mut i16))), + start: i32 +) -> i32 +{ + // NOTE: `start` is interpreted as a signed 32-bit integer when converted + // to a JS value using standard conversions. Reinterpret as unsigned here. + start >>>= 0; + + if (array === null) + trap(); + + // Technically a partially redundant test, but want to be clear the null is + // not allowed. + if (string === null || + typeof string !== "string") + trap(); + + // The following addition is safe from overflow as adding two 32-bit integers + // cannot overflow Number.MAX_SAFE_INTEGER (2^53-1). + if (start + string.length > array_len(array)) + trap(); + + for (let i = 0; i < string.length; i++) { + let charCode = string.charCodeAt(i); + array_i16_set(array, start + i, charCode); + } + return string.length; +} +``` + +### "wasm:js-string" "fromCharCode" + +``` +func fromCharCode( + charCode: i32 +) -> (ref extern) +{ + // NOTE: `charCode` is interpreted as a signed 32-bit integer when converted + // to a JS value using standard conversions. Reinterpret as unsigned here. + charCode >>>= 0; + + return String.fromCharCode(charCode); +} +``` + +### "wasm:js-string" "fromCodePoint" + +``` +func fromCodePoint( + codePoint: i32 +) -> (ref extern) +{ + // NOTE: `codePoint` is interpreted as a signed 32-bit integer when converted + // to a JS value using standard conversions. Reinterpret as unsigned here. + codePoint >>>= 0; + + // fromCodePoint will throw a RangeError for values outside of this range, + // eagerly check for this an present as a wasm trap. + if (codePoint > 0x10FFFF) + trap(); + + return String.fromCodePoint(codePoint); +} +``` + +### "wasm:js-string" "charCodeAt" + +``` +func charCodeAt( + string: externref, + index: i32 +) -> i32 +{ + // NOTE: `index` is interpreted as a signed 32-bit integer when converted to + // a JS value using standard conversions. Reinterpret as unsigned here. + index >>>= 0; + + // Technically a partially redundant test, but want to be clear the null is + // not allowed. + if (string === null || + typeof string !== "string") + trap(); + + if (index >= string.length) + trap(); + + return string.charCodeAt(index); +} +``` + +### "wasm:js-string" "codePointAt" + +``` +func codePointAt( + string: externref, + index: i32 +) -> i32 +{ + // NOTE: `index` is interpreted as a signed 32-bit integer when converted to + // a JS value using standard conversions. Reinterpret as unsigned here. + index >>>= 0; + + // Technically a partially redundant test, but want to be clear the null is + // not allowed. + if (string === null || + typeof string !== "string") + trap(); + + if (index >= string.length) + trap(); + + return string.codePointAt(index); +} +``` + +### "wasm:js-string" "length" + +``` +func length(string: externref) -> i32 { + // Technically a partially redundant test, but want to be clear the null is + // not allowed. + if (string === null || + typeof string !== "string") + trap(); + + return string.length; +} +``` + +### "wasm:js-string" "concat" + +``` +func concat( + first: externref, + second: externref +) -> (ref extern) +{ + if (first === null || + typeof first !== "string") + trap(); + if (second === null || + typeof second !== "string") + trap(); + + return first.concat(second); +} +``` + +### "wasm:js-string" "substring" + +``` +func substring( + string: externref, + start: i32, + end: i32 +) -> (ref extern) +{ + // NOTE: `start` and `end` are interpreted as signed 32-bit integers when + // converted to JS values using standard conversions. Reinterpret them as + // unsigned here. + start >>>= 0; + end >>>= 0; + + // Technically a partially redundant test, but want to be clear the null is + // not allowed. + if (string === null || + typeof string !== "string") + trap(); + + // Ensure the range is ordered to avoid the complex behavior that `substring` + // performs when that is not the case. + if (start > end || + start > string.length) + return ""; + + // If end > string.length, `substring` is specified to clamp it + // start is guaranteed to be at least zero (as it is unsigned), so there will + // not be any clamping of start. + return string.substring(start, end); +} +``` + +### "wasm:js-string" "equals" + +``` +func equals( + first: externref, + second: externref +) -> i32 +{ + // Explicitly allow null strings to be compared for equality as that is + // meaningful. + if (first !== null && + typeof first !== "string") + trap(); + if (second !== null && + typeof second !== "string") + trap(); + return first === second ? 1 : 0; +} +``` + +### "wasm:js-string" "compare" + +``` +function compare( + first: externref, + second: externref +) -> i32 +{ + // Explicitly do not allow null strings to be compared, as there is no + // meaningful ordering given by the JS `<` operator. + if (first === null || + typeof first !== "string") + trap(); + if (second === null || + typeof second !== "string") + trap(); + + if (first === second) + return 0; + return first < second ? -1 : 1; +} +``` + +## Future extensions + +There are several extensions we can make in the future as need arrives. + +### UTF8/WTF8 support + +As stated above in 'goals for builtins', builtins are intended to just wrap existing primitives and not invent new functionality. + +JS Strings are semantically a sequence of 16-bit code units (referred to as char codes in method naming), and there are no builtin operations on them to acquire a UTF-8 or WTF-8 view. This makes it difficult to write Wasm builtins for these encodings without introducing significant new logic to them. + +There is the Encoding API for `TextEncoder`/`TextDecoder` which can be used for UTF-8 support. However, this is technically a separate spec from JS and may not be available on all JS engines (in practice it's available widely). This proposal exposes UTF-8 data conversions using this API under separate `wasm:text-encoder` `wasm:text-decoder` interfaces which are available when the host implements these interfaces. + +### Encoding API + +The following is an initial set of function builtins for the [`TextEncoder`](https://encoding.spec.whatwg.org/#interface-textencoder) and the [`TextDecoder`](https://encoding.spec.whatwg.org/#interface-textdecoder) interfaces. These builtins are exposed under `wasm:text-encoder` and `wasm:text-decoder`, respectively. + +All below references to builtins on the Global object (e.g. `String.fromCharCode()`) refer to the original version on the Global object before any modifications by user code. + +The following internal helpers are defined in Wasm and used by the below definitions: + +```wasm +(module + (type $array_i8 (array (mut i8))) + + (func (export "unreachable") + unreachable + ) + (func (export "array_len") (param arrayref) (result i32) + local.get 0 + array.len + ) + (func (export "array_i8_get") (param (ref $array_i8) i32) (result i32) + local.get 0 + local.get 1 + array.get_u $array_i8 + ) + (func (export "array_i8_new") (param i32) (result (ref $array_i8)) + local.get 0 + array.new_default $array_i8 + ) + (func (export "array_i8_set") (param (ref $array_i8) i32 i32) + local.get 0 + local.get 1 + local.get 2 + array.set $array_i8 + ) +) +``` + +```js +// Triggers a wasm trap, which will generate a WebAssembly.RuntimeError that is +// uncatchable to WebAssembly with an implementation defined message. +function trap() { + // Directly constructing and throwing a WebAssembly.RuntimeError will yield + // an exception that is catchable by the WebAssembly exception-handling + // proposal. Workaround this by executing an unreachable trap and + // modifying it. The final spec will probably use a non-polyfillable + // intrinsic to get this exactly right. + try { + unreachable(); + } catch (err) { + // Wasm trap error messages are not defined by the JS-API spec currently. + err.message = IMPL_DEFINED; + throw err; + } +} +``` + +#### "wasm:text-decoder" "decodeStringFromUTF8Array" + +``` +/// Decode the specified range of an i8 array using UTF-8 into a string. +/// +/// The range is given by [start, end). This function traps if the range is +/// outside the bounds of the array. +/// +/// NOTE: This function only takes an immutable i8 array defined in its own +/// recursion group. +/// +/// If this is an issue for toolchains, we can look into how to relax the +/// function type while still maintaining good performance. +func decodeStringFromUTF8Array( + array: (ref null (array (mut i8))), + start: i32, + end: i32 +) -> (ref extern) +{ + // NOTE: `start` and `end` are interpreted as signed 32-bit integers when + // converted to JS values using standard conversions. Reinterpret them as + // unsigned here. + start >>>= 0; + end >>>= 0; + + if (array === null) + trap(); + + if (start > end || + end > array_len(array)) + trap(); + + // Inialize a UTF-8 decoder with the default options + let decoder = new TextDecoder("utf-8", { + fatal: false, + ignoreBOM: false, + }); + + // Copy the wasm array into a Uint8Array for decoding + let bytesLength = end - start; + let bytes = new Uint8Array(bytesLength); + for (let i = start; i < end; i++) { + bytes[i - start] = array_i8_get(array, i); + } + + return decoder.decode(bytes); +} +``` + +#### "wasm:text-encoder" "measureStringAsUTF8" + +``` +/// Returns the number of bytes string would take when encoded as UTF-8. +/// +/// Traps if the length of the UTF-8 encoded string doesn't fit into an i32 +func measureStringAsUTF8( + string: externref +) -> i32 +{ + // Technically a partially redundant test, but want to be clear the null is + // not allowed. + if (string === null || + typeof string !== "string") + trap(); + + // Encode the string into bytes using UTF-8 + let encoder = new TextEncoder(); + let bytes = encoder.encode(string); + + // Trap if the number of bytes is larger than can fit into an i32 + if (bytes.length > 0xffff_ffff) { + trap(); + } + return bytes.length; +} +``` + +#### "wasm:text-encoder" "encodeStringIntoUTF8Array" + +``` +/// Encode a string into a pre-allocated mutable i8 array at `start` index using +/// the UTF-8 encoding. This uses the replacement character for unpaired +/// surrogates and so it doesn't support lossless round-tripping with +/// `decodeStringFromUTF8Array`. +/// +/// Returns the number of bytes written. +/// +/// Traps if the string doesn't fit into the array. +func encodeStringIntoUTF8Array( + string: externref, + array: (ref null (array (mut i8))), + start: i32 +) -> i32 +{ + // NOTE: `start` is interpreted as a signed 32-bit integer when converted + // to a JS value using standard conversions. Reinterpret as unsigned here. + start >>>= 0; + + if (array === null) + trap(); + + // Technically a partially redundant test, but want to be clear the null is + // not allowed. + if (string === null || + typeof string !== "string") + trap(); + + // Encode the string into bytes using UTF-8 + let encoder = new TextEncoder(); + let bytes = encoder.encode(string); + + // The following addition is safe from overflow as adding two 32-bit integers + // cannot overflow Number.MAX_SAFE_INTEGER (2^53-1). + if (start + bytes.length > array_len(array)) + trap(); + + for (let i = 0; i < bytes.length; i++) { + array_i8_set(array, start + i, bytes[i]); + } + + return bytes.length; +} +``` + +#### "wasm:text-encoder" "encodeStringToUTF8Array" + +``` +/// Encode a string into a new mutable i8 array using UTF-8. +//// +/// This uses the replacement character for unpaired surrogates and so it +/// doesn't support lossless round-tripping with `decodeStringFromUTF8Array`. +func encodeStringToUTF8Array( + string: externref +) -> (ref (array (mut i8))) +{ + // Technically a partially redundant test, but want to be clear the null is + // not allowed. + if (string === null || + typeof string !== "string") + trap(); + + // Encode the string into bytes using UTF-8 + let encoder = new TextEncoder(); + let bytes = encoder.encode(string); + + let array = array_i8_new(bytes.length); + for (let i = 0; i < bytes.length; i++) { + array_i8_set(array, i, bytes[i]); + } + return array; +} +``` + +### Binding memory to builtins + +It may be useful to have a builtin that operates on a specific Wasm memory. For JS strings, this could allow us to encode a JS string directly into linear memory. + +One way we could do this is by having the JS-API bind the first imported memory of a module to any imported builtin functions that want to operate on memory. If there is no imported memory and a builtin function that needs memory is imported, then a link error is reported. + +The memory is imported as opposed to exported so that it is guaranteed to exist when the builtin imports are provided. Using a memory defined only locally would have limited flexibility and would also be exposing a potentially private memory to outside its module. + +A quick example: + +```wasm +(module + (; memory 0 ;) + (import ... (memory ...)) + + (; bound to memory 0 through the JS-API instantiating the builtins ;) + (import "wasm:js-string" "encodeStringToMemoryUTF16" (func ...)) +) +``` + +Because the `wasm:js-string` module is instantiated when the module using it is instantiated, the imported memory will be around to be provided to both modules. + +If multi-memory is in use and the desired memory to bind with is not the first import, then we could consider parsing the imports to determine which memory is needed for which builtin. For example, `encodeStringToMemoryUTF16.2` for binding to memory 2. + +### Better function types to avoid runtime checks + +The initial set of JS String Builtins are typed to use `externref` for wherever a JS String is needed. This can lead to runtime checks that should be avoidable. Optimizing compilers can probably get rid of some of these, but not all. + +In the future, we could have type imports or a core stringref type. In this event, it would be desirable to use those in the function types to avoid unnecessary runtime checks. + +The difficulty is how to do this in a backwards compatible way. If we, for example, changed the type of a builtin from `[externref] -> []` to `(ref null 0) -> []`, we would break old code that imported it with the externref parameter. + +One option would be to version the name of the function builtins, and add a new one for the more advanced type signature. + +Another option to do this would be to extend the JS-API to inspect the function types used when importing these builtins to determine whether to provide it the 'advanced type' version or the 'basic type' version. This would be a heuristic, something like checking if the type refers to a type import or not. diff --git a/test/js-api/js-string/basic.any.js b/test/js-api/js-string/basic.any.js new file mode 100644 index 0000000000..de4a21c976 --- /dev/null +++ b/test/js-api/js-string/basic.any.js @@ -0,0 +1,383 @@ +// META: global=window,dedicatedworker,jsshell,shadowrealm +// META: script=/wasm/jsapi/assertions.js +// META: script=/wasm/jsapi/wasm-module-builder.js +// META: script=/wasm/jsapi/js-string/polyfill.js + +// Generate two sets of exports, one from a polyfill implementation and another +// from the builtins provided by the host. +let polyfillExports; +let builtinExports; +setup(() => { + // Compile a module that exports a function for each builtin that will call + // it. We could just generate a module that re-exports the builtins, but that + // would not catch any special codegen that could happen when direct calling + // a known builtin function from wasm. + const builder = new WasmModuleBuilder(); + const arrayIndex = builder.addArray(kWasmI16, true, kNoSuperType, true); + const builtins = [ + { + name: "test", + params: [kWasmExternRef], + results: [kWasmI32], + }, + { + name: "cast", + params: [kWasmExternRef], + results: [wasmRefType(kWasmExternRef)], + }, + { + name: "fromCharCodeArray", + params: [wasmRefNullType(arrayIndex), kWasmI32, kWasmI32], + results: [wasmRefType(kWasmExternRef)], + }, + { + name: "intoCharCodeArray", + params: [kWasmExternRef, wasmRefNullType(arrayIndex), kWasmI32], + results: [kWasmI32], + }, + { + name: "fromCharCode", + params: [kWasmI32], + results: [wasmRefType(kWasmExternRef)], + }, + { + name: "fromCodePoint", + params: [kWasmI32], + results: [wasmRefType(kWasmExternRef)], + }, + { + name: "charCodeAt", + params: [kWasmExternRef, kWasmI32], + results: [kWasmI32], + }, + { + name: "codePointAt", + params: [kWasmExternRef, kWasmI32], + results: [kWasmI32], + }, + { + name: "length", + params: [kWasmExternRef], + results: [kWasmI32], + }, + { + name: "concat", + params: [kWasmExternRef, kWasmExternRef], + results: [wasmRefType(kWasmExternRef)], + }, + { + name: "substring", + params: [kWasmExternRef, kWasmI32, kWasmI32], + results: [wasmRefType(kWasmExternRef)], + }, + { + name: "equals", + params: [kWasmExternRef, kWasmExternRef], + results: [kWasmI32], + }, + { + name: "compare", + params: [kWasmExternRef, kWasmExternRef], + results: [kWasmI32], + }, + ]; + + // Add a function type for each builtin + for (let builtin of builtins) { + builtin.type = builder.addType({ + params: builtin.params, + results: builtin.results + }); + } + + // Add an import for each builtin + for (let builtin of builtins) { + builtin.importFuncIndex = builder.addImport( + "wasm:js-string", + builtin.name, + builtin.type); + } + + // Generate an exported function to call the builtin + for (let builtin of builtins) { + let func = builder.addFunction(builtin.name + "Imp", builtin.type); + func.addLocals(builtin.params.length); + let body = []; + for (let i = 0; i < builtin.params.length; i++) { + body.push(kExprLocalGet); + body.push(...wasmSignedLeb(i)); + } + body.push(kExprCallFunction); + body.push(...wasmSignedLeb(builtin.importFuncIndex)); + func.addBody(body); + func.exportAs(builtin.name); + } + + const buffer = builder.toBuffer(); + + // Instantiate this module using the builtins from the host + const builtinModule = new WebAssembly.Module(buffer, { + builtins: ["js-string"] + }); + const builtinInstance = new WebAssembly.Instance(builtinModule, {}); + builtinExports = builtinInstance.exports; + + // Instantiate this module using the polyfill module + const polyfillModule = new WebAssembly.Module(buffer); + const polyfillInstance = new WebAssembly.Instance(polyfillModule, { + "wasm:js-string": polyfillImports + }); + polyfillExports = polyfillInstance.exports; +}); + +// A helper function to assert that the behavior of two functions are the +// same. +function assert_same_behavior(funcA, funcB, ...params) { + let resultA; + let errA = null; + try { + resultA = funcA(...params); + } catch (err) { + errA = err; + } + + let resultB; + let errB = null; + try { + resultB = funcB(...params); + } catch (err) { + errB = err; + } + + if (errA || errB) { + assert_equals(errA === null, errB === null, errA ? errA.message : errB.message); + assert_equals(Object.getPrototypeOf(errA), Object.getPrototypeOf(errB)); + } + assert_equals(resultA, resultB); + + if (errA) { + throw errA; + } + return resultA; +} + +function assert_throws_if(func, shouldThrow, constructor) { + let error = null; + try { + func(); + } catch (e) { + error = e; + } + assert_equals(error !== null, shouldThrow, "shouldThrow mismatch"); + if (shouldThrow && error !== null) { + assert_true(error instanceof constructor); + } +} + +// Constant values used in the tests below +const testStrings = [ + "", + "a", + "1", + "ab", + "hello, world", + "\n", + "☺", + "☺☺", + String.fromCodePoint(0x10000, 0x10001) +]; +const testCharCodes = [1, 2, 3, 10, 0x7f, 0xff, 0xfffe, 0xffff]; +const testCodePoints = [1, 2, 3, 10, 0x7f, 0xff, 0xfffe, 0xffff, 0x10000, 0x10001]; +const testExternRefValues = [ + null, + undefined, + true, + false, + {x:1337}, + ["abracadabra"], + 13.37, + -0, + 0x7fffffff + 0.1, + -0x7fffffff - 0.1, + 0x80000000 + 0.1, + -0x80000000 - 0.1, + 0xffffffff + 0.1, + -0xffffffff - 0.1, + Number.EPSILON, + Number.MAX_SAFE_INTEGER, + Number.MIN_SAFE_INTEGER, + Number.MIN_VALUE, + Number.MAX_VALUE, + Number.NaN, + "hi", + 37n, + new Number(42), + new Boolean(true), + Symbol("status"), + () => 1337, +]; + +// Test that `test` and `cast` work on various JS values. Run all the +// other builtins and assert that they also perform equivalent type +// checks. +test(() => { + for (let a of testExternRefValues) { + let isString = assert_same_behavior( + builtinExports['test'], + polyfillExports['test'], + a + ); + + assert_throws_if(() => assert_same_behavior( + builtinExports['cast'], + polyfillExports['cast'], + a + ), !isString, WebAssembly.RuntimeError); + + let arrayMutI16 = helperExports.createArrayMutI16(10); + assert_throws_if(() => assert_same_behavior( + builtinExports['intoCharCodeArray'], + polyfillExports['intoCharCodeArray'], + a, arrayMutI16, 0 + ), !isString, WebAssembly.RuntimeError); + + assert_throws_if(() => assert_same_behavior( + builtinExports['charCodeAt'], + polyfillExports['charCodeAt'], + a, 0 + ), !isString, WebAssembly.RuntimeError); + + assert_throws_if(() => assert_same_behavior( + builtinExports['codePointAt'], + polyfillExports['codePointAt'], + a, 0 + ), !isString, WebAssembly.RuntimeError); + + assert_throws_if(() => assert_same_behavior( + builtinExports['length'], + polyfillExports['length'], + a + ), !isString, WebAssembly.RuntimeError); + + assert_throws_if(() => assert_same_behavior( + builtinExports['concat'], + polyfillExports['concat'], + a, a + ), !isString, WebAssembly.RuntimeError); + + assert_throws_if(() => assert_same_behavior( + builtinExports['substring'], + polyfillExports['substring'], + a, 0, 0 + ), !isString, WebAssembly.RuntimeError); + + assert_throws_if(() => assert_same_behavior( + builtinExports['equals'], + polyfillExports['equals'], + a, a + ), a !== null && !isString, WebAssembly.RuntimeError); + + assert_throws_if(() => assert_same_behavior( + builtinExports['compare'], + polyfillExports['compare'], + a, a + ), !isString, WebAssembly.RuntimeError); + } +}); + +// Test that `fromCharCode` works on various char codes +test(() => { + for (let a of testCharCodes) { + assert_same_behavior( + builtinExports['fromCharCode'], + polyfillExports['fromCharCode'], + a + ); + } +}); + +// Test that `fromCodePoint` works on various code points +test(() => { + for (let a of testCodePoints) { + assert_same_behavior( + builtinExports['fromCodePoint'], + polyfillExports['fromCodePoint'], + a + ); + } +}); + +// Perform tests on various strings +test(() => { + for (let a of testStrings) { + let length = assert_same_behavior( + builtinExports['length'], + polyfillExports['length'], + a + ); + + for (let i = 0; i < length; i++) { + let charCode = assert_same_behavior( + builtinExports['charCodeAt'], + polyfillExports['charCodeAt'], + a, i + ); + } + + for (let i = 0; i < length; i++) { + let charCode = assert_same_behavior( + builtinExports['codePointAt'], + polyfillExports['codePointAt'], + a, i + ); + } + + let arrayMutI16 = helperExports.createArrayMutI16(length); + assert_same_behavior( + builtinExports['intoCharCodeArray'], + polyfillExports['intoCharCodeArray'], + a, arrayMutI16, 0 + ); + + assert_same_behavior( + builtinExports['fromCharCodeArray'], + polyfillExports['fromCharCodeArray'], + arrayMutI16, 0, length + ); + + for (let i = 0; i < length; i++) { + for (let j = 0; j < length; j++) { + assert_same_behavior( + builtinExports['substring'], + polyfillExports['substring'], + a, i, j + ); + } + } + } +}); + +// Test various binary operations +test(() => { + for (let a of testStrings) { + for (let b of testStrings) { + assert_same_behavior( + builtinExports['concat'], + polyfillExports['concat'], + a, b + ); + + assert_same_behavior( + builtinExports['equals'], + polyfillExports['equals'], + a, b + ); + + assert_same_behavior( + builtinExports['compare'], + polyfillExports['compare'], + a, b + ); + } + } +}); diff --git a/test/js-api/js-string/constants.any.js b/test/js-api/js-string/constants.any.js new file mode 100644 index 0000000000..ef391a90b7 --- /dev/null +++ b/test/js-api/js-string/constants.any.js @@ -0,0 +1,61 @@ +// META: global=window,dedicatedworker,jsshell,shadowrealm +// META: script=/wasm/jsapi/wasm-module-builder.js + +// Instantiate a module with an imported global and return the global. +function instantiateImportedGlobal(module, name, type, mutable, importedStringConstants) { + let builder = new WasmModuleBuilder(); + builder.addImportedGlobal(module, name, type, mutable); + builder.addExportOfKind("global", kExternalGlobal, 0); + let bytes = builder.toBuffer(); + let mod = new WebAssembly.Module(bytes, { importedStringConstants }); + let instance = new WebAssembly.Instance(mod, {}); + return instance.exports["global"]; +} + +const badGlobalTypes = [ + [kWasmAnyRef, false], + [kWasmAnyRef, true], + [wasmRefType(kWasmAnyRef), false], + [wasmRefType(kWasmAnyRef), true], + [kWasmFuncRef, false], + [kWasmFuncRef, true], + [wasmRefType(kWasmFuncRef), false], + [wasmRefType(kWasmFuncRef), true], + [kWasmExternRef, true], + [wasmRefType(kWasmExternRef), true], +]; +for ([type, mutable] of badGlobalTypes) { + test(() => { + assert_throws_js(WebAssembly.CompileError, + () => instantiateImportedGlobal("'", "constant", type, mutable, "'"), + "type mismatch"); + }); +} + +const goodGlobalTypes = [ + [kWasmExternRef, false], + [wasmRefType(kWasmExternRef), false], +]; +const constants = [ + '', + '\0', + '0', + '0'.repeat(100000), + '\uD83D\uDE00', +]; +const namespaces = [ + "", + "'", + "strings" +]; + +for (let namespace of namespaces) { + for (let constant of constants) { + for ([type, mutable] of goodGlobalTypes) { + test(() => { + let result = instantiateImportedGlobal(namespace, constant, type, mutable, namespace); + assert_equals(result.value, constant); + }); + } + } +} diff --git a/test/js-api/js-string/imports.any.js b/test/js-api/js-string/imports.any.js new file mode 100644 index 0000000000..c357760bef --- /dev/null +++ b/test/js-api/js-string/imports.any.js @@ -0,0 +1,26 @@ +// META: global=window,dedicatedworker,jsshell,shadowrealm +// META: script=/wasm/jsapi/wasm-module-builder.js + +test(() => { + let builder = new WasmModuleBuilder(); + + // Import a string constant + builder.addImportedGlobal("constants", "constant", kWasmExternRef, false); + + // Import a builtin function + builder.addImport( + "wasm:js-string", + "test", + {params: [kWasmExternRef], results: [kWasmI32]}); + + let buffer = builder.toBuffer(); + let module = new WebAssembly.Module(buffer, { + builtins: ["js-string"], + importedStringConstants: "constants" + }); + let imports = WebAssembly.Module.imports(module); + + // All imports that refer to a builtin module are suppressed from import + // reflection. + assert_equals(imports.length, 0); +}); diff --git a/test/js-api/js-string/polyfill.js b/test/js-api/js-string/polyfill.js new file mode 100644 index 0000000000..7a00d4285d --- /dev/null +++ b/test/js-api/js-string/polyfill.js @@ -0,0 +1,170 @@ +// Generate some helper functions for manipulating (array (mut i16)) from JS +let helperExports; +{ + const builder = new WasmModuleBuilder(); + const arrayIndex = builder.addArray(kWasmI16, true, kNoSuperType, true); + + builder + .addFunction("createArrayMutI16", { + params: [kWasmI32], + results: [kWasmAnyRef] + }) + .addBody([ + kExprLocalGet, + ...wasmSignedLeb(0), + ...GCInstr(kExprArrayNewDefault), + ...wasmSignedLeb(arrayIndex) + ]) + .exportFunc(); + + builder + .addFunction("arrayLength", { + params: [kWasmArrayRef], + results: [kWasmI32] + }) + .addBody([ + kExprLocalGet, + ...wasmSignedLeb(0), + ...GCInstr(kExprArrayLen) + ]) + .exportFunc(); + + builder + .addFunction("arraySet", { + params: [wasmRefNullType(arrayIndex), kWasmI32, kWasmI32], + results: [] + }) + .addBody([ + kExprLocalGet, + ...wasmSignedLeb(0), + kExprLocalGet, + ...wasmSignedLeb(1), + kExprLocalGet, + ...wasmSignedLeb(2), + ...GCInstr(kExprArraySet), + ...wasmSignedLeb(arrayIndex) + ]) + .exportFunc(); + + builder + .addFunction("arrayGet", { + params: [wasmRefNullType(arrayIndex), kWasmI32], + results: [kWasmI32] + }) + .addBody([ + kExprLocalGet, + ...wasmSignedLeb(0), + kExprLocalGet, + ...wasmSignedLeb(1), + ...GCInstr(kExprArrayGetU), + ...wasmSignedLeb(arrayIndex) + ]) + .exportFunc(); + + let bytes = builder.toBuffer(); + let module = new WebAssembly.Module(bytes); + let instance = new WebAssembly.Instance(module); + + helperExports = instance.exports; +} + +function throwIfNotString(a) { + if (typeof a !== "string") { + throw new WebAssembly.RuntimeError(); + } +} + +this.polyfillImports = { + test: (string) => { + if (string === null || + typeof string !== "string") { + return 0; + } + return 1; + }, + cast: (string) => { + throwIfNotString(string); + return string; + }, + fromCharCodeArray: (array, arrayStart, arrayCount) => { + arrayStart >>>= 0; + arrayCount >>>= 0; + let length = helperExports.arrayLength(array); + if (BigInt(arrayStart) + BigInt(arrayCount) > BigInt(length)) { + throw new WebAssembly.RuntimeError(); + } + let result = ''; + for (let i = arrayStart; i < arrayStart + arrayCount; i++) { + result += String.fromCharCode(helperExports.arrayGet(array, i)); + } + return result; + }, + intoCharCodeArray: (string, arr, arrayStart) => { + arrayStart >>>= 0; + throwIfNotString(string); + let arrLength = helperExports.arrayLength(arr); + let stringLength = string.length; + if (BigInt(arrayStart) + BigInt(stringLength) > BigInt(arrLength)) { + throw new WebAssembly.RuntimeError(); + } + for (let i = 0; i < stringLength; i++) { + helperExports.arraySet(arr, arrayStart + i, string[i].charCodeAt(0)); + } + return stringLength; + }, + fromCharCode: (charCode) => { + charCode >>>= 0; + return String.fromCharCode(charCode); + }, + fromCodePoint: (codePoint) => { + codePoint >>>= 0; + return String.fromCodePoint(codePoint); + }, + charCodeAt: (string, stringIndex) => { + stringIndex >>>= 0; + throwIfNotString(string); + if (stringIndex >= string.length) + throw new WebAssembly.RuntimeError(); + return string.charCodeAt(stringIndex); + }, + codePointAt: (string, stringIndex) => { + stringIndex >>>= 0; + throwIfNotString(string); + if (stringIndex >= string.length) + throw new WebAssembly.RuntimeError(); + return string.codePointAt(stringIndex); + }, + length: (string) => { + throwIfNotString(string); + return string.length; + }, + concat: (stringA, stringB) => { + throwIfNotString(stringA); + throwIfNotString(stringB); + return stringA + stringB; + }, + substring: (string, startIndex, endIndex) => { + startIndex >>>= 0; + endIndex >>>= 0; + throwIfNotString(string); + if (startIndex > string.length, + endIndex > string.length, + endIndex < startIndex) { + return ""; + } + return string.substring(startIndex, endIndex); + }, + equals: (stringA, stringB) => { + if (stringA !== null) throwIfNotString(stringA); + if (stringB !== null) throwIfNotString(stringB); + return stringA === stringB; + }, + compare: (stringA, stringB) => { + throwIfNotString(stringA); + throwIfNotString(stringB); + if (stringA < stringB) { + return -1; + } + return stringA === stringB ? 0 : 1; + }, +}; diff --git a/test/js-api/wasm-module-builder.js b/test/js-api/wasm-module-builder.js index 1ffb00cbac..babec0fbe4 100644 --- a/test/js-api/wasm-module-builder.js +++ b/test/js-api/wasm-module-builder.js @@ -146,6 +146,10 @@ function wasmRefType(heap_type, is_shared = false) { return {opcode: kWasmRef, heap_type: heap_type, is_shared: is_shared}; } +// Packed storage types +let kWasmI8 = 0x78; +let kWasmI16 = 0x77; + let kExternalFunction = 0; let kExternalTable = 1; let kExternalMemory = 2;