From a53ddb8708c25e56da1e90d969a970f22a7a50e8 Mon Sep 17 00:00:00 2001 From: Joshua Bell Date: Tue, 28 Jan 2025 20:35:23 -0800 Subject: [PATCH] Ensure object creation specifies the realm (#810) * 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. * Don't double-init realm; and don't need realm for dicts * Variable name improvement from @fdwr --- index.bs | 64 +++++++++++++++++++++++++++----------------------- tools/lint.mjs | 20 ++++++++++++++-- 2 files changed, 53 insertions(+), 31 deletions(-) diff --git a/index.bs b/index.bs index 27cb54e3..a95f8317 100644 --- a/index.bs +++ b/index.bs @@ -810,13 +810,13 @@ 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/[[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/powerPreference}}"] [=map/exists=], then set |context|.{{MLContext/[[powerPreference]]}} to |options|["{{MLContextOptions/powerPreference}}"]. 1. Otherwise, set |context|.{{MLContext/[[powerPreference]]}} to {{MLPowerPreference/"default"}}. 1. If the user agent cannot support |context|.{{MLContext/[[contextType]]}} and |context|.{{MLContext/[[powerPreference]]}}, return failure. @@ -828,9 +828,9 @@ 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 |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|. @@ -842,9 +842,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|. @@ -1043,9 +1043,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. @@ -1072,10 +1073,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]]}}. @@ -1102,11 +1103,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]]}}. @@ -1473,7 +1475,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|. @@ -1483,8 +1486,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|. @@ -1582,7 +1587,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. @@ -1771,12 +1777,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=]. @@ -1789,8 +1796,7 @@ Build a composed graph up to a given output operand into a computational graph a 1. [=list/For each=] |input| of |operand|.{{MLOperand/[[operator]]}}'s [=operator/inputs=]: 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|: @@ -1798,7 +1804,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 fdab7800..85e4ba8f 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(); } @@ -350,4 +351,19 @@ for (const match of source.matchAll(/\|(\w+)\|\.{{(\w+)\/.*?}}/g)) { }); } +// TODO: Generate this from the IDL itself. +const dictionaryTypes = ['MLOperandDescriptor', 'MLContextLostInfo']; + +// 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 (dictionaryTypes.includes(type)) + continue; + error(`Object creation must specify realm: ${format(match)}`); +} + globalThis.process.exit(exitCode);