diff --git a/CMakeLists.txt b/CMakeLists.txt index 766f1c7..51233a9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,11 +36,21 @@ FetchContent_Declare( SOURCE_SUBDIR llvm ) +FetchContent_Declare( + diff + GIT_REPOSITORY https://github.com/fosterbrereton/diff.git + GIT_TAG 2ba1687a30de266416caa141f8be408e72843be0 + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE + SOURCE_SUBDIR diff +) + set(LLVM_ENABLE_PROJECTS "clang") set(LLVM_TARGETS_TO_BUILD "X86;AArch64") set(LLVM_ENABLE_ZSTD OFF) FetchContent_MakeAvailable(llvm) +FetchContent_MakeAvailable(diff) message(STATUS "INFO: LLVM source dir: ${llvm_SOURCE_DIR}") message(STATUS "INFO: LLVM binary dir: ${llvm_BINARY_DIR}") @@ -127,6 +137,7 @@ target_include_directories(hyde ${llvm_BINARY_DIR}/tools/clang/include ${llvm_SOURCE_DIR}/llvm/include ${llvm_BINARY_DIR}/include + ${diff_SOURCE_DIR}/include ) target_compile_options(hyde diff --git a/emitters/yaml_base_emitter.cpp b/emitters/yaml_base_emitter.cpp index ef9cd0f..d886e2c 100644 --- a/emitters/yaml_base_emitter.cpp +++ b/emitters/yaml_base_emitter.cpp @@ -260,6 +260,7 @@ json yaml_base_emitter::base_emitter_node(std::string layout, node["hyde"]["owner"] = default_tag_value; node["hyde"]["tags"].emplace_back(std::move(tag)); node["hyde"]["brief"] = default_tag_value; + node["hyde"]["version"] = hyde_version(); return node; } @@ -392,6 +393,7 @@ void yaml_base_emitter::check_notify(const std::string& filepath, std::cerr << filepath << "@" << escaped_nodepath << "['" << escaped_key << "']: " << validate_message << "\n"; } break; + case yaml_mode::transcribe: case yaml_mode::update: { std::cout << filepath << "@" << escaped_nodepath << "['" << escaped_key << "']: " << update_message << "\n"; @@ -980,6 +982,78 @@ bool yaml_base_emitter::check_object_array(const std::string& filepath, /**************************************************************************************************/ +std::vector object_keys(const json& j) { + std::vector result; + + for (auto iter{j.begin()}, last{j.end()}; iter != last; ++iter) { + result.push_back(static_cast(iter.key())); + } + + return result; +} + +template +inline void move_append(T& dst, T&& src) { + dst.insert(dst.end(), std::make_move_iterator(src.begin()), std::make_move_iterator(src.end())); +} + +struct transcribe_pair { + std::string src; + std::string dst; +}; + +using transcribe_pairs = std::vector; + +// This is O(N^2), where N is the size of both `src` and `dst`. Therefore transcription +// should only be run when it is shown to be necessary. At the same time, if your code base +// has enough overrides to really slow this algorithm down, hyde's performance is the least +// of your concerns. +transcribe_pairs derive_transcribe_pairs(const json& src, const json& dst) { + std::vector src_keys = object_keys(src); + std::vector dst_keys = object_keys(dst); + + if (src_keys.size() != dst_keys.size()) { + std::cerr << "WARNING: transcription key count mismatch\n"; + } + + transcribe_pairs result; + + while (!src_keys.empty()) { + transcribe_pair cur_pair; + + // pop a key off the old name set + cur_pair.src = std::move(src_keys.back()); + src_keys.pop_back(); + + // find the best match of the dst keys to the src key + std::size_t best_match = std::numeric_limits::max(); + std::size_t best_index = 0; + for (std::size_t i = 0; i < dst_keys.size(); ++i) { + // generate the diff score of the src key and the candidate dst + std::size_t cur_match = diff_score(cur_pair.src, dst_keys[i]); + + if (cur_match > best_match) { + continue; + } + + // if this dst candidate is better than what we've seen, remember that. + best_match = cur_match; + best_index = i; + } + + // pair the best match dst and src keys and remove dst + cur_pair.dst = std::move(dst_keys[best_index]); + dst_keys.erase(dst_keys.begin() + best_index); + + // save off the pair and repeat + result.emplace_back(std::move(cur_pair)); + } + + return result; +} + +/**************************************************************************************************/ + bool yaml_base_emitter::check_map(const std::string& filepath, const json& have_node, const json& expected_node, @@ -1013,38 +1087,68 @@ bool yaml_base_emitter::check_map(const std::string& filepath, } const json& have = have_node[key]; + bool failure{false}; + json result_map; - std::vector keys; - - for (auto iter{have.begin()}, last{have.end()}; iter != last; ++iter) { - keys.push_back(static_cast(iter.key())); - } - for (auto iter{expected.begin()}, last{expected.end()}; iter != last; ++iter) { - keys.push_back(static_cast(iter.key())); - } + if (key == "overloads" && _mode == yaml_mode::transcribe) { + /* + It is common during the upgrade from one version of hyde to another that the underlying + clang tooling will output different symbol names for a given symbol (e.g., a namespace + may get removed or added.) Although the symbol is unchanged, because its `expected` name + differs from the `have` name, hyde will consider the symbols different, remove the old name + and insert the new one. This wipes out any previous documentation under the old name that + should have been migrated to the new name. + + The solution here is very specialized. For the "overloads" key only, we gather the name + of each overload in both the `have` and `expected` set. We then pair them up according + to how well they match to one another (using the Meyers' string diff algorithm; two strings + with less "patchwork" between them are considered a better match). Ideally this results in + key pairs that represent the same symbol, just with different names. Then we call the + `proc` with `have[old_name]` and `expected[new_name]` which will migrate any documentation + from the old name to the new. + + This capability assumes the overload count of both `have` and `expected` are the same. + If any new functions are created or removed between upgrades in the clang driver (e.g., + a new compiler-generated routine is created and documented) that will have to be managed + manually. Assuming the count is the same, it also assumes there is a 1:1 mapping from the + set of old names to the set of new names. This implies the transcription mode should be + done as a separate step from an update. In other words, a transcription assumes the + documentation is actually the same between the `have` and `expected` sets, it is _just the + overload names_ that have changed, so map the old-named documentation to the new-named + documentation as reasonably as possible. + */ + for (const auto& pair : derive_transcribe_pairs(have, expected)) { + const std::string curnodepath = nodepath + "['" + pair.dst + "']"; + failure |= proc(filepath, have[pair.src], expected[pair.dst], curnodepath, + result_map[pair.dst]); + } + } else { + std::vector keys; - std::sort(keys.begin(), keys.end()); - keys.erase(std::unique(keys.begin(), keys.end()), keys.end()); + move_append(keys, object_keys(have)); + move_append(keys, object_keys(expected)); - bool failure{false}; + std::sort(keys.begin(), keys.end()); + keys.erase(std::unique(keys.begin(), keys.end()), keys.end()); - json result_map; - for (const auto& subkey : keys) { - std::string curnodepath = nodepath + "['" + subkey + "']"; + for (const auto& subkey : keys) { + const std::string curnodepath = nodepath + "['" + subkey + "']"; - if (!expected.count(subkey)) { - // Issue #75: only remove non-root keys to allow non-hyde YAML into the file. - if (!at_root) { - notify("extraneous map key: `" + subkey + "`", "map key removed: `" + subkey + "`"); + if (!expected.count(subkey)) { + // Issue #75: only remove non-root keys to allow non-hyde YAML into the file. + if (!at_root) { + notify("extraneous map key: `" + subkey + "`", + "map key removed: `" + subkey + "`"); + failure = true; + } + } else if (!have.count(subkey)) { + notify("map key missing: `" + subkey + "`", "map key inserted: `" + subkey + "`"); + result_map[subkey] = expected[subkey]; failure = true; + } else { + failure |= + proc(filepath, have[subkey], expected[subkey], curnodepath, result_map[subkey]); } - } else if (!have.count(subkey)) { - notify("map key missing: `" + subkey + "`", "map key inserted: `" + subkey + "`"); - result_map[subkey] = expected[subkey]; - failure = true; - } else { - failure |= - proc(filepath, have[subkey], expected[subkey], curnodepath, result_map[subkey]); } } @@ -1103,6 +1207,24 @@ std::pair yaml_base_emitter::merge(const std::string& filepath, check_editable_scalar(filepath, have_hyde, expected_hyde, "", merged_hyde, "brief"); failure |= check_scalar_array(filepath, have_hyde, expected_hyde, "", merged_hyde, "tags"); + // We don't want to use `check_scalar` on the version key. If the versions mismatch its not + // necessarily a validation error (as the docs may match OK), but something we want to warn + // about. Then in transcription/update we want to hard-set the value to the version of this + // tool. + + switch (_mode) { + case yaml_mode::validate: { + if (!have_hyde.count("version") || + static_cast(have_hyde["version"]) != hyde_version()) { + std::cerr << "INFO: Validation phase with a mismatched version of hyde. Consider updating then/or transcribing.\n"; + } + } break; + case yaml_mode::update: + case yaml_mode::transcribe: { + merged_hyde["version"] = hyde_version(); + } break; + } + failure |= do_merge(filepath, have_hyde, expected_hyde, merged_hyde); } @@ -1264,7 +1386,7 @@ documentation parse_documentation(const std::filesystem::path& path, bool fixup_ const auto front_matter_end = contents_end + front_matter_end_k.size(); std::string yaml_src = have_contents.substr(0, front_matter_end); have_contents.erase(0, front_matter_end); - + result._remainder = std::move(have_contents); result._json = yaml_to_json(load_yaml(path)); @@ -1342,6 +1464,7 @@ bool yaml_base_emitter::reconcile(json expected, case hyde::yaml_mode::validate: { // do nothing } break; + case hyde::yaml_mode::transcribe: case hyde::yaml_mode::update: { failure = write_documentation({std::move(merged), std::move(remainder)}, path); } break; @@ -1354,6 +1477,7 @@ bool yaml_base_emitter::reconcile(json expected, std::cerr << relative_path << ": required file does not exist\n"; failure = true; } break; + case hyde::yaml_mode::transcribe: case hyde::yaml_mode::update: { // Add update. No remainder yet, as above. // REVISIT: Refactor all this into a call to write_documentation, diff --git a/emitters/yaml_class_emitter.cpp b/emitters/yaml_class_emitter.cpp index f8fd7ea..7f3ab10 100644 --- a/emitters/yaml_class_emitter.cpp +++ b/emitters/yaml_class_emitter.cpp @@ -17,6 +17,7 @@ written permission of Adobe. // application #include "emitters/yaml_function_emitter.hpp" +#include "matchers/utilities.hpp" /**************************************************************************************************/ @@ -108,19 +109,32 @@ bool yaml_class_emitter::emit(const json& j, json& out_emitted, const json& inhe auto dst = dst_path(j, static_cast(j["name"])); + if (_mode == yaml_mode::transcribe && !exists(dst)) { + // In this case the symbol name has changed, which has caused a change to the directory name + // we are now trying to load and reconcile with what we've created. In this case, we can + // assume the "shape" of the documentation is the same, which means that within the parent + // folder of `dst` is the actual source folder that holds the old documentation, just under + // a different name. Find that folder and rename it. + + std::filesystem::rename(derive_transcription_src_path(dst, node["title"]), dst); + } + bool failure = reconcile(std::move(node), _dst_root, std::move(dst) / index_filename_k, out_emitted); - const auto& methods = j["methods"]; yaml_function_emitter function_emitter(_src_root, _dst_root, _mode, _options, true); + auto emitted_methods = hyde::json::array(); + const auto& methods = j["methods"]; for (auto it = methods.begin(); it != methods.end(); ++it) { function_emitter.set_key(it.key()); auto function_emitted = hyde::json::object(); failure |= function_emitter.emit(it.value(), function_emitted, out_emitted.at("hyde")); - out_emitted["methods"].push_back(std::move(function_emitted)); + emitted_methods.push_back(std::move(function_emitted)); } + out_emitted["methods"] = std::move(emitted_methods); + return failure; } diff --git a/emitters/yaml_function_emitter.cpp b/emitters/yaml_function_emitter.cpp index f195552..d81f7f5 100644 --- a/emitters/yaml_function_emitter.cpp +++ b/emitters/yaml_function_emitter.cpp @@ -15,6 +15,9 @@ written permission of Adobe. // stdc++ #include +// application +#include "matchers/utilities.hpp" + /**************************************************************************************************/ namespace hyde { @@ -256,6 +259,16 @@ bool yaml_function_emitter::emit(const json& jsn, json& out_emitted, const json& if (is_ctor) node["hyde"]["is_ctor"] = true; if (is_dtor) node["hyde"]["is_dtor"] = true; + if (_mode == yaml_mode::transcribe && !exists(dst)) { + // In this case the symbol name has changed, which has caused a change to the directory name + // we are now trying to load and reconcile with what we've created. In this case, we can + // assume the "shape" of the documentation is the same, which means that within the parent + // folder of `dst` is the actual source folder that holds the old documentation, just under + // a different name. Find that folder and rename it. + + std::filesystem::rename(derive_transcription_src_path(dst, node["title"]), dst); + } + return reconcile(std::move(node), _dst_root, dst / (filename + ".md"), out_emitted); } diff --git a/include/output_yaml.hpp b/include/output_yaml.hpp index bf48e82..68b8bdd 100644 --- a/include/output_yaml.hpp +++ b/include/output_yaml.hpp @@ -25,8 +25,9 @@ namespace hyde { /**************************************************************************************************/ enum class yaml_mode { - validate, - update, + validate, // ensure the destination docs match the shape of the generated docs + update, // update the destination docs to match the shape of the generated docs + transcribe // transcribe the destination docs to match the symbols put out by upgraded tooling }; /**************************************************************************************************/ diff --git a/matchers/class_matcher.cpp b/matchers/class_matcher.cpp index e217423..3627d0d 100644 --- a/matchers/class_matcher.cpp +++ b/matchers/class_matcher.cpp @@ -96,6 +96,39 @@ namespace hyde { /**************************************************************************************************/ +json fixup_short_name(json&& method) { + // We have encountered cases with the latest clang drivers where the short_name field for some + // methods is missing. In such case we try to cobble up a solution by finding the + // first sequence of alphanumeric characters after the return type. If _that_ doesn't work, + // then we fall back on the signature name. + if (method.count("short_name") && + method.at("short_name").is_string() && + !static_cast(method.at("short_name")).empty()) { + return std::move(method); + } + + std::string short_name = method["signature"]; + + if (method.count("return_type") && method["return_type"].is_string() && + method.count("signature") && method["signature"].is_string()) { + const std::string& return_type = method["return_type"]; + const std::string& signature = method["signature"]; + const auto offset = signature.find(return_type); + if (offset != std::string::npos) { + const auto start = offset + return_type.size() + 1; + const auto end = signature.find_first_of("(", start); + short_name = signature.substr(start, end - start); + + } + } + + method["short_name"] = std::move(short_name); + + return std::move(method); +} + +/**************************************************************************************************/ + void ClassInfo::run(const MatchFinder::MatchResult& Result) { auto clas = Result.Nodes.getNodeAs("class"); @@ -138,8 +171,9 @@ void ClassInfo::run(const MatchFinder::MatchResult& Result) { auto methodInfo_opt = DetailFunctionDecl(_options, method); if (!methodInfo_opt) continue; auto methodInfo = std::move(*methodInfo_opt); - info["methods"][static_cast(methodInfo["short_name"])].push_back( - std::move(methodInfo)); + methodInfo = fixup_short_name(std::move(methodInfo)); + const auto& short_name = static_cast(methodInfo["short_name"]); + info["methods"][short_name].push_back(std::move(methodInfo)); } for (const auto& decl : clas->decls()) { @@ -149,8 +183,9 @@ void ClassInfo::run(const MatchFinder::MatchResult& Result) { DetailFunctionDecl(_options, function_template_decl->getTemplatedDecl()); if (!methodInfo_opt) continue; auto methodInfo = std::move(*methodInfo_opt); - info["methods"][static_cast(methodInfo["short_name"])].push_back( - std::move(methodInfo)); + methodInfo = fixup_short_name(std::move(methodInfo)); + const auto& short_name = static_cast(methodInfo["short_name"]); + info["methods"][short_name].push_back(std::move(methodInfo)); } for (const auto& field : clas->fields()) { diff --git a/matchers/utilities.cpp b/matchers/utilities.cpp index 75e9fbe..6d8c4c2 100644 --- a/matchers/utilities.cpp +++ b/matchers/utilities.cpp @@ -31,7 +31,11 @@ written permission of Adobe. #include "_clang_include_suffix.hpp" // must be last to re-enable warnings // clang-format on +// diff +#include "diff/myers.hpp" + // application +#include "emitters/yaml_base_emitter_fwd.hpp" #include "json.hpp" using namespace clang; @@ -973,6 +977,104 @@ hyde::optional_json ProcessComments(const Decl* d) { /**************************************************************************************************/ +constexpr auto hyde_version_major_k = 2; +constexpr auto hyde_version_minor_k = 1; +constexpr auto hyde_version_patch_k = 0; + +const std::string& hyde_version() { + static const std::string result = std::to_string(hyde_version_major_k) + + "." + std::to_string(hyde_version_minor_k) + + "." + std::to_string(hyde_version_patch_k); + return result; +} + +/**************************************************************************************************/ + +std::filesystem::path derive_transcription_src_path(const std::filesystem::path& dst, + const std::string& title) { + // std::cout << "deriving transcription src for \"" << title << "\", dst: " << dst.string() << '\n'; + + const auto parent = dst.parent_path(); + std::size_t best_match = std::numeric_limits::max(); + std::filesystem::path result; + const std::string& current_version = hyde_version(); + + for (const auto& entry : std::filesystem::directory_iterator(dst.parent_path())) { + const auto sibling = entry.path(); + if (!is_directory(sibling)) continue; + const auto index_path = sibling / index_filename_k; + + if (!exists(index_path)) { + std::cerr << "WARN: expected " << index_path.string() << " but did not find one\n"; + continue; + } + + const auto have_docs = parse_documentation(index_path, true); + + if (have_docs._error) { + std::cerr << "WARN: expected " << index_path.string() << " to have docs\n"; + continue; + } + + const auto& have = have_docs._json; + + if (have.count("hyde")) { + const auto& have_hyde = have.at("hyde"); + if (have_hyde.count("version")) { + // Transcription is when we're going from a previous version of hyde to this one. + // So if the versions match, this is a directory that has already been transcribed. + // (Transcribing from a newer version of hyde docs to older ones isn't supported.) + if (static_cast(have_hyde.at("version")) == current_version) { + // std::cout << " candidate (VSKIP) src: " << sibling.string() << '\n'; + continue; + } + } + } + + // REVISIT (fosterbrereton): Are these titles editable? Would + // users muck with them and thus break this algorithm? + const std::string& have_title = static_cast(have["title"]); + + // score going from what we have to what this version computed. + const auto match = diff_score(have_title, title); + + // std::cout << " candidate (" << match << "): \"" << have_title << "\", src: " << sibling.string() << '\n'; + + if (match > best_match) { + continue; + } + + best_match = match; + result = sibling; + } + + // std::cout << " result is: " << result.string() << " (" << best_match << ")\n"; + + return result; +} + +/**************************************************************************************************/ + +std::size_t diff_score(std::string_view src, std::string_view dst) { + const myers::patch patch = myers::diff(src, dst); + std::size_t score = 0; + + for (const auto& c : patch) { + switch (c.operation) { + case myers::operation::cpy: + break; + case myers::operation::del: + case myers::operation::ins: { + score += c.text.size(); + } break; + } + } + + return score; +} + +/**************************************************************************************************/ + } // namespace hyde /**************************************************************************************************/ diff --git a/matchers/utilities.hpp b/matchers/utilities.hpp index 8e68cd1..1844d86 100644 --- a/matchers/utilities.hpp +++ b/matchers/utilities.hpp @@ -11,6 +11,9 @@ written permission of Adobe. #pragma once +// stdc++ +#include + // clang/llvm // clang-format off #include "_clang_include_prefix.hpp" // must be first to disable warnings for clang headers @@ -62,6 +65,21 @@ optional_json ProcessComments(const clang::Decl* d); /**************************************************************************************************/ +const std::string& hyde_version(); + +/**************************************************************************************************/ + +/// compute a value (based on a diff algorithm) to determine roughly how much two strings are like +/// each other. The lower the value the better, with 0 meaning the two strings are identical. +std::size_t diff_score(std::string_view src, std::string_view dst); + +// Iterate the list of `dst` subfolders, find their `index.md` files, load the `title` of each, and +// find the best-fit against the given `title`. This facilitates the transcription behavior. +std::filesystem::path derive_transcription_src_path(const std::filesystem::path& dst, + const std::string& title); + +/**************************************************************************************************/ + inline std::string to_string(clang::AccessSpecifier access) { switch (access) { case clang::AccessSpecifier::AS_public: diff --git a/sources/main.cpp b/sources/main.cpp index 5413616..4ef4506 100644 --- a/sources/main.cpp +++ b/sources/main.cpp @@ -43,6 +43,7 @@ written permission of Adobe. #include "matchers/namespace_matcher.hpp" #include "matchers/typealias_matcher.hpp" #include "matchers/typedef_matcher.hpp" +#include "matchers/utilities.hpp" using namespace clang::tooling; using namespace llvm; @@ -91,10 +92,10 @@ std::vector make_absolute(std::vector paths) { Command line arguments section. These are intentionally global. See: https://llvm.org/docs/CommandLine.html */ -enum ToolMode { ToolModeJSON, ToolModeYAMLValidate, ToolModeYAMLUpdate, ToolModeFixupSubfield }; +enum ToolMode { ToolModeJSON, ToolModeYAMLValidate, ToolModeYAMLUpdate, ToolModeYAMLTranscribe, ToolModeFixupSubfield }; enum ToolDiagnostic { ToolDiagnosticQuiet, ToolDiagnosticVerbose, ToolDiagnosticVeryVerbose }; static llvm::cl::OptionCategory MyToolCategory( - "Hyde is a tool to scan library headers to ensure documentation is kept up to date"); + "Hyde is a tool for the semi-automatic maintenance of API reference documentation"); static cl::opt ToolMode( cl::desc("There are several modes under which the tool can run:"), cl::values( @@ -105,7 +106,10 @@ static cl::opt ToolMode( "Write updated YAML documentation for missing elements"), clEnumValN(ToolModeFixupSubfield, "hyde-fixup-subfield", - "Fix-up preexisting documentation; move all fields except `layout` and `title` into a `hyde` subfield. Note this mode is unique in that it takes pre-existing documentation as source(s), not a C++ source file.") + "Fix-up preexisting documentation; move all fields except `layout` and `title` into a `hyde` subfield. Note this mode is unique in that it takes pre-existing documentation as source(s), not a C++ source file."), + clEnumValN(ToolModeYAMLTranscribe, + "hyde-transcribe", + "Transcribe preexisting documentation given the same symbols have different names because hyde updated its clang driver. This mode assumes the generated and present documentation would otherwise be identical.") ), cl::cat(MyToolCategory)); static cl::opt ToolAccessFilter( @@ -426,18 +430,6 @@ bool fixup_have_file_subfield(const std::filesystem::path& path) { /**************************************************************************************************/ -constexpr auto hyde_version_major_k = 2; -constexpr auto hyde_version_minor_k = 0; -constexpr auto hyde_version_patch_k = 2; - -auto hyde_version() { - return std::to_string(hyde_version_major_k) + - "." + std::to_string(hyde_version_minor_k) + - "." + std::to_string(hyde_version_patch_k); -} - -/**************************************************************************************************/ - } // namespace /**************************************************************************************************/ @@ -450,7 +442,7 @@ std::vector source_paths(int argc, const char** argv) { int main(int argc, const char** argv) try { llvm::cl::SetVersionPrinter([](llvm::raw_ostream &OS) { - OS << "hyde " << hyde_version() << "; llvm " << LLVM_VERSION_STRING << "\n"; + OS << "hyde " << hyde::hyde_version() << "; llvm " << LLVM_VERSION_STRING << "\n"; }); command_line_args args = integrate_hyde_config(argc, argv); @@ -687,10 +679,18 @@ int main(int argc, const char** argv) try { emit_options._tested_by = TestedBy; emit_options._ignore_extraneous_files = IgnoreExtraneousFiles; + const auto yaml_mode = [&]{ + switch (ToolMode) { + case ToolModeYAMLValidate: return hyde::yaml_mode::validate; + case ToolModeYAMLUpdate: return hyde::yaml_mode::update; + case ToolModeYAMLTranscribe: return hyde::yaml_mode::transcribe; + default: throw std::runtime_error("Invalid YAML mode"); + } + }(); + auto out_emitted = hyde::json::object(); output_yaml(std::move(result), std::move(src_root), std::move(dst_root), out_emitted, - ToolMode == ToolModeYAMLValidate ? hyde::yaml_mode::validate : - hyde::yaml_mode::update, std::move(emit_options)); + yaml_mode, std::move(emit_options)); if (EmitJson) { std::cout << out_emitted << '\n';