Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sketch out full extent of Value variants #100

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

aapoalas
Copy link
Collaborator

I tried sketching out all Value "enum-space" variants that I could squeeze out of the ECMAScript spec. Some of these I'm not even sure if they're truly possible, mainly the generator functions vs iterators I'm not sure if they're the same thing or not. (AsyncFromSyncIterator is explicitly mentioned to not be observable by user code.)

Anyway, the end result was that with this absolutely bloated selection of variants we have a total of 56 different Value variants, an equal amount of thrown variants of course, 0 for None and 0x80 for ReferenceRecord. With a u8 repr we can afford 127 Value variants in total if we have thrown variants as well, so this would be 44.5% of the way to us being full. Put another way, we have 71 variants open. This number can be increased by eg. combining primitive value objects into a single variant, combining TypedArrays into a single variant, combining functions into less variants (it's very much debatable if all the different versions are all that useful). That would give us about 20 more variants.

So anyway, does this spark any ideas? Anything I've missed? Something that obviously doesn't make sense?

@aapoalas aapoalas requested a review from sno2 February 11, 2024 00:20
@sno2
Copy link
Contributor

sno2 commented Feb 11, 2024

I like the look of this, along with how many variants we have open still. Also, I believe we should be able to make a tt muncher macro that will generate our value variants with a syntax such as the following:

scuffed_enum! {
  #[derive(Debug, Clone, Copy, PartialEq)]
  #[repr(u8)])
  enum Value {
    #[derive(Debug, Clone, Copy, PartialEq)]
    #[repr(u8)]
    enum Thrown? {
      Undefined = 1,
      Null,
      // ...
      #[derive(Debug, Clone, Copy, PartialEq)]
      #[repr(u8)]
      enum String {
        Index(StringIndex),
        Small(SmallString),
      }
      // ...
    }
    EmbedderObject,
    Reference,
  }
}

Which would recursively build all of our enum types and From impls for all higher enums and TryFrom for lower enums. What do you think of this?

Edit: It could also be really cool if we could have ResolvedFoo variants of each value to represent resolved promises in the future where Nova supports promises.

@aapoalas
Copy link
Collaborator Author

A macro would probably be useful, yeah. I think we have some cases where it wouldn't work though; mainly I'm thinking of PropertyKey which is not necessarily going to be a strict subset of the "root Value".

I expect that with private properties we'll want to introduce a PrivateKey variant to PropertyKey. This will be ... interesting since PropertyKey shouldn't appear as a valid variant of essentially any other enum, in a way not even the root enum if we use it anywhere. Though, I do not think we will so maybe it's not an issue that it's present in the root. But: String and friends should appear in PropertyKey yet PropertyKey is not a subset of Value, rather it's a "separate side path" in a sense. This couldn't be expressed in the macro unless some extra logic is added.

Additionally: Due to the Integer optimization in PropertyKey it would actually be incorrect to implement a From<PropertyKey> for Value as it's would not be correct to use that impl for eg. getting Object.keys() values.

This isn't too much of an issue though: It just means that PropertyKey is separately defined and sits outside of the macro. As it's a special case that's just fine.

Regarding Promise variants, I don't think that's possible or reasonable. For one it would take up most of the variants we have left. There's still a wealth of ES proposals out there in the world and they bring in extra spec items that require variants; we can't eat them all up :) (Though if we really run out we can bump up to 16 bit variants and then we're golden forever.)

But most importantly: I'm not sure what the benefit would be. What's the difference between a Promise<SmallString> and a Promise<String> for instance? Either the heap Promise data is the same for both in which case the heap data contains the variant already and duplicating it at the Value level only gives the benefit of knowing what value your Promise has resolved into without looking into the heap. Or the heap Promise data is different for each Resolved variant in which case resolving a function means having to do a full heap trace and replacing all references to the resolved Promise with a Resolved promise.

eg. Every place that keeps Value::Promise(1235) needs to be changed to Value::ResolvedString(11), globally. Furthermore, the 11 here still isn't the index to get the heap string value, nuh uh: This still needs to be the index for the ResolvedStringHeapData vector in the heap, as we need some place where we can keep the potential ObjectIndex of the Promise in case someone assigns or had assigned data to it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants