05:40
<Mathieu Hofman>

I think I caught up on the discussion.

  1. I strongly hold that existing Reflect & co APIs should not work on writable shared structs fields in non unsafe blocks.
    a. I understand this may break existing code that blindly try to access objects, but technically this is already a possibility with exotic objects, and I really don't want to see us modify the shape of property descriptors.
    b. It may be acceptable for some new dedicated Reflect APIs to access writable shared struct fields in non unsafe block, but I like the idea that "unsafe" access requires new syntax. It'd be a much more consistent model for audits.
  2. I believe that existing Reflect & co APIs should work on shared struct fields inside unsafe blocks. This is especially true if we don't have new dedicated unsafe APIs
  3. At first I didn't like function.unsafe as it felt like a form of dynamic scoping, but I am now warming up to it. As explained it is similar to new.target: the unsafe block changes the semantics of the call, like new would, and as the callee you get to sense the semantic change through some new piece of syntax.
    a. Unlike construct, I don't think we need Reflect.unsafeCall/Reflect.unsafeContruct and the corresponding proxy traps as long as the existing call/construct traps get to sense through function.unsafe, so they can use an unsafe block to trigger the change of semantics on the target.
  4. unsafe function () { ... } would effectively be the equivalent of function () { if (!function.unsafe) throw TypeError(); unsafe { ... } }
  5. I like the idea of non-writable properties of shared structs being safe to access anywhere, but since the change from writable to non-writable is intrinsically dynamic, we have to consider whether we might opening too wide a door for authors to shoot themselves in the foot: since the access may not be audited as unsafe, they might not realize that there could be a race with the freezing of the property.
    a. even if the property becomes non-writable at init, given that you can share the struct before init completes, it is still a dynamic state change
09:16
<shu>
(5) is a non-starter
09:17
<shu>
it's not possible to change from writable->non-writable for shared structs fields, because the invariant is shared structs have fixed shape
09:17
<shu>
freezing the properties changes the shape, which requires synchronization, which means all accesses will need to become synchronized on the shape, which is too slow
09:19
<shu>
the only things i can imagine working is something like being able to freeze the properties before an instance escapes the local thread, but the precise form of that check is also too expensive to perform, so it'll be a conservative check like "has this instance ever been assigned to another shared struct, or been postMessaged"
09:19
<shu>
i am not sure how useful that is
09:20
<shu>
the more sensible thing is to declare fields as non-writable and create them non-writable
09:23
<shu>
(5.a) is also not true, you cannot share a struct before init completes
09:24
<shu>
but it may be because we have different models of "init" here
09:24
<shu>
for the same reason of the shape itself being immutable, it won't be possible to do any freezing post-construction
09:25
<shu>
so the only way is to declare the shape up front to have some field f be already frozen, and there would be some generated constructor that takes the initial value for f such that by the time user code gets a constructed instance, it has the value for f already. the user initializer won't be able to change the value of the field
09:26
<shu>
i'd really like to defer declaration of frozen fields and to be a follow-on proposal if possible
09:28
<Mathieu Hofman>
My understanding of structs was that the object is constructed with a known set of fields with each a value of undefined, then the init step runs, which can set these fields to their value.
09:28
<shu>
freezing the properties changes the shape, which requires synchronization, which means all accesses will need to become synchronized on the shape, which is too slow
actually i'll caveat this: it might not be too slow to have rel/acq accesses on the shape itself, but that opens up precisely the "too wide a door" issue you raised above
09:28
<shu>
My understanding of structs was that the object is constructed with a known set of fields with each a value of undefined, then the init step runs, which can set these fields to their value.
right, there's no way to declare a field to be frozen right now. everything is mutable
09:29
<shu>
so your (5) can't come up in the current proposal, is what i was explaining
09:29
<Mathieu Hofman>
That init step could share or otherwise set the struct as a field of another struct, which means it can escape before all the init steps complete
09:29
<shu>
"non-applicable" would've been better than "non-starter"
09:29
<shu>
That init step could share or otherwise set the struct as a field of another struct, which means it can escape before all the init steps complete
that's right. but the initializer can't freeze, because you just can't freeze shared structs right now
09:33
<Mathieu Hofman>
Yeah I just don't see in this construction+init model how you could provide a value for a field before the instance is constructed, without reverting to the model classes have, aka have a dead zone before super is called.
09:34
<shu>
oh it'd probably be some ugly thing
09:35
<shu>
but yeah i haven't fully thought out how this would look
09:36
<shu>
you can imagine something like, the first argument to a shared struct constructor is always an "initializer object" whose fields get assigned to like-named fields on the shared struct, before the user initializer is called
09:36
<shu>
this is real ugly because if your user initializer takes arguments you'd always be doing new MyStruct(undefined, myFirstArg, etc)
09:37
<Mathieu Hofman>
Well I suppose only the base constructor needs that argument
09:38
<shu>
well, the subclasses need to pass it along somehow
09:38
<Mathieu Hofman>
Oh right. Ugh that's not ergonomic
09:39
<shu>
nope, "some ugly thing"
09:39
<shu>
which is partly why i'd like to avoid speccing frozen-at-declaration fields initially
09:41
<Mathieu Hofman>
Should probably think it through to make sure the init mechanism doesn't make that impossible in the future
09:43
<Mathieu Hofman>
The way we've been handling a similar problem currently is to have our "init" return the set of fields and not have access to the instance reference (which doesn't actually get created until after init runs)
09:45
<Mathieu Hofman>
Then we have an optional "finalize" step which gets access to the populated instance and gets to perform any external wiring before the instance is returned to the caller
11:32
<littledan>
We would really need C++-style initializer lists to do this kind of frozen property well, IMO
11:33
<littledan>
or, we could go back to Records and Tuples, but object-based -- the various ways of constructing them let you fill in contents without modifying existing things
11:34
<littledan>
ES6 classes didn't really give us a great basis for initializer lists because of how the instance is constructed in the base class and then subsequent subclass constructors can just mutate it. This constrained the design of class fields a lot
11:56
<shu>
We would really need C++-style initializer lists to do this kind of frozen property well, IMO
this intuitively seems true to me