Crafter.Debug/interfaces/Crafter.Debug.Core-Reflect.cppm

205 lines
8.4 KiB
Text
Raw Permalink Normal View History

feat(core): Value, reflection, codecs and results, shared by app and client Crafter.Debug.Core is what the app side and the crafter-debug client both need, without the session: - Value: a JSON value with its own parser and printer, so no consumer interface sees a JSON library. Integers stay integers and whole doubles stay doubles; NaN and the infinities, which JSON cannot spell, are written as strings rather than hidden, because a debugger must not lose a NaN; invalid UTF-8 is replaced so output always parses. Objects keep insertion order, the order an agent reads. - Reflect: aggregate members and names through structured-binding packs and source_location, taken from Crafter.DB, plus scoped-enum names by probing a range of values. Self-tested with static_asserts so a compiler that changes function_name()'s shape fails the build. - Codec<T>: encode, decode and describe for bool, integers, floating point, strings, enums, optional, vector, array, string-keyed maps, Value, Required<T> and every aggregate. Decode reports every problem in a call with its path and, for a typo, the likely intended name, and never throws: a bad call cannot take an app down, which a wrongly typed parameter could in 3DForts' hand-written from_json. - Result, Error and Async: handler results, including ones that take frames to arrive, so callers need no ready:false polling. project.cpp now builds Crafter.Debug.Core as its own library, used by both the app library and the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Compile-time introspection without P2996 (clang 23 has no <meta>), 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 <meta>, this partition is the only thing to replace.
export module Crafter.Debug.Core:Reflect;
import std;
namespace Crafter::Debug::Reflect {
template<class T>
struct CountHelper {
template<class U>
static consteval auto Count(U& u) {
auto&& [...m] = u;
return std::integral_constant<std::size_t, sizeof...(m)>{};
}
static constexpr std::size_t value = decltype(Count(std::declval<T&>()))::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<class T>
[[clang::no_destroy]] inline const T external{};
template<auto Probe>
consteval std::string_view RawName() {
return std::source_location::current().function_name();
}
template<class T>
consteval std::string_view RawTypeName() {
return std::source_location::current().function_name();
}
// The part of RawName<P>()'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<auto V>
consteval std::string_view EnumeratorName() {
const std::string_view text = ProbeText(RawName<V>());
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<class T>
concept Aggregate = std::is_class_v<T> && std::is_aggregate_v<T> && requires { Reflect::CountHelper<T>::value; };
template<Aggregate T>
inline constexpr std::size_t memberCount = Reflect::CountHelper<T>::value;
// The I-th member of an aggregate lvalue.
template<std::size_t I, class T>
constexpr decltype(auto) Member(T&& t) noexcept {
auto&& [...m] = t;
return (m...[I]);
}
template<std::size_t I, class T>
using MemberType = std::remove_cvref_t<decltype(Member<I>(std::declval<T&>()))>;
// The declared name of the I-th member: "yaw". Parsed out of
// "std::string_view ...::RawName() [Probe = &external.yaw]".
template<std::size_t I, Aggregate T>
consteval std::string_view MemberName() {
constexpr std::string_view text = Reflect::ProbeText(Reflect::RawName<&Member<I>(Reflect::external<T>)>());
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<class T>
consteval std::string_view TypeName() {
constexpr std::string_view raw = Reflect::RawTypeName<T>();
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()<I>(member) for every member, in order.
template<Aggregate T, class F>
constexpr void ForEachMember(T& t, F&& f) {
[&]<std::size_t... I>(std::index_sequence<I...>) {
(f.template operator()<I>(Member<I>(t)), ...);
}(std::make_index_sequence<memberCount<T>>{});
}
// 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<class E>
struct EnumRange {
static constexpr std::int64_t min = -128;
static constexpr std::int64_t max = 128;
};
template<class E>
struct Enumerator {
E value;
std::string_view name;
};
}
namespace Crafter::Debug::Reflect {
template<class E>
consteval std::int64_t ProbeMin() {
using U = std::underlying_type_t<E>;
return std::max<std::int64_t>(EnumRange<E>::min, static_cast<std::int64_t>(std::numeric_limits<U>::min()));
}
template<class E>
consteval std::int64_t ProbeMax() {
using U = std::underlying_type_t<E>;
constexpr std::int64_t limit = static_cast<std::uint64_t>(std::numeric_limits<U>::max()) >= static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())
? std::numeric_limits<std::int64_t>::max()
: static_cast<std::int64_t>(std::numeric_limits<U>::max()) + 1;
return std::min<std::int64_t>(EnumRange<E>::max, limit);
}
template<class E, std::size_t... I>
consteval auto EnumTable(std::index_sequence<I...>) {
constexpr std::int64_t first = ProbeMin<E>();
constexpr std::array<std::string_view, sizeof...(I)> names = {
EnumeratorName<static_cast<E>(first + static_cast<std::int64_t>(I))>()...
};
constexpr std::size_t count = static_cast<std::size_t>(std::ranges::count_if(names, [](std::string_view name) { return !name.empty(); }));
std::array<Enumerator<E>, 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<E>(first + static_cast<std::int64_t>(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<class E>
requires std::is_scoped_enum_v<E>
inline constexpr auto enumerators = Reflect::EnumTable<E>(std::make_index_sequence<static_cast<std::size_t>(Reflect::ProbeMax<E>() - Reflect::ProbeMin<E>())>{});
template<class E>
requires std::is_scoped_enum_v<E>
constexpr std::optional<std::string_view> EnumName(E value) {
for (const Enumerator<E>& e : enumerators<E>) {
if (e.value == value) return e.name;
}
return std::nullopt;
}
template<class E>
requires std::is_scoped_enum_v<E>
constexpr std::optional<E> EnumFromName(std::string_view name) {
for (const Enumerator<E>& e : enumerators<E>) {
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<ProbeStruct>);
static_assert(memberCount<ProbeStruct> == 3);
static_assert(MemberName<0, ProbeStruct>() == "alpha");
static_assert(MemberName<2, ProbeStruct>() == "gamma");
static_assert(std::is_same_v<MemberType<1, ProbeStruct>, double>);
static_assert(TypeName<ProbeStruct>().ends_with("ProbeStruct"));
static_assert(enumerators<ProbeEnum>.size() == 3);
static_assert(EnumName(ProbeEnum::Seven) == "Seven");
static_assert(!EnumName(static_cast<ProbeEnum>(5)).has_value());
static_assert(EnumFromName<ProbeEnum>("One") == ProbeEnum::One);
static_assert(EnumName(ProbeSigned::Minus) == "Minus");
static_assert(enumerators<ProbeSigned>.size() == 2);
}