LibJS: Implement WeakMap.prototype.getOrInsert[Computed]

This is part of the Upsert proposal:
https://github.com/tc39/proposal-upsert
This commit is contained in:
Timothy Flynn 2025-10-02 16:29:29 -04:00 committed by Jelle Raaijmakers
parent b6924309a0
commit 979761ad82
4 changed files with 191 additions and 0 deletions

View file

@ -26,6 +26,8 @@ void WeakMapPrototype::initialize(Realm& realm)
define_native_function(realm, vm.names.delete_, delete_, 1, attr);
define_native_function(realm, vm.names.get, get, 1, attr);
define_native_function(realm, vm.names.getOrInsert, get_or_insert, 2, attr);
define_native_function(realm, vm.names.getOrInsertComputed, get_or_insert_computed, 2, attr);
define_native_function(realm, vm.names.has, has, 1, attr);
define_native_function(realm, vm.names.set, set, 2, attr);
@ -79,6 +81,79 @@ JS_DEFINE_NATIVE_FUNCTION(WeakMapPrototype::get)
return js_undefined();
}
// 3 WeakMap.prototype.getOrInsert ( key, value ), https://tc39.es/proposal-upsert/#sec-weakmap.prototype.getOrInsert
JS_DEFINE_NATIVE_FUNCTION(WeakMapPrototype::get_or_insert)
{
auto key = vm.argument(0);
auto value = vm.argument(1);
// 1. Let M be the this value.
// 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]).
auto weak_map = TRY(typed_this_object(vm));
// 3. If CanBeHeldWeakly(key) is false, throw a TypeError exception.
if (!can_be_held_weakly(key))
return vm.throw_completion<TypeError>(ErrorType::CannotBeHeldWeakly, key);
auto& values = weak_map->values();
// 4. For each Record { [[Key]], [[Value]] } p of M.[[WeakMapData]], do
if (auto result = values.find(&key.as_cell()); result != values.end()) {
// a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return p.[[Value]].
return result->value;
}
// 5. Let p be the Record { [[Key]]: key, [[Value]]: value }.
// 6. Append p to M.[[WeakMapData]].
values.set(&key.as_cell(), value);
// 7. Return value.
return value;
}
// 4 WeakMap.prototype.getOrInsertComputed ( key, callback ), https://tc39.es/proposal-upsert/#sec-weakmap.prototype.getOrInsertComputed
JS_DEFINE_NATIVE_FUNCTION(WeakMapPrototype::get_or_insert_computed)
{
auto key = vm.argument(0);
auto callback = vm.argument(1);
// 1. Let M be the this value.
// 2. Perform ? RequireInternalSlot(M, [[WeakMapData]]).
auto weak_map = TRY(typed_this_object(vm));
// 3. If CanBeHeldWeakly(key) is false, throw a TypeError exception.
if (!can_be_held_weakly(key))
return vm.throw_completion<TypeError>(ErrorType::CannotBeHeldWeakly, key);
// 4. If IsCallable(callback) is false, throw a TypeError exception.
if (!callback.is_function())
return vm.throw_completion<TypeError>(ErrorType::NotAFunction, callback);
auto& values = weak_map->values();
// 5. For each Record { [[Key]], [[Value]] } p of M.[[WeakMapData]], do
if (auto result = values.find(&key.as_cell()); result != values.end()) {
// a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, return p.[[Value]].
return result->value;
}
// 6. Let value be ? Call(callback, undefined, « key »).
auto value = TRY(call(vm, callback.as_function(), js_undefined(), key));
// 7. NOTE: The WeakMap may have been modified during execution of callback.
// 8. For each Record { [[Key]], [[Value]] } p of M.[[WeakMapData]], do
// a. If p.[[Key]] is not empty and SameValue(p.[[Key]], key) is true, then
// i. Set p.[[Value]] to value.
// ii. Return value.
// 9. Let p be the Record { [[Key]]: key, [[Value]]: value }.
// 10. Append p to M.[[WeakMapData]].
values.set(&key.as_cell(), value);
// 11. Return value.
return value;
}
// 24.3.3.4 WeakMap.prototype.has ( key ), https://tc39.es/ecma262/#sec-weakmap.prototype.has
JS_DEFINE_NATIVE_FUNCTION(WeakMapPrototype::has)
{

View file

@ -24,6 +24,8 @@ private:
JS_DECLARE_NATIVE_FUNCTION(delete_);
JS_DECLARE_NATIVE_FUNCTION(get);
JS_DECLARE_NATIVE_FUNCTION(get_or_insert);
JS_DECLARE_NATIVE_FUNCTION(get_or_insert_computed);
JS_DECLARE_NATIVE_FUNCTION(has);
JS_DECLARE_NATIVE_FUNCTION(set);
};

View file

@ -0,0 +1,45 @@
const a = new String("a");
const b = new String("b");
describe("errors", () => {
test("called on non-WeakMap object", () => {
expect(() => {
WeakMap.prototype.getOrInsert(a, 1);
}).toThrowWithMessage(TypeError, "Not an object of type WeakMap");
});
test("key cannot be held weakly", () => {
const map = new WeakMap();
[-100, Infinity, NaN, "hello", 152n].forEach(key => {
const suffix = typeof key === "bigint" ? "n" : "";
expect(() => {
map.getOrInsert(key, 1);
}).toThrowWithMessage(TypeError, `${key}${suffix} cannot be held weakly`);
});
});
});
describe("correct behavior", () => {
test("length is 2", () => {
expect(WeakMap.prototype.getOrInsert).toHaveLength(2);
});
test("inserts new value", () => {
const map = new WeakMap();
let result = map.getOrInsert(a, 2);
expect(result).toBe(2);
result = map.getOrInsert(b, 4);
expect(result).toBe(4);
});
test("does not overwrite existing value", () => {
const map = new WeakMap([[a, 2]]);
let result = map.getOrInsert(a, 3);
expect(result).toBe(2);
});
});

View file

@ -0,0 +1,69 @@
const a = new String("a");
const b = new String("b");
describe("errors", () => {
test("called on non-WeakMap object", () => {
expect(() => {
WeakMap.prototype.getOrInsertComputed(a, () => 0);
}).toThrowWithMessage(TypeError, "Not an object of type WeakMap");
});
test("key cannot be held weakly", () => {
const map = new WeakMap();
[-100, Infinity, NaN, "hello", 152n].forEach(key => {
const suffix = typeof key === "bigint" ? "n" : "";
expect(() => {
map.getOrInsertComputed(key, 1);
}).toThrowWithMessage(TypeError, `${key}${suffix} cannot be held weakly`);
});
});
test("called with non-function", () => {
expect(() => {
new WeakMap().getOrInsertComputed(a, 1);
}).toThrowWithMessage(TypeError, "1 is not a function");
});
test("callback function throws", () => {
expect(() => {
new WeakMap().getOrInsertComputed(a, () => {
throw Error(":^)");
});
}).toThrowWithMessage(Error, ":^)");
});
});
describe("correct behavior", () => {
test("length is 2", () => {
expect(WeakMap.prototype.getOrInsertComputed).toHaveLength(2);
});
test("inserts new value", () => {
const map = new WeakMap();
let result = map.getOrInsertComputed(a, () => 2);
expect(result).toBe(2);
result = map.getOrInsertComputed(b, () => 4);
expect(result).toBe(4);
});
test("does not overwrite existing value", () => {
const map = new WeakMap([[a, 2]]);
let result = map.getOrInsertComputed(a, () => 3);
expect(result).toBe(2);
});
test("does not invoke callback if already existing", () => {
const map = new WeakMap([[a, 2]]);
let invoked = false;
let result = map.getOrInsertComputed(a, () => {
invoked = true;
});
expect(invoked).toBeFalse();
});
});