17:59
<rbuckton>
My apologies, I will be about 2 minutes late to the working session today
19:01
<rbuckton>
shu: At one point you had discussed having one shared struct inherit from another shared struct. If we ignore TLS prototypes and behavior for a moment, is there any specific benefit to modeling an actual inheritance model here, or would having the inherited struct just maintain the initial field layout of the base struct be sufficient?
19:03
<shu>
i think the benefit is more like "full composability with rest of the language", mainly
19:03
<shu>
i know the field has kind of soured on inheritance hierarchies vs inline storage of stuff
19:03
<shu>
but for e.g. AST nodes, you probably do want an inheritance hierarchy in the "layout prefix" sense that i was imagining
19:04
<shu>
AstNodeBase has loc or whatever
19:04
<rbuckton>
I'm more asking if there is any reason that struct B extends A {} needs to care about A other than its field layout (if you ignore TLS prototypes and constructor initialization logic)
19:05
<rbuckton>
(aside from internal AST reasons)
19:05
<shu>
ooh
19:05
<rbuckton>
It goes to simplifying the syntax I've been considering.
19:05
<shu>
i feel like no?
19:05
<shu>
my intention was literally for layout
19:06
<rbuckton>
In classes, field order is determined by calling super(), where each super constructor installs its fields and returns the thing to be the used as the this in the subclass constructor.
19:06
<rbuckton>
That helps
19:08
<shu>

what i want for struct inheritance semantics:

  • one shot initialization. even if we allow field initializers or user-programmable constructors, they get a fully initialized instance with all fields initialized to a sentinel (undefined, i guess)
  • superclass's fields precede your own fields
19:08
<shu>
the invariant is that a half-constructed, out-of-declared-order instance is not observable if you use structs
19:14
<rbuckton>

That syntax sketch I wrote up a few months back has a lot of corner cases to handle future complexity, like:

  • declaring whether a struct has a null prototype, or a "shared" prototype, or a TLS prototype.
  • declaring whether a struct field is "non-shared" on a shared struct (i.e., a TLS-backed field).
  • indicating whether a method is shared or non-shared, for a potential future that might somehow include shared functions.

I'd like to cut a lot of that for simplicity's sake. For example, every struct declaration has a non-shared prototype (a TLS prototype for shared structs). You can use extends null if you don't need the prototype, and we can just make that work as opposed to how class extends null doesn't work today.

19:14
<rbuckton>
So shared struct A extends B {} gives A a TLS prototype that inherits from B's TLS prototype.
19:15
<rbuckton>
If you do shared struct A extends B {} and B isn't shared, it doesn't matter. You just get A with the same layout as B, except it's shared, and the prototypes are non-shared anyways.
19:17
<rbuckton>
In a struct constructor, super() could be designed such that it doesn't support return override tricks, since the layout is already wired up.
19:19
<rbuckton>
And we could just assume methods are non-shared by default, and if shared functions ever becomes a thing you have to opt-in on a method-by-method basis. That seems like a good idea anyways, since you'd want to explicitly indicate that you'd thought about thread safety for a given "shared" method anyways.
19:19
<rbuckton>
All of that makes the syntax fairly simple.
19:25
<rbuckton>

Basically:

// non-shared struct
struct S1 {
  foo; // fixed-layout, non-shared field

  constructor() { } // realm-local constructor

  bar() { } // attached to realm-local prototype
  get baz() { } // attached to realm-local prototype
  set baz(value) { } // attached to realm-local prototype
}

// shared struct
shared struct S2 {
  foo; // fixed-layout, shared field

  constructor() { } // realm-local constructor

  bar() { } // attached to realm-local prototype
  get baz() { } // attached to realm-local prototype
  set baz(value) { } // attached to realm-local prototype
}

// null prototypes
struct S3 extends null {
  foo; // fixed-layout, non-shared field

  constructor() { } // realm-local constructor

  // cannot have methods/getters/setters
}

shared struct S4 extends null {
  foo; // fixed-layout, shared field

  constructor() { } // realm-local constructor

  // cannot have methods/getters/setters
}

// subclassing
struct S5 extends S1 {} // ok
struct S6 extends S2 {} // ok? S6 would be non-shared, even though S2 is declared as shared
shared struct S7 extends S1 {} // ok? S7 would be shared, even though S1 is declared as non-shared
shared struct S8 extends S2 {} // ok
19:27
<rbuckton>
Ideally, we could find some way of supporting private names and accessor, as I'd also like to support decorators long term. The private names bit is tricky for shared structs, though, as you wouldn't be able to guarantee "hard privacy" if it were supported, but private names are necessary to support accessor for decorators.
19:30
<Mathieu Hofman>

The private names bit is tricky for shared structs, though, as you wouldn't be able to guarantee "hard privacy" if it were supported

Couldn't private declarations help?

19:31
<rbuckton>
IMO, private names should be viable and are just part of the field layout. Wiring up identical struct definitions between two workers would verify they have identical layouts. It might not be true "hard privacy" though, if you are able to create a new worker with an altered struct definition that can still be correlated, but has a prototype method that exposes the private field. Maybe its not actually an issue, though, if we are planning to have struct layout identity based on file path/line number/etc.
19:31
<rbuckton>
Not unless private declarations are also shareable, and that seems even less safe.
19:32
<Mathieu Hofman>

Wiring up identical struct definitions between two workers would verify they have identical layouts.

I suspect if you had to explicitly register your structs, you could guarantee true privacy for private fields ;)

19:33
<rbuckton>
If the correlation mechanism is still file+position based, as we've discussed previously, then hard privacy isn't as much of an issue because the declarations have the same code.
19:33
<Mathieu Hofman>
Correct
19:33
<rbuckton>
If you had to use an API to explicitly register, you have even less privacy.
19:33
<Mathieu Hofman>
it's only a problem if you can forge the struct definition
19:34
<rbuckton>
Since I could spin up a Worker that registers its own version of the class that just replaces its methods with return this.#whatever and programmatically wire them up.
19:34
<rbuckton>
To prevent forging the struct definition, it would likely need to be path+position based
19:34
<Mathieu Hofman>
If you had to use an API to explicitly register, you have even less privacy.
Not if you have to use a type object that is itself sharable to hook the local behavior
19:35
<rbuckton>
If I have access to construct a Worker to do the right thing, then I have access to construct a Worker to do the wrong thing.
19:35
<rbuckton>
Unless that Worker has no control over how the correlation happens.
19:36
<Mathieu Hofman>
instead of using examplar
19:36
<rbuckton>
If I can send a trusted piece of information over to a Worker to establish the struct, then malfeasant code can do the same thing to forge the struct as well.
19:37
<Mathieu Hofman>
not if that piece of information is only obtained when declaring the struct
19:38
<rbuckton>
How do you do that, and have it declared in two different threads with the same information?
19:39
<Mathieu Hofman>
that's the tricky bit, especially with syntax
19:39
<rbuckton>
file+position is essentially obtained when declaring the struct and is potentially unforgeable (especially if all workers pointing to the same file have to use the same cached source)
19:40
<Mathieu Hofman>
I can do it imperatively. I believe I actually did in some of my earlier attempts at linking types
19:41
<rbuckton>
Also, my argument isn't that "if we can't do hard privacy we can't have this feature", it's "if we can't do hard privacy, users would need to accept that if they want to use this feature"
19:42
<Mathieu Hofman>
I agree that file + position is unforgeable (caveats when you start introducing a module loader). I was talking about an escape hatch to avoid that constraint
19:43
<rbuckton>
To be fair, the forgeability is only a concern if you hand untrusted code the ability to create a new Worker with the necessary correlation information. If the untrusted code doesn't have access to that, they can't forge it.
19:46
<rbuckton>
file+position is potentially easier for consumers as its less complex to set up, though its harder for bundlers since they need to isolate struct definitions to individual files. Defining some kind of private token that you need to attach to a declaration before the module graph is loaded seems extremely hard to do correctly, and if the token is just a string/URI/UUID then malfeasant code just needs to know what that string is to construct a new Worker that points to a different file with a struct that masquerades as the original one.
19:47
<rbuckton>
I'm a fan of being able to tag a struct with something like a UUID to correlate, but it does weaken private names in that context.
19:48
<rbuckton>
Of course, malfeasant code would have to be able to execute a custom tailored script, which could run afoul of CSP in a properly configured environment, so maybe that's not so much of a concern either.
20:38
<rbuckton>
shu: Could you make me a maintainer on https://github.com/tc39/proposal-structs? I don't seem to have enough access to add PR reviewers
21:32
<shu>
rbuckton: done