1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-28 19:35:09 +00:00
serenity/Meta/Lagom/Tools/CodeGenerators/GMLCompiler/main.cpp
kleines Filmröllchen d1645efde9 Meta+Userland: Allow generating C++ initializer code from GML
This does the exact same thing as the runtime initializer,
except it is faster and can catch some errors much earlier.

The code generator includes these important features:
- Automatic include generation where necessary
- Special-casing for TabWidget and ScrollableContainerWidget
- No use of DeprecatedString where possible
2023-08-11 21:33:48 +02:00

301 lines
13 KiB
C++

/*
* Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Forward.h>
#include <AK/HashTable.h>
#include <AK/SourceGenerator.h>
#include <AK/String.h>
#include <AK/Try.h>
#include <AK/Utf8View.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/File.h>
#include <LibGUI/GML/Parser.h>
#include <LibGUI/UIDimensions.h>
#include <LibMain/Main.h>
enum class UseObjectConstructor : bool {
No,
Yes,
};
// Classes whose header doesn't have the same name as the class.
static Optional<StringView> map_class_to_file(StringView class_)
{
static HashMap<StringView, StringView> class_file_mappings {
{ "GUI::HorizontalSplitter"sv, "GUI/Splitter"sv },
{ "GUI::VerticalSplitter"sv, "GUI/Splitter"sv },
{ "GUI::HorizontalBoxLayout"sv, "GUI/BoxLayout"sv },
{ "GUI::VerticalBoxLayout"sv, "GUI/BoxLayout"sv },
{ "GUI::HorizontalProgressbar"sv, "GUI/Progressbar"sv },
{ "GUI::VerticalProgressbar"sv, "GUI/Progressbar"sv },
};
return class_file_mappings.get(class_);
}
// Properties which don't take a direct JSON-like primitive (StringView, int, bool, Array etc) as arguments and need the arguments to be wrapped in a constructor call.
static Optional<StringView> map_property_to_type(StringView property)
{
static HashMap<StringView, StringView> property_to_type_mappings {
{ "container_margins"sv, "GUI::Margins"sv },
{ "margins"sv, "GUI::Margins"sv },
};
return property_to_type_mappings.get(property);
}
// Properties which take a UIDimension which can handle JSON directly.
static bool is_ui_dimension_property(StringView property)
{
static HashTable<StringView> ui_dimension_properties;
if (ui_dimension_properties.is_empty()) {
ui_dimension_properties.set("min_width"sv);
ui_dimension_properties.set("max_width"sv);
ui_dimension_properties.set("preferred_width"sv);
ui_dimension_properties.set("min_height"sv);
ui_dimension_properties.set("max_height"sv);
ui_dimension_properties.set("preferred_height"sv);
}
return ui_dimension_properties.contains(property);
}
// FIXME: Since normal string-based properties take either String or StringView (and the latter can be implicitly constructed from the former),
// we need to special-case DeprecatedString property setters while those still exist.
// Please remove a setter from this list once it uses StringView or String.
static bool takes_deprecated_string(StringView property)
{
static HashTable<StringView> deprecated_string_properties;
if (deprecated_string_properties.is_empty()) {
deprecated_string_properties.set("name"sv);
}
return deprecated_string_properties.contains(property);
}
static ErrorOr<String> include_path_for(StringView class_name)
{
String pathed_name;
if (auto mapping = map_class_to_file(class_name); mapping.has_value())
pathed_name = TRY(String::from_utf8(mapping.value()));
else
pathed_name = TRY(TRY(String::from_utf8(class_name)).replace("::"sv, "/"sv, ReplaceMode::All));
if (class_name.starts_with("GUI::"sv) || class_name.starts_with("WebView::"sv))
return String::formatted("<Lib{}.h>", pathed_name);
// We assume that all other paths are within the current application, for now.
return String::formatted("<Applications/{}.h>", pathed_name);
}
// Each entry is an include path, without the "#include" itself.
static ErrorOr<HashTable<String>> extract_necessary_includes(GUI::GML::Object const& gml_hierarchy)
{
HashTable<String> necessary_includes;
TRY(necessary_includes.try_set(TRY(include_path_for(gml_hierarchy.name()))));
if (gml_hierarchy.layout_object() != nullptr)
TRY(necessary_includes.try_set(TRY(include_path_for(gml_hierarchy.layout_object()->name()))));
TRY(gml_hierarchy.try_for_each_child_object([&](auto const& object) -> ErrorOr<void> {
auto necessary_child_includes = TRY(extract_necessary_includes(object));
for (auto const& include : necessary_child_includes)
TRY(necessary_includes.try_set(include));
return {};
}));
return necessary_includes;
}
static char const header[] = R"~~~(
/*
* Auto-generated by the GML compiler
*/
)~~~";
static char const function_start[] = R"~~~(
// Creates a @main_class_name@ and initializes it.
// This function was auto-generated by the GML compiler.
ErrorOr<NonnullRefPtr<@main_class_name@>> @main_class_name@::try_create()
{
RefPtr<@main_class_name@> main_object;
)~~~";
static char const footer[] = R"~~~(
return main_object.release_nonnull();
}
)~~~";
// FIXME: In case of error, propagate the precise array+property that triggered the error.
static ErrorOr<String> generate_initializer_for(Optional<StringView> property_name, JsonValue value)
{
if (value.is_string()) {
if (property_name.has_value() && takes_deprecated_string(*property_name))
return String::formatted(R"~~~("{}"sv)~~~", value.as_string());
return String::formatted(R"~~~("{}"_string)~~~", value.as_string());
}
// No need to handle the smaller integer types separately.
if (value.is_integer<i64>())
return String::formatted("static_cast<i64>({})", value.as_integer<i64>());
if (value.is_integer<u64>())
return String::formatted("static_cast<u64>({})", value.as_integer<u64>());
if (value.is_bool())
return String::formatted("{}", value.as_bool());
if (value.is_double())
return String::formatted("static_cast<double>({})", value.as_double());
if (value.is_array()) {
auto const& array = value.as_array();
auto child_type = Optional<StringView> {};
for (auto const& child_value : array.values()) {
if (child_value.is_array())
return Error::from_string_view("Nested arrays are not supported"sv);
#define HANDLE_TYPE(type_name, is_type) \
if (child_value.is_type() && (!child_type.has_value() || child_type.value() == #type_name##sv)) \
child_type = #type_name##sv; \
else
HANDLE_TYPE(StringView, is_string)
HANDLE_TYPE(i64, is_integer<i64>)
HANDLE_TYPE(u64, is_integer<u64>)
HANDLE_TYPE(bool, is_bool)
HANDLE_TYPE(double, is_double)
return Error::from_string_view("Inconsistent contained type in JSON array"sv);
#undef HANDLE_TYPE
}
if (!child_type.has_value())
return Error::from_string_view("Empty JSON array; cannot deduce type."sv);
StringBuilder initializer;
initializer.appendff("Array<{}, {}> {{ "sv, child_type.release_value(), array.size());
for (auto const& child_value : array.values())
initializer.appendff("{}, ", TRY(generate_initializer_for({}, child_value)));
initializer.append("}"sv);
return initializer.to_string();
}
return Error::from_string_view("Unsupported JSON value"sv);
}
// Loads an object and assigns it to the RefPtr<Widget> variable named object_name.
// All loading happens in a separate block.
static ErrorOr<void> generate_loader_for_object(GUI::GML::Object const& gml_object, SourceGenerator generator, String object_name, size_t indentation, UseObjectConstructor use_object_constructor)
{
generator.set("object_name", object_name.to_deprecated_string());
generator.set("class_name", gml_object.name());
auto append = [&]<size_t N>(auto& generator, char const(&text)[N]) -> ErrorOr<void> {
generator.append(TRY(String::repeated(' ', indentation * 4)).bytes_as_string_view());
generator.appendln(text);
return {};
};
generator.append(TRY(String::repeated(' ', (indentation - 1) * 4)).bytes_as_string_view());
generator.appendln("{");
if (use_object_constructor == UseObjectConstructor::Yes)
TRY(append(generator, "@object_name@ = TRY(@class_name@::try_create());"));
else
TRY(append(generator, "@object_name@ = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) @class_name@()));"));
// Properties
TRY(gml_object.try_for_each_property([&](StringView key, NonnullRefPtr<GUI::GML::JsonValueNode> value) -> ErrorOr<void> {
auto value_code = TRY(generate_initializer_for(key, value));
if (is_ui_dimension_property(key)) {
if (auto ui_dimension = GUI::UIDimension::construct_from_json_value(value); ui_dimension.has_value())
value_code = TRY(ui_dimension->as_cpp_source());
else
// FIXME: propagate precise error cause
return Error::from_string_view("UI dimension invalid"sv);
} else {
// Wrap value in an extra constructor call if necessary.
if (auto type = map_property_to_type(key); type.has_value())
value_code = TRY(String::formatted("{} {{ {} }}", type.release_value(), value_code));
}
auto property_generator = TRY(generator.fork());
property_generator.set("key", key);
property_generator.set("value", value_code.bytes_as_string_view());
TRY(append(property_generator, R"~~~(@object_name@->set_@key@(@value@);)~~~"));
return {};
}));
generator.appendln("");
// Layout
if (gml_object.layout_object() != nullptr) {
TRY(append(generator, "RefPtr<GUI::Layout> layout;"));
TRY(generate_loader_for_object(*gml_object.layout_object(), TRY(generator.fork()), TRY(String::from_utf8("layout"sv)), indentation + 1, UseObjectConstructor::Yes));
TRY(append(generator, "@object_name@->set_layout(layout.release_nonnull());"));
generator.appendln("");
}
// Children
size_t current_child_index = 0;
auto next_child_name = [&]() {
return String::formatted("{}_child_{}", object_name, current_child_index++);
};
TRY(gml_object.try_for_each_child_object([&](auto const& child) -> ErrorOr<void> {
auto child_generator = TRY(generator.fork());
auto child_variable_name = TRY(next_child_name());
child_generator.set("child_variable_name", child_variable_name.bytes_as_string_view());
child_generator.set("child_class_name", child.name());
TRY(append(child_generator, "RefPtr<@child_class_name@> @child_variable_name@;"));
TRY(generate_loader_for_object(child, TRY(child_generator.fork()), child_variable_name, indentation + 1, UseObjectConstructor::Yes));
// Handle the current two special cases of child adding.
if (gml_object.name() == "GUI::ScrollableContainerWidget"sv)
TRY(append(child_generator, "static_ptr_cast<GUI::ScrollableContainerWidget>(@object_name@)->set_widget(*@child_variable_name@);"));
else if (gml_object.name() == "GUI::TabWidget"sv)
TRY(append(child_generator, "static_ptr_cast<GUI::TabWidget>(@object_name@)->add_widget(*@child_variable_name@);"));
else
TRY(append(child_generator, "TRY(@object_name@->try_add_child(*@child_variable_name@));"));
child_generator.appendln("");
return {};
}));
generator.append(TRY(String::repeated(' ', (indentation - 1) * 4)).bytes_as_string_view());
generator.appendln("}");
return {};
}
static ErrorOr<String> generate_cpp(NonnullRefPtr<GUI::GML::GMLFile> gml)
{
StringBuilder builder;
SourceGenerator generator { builder };
generator.append(header);
auto& main_class = gml->main_class();
auto necessary_includes = TRY(extract_necessary_includes(main_class));
static String const always_necessary_includes[] = {
TRY(String::from_utf8("<AK/Error.h>"sv)),
TRY(String::from_utf8("<AK/JsonValue.h>"sv)),
TRY(String::from_utf8("<AK/NonnullRefPtr.h>"sv)),
TRY(String::from_utf8("<AK/RefPtr.h>"sv)),
TRY(String::from_utf8("<LibGUI/Widget.h>"sv)),
};
TRY(necessary_includes.try_set_from(always_necessary_includes));
for (auto const& include : necessary_includes)
generator.appendln(TRY(String::formatted("#include {}", include)).bytes_as_string_view());
// FIXME: Use a UTF-8 aware function once possible.
generator.set("main_class_name", main_class.name());
generator.append(function_start);
TRY(generate_loader_for_object(main_class, TRY(generator.fork()), "main_object"_string, 2, UseObjectConstructor::No));
generator.append(footer);
return builder.to_string();
}
ErrorOr<int> serenity_main(Main::Arguments arguments)
{
Core::ArgsParser argument_parser;
StringView gml_file_name;
argument_parser.add_positional_argument(gml_file_name, "GML file to compile", "GML_FILE", Core::ArgsParser::Required::Yes);
argument_parser.parse(arguments);
auto gml_text = TRY(TRY(Core::File::open(gml_file_name, Core::File::OpenMode::Read))->read_until_eof());
auto parsed_gml = TRY(GUI::GML::parse_gml(gml_text));
auto generated_cpp = TRY(generate_cpp(parsed_gml));
outln("{}", generated_cpp);
return 0;
}