21:56
<Mathieu Hofman>
Can someone remind me, why is disallowing Reflect.get / Reflect.set from accessing shared struct fields not acceptable? Aka always triggering the [[Get]] behavior? Also have proxies implement an [[UnsafeGet]] that triggers the get trap as is (no extra arg)? From what I gather it would just prevent using proxies or reflect to access shared fields on shared structs, but not prevent access to non-shared behavior. Is that really unacceptable?
22:30
<rbuckton>
Inside of an unsafe block, every get is an [[UnsafeGet]]. Otherwise implementations would need to check the type of the object on every get/set, which would slow down every get/set operation even outside of an unsafe {} block. This was deemed an unacceptable performance regression.
22:32
<rbuckton>
(though I've refactored [[UnsafeGet]] and [[UnsafeSet]] out now, so its just an unsafe parameter passed to [[Get]]).
22:52
<shu>
well, IIRC the idea is that inside unsafe {}, we compile . and [ ] to a different bytecode (say, UnsafeGet vs Get)
22:53
<shu>
that should be orthogonal to what API calls do
22:53
<shu>
if Reflect.get wants to always be equivalent to a Get i don't think that slows down anything
22:54
<shu>
for proxies i'm not entirely clear, since the . access in an unsafe {} gotta do something
22:55
<Mathieu Hofman>
Inside of an unsafe block, every get is an [[UnsafeGet]]. Otherwise implementations would need to check the type of the object on every get/set, which would slow down every get/set operation even outside of an unsafe {} block. This was deemed an unacceptable performance regression.
I don't see how that's relevant to the question I asked. Afaik, the default [[UnsafeGet]] behavior is to execute the [[Get]] steps, no ?
22:55
<shu>
i feel like the degree to which Reflect.get is always [[Get]] is unacceptable comes down to language compositionality concerns
22:55
<rbuckton>
Not quite. It carries the [[UnsafeGet]] down through the prototype walk in case you have someone who did Object.create(sharedStructInstance).
22:56
<shu>
but that's still like, internal MOP operations
22:56
<Mathieu Hofman>
for proxies i'm not entirely clear, since the . access in an unsafe {} gotta do something
I'm suggesting it would just trigger the get trap as normal, which wouldn't be able to access the target's field unless the get trap uses syntax and an unsafe block
22:56
<Mathieu Hofman>
so yeah a proxy would break that "carry through"
22:57
<rbuckton>
As to why disallowing Reflect.get/set on shared structs, I'd like to be able to benefit from Reflect.get/set's prototype walking behavior and support for a receiver when working with objects.
22:57
<rbuckton>
We do expect shared structs will have prototypes, so I'd like for this to work.
22:58
<shu>

i don't think that slows anything down AFAICT, but it's a sharp corner and i'd rather avoid because (unless i'm still missing something, Ron seems to have thought much more about this):

  • Atomics methods are always [[UnsafeGet]]
  • so if Reflect methods and Proxy methods are always [[Get]]...

that just feels non-compositional and bad? like you're just gonna have to memorize some API entry points are unsafe, some are safe?

22:58
<Mathieu Hofman>
We do expect shared structs will have prototypes, so I'd like for this to work.
do we expect shared structs to be used as prototypes is the question
22:58
<shu>
probably not the common case but i don't see why not
22:59
<shu>
if you prohibit shared structs from being usable as prototypes that's really introducing a big new prohibition to work around a niche API
22:59
<shu>
like what objects can't be used as prototypes?
22:59
<rbuckton>
Yes. We've even discussed shared prototypes in the past as well, and would likely want them if we ever have shared functions.
23:00
<shu>
again, this is not an airtight defense against racy access because SABs exist
23:00
<shu>
don't rathole
23:01
<Mathieu Hofman>
Btw, I forgot to mention this morning, but yesterday some people were under the impression that shared struct could somehow be primitives. I think because of their "only contain other shared structs", and the time when this was presented, it somehow got confused with records/tuples. Just an FYI for your presentation, making clear these are mutable objects.
23:01
<shu>
thanks for the heads up
23:01
<shu>
who was confused?
23:02
<Mathieu Hofman>
Jordan
23:03
<shu>
rbuckton: help me understand the cons of making Reflect.get() always [[Get]], and adding Reflect.unsafeGet as always [[UnsafeGet]]
23:03
<shu>
that doesn't reproduce the coloring problem does it?
23:04
<rbuckton>
If a Proxy cannot trap an unsafeGet, then it cannot realistically know whether it should forward on the operation via Reflect.get or Reflect.unsafeGet.
23:04
<shu>
is that the only downside?
23:05
<rbuckton>
no
23:05
<rbuckton>
Also, committee reticence for having any methods on Reflect that aren't related to the proxy traps.
23:05
<shu>
the proxy downside sounds to me like "you can't proxy shared structs and be aware of unsafe {} blocks"
23:06
<ljharb>
fwiw this is no longer the case, as we explicitly decided last year wrt getIntrinsic and the symbol predicates
23:06
<shu>
ljharb: so no these aren't new primitives, they're exotic objects
23:07
<rbuckton>
If a Proxy can trap unsafeGet and we always forward GetValue through unsafe get inside of an unsafe block, then Proxies stop working for regular objects inside of unsafe.
23:07
<ljharb>
yeah i had some picture in my mind of them as immutable one-shot-init structs that can only contain primitives
23:07
<shu>
they are one-shot-init sealed objects
23:07
<ljharb>
but i guess that's what Records are
23:07
<shu>
yeah
23:08
<rbuckton>
Structs aren't immutable, no. That's half the point.
23:09
<shu>
rbuckton: so i can live with proxy traps passing a bool i think and Reflect.get/set taking a bool?
23:09
<shu>
like that'd solve everyone's problems wouldn't it?
23:09
<shu>
i don't feel like that actually slows down anything either?
23:10
<rbuckton>
fwiw this is no longer the case, as we explicitly decided last year wrt getIntrinsic and the symbol predicates
In the call today it was mentioned that some people do new Proxy(obj, { ...Reflect, <whatever traps> }) for some reason, and IMO that seems like a very bad practice
23:10
<ljharb>
it is, and it's fine if new additions break them, but i also don't think "extra" handler methods would throw?
23:10
<rbuckton>
rbuckton: so i can live with proxy traps passing a bool i think and Reflect.get/set taking a bool?
I mean, that's one of the three options we discussed, yes.
23:10
<shu>
yeah i think that seems doable
23:10
<ljharb>
(btw presumably there's a way to brand-check both kinds of structs?)
23:10
<shu>
there's no way to brand-check non-shared structs
23:10
<shu>
because they are "just" sealed objects
23:11
<shu>
so i don't know why you'd want to brand check them
23:11
<shu>
for shared structs, the spec draft doesn't have a predicate but you can actually write one that's kinda funny
23:12
<rbuckton>
  1. Proxy/Reflect is permissive: treats all get/set as if in unsafe {}
  2. Proxy/Reflect is restrictive: treats all get/set as if outside of unsafe {}, so they don't work in unsafe {} at all
  3. Proxy/Reflect accept/pass an unsafe boolean argument: Existing implementations will continue to work in unsafe {} with normal objects, need to opt in to work with shared structs.
23:13
<shu>
shared struct Tester { field; }; function IsSharedStruct(x) { let tester = new Tester(); try { unsafe { tester.field = x; } } catch (e) { return false; } return typeof x === 'object'; }
23:13
<rbuckton>
for shared structs, the spec draft doesn't have a predicate but you can actually write one that's kinda funny
Spec draft doesn't have one, but we did need to add one in the dev trial so we should probably add it.
23:14
<shu>
i dunno where to put it
23:14
<Mathieu Hofman>
If a Proxy can trap unsafeGet and we always forward GetValue through unsafe get inside of an unsafe block, then Proxies stop working for regular objects inside of unsafe.
Unless we specify the behavior of a missing unsafeGet trap to call the get trap and its target [[Get]] fallback (instead of falling back on the target's [[UnsafeGet]])
23:14
<rbuckton>
On Reflect or Atomics, I suppose.
23:14
<shu>
hm yeah maybe Reflect.canBeSharedAcrossAgents?
23:14
<rbuckton>
Unless we specify the behavior of a missing unsafeGet trap to call the get trap and its target [[Get]] fallback (instead of falling back on the target's [[UnsafeGet]])
That again breaks the prototype walk behavior of [[Get]] if you have a shared struct prototype
23:14
<shu>
and then you can use that in conjunction with typeof == object?
23:15
<Mathieu Hofman>
I think there is no case where supporting existing proxies that implement a get trap can transparently forward to the unsafe behavior up the prototype chain is possible. You just need to update your proxy implementation
23:16
<rbuckton>
I think there is no case where supporting existing proxies that implement a get trap can transparently forward to the unsafe behavior up the prototype chain is possible. You just need to update your proxy implementation
Except for option (1) where Proxy/Reflect is permissive
23:16
<ljharb>
fair, altho the "why" is "for debugging", a reason you'd want to brand-check anything (not the only reason, but certainly one of them)
23:17
<shu>
but for this you'd want to brand check if it's an instance of a particular struct declaration
23:17
<shu>
not that it is a struct
23:17
<Mathieu Hofman>
Except for option (1) where Proxy/Reflect is permissive
Not really, that's just taking the opposite approach, any access from "safe" code becomes unsafe when going through a proxy
23:17
<shu>
because an unshared struct has no additional exotic behavior, unlike shared structs
23:18
<shu>
Mathieu Hofman: rbuckton it sounds like we won't get agreement on either the completely permissive (1) or the completely restrictive (2)
23:18
<rbuckton>
Not really, that's just taking the opposite approach, any access from "safe" code becomes unsafe when going through a proxy
regular objects with a regular prototype chain wouldn't care.
23:18
<shu>
and the only hiccup for a boolean-passing (3) is possibly implementation/performance difficulty, which i personally can't think of right now but maybe one of the other engines have something more concrete
23:18
<shu>
if that's the case why don't we focus our energy on (3)?
23:20
<shu>
fair, altho the "why" is "for debugging", a reason you'd want to brand-check anything (not the only reason, but certainly one of them)
so for brand-checking for instances of a particular struct declaration, struct methods are non-generic, so every method already includes a brand check
23:20
<ljharb>
ok, that kindof works
23:20
<nicolo-ribaudo>
What if we had [[SafeGet]]/[[Get]] instead of [[Get]]/[[UnsafeGet]], so that Reflect.get can be unsafe and we can add a Reflect.safeGet for when you want to be safe
23:20
<ljharb>
but yeah i'd want some kind of isSharedStruct method or similar for any of them
23:21
<shu>
but yeah i'd want some kind of isSharedStruct method or similar for any of them
what about Reflect.canBeSharedAcrossAgents() that returns true for shared structs + primitives
23:21
<shu>
and you'd use that in conjunction with checking for non-primitive to know it's a shared struct
23:21
<ljharb>
SGTM
23:21
<ljharb>
not in love with adding the word "agent" to observable JS, but the semantics are fine
23:21
<shu>
well i mean
23:21
<shu>
i'd like it to be just canBeShared
23:22
<ljharb>
better :-)
23:22
<shu>
i was gonna say maybe "shared" is confusing but maybe not
23:22
<shu>
since we already call it SharedArrayBuffer
23:22
<rbuckton>
I think Reflect.isShareable is probably good enough. I'm in favor of concise names.
23:22
<rbuckton>
That SharedArrayBuffer isn't shareable is unfortunate
23:22
<shu>
ehhh ok
23:22
<shu>
lol right
23:23
<shu>
i don't think we can win a fight on distinguishing the nuanced meaning between Shareable and Shared
23:23
<rbuckton>
We do probably want a way to address that, like a shared handle to a SharedArrayBuffer.
23:23
<shu>
right, that came up earlier with nicolo-ribaudo
23:24
<shu>
i don't want to expand the scope more here, i want to do that in a separate proposal
23:24
<rbuckton>
past-tense isShared seems like it would indicate whether the struct is actually shared between two agents.
23:24
<shu>
agreed, that's why i had "can be"
23:26
<shu>
all right let me add this real quick to the spec draft
23:26
<Mathieu Hofman>
if that's the case why don't we focus our energy on (3)?
Agreed, I was just trying to point out that a boolean passing also does not satisfy the requirement of "automatically work for existing proxy which have a shared struct somewhere on their chain", which IMO I think is fine
23:26
<shu>
ah, gotcha
23:28
<Mathieu Hofman>
Btw, is there a reason getOwnPropertyDescriptor cannot return {value: undefined} (or maybe even a missing value field) for shared structs ? Also what is the expected behavior of defineOwnProperty on shared struct (aka does it ignore the define) ?
23:28
<shu>
i don't understand the getOwnPropertyDescriptor question. it does return a property descriptor for properties that exist
23:28
<shu>
oh, you're asking why it shouldn't just always return value: undefined?
23:29
<shu>
because that seems like... lying?
23:29
<Mathieu Hofman>
correct
23:29
<Mathieu Hofman>
it's exotic behavior that I believe is legal
23:29
<rbuckton>
For all of this discussion we should keep in mind that the distinction as to whether a get/set is safe or unsafe is a purely manufactured restriction. What we're trying to determine is how far we feel we need to push that restriction. IMO, stepping into Proxy/Reflect land is already complex behavior.
23:29
<shu>
sure, but it seems incredibly confusing
23:29
<shu>
why would i strive to make it confusing
23:29
<shu>
For all of this discussion we should keep in mind that the distinction as to whether a get/set is safe or unsafe is a purely manufactured restriction. What we're trying to determine is how far we feel we need to push that restriction. IMO, stepping into Proxy/Reflect land is already complex behavior.
big +1 to this
23:29
<shu>
it's about reducing the likelihood
23:30
<shu>
there is such a thing as diminishing returns
23:31
<shu>
Btw, is there a reason getOwnPropertyDescriptor cannot return {value: undefined} (or maybe even a missing value field) for shared structs ? Also what is the expected behavior of defineOwnProperty on shared struct (aka does it ignore the define) ?
the behavior of defineOwnProperty mirrors that of ordinary defineOwnProperty. if all the the property descriptor's fields match except the value, it writes the value
23:31
<shu>
otherwise it returns false
23:31
<rbuckton>
IMO, any API call like Object.gOPD is far enough away from guarding simple x.y access.
23:31
<Mathieu Hofman>
ok let's start with the defineOwnProperty case and go from there, what is the expected behavior? Does it do a "set" of the value?
23:31
<shu>
yes, if enumerable, writable, configurable match
23:31
<shu>
(and if it exists, of course)
23:32
<rbuckton>
I'm also really hesitant to manufacture an entire bifurcated reflection API across Reflect and Object just to preserve a manufactured distinction.
23:32
<shu>
yes, let's keep the use case in sight
23:33
<Mathieu Hofman>
I mean my undefined / ignore defined value is consistent with the idea that these objects have exotic access behavior, and that you need to use explicit path to access the value
23:34
<shu>
but why make it more confusing
23:34
<shu>
the mental model here is "like unshared structs, but with more restrictions"
23:35
<Mathieu Hofman>
Coming from the "author interacting with objects" that doesn't seem like a bifurcation to me.
23:35
<shu>
the undefined / ignore defined value moves the needle towards "shared structs are actually a completely different thing"
23:35
<Mathieu Hofman>
yes and the restriction is you cannot use own prop MOPs to interact with the value of these fields
23:35
<shu>
i think you should be able to if you want to?
23:36
<Mathieu Hofman>
how is that different from saying I should be able to "get" those fields from anywhere?
23:36
<shu>
it's different because i think own prop MOPs and Reflect are already escape hatches
23:37
<Mathieu Hofman>
they're used a fair amount by libraries
23:38
<shu>
does that contradict what i said?
23:41
<shu>
but also why wouldn't we extend the boolean parameter to gOPD and dOP?
23:43
<Mathieu Hofman>
I think if we allow access to shared struct field values through own prop MOP, we're opening too wide the door for reasoning about "code will only interact with shared fields in unsafe aware code". A boolean value on get is ok because it's an explicit opt-in. The others are not an opt-in of understanding what you're dealing with
23:44
<shu>
but why wouldn't we pass the boolean on gOPD?
23:45
<Mathieu Hofman>
because own prop MOP are meaningless for shared fields: you can't reconfigure them, at best you can make them non-writable
23:45
<shu>
you can't make them non-writable either
23:45
<shu>
that would be a mutation on the underlying layout
23:46
<shu>
well, they're not meaningless, some people use it just to test for existence of an own prop
23:46
<shu>
define is kind of meaningless, but i suppose some people have code that wants to not trigger setters
23:46
<Mathieu Hofman>
right, at best was if we can figure out how to do that (I share Ron's view that it'd be great to support non writable fields)
23:46
<shu>
but they're not meaningless, they're not very useful at the moment
23:46
<Mathieu Hofman>
ah but you can test for the existence, you just can't access the value
23:47
<shu>
so i don't see the harm in also passing a boolean to getOwnPropDescriptor and defineOwnProperty
23:47
<rbuckton>
right, at best was if we can figure out how to do that (I share Ron's view that it'd be great to support non writable fields)
I'm still noodling on finding a way to do init-only fields, at the very least.
23:47
<shu>
i want these to be as close as i can get to ordinary objects, and where they differ, the divergence is signaled loud and clear (like throwing, if the boolean is not passed)
23:48
<shu>
instead of subtly different, like you get a property descriptor back without a value
23:48
<Mathieu Hofman>
I think a boolean to gOPD / dOP is not sufficiently motivated, I don't think authors have any need to access the value of the field through them
23:49
<shu>
i am not understanding the harm
23:49
<shu>
it doesn't seem like a lot of complexity, it's explained by simple symmetry to get/set
23:49
<rbuckton>
I think a boolean to gOPD / dOP is not sufficiently motivated, I don't think authors have any need to access the value of the field through them
I disagree. Reflection capabilities should be consistently usable for any object.
23:49
<Mathieu Hofman>
I suppose however it would work too if you want to avoid the potentially surprising result
23:50
<rbuckton>
Between Reflect and decorators and decorator metadata, I want more reflection capabilities in JS, not less.
23:50
<shu>
right, i want to minimize surprise. i want the actual surprising thing to be thrown in their face (it's racy and unsafe)
23:50
<Mathieu Hofman>
ok so boolean for gOPD / dOP, and if the property is an existing shared struct field, throw if the boolean isn't true ?
23:51
<shu>
ya
23:51
<shu>
well that's what i'm thinking at this time, to be worked out pending the feasibility of the boolean in general
23:51
<shu>
which i am optimistic on to be clear
23:51
<shu>
but need to do more homework