From d1645efde994a6574fb95b21160689ccbd27dc92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kleines=20Filmr=C3=B6llchen?= Date: Fri, 11 Aug 2023 15:16:51 +0200 Subject: [PATCH] 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 --- Meta/CMake/code_generators.cmake | 16 + Meta/Lagom/CMakeLists.txt | 11 + .../Lagom/Tools/CodeGenerators/CMakeLists.txt | 1 + .../CodeGenerators/GMLCompiler/CMakeLists.txt | 5 + .../Tools/CodeGenerators/GMLCompiler/main.cpp | 301 ++++++++++++++++++ Userland/Libraries/LibGUI/GML/AST.h | 13 + 6 files changed, 347 insertions(+) create mode 100644 Meta/Lagom/Tools/CodeGenerators/GMLCompiler/CMakeLists.txt create mode 100644 Meta/Lagom/Tools/CodeGenerators/GMLCompiler/main.cpp diff --git a/Meta/CMake/code_generators.cmake b/Meta/CMake/code_generators.cmake index c7d9b60948..59ec3f9b79 100644 --- a/Meta/CMake/code_generators.cmake +++ b/Meta/CMake/code_generators.cmake @@ -18,6 +18,22 @@ function(stringify_gml source output string_name) add_dependencies(all_generated generate_${output_name}) endfunction() +function(compile_gml source output) + set(source ${CMAKE_CURRENT_SOURCE_DIR}/${source}) + add_custom_command( + OUTPUT ${output} + COMMAND $ ${source} > ${output}.tmp + COMMAND "${CMAKE_COMMAND}" -E copy_if_different ${output}.tmp ${output} + COMMAND "${CMAKE_COMMAND}" -E remove ${output}.tmp + VERBATIM + DEPENDS Lagom::GMLCompiler + MAIN_DEPENDENCY ${source} + ) + get_filename_component(output_name ${output} NAME) + add_custom_target(generate_${output_name} DEPENDS ${output}) + add_dependencies(all_generated generate_${output_name}) +endfunction() + function(compile_ipc source output) if (NOT IS_ABSOLUTE ${source}) set(source ${CMAKE_CURRENT_SOURCE_DIR}/${source}) diff --git a/Meta/Lagom/CMakeLists.txt b/Meta/Lagom/CMakeLists.txt index dc8284ee65..6762b038d4 100644 --- a/Meta/Lagom/CMakeLists.txt +++ b/Meta/Lagom/CMakeLists.txt @@ -370,6 +370,17 @@ install(TARGETS LibTimeZone EXPORT LagomTargets) # This is used by the BindingsGenerator so needs to always be built. add_serenity_subdirectory(Userland/Libraries/LibIDL) +# LibGUI - only GML +# This is used by the GML compiler and therefore always needed. +set(LIBGUI_GML_SOURCES + GML/Lexer.cpp + GML/Parser.cpp +) +list(TRANSFORM LIBGUI_GML_SOURCES PREPEND "${SERENITY_PROJECT_ROOT}/Userland/Libraries/LibGUI/") +lagom_lib(LibGUI_GML gui_gml + SOURCES ${LIBGUI_GML_SOURCES} +) + # Manually install AK headers install( DIRECTORY "${SERENITY_PROJECT_ROOT}/AK" diff --git a/Meta/Lagom/Tools/CodeGenerators/CMakeLists.txt b/Meta/Lagom/Tools/CodeGenerators/CMakeLists.txt index c391d6e8b7..fff1d4276b 100644 --- a/Meta/Lagom/Tools/CodeGenerators/CMakeLists.txt +++ b/Meta/Lagom/Tools/CodeGenerators/CMakeLists.txt @@ -1,3 +1,4 @@ +add_subdirectory(GMLCompiler) add_subdirectory(IPCCompiler) add_subdirectory(LibEDID) add_subdirectory(LibGL) diff --git a/Meta/Lagom/Tools/CodeGenerators/GMLCompiler/CMakeLists.txt b/Meta/Lagom/Tools/CodeGenerators/GMLCompiler/CMakeLists.txt new file mode 100644 index 0000000000..ad30e72e05 --- /dev/null +++ b/Meta/Lagom/Tools/CodeGenerators/GMLCompiler/CMakeLists.txt @@ -0,0 +1,5 @@ +set(SOURCES + main.cpp +) + +lagom_tool(GMLCompiler LIBS LibMain LibCore LibGUI_GML) diff --git a/Meta/Lagom/Tools/CodeGenerators/GMLCompiler/main.cpp b/Meta/Lagom/Tools/CodeGenerators/GMLCompiler/main.cpp new file mode 100644 index 0000000000..ee37eecf45 --- /dev/null +++ b/Meta/Lagom/Tools/CodeGenerators/GMLCompiler/main.cpp @@ -0,0 +1,301 @@ +/* + * Copyright (c) 2023, kleines Filmröllchen + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum class UseObjectConstructor : bool { + No, + Yes, +}; + +// Classes whose header doesn't have the same name as the class. +static Optional map_class_to_file(StringView class_) +{ + static HashMap 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 map_property_to_type(StringView property) +{ + static HashMap 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 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 deprecated_string_properties; + if (deprecated_string_properties.is_empty()) { + deprecated_string_properties.set("name"sv); + } + return deprecated_string_properties.contains(property); +} + +static ErrorOr 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("", pathed_name); + + // We assume that all other paths are within the current application, for now. + return String::formatted("", pathed_name); +} + +// Each entry is an include path, without the "#include" itself. +static ErrorOr> extract_necessary_includes(GUI::GML::Object const& gml_hierarchy) +{ + HashTable 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 { + 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> @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 generate_initializer_for(Optional 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()) + return String::formatted("static_cast({})", value.as_integer()); + if (value.is_integer()) + return String::formatted("static_cast({})", value.as_integer()); + if (value.is_bool()) + return String::formatted("{}", value.as_bool()); + if (value.is_double()) + return String::formatted("static_cast({})", value.as_double()); + if (value.is_array()) { + auto const& array = value.as_array(); + auto child_type = Optional {}; + 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) + HANDLE_TYPE(u64, is_integer) + 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 variable named object_name. +// All loading happens in a separate block. +static ErrorOr 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 = [&](auto& generator, char const(&text)[N]) -> ErrorOr { + 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 value) -> ErrorOr { + 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 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 { + 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(@object_name@)->set_widget(*@child_variable_name@);")); + else if (gml_object.name() == "GUI::TabWidget"sv) + TRY(append(child_generator, "static_ptr_cast(@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 generate_cpp(NonnullRefPtr 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(""sv)), + TRY(String::from_utf8(""sv)), + TRY(String::from_utf8(""sv)), + TRY(String::from_utf8(""sv)), + TRY(String::from_utf8(""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 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; +} diff --git a/Userland/Libraries/LibGUI/GML/AST.h b/Userland/Libraries/LibGUI/GML/AST.h index 039002c144..7e62947116 100644 --- a/Userland/Libraries/LibGUI/GML/AST.h +++ b/Userland/Libraries/LibGUI/GML/AST.h @@ -188,6 +188,19 @@ public: } } + template> Callback> + ErrorOr try_for_each_property(Callback callback) const + { + for (auto const& child : m_properties) { + if (is(child)) { + auto const& property = static_cast(*child); + if (property.key() != "layout" && is(property.value().ptr())) + TRY(callback(property.key(), static_ptr_cast(property.value()))); + } + } + return {}; + } + template void for_each_child_object(Callback callback) const {