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

feat(ecmascript): Map.groupBy #559

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ use std::hash::Hasher;

use ahash::AHasher;

use crate::ecmascript::abstract_operations::operations_on_objects::try_get;
use crate::ecmascript::abstract_operations::operations_on_objects::{
create_array_from_scoped_list, group_by_property, try_get,
};
use crate::ecmascript::abstract_operations::testing_and_comparison::same_value;
use crate::ecmascript::builtins::map::data::MapHeapData;
use crate::engine::context::GcScope;
use crate::engine::TryResult;
use crate::heap::CreateHeapData;
use crate::{
ecmascript::{
abstract_operations::{
Expand Down Expand Up @@ -144,13 +149,84 @@ impl MapConstructor {
}
}

// ### [24.1.2.1 Map.groupBy ( items, callback )](https://tc39.es/ecma262/multipage/keyed-collections.html#sec-map.groupby)
fn group_by(
_agent: &mut Agent,
agent: &mut Agent,
_this_value: Value,
_arguments: ArgumentsList,
_gc: GcScope,
arguments: ArgumentsList,
mut gc: GcScope,
) -> JsResult<Value> {
todo!()
let items = arguments.get(0);
let callback_fn = arguments.get(1);
// 1. Let groups be ? GroupBy(items, callback, collection).
let groups = group_by_property(agent, items, callback_fn, gc.reborrow())?;
// 2. Let map be ! Construct(%Map%).
let mut map_data = MapHeapData::default();
map_data.reserve(groups.len());
let map = agent.heap.create(map_data);

let gc = gc.into_nogc();

// 3. For each Record { [[Key]], [[Elements]] } g of groups, do
let mut keys_and_elements = Vec::with_capacity(groups.len());
for g in groups {
let key = g.key.get(agent); // Get the key BEFORE mutable borrow
let key = key.convert_to_value(agent, gc);
// a. Let elements be CreateArrayFromList(g.[[Elements]]).
let elements = create_array_from_scoped_list(agent, g.elements, gc);
keys_and_elements.push((key, elements));
}

let Heap {
maps,
bigints,
numbers,
strings,
..
} = &mut agent.heap;
let primitive_heap = PrimitiveHeap::new(bigints, numbers, strings);

let map_entry = &mut maps[map];
let MapData {
keys,
values,
map_data,
..
} = &mut map_entry.borrow_mut(&primitive_heap);
let map_data = map_data.get_mut();
let hasher = |value: Value| {
let mut hasher = AHasher::default();
value.hash(&primitive_heap, &mut hasher);
hasher.finish()
};

for (key, elements) in keys_and_elements {
let key_hash = hasher(key);
let entry = map_data.entry(
key_hash,
|hash_equal_index| {
let found_key = keys[*hash_equal_index as usize].unwrap();
found_key == key || same_value(&primitive_heap, found_key, key)
},
|index_to_hash| hasher(keys[*index_to_hash as usize].unwrap()),
);
match entry {
hashbrown::hash_table::Entry::Occupied(occupied) => {
let index = *occupied.get();
values[index as usize] = Some(elements.into_value());
}
hashbrown::hash_table::Entry::Vacant(vacant) => {
// b. Let entry be the Record { [[Key]]: g.[[Key]], [[Value]]: elements }.
// c. Append entry to map.[[MapData]].
let index = u32::try_from(values.len()).unwrap();
vacant.insert(index);
keys.push(Some(key));
values.push(Some(elements.into_value()));
}
}
}
// 4. Return map
Ok(map.into_value())
}

fn get_species(
Expand Down
13 changes: 13 additions & 0 deletions nova_vm/src/ecmascript/builtins/map/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,19 @@ impl MapHeapData {
self.map_data.rehash_if_needed_mut(arena);
&mut self.map_data
}

pub fn reserve(&mut self, new_len: usize) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

issue: Take arena: &impl PrimitiveHeapIndexable here to use the value.hash() function; read the key value from map_data.keys[key_index as usize] like elsewhere.

Copy link
Contributor Author

@yossydev yossydev Feb 10, 2025

Choose a reason for hiding this comment

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

Thank you! I’d like to confirm something regarding this.

If we add arena as an argument, I think we need to define primitive_heap before calling map_data.reserve(), so would the code look like this?

let groups = group_by_property(agent, items, callback_fn, gc.reborrow())?;
// 2. Let map be ! Construct(%Map%).
let Heap {
    maps,
    bigints,
    numbers,
    strings,
    ..
} = &mut agent.heap;
let primitive_heap = PrimitiveHeap::new(bigints, numbers, strings);
let mut map_data = MapHeapData::default();
map_data.reserve(groups.len(), &primitive_heap);
let map = agent.heap.create(map_data);

However, this results in the error:
“cannot borrow agent.heap as mutable more than once at a time second mutable borrow occurs here.”
Does this mean my implementation approach is different from what you had in mind?

Could you explain the correct approach? 🙇‍♂️

self.map_data.keys.reserve(new_len);
self.map_data.values.reserve(new_len);
self.map_data
.map_data
.borrow_mut()
.reserve(new_len, |key_index: &u32| {
let mut hasher = AHasher::default();
key_index.hash(&mut hasher);
hasher.finish()
});
}
}

impl MapData {
Expand Down
8 changes: 0 additions & 8 deletions tests/expectations.json
Original file line number Diff line number Diff line change
Expand Up @@ -2025,16 +2025,8 @@
"built-ins/JSON/stringify/value-tojson-not-function.js": "FAIL",
"built-ins/JSON/stringify/value-tojson-object-circular.js": "CRASH",
"built-ins/JSON/stringify/value-tojson-result.js": "FAIL",
"built-ins/Map/groupBy/callback-arg.js": "CRASH",
"built-ins/Map/groupBy/callback-throws.js": "CRASH",
"built-ins/Map/groupBy/emptyList.js": "CRASH",
"built-ins/Map/groupBy/evenOdd.js": "CRASH",
"built-ins/Map/groupBy/groupLength.js": "CRASH",
"built-ins/Map/groupBy/invalid-callback.js": "CRASH",
"built-ins/Map/groupBy/invalid-iterable.js": "CRASH",
"built-ins/Map/groupBy/iterator-next-throws.js": "CRASH",
"built-ins/Map/groupBy/length.js": "FAIL",
"built-ins/Map/groupBy/map-instance.js": "CRASH",
"built-ins/Map/groupBy/negativeZero.js": "CRASH",
"built-ins/Map/groupBy/string.js": "CRASH",
"built-ins/Map/groupBy/toPropertyKey.js": "CRASH",
Expand Down
6 changes: 3 additions & 3 deletions tests/metrics.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"results": {
"crash": 13145,
"fail": 9052,
"pass": 24539,
"crash": 13127,
"fail": 9063,
"pass": 24546,
"skip": 65,
"timeout": 0,
"unresolved": 0
Expand Down