// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Compile-time introspection of aggregates without P2996 (clang 23 has no
// ). Three ingredients, all verified on the toolchain this library
// targets: structured-binding packs give member count and per-member access,
// std::source_location::function_name() leaks the member name when a pointer
// to a member of an extern object is used as a template argument, and address
// comparison on a constexpr instance maps a pointer-to-member to its index.
// When a clang ships , this partition is the only thing to replace.
export module Crafter.DB:Reflect;
import std;
namespace Crafter::DB::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;
};
// A constant-initialised instance whose member addresses are turned into
// text by function_name(). Only its address is ever used, in a constant
// expression, so nothing reads or writes it at run time; no_destroy keeps
// the compiler from registering a destructor for it, which lets the linker
// drop the object entirely. (An undefined `extern` declaration would be
// cheaper still, but clang warns about it at every instantiation.)
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();
}
}
export namespace Crafter::DB {
// True for class types whose non-static data members can be bound by a
// structured binding: public members, no bases carrying members, no
// user-declared constructors. Tables and nested column groups must be this.
template
concept Aggregate = std::is_aggregate_v && !std::is_array_v && requires(T& t) { Reflect::CountHelper::value; };
template
inline constexpr std::size_t memberCount = Reflect::CountHelper::value;
// Reference to 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, e.g. "iban". Parsed out of
// "std::string_view Crafter::DB::Reflect::RawName() [P = &external.iban]".
template
consteval std::string_view MemberName() {
constexpr std::string_view raw = Reflect::RawName<&Member(Reflect::external)>();
const std::size_t end = raw.rfind(']');
const std::size_t dot = raw.rfind('.', end);
return raw.substr(dot + 1, end - dot - 1);
}
// The type's qualified name as the compiler spells it ("Nostro::Transaction").
// Used in diagnostics only, so the exact 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 eq = raw.rfind("T = ", end);
return raw.substr(eq + 4, end - eq - 4);
}
template
struct MemberPointerTraits;
template
struct MemberPointerTraits {
using Class = C;
using Type = M;
};
template
using MemberClass = typename MemberPointerTraits::Class;
template
using MemberTypeOf = typename MemberPointerTraits::Type;
inline constexpr std::size_t noMember = static_cast(-1);
// Equality of two pointers-to-member that may point into different member
// types (comparing those directly is ill-formed): false unless the types
// match and the pointers are equal.
template
consteval bool SameMemberPointer() {
if constexpr (std::is_same_v, std::remove_cv_t>) return A == B;
else return false;
}
// Index of the member a pointer-to-member designates, or noMember. Works
// by comparing the address of each bound member with the address the
// pointer selects on the same constexpr instance.
template
consteval std::size_t MemberIndex() {
T t{};
std::size_t index = noMember;
[&](std::index_sequence) {
((void)[&] {
if constexpr (std::is_same_v, std::remove_cvref_t>) {
if (std::addressof(Member(t)) == std::addressof(t.*Ptr)) index = I;
}
}(), ...);
}(std::make_index_sequence>{});
return index;
}
// Runtime counterpart of MemberIndex for a pointer that is not a template
// argument. Every call site passes a literal `&T::member`, and the probe
// object is constant-initialised (no guard to check), so after inlining the
// comparison chain folds to a constant.
template
std::size_t MemberIndexOf(M T::* ptr) noexcept {
const T& instance = Reflect::external;
std::size_t index = noMember;
[&](std::index_sequence) {
((void)[&] {
if constexpr (std::is_same_v, M>) {
if (std::addressof(Member(instance)) == std::addressof(instance.*ptr)) index = I;
}
}(), ...);
}(std::make_index_sequence>{});
return index;
}
// 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>{});
}
}
// Self-tests: a compiler whose function_name() text changes shape fails here,
// at the library's build, instead of producing garbage column names.
namespace Crafter::DB::Reflect {
struct Probe {
std::int32_t alpha;
double beta;
std::string gamma;
std::chrono::system_clock::time_point delta;
};
static_assert(Aggregate);
static_assert(memberCount == 4);
static_assert(MemberName<0, Probe>() == "alpha");
static_assert(MemberName<2, Probe>() == "gamma");
static_assert(MemberName<3, Probe>() == "delta");
static_assert(MemberIndex() == 2);
static_assert(MemberIndex() == 3);
static_assert(std::is_same_v, double>);
static_assert(std::is_same_v, std::string>);
static_assert(TypeName().ends_with("Probe"));
}