From 1f72cca65b65ac4aa32515c3cf2cc54b1bb1ecac Mon Sep 17 00:00:00 2001 From: Joshua Bell Date: Tue, 28 Jan 2025 11:51:12 -0800 Subject: [PATCH] Ensure object creation specifies the realm "Realm" is an ECMAScript concept best explained in https://html.spec.whatwg.org/multipage/webappapis.html#realms-and-their-counterparts Newly created JS objects must be associated with a Realm; while older specs didn't do this explicitly, best practice is to be explicit about this, especially for steps running "in parallel", or in algorithms separate from method steps. Do so! This also adds lint tests to try and catch future violations. Note that dictionaries (e.g. MLOperatorDescriptor) are Infra "ordered maps" it the body of spec algorithms, not JS objects, so they don't have a realm. Conversion to a JS object when returning a dictionary to script is handled by WebIDL bindings logic. Also note that DOMExceptions, either thrown or as promise rejection values, are not given a realm. This is a known issue across all web specs and is tracked in whatwg/webidl#135. Resolves #793. --- index.bs | 67 ++++++++++++++++++++++++++++---------------------- tools/lint.mjs | 17 +++++++++++-- 2 files changed, 53 insertions(+), 31 deletions(-) diff --git a/index.bs b/index.bs index f5284f02..8119dc40 100644 --- a/index.bs +++ b/index.bs @@ -825,14 +825,14 @@ The powerPreference opt To create a context given [=realm=] |realm| and |options| (a {{GPUDevice}} or {{MLContextOptions}}), run these steps: - 1. Let |context| be a new {{MLContext}} object with |realm|. + 1. Let |context| be a new {{MLContext}} in |realm|. 1. If |options| is a {{GPUDevice}} object: 1. Set |context|.{{MLContext/[[contextType]]}} to "[=context type/webgpu=]". 1. Set |context|.{{MLContext/[[deviceType]]}} to {{MLDeviceType/"gpu"}}. 1. Set |context|.{{MLContext/[[powerPreference]]}} to {{MLPowerPreference/"default"}}. 1. Otherwise: 1. Set |context|.{{MLContext/[[contextType]]}} to "[=context type/default=]". - 1. Set |context|.{{MLContext/[[lost]]}} to [=a new promise=]. + 1. Set |context|.{{MLContext/[[lost]]}} to [=a new promise=] in |realm|. 1. If |options|["{{MLContextOptions/deviceType}}"] [=map/exists=], then set |context|.{{MLContext/[[deviceType]]}} to |options|["{{MLContextOptions/deviceType}}"]. 1. Otherwise, set |context|.{{MLContext/[[deviceType]]}} to {{MLDeviceType/"cpu"}}. 1. If |options|["{{MLContextOptions/powerPreference}}"] [=map/exists=], then set |context|.{{MLContext/[[powerPreference]]}} to |options|["{{MLContextOptions/powerPreference}}"]. @@ -846,9 +846,10 @@ The powerPreference opt The createContext(|options|) steps are: 1. Let |global| be [=this=]'s [=relevant global object=]. - 1. If |global|'s [=associated Document=] is not [=allowed to use=] the [=webnn-feature|webnn=] feature, return [=a new promise=] [=rejected=] with a "{{SecurityError}}" {{DOMException}}. 1. Let |realm| be [=this=]'s [=relevant realm=]. - 1. Let |promise| be [=a new promise=]. + 1. If |global|'s [=associated Document=] is not [=allowed to use=] the [=webnn-feature|webnn=] feature, return [=a new promise=] in |realm| [=rejected=] with a "{{SecurityError}}" {{DOMException}}. + 1. Let |realm| be [=this=]'s [=relevant realm=]. + 1. Let |promise| be [=a new promise=] in |realm|. 1. Run the following steps [=in parallel=]. 1. Let |context| be the result of [=creating a context=] given |realm| and |options|. If that returns failure, then [=queue an ML task=] with |global| to [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and abort these steps. 1. [=Queue an ML task=] with |global| to [=resolve=] |promise| with |context|. @@ -860,9 +861,9 @@ The powerPreference opt The createContext(|gpuDevice|) method steps are: 1. Let |global| be [=this=]'s [=relevant global object=]. - 1. If |global|'s [=associated Document=] is not [=allowed to use=] the [=webnn-feature|webnn=] feature, return [=a new promise=] [=rejected=] with a "{{SecurityError}}" {{DOMException}}. 1. Let |realm| be [=this=]'s [=relevant realm=]. - 1. Let |promise| be [=a new promise=]. + 1. If |global|'s [=associated Document=] is not [=allowed to use=] the [=webnn-feature|webnn=] feature, return [=a new promise=] in |realm| [=rejected=] with a "{{SecurityError}}" {{DOMException}}. + 1. Let |promise| be [=a new promise=] in |realm|. 1. Run the following steps [=in parallel=]. 1. Let |context| be the result of [=creating a context=] given |realm| and |gpuDevice|. If that returns failure, then [=queue an ML task=] with |global| to [=reject=] |promise| with a "{{NotSupportedError}}" {{DOMException}} and abort these steps. 1. [=Queue an ML task=] with |global| to [=resolve=] |promise| with |context|. @@ -1068,9 +1069,10 @@ Creates an {{MLTensor}} associated with this {{MLContext}}. The createTensor(|descriptor|) method steps are: 1. Let |global| be [=this=]'s [=relevant global object=]. - 1. If [=this=] [=MLContext/is lost=], then return [=a new promise=] [=rejected=] with an "{{InvalidStateError}}" {{DOMException}}. + 1. Let |realm| be [=this=]'s [=relevant realm=]. + 1. If [=this=] [=MLContext/is lost=], then return [=a new promise=] in |realm| [=rejected=] with an "{{InvalidStateError}}" {{DOMException}}. 1. Let |tensor| be the result of [=creating an MLTensor=] given [=this=], and |descriptor|. - 1. Let |promise| be [=a new promise=]. + 1. Let |promise| be [=a new promise=] in |realm|. 1. Enqueue the following steps to [=this=].{{MLContext/[[timeline]]}}: 1. Run these steps, but [=/abort when=] [=this=] [=MLContext/is lost=]: 1. Create |tensor|.{{MLTensor/[[data]]}} given |descriptor| and initialize all bytes to zeros. @@ -1097,10 +1099,10 @@ Reads back the {{MLTensor/[[data]]}} of an {{MLTensor}} from the {{MLContext}}.{ 1. Let |global| be [=this=]'s [=relevant global object=]. 1. Let |realm| be [=this=]'s [=relevant realm=]. - 1. If |tensor|.{{MLTensor/[[context]]}} is not [=this=], then return [=a new promise=] [=rejected=] with a {{TypeError}}. - 1. If |tensor|.{{MLTensor/[[isDestroyed]]}} is true, then return [=a new promise=] [=rejected=] with a {{TypeError}}. - 1. If |tensor|.{{MLTensor/[[descriptor]]}}.{{MLTensorDescriptor/readable}} is false, then return [=a new promise=] [=rejected=] with a {{TypeError}}. - 1. Let |promise| be [=a new promise=]. + 1. If |tensor|.{{MLTensor/[[context]]}} is not [=this=], then return [=a new promise=] in |realm| [=rejected=] with a {{TypeError}}. + 1. If |tensor|.{{MLTensor/[[isDestroyed]]}} is true, then return [=a new promise=] in |realm| [=rejected=] with a {{TypeError}}. + 1. If |tensor|.{{MLTensor/[[descriptor]]}}.{{MLTensorDescriptor/readable}} is false, then return [=a new promise=] in |realm| [=rejected=] with a {{TypeError}}. + 1. Let |promise| be [=a new promise=] in |realm|. 1. Enqueue the following steps to |tensor|.{{MLTensor/[[context]]}}.{{MLContext/[[timeline]]}}: 1. Run these steps, but [=/abort when=] [=this=] [=MLContext/is lost=]: 1. Let |bytes| be a [=/byte sequence=] containing a copy of |tensor|.{{MLTensor/[[data]]}}. @@ -1127,11 +1129,12 @@ Bring-your-own-buffer variant of {{MLContext/readTensor(tensor)}}. Reads back th The readTensor(|tensor|, |outputData|) method steps are: 1. Let |global| be [=this=]'s [=relevant global object=]. - 1. If |tensor|.{{MLTensor/[[context]]}} is not [=this=], then return [=a new promise=] [=rejected=] with a {{TypeError}}. - 1. If |tensor|.{{MLTensor/[[isDestroyed]]}} is true, then return [=a new promise=] [=rejected=] with a {{TypeError}}. - 1. If |tensor|.{{MLTensor/[[descriptor]]}}.{{MLTensorDescriptor/readable}} is false, then return [=a new promise=] [=rejected=] with a {{TypeError}}. - 1. If [=validating buffer with descriptor=] given |outputData| and |tensor|.{{MLTensor/[[descriptor]]}} returns false, then return [=a new promise=] [=rejected=] with a {{TypeError}}. - 1. Let |promise| be [=a new promise=]. + 1. Let |realm| be [=this=]'s [=relevant realm=]. + 1. If |tensor|.{{MLTensor/[[context]]}} is not [=this=], then return [=a new promise=] in |realm| [=rejected=] with a {{TypeError}}. + 1. If |tensor|.{{MLTensor/[[isDestroyed]]}} is true, then return [=a new promise=] in |realm| [=rejected=] with a {{TypeError}}. + 1. If |tensor|.{{MLTensor/[[descriptor]]}}.{{MLTensorDescriptor/readable}} is false, then return [=a new promise=] in |realm| [=rejected=] with a {{TypeError}}. + 1. If [=validating buffer with descriptor=] given |outputData| and |tensor|.{{MLTensor/[[descriptor]]}} returns false, then return [=a new promise=] in |realm| [=rejected=] with a {{TypeError}}. + 1. Let |promise| be [=a new promise=] in |realm|. 1. Enqueue the following steps to |tensor|.{{MLTensor/[[context]]}}.{{MLContext/[[timeline]]}}: 1. Run these steps, but [=/abort when=] [=this=] [=MLContext/is lost=]: 1. Let |bytes| be a [=/byte sequence=] containing a copy of |tensor|.{{MLTensor/[[data]]}}. @@ -1279,7 +1282,8 @@ The context lost steps for {{MLContext}} |context|, are: To lose {{MLContext}} |context| with {{DOMString}} |message|: - 1. Let |info| be a new {{MLContextLostInfo}}. + 1. Let |realm| be |context|'s [=relevant realm=]. + 1. Let |info| be a new {{MLContextLostInfo}} in |realm|. 1. Set |info|.{{MLContextLostInfo/message}} to |message|. 1. [=Resolve=] |context|.{{MLContext/[[lost]]}} with |info|. 1. For each {{MLGraph}} |graph| where |graph|.{{MLGraph/[[context]]}} equals [=this=]: @@ -1498,7 +1502,8 @@ The {{MLOperand}} objects are created by the methods of {{MLGraphBuilder}}, inte To create an MLOperand given {{MLGraphBuilder}} |builder| and {{MLOperandDescriptor}} |desc|, run the following steps: - 1. Let |operand| be a new {{MLOperand}}. + 1. Let |realm| be |builder|'s [=relevant realm=]. + 1. Let |operand| be a new {{MLOperand}} in |realm|. 1. Set |operand|.{{MLOperand/[[builder]]}} to |builder|. 1. Set |operand|.{{MLOperand/[[descriptor]]}} to |desc|. 1. Return |operand|. @@ -1508,8 +1513,10 @@ The {{MLOperand}} objects are created by the methods of {{MLGraphBuilder}}, inte To copy an MLOperand given {{MLOperand}} |operand|, run the following steps: - 1. Let |result| be a new {{MLOperand}}. - 1. Set |result|.{{MLOperand/[[builder]]}} to |operand|.{{MLOperand/[[builder]]}}. + 1. Let |builder| be |operand|.{{MLOperand/[[builder]]}}. + 1. Let |realm| be |builder|'s [=relevant realm=]. + 1. Let |result| be a new {{MLOperand}} in |realm|. + 1. Set |result|.{{MLOperand/[[builder]]}} to |builder|. 1. Set |result|.{{MLOperand/[[descriptor]]}} to |operand|.{{MLOperand/[[descriptor]]}}. 1. If |operand|.{{MLOperand/[[name]]}} [=map/exists=], then set |result|.{{MLOperand/[[name]]}} to |operand|.{{MLOperand/[[name]]}}. 1. Return |result|. @@ -1607,7 +1614,8 @@ An {{MLTensor}} is created by its associated {{MLContext}}. To create an MLTensor given {{MLContext}} |context| and {{MLTensorDescriptor}} |descriptor|, run the following steps: - 1. Let |tensor| be a new {{MLTensor}}. + 1. Let |realm| be |context|'s [=relevant realm=]. + 1. Let |tensor| be a new {{MLTensor}} in |realm|. 1. Set |tensor|.{{MLTensor/[[context]]}} to |context|. 1. Set |tensor|.{{MLTensor/[[descriptor]]}} to |descriptor|. 1. Set |tensor|.{{MLTensor/[[isDestroyed]]}} to false. @@ -1796,12 +1804,13 @@ Build a composed graph up to a given output operand into a computational graph a The build(|outputs|) method steps are: - 1. If [=this=] [=MLGraphBuilder/can not build=], then return [=a new promise=] [=rejected=] with an "{{InvalidStateError}}" {{DOMException}}. - 1. If |outputs| is empty, then return [=a new promise=] [=rejected=] with a {{TypeError}}. + 1. Let |realm| be [=this=]'s [=relevant realm=]. + 1. If [=this=] [=MLGraphBuilder/can not build=], then return [=a new promise=] in |realm| [=rejected=] with an "{{InvalidStateError}}" {{DOMException}}. + 1. If |outputs| is empty, then return [=a new promise=] in |realm| [=rejected=] with a {{TypeError}}. 1. [=map/For each=] |name| → |operand| of |outputs|: - 1. If |name| is empty, then return [=a new promise=] [=rejected=] with a {{TypeError}}. - 1. If [=MLGraphBuilder/validating operand=] given [=this=] and |operand| returns false, then return [=a new promise=] [=rejected=] with a {{TypeError}}. - 1. If |operand| is in [=this=]'s [=MLGraphBuilder/graph=]'s [=computational graph/inputs=] or [=computational graph/constants=], then return [=a new promise=] [=rejected=] with a {{TypeError}}. + 1. If |name| is empty, then return [=a new promise=] in |realm| [=rejected=] with a {{TypeError}}. + 1. If [=MLGraphBuilder/validating operand=] given [=this=] and |operand| returns false, then return [=a new promise=] in |realm| [=rejected=] with a {{TypeError}}. + 1. If |operand| is in [=this=]'s [=MLGraphBuilder/graph=]'s [=computational graph/inputs=] or [=computational graph/constants=], then return [=a new promise=] in |realm| [=rejected=] with a {{TypeError}}. 1. Let |operands| be a new empty [=/set=]. 1. Let |operators| be a new empty [=/set=]. 1. Let |inputs| be a new empty [=/set=]. @@ -1815,7 +1824,7 @@ Build a composed graph up to a given output operand into a computational graph a 1. [=queue/Enqueue=] |input| to |queue|. 1. Let |global| be [=this=]'s [=relevant global object=]. 1. Let |realm| be [=this=]'s [=relevant realm=]. - 1. Let |graph| be a new {{MLGraph}} with |realm|. + 1. Let |graph| be a new {{MLGraph}} in |realm|. 1. Set |graph|.{{MLGraph/[[context]]}} to [=this=].{{MLGraphBuilder/[[context]]}}. 1. Set |graph|.{{MLGraph/[[isDestroyed]]}} to false. 1. [=set/For each=] |operand| in |inputs|: @@ -1823,7 +1832,7 @@ Build a composed graph up to a given output operand into a computational graph a 1. [=map/For each=] |name| → |operand| of |outputs|: 1. Set |graph|.{{MLGraph/[[outputDescriptors]]}}[|name|] to |operand|.{{MLOperand/[[descriptor]]}}. 1. Set [=this=].{{MLGraphBuilder/[[hasBuilt]]}} to true. - 1. Let |promise| be [=a new promise=]. + 1. Let |promise| be [=a new promise=] in |realm|. 1. Run the following steps [=in parallel=]: 1. Run these steps, but [=/abort when=] |graph|.{{MLGraph/[[context]]}} [=MLContext/is lost=]: 1. Let |graphImpl| be the result of converting [=this=]'s [=MLGraphBuilder/graph=] with |operands|, |operators|, |inputs|, and |outputs|'s [=map/values=] into an [=implementation-defined=] format which can be interpreted by the underlying platform. diff --git a/tools/lint.mjs b/tools/lint.mjs index 2c44e6ce..fbe516df 100755 --- a/tools/lint.mjs +++ b/tools/lint.mjs @@ -83,8 +83,9 @@ const root = parse(file, { }); log('simplifying DOM...'); -// Remove script and style elements from consideration -for (const element of root.querySelectorAll('script, style')) { +// Remove script and style elements from consideration. Remove generated indexes +// too, since they can lead to duplicate false-positive matches for lint rules. +for (const element of root.querySelectorAll('script, style, .index')) { element.remove(); } @@ -340,4 +341,16 @@ for (const match of source.matchAll(/\|(\w+)\|\.{{(\w+)\/.*?}}/g)) { }); } +// Ensure JS objects are created with explicit realm +for (const match of text.matchAll(/ a new promise\b(?! in realm)/g)) { + error(`Promise creation must specify realm: ${format(match)}`); +} +for (const match of text.matchAll(/ be a new ([A-Z]\w+)\b(?! in realm)/g)) { + const type = match[1]; + // Dictionaries are just maps, so they don't need a realm. + if (type === 'MLOperandDescriptor') + continue; + error(`Object creation must specify realm: ${format(match)}`); +} + globalThis.process.exit(exitCode);