don't automatically initialise all internal slots to undefined
We need to clean up how we create objects and set initial values of their internal slots. It is a common pattern today to call OrdinaryCreateFromConstructor with a list of additional slot names and then in the immediately following steps, set the initial values of each of these additional fields. But technically according to 6.1.7.2 Object Internal Methods and Internal Slots, these slots are given the value undefined in the interim.
Unless specified otherwise, the initial value of an internal slot is the value undefined.
This means that the declared type of each of these internal slots is implicitly unioned with undefined. We should instead create these slots and give them their initial values atomically, then remove this provision.
See related https://github.com/tc39/ecma262/pull/3383.
Recap of editor call:
- Let's change MakeBasicObject to take anonymous records of { [[SlotName]]: initialValue } "templates"
- Do the exercise of changing the callers to use the new form, which hopefully naturally points out where currently the initialization is already too far away from the allocation
And of course remove the implicit initialisation affordance.
MakeBasicObject is used upstream, at least in WebIDL but plausibly also other specs, so this will take at least a little integration work.
There's a related issue here in that there may be an invariant that is stated in terms of 2 or more slots, such that there is no way to change the value of 1 slot at a time without violating the invariant between steps. The slots would need to be atomically set in the same way we need to atomically set them as we are initialising an object. Maybe our solution should cover both of these use cases?
We should just violate the invariant between execution of those two steps.
Or, alternatively, use one slot with a Record in it instead of two separate slots?
@bakkot If we were okay with that kind of sloppiness, I don't see why we wouldn't be okay with the sloppiness in the OP: just create the object without specifying the values of the slots and then populate the slots afterward. I'm personally not okay with that kind of sloppiness though. I think this is a worthwhile problem to solve.
@ljharb That's reasonable though unfortunate that we have to refactor the data because we have no spec facility for atomic assignment. Another option would be to just do multiple assignments in one step and say the spec invariants only hold between any two steps.
A proposal: remove the line about slots being initially undefined (including https://github.com/tc39/ecma262/pull/3537), and change all the places where we initialize a slot using the wording "Set obj.[[slot]] to value" to instead use the wording "Initialize obj.[[slot]] to value". That will hopefully make it less likely that a later refactoring will move the initialization of the slot away from its creation.
(Right now at least [[GeneratorContext]] is initialized pretty far from its creation, which is annoying, as mentioned in https://github.com/tc39/ecma262/issues/3482.)
I want to talk about a step with atomic substeps again at editor call. We also need to document our decision here so I can work on this.
At editor call today, @syg and I are leaning toward an in-between solution where we have a special multi-step form where objects with internal slots are allocated and the slots are initialised. We will use machine checking to ensure that objects with internal slots are not created in any other way, and that all internal slots are initialised before the object escapes this form.
I'll also look into what other places we might want to temporarily violate some spec invariant to see if it's worth adding the more generalised atomic substeps form.
At editor call today, @syg and I are leaning toward an in-between solution where we have a special multi-step form where objects with internal slots are allocated and the slots are initialised.
Is this better than the plan from a year ago? That one didn't involve defining new forms.
We will use machine checking to ensure that objects with internal slots are not created in any other way, and that all internal slots are initialised before the object escapes this form.
So if a step says Set _obj_.[[Foo]] to X, the checker must complain if it can't prove that _obj_ was created with a [[Foo]] slot. This sounds like more than ecmarkup can handle, though I imagine esmeta could (or already does?).
Is this better than the plan from a year ago? That one didn't involve defining new forms.
Not really better, just a bit different. It would allow us to interleave other steps during the initialisation as long as the object being initialised doesn't escape. Though it does syntactically allow for that possibility, and could cause a problem if not machine checked. But it's in the direction of a more generic step with atomic substeps, if we ever end up going that route.
So if a step says
Set _obj_.[[Foo]] to X, the checker must complain if it can't prove that_obj_was created with a[[Foo]]slot.
No, it would know that it couldn't possibly have received a reference to _obj_ before it was fully initialised. All that ecmarkup would need to do is check that an initialize _obj_.[[....]] to ... step for every slot of its Record type dominates any read references of _obj_.