LibIDL+Meta: Add support for WebIDL maplike declarations on interfaces

Currently we only support string keys and numeric sequence values.

This is heavily based on the implementation of `setlike`.
This commit is contained in:
Callum Law 2026-01-13 11:13:19 +13:00 committed by Sam Atkins
parent 6294fb1f7a
commit 3f8da74e63
4 changed files with 292 additions and 1 deletions

View file

@ -349,6 +349,8 @@ void Parser::parse_attribute(HashMap<ByteString, ByteString>& extended_attribute
consume_whitespace();
else if (lexer.consume_specific("setlike"sv) && !inherit)
parse_setlike(interface, readonly);
else if (lexer.consume_specific("maplike"sv) && !inherit)
parse_maplike(interface, readonly);
else
report_parsing_error("expected 'attribute'"sv, filename, input, lexer.tell());
@ -583,6 +585,9 @@ void Parser::parse_setlike(Interface& interface, bool is_readonly)
if (interface.value_iterator_type.has_value() || interface.pair_iterator_types.has_value())
report_parsing_error("Interfaces with a setlike declaration must not must not be iterable."sv, filename, input, lexer.tell());
if (interface.map_key_type.has_value())
report_parsing_error("Interfaces with a setlike declaration must not have a maplike declaration."sv, filename, input, lexer.tell());
assert_string("setlike"sv);
assert_specific('<');
@ -593,6 +598,31 @@ void Parser::parse_setlike(Interface& interface, bool is_readonly)
assert_specific(';');
}
void Parser::parse_maplike(Interface& interface, bool is_readonly)
{
if (interface.supports_indexed_properties())
report_parsing_error("Interfaces with a maplike declaration must not support indexed properties."sv, filename, input, lexer.tell());
if (interface.value_iterator_type.has_value() || interface.pair_iterator_types.has_value())
report_parsing_error("Interfaces with a maplike declaration must not must not be iterable."sv, filename, input, lexer.tell());
if (interface.set_entry_type.has_value())
report_parsing_error("Interfaces with a maplike declaration must not have a setlike declaration."sv, filename, input, lexer.tell());
assert_string("maplike"sv);
assert_specific('<');
interface.map_key_type = parse_type();
consume_whitespace();
assert_specific(',');
consume_whitespace();
interface.map_value_type = parse_type();
interface.is_map_readonly = is_readonly;
assert_specific('>');
assert_specific(';');
}
void Parser::parse_getter(HashMap<ByteString, ByteString>& extended_attributes, Interface& interface)
{
assert_string("getter"sv);
@ -759,6 +789,12 @@ void Parser::parse_interface(Interface& interface)
continue;
}
if (lexer.next_is("maplike"sv)) {
bool is_readonly = false;
parse_maplike(interface, is_readonly);
continue;
}
if (lexer.next_is("inherit"sv) || lexer.next_is("readonly"sv) || lexer.next_is("attribute"sv)) {
parse_attribute(extended_attributes, interface);
continue;

View file

@ -63,6 +63,7 @@ private:
void parse_iterable(Interface&);
void parse_async_iterable(Interface&);
void parse_setlike(Interface&, bool is_readonly);
void parse_maplike(Interface&, bool is_readonly);
Function parse_function(HashMap<ByteString, ByteString>& extended_attributes, Interface&, IsStatic is_static = IsStatic::No, IsSpecialOperation is_special_operation = IsSpecialOperation::No);
Vector<Parameter> parse_parameters();
NonnullRefPtr<Type const> parse_type();

View file

@ -297,6 +297,10 @@ public:
Optional<NonnullRefPtr<Type const>> set_entry_type;
bool is_set_readonly { false };
Optional<NonnullRefPtr<Type const>> map_key_type;
Optional<NonnullRefPtr<Type const>> map_value_type;
bool is_map_readonly { false };
Optional<Function> named_property_getter;
Optional<Function> named_property_setter;

View file

@ -3215,6 +3215,29 @@ static void generate_prototype_or_global_mixin_declarations(IDL::Interface const
}
}
if (interface.map_key_type.has_value()) {
auto maplike_generator = generator.fork();
maplike_generator.append(R"~~~(
JS_DECLARE_NATIVE_FUNCTION(get_size);
JS_DECLARE_NATIVE_FUNCTION(entries);
JS_DECLARE_NATIVE_FUNCTION(keys);
JS_DECLARE_NATIVE_FUNCTION(values);
JS_DECLARE_NATIVE_FUNCTION(for_each);
JS_DECLARE_NATIVE_FUNCTION(get);
JS_DECLARE_NATIVE_FUNCTION(has);
)~~~");
if (!interface.overload_sets.contains("set"sv) && !interface.is_map_readonly)
maplike_generator.appendln(" JS_DECLARE_NATIVE_FUNCTION(set);");
if (!interface.overload_sets.contains("delete"sv) && !interface.is_map_readonly)
maplike_generator.appendln(" JS_DECLARE_NATIVE_FUNCTION(delete_);");
if (!interface.overload_sets.contains("clear"sv) && !interface.is_map_readonly)
maplike_generator.appendln(" JS_DECLARE_NATIVE_FUNCTION(clear);");
}
for (auto& attribute : interface.attributes) {
if (attribute.extended_attributes.contains("FIXME"))
continue;
@ -3951,6 +3974,30 @@ void @class_name@::initialize(JS::Realm& realm)
}
}
if (interface.map_key_type.has_value() && generate_unforgeables == GenerateUnforgeables::No) {
auto maplike_generator = generator.fork();
maplike_generator.append(R"~~~(
@define_native_accessor@(realm, vm.names.size, get_size, nullptr, JS::Attribute::Enumerable | JS::Attribute::Configurable);
@define_native_function@(realm, vm.names.entries, entries, 0, default_attributes);
@define_direct_property@(vm.well_known_symbol_iterator(), get_without_side_effects(vm.names.entries), JS::Attribute::Configurable | JS::Attribute::Writable);
@define_native_function@(realm, vm.names.keys, keys, 0, default_attributes);
@define_native_function@(realm, vm.names.values, values, 0, default_attributes);
@define_native_function@(realm, vm.names.forEach, for_each, 1, default_attributes);
@define_native_function@(realm, vm.names.get, get, 1, default_attributes);
@define_native_function@(realm, vm.names.has, has, 1, default_attributes);
)~~~");
if (!interface.overload_sets.contains("set"sv) && !interface.is_map_readonly)
maplike_generator.appendln(" @define_native_function@(realm, vm.names.set, set, 2, default_attributes);");
if (!interface.overload_sets.contains("delete"sv) && !interface.is_map_readonly)
maplike_generator.appendln(" @define_native_function@(realm, vm.names.delete_, delete_, 1, default_attributes);");
if (!interface.overload_sets.contains("clear"sv) && !interface.is_map_readonly)
maplike_generator.appendln(" @define_native_function@(realm, vm.names.clear, clear, 0, default_attributes);");
}
if (interface.has_unscopable_member) {
generator.append(R"~~~(
@define_direct_property@(vm.well_known_symbol_unscopables(), unscopable_object, JS::Attribute::Configurable);
@ -4003,7 +4050,7 @@ static void generate_prototype_or_global_mixin_definitions(IDL::Interface const&
generator.set("iterator_name", ByteString::formatted("{}Iterator", interface.name));
}
if (!interface.attributes.is_empty() || !interface.functions.is_empty() || interface.has_stringifier || interface.set_entry_type.has_value()) {
if (!interface.attributes.is_empty() || !interface.functions.is_empty() || interface.has_stringifier || interface.set_entry_type.has_value() || interface.map_key_type.has_value()) {
generator.append(R"~~~(
[[maybe_unused]] static JS::ThrowCompletionOr<@fully_qualified_name@*> impl_from(JS::VM& vm)
{
@ -4988,6 +5035,209 @@ JS_DEFINE_NATIVE_FUNCTION(@class_name@::clear)
}
}
if (interface.map_key_type.has_value()) {
auto maplike_generator = generator.fork();
if (interface.map_key_type.value()->is_string()) {
maplike_generator.set("key_arg_converted_to_idl_type", "JS::PrimitiveString::create(vm, TRY(key_arg.to_string(vm)));");
} else {
TODO();
}
if (interface.map_value_type.value()->is_sequence() && interface.map_value_type.value()->as_parameterized().parameters().at(0)->is_numeric()) {
// FIXME: We should convert rather than just fail if we have the wrong type.
maplike_generator.set("value_arg_converted_to_idl_type", R"~~~([&](){
if (!value_arg.is_object() || !is<JS::Array>(value_arg.as_object())) {
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "Array");
}
for (auto const& item : as<JS::Array>(value_arg.as_object())) {
if (!item.is_numeric()) {
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "Number");
}
}
return value_arg;
}();
)~~~");
} else {
TODO();
}
maplike_generator.append(R"~~~(
// https://webidl.spec.whatwg.org/#js-map-size
JS_DEFINE_NATIVE_FUNCTION(@class_name@::get_size)
{
WebIDL::log_trace(vm, "@class_name@::size");
auto* impl = TRY(impl_from(vm));
GC::Ref<JS::Map> map = impl->map_entries();
return map->map_size();
}
// https://webidl.spec.whatwg.org/#js-map-entries
JS_DEFINE_NATIVE_FUNCTION(@class_name@::entries)
{
WebIDL::log_trace(vm, "@class_name@::entries");
auto& realm = *vm.current_realm();
auto* impl = TRY(impl_from(vm));
GC::Ref<JS::Map> map = impl->map_entries();
return TRY(throw_dom_exception_if_needed(vm, [&] { return JS::MapIterator::create(realm, *map, Object::PropertyKind::KeyAndValue); }));
}
// https://webidl.spec.whatwg.org/#js-map-keys
JS_DEFINE_NATIVE_FUNCTION(@class_name@::keys)
{
WebIDL::log_trace(vm, "@class_name@::keys");
auto& realm = *vm.current_realm();
auto* impl = TRY(impl_from(vm));
GC::Ref<JS::Map> map = impl->map_entries();
return TRY(throw_dom_exception_if_needed(vm, [&] { return JS::MapIterator::create(realm, *map, Object::PropertyKind::Key); }));
}
// https://webidl.spec.whatwg.org/#js-map-values
JS_DEFINE_NATIVE_FUNCTION(@class_name@::values)
{
WebIDL::log_trace(vm, "@class_name@::values");
auto& realm = *vm.current_realm();
auto* impl = TRY(impl_from(vm));
GC::Ref<JS::Map> map = impl->map_entries();
return TRY(throw_dom_exception_if_needed(vm, [&] { return JS::MapIterator::create(realm, *map, Object::PropertyKind::Value); }));
}
// https://webidl.spec.whatwg.org/#js-map-forEach
JS_DEFINE_NATIVE_FUNCTION(@class_name@::for_each)
{
WebIDL::log_trace(vm, "@class_name@::for_each");
auto* impl = TRY(impl_from(vm));
GC::Ref<JS::Map> map = impl->map_entries();
auto callback = vm.argument(0);
if (!callback.is_function())
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAFunction, callback);
for (auto& entry : *map)
TRY(JS::call(vm, callback.as_function(), vm.argument(1), entry.key, entry.value, impl));
return JS::js_undefined();
}
// https://webidl.spec.whatwg.org/#js-map-get
JS_DEFINE_NATIVE_FUNCTION(@class_name@::get)
{
WebIDL::log_trace(vm, "@class_name@::get");
auto* impl = TRY(impl_from(vm));
GC::Ref<JS::Map> map = impl->map_entries();
auto key_arg = vm.argument(0);
auto key = @key_arg_converted_to_idl_type@
// FIXME: If key is -0, set key to +0.
// What? Which interfaces have a number as their map key type?
auto result = map->map_get(key);
if (!result.has_value())
return JS::js_undefined();
return result.release_value();
}
// https://webidl.spec.whatwg.org/#js-map-has
JS_DEFINE_NATIVE_FUNCTION(@class_name@::has)
{
WebIDL::log_trace(vm, "@class_name@::has");
auto* impl = TRY(impl_from(vm));
GC::Ref<JS::Map> map = impl->map_entries();
auto key_arg = vm.argument(0);
auto key = @key_arg_converted_to_idl_type@
// FIXME: If key is -0, set key to +0.
// What? Which interfaces have a number as their map key type?
return map->map_has(key);
}
)~~~");
if (!interface.overload_sets.contains("set"sv) && !interface.is_map_readonly) {
maplike_generator.append(R"~~~(
// https://webidl.spec.whatwg.org/#js-map-set
JS_DEFINE_NATIVE_FUNCTION(@class_name@::set)
{
WebIDL::log_trace(vm, "@class_name@::set");
auto* impl = TRY(impl_from(vm));
GC::Ref<JS::Map> map = impl->map_entries();
auto key_arg = vm.argument(0);
auto key = @key_arg_converted_to_idl_type@
// FIXME: If value is -0, set value to +0.
// What? Which interfaces have a number as their set type?
auto value_arg = vm.argument(1);
auto value = @value_arg_converted_to_idl_type@
map->map_set(key, value);
impl->on_map_modified_from_js({});
return impl;
}
)~~~");
}
if (!interface.overload_sets.contains("delete"sv) && !interface.is_map_readonly) {
maplike_generator.append(R"~~~(
// https://webidl.spec.whatwg.org/#js-map-delete
JS_DEFINE_NATIVE_FUNCTION(@class_name@::delete_)
{
WebIDL::log_trace(vm, "@class_name@::delete_");
auto* impl = TRY(impl_from(vm));
GC::Ref<JS::Map> map = impl->map_entries();
auto key_arg = vm.argument(0);
auto key = @key_arg_converted_to_idl_type@
// FIXME: If key is -0, set key to +0.
// What? Which interfaces have a number as their map key type?
auto result = map->map_remove(key);
impl->on_map_modified_from_js({});
return result;
}
)~~~");
}
if (!interface.overload_sets.contains("clear"sv) && !interface.is_map_readonly) {
maplike_generator.append(R"~~~(
// https://webidl.spec.whatwg.org/#js-map-clear
JS_DEFINE_NATIVE_FUNCTION(@class_name@::clear)
{
WebIDL::log_trace(vm, "@class_name@::clear");
auto* impl = TRY(impl_from(vm));
GC::Ref<JS::Map> map = impl->map_entries();
map->map_clear();
impl->on_map_modified_from_js({});
return JS::js_undefined();
}
)~~~");
}
}
generate_dictionaries(generator, interface);
}