- From: Boris Zbarsky <notifications@github.com>
- Date: Tue, 10 Mar 2020 13:39:10 -0700
- To: heycam/webidl <webidl@noreply.github.com>
- Cc: Subscribed <subscribed@noreply.github.com>
- Message-ID: <heycam/webidl/pull/840/review/372257720@github.com>
bzbarsky requested changes on this pull request.
This is looking pretty good! There are some semantic differences from existing indexed setter things in `defineProperty`, but that seems ok, I think. The existing indexed setter semantics of just ignoring attempts to make indices non-configurable instead of throwing are not great.
I have some nits/comments/questions, but the two main substantive things are:
1) The behavior of `defineProperty` when `[[Value]]` is not present in a descriptor for an index property name.
2) The weirdness around the in-range check at the top of "set the length".
> @@ -6775,6 +6784,127 @@ The [=type name=] of a frozen array
type is the concatenation of the type name for |T| and the string
"<code>Array</code>".
+<h4 id="idl-observable-array" interface lt="ObservableArrayT|ObservableArray<T>">Observable array types — ObservableArray<|T|></h4>
+
+An <dfn id="dfn-observable-array-type" export>observable array type</dfn> is a parametrized type
+whose values are references to a combination of a mutable list of objects of type |T|, as well as
+behavior to perform when author code modifies the contents of the list.
+
+The parametrized type must not be a [=dictionary type=], [=sequence type=], or [=record type=].
+
+Similar to [=sequence types=] and [=frozen array types=], observable array types wrap around
+ECMAScript array types, imposing additional semantics on their usage.
+
+Observable array types must only be used as the type of [=regular attributes=].
+
+For an attribute whose type is an observable array type, specification authors can specify a series
"series of"
> @@ -11580,6 +11733,11 @@ in which case they are exposed on every object that [=implements=] the interface
\[[Enumerable]]: <emu-val>true</emu-val>, \[[Configurable]]: |configurable|}.
1. Let |id| be |attr|'s [=identifier=].
1. Perform [=!=] <a abstract-op>DefinePropertyOrThrow</a>(|target|, |id|, |desc|).
+ 1. If |attr|'s type is an [=observable array type=] with type argument |T|, then set
This seems over-indented. I'd think it should just be another item after the DefinePropertyOrThrow, not a sub-item under it.
> @@ -11671,6 +11831,17 @@ in which case they are exposed on every object that [=implements=] the interface
1. Return <emu-val>undefined</emu-val>.
1. Set |idlObject| to the IDL [=interface type=] value that represents a reference
to |esValue|.
+ 1. If |attribute|'s type is an [=observable array type=] with type argument |T|:
+ 1. Let |newValues| be the result of [=converted to an IDL value|converting=] |V| to
So this happens up front, great.
> @@ -11671,6 +11831,17 @@ in which case they are exposed on every object that [=implements=] the interface
1. Return <emu-val>undefined</emu-val>.
1. Set |idlObject| to the IDL [=interface type=] value that represents a reference
to |esValue|.
+ 1. If |attribute|'s type is an [=observable array type=] with type argument |T|:
+ 1. Let |newValues| be the result of [=converted to an IDL value|converting=] |V| to
+ an IDL value of type <a lt="sequence type">sequence<|T|></a>.
+ 1. Let |oa| be |idlObject|'s |attribute|'s [=backing observable array exotic object=].
+ 1. [=observable array exotic object/Set the length=] of |oa|.\[[ProxyHandler]] to 0.
+ 1. Let |i| be 0.
+ 1. While |i| < |newValues|'s [=list/size=]:
+ 1. Perform the algorithm steps given by
An exception here would mean we only append the first however many things. That said, that seems fine; it basically behaves just like `concat()` would.
> + 1. Perform [=!=] [$CreateDataProperty$](|handler|, "<code>defineProperty</code>", |defineProperty|).
+ 1. Let |deleteProperty| be [=!=] [$CreateBuiltinFunction$](the steps from [[#es-observable-array-deleteProperty]], « », |realm|).
+ 1. Perform [=!=] [$CreateDataProperty$](|handler|, "<code>deleteProperty</code>", |deleteProperty|).
+ 1. Let |get| be [=!=] [$CreateBuiltinFunction$](the steps from [[#es-observable-array-get]], « », |realm|).
+ 1. Perform [=!=] [$CreateDataProperty$](|handler|, "<code>get</code>", |get|).
+ 1. Let |getOwnPropertyDescriptor| be [=!=] [$CreateBuiltinFunction$](the steps from [[#es-observable-array-getOwnPropertyDescriptor]], « », |realm|).
+ 1. Perform [=!=] [$CreateDataProperty$](|handler|, "<code>getOwnPropertyDescriptor</code>", |getOwnPropertyDescriptor|).
+ 1. Let |has| be [=!=] [$CreateBuiltinFunction$](the steps from [[#es-observable-array-has]], « », |realm|).
+ 1. Perform [=!=] [$CreateDataProperty$](|handler|, "<code>has</code>", |has|).
+ 1. Let |ownKeys| be [=!=] [$CreateBuiltinFunction$](the steps from [[#es-observable-array-ownKeys]], « », |realm|).
+ 1. Perform [=!=] [$CreateDataProperty$](|handler|, "<code>ownKeys</code>", |ownKeys|).
+ 1. Let |preventExtensions| be [=!=] [$CreateBuiltinFunction$](the steps from [[#es-observable-array-preventExtensions]], « », |realm|).
+ 1. Perform [=!=] [$CreateDataProperty$](|handler|, "<code>preventExtensions</code>", |preventExtensions|).
+ 1. Let |set| be [=!=] [$CreateBuiltinFunction$](the steps from [[#es-observable-array-set]], « », |realm|).
+ 1. Perform [=!=] [$CreateDataProperty$](|handler|, "<code>set</code>", |set|).
+ 1. Return [=!=] [$ProxyCreate$](|innerArray|, |handler|).
Do we want to just use the forwarding `setPrototypeOf` like this proposal does? It seems we could go either way on that, and we generally don't prevent setting prototypes of random things in the web platform, so OK.
> + 1. If |P| is "length", then return the result of
+ [=observable array exotic object/setting the length=] given |handler| and |V|.
+ 1. If |P| [=is an array index=], then return the result of
+ [=observable array exotic object/setting the indexed value=] given |handler|, |P|, and |V|.
+ 1. Return [=?=] |O|.\[[Set]](|P|, |V|, |Receiver|).
+</div>
+
+<h4 id="es-observable-array-abstract-operations">Abstract operations</h4>
+
+<div algorithm>
+ To <dfn for="observable array exotic object" lt="set the length|setting the length">set the length</dfn>
+ of an observable array exotic object given |handler| and |newLen|:
+
+ 1. Set |newLen| to [=?=] [$ToUint32$](|newLen|).
+ 1. Let |numberLen| be [=?=] [$ToNumber$](|newLen|).
+ 1. If |newLen| ≠ |numberLen|, then throw a {{RangeError}} exception.
I don't follow this step. `newLen` after step 1 is a 32-bit unsigned integer. `numberLen` will be exactly equal to `newLen`, since all 32-bit integers are exactly representable as doubles.
I assume that what this really wants to do is `ToNumber` the incoming value (which could fail), then `ToUint32` the resulting number (this part is infallible), then compare the 32-bit int to the number. It's worth calling out that this is numeric comparison as doubles or something, so a 0 int tests equal to a `-0` double, but tests unequal to a `NaN`.
> + |handler|.\[[BackingList]][|indexToDelete|] and |indexToDelete|.
+ 1. [=list/Remove=] the last item from |handler|.\[[BackingList]].
+ 1. Set |indexToDelete| to |indexToDelete| − 1.
+</div>
+
+<div algorithm>
+ To <dfn for="observable array exotic object" lt="set the indexed value|setting the indexed value">set the indexed value</dfn>
+ of an observable array exotic object given |handler|, |P|, and |V|:
+
+ 1. Let |oldLen| be |handler|.\[[BackingList]]'s [=list/size=].
+ 1. Let |index| be [=!=] [$ToUint32$](|P|).
+ 1. If |index| > |oldLen|, return <emu-val>false</emu-val>.
+ 1. Let |idlValue| be the result of [=converted to an IDL value|converting=]
+ |V| to the type given by |handler|.\[[Type]].
+ 1. If |index| < |oldLen|, then:
+ 1. Perform the algorithm steps given by |handler|.\[[DeleteAlgorithm]], given
It's worth noting somewhere where the overall concept is defined that a set will do the delete algorithm, then the set algorithm, and only then change the value. Because a plausible alternative would have been to just do the set algorithm and let the implementation figure out that there's the implicit delete of the old value... Either choice is OK, I think; we just need to make sure we make it clear to someone who has not read these specific steps which one is happening.
> + 1. If |descriptor|.\[[Value]] is present, then return the result of
+ [=observable array exotic object/setting the length=] given |handler| and
+ |descriptor|.\[[Value]].
+ 1. Return <emu-val>true</emu-val>.
+ 1. If |P| [=is an array index=], then:
+ 1. If [=!=] [$IsAccessorDescriptor$](|descriptor|) is <emu-val>true</emu-val>, then return
+ <emu-val>false</emu-val>.
+ 1. If |descriptor|.\[[Configurable]] is present and has the value <emu-val>false</emu-val>,
+ then return <emu-val>false</emu-val>.
+ 1. If |descriptor|.\[[Enumerable]] is present and has the value <emu-val>false</emu-val>,
+ then return <emu-val>false</emu-val>.
+ 1. If |descriptor|.\[[Writable]] is present and has the value <emu-val>false</emu-val>,
+ then return <emu-val>false</emu-val>.
+ 1. If |descriptor|.\[[Value]] is present, then return the result of
+ [=observable array exotic object/setting the indexed value=] given |handler|, |P|, and
+ |descriptor|.\[[Value]].
If `[[Value]]` is not present, do we really want to fall through to `O.[[DefineOwnProperty]]`? That will create an indexed property on the array with the value `undefined`, if I read the ES spec correctly. Seems weird to me to allow that.
> +
+<div algorithm="observable array exotic object deleteProperty trap">
+ The steps for the <code>deleteProperty</code> proxy trap for
+ [=observable array exotic objects=], given |O| and |P|, are as follows:
+
+ 1. Let |handler| be the <emu-val>this</emu-val> value.
+ 1. If |P| is "length", then return <emu-val>false</emu-val>.
+ 1. If |P| [=is an array index=], then:
+ 1. Let |oldLen| be |handler|.\[[BackingList]]'s [=list/size=].
+ 1. Let |index| be [=!=] [$ToUint32$](|P|).
+ 1. If |index| ≠ |oldLen| − 1, then return
+ <emu-val>false</emu-val>.
+ 1. Perform the algorithm steps given by |handler|.\[[DeleteAlgorithm]], given
+ |handler|.\[[BackingList]][|index|] and |index|.
+ 1. [=list/Remove=] the last item from |handler|.\[[BackingList]].
+ 1. Perform [=!=] |O|.\[[DefineOwnProperty]]("length", PropertyDescriptor{\[[Value]]: |oldLen| − 1}).
Why is this step needed? I don't think it's needed... nothing looks at "length" on the target array, right?
> +</div>
+
+<h4 id="es-observable-array-ownKeys"><code>ownKeys</code></h4>
+
+<div algorithm="observable array exotic object ownKeys trap">
+ The steps for the <code>ownKeys</code> proxy trap for
+ [=observable array exotic objects=], given |O|, are as follows:
+
+ 1. Let |handler| be the <emu-val>this</emu-val> value.
+ 1. Let |length| be |handler|.\[[BackingList]]'s [=list/size=].
+ 1. Let |keys| be an empty [=list=].
+ 1. Let |i| be 0.
+ 1. [=While=] |i| < |length|:
+ 1. [=list/Append=] [=!=] [$ToString$](|i|) to |keys|.
+ 1. Set |i| to |i| + 1.
+ 1. [=list/Extend=] |keys| with [=!=] |O|.\[[OwnPropertyKeys]]().
Aha, here the "define a random index with undefined as value on the target array" bit would be observable. So yes, I think we should prevent defining it in `defineProperty` by either no-opping or throwing or treating no `[[Value]]` as undefined and doing the usual IDL type coercion (e.g. converting to 0 for numeric types).
--
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/heycam/webidl/pull/840#pullrequestreview-372257720
Received on Tuesday, 10 March 2020 20:39:24 UTC