Skip to content

feat(ecmascript): WeakMap Constructor #561

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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

yossydev
Copy link
Contributor

@yossydev yossydev commented Feb 6, 2025

The WeakMap constructor itself is almost the same as the Map constructor, so most of the implementation is unchanged from Map.
Among them, add_entries_from_iterable_map_constructor is designed to accept a Map type, so I have modified it to accept a WeakMap instead.
I’m wondering whether this is a good approach implementation-wise or if it should be further abstracted.

ref: #148
doc: https://tc39.es/ecma262/multipage/keyed-collections.html#sec-weakmap-iterable

Copy link
Member

@aapoalas aapoalas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this! You've picked a prickly customer again; Weak references AKA ephemerons are something that I've not yet implemented in the GC, and I somewhat dread implementing them due to the horrible things they require :D

Implementing the constructor and the prototype methods is definitely possible even without the GC support, though, so no worries there! We'll just need to file off a few rough edges.

/// ### [24.5.1 CanonicalizeKeyedCollectionKey ( key )](https://tc39.es/ecma262/#sec-canonicalizekeyedcollectionkey)
/// The abstract operation CanonicalizeKeyedCollectionKey takes argument key
/// (an ECMAScript language value) and returns an ECMAScript language value.
pub(crate) fn canonicalize_keyed_collection_key(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Don't copy this code, refer to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you! I fixed! 2190159

unreachable!()
};
let slice = entry.as_slice(&array_heap);
let key = canonicalize_keyed_collection_key(numbers, slice[0].unwrap());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: The key in a WeakMap cannot be any Value but needs to be an object or a symbol.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I’ve fixed this too! 26d5892
However, I think the code will probably change again once we create the enum 👀 ref: #561 (comment)

/// > element array-like object whose first element is a value that will be used
/// > as a WeakMap key and whose second element is the value to associate with that
/// > key.
pub(crate) fn add_entries_from_iterable<'a>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: We should reuse the add_entries_from_iterable over in map_constructor.rs here and just change its target type to be Object.

(There is also a possibility of making it take an adder: impl FnMut but that's kind of a side point.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you! I fixed this too! 993cba5

@@ -89,6 +89,17 @@ impl<'a> From<WeakMap<'a>> for Object<'a> {
}
}

impl<'a> TryFrom<Object<'a>> for WeakMap<'a> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Nice; it's a bit awkward to be missing these all over the place :D

// TODO: When an non-terminal (start or end) iterator exists for the Map,
// the items in the map cannot be compacted.
// pub(crate) observed: bool;
pub(crate) keys: Vec<Option<Value>>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: The keys are always Object or Symbol. That should probably be a new enum equivalent to Object but with the Symbol added in.

Naming is always the hardest part... SymbolOrObject sounds dumb so probably not that. Identifiable? That's very vague... I don't know of a good name for these :/ "Referenceable" is somewhat correct. Spec calls these "value with identity", and tests for them via "CanBeHeldWeakly". "WeaklyReferenceableValue", "ValueWithIdentity", "IdentityValue", ... All of these sound a bit weak :) (pun very much intended!)

Good ideas are very much welcome on the naming front :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like WeakMapKey or WeakKey be too vague and lacking information? 👀

Copy link
Contributor Author

@yossydev yossydev Feb 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expected enum to be created looks something like this, right? 👀

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum WeakMapKey<'a> {
    Object(Option<Object<'a>>),
    Symbol(Option<Symbol<'a>>),
}

#[derive(Debug, Default)]
pub(crate) struct WeakMapData<'a> {
    // TODO: Use a ParallelVec to remove one unnecessary allocation.
    // pub(crate) key_values: ParallelVec<Option<Value>, Option<Value>>
    pub(crate) keys: Vec<Option<WeakMapKey<'a>>>,
    pub(crate) values: Vec<Option<Value>>,
    /// Low-level hash table pointing to keys-values indexes.
    pub(crate) weak_map_data: RefCell<HashTable<u32>>,
    pub(crate) needs_primitive_rehashing: AtomicBool,
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm.... WeakKey sounds pretty good indeed. It might even suggest a unification of all objects that hold weak references (WeakMap, WeakSet, WeakRef, FinalizationRegistry) to not hold a reference to the key object/symbol directly but instead point to a 'weak key" vector which would then hold the object/symbol reference. This would make weak objects more expensive to access but would make GC'ing them maybe more straightforward. Choices, choices...

But that's a decision for later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue; The WeakMapKey enum should not be the kind of "recursive enum" that you've suggested there, but should instead be basically a copy-paste of the Object enum with just one extra variant, Symbol added in. The Symbol variant's value should also be SYMBOL_DISCRIMINANT.

pub(crate) keys: Vec<Option<Value>>,
pub(crate) values: Vec<Option<Value>>,
/// Low-level hash table pointing to keys-values indexes.
pub(crate) weak_map_data: RefCell<HashTable<u32>>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: We actually do not need the keys and values here at all: A WeakMap cannot be iterated in its insertion order, so we do not need to keep keys and values separately from the hashmap.

I believe we should use a plain AHashMap<SymbolOrObject, Value> here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

With key and value removed, the WeakMapData struct would look like this, right? 👀

pub(crate) struct WeakMapData {
    /// Low-level hash table pointing to keys-values indexes.
    pub(crate) weak_map_data: AHashMap<SymbolOrObject, Value>,
    pub(crate) needs_primitive_rehashing: AtomicBool,
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that looks perfect.

/// The abstract operation MapDataSize takes argument setData (a List of either
/// ECMAScript language values or EMPTY) and returns a non-negative integer.
#[inline(always)]
pub fn size(&self) -> u32 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: The 24.2.1.5 abstract operation isn't about getting the data size, and we're mildly unlikely to need one as it's not possible to read the size of a WeakMap from JS.

The keys and values are likewise probably not going to be needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you! I removed this! 5d19165

@yossydev
Copy link
Contributor Author

yossydev commented Feb 7, 2025

Thank you for this! You've picked a prickly customer again; Weak references AKA ephemerons are something that I've not yet implemented in the GC, and I somewhat dread implementing them due to the horrible things they require :D

Implementing the constructor and the prototype methods is definitely possible even without the GC support, though, so no worries there! We'll just need to file off a few rough edges.

I see, so that’s how it is.
Looking at just the constructor, I thought it was almost the same as Map and wouldn’t be that difficult…

I’ll fix the parts you pointed out in the review later! Thank you!!

@yossydev yossydev force-pushed the feat/weakmap-constructor branch from 5b49404 to 52b67a0 Compare February 11, 2025 07:41
@yossydev yossydev requested a review from aapoalas February 11, 2025 07:46
.unbind()
.bind(gc.nogc());
// 3. Set map.[[WeakMapData]] to a new empty List.
let iterable = arguments.get(0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Take the iterable parameter out from arguments at the very beginning of the function and bind it to gc.nogc(). Also do the same for new_target, although in this case (and in most cases) it is already correct as-is.

The iterable parameter usage, however, currently holds a possibility for use-after-free bugs when interleaved GC is turned on.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I probably managed to fix it while resolving the conflicts!

// TODO: When an non-terminal (start or end) iterator exists for the Map,
// the items in the map cannot be compacted.
// pub(crate) observed: bool;
pub(crate) keys: Vec<Option<Value>>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue; The WeakMapKey enum should not be the kind of "recursive enum" that you've suggested there, but should instead be basically a copy-paste of the Object enum with just one extra variant, Symbol added in. The Symbol variant's value should also be SYMBOL_DISCRIMINANT.

pub(crate) keys: Vec<Option<Value>>,
pub(crate) values: Vec<Option<Value>>,
/// Low-level hash table pointing to keys-values indexes.
pub(crate) weak_map_data: RefCell<HashTable<u32>>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that looks perfect.

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