// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Compile-time introspection without P2996 (clang 23 has no ), taken // from Crafter.DB:Reflect. For aggregates: structured-binding packs give the // member count and per-member access, and std::source_location's // function_name() spells a member's name when a pointer to a member of an // extern object is a template argument. For scoped enums: function_name() // spells a template argument that is an enumerator by its name and any other // value as a cast, so probing a range of values finds every enumerator. // When a clang ships , this partition is the only thing to replace. export module Crafter.Debug.Core:Reflect; import std; namespace Crafter::Debug::Reflect { template struct CountHelper { template static consteval auto Count(U& u) { auto&& [...m] = u; return std::integral_constant{}; } static constexpr std::size_t value = decltype(Count(std::declval()))::value; }; // An instance whose member addresses are turned into text. Only its // address is used, in constant expressions; no_destroy keeps the compiler // from registering a destructor for it. template [[clang::no_destroy]] inline const T external{}; template consteval std::string_view RawName() { return std::source_location::current().function_name(); } template consteval std::string_view RawTypeName() { return std::source_location::current().function_name(); } // The part of RawName

()'s text that spells P: what follows "Probe = " // up to the closing bracket. consteval std::string_view ProbeText(std::string_view raw) { const std::size_t end = raw.rfind(']'); const std::size_t start = raw.rfind("Probe = ", end); return raw.substr(start + 8, end - start - 8); } // The enumerator name for V, or "" when V is not an enumerator: clang // spells those as a cast, "(Ns::Color)5". template consteval std::string_view EnumeratorName() { const std::string_view text = ProbeText(RawName()); if (text.starts_with('(')) return {}; const std::size_t colon = text.rfind("::"); return colon == std::string_view::npos ? text : text.substr(colon + 2); } } export namespace Crafter::Debug { // True for class types whose non-static data members a structured // binding can bind: public members, no bases carrying members, no // user-declared constructors. template concept Aggregate = std::is_class_v && std::is_aggregate_v && requires { Reflect::CountHelper::value; }; template inline constexpr std::size_t memberCount = Reflect::CountHelper::value; // The I-th member of an aggregate lvalue. template constexpr decltype(auto) Member(T&& t) noexcept { auto&& [...m] = t; return (m...[I]); } template using MemberType = std::remove_cvref_t(std::declval()))>; // The declared name of the I-th member: "yaw". Parsed out of // "std::string_view ...::RawName() [Probe = &external.yaw]". template consteval std::string_view MemberName() { constexpr std::string_view text = Reflect::ProbeText(Reflect::RawName<&Member(Reflect::external)>()); return text.substr(text.rfind('.') + 1); } // The type's name as the compiler spells it ("Game::AimArgs"), for help // and errors; the spelling is not load-bearing. template consteval std::string_view TypeName() { constexpr std::string_view raw = Reflect::RawTypeName(); const std::size_t end = raw.rfind(']'); const std::size_t start = raw.rfind("T = ", end); return raw.substr(start + 4, end - start - 4); } // Calls f.template operator()(member) for every member, in order. template constexpr void ForEachMember(T& t, F&& f) { [&](std::index_sequence) { (f.template operator()(Member(t)), ...); }(std::make_index_sequence>{}); } // The values probed for enumerator names, a half-open range clamped to // the enum's underlying type. Specialize for an enum with values outside // the default range; every probe is one template instantiation. template struct EnumRange { static constexpr std::int64_t min = -128; static constexpr std::int64_t max = 128; }; template struct Enumerator { E value; std::string_view name; }; } namespace Crafter::Debug::Reflect { template consteval std::int64_t ProbeMin() { using U = std::underlying_type_t; return std::max(EnumRange::min, static_cast(std::numeric_limits::min())); } template consteval std::int64_t ProbeMax() { using U = std::underlying_type_t; constexpr std::int64_t limit = static_cast(std::numeric_limits::max()) >= static_cast(std::numeric_limits::max()) ? std::numeric_limits::max() : static_cast(std::numeric_limits::max()) + 1; return std::min(EnumRange::max, limit); } template consteval auto EnumTable(std::index_sequence) { constexpr std::int64_t first = ProbeMin(); constexpr std::array names = { EnumeratorName(first + static_cast(I))>()... }; constexpr std::size_t count = static_cast(std::ranges::count_if(names, [](std::string_view name) { return !name.empty(); })); std::array, count> table{}; std::size_t next = 0; for (std::size_t i = 0; i < names.size(); ++i) { if (!names[i].empty()) table[next++] = { static_cast(first + static_cast(i)), names[i] }; } return table; } } export namespace Crafter::Debug { // Every named value of a scoped enum within EnumRange, in value order. // Scoped only: clang rejects casting an out-of-range value to an unscoped // enum without a fixed underlying type, even in a template argument. template requires std::is_scoped_enum_v inline constexpr auto enumerators = Reflect::EnumTable(std::make_index_sequence(Reflect::ProbeMax() - Reflect::ProbeMin())>{}); template requires std::is_scoped_enum_v constexpr std::optional EnumName(E value) { for (const Enumerator& e : enumerators) { if (e.value == value) return e.name; } return std::nullopt; } template requires std::is_scoped_enum_v constexpr std::optional EnumFromName(std::string_view name) { for (const Enumerator& e : enumerators) { if (e.name == name) return e.value; } return std::nullopt; } } // Self-tests: a compiler whose function_name() text changes shape fails // here, at the library's build, instead of producing garbage names. namespace Crafter::Debug::Reflect { struct ProbeStruct { std::int32_t alpha; double beta; std::string gamma; }; enum class ProbeEnum : std::uint8_t { Zero, One, Seven = 7 }; enum class ProbeSigned : std::int16_t { Minus = -3, Big = 100 }; static_assert(Aggregate); static_assert(memberCount == 3); static_assert(MemberName<0, ProbeStruct>() == "alpha"); static_assert(MemberName<2, ProbeStruct>() == "gamma"); static_assert(std::is_same_v, double>); static_assert(TypeName().ends_with("ProbeStruct")); static_assert(enumerators.size() == 3); static_assert(EnumName(ProbeEnum::Seven) == "Seven"); static_assert(!EnumName(static_cast(5)).has_value()); static_assert(EnumFromName("One") == ProbeEnum::One); static_assert(EnumName(ProbeSigned::Minus) == "Minus"); static_assert(enumerators.size() == 2); }