Skip to content

Commit

Permalink
LibWeb: Add a very basic and ad-hoc version of IDL overload resolution
Browse files Browse the repository at this point in the history
This initial version lays down the basic foundation of IDL overload
resolution, but much of it will have to be replaced with the actual IDL
overload resolution algorithms once we start implementing more complex
IDL overloading scenarios.
  • Loading branch information
IdanHo authored and awesomekling committed Mar 5, 2022
1 parent 24cf568 commit 59e9e7c
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "IDLTypes.h"
#include <AK/LexicalPath.h>
#include <AK/Queue.h>
#include <AK/QuickSort.h>

Vector<StringView> s_header_search_paths;

Expand Down Expand Up @@ -1079,21 +1080,21 @@ static void generate_to_cpp(SourceGenerator& generator, ParameterType& parameter
}
}

template<typename FunctionType>
static void generate_argument_count_check(SourceGenerator& generator, FunctionType& function)
static void generate_argument_count_check(SourceGenerator& generator, String const& function_name, size_t argument_count)
{
if (argument_count == 0)
return;

auto argument_count_check_generator = generator.fork();
argument_count_check_generator.set("function.name", function.name);
argument_count_check_generator.set("function.nargs", String::number(function.length()));
argument_count_check_generator.set("function.name", function_name);
argument_count_check_generator.set("function.nargs", String::number(argument_count));

if (function.length() == 0)
return;
if (function.length() == 1) {
if (argument_count == 1) {
argument_count_check_generator.set(".bad_arg_count", "JS::ErrorType::BadArgCountOne");
argument_count_check_generator.set(".arg_count_suffix", "");
} else {
argument_count_check_generator.set(".bad_arg_count", "JS::ErrorType::BadArgCountMany");
argument_count_check_generator.set(".arg_count_suffix", String::formatted(", \"{}\"", function.length()));
argument_count_check_generator.set(".arg_count_suffix", String::formatted(", \"{}\"", argument_count));
}

argument_count_check_generator.append(R"~~~(
Expand Down Expand Up @@ -1316,6 +1317,7 @@ static void generate_function(SourceGenerator& generator, IDL::Function const& f
function_generator.set("interface_fully_qualified_name", interface_fully_qualified_name);
function_generator.set("function.name", function.name);
function_generator.set("function.name:snakecase", make_input_acceptable_cpp(function.name.to_snakecase()));
function_generator.set("overload_suffix", function.is_overloaded ? String::number(function.overload_index) : String::empty());

if (function.extended_attributes.contains("ImplementedAs")) {
auto implemented_as = function.extended_attributes.get("ImplementedAs").value();
Expand All @@ -1325,7 +1327,7 @@ static void generate_function(SourceGenerator& generator, IDL::Function const& f
}

function_generator.append(R"~~~(
JS_DEFINE_NATIVE_FUNCTION(@class_name@::@function.name:snakecase@)
JS_DEFINE_NATIVE_FUNCTION(@class_name@::@function.name:snakecase@@overload_suffix@)
{
)~~~");

Expand All @@ -1335,7 +1337,9 @@ JS_DEFINE_NATIVE_FUNCTION(@class_name@::@function.name:snakecase@)
)~~~");
}

generate_argument_count_check(generator, function);
// Optimization: overloaded functions' arguments count is checked by the overload arbiter
if (!function.is_overloaded)
generate_argument_count_check(generator, function.name, function.length());

StringBuilder arguments_builder;
generate_arguments(generator, function.parameters, arguments_builder, interface);
Expand All @@ -1358,6 +1362,108 @@ JS_DEFINE_NATIVE_FUNCTION(@class_name@::@function.name:snakecase@)
)~~~");
}

// FIXME: This is extremely ad-hoc, implement the WebIDL overload resolution algorithm instead
static Optional<String> generate_arguments_match_check_for_count(Vector<IDL::Parameter> const& parameters, size_t argument_count)
{
Vector<String> conditions;
for (auto i = 0u; i < argument_count; ++i) {
auto const& parameter = parameters[i];
if (parameter.type->is_string() || parameter.type->is_numeric())
continue;
auto argument = String::formatted("arg{}", i);
StringBuilder condition;
condition.append('(');
if (parameter.type->nullable)
condition.appendff("{}.is_nullish() || ", argument);
else if (parameter.optional)
condition.appendff("{}.is_undefined() || ", argument);
condition.appendff("{}.is_object()", argument);
condition.append(')');
conditions.append(condition.build());
}
if (conditions.is_empty())
return {};
return String::formatted("({})", String::join(" && ", conditions));
}

static String generate_arguments_match_check(Function const& function)
{
Vector<String> options;
for (size_t i = 0; i < function.parameters.size(); ++i) {
if (!function.parameters[i].optional && !function.parameters[i].variadic)
continue;
auto match_check = generate_arguments_match_check_for_count(function.parameters, i);
if (match_check.has_value())
options.append(match_check.release_value());
}
if (!function.parameters.is_empty() && !function.parameters.last().variadic) {
auto match_check = generate_arguments_match_check_for_count(function.parameters, function.parameters.size());
if (match_check.has_value())
options.append(match_check.release_value());
}
return String::join(" || ", options);
}

static void generate_overload_arbiter(SourceGenerator& generator, auto const& overload_set, String const& class_name)
{
auto function_generator = generator.fork();
function_generator.set("class_name", class_name);
function_generator.set("function.name:snakecase", make_input_acceptable_cpp(overload_set.key.to_snakecase()));

function_generator.append(R"~~~(
JS_DEFINE_NATIVE_FUNCTION(@class_name@::@function.name:snakecase@)
{
)~~~");

auto minimum_argument_count = get_shortest_function_length(overload_set.value);
generate_argument_count_check(function_generator, overload_set.key, minimum_argument_count);

auto overloaded_functions = overload_set.value;
quick_sort(overloaded_functions, [](auto const& a, auto const& b) { return a.length() < b.length(); });
auto fetched_arguments = 0u;
for (auto i = 0u; i < overloaded_functions.size(); ++i) {
auto const& overloaded_function = overloaded_functions[i];
auto argument_count = overloaded_function.length();

function_generator.set("argument_count", String::number(argument_count));
function_generator.set("arguments_match_check", generate_arguments_match_check(overloaded_function));
function_generator.set("overload_suffix", String::number(i));

if (argument_count > fetched_arguments) {
for (auto j = fetched_arguments; j < argument_count; ++j) {
function_generator.set("argument.index", String::number(j));
function_generator.append(R"~~~(
[[maybe_unused]] auto [email protected]@ = vm.argument(@argument.index@);
)~~~");
}
fetched_arguments = argument_count;
}

auto is_last = i == overloaded_functions.size() - 1;
if (!is_last) {
function_generator.append(R"~~~(
if (vm.argument_count() == @argument_count@) {
)~~~");
}

function_generator.append(R"~~~(
if (@arguments_match_check@)
return @function.name:snakecase@@overload_suffix@(vm, global_object);
)~~~");

if (!is_last) {
function_generator.append(R"~~~(
}
)~~~");
}
}

function_generator.append(R"~~~(
return vm.throw_completion<JS::TypeError>(global_object, JS::ErrorType::OverloadResolutionFailed);
}
)~~~");
}

void generate_header(IDL::Interface const& interface)
{
StringBuilder builder;
Expand Down Expand Up @@ -2355,12 +2461,20 @@ class @constructor_class@ : public JS::NativeFunction {
virtual bool has_constructor() const override { return true; }
)~~~");

for (auto& function : interface.static_functions) {
for (auto const& overload_set : interface.static_overload_sets) {
auto function_generator = generator.fork();
function_generator.set("function.name:snakecase", make_input_acceptable_cpp(function.name.to_snakecase()));
function_generator.set("function.name:snakecase", make_input_acceptable_cpp(overload_set.key.to_snakecase()));
function_generator.append(R"~~~(
JS_DECLARE_NATIVE_FUNCTION(@function.name:snakecase@);
)~~~");
if (overload_set.value.size() > 1) {
for (auto i = 0u; i < overload_set.value.size(); ++i) {
function_generator.set("overload_suffix", String::number(i));
function_generator.append(R"~~~(
JS_DECLARE_NATIVE_FUNCTION(@function.name:snakecase@@overload_suffix@);
)~~~");
}
}
}

generator.append(R"~~~(
Expand Down Expand Up @@ -2486,7 +2600,7 @@ JS::ThrowCompletionOr<JS::Object*> @constructor_class@::construct(FunctionObject
)~~~");

if (!constructor.parameters.is_empty()) {
generate_argument_count_check(generator, constructor);
generate_argument_count_check(generator, constructor.name, constructor.length());

StringBuilder arguments_builder;
generate_arguments(generator, constructor.parameters, arguments_builder, interface);
Expand Down Expand Up @@ -2534,11 +2648,11 @@ define_direct_property("@constant.name@", JS::Value((i32)@constant.value@), JS::
}

// https://webidl.spec.whatwg.org/#es-operations
for (auto& function : interface.static_functions) {
for (auto const& overload_set : interface.static_overload_sets) {
auto function_generator = generator.fork();
function_generator.set("function.name", function.name);
function_generator.set("function.name:snakecase", make_input_acceptable_cpp(function.name.to_snakecase()));
function_generator.set("function.length", String::number(function.length()));
function_generator.set("function.name", overload_set.key);
function_generator.set("function.name:snakecase", make_input_acceptable_cpp(overload_set.key.to_snakecase()));
function_generator.set("function.length", String::number(get_shortest_function_length(overload_set.value)));

function_generator.append(R"~~~(
define_native_function("@function.name@", @function.name:snakecase@, @function.length@, default_attributes);
Expand All @@ -2550,8 +2664,12 @@ define_direct_property("@constant.name@", JS::Value((i32)@constant.value@), JS::
)~~~");

// Implementation: Static Functions
for (auto& function : interface.static_functions) {
for (auto& function : interface.static_functions)
generate_function(generator, function, StaticFunction::Yes, interface.constructor_class, interface.fully_qualified_name, interface);
for (auto const& overload_set : interface.static_overload_sets) {
if (overload_set.value.size() == 1)
continue;
generate_overload_arbiter(generator, overload_set, interface.constructor_class);
}

generator.append(R"~~~(
Expand Down Expand Up @@ -2587,12 +2705,20 @@ class @prototype_class@ : public JS::Object {
private:
)~~~");

for (auto& function : interface.functions) {
for (auto const& overload_set : interface.overload_sets) {
auto function_generator = generator.fork();
function_generator.set("function.name:snakecase", make_input_acceptable_cpp(function.name.to_snakecase()));
function_generator.set("function.name:snakecase", make_input_acceptable_cpp(overload_set.key.to_snakecase()));
function_generator.append(R"~~~(
JS_DECLARE_NATIVE_FUNCTION(@function.name:snakecase@);
)~~~");
if (overload_set.value.size() > 1) {
for (auto i = 0u; i < overload_set.value.size(); ++i) {
function_generator.set("overload_suffix", String::number(i));
function_generator.append(R"~~~(
JS_DECLARE_NATIVE_FUNCTION(@function.name:snakecase@@overload_suffix@);
)~~~");
}
}
}

if (interface.has_stringifier) {
Expand Down Expand Up @@ -2788,13 +2914,14 @@ void @prototype_class@::initialize(JS::GlobalObject& global_object)
}

// https://webidl.spec.whatwg.org/#es-operations
for (auto& function : interface.functions) {
for (auto const& overload_set : interface.overload_sets) {
auto function_generator = generator.fork();
function_generator.set("function.name", function.name);
function_generator.set("function.name:snakecase", make_input_acceptable_cpp(function.name.to_snakecase()));
function_generator.set("function.length", String::number(function.length()));
function_generator.set("function.name", overload_set.key);
function_generator.set("function.name:snakecase", make_input_acceptable_cpp(overload_set.key.to_snakecase()));
function_generator.set("function.length", String::number(get_shortest_function_length(overload_set.value)));

if (function.extended_attributes.contains("Unscopable")) {
// FIXME: What if only some of the overloads are Unscopable?
if (any_of(overload_set.value, [](auto const& function) { return function.extended_attributes.contains("Unscopable"); })) {
function_generator.append(R"~~~(
MUST(unscopable_object->create_data_property("@function.name@", JS::Value(true)));
)~~~");
Expand Down Expand Up @@ -2971,8 +3098,12 @@ JS_DEFINE_NATIVE_FUNCTION(@prototype_class@::@attribute.setter_callback@)
}

// Implementation: Functions
for (auto& function : interface.functions) {
for (auto& function : interface.functions)
generate_function(generator, function, StaticFunction::No, interface.prototype_class, interface.fully_qualified_name, interface);
for (auto const& overload_set : interface.overload_sets) {
if (overload_set.value.size() == 1)
continue;
generate_overload_arbiter(generator, overload_set, interface.prototype_class);
}

if (interface.has_stringifier) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ Function Parser::parse_function(HashMap<String, String>& extended_attributes, In
consume_whitespace();
assert_specific(';');

Function function { move(return_type), name, move(parameters), move(extended_attributes) };
Function function { move(return_type), name, move(parameters), move(extended_attributes), {}, false };

// "Defining a special operation with an identifier is equivalent to separating the special operation out into its own declaration without an identifier."
if (is_special_operation == IsSpecialOperation::No || (is_special_operation == IsSpecialOperation::Yes && !name.is_empty())) {
Expand Down Expand Up @@ -803,6 +803,31 @@ NonnullOwnPtr<Interface> Parser::parse()
}
}

// Create overload sets
for (auto& function : interface->functions) {
auto& overload_set = interface->overload_sets.ensure(function.name);
function.overload_index = overload_set.size();
overload_set.append(function);
}
for (auto& overload_set : interface->overload_sets) {
if (overload_set.value.size() == 1)
continue;
for (auto& overloaded_function : overload_set.value)
overloaded_function.is_overloaded = true;
}
for (auto& function : interface->static_functions) {
auto& overload_set = interface->static_overload_sets.ensure(function.name);
function.overload_index = overload_set.size();
overload_set.append(function);
}
for (auto& overload_set : interface->static_overload_sets) {
if (overload_set.value.size() == 1)
continue;
for (auto& overloaded_function : overload_set.value)
overloaded_function.is_overloaded = true;
}
// FIXME: Add support for overloading constructors

interface->imported_modules = move(imports);

return interface;
Expand Down
13 changes: 13 additions & 0 deletions Meta/Lagom/Tools/CodeGenerators/LibWeb/WrapperGenerator/IDLTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ struct Function {
String name;
Vector<Parameter> parameters;
HashMap<String, String> extended_attributes;
size_t overload_index { 0 };
bool is_overloaded { false };

size_t length() const { return get_function_length(*this); }
};
Expand Down Expand Up @@ -144,6 +146,14 @@ struct ParameterizedType : public Type {
void generate_sequence_from_iterable(SourceGenerator& generator, String const& cpp_name, String const& iterable_cpp_name, String const& iterator_method_cpp_name, IDL::Interface const&, size_t recursion_depth) const;
};

static inline size_t get_shortest_function_length(Vector<Function&> const& overload_set)
{
size_t longest_length = SIZE_MAX;
for (auto const& function : overload_set)
longest_length = min(function.length(), longest_length);
return longest_length;
}

struct Interface {
String name;
String parent_name;
Expand Down Expand Up @@ -189,6 +199,9 @@ struct Interface {
HashTable<String> imported_paths;
NonnullOwnPtrVector<Interface> imported_modules;

HashMap<String, Vector<Function&>> overload_sets;
HashMap<String, Vector<Function&>> static_overload_sets;

// https://webidl.spec.whatwg.org/#dfn-support-indexed-properties
bool supports_indexed_properties() const { return indexed_property_getter.has_value(); }

Expand Down
1 change: 1 addition & 0 deletions Userland/Libraries/LibJS/Runtime/ErrorTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"Object prototype must not be {} on a super property access") \
M(ObjectPrototypeWrongType, "Prototype must be an object or null") \
M(OptionIsNotValidValue, "{} is not a valid value for option {}") \
M(OverloadResolutionFailed, "Overload resolution failed") \
M(PrivateFieldAlreadyDeclared, "Private field '{}' has already been declared") \
M(PrivateFieldDoesNotExistOnObject, "Private field '{}' does not exist on object") \
M(PrivateFieldGetAccessorWithoutGetter, "Cannot get private field '{}' as accessor without getter") \
Expand Down

0 comments on commit 59e9e7c

Please sign in to comment.