17:14
<rbuckton>
Thanks. Starting with the syntax sketch I boiled down https://gist.github.com/rbuckton/e1e8947da16f936edec1d269f00e2c53 to the things we actually need. In essence, it uses the same syntax as class, except with the keywords struct or shared struct to indicate how both definition evaluation and instantiation will fundamentally differ from regular classes.
17:18
<rbuckton>
I'd also like to include support for Decorators in the actual final grammar as the same rationale for decorators on classes applies to structs. The caveat being that we would need to decide how we would solve for private fields to be able to support accessor as a construct. If structs are to have behavior, I feel it is important that the MVP for this proposal not ignore Decorators.
17:29
<rbuckton>

As a result, the actual specified grammar for struct and shared struct will be fairly minimal as it will mostly borrow from ClassDeclaration. There are a few things we need a clear position on:

  • Will there be such a thing as a StructExpression, akin to ClassExpression? For non-shared structs, it could possibly be supported, but I don't think its viable for shared structs if we are to go the path+position route for cross-thread correlation.
  • Are we restricting StructDeclaration to only be allowed at the top level of a Script or Module? If they can be used inside of a function body, that also would break path+position for cross-thread correlation.
  • Would a StructDeclaration be allowed inside of an if or switch at the top level? That would not break path+position correlation.
  • What about for, while, do? They could conceivably be run zero or one times, but evaluating them multiple times would break path+position correlation.
  • Would shared struct fields be allowed to have Symbol-named properties? If so, are there restrictions regarding whether those symbols are built-ins, from Symbol(), or from Symbol.for()?
17:33
<rbuckton>
Also, in the doc you shared you indicate non-shared structs might be out of scope? Can you clarify what you mean about non-compositionality? Do you mean if that we only had fixed-layout shared structs, the restriction that prohibits non-shareable values in its fields would be problematic? And if so, is that be problematic for JS, WASM-GC, or both?
17:40
<rbuckton>
Also, you indicate that Mutex/Condition are "Nice to have features immediately after MVP". Are you indicating this would be a follow-on proposal, or just that these are JS-specific needs that are over and above the shared needs of JS and WASM?
19:20
<shu>
Also, you indicate that Mutex/Condition are "Nice to have features immediately after MVP". Are you indicating this would be a follow-on proposal, or just that these are JS-specific needs that are over and above the shared needs of JS and WASM?
definitely the latter, but i'm undecided yet about the former
19:20
<shu>
i feel like it shouldn't be a follow-on proposal, but bundling means slower progress for now, which may in itself be okay
19:28
<shu>
Also, in the doc you shared you indicate non-shared structs might be out of scope? Can you clarify what you mean about non-compositionality? Do you mean if that we only had fixed-layout shared structs, the restriction that prohibits non-shareable values in its fields would be problematic? And if so, is that be problematic for JS, WASM-GC, or both?
no, not that targeted. i meant something like: if we're looking at the use cases and design constraints alone, there isn't anything too compelling at this time to motivate normal structs. but that feels pretty bad from a PL design perspective and is a sharp corner. it seems like the "fixed layout" part should compose (with additional constraints) with the sharing, and leaving it out seems like an arbitrary non-compositionality
19:33
<littledan>
The main thing that would motivate non-shared structs is if engines felt like they could encourage developers to adopt it in exchange for lower overhead vs classes. This is a thing that JS developers widely say they want, and the question is whether engines feel like non-shared structs might provide that. Historically, engines have been skeptical of making such a promise around performance--it's not clear whether that's the right thing to be optimizing, or whether this construct will always give it when ranging across all future optimizations, so it's not clear whether a performance tradeoff can be controlled this way. [There might be other "integrity"-related arguments for non-shared structs, but I'm not so interested in those; IMO just use private fields if you want integrity.]
19:33
<rbuckton>
I see, thanks. The impact regarding syntax is that if we only ever had shared structs, then I would just use struct to mean "the shared, fixed-layout thing". There would be no reason to disambiguate with a shared keyword.
19:34
<littledan>
I've pushed for non-shared structs for that PL design argument, and I accept that that's fairly weak.
19:36
<shu>
The main thing that would motivate non-shared structs is if engines felt like they could encourage developers to adopt it in exchange for lower overhead vs classes. This is a thing that JS developers widely say they want, and the question is whether engines feel like non-shared structs might provide that. Historically, engines have been skeptical of making such a promise around performance--it's not clear whether that's the right thing to be optimizing, or whether this construct will always give it when ranging across all future optimizations, so it's not clear whether a performance tradeoff can be controlled this way. [There might be other "integrity"-related arguments for non-shared structs, but I'm not so interested in those; IMO just use private fields if you want integrity.]
we have ideas there, but i kinda don't want them to lump those ideas into this proposal at the moment. namely, when i discussed with V8 staff, the sentiment was that explicit classes that don't change layout aren't necessarily more performant than hidden classes from a megamorphism POV, but we may have opportunities in layering additional restrictions on top to aid performance
19:37
<shu>
one idea that was raised was additional restrictions on methods declared within structs, like making them always throw on instances of different types, and making them unbindable (unrelated ideas)
19:37
<shu>
though those restriction just as well applies to shared structs
19:41
<rbuckton>
While I doubt that structs would solve it, my biggest wish for V8 would be some mechanism to avoid megamorphism on the discriminant property an ADT union, for example: node.kind. Pretty much every access to .kind in the TS compiler is megamorphic, though we often branch on kind and those branches are usually monomorphic, at least with respect to the node used in those branches.
19:42
<shu>
i have been thinking about this for like 10 years
19:42
<rbuckton>
If there's a chance that fixed layout, non-shared structs could solve that, it would be a strong indication for me that they have value beyond just shared structs.
19:42
<shu>
no idea how to solve it
19:42
<shu>
the levers i know for monomorphization like that depend on duplicating code and type systems
19:43
<rbuckton>
My hope is that a proposal like ADT enums would be a possible solution, since all branches of an ADT enum would be known at declaration time.
19:43
<shu>
but how do you know what's worth monomorphizing and what's not worth it?
19:43
<shu>
and what do you monomorphize? do you like, peel off a little chunk of code and duplicate that, parameterized around the "arms" of the union? do you do it at the whole function level? what if the function is really big?
19:44
<shu>
lots of art
19:45
<rbuckton>
i.e., an ADT enum declaration could encode into its type the internal discriminant used to differentiate between each constituent of the enum and the optimizer could leverage that when encoding the IC.
19:45
<shu>
oh i see, at the IC level
19:45
<shu>
shouldn't that already be the case?
19:45
<shu>
i guess you're saying that the cut-off for when an IC goes polymorphic -> megamorphic is too low for these ADT union cases
19:46
<shu>
and if we can tell the IC system "actually, the number of cases is bounded, so you should just do the polymorphic thing even in this case that looks like it'll grow new type cases forever"?
19:46
<rbuckton>
enum Node {
  Identifier(text),
  BinaryExpression(left, op, right),
  PrefixUnaryExpression(op, operand),
  PostfixUnaryExpression(operand, op),
  // ...
}

match (node) {
  when Node.Identifier: ...;
  when Node.BinaryExpression: ...;
}
19:47
<rbuckton>
So checking the internal discriminant for each constituent would be monomorphic, and thus the types collected within the match leg for that case would also be monomorphic as those ICs only ever see the Node constituent for that branch.
19:49
<rbuckton>
You could imagine a Node constituent is internally represented as something like a TaggedNode { tag, data }. A match leg would test against the tag (monomorphic), but the rest of the properties are in data, even though the runtime perceives it as a single object.
19:53
<rbuckton>
But that's orthogonal to the discussion. I don't have strong feelings one way or the other towards unshared structs. All of my use cases are for shared structs. If fixed layout could address the ADT union issue, it would maybe push me more in favor of non-shared, but otherwise I'm mostly ambivalent.
19:53
<rbuckton>
The other benefits I'd hoped to gain from non-shared structs have already been ruled out (i.e., value types and operator overloading)