| 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 [[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 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):
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. |
| 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 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? |
| 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> |
|
| 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 |
| 23:14 | <shu> | i dunno where to put it |
| 23:14 | <Mathieu Hofman> | If a Proxy can trap 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 |
| 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 |
| 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 |
| 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 |
| 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) |
| 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 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)? |
| 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 |
| 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 |
| 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) |
| 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 |
| 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 |