Skip to content

feat: Implement symbols as WeakMap keys (closes #4439)#4732

Closed
gluzandii wants to merge 3 commits intoboa-dev:mainfrom
gluzandii:feature/symbolaskeys
Closed

feat: Implement symbols as WeakMap keys (closes #4439)#4732
gluzandii wants to merge 3 commits intoboa-dev:mainfrom
gluzandii:feature/symbolaskeys

Conversation

@gluzandii
Copy link
Copy Markdown
Contributor

@gluzandii gluzandii commented Feb 25, 2026

This Pull Request fixes/closes #4439

Implements the Symbols as WeakMap keys proposal
(Stage 4), allowing unique symbols (along with well-known symbols, ie Symbol.iterator etc) to be used as keys in WeakMap.

What I have changed.

  • can_be_held_weakly — A helper that is used to check if can be used as a key in weak map. Returns true for objects and symbols that are not registered (ie Symbol.for).

  • JsSymbol::as_weak — Added a method that returns a Weak<RawJsSymbol> reference,
    enabling the weak-keyed storage needed by NativeWeakMap.

  • NativeWeakMap — Extended with a two symbols map
    (HashMap<u64, (Weak<SymbolData>, JsValue)>), and (HashMap<u64, JsSymbol>) keyed by symbol hash. One map is for user defined Symbols (ie from Symbol()) and the other is for well-known symbols (ie Symbol.iterator etc). Both together are used for storing symbols as keys for WeakMap.

All five public WeakMap
operations (get, has, set, delete, getOrInsert, getOrInsertComputed) now handle
both object and symbol keys through dedicated helper methods. Dead entries are purged on
every insert and delete to prevent unbounded growth.

  • is_well_known — Present in JsSymbol. Used to check if the symbol is a well-known symbol like Symbol.iterator etc.

  • is_registered — Present in JsSymbol. Used to check if the symbol is a registered symbol (ie from Symbol.for).

Copilot AI review requested due to automatic review settings February 25, 2026 21:08
@gluzandii gluzandii requested a review from a team as a code owner February 25, 2026 21:08
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements TC39 “Symbols as WeakMap keys” (Stage 4) support by allowing unique (non-registered, non-well-known) symbols to be used as WeakMap keys, extending the engine’s WeakMap storage and adding symbol classification helpers.

Changes:

  • Added JsSymbol::as_weak() and JsSymbol::is_well_known() to support weak symbol-key storage and well-known symbol detection.
  • Extended WeakMap’s internal storage with a secondary symbol-key map and updated all WeakMap operations to accept unique symbol keys.
  • Added test coverage for symbol-key behavior and updated error-message expectations for new key eligibility rules.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
core/engine/src/symbol.rs Adds as_weak() to obtain Weak<RawJsSymbol> and is_well_known() to classify well-known/reserved symbols.
core/engine/src/builtins/weak_map/mod.rs Extends internal NativeWeakMap to support unique symbol keys and updates all WeakMap operations + CanBeHeldWeakly logic.
core/engine/src/builtins/weak_map/tests.rs Adds symbol-as-key tests and updates error message assertions for expanded accepted key types.
core/engine/src/builtins/symbol/mod.rs Adds is_registered_symbol / is_unique_symbol helpers used by WeakMap key eligibility.
core/engine/src/builtins/symbol/tests.rs Adds tests for symbol classification (registered / well-known / unique).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

///
/// Returns `true` if `v` may be used as a `WeakMap`/`WeakSet` key or `WeakRef` target.
/// Objects are always eligible. Symbols are eligible unless they are registered
/// (created via `Symbol.for()`).
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The can_be_held_weakly doc comment says symbols are eligible “unless they are registered (created via Symbol.for())”, but the implementation also excludes well-known symbols via symbol::is_unique_symbol(..)/sym.is_well_known(). Please update the comment to reflect the actual eligibility rules (registered and well-known symbols are rejected).

Suggested change
/// (created via `Symbol.for()`).
/// (created via `Symbol.for()`) or well-known.

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +60
// `symbols` whose symbol key is still alive. Dead entries are intentionally left unmarked
// so the GC can collect the values they hold. The `Weak<RawJsSymbol>` keys are Arc-based
// and contain no GC pointers.
unsafe impl Trace for NativeWeakMap {
custom_trace!(this, mark, {
mark(&this.objects);
for (weak, value) in this.symbols.values() {
if weak.upgrade().is_some() {
mark(value);
}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

Trace/custom_trace! here intentionally skips tracing value when weak.upgrade() is None, but the entry still stores a full JsValue in self.symbols. In boa_gc, untraced Gc edges are treated as roots (rootedness is based on non_root_count < ref_count), so these values will be kept alive indefinitely while the dead entry remains in the map. This breaks WeakMap weak semantics for symbol keys and can retain large object graphs.

To fix, ensure dead symbol entries are actually removed (dropping the JsValue) once weak.upgrade() fails—e.g., prune on get/has as well (use downcast_mut and make the symbol lookup helpers take &mut self so they can remove dead entries). After that, Trace can simply trace values for the remaining live entries.

Suggested change
// `symbols` whose symbol key is still alive. Dead entries are intentionally left unmarked
// so the GC can collect the values they hold. The `Weak<RawJsSymbol>` keys are Arc-based
// and contain no GC pointers.
unsafe impl Trace for NativeWeakMap {
custom_trace!(this, mark, {
mark(&this.objects);
for (weak, value) in this.symbols.values() {
if weak.upgrade().is_some() {
mark(value);
}
// `symbols` whose symbol key is still alive. Dead entries are pruned from the map before
// tracing so their `JsValue`s are dropped and no longer treated as GC roots. The
// `Weak<RawJsSymbol>` keys are Arc-based and contain no GC pointers.
unsafe impl Trace for NativeWeakMap {
custom_trace!(this, mark, {
// Trace the GC weak-map entries.
mark(&this.objects);
// Remove any symbol entries whose key has been dropped, so their `JsValue`s are
// no longer reachable from this map and can be collected.
this.symbols.retain(|_, (weak, _)| weak.upgrade().is_some());
// Trace the values for all remaining (live) symbol entries.
for (_, value) in this.symbols.values() {
mark(value);

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +100
/// Remove a symbol entry and return its value (regardless of liveness).
fn remove_symbol(&mut self, sym: &JsSymbol) -> bool {
let removed = self.symbols.remove(&sym.hash()).is_some();
self.symbols.retain(|_, (w, _)| w.upgrade().is_some());
removed
}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The doc comment says this function “Remove[s] a symbol entry and return[s] its value”, but the signature returns bool (and the implementation returns whether an entry was removed). Please update the comment to match the actual behavior (or change the return type if the value is needed).

Copilot uses AI. Check for mistakes.
Comment thread core/engine/src/builtins/symbol/mod.rs Outdated
Comment on lines +89 to +94
}

/// Returns `true` if the symbol was created via `Symbol.for()` and is therefore
/// in the global symbol registry.
pub(crate) fn is_registered_symbol(sym: &JsSymbol) -> bool {
GLOBAL_SYMBOL_REGISTRY.get_key(sym).is_some()
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

is_registered_symbol currently calls GLOBAL_SYMBOL_REGISTRY.get_key(sym) just to check is_some(). get_key constructs a JsString, so this turns a membership test into an allocation/copy on every call (and is_unique_symbol depends on it).

Consider implementing this as a pure lookup, e.g. GLOBAL_SYMBOL_REGISTRY.symbols.contains_key(sym) (or adding a dedicated contains_symbol method on GlobalSymbolRegistry) to avoid creating a JsString unnecessarily.

Suggested change
}
/// Returns `true` if the symbol was created via `Symbol.for()` and is therefore
/// in the global symbol registry.
pub(crate) fn is_registered_symbol(sym: &JsSymbol) -> bool {
GLOBAL_SYMBOL_REGISTRY.get_key(sym).is_some()
/// Returns `true` if the given symbol exists in the global symbol registry.
#[inline]
fn contains_symbol(&self, sym: &JsSymbol) -> bool {
self.symbols.contains_key(sym)
}
}
/// Returns `true` if the symbol was created via `Symbol.for()` and is therefore
/// in the global symbol registry.
pub(crate) fn is_registered_symbol(sym: &JsSymbol) -> bool {
GLOBAL_SYMBOL_REGISTRY.contains_symbol(sym)

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 25, 2026

Test262 conformance changes

Test result main count PR count difference
Total 52,862 52,862 0
Passed 49,627 49,627 0
Ignored 2,263 2,263 0
Failed 972 972 0
Panics 0 0 0
Conformance 93.88% 93.88% 0.00%

Tested PR commit: 57bd41d7bacf6635c686e1b8aff4f2e699f88146

@gluzandii gluzandii force-pushed the feature/symbolaskeys branch from ef28e85 to 8d3c19b Compare February 26, 2026 06:02
@jedel1043
Copy link
Copy Markdown
Member

I recommend you to use the spec as the true source of truth for the implementation.

According to the note in https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-canbeheldweakly, well-known symbols are suitable for being keys in a weakmap, even if their values will never get collected.

@gluzandii gluzandii force-pushed the feature/symbolaskeys branch from 7c64530 to 0252a72 Compare February 26, 2026 09:12
@gluzandii
Copy link
Copy Markdown
Contributor Author

I recommend you to use the spec as the true source of truth for the implementation.

According to the note in https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-canbeheldweakly, well-known symbols are suitable for being keys in a weakmap, even if their values will never get collected.

@jedel1043 Well-known symbols are now allowed as keys for WeakMap.

Comment thread core/engine/src/builtins/weak_map/mod.rs Outdated
- Add is_unique_symbol to check if a JsSymbol was created via Symbol()
  (i.e. not a registered or well-known symbol)
- Add can_be_held_weakly to check if a JsValue is a valid weak reference
  target (object or unique symbol)
- Add as_weak to JsSymbol for use in WeakMap symbol key handling
- Add NativeWeakMap with object and unique symbol key support
- Extend WeakMap to support unique symbol keys beyond NativeWeakMap
- Move is_well_known check into JsSymbol, using hash-based detection
- NativeWeakMap::remove_symbol now also purges already-collected symbols
- Add tests for WeakMap with symbol keys
Per CanBeHeldWeakly, only registered symbols (Symbol.for) are invalid as
WeakMap keys — well-known symbols (e.g. Symbol.iterator) are finite and
permitted.

- Allow well-known symbols as WeakMap keys; reject only registered symbols
- Improve error message: label key type as "registered symbol" not "symbol"
- Add JsSymbol::is_registered for global registry check
- Refactor NativeWeakMap to use GC ephemerons (boa_gc::WeakMap + marker Gc)
  for unique symbol keys, replacing manual marking
@gluzandii gluzandii force-pushed the feature/symbolaskeys branch from 0252a72 to 57bd41d Compare March 4, 2026 06:41
@gluzandii gluzandii requested a review from jedel1043 March 4, 2026 06:49
@jedel1043
Copy link
Copy Markdown
Member

I'm gonna close this because we should at least add support for symbols in our GC for this to be supported properly. We can revisit this implementation after we address that.

@jedel1043 jedel1043 closed this Mar 6, 2026
@gluzandii gluzandii deleted the feature/symbolaskeys branch March 6, 2026 15:59
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.

Implement Symbols as WeakMap keys proposal

3 participants