From 68813fbe701c409d00a4f4bc0c6c941ece94d384 Mon Sep 17 00:00:00 2001 From: Luke Wilde Date: Tue, 25 Jan 2022 19:24:00 +0000 Subject: [PATCH] LibWeb: Add initial support for passing union types into IDL functions This currently does not accept ArrayBuffer, DataView, TypedArrays, callback functions, nullable types, dictionaries, records and frozen arrays. --- .../LibWeb/WrapperGenerator.cpp | 517 +++++++++++++++++- 1 file changed, 515 insertions(+), 2 deletions(-) diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/WrapperGenerator.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/WrapperGenerator.cpp index b7a4357c80..c9a285d784 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/WrapperGenerator.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/WrapperGenerator.cpp @@ -106,6 +106,135 @@ struct Type : public RefCounted { String name; bool nullable { false }; bool is_string() const { return name.is_one_of("ByteString", "CSSOMString", "DOMString", "USVString"); } + + // https://webidl.spec.whatwg.org/#dfn-integer-type + bool is_integer() const { return name.is_one_of("byte", "octet", "short", "unsigned short", "long", "unsigned long", "long long", "unsigned long long"); } + + // https://webidl.spec.whatwg.org/#dfn-numeric-type + bool is_numeric() const { return is_integer() || name.is_one_of("float", "unrestricted float", "double", "unrestricted double"); } +}; + +static CppType idl_type_name_to_cpp_type(Type const& type); + +struct UnionType : public Type { + UnionType() = default; + + UnionType(String name, bool nullable, NonnullRefPtrVector member_types) + : Type(move(name), nullable) + , member_types(move(member_types)) + { + } + + virtual ~UnionType() override = default; + + NonnullRefPtrVector member_types; + + // https://webidl.spec.whatwg.org/#dfn-flattened-union-member-types + NonnullRefPtrVector flattened_member_types() const + { + // 1. Let T be the union type. + + // 2. Initialize S to ∅. + NonnullRefPtrVector types; + + // 3. For each member type U of T: + for (auto& type : member_types) { + // FIXME: 1. If U is an annotated type, then set U to be the inner type of U. + + // 2. If U is a nullable type, then set U to be the inner type of U. (NOTE: Not necessary as nullable is stored with Type and not as a separate struct) + + // 3. If U is a union type, then add to S the flattened member types of U. + if (is(type)) { + auto& union_member_type = verify_cast(type); + types.extend(union_member_type.flattened_member_types()); + } else { + // 4. Otherwise, U is not a union type. Add U to S. + types.append(type); + } + } + + // 4. Return S. + return types; + } + + // https://webidl.spec.whatwg.org/#dfn-number-of-nullable-member-types + size_t number_of_nullable_member_types() const + { + // 1. Let T be the union type. + + // 2. Initialize n to 0. + size_t num_nullable_member_types = 0; + + // 3. For each member type U of T: + for (auto& type : member_types) { + // 1. If U is a nullable type, then: + if (type.nullable) { + // 1. Set n to n + 1. + ++num_nullable_member_types; + + // 2. Set U to be the inner type of U. (NOTE: Not necessary as nullable is stored with Type and not as a separate struct) + } + + // 2. If U is a union type, then: + if (is(type)) { + auto& union_member_type = verify_cast(type); + + // 1. Let m be the number of nullable member types of U. + // 2. Set n to n + m. + num_nullable_member_types += union_member_type.number_of_nullable_member_types(); + } + } + + // 4. Return n. + return num_nullable_member_types; + } + + // https://webidl.spec.whatwg.org/#dfn-includes-a-nullable-type + bool includes_nullable_type() const + { + // -> the type is a union type and its number of nullable member types is 1. + return number_of_nullable_member_types() == 1; + } + + // -> https://webidl.spec.whatwg.org/#dfn-includes-undefined + bool includes_undefined() const + { + // -> the type is a union type and one of its member types includes undefined. + for (auto& type : member_types) { + if (is(type)) { + auto& union_type = verify_cast(type); + if (union_type.includes_undefined()) + return true; + } + + if (type.name == "undefined"sv) + return true; + } + return false; + } + + String to_variant() const + { + StringBuilder builder; + builder.append("Variant<"); + + auto flattened_types = flattened_member_types(); + for (size_t type_index = 0; type_index < flattened_types.size(); ++type_index) { + auto& type = flattened_types.at(type_index); + + if (type_index > 0) + builder.append(", "); + + auto cpp_type = idl_type_name_to_cpp_type(type); + builder.append(cpp_type.name); + } + + if (includes_undefined()) + builder.append(", Empty"); + + builder.append('>'); + return builder.to_string(); + } }; struct Parameter { @@ -300,6 +429,28 @@ static NonnullOwnPtr parse_interface(StringView filename, StringView interface->extended_attributes = parse_extended_attributes(); AK::Function()> parse_type = [&]() -> NonnullRefPtr { + if (lexer.consume_specific('(')) { + NonnullRefPtrVector union_member_types; + union_member_types.append(parse_type()); + consume_whitespace(); + assert_string("or"); + consume_whitespace(); + union_member_types.append(parse_type()); + consume_whitespace(); + + while (lexer.consume_specific("or")) { + consume_whitespace(); + union_member_types.append(parse_type()); + consume_whitespace(); + } + + assert_specific(')'); + + bool nullable = lexer.consume_specific('?'); + + return adopt_ref(*new UnionType("", nullable, move(union_member_types))); + } + auto consume_name = [&] { return lexer.consume_until([](auto ch) { return !isalnum(ch) && ch != '_'; }); }; @@ -852,6 +1003,11 @@ static CppType idl_type_name_to_cpp_type(Type const& type) return { .name = String::formatted("{}<{}>", storage_type_name, sequence_cpp_type.name), .sequence_storage_type = SequenceStorageType::Vector }; } + if (is(type)) { + auto& union_type = verify_cast(type); + return { .name = union_type.to_variant(), .sequence_storage_type = SequenceStorageType::Vector }; + } + dbgln("Unimplemented type for idl_type_name_to_cpp_type: {}{}", type.name, type.nullable ? "?" : ""); TODO(); } @@ -1052,10 +1208,11 @@ static void generate_to_cpp(SourceGenerator& generator, ParameterType& parameter if (!parameter.type->nullable) { scoped_generator.append(R"~~~( String @cpp_name@; - if (@js_name@@js_suffix@.is_null() && @legacy_null_to_empty_string@) + if (@js_name@@js_suffix@.is_null() && @legacy_null_to_empty_string@) { @cpp_name@ = String::empty(); - else + } else { @cpp_name@ = TRY(@js_name@@js_suffix@.to_string(global_object)); + } )~~~"); } else { scoped_generator.append(R"~~~( @@ -1319,6 +1476,360 @@ static void generate_to_cpp(SourceGenerator& generator, ParameterType& parameter )~~~"); parameterized_type.generate_sequence_from_iterable(sequence_generator, acceptable_cpp_name, String::formatted("{}{}", js_name, js_suffix), String::formatted("iterator_method{}", recursion_depth), dictionaries, recursion_depth + 1); + } else if (is(*parameter.type)) { + // https://webidl.spec.whatwg.org/#es-union + + auto union_generator = scoped_generator.fork(); + + auto& union_type = verify_cast(*parameter.type); + union_generator.set("union_type", union_type.to_variant()); + union_generator.set("recursion_depth", String::number(recursion_depth)); + + // A lambda is used because Variants without "Empty" can't easily be default initialized. + // Plus, this would require the user of union types to always accept a Variant with an Empty type. + + // Additionally, it handles the case of unconditionally throwing a TypeError at the end if none of the types match. + // This is because we cannot unconditionally throw in generate_to_cpp as generate_to_cpp is supposed to assign to a variable and then continue. + // Note that all the other types only throw on a condition. + + // The lambda must take the JS::Value to convert as a parameter instead of capturing it in order to support union types being variadic. + union_generator.append(R"~~~( + auto @js_name@@js_suffix@_to_variant = [&global_object, &vm](JS::Value @js_name@@js_suffix@) -> JS::ThrowCompletionOr<@union_type@> { + // These might be unused. + (void)global_object; + (void)vm; +)~~~"); + + // 1. If the union type includes undefined and V is undefined, then return the unique undefined value. + if (union_type.includes_undefined()) { + scoped_generator.append(R"~~~( + if (@js_name@@js_suffix@.is_undefined()) + return Empty {}; +)~~~"); + } + + // 3. Let types be the flattened member types of the union type. + auto types = union_type.flattened_member_types(); + + bool contains_dictionary_type = false; + for (auto& dictionary : dictionaries) { + for (auto& type : types) { + if (type.name == dictionary.key) { + contains_dictionary_type = true; + break; + } + } + + if (contains_dictionary_type) + break; + } + + // FIXME: 2. If the union type includes a nullable type and V is null or undefined, then return the IDL value null. + if (union_type.includes_nullable_type()) { + TODO(); + } else if (contains_dictionary_type) { + // FIXME: 4. If V is null or undefined, then + // 4.1 If types includes a dictionary type, then return the result of converting V to that dictionary type. + TODO(); + } + + bool includes_object = false; + for (auto& type : types) { + if (type.name == "object") { + includes_object = true; + break; + } + } + + // FIXME: Don't generate this if the union type doesn't include any object types. + union_generator.append(R"~~~( + if (@js_name@@js_suffix@.is_object()) { + [[maybe_unused]] auto& @js_name@@js_suffix@_object = @js_name@@js_suffix@.as_object(); +)~~~"); + + bool includes_wrappable_type = false; + for (auto& type : types) { + if (IDL::is_wrappable_type(type)) { + includes_wrappable_type = true; + break; + } + } + + if (includes_wrappable_type) { + // 5. If V is a platform object, then: + union_generator.append(R"~~~( + if (is(@js_name@@js_suffix@_object)) { +)~~~"); + + // 1. If types includes an interface type that V implements, then return the IDL value that is a reference to the object V. + for (auto& type : types) { + if (!IDL::is_wrappable_type(type)) + continue; + + auto union_platform_object_type_generator = union_generator.fork(); + union_platform_object_type_generator.set("platform_object_type", String::formatted("{}Wrapper", type.name)); + auto cpp_type = IDL::idl_type_name_to_cpp_type(type); + union_platform_object_type_generator.set("refptr_type", cpp_type.name); + + union_platform_object_type_generator.append(R"~~~( + if (is<@platform_object_type@>(@js_name@@js_suffix@_object)) + return @refptr_type@ { static_cast<@platform_object_type@&>(@js_name@@js_suffix@_object).impl() }; +)~~~"); + } + + // 2. If types includes object, then return the IDL value that is a reference to the object V. + if (includes_object) { + union_generator.append(R"~~~( + return @js_name@@js_suffix@_object; +)~~~"); + } + + union_generator.append(R"~~~( + } +)~~~"); + } + + // FIXME: 6. If Type(V) is Object and V has an [[ArrayBufferData]] internal slot, then + // 1. If types includes ArrayBuffer, then return the result of converting V to ArrayBuffer. + // 2. If types includes object, then return the IDL value that is a reference to the object V. + + // FIXME: 7. If Type(V) is Object and V has a [[DataView]] internal slot, then: + // 1. If types includes DataView, then return the result of converting V to DataView. + // 2. If types includes object, then return the IDL value that is a reference to the object V. + + // FIXME: 8. If Type(V) is Object and V has a [[TypedArrayName]] internal slot, then: + // 1. If types includes a typed array type whose name is the value of V’s [[TypedArrayName]] internal slot, then return the result of converting V to that type. + // 2. If types includes object, then return the IDL value that is a reference to the object V. + + // FIXME: 9. If IsCallable(V) is true, then: + // 1. If types includes a callback function type, then return the result of converting V to that callback function type. + // 2. If types includes object, then return the IDL value that is a reference to the object V. + + // 10. If Type(V) is Object, then: + // 1. If types includes a sequence type, then: + RefPtr sequence_type; + for (auto& type : types) { + if (type.name == "sequence") { + sequence_type = verify_cast(type); + break; + } + } + + if (sequence_type) { + // 1. Let method be ? GetMethod(V, @@iterator). + union_generator.append(R"~~~( + auto* method = TRY(@js_name@@js_suffix@.get_method(global_object, *vm.well_known_symbol_iterator())); +)~~~"); + + // 2. If method is not undefined, return the result of creating a sequence of that type from V and method. + union_generator.append(R"~~~( + if (method) { +)~~~"); + + sequence_type->generate_sequence_from_iterable(union_generator, acceptable_cpp_name, String::formatted("{}{}", js_name, js_suffix), "method", dictionaries, recursion_depth + 1); + + union_generator.append(R"~~~( + + return @cpp_name@; + } +)~~~"); + } + + // FIXME: 2. If types includes a frozen array type, then + // 1. Let method be ? GetMethod(V, @@iterator). + // 2. If method is not undefined, return the result of creating a frozen array of that type from V and method. + + // FIXME: 3. If types includes a dictionary type, then return the result of converting V to that dictionary type. + if (contains_dictionary_type) + TODO(); + + // FIXME: 4. If types includes a record type, then return the result of converting V to that record type. + + // FIXME: 5. If types includes a callback interface type, then return the result of converting V to that callback interface type. + + // 6. If types includes object, then return the IDL value that is a reference to the object V. + if (includes_object) { + union_generator.append(R"~~~( + return @js_name@@js_suffix@_object; +)~~~"); + } + + // End of is_object. + union_generator.append(R"~~~( + } +)~~~"); + + // 11. If Type(V) is Boolean, then: + // 1. If types includes boolean, then return the result of converting V to boolean. + bool includes_boolean = false; + for (auto& type : types) { + if (type.name == "boolean") { + includes_boolean = true; + break; + } + } + + if (includes_boolean) { + union_generator.append(R"~~~( + if (@js_name@@js_suffix@.is_boolean()) + return @js_name@@js_suffix@.as_boolean(); +)~~~"); + } + + RefPtr numeric_type; + for (auto& type : types) { + if (type.is_numeric()) { + numeric_type = type; + break; + } + } + + // 12. If Type(V) is Number, then: + // 1. If types includes a numeric type, then return the result of converting V to that numeric type. + if (numeric_type) { + union_generator.append(R"~~~( + if (@js_name@@js_suffix@.is_number()) { +)~~~"); + // NOTE: generate_to_cpp doesn't use the parameter name. + // NOTE: generate_to_cpp will use to_{u32,etc.} which uses to_number internally and will thus use TRY, but it cannot throw as we know we are dealing with a number. + IDL::Parameter parameter { .type = *numeric_type, .name = String::empty(), .optional_default_value = {}, .extended_attributes = {} }; + generate_to_cpp(union_generator, parameter, js_name, js_suffix, String::formatted("{}{}_number", js_name, js_suffix), dictionaries, false, false, {}, false, recursion_depth + 1); + + union_generator.append(R"~~~( + return @js_name@@js_suffix@_number; + } +)~~~"); + } + + // 13. If Type(V) is BigInt, then: + // 1. If types includes bigint, then return the result of converting V to bigint + bool includes_bigint = false; + for (auto& type : types) { + if (type.name == "bigint") { + includes_bigint = true; + break; + } + } + + if (includes_bigint) { + union_generator.append(R"~~~( + if (@js_name@@js_suffix@.is_bigint()) + return @js_name@@js_suffix@.as_bigint(); +)~~~"); + } + + bool includes_string = false; + for (auto& type : types) { + if (type.is_string()) { + includes_string = true; + break; + } + } + + if (includes_string) { + // 14. If types includes a string type, then return the result of converting V to that type. + // NOTE: Currently all string types are converted to String. + union_generator.append(R"~~~( + return TRY(@js_name@@js_suffix@.to_string(global_object)); +)~~~"); + } else if (numeric_type && includes_bigint) { + // 15. If types includes a numeric type and bigint, then return the result of converting V to either that numeric type or bigint. + // https://webidl.spec.whatwg.org/#converted-to-a-numeric-type-or-bigint + // NOTE: This algorithm is only used here. + + // An ECMAScript value V is converted to an IDL numeric type T or bigint value by running the following algorithm: + // 1. Let x be ? ToNumeric(V). + // 2. If Type(x) is BigInt, then + // 1. Return the IDL bigint value that represents the same numeric value as x. + // 3. Assert: Type(x) is Number. + // 4. Return the result of converting x to T. + + auto union_numeric_type_generator = union_generator.fork(); + auto cpp_type = IDL::idl_type_name_to_cpp_type(*numeric_type); + union_numeric_type_generator.set("numeric_type", cpp_type.name); + + union_numeric_type_generator.append(R"~~~( + auto x = TRY(@js_name@@js_suffix@.to_numeric(global_object)); + if (x.is_bigint()) + return x.as_bigint(); + VERIFY(x.is_number()); +)~~~"); + + // NOTE: generate_to_cpp doesn't use the parameter name. + // NOTE: generate_to_cpp will use to_{u32,etc.} which uses to_number internally and will thus use TRY, but it cannot throw as we know we are dealing with a number. + IDL::Parameter parameter { .type = *numeric_type, .name = String::empty(), .optional_default_value = {}, .extended_attributes = {} }; + generate_to_cpp(union_numeric_type_generator, parameter, "x", String::empty(), "x_number", dictionaries, false, false, {}, false, recursion_depth + 1); + + union_numeric_type_generator.append(R"~~~( + return x_number; +)~~~"); + } else if (numeric_type) { + // 16. If types includes a numeric type, then return the result of converting V to that numeric type. + + // NOTE: generate_to_cpp doesn't use the parameter name. + // NOTE: generate_to_cpp will use to_{u32,etc.} which uses to_number internally and will thus use TRY, but it cannot throw as we know we are dealing with a number. + IDL::Parameter parameter { .type = *numeric_type, .name = String::empty(), .optional_default_value = {}, .extended_attributes = {} }; + generate_to_cpp(union_generator, parameter, js_name, js_suffix, String::formatted("{}{}_number", js_name, js_suffix), dictionaries, false, false, {}, false, recursion_depth + 1); + + union_generator.append(R"~~~( + return @js_name@@js_suffix@_number; +)~~~"); + } else if (includes_boolean) { + // 17. If types includes boolean, then return the result of converting V to boolean. + union_generator.append(R"~~~( + return @js_name@@js_suffix@.to_boolean(); +)~~~"); + } else if (includes_bigint) { + // 18. If types includes bigint, then return the result of converting V to bigint. + union_generator.append(R"~~~( + return TRY(@js_name@@js_suffix@.to_bigint(global_object)); +)~~~"); + } else { + // 19. Throw a TypeError. + // FIXME: Replace the error message with something more descriptive. + union_generator.append(R"~~~( + return vm.throw_completion(global_object, "No union types matched"); +)~~~"); + } + + // Close the lambda and then perform the conversion. + union_generator.append(R"~~~( + }; + + )~~~"); + + if (!variadic) { + if (!optional) { + union_generator.append(R"~~~( + @union_type@ @cpp_name@ = TRY(@js_name@@js_suffix@_to_variant(@js_name@@js_suffix@)); + )~~~"); + } else { + if (!optional_default_value.has_value()) { + union_generator.append(R"~~~( + Optional<@union_type@> @cpp_name@; + if (!@js_name@@js_suffix@.is_undefined()) + @cpp_name@ = TRY(@js_name@@js_suffix@_to_variant(@js_name@@js_suffix@)); + )~~~"); + } else { + if (optional_default_value != "\"\"") + TODO(); + + union_generator.append(R"~~~( + @union_type@ @cpp_name@ = @js_name@@js_suffix@.is_undefined() ? String::empty() : TRY(@js_name@@js_suffix@_to_variant(@js_name@@js_suffix@)); + )~~~"); + } + } + } else { + union_generator.append(R"~~~( + Vector<@union_type@> @cpp_name@; + @cpp_name@.ensure_capacity(vm.argument_count() - @js_suffix@); + + for (size_t i = @js_suffix@; i < vm.argument_count(); ++i) { + auto result = TRY(@js_name@@js_suffix@_to_variant(vm.argument(i))); + @cpp_name@.append(move(result)); + } + )~~~"); + } } else { dbgln("Unimplemented JS-to-C++ conversion: {}", parameter.type->name); VERIFY_NOT_REACHED(); @@ -1506,6 +2017,8 @@ static void generate_wrap_statement(SourceGenerator& generator, String const& va else @result_expression@ @value@.callback.cell(); )~~~"); + } else if (is(type)) { + TODO(); } else { if (wrapping_reference == WrappingReference::No) { scoped_generator.append(R"~~~(