diff --git a/.gitignore b/.gitignore index d4fb281..37fe30e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,41 +1,12 @@ -# Prerequisites -*.d +build*/ +.vix/ +.cache/ -# Compiled Object files -*.slo -*.lo *.o *.obj - -# Precompiled Headers -*.gch -*.pch - -# Linker files -*.ilk - -# Debugger Files -*.pdb - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la *.a *.lib - -# Executables +*.so +*.dll +*.dylib *.exe -*.out -*.app - -# debug information files -*.dwo diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8f640f0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.20) + +project(rix_debug + VERSION 0.1.0 + DESCRIPTION "Debug printing, formatting, logging, and inspection utilities for Rix" + LANGUAGES CXX +) + +option(RIX_DEBUG_BUILD_EXAMPLES "Build rix/debug examples" ON) +option(RIX_DEBUG_BUILD_TESTS "Build rix/debug tests" ON) + +add_library(rix_debug INTERFACE) +add_library(rix::debug ALIAS rix_debug) + +target_compile_features(rix_debug INTERFACE cxx_std_20) + +target_include_directories(rix_debug + INTERFACE + $ + $ +) + +if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + if(RIX_DEBUG_BUILD_EXAMPLES) + add_subdirectory(examples) + endif() + + if(RIX_DEBUG_BUILD_TESTS) + enable_testing() + add_subdirectory(tests) + endif() +endif() diff --git a/README.md b/README.md index 180f257..8c4199e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,208 @@ -# rix-thread -Thread and synchronization utilities: thread creation, mutex, lock, and condition variables. +# @rix/debug + +Debug utilities for Rix. + +`@rix/debug` provides a small set of developer-focused tools for C++ projects: + +- formatted strings +- simple logging +- value inspection +- print helpers + +It can be used as an independent Rix package, or through the unified `@rix/rix` facade. + +## Installation + +```bash +vix add @rix/debug +vix install +``` + +## Basic usage + +```cpp +#include + +int main() +{ + rixlib::debug::Debug debug{}; + + debug.print("Hello", "Rix"); + + auto text = debug.format("Package: {}", "rix/debug"); + + debug.log("loaded {} rows", 3); + debug.log.warn("slow request: {}ms", 120); + + debug.inspect(text); + + return 0; +} +``` + +Output: + +```txt +Hello Rix +[debug] loaded 3 rows +[warn] slow request: 120ms +Package: rix/debug +``` + +## Formatting + +`debug.format` supports simple placeholder-based formatting. + +```cpp +auto message = debug.format("Hello {}", "Rix"); +auto result = debug.format("{0} + {0} = {1}", 2, 4); +auto escaped = debug.format("{{ value }} = {}", 42); +``` + +Supported placeholders: + +```txt +{} automatic argument indexing +{0} explicit positional indexing +{{ escaped opening brace +}} escaped closing brace +``` + +Format specifiers such as `{:>10}` or `{:.2f}` are intentionally not supported. + +## Logging + +```cpp +debug.log("loaded {} rows", 3); +debug.log.info("server started"); +debug.log.warn("slow request: {}ms", 120); +debug.log.error("failed: {}", "timeout"); +``` + +Output: + +```txt +[debug] loaded 3 rows +[info] server started +[warn] slow request: 120ms +[error] failed: timeout +``` + +## Printing + +`debug.print` is not a formatter. + +It prints values separated by spaces. + +```cpp +debug.print("Hello", "Rix"); +debug.print(1, 2, 3); +``` + +Output: + +```txt +Hello Rix +1 2 3 +``` + +For formatted strings, use `debug.format` or `debug.log`. + +```cpp +debug.print(debug.format("Hello {}", "Rix")); +``` + +## Inspection + +```cpp +debug.inspect(42); + +auto value = debug.inspect.to_string(true); + +bool ok = debug.inspect.check(42, 42); +``` + +Inspection is useful for debugging values, containers, optional values, variants, and other supported C++ types. + +## Independent API + +You can also use the free functions directly. + +```cpp +#include + +int main() +{ + rixlib::print("Hello", "Rix"); + + auto text = rixlib::format("Package: {}", "rix/debug"); + + rixlib::inspect(text); + + return 0; +} +``` + +## Unified Rix facade + +When used through `@rix/rix`, the debug package is mounted under `rix.debug`. + +```cpp +#include + +int main() +{ + rix.debug.print("Hello", "Rix"); + rix.debug.log("loaded {} rows", 3); + + return 0; +} +``` + +## Design + +`@rix/debug` is intentionally small and focused. + +It does not try to replace full logging frameworks or formatting libraries. Its role is to provide simple debugging primitives that are easy to use in Rix-based C++ projects. + +The package exposes two layers: + +```txt +rixlib::format(...) +rixlib::print(...) +rixlib::inspect(...) +``` + +and the object-style API: + +```txt +rixlib::debug::Debug +``` + +This keeps the package usable on its own while allowing the unified Rix facade to mount it cleanly as: + +```txt +rix.debug +``` + +## Build + +```bash +vix build +``` + +## Run example + +```bash +vix run +``` + +## Tests + +```bash +vix tests +``` + +## License + +MIT diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..5003d17 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,8 @@ +add_executable(rix_debug_basic + basic.cpp +) + +target_link_libraries(rix_debug_basic + PRIVATE + rix::debug +) diff --git a/examples/basic.cpp b/examples/basic.cpp new file mode 100644 index 0000000..08b5fc4 --- /dev/null +++ b/examples/basic.cpp @@ -0,0 +1,22 @@ +/** + * @file basic.cpp + * @brief Basic example for rix/debug. + */ + +#include + +int main() +{ + rixlib::debug::Debug debug{}; + + debug.print("Hello", "Rix"); + + auto text = debug.format("Package: {}", "rix/debug"); + + debug.log("loaded {} rows", 3); + debug.log.warn("slow request: {}ms", 120); + + debug.inspect(text); + + return 0; +} diff --git a/include/rix/debug.hpp b/include/rix/debug.hpp new file mode 100644 index 0000000..2aae944 --- /dev/null +++ b/include/rix/debug.hpp @@ -0,0 +1,77 @@ +/** + * @file debug.hpp + * @brief Unified debug module for Rix. + */ + +#ifndef RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_HPP_INCLUDED +#define RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_HPP_INCLUDED + +#include + +#include +#include +#include +#include + +namespace rixlib::debug +{ + /** + * @brief Object-style facade for the rix/debug package. + */ + class Debug + { + public: + /** + * @brief Placeholder-based formatting component. + */ + Format format{}; + + /** + * @brief Logging component. + */ + Log log{}; + + /** + * @brief Inspection component. + */ + Inspect inspect{}; + + /** + * @brief Print values separated by spaces, with a trailing newline. + */ + template + void print(const Args &...args) const + { + rixlib::print(args...); + } + + /** + * @brief Print values to stderr. + */ + template + void eprint(const Args &...args) const + { + rixlib::eprint(args...); + } + + /** + * @brief Debug-only print to stderr. + */ + template + void dprint(const Args &...args) const + { + rixlib::dprint(args...); + } + + /** + * @brief Format printed values into a string. + */ + template + [[nodiscard]] std::string sprint(const Args &...args) const + { + return rixlib::sprint(args...); + } + }; +} + +#endif // RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_HPP_INCLUDED diff --git a/include/rix/debug/format.hpp b/include/rix/debug/format.hpp new file mode 100644 index 0000000..5a6c8bd --- /dev/null +++ b/include/rix/debug/format.hpp @@ -0,0 +1,323 @@ +/** + * + * @file format.hpp + * @author Gaspard Kirira + * + * Copyright 2025, Gaspard Kirira. All rights reserved. + * https://github.com/rixcpp/rix + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Rix + * + * Lightweight formatting utilities for rix/debug. + * + * Supported placeholders: + * - {} : automatic argument indexing + * - {0} : explicit positional indexing + * - {{ : escaped opening brace + * - }} : escaped closing brace + * + * Unsupported on purpose: + * - format specifiers such as {:>10}, {:.2f}, etc. + * + * Example: + * + * std::string s1 = rixlib::format("Hello, {}", "world"); + * std::string s2 = rixlib::format("Value = {0}, name = {1}", 42, "Ada"); + * std::string s3 = rixlib::format("{{ config }} = {}", "ready"); + * + */ + +#ifndef RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_FORMAT_HPP_INCLUDED +#define RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_FORMAT_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace rixlib +{ + /** + * @brief Exception type thrown on invalid format strings or invalid argument access. + */ + class format_error : public std::runtime_error + { + public: + explicit format_error(const std::string &message) + : std::runtime_error(message) + { + } + + explicit format_error(const char *message) + : std::runtime_error(message) + { + } + }; + + namespace detail + { + /** + * @brief Convert a value to string using the Rix rendering pipeline. + */ + template + [[nodiscard]] inline std::string format_arg_to_string(const T &value) + { + std::ostringstream oss; + + auto &cfg = rixlib::default_config(); + const bool old_raw_strings = cfg.raw_strings; + + cfg.raw_strings = true; + + try + { + rixlib::detail::write(oss, value); + } + catch (...) + { + cfg.raw_strings = old_raw_strings; + throw; + } + + cfg.raw_strings = old_raw_strings; + return oss.str(); + } + + /** + * @brief Small non-owning view over a pre-rendered argument list. + */ + class rendered_arg_list + { + public: + template + explicit rendered_arg_list(const std::array &values) noexcept + : data_(values.data()), size_(N) + { + } + + [[nodiscard]] std::size_t size() const noexcept + { + return size_; + } + + [[nodiscard]] const std::string &at(std::size_t index) const + { + if (index >= size_) + { + throw format_error("format argument index out of range"); + } + + return data_[index]; + } + + private: + const std::string *data_ = nullptr; + std::size_t size_ = 0; + }; + + /** + * @brief Parse an unsigned decimal integer from a placeholder body. + */ + [[nodiscard]] inline std::size_t parse_index(std::string_view text) + { + if (text.empty()) + { + throw format_error("empty explicit format index"); + } + + std::size_t value = 0; + + for (char ch : text) + { + if (!std::isdigit(static_cast(ch))) + { + throw format_error("invalid explicit format index"); + } + + value = (value * 10u) + static_cast(ch - '0'); + } + + return value; + } + + /** + * @brief Render a format string into the destination string. + */ + inline void render_format_string(std::string &out, + std::string_view fmt, + const rendered_arg_list &args) + { + std::size_t i = 0; + std::size_t next_auto_index = 0; + bool used_auto_index = false; + bool used_explicit_index = false; + + while (i < fmt.size()) + { + const char ch = fmt[i]; + + if (ch == '{') + { + if ((i + 1u) < fmt.size() && fmt[i + 1u] == '{') + { + out.push_back('{'); + i += 2u; + continue; + } + + const std::size_t close = fmt.find('}', i + 1u); + if (close == std::string_view::npos) + { + throw format_error("unmatched '{' in format string"); + } + + const std::string_view token = fmt.substr(i + 1u, close - (i + 1u)); + + if (token.empty()) + { + if (used_explicit_index) + { + throw format_error("cannot mix automatic and explicit argument indexing"); + } + + used_auto_index = true; + out += args.at(next_auto_index++); + } + else + { + if (used_auto_index) + { + throw format_error("cannot mix automatic and explicit argument indexing"); + } + + if (token.find(':') != std::string_view::npos) + { + throw format_error("format specifiers are not supported"); + } + + used_explicit_index = true; + out += args.at(parse_index(token)); + } + + i = close + 1u; + continue; + } + + if (ch == '}') + { + if ((i + 1u) < fmt.size() && fmt[i + 1u] == '}') + { + out.push_back('}'); + i += 2u; + continue; + } + + throw format_error("single '}' encountered in format string"); + } + + out.push_back(ch); + ++i; + } + } + + /** + * @brief Estimate a useful output capacity before rendering. + */ + [[nodiscard]] inline std::size_t estimate_output_size(std::string_view fmt, + const rendered_arg_list &args) + { + std::size_t total = fmt.size(); + + for (std::size_t i = 0; i < args.size(); ++i) + { + total += args.at(i).size(); + } + + return total; + } + + } // namespace detail + + /** + * @brief Format values into a new string using Rix placeholder syntax. + */ + template + [[nodiscard]] std::string format(std::string_view fmt, const Args &...args) + { + std::array rendered_args{ + detail::format_arg_to_string(args)...}; + + detail::rendered_arg_list rendered{rendered_args}; + + std::string out; + out.reserve(detail::estimate_output_size(fmt, rendered)); + + detail::render_format_string(out, fmt, rendered); + + return out; + } + + /** + * @brief Append formatted output to an existing string. + */ + template + void format_append(std::string &out, std::string_view fmt, const Args &...args) + { + std::array rendered_args{ + detail::format_arg_to_string(args)...}; + + detail::rendered_arg_list rendered{rendered_args}; + + out.reserve(out.size() + detail::estimate_output_size(fmt, rendered)); + detail::render_format_string(out, fmt, rendered); + } + + /** + * @brief Replace the destination string with formatted output. + */ + template + void format_to(std::string &out, std::string_view fmt, const Args &...args) + { + out.clear(); + format_append(out, fmt, args...); + } + +} // namespace rixlib + +namespace rixlib::debug +{ + /** + * @brief Object-style formatting API mounted into rixlib::debug::Debug. + */ + class Format + { + public: + template + [[nodiscard]] std::string operator()(std::string_view fmt, const Args &...args) const + { + return rixlib::format(fmt, args...); + } + + template + void append(std::string &out, std::string_view fmt, const Args &...args) const + { + rixlib::format_append(out, fmt, args...); + } + + template + void to(std::string &out, std::string_view fmt, const Args &...args) const + { + rixlib::format_to(out, fmt, args...); + } + }; +} + +#endif // RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_FORMAT_HPP_INCLUDED diff --git a/include/rix/debug/inspect.hpp b/include/rix/debug/inspect.hpp new file mode 100644 index 0000000..0a56eaa --- /dev/null +++ b/include/rix/debug/inspect.hpp @@ -0,0 +1,2096 @@ +/** + * + * @file inspect.hpp + * @author Gaspard Kirira + * + * Copyright 2025, Gaspard Kirira. All rights reserved. + * https://github.com/rixcpp/rix + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Rix + * + * Inspection and diagnostic rendering API for rix/debug. + * + */ + +#ifndef RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_INSPECT_HPP_INCLUDED +#define RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_INSPECT_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#if defined(__cpp_lib_expected) && __cpp_lib_expected >= 202202L +#include +#define RIXCPP_DEBUG_HAS_EXPECTED 1 +#else +#define RIXCPP_DEBUG_HAS_EXPECTED 0 +#endif + +#if defined(__GNUC__) || defined(__clang__) +#include +#define RIXCPP_DEBUG_HAS_DEMANGLE 1 +#else +#define RIXCPP_DEBUG_HAS_DEMANGLE 0 +#endif + +namespace rixlib +{ + struct inspect_context; + struct inspect_options; + + /// @brief Primary inspector template — specialize to teach inspect about your type. + template + struct inspector; + + /// @brief Field map template — specialize to register struct fields for reflection. + template + struct field_map; + + namespace detail + { + template + void iwrite(inspect_context &ctx, const T &value); + } // namespace detail + + namespace demangle + { + /** + * @brief Return a human-readable type name for type T. + * + * Uses abi::__cxa_demangle on GCC/Clang. Falls back to typeid().name() + * on MSVC and other compilers. + */ + template + [[nodiscard]] std::string type_name() + { +#if RIXCPP_DEBUG_HAS_DEMANGLE + int status = 0; + const char *mangled = typeid(T).name(); + char *demangled = abi::__cxa_demangle(mangled, nullptr, nullptr, &status); + std::string result = (status == 0 && demangled) ? demangled : mangled; + std::free(demangled); + return result; +#else + return typeid(T).name(); +#endif + } + + /** + * @brief Return a human-readable type name for the type of a value. + */ + template + [[nodiscard]] std::string type_name_of(const T &) + { + return type_name(); + } + + /** + * @brief Shorten a fully-qualified type name by removing common namespaces. + * + * Strips std::__cxx11::, std::__1::, std::allocator<...> and similar noise. + */ + [[nodiscard]] inline std::string shorten(std::string name) + { + // Remove std::__cxx11:: (GCC inline namespace) + for (std::string ns : {"std::__cxx11::", "std::__1::", "std::pmr::"}) + { + std::string::size_type pos; + while ((pos = name.find(ns)) != std::string::npos) + name.erase(pos, ns.size()); + } + // Collapse allocator noise: ", std::allocator<...>" + auto collapse_alloc = [&](std::string &s) + { + std::string tok = ", std::allocator<"; + std::string::size_type p = s.find(tok); + while (p != std::string::npos) + { + // Find matching closing '>' + int depth = 1; + std::string::size_type i = p + tok.size(); + while (i < s.size() && depth > 0) + { + if (s[i] == '<') + ++depth; + else if (s[i] == '>') + --depth; + ++i; + } + s.erase(p, i - p); + p = s.find(tok); + } + }; + collapse_alloc(name); + return name; + } + + /** + * @brief Return shortened demangled type name for T. + */ + template + [[nodiscard]] std::string short_type_name() + { + return shorten(type_name()); + } + + } // namespace demangle + + /** + * @brief Complete set of compile-time and runtime metadata for a type T. + * + * Gathered once, this struct provides a unified view of a type's properties: + * size, alignment, qualifiers, category flags, and a human-readable name. + */ + struct type_metadata + { + std::string name; ///< Shortened demangled type name + std::string full_name; ///< Full demangled type name + std::size_t size_bytes; ///< sizeof(T) + std::size_t align_bytes; ///< alignof(T) + + // Category flags + bool is_void; + bool is_bool; + bool is_integral; + bool is_floating_point; + bool is_arithmetic; + bool is_enum; + bool is_class; + bool is_union; + bool is_pointer; + bool is_lvalue_ref; + bool is_rvalue_ref; + bool is_array; + bool is_function; + bool is_member_pointer; + + // Qualifier flags + bool is_const; + bool is_volatile; + + // Class property flags + bool is_abstract; + bool is_polymorphic; + bool is_final; + bool is_empty; + bool is_aggregate; + bool is_standard_layout; + bool is_trivially_copyable; + bool is_trivially_destructible; + bool is_trivially_constructible; + bool is_default_constructible; + bool is_copy_constructible; + bool is_move_constructible; + bool is_copy_assignable; + bool is_move_assignable; + bool is_destructible; + + // Sign and size + bool is_signed; + bool is_unsigned; + }; + + /** + * @brief Build a type_metadata instance for type T at compile time (constexpr-friendly). + */ + template + [[nodiscard]] type_metadata make_type_metadata() + { + using U = std::remove_cvref_t; + return type_metadata{ + .name = demangle::short_type_name(), + .full_name = demangle::type_name(), + .size_bytes = sizeof(U), + .align_bytes = alignof(U), + .is_void = std::is_void_v, + .is_bool = std::is_same_v, + .is_integral = std::is_integral_v, + .is_floating_point = std::is_floating_point_v, + .is_arithmetic = std::is_arithmetic_v, + .is_enum = std::is_enum_v, + .is_class = std::is_class_v, + .is_union = std::is_union_v, + .is_pointer = std::is_pointer_v, + .is_lvalue_ref = std::is_lvalue_reference_v, + .is_rvalue_ref = std::is_rvalue_reference_v, + .is_array = std::is_array_v, + .is_function = std::is_function_v, + .is_member_pointer = std::is_member_pointer_v, + .is_const = std::is_const_v>, + .is_volatile = std::is_volatile_v>, + .is_abstract = std::is_abstract_v, + .is_polymorphic = std::is_polymorphic_v, + .is_final = std::is_final_v, + .is_empty = std::is_empty_v, + .is_aggregate = std::is_aggregate_v, + .is_standard_layout = std::is_standard_layout_v, + .is_trivially_copyable = std::is_trivially_copyable_v, + .is_trivially_destructible = std::is_trivially_destructible_v, + .is_trivially_constructible = std::is_trivially_constructible_v, + .is_default_constructible = std::is_default_constructible_v, + .is_copy_constructible = std::is_copy_constructible_v, + .is_move_constructible = std::is_move_constructible_v, + .is_copy_assignable = std::is_copy_assignable_v, + .is_move_assignable = std::is_move_assignable_v, + .is_destructible = std::is_destructible_v, + .is_signed = std::is_signed_v, + .is_unsigned = std::is_unsigned_v, + }; + } + + /** + * @brief A single registered field descriptor (name + member pointer). + * + * @tparam Owner Struct/class that owns this field. + * @tparam FieldT Type of the field. + */ + template + struct field_descriptor + { + std::string_view name; + FieldT Owner::*ptr; + + constexpr field_descriptor(std::string_view n, FieldT Owner::*p) + : name(n), ptr(p) {} + + const FieldT &get(const Owner &obj) const { return obj.*ptr; } + }; + + /** + * @brief Helper function to create a field_descriptor (type-deduced). + * + * @example + * vix::field("x", &MyStruct::x) + */ + template + constexpr auto field(std::string_view name, FieldT Owner::*ptr) + -> field_descriptor + { + return {name, ptr}; + } + + /** + * @brief Tuple of field descriptors for a given type — returned by field_map::fields(). + */ + template + constexpr auto fields(Fields &&...fs) + { + return std::tuple...>(std::forward(fs)...); + } + + /** + * @brief Options controlling the depth, format, and verbosity of inspection. + */ + struct inspect_options + { + int max_depth = 8; ///< Maximum nesting depth + std::size_t max_items = 64; ///< Max items per container + bool show_type = true; ///< Show type annotation + bool show_meta = false; ///< Show full type metadata (size, align…) + bool compact = false; ///< Single-line compact mode + bool show_address = false; ///< Show object address (for pointers) + std::string indent_str = " "; ///< Indentation unit + std::ostream *out = &std::cout; + }; + + inline inspect_options &default_options() noexcept + { + static thread_local inspect_options opts; + return opts; + } + + /** + * @brief Mutable context threaded through the recursive inspection engine. + * + * Carries output stream, indentation state, depth tracking, and options. + * All rendering operations emit to ctx.os. + */ + struct inspect_context + { + std::ostream &os; + int depth = 0; + inspect_options opts; + + explicit inspect_context(std::ostream &os_, + inspect_options o = default_options()) + : os(os_), opts(std::move(o)) {} + + // Indentation helpers + void indent() const + { + for (int i = 0; i < depth; ++i) + os << opts.indent_str; + } + + void newline_indent() const + { + if (!opts.compact) + { + os << '\n'; + indent(); + } + else + os << ' '; + } + + [[nodiscard]] bool at_max_depth() const noexcept + { + return depth >= opts.max_depth; + } + + // Output helpers + inspect_context child() const + { + inspect_context c{os, opts}; + c.depth = depth + 1; + return c; + } + + void emit(std::string_view sv) { os << sv; } + void emit(char c) { os << c; } + + template + void emit_value(const T &v) { os << v; } + }; + + namespace detail + { + + // Type annotation helper + template + void emit_type_tag(inspect_context &ctx) + { + if (ctx.opts.show_type) + { + ctx.os << '<' << demangle::short_type_name>() << '>'; + } + } + + // Bool + inline void iwrite_bool(inspect_context &ctx, bool v) + { + ctx.os << (v ? "true" : "false"); + } + + // Char types + inline void iwrite_char(inspect_context &ctx, char c) + { + ctx.os << '\'' << c << '\''; + } + inline void iwrite_char(inspect_context &ctx, signed char c) + { + ctx.os << '\'' << static_cast(c) << '\''; + } + inline void iwrite_char(inspect_context &ctx, unsigned char c) + { + ctx.os << "0x" << std::hex << std::uppercase + << static_cast(c) << std::dec; + } + inline void iwrite_char(inspect_context &ctx, wchar_t c) + { + ctx.os << "L'\\u" << std::hex << std::uppercase + << static_cast(c) << std::dec << "'"; + } + inline void iwrite_char(inspect_context &ctx, char8_t c) + { + ctx.os << "u8'" << static_cast(c) << '\''; + } + inline void iwrite_char(inspect_context &ctx, char16_t c) + { + ctx.os << "u'\\u" << std::hex << std::uppercase + << static_cast(c) << std::dec << "'"; + } + inline void iwrite_char(inspect_context &ctx, char32_t c) + { + ctx.os << "U'\\U" << std::hex << std::uppercase + << static_cast(c) << std::dec << "'"; + } + + // Nullptr + inline void iwrite_nullptr(inspect_context &ctx) + { + ctx.os << "nullptr"; + } + + // 8.5 String + inline void iwrite_string(inspect_context &ctx, std::string_view sv) + { + ctx.os << '"' << sv << '"'; + if (ctx.opts.show_type) + { + ctx.os << " [len=" << sv.size() << ']'; + } + } + + inline void iwrite_wstring(inspect_context &ctx, std::wstring_view wsv) + { + ctx.os << "L\""; + for (wchar_t wc : wsv) + { + if (wc < 0x80) + ctx.os << static_cast(wc); + else + ctx.os << "\\u" << std::hex << std::uppercase + << static_cast(wc) << std::dec; + } + ctx.os << '"'; + if (ctx.opts.show_type) + ctx.os << " [len=" << wsv.size() << ']'; + } + + // 8.6 Pointer + template + void iwrite_pointer(inspect_context &ctx, const T *ptr) + { + if (!ptr) + { + ctx.os << "nullptr"; + return; + } + ctx.os << "(ptr) + << std::dec << '>'; + if (ctx.opts.show_address) + { + // Attempt to dereference and inspect the pointed-to value + if (!ctx.at_max_depth()) + { + ctx.os << " -> "; + auto child = ctx.child(); + iwrite(child, *ptr); + } + } + } + + // Enum + template + requires std::is_enum_v + void iwrite_enum(inspect_context &ctx, E e) + { + ctx.os << static_cast>(e); + if (ctx.opts.show_type) + { + ctx.os << " [enum:" + << demangle::short_type_name() + << ']'; + } + } + + // Duration + template + void iwrite_duration(inspect_context &ctx, + const std::chrono::duration &d) + { + using namespace std::chrono; + if constexpr (std::is_same_v) + ctx.os << d.count() << "ns"; + else if constexpr (std::is_same_v) + ctx.os << d.count() << "µs"; + else if constexpr (std::is_same_v) + ctx.os << d.count() << "ms"; + else if constexpr (std::ratio_equal_v>) + ctx.os << d.count() << "s"; + else if constexpr (std::ratio_equal_v>) + ctx.os << d.count() << "min"; + else if constexpr (std::ratio_equal_v>) + ctx.os << d.count() << "h"; + else + ctx.os << d.count() << " [" << Period::num << '/' << Period::den << "]s"; + } + + // time_point + template + void iwrite_time_point(inspect_context &ctx, + const std::chrono::time_point &tp) + { + ctx.os << ""; + } + + // filesystem::path + inline void iwrite_fs_path(inspect_context &ctx, + const std::filesystem::path &p) + { + ctx.os << "path(\"" << p.string() << "\")"; + if (ctx.opts.show_type) + { + ctx.os << " [exists=" << std::boolalpha + << std::filesystem::exists(p) << "]"; + } + } + + // Optional + template + void iwrite_optional(inspect_context &ctx, const std::optional &opt) + { + if (!opt.has_value()) + { + ctx.os << "None"; + if (ctx.opts.show_type) + ctx.os << '<' << demangle::short_type_name() << '>'; + return; + } + ctx.os << "Some("; + auto child = ctx.child(); + iwrite(child, *opt); + ctx.os << ')'; + } + + // Variant + template + void iwrite_variant(inspect_context &ctx, + const std::variant &var) + { + ctx.os << "variant("; + std::visit([&ctx](const auto &v) + { + auto child = ctx.child(); + iwrite(child, v); }, var); + ctx.os << ')'; + } + + // 8.13 std::any + inline void iwrite_any(inspect_context &ctx, const std::any &a) + { + if (!a.has_value()) + { + ctx.os << "any(empty)"; + } + else + { +#if RIXCPP_DEBUG_HAS_DEMANGLE + int status = 0; + const char *mangled = a.type().name(); + char *d = abi::__cxa_demangle(mangled, nullptr, nullptr, &status); + ctx.os << "any(" << (status == 0 && d ? d : mangled) << ')'; + std::free(d); +#else + ctx.os << "any(" << a.type().name() << ')'; +#endif + } + } + + // reference_wrapper + template + void iwrite_ref_wrapper(inspect_context &ctx, + const std::reference_wrapper &rw) + { + ctx.os << "ref("; + auto child = ctx.child(); + iwrite(child, rw.get()); + ctx.os << ')'; + } + + // Smart pointers + template + void iwrite_unique(inspect_context &ctx, const std::unique_ptr &p) + { + if (!p) + { + ctx.os << "unique_ptr(null)"; + return; + } + ctx.os << "unique_ptr("; + auto child = ctx.child(); + iwrite(child, *p); + ctx.os << ')'; + } + + template + void iwrite_shared(inspect_context &ctx, const std::shared_ptr &p) + { + if (!p) + { + ctx.os << "shared_ptr(null)"; + return; + } + ctx.os << "shared_ptr[use_count=" << p.use_count() << "]("; + auto child = ctx.child(); + iwrite(child, *p); + ctx.os << ')'; + } + + template + void iwrite_weak(inspect_context &ctx, const std::weak_ptr &p) + { + if (p.expired()) + { + ctx.os << "weak_ptr(expired)"; + return; + } + auto sp = p.lock(); + ctx.os << "weak_ptr[use_count=" << sp.use_count() << "]("; + auto child = ctx.child(); + iwrite(child, *sp); + ctx.os << ')'; + } + + // Container adapter helpers (copy-drain) + template + void iwrite_stack(inspect_context &ctx, std::stack s, std::size_t max) + { + std::vector items; + items.reserve(s.size()); + while (!s.empty()) + { + items.push_back(s.top()); + s.pop(); + } + std::reverse(items.begin(), items.end()); + ctx.os << "stack["; + std::size_t count = 0; + bool first = true; + for (const auto &v : items) + { + if (count++ >= max) + { + ctx.os << ", ..."; + break; + } + if (!first) + ctx.os << ", "; + first = false; + auto child = ctx.child(); + iwrite(child, v); + } + ctx.os << ']'; + } + + template + void iwrite_queue(inspect_context &ctx, std::queue q, std::size_t max) + { + ctx.os << "queue["; + std::size_t count = 0; + bool first = true; + while (!q.empty()) + { + if (count++ >= max) + { + ctx.os << ", ..."; + break; + } + if (!first) + ctx.os << ", "; + first = false; + auto child = ctx.child(); + iwrite(child, q.front()); + q.pop(); + } + ctx.os << ']'; + } + + template + void iwrite_pqueue(inspect_context &ctx, + std::priority_queue pq, std::size_t max) + { + ctx.os << "priority_queue["; + std::size_t count = 0; + bool first = true; + while (!pq.empty()) + { + if (count++ >= max) + { + ctx.os << ", ..."; + break; + } + if (!first) + ctx.os << ", "; + first = false; + auto child = ctx.child(); + iwrite(child, pq.top()); + pq.pop(); + } + ctx.os << ']'; + } + + // Range rendering + template + void iwrite_range(inspect_context &ctx, const Range &rng, + char open, char close) + { + ctx.os << open; + std::size_t count = 0; + bool first = true; + for (const auto &elem : rng) + { + if (count >= ctx.opts.max_items) + { + ctx.os << ", ..."; + break; + } + if (!first) + ctx.os << ", "; + first = false; + if constexpr (traits::is_map_like_v) + { + auto kctx = ctx.child(); + iwrite(kctx, elem.first); + ctx.os << ": "; + auto vctx = ctx.child(); + iwrite(vctx, elem.second); + } + else + { + auto child = ctx.child(); + iwrite(child, elem); + } + ++count; + } + ctx.os << close; + if (ctx.opts.show_type) + { + // Show size if available via std::size or distance + if constexpr (requires { std::size(rng); }) + { + ctx.os << " [n=" << std::size(rng) << ']'; + } + } + } + + // Tuple / pair + template + void iwrite_tuple_elems(inspect_context &ctx, const Tuple &t, + std::index_sequence) + { + (( + [&]() + { + if constexpr (Is > 0) + ctx.os << ", "; + auto child = ctx.child(); + iwrite(child, std::get(t)); + }()), + ...); + } + + template + void iwrite_tuple(inspect_context &ctx, const Tuple &t) + { + ctx.os << '('; + iwrite_tuple_elems(ctx, t, std::make_index_sequence>{}); + ctx.os << ')'; + } + + template + void iwrite_pair(inspect_context &ctx, const std::pair &p) + { + ctx.os << '('; + auto fst = ctx.child(); + iwrite(fst, p.first); + ctx.os << ": "; + auto snd = ctx.child(); + iwrite(snd, p.second); + ctx.os << ')'; + } + + // Field-map based struct rendering + template + void iwrite_fields(inspect_context &ctx, const T &obj, + const Fields &fmap, + std::index_sequence) + { + (( + [&]() + { + if constexpr (Is > 0) + { + ctx.os << ','; + ctx.newline_indent(); + } + const auto &fd = std::get(fmap); + ctx.os << fd.name << ": "; + auto child = ctx.child(); + iwrite(child, fd.get(obj)); + }()), + ...); + } + + template + requires traits::has_field_map_v + void iwrite_struct(inspect_context &ctx, const T &obj) + { + const auto fmap = field_map::fields(); + constexpr std::size_t N = std::tuple_size_v>; + ctx.os << demangle::short_type_name() << " {"; + if (!ctx.opts.compact) + { + ctx.os << '\n'; + auto child = ctx.child(); + child.indent(); + iwrite_fields(child, obj, fmap, std::make_index_sequence{}); + ctx.os << '\n'; + ctx.indent(); + } + else + { + iwrite_fields(ctx, obj, fmap, std::make_index_sequence{}); + } + ctx.os << '}'; + } + +#if RIXCPP_DEBUG_HAS_EXPECTED + // std::expected + template + void iwrite_expected(inspect_context &ctx, const std::expected &exp) + { + if (exp.has_value()) + { + ctx.os << "Ok("; + auto child = ctx.child(); + iwrite(child, *exp); + ctx.os << ')'; + } + else + { + ctx.os << "Err("; + auto child = ctx.child(); + iwrite(child, exp.error()); + ctx.os << ')'; + } + } +#endif + + /** + * @brief Central dispatch for the inspection engine. + * + * Priority: + * 1. rixlib::inspector specialization + * 2. ADL rix_inspect hook + * 3. field_map struct reflection + * 4. nullptr_t + * 5. bool + * 6. char types + * 7. wstring / wstring_view + * 8. string-like + * 9. filesystem::path + * 10. std::any + * 11. std::optional + * 12. std::variant + * 13. std::reference_wrapper + * 14. chrono duration + * 15. chrono time_point + * 16. std::unique_ptr + * 17. std::shared_ptr + * 18. std::weak_ptr + * 19. Container adapters (stack / queue / priority_queue) + * 20. std::expected (C++23) + * 21. enum + * 22. map-like range + * 23. other range (vector, list, set, …) + * 24. pair + * 25. tuple-like + * 26. raw pointer (dereferences if show_address) + * 27. streamable via operator<< + * 28. fallback: unprintable + */ + template + void iwrite(inspect_context &ctx, const T &value) + { + using U = std::remove_cvref_t; + + if (ctx.at_max_depth()) + { + ctx.os << ""; + return; + } + + // inspector specialization + if constexpr (traits::has_inspector_v) + { + inspector::inspect(ctx, value); + } + // ADL rix_inspect hook + else if constexpr (traits::has_rix_inspect_hook_v) + { + rix_inspect(ctx, value); + } + // field_map struct reflection + else if constexpr (traits::has_field_map_v) + { + iwrite_struct(ctx, value); + } + // nullptr_t + else if constexpr (traits::is_nullptr_v) + { + iwrite_nullptr(ctx); + } + // bool + else if constexpr (traits::is_bool_v) + { + iwrite_bool(ctx, value); + } + // char types + else if constexpr (traits::is_char_v) + { + iwrite_char(ctx, value); + } + // wstring + else if constexpr (std::is_same_v || + std::is_same_v) + { + iwrite_wstring(ctx, value); + } + else if constexpr (std::is_same_v || + std::is_same_v) + { + if (value == nullptr) + iwrite_nullptr(ctx); + else + iwrite_wstring(ctx, std::wstring_view{value}); + } + // string-like + else if constexpr (traits::is_string_like_v) + { + iwrite_string(ctx, value); + } + // filesystem::path + else if constexpr (traits::is_fs_path_v) + { + iwrite_fs_path(ctx, value); + } + // std::any + else if constexpr (traits::is_any_v) + { + iwrite_any(ctx, value); + } + // std::optional + else if constexpr (traits::is_optional_v) + { + iwrite_optional(ctx, value); + } + // std::variant + else if constexpr (traits::is_variant_v) + { + iwrite_variant(ctx, value); + } + // reference_wrapper + else if constexpr (traits::is_ref_wrapper_v) + { + iwrite_ref_wrapper(ctx, value); + } + // chrono duration + else if constexpr (traits::is_duration_v) + { + iwrite_duration(ctx, value); + } + // chrono time_point + else if constexpr (traits::is_time_point_v) + { + iwrite_time_point(ctx, value); + } + // unique_ptr + else if constexpr (traits::is_unique_ptr::value) + { + iwrite_unique(ctx, value); + } + // shared_ptr + else if constexpr (traits::is_shared_ptr::value) + { + iwrite_shared(ctx, value); + } + // weak_ptr + else if constexpr (traits::is_weak_ptr::value) + { + iwrite_weak(ctx, value); + } + // Container adapters + else if constexpr (traits::is_stack::value) + { + iwrite_stack(ctx, value, ctx.opts.max_items); + } + else if constexpr (traits::is_queue::value) + { + iwrite_queue(ctx, value, ctx.opts.max_items); + } + else if constexpr (traits::is_priority_queue::value) + { + iwrite_pqueue(ctx, value, ctx.opts.max_items); + } +#if RIXCPP_DEBUG_HAS_EXPECTED + // std::expected + else if constexpr (traits::is_expected_v) + { + iwrite_expected(ctx, value); + } +#endif + // enum + else if constexpr (std::is_enum_v) + { + iwrite_enum(ctx, value); + } + // map-like range + else if constexpr (traits::is_map_like_v && traits::is_range_v) + { + iwrite_range(ctx, value, '{', '}'); + } + // sequence range + else if constexpr (traits::is_range_v) + { + iwrite_range(ctx, value, '[', ']'); + } + // pair + else if constexpr (traits::is_pair_v) + { + iwrite_pair(ctx, value); + } + // tuple-like + else if constexpr (traits::is_tuple_like_v) + { + iwrite_tuple(ctx, value); + } + // raw pointer + else if constexpr (std::is_pointer_v && !traits::is_string_like_v) + { + if constexpr (std::is_function_v>) + { + ctx.os << "( + reinterpret_cast(value)) + << std::dec << '>'; + } + else + { + iwrite_pointer(ctx, value); + } + } + // streamable via operator<< + else if constexpr (traits::is_ostreamable_v) + { + ctx.os << value; + } + // fallback + else + { + ctx.os << "'; + } + } + + } // namespace detail + + namespace detail + { + + /** + * @brief Emit a full type_metadata block to the context stream. + */ + inline void render_metadata(inspect_context &ctx, const type_metadata &m) + { + ctx.os << "type_info {\n"; + auto ind = [&](std::string_view key, auto val) + { + ctx.os << ctx.opts.indent_str << " " + << key << ": " << val << '\n'; + }; + auto indb = [&](std::string_view key, bool val) + { + if (val) + ctx.os << ctx.opts.indent_str << " " + << key << ": true\n"; + }; + + ind("name", m.name); + ind("full_name", m.full_name); + ind("size", std::to_string(m.size_bytes) + " bytes"); + ind("align", std::to_string(m.align_bytes) + " bytes"); + + ctx.os << ctx.opts.indent_str << " categories: ["; + bool first = true; + auto cat = [&](std::string_view s, bool v) + { + if (!v) + return; + if (!first) + ctx.os << ", "; + first = false; + ctx.os << s; + }; + cat("void", m.is_void); + cat("bool", m.is_bool); + cat("integral", m.is_integral); + cat("float", m.is_floating_point); + cat("enum", m.is_enum); + cat("class", m.is_class); + cat("union", m.is_union); + cat("pointer", m.is_pointer); + cat("array", m.is_array); + cat("function", m.is_function); + cat("const", m.is_const); + cat("volatile", m.is_volatile); + ctx.os << "]\n"; + + ctx.os << ctx.opts.indent_str << " traits: ["; + first = true; + auto tr = [&](std::string_view s, bool v) + { + if (!v) + return; + if (!first) + ctx.os << ", "; + first = false; + ctx.os << s; + }; + tr("abstract", m.is_abstract); + tr("polymorphic", m.is_polymorphic); + tr("final", m.is_final); + tr("empty", m.is_empty); + tr("aggregate", m.is_aggregate); + tr("standard_layout", m.is_standard_layout); + tr("trivially_copyable", m.is_trivially_copyable); + tr("trivially_destructible", m.is_trivially_destructible); + tr("default_constructible", m.is_default_constructible); + tr("copy_constructible", m.is_copy_constructible); + tr("move_constructible", m.is_move_constructible); + tr("copy_assignable", m.is_copy_assignable); + tr("move_assignable", m.is_move_assignable); + ctx.os << "]\n"; + + ctx.os << "}"; + } + + } // namespace detail + + /** + * @brief Inspect a value and write its representation to stdout. + * + * Uses the default inspect_options. Every supported type is rendered + * with contextual information: value, type name (if show_type), size, etc. + * + * @param value Any value of a supported type. + * + * @example + * rixlib::inspect(42); + * rixlib::inspect(std::vector{1, 2, 3}); + * rixlib::inspect(std::make_optional(3.14)); + */ + template + void inspect(const T &value) + { + inspect_context ctx{*default_options().out}; + detail::iwrite(ctx, value); + ctx.os << '\n'; + } + + /** + * @brief Inspect a value with custom options. + * @param value Value to inspect. + * @param opts Custom inspect_options. + */ + template + void inspect(const T &value, const inspect_options &opts) + { + inspect_context ctx{*opts.out, opts}; + detail::iwrite(ctx, value); + ctx.os << '\n'; + } + + /** + * @brief Inspect a value into a specific ostream. + * @param os Destination stream. + * @param value Value to inspect. + */ + template + void inspect_to(std::ostream &os, const T &value) + { + inspect_options opts = default_options(); + opts.out = &os; + inspect_context ctx{os, opts}; + detail::iwrite(ctx, value); + ctx.os << '\n'; + } + + // inspect_to_string — capture as std::string + + /** + * @brief Inspect a value and return its representation as a std::string. + * @param value Any supported value. + * @return Inspection string (no trailing newline). + */ + template + [[nodiscard]] std::string inspect_to_string(const T &value) + { + std::ostringstream oss; + inspect_options opts = default_options(); + opts.out = &oss; + inspect_context ctx{oss, opts}; + detail::iwrite(ctx, value); + return oss.str(); + } + + /** + * @brief Inspect a value to string with custom options. + */ + template + [[nodiscard]] std::string inspect_to_string(const T &value, + const inspect_options &opts) + { + std::ostringstream oss; + inspect_options o = opts; + o.out = &oss; + inspect_context ctx{oss, o}; + detail::iwrite(ctx, value); + return oss.str(); + } + + /** + * @brief Inspect the type T, printing its full metadata report. + * + * Does not require a value. Reports size, alignment, category flags, + * trait flags, and the demangled type name. + * + * @example + * rixlib::inspect_type>(); + * rixlib::inspect_type(); + */ + template + void inspect_type() + { + auto &os = *default_options().out; + type_metadata m = make_type_metadata(); + inspect_context ctx{os}; + detail::render_metadata(ctx, m); + os << '\n'; + } + + /** + * @brief Inspect the type of a value (deduced, no explicit template arg needed). + * @param value Value whose type will be inspected. + */ + template + void inspect_type(const T &) + { + inspect_type>(); + } + + /** + * @brief Inspect multiple values on a single line, separated by " | ". + * + * @example + * rixlib::inspect_line(x, y, z); + * // x_value | y_value | z_value + */ + template + void inspect_line(const Args &...args) + { + auto &os = *default_options().out; + bool first = true; + ([&]() + { + if (!first) os << " | "; + first = false; + inspect_context ctx{os}; + detail::iwrite(ctx, args); }(), ...); + os << '\n'; + } + + /** + * @brief Inspect a named value: "label: ". + * + * @example + * rixlib::inspect_value("batch_size", config.batch_size); + */ + template + void inspect_value(std::string_view label, const T &value) + { + auto &os = *default_options().out; + os << label << ": "; + inspect_context ctx{os}; + detail::iwrite(ctx, value); + os << '\n'; + } + + /** + * @brief Inspect multiple values, each on its own line with a type tag. + */ + template + void inspect_all(const Args &...args) + { + inspect_options opts = default_options(); + opts.show_type = true; + ([&]() + { + inspect_context ctx{*opts.out, opts}; + detail::iwrite(ctx, args); + ctx.os << '\n'; }(), ...); + } + + /** + * @brief Inspect a value AND its full type metadata. + * + * Shows the value representation followed by a complete type_metadata block. + */ + template + void inspect_meta(const T &value) + { + auto &os = *default_options().out; + // Value line + os << "value: "; + inspect_context vctx{os}; + detail::iwrite(vctx, value); + os << '\n'; + // Type metadata + auto m = make_type_metadata(); + inspect_context mctx{os}; + detail::render_metadata(mctx, m); + os << '\n'; + } + + /** + * @brief Inspect in compact mode: single line, no type annotations. + */ + template + [[nodiscard]] std::string inspect_compact(const T &value) + { + inspect_options opts; + opts.compact = true; + opts.show_type = false; + opts.show_meta = false; + return inspect_to_string(value, opts); + } + + /** + * @brief Inspect in verbose mode: type tags, metadata, show addresses. + */ + template + void inspect_verbose(const T &value) + { + inspect_options opts = default_options(); + opts.show_type = true; + opts.show_meta = true; + opts.show_address = true; + inspect(value, opts); + } + + /** + * @brief RAII guard that temporarily overrides the default inspect_options. + * + * @example + * { + * rixlib::scoped_inspect_options g{}; + * g.opts.show_type = true; + * g.opts.compact = true; + * rixlib::inspect(my_object); + * } + * // original options restored + */ + class scoped_inspect_options + { + public: + inspect_options opts; + + explicit scoped_inspect_options(inspect_options override = default_options()) + : opts(std::move(override)), saved_(default_options()) + { + default_options() = opts; + } + + ~scoped_inspect_options() + { + default_options() = saved_; + } + + scoped_inspect_options(const scoped_inspect_options &) = delete; + scoped_inspect_options &operator=(const scoped_inspect_options &) = delete; + + private: + inspect_options saved_; + }; + + /// @brief Base for inspector specializations that delegate to operator<<. + template + struct streamable_inspector + { + static void inspect(inspect_context &ctx, const T &v) + { + ctx.os << v; + } + }; + + /// @brief Inspector for std::monostate. + template <> + struct inspector + { + static void inspect(inspect_context &ctx, const std::monostate &) + { + ctx.os << "monostate"; + } + }; + + /// @brief Inspector for std::byte. + template <> + struct inspector + { + static void inspect(inspect_context &ctx, const std::byte &b) + { + ctx.os << "0x" << std::hex << std::uppercase + << static_cast(b) << std::dec; + if (ctx.opts.show_type) + ctx.os << " [byte]"; + } + }; + + /// @brief Inspector for std::error_code. + template <> + struct inspector + { + static void inspect(inspect_context &ctx, const std::error_code &ec) + { + ctx.os << "error_code(" << ec.value() + << ", \"" << ec.message() << "\")"; + } + }; + + /// @brief Inspector for std::error_condition. + template <> + struct inspector + { + static void inspect(inspect_context &ctx, const std::error_condition &ec) + { + ctx.os << "error_condition(" << ec.value() + << ", \"" << ec.message() << "\")"; + } + }; + + /** + * @brief Return a string describing which rendering path would be used for T. + */ + template + [[nodiscard]] std::string_view inspect_path() noexcept + { + using U = std::remove_cvref_t; + if constexpr (traits::has_inspector_v) + return "inspector"; + else if constexpr (traits::has_rix_inspect_hook_v) + return "ADL rix_inspect"; + else if constexpr (traits::has_field_map_v) + return "field_map"; + else if constexpr (traits::is_nullptr_v) + return "nullptr_t"; + else if constexpr (traits::is_bool_v) + return "bool"; + else if constexpr (traits::is_char_v) + return "char type"; + else if constexpr (traits::is_string_like_v) + return "string-like"; + else if constexpr (traits::is_fs_path_v) + return "filesystem::path"; + else if constexpr (traits::is_any_v) + return "std::any"; + else if constexpr (traits::is_optional_v) + return "std::optional"; + else if constexpr (traits::is_variant_v) + return "std::variant"; + else if constexpr (traits::is_ref_wrapper_v) + return "reference_wrapper"; + else if constexpr (traits::is_duration_v) + return "chrono::duration"; + else if constexpr (traits::is_time_point_v) + return "chrono::time_point"; + else if constexpr (traits::is_unique_ptr::value) + return "unique_ptr"; + else if constexpr (traits::is_shared_ptr::value) + return "shared_ptr"; + else if constexpr (traits::is_weak_ptr::value) + return "weak_ptr"; + else if constexpr (traits::is_stack::value) + return "std::stack"; + else if constexpr (traits::is_queue::value) + return "std::queue"; + else if constexpr (traits::is_priority_queue::value) + return "priority_queue"; +#if RIXCPP_DEBUG_HAS_EXPECTED + else if constexpr (traits::is_expected_v) + return "std::expected"; +#endif + else if constexpr (std::is_enum_v) + return "enum"; + else if constexpr (traits::is_map_like_v) + return "map-like range"; + else if constexpr (traits::is_range_v) + return "range"; + else if constexpr (traits::is_pair_v) + return "std::pair"; + else if constexpr (traits::is_tuple_like_v) + return "tuple-like"; + else if constexpr (std::is_pointer_v) + return "raw pointer"; + else if constexpr (traits::is_ostreamable_v) + return "operator<<"; + else + return "fallback"; + } + + /** + * @brief Print the rendering path for each type in a pack. + */ + template + void inspect_paths() + { + auto &os = *default_options().out; + ((os << demangle::short_type_name() + << " => " << inspect_path() << '\n'), + ...); + } + + /** + * @brief Print a detailed inspection report for a struct with a field_map. + * + * Produces a multi-line report with type metadata and field-by-field values. + */ + template + requires traits::has_field_map_v + void inspect_report(const T &obj) + { + auto &os = *default_options().out; + const auto m = make_type_metadata(); + + os << "══ Inspect Report: " << m.name << " ══\n"; + os << " sizeof: " << m.size_bytes << " bytes\n"; + os << " alignof: " << m.align_bytes << " bytes\n"; + os << " layout: " + << (m.is_standard_layout ? "standard" : "non-standard") << '\n'; + os << " trivial: " + << (m.is_trivially_copyable ? "yes" : "no") << '\n'; + + os << " fields:\n"; + const auto fmap = field_map::fields(); + constexpr std::size_t N = + std::tuple_size_v>; + + auto print_field = [&os](const auto &fd, const T &o) + { + os << " " << fd.name << ": "; + inspect_options opts = default_options(); + opts.show_type = true; + inspect_context ctx{os, opts}; + detail::iwrite(ctx, fd.get(o)); + os << '\n'; + }; + + [&](std::index_sequence) + { + (print_field(std::get(fmap), obj), ...); + }(std::make_index_sequence{}); + + os << "══════════════════════════════\n"; + } + + /** + * @brief Print a size/capacity/type report for a sequence container. + */ + template + requires traits::is_range_v + void inspect_container(const T &c) + { + auto &os = *default_options().out; + os << demangle::short_type_name() << " {\n"; + os << " size: "; + + if constexpr (requires { c.size(); }) + os << c.size(); + else + os << std::distance(std::begin(c), std::end(c)); + os << '\n'; + + if constexpr (requires { c.capacity(); }) + os << " capacity: " << c.capacity() << '\n'; + + if constexpr (requires { c.bucket_count(); }) + os << " buckets: " << c.bucket_count() << '\n'; + + if constexpr (requires { c.max_size(); }) + os << " max_size: " << c.max_size() << '\n'; + + if constexpr (requires { c.empty(); }) + os << " empty: " << std::boolalpha << c.empty() << '\n'; + + // Value type metadata + using V = std::remove_cvref_t; + os << " value_type: " << demangle::short_type_name() << '\n'; + os << " value_size: " << sizeof(V) << " bytes\n"; + + os << " elements: "; + inspect_options opts = default_options(); + opts.show_type = false; + inspect_context ctx{os, opts}; + detail::iwrite_range(ctx, c, + traits::is_map_like_v ? '{' : '[', + traits::is_map_like_v ? '}' : ']'); + os << "\n}\n"; + } + + /** + * @brief Print a hex dump of the raw bytes of a trivially-copyable value. + * + * Only available for trivially copyable types. + * @param value Value to dump. + * @param label Optional label. + */ + template + requires std::is_trivially_copyable_v + void inspect_bytes(const T &value, std::string_view label = "") + { + auto &os = *default_options().out; + const std::size_t n = sizeof(T); + const auto *bytes = reinterpret_cast(&value); + + if (!label.empty()) + os << label << " - "; + os << demangle::short_type_name() + << " [" << n << " bytes @ 0x" + << std::hex << std::uppercase + << reinterpret_cast(&value) + << std::dec << "]:\n "; + + for (std::size_t i = 0; i < n; ++i) + { + os << std::hex << std::uppercase + << std::setfill('0') << std::setw(2) + << static_cast(bytes[i]) << std::dec; + if (i + 1 < n) + os << (((i + 1) % 8 == 0) ? "\n " : " "); + } + os << '\n'; + } + + /** + * @brief Inspect two values side-by-side for comparison. + */ + template + void inspect_diff(const A &a, const B &b, + std::string_view label_a = "a", + std::string_view label_b = "b") + { + auto &os = *default_options().out; + os << label_a << ": "; + inspect_context ca{os}; + detail::iwrite(ca, a); + os << '\n'; + os << label_b << ": "; + inspect_context cb{os}; + detail::iwrite(cb, b); + os << '\n'; + os << "types_equal: " << std::boolalpha + << std::is_same_v, std::remove_cvref_t> << '\n'; + if constexpr (requires { a == b; }) + { + os << "values_equal: " << (a == b) << '\n'; + } + } + + /** + * @brief Inspect a check: compare expected vs actual and label PASS/FAIL. + */ + template + bool inspect_check(std::string_view label, + const Expected &expected, + const Actual &actual) + { + auto &os = *default_options().out; + bool ok = false; + if constexpr (requires { actual == expected; }) + { + ok = (actual == expected); + } + os << (ok ? "[PASS] " : "[FAIL] ") << label << '\n'; + if (!ok) + { + os << " expected: "; + inspect_context ce{os}; + detail::iwrite(ce, expected); + os << '\n'; + os << " actual: "; + inspect_context ca{os}; + detail::iwrite(ca, actual); + os << '\n'; + } + return ok; + } + + /** + * @brief Inspect a value mid-expression and return it unchanged. + * + * @example + * auto result = rixlib::inspect_tap(compute()); // prints and returns + */ + template + const T &inspect_tap(const T &value, std::string_view label = "") + { + auto &os = *default_options().out; + if (!label.empty()) + os << label << ": "; + inspect_context ctx{os}; + detail::iwrite(ctx, value); + os << '\n'; + return value; + } + + /** + * @brief Conditionally inspect: only outputs if condition is true. + */ + template + void inspect_if(bool condition, const T &value, + std::string_view label = "") + { + if (condition) + { + if (!label.empty()) + inspect_value(label, value); + else + inspect(value); + } + } + + /** + * @brief Inspect a value inside a lambda, then return the original value. + * + * @example + * auto v = rixlib::tap_with(compute(), [](const auto& x){ + * rixlib::inspect_value("intermediate", x); + * }); + */ + template + const T &tap_with(const T &value, Fn &&fn) + { + fn(value); + return value; + } + + /** + * @brief Inspect a numeric range: min, max, sum, mean, and distribution. + */ + template + requires traits::is_range_v && + std::is_arithmetic_v< + std::remove_cvref_t()))>> + void inspect_numeric(const Range &rng, std::string_view label = "") + { + auto &os = *default_options().out; + using V = std::remove_cvref_t; + + if (!label.empty()) + os << label << ":\n"; + + std::vector items(std::begin(rng), std::end(rng)); + const std::size_t n = items.size(); + + if (n == 0) + { + os << " \n"; + return; + } + + V mn = items[0], mx = items[0]; + double sum = 0.0, sum_sq = 0.0; + for (const auto &v : items) + { + if (v < mn) + mn = v; + if (v > mx) + mx = v; + double d = static_cast(v); + sum += d; + sum_sq += d * d; + } + double mean = sum / n; + double variance = (sum_sq / n) - (mean * mean); + double stddev = std::sqrt(variance < 0.0 ? 0.0 : variance); + + os << " count: " << n << '\n' + << " min: " << mn << '\n' + << " max: " << mx << '\n' + << " sum: " << sum << '\n' + << " mean: " << mean << '\n' + << " stddev: " << stddev << '\n'; + + // Histogram (5 buckets) + if (n >= 5 && mx > mn) + { + constexpr int BUCKETS = 5; + double range_w = static_cast(mx - mn); + double bucket_w = range_w / BUCKETS; + std::array hist{}; + for (const auto &v : items) + { + int idx = static_cast( + std::min(static_cast(BUCKETS - 1), + (static_cast(v) - static_cast(mn)) / bucket_w)); + ++hist[idx]; + } + os << " histogram:\n"; + for (int i = 0; i < BUCKETS; ++i) + { + double lo = static_cast(mn) + i * bucket_w; + double hi = lo + bucket_w; + int bar = hist[i] * 20 / static_cast(n); + os << " [" << std::setw(7) << lo + << ", " << std::setw(7) << hi << ") | "; + for (int j = 0; j < bar; ++j) + os << '#'; + os << " (" << hist[i] << ")\n"; + } + } + } + + namespace tree + { + /** + * @brief Recursive tree-rendering context. + * + * Renders nested structures as a visual tree using box-drawing characters. + */ + struct tree_context + { + std::ostream &os; + int depth = 0; + int max_depth = 6; + bool last = true; + std::string prefix; + + void node(std::string_view label) const + { + os << prefix; + os << (last ? "└─ " : "├─ "); + os << label; + } + + tree_context child_ctx(bool is_last) const + { + tree_context c{os, depth + 1, max_depth, is_last, + prefix + (last ? " " : "│ ")}; + return c; + } + }; + + template + void twrite(tree_context &tctx, std::string_view label, const T &value); + + template + void twrite_leaf(tree_context &tctx, std::string_view label, const T &value) + { + tctx.node(label); + tctx.os << ": "; + inspect_context ictx{tctx.os}; + detail::iwrite(ictx, value); + tctx.os << '\n'; + } + + template + void twrite_range(tree_context &tctx, std::string_view label, const Range &rng) + { + tctx.node(label); + std::size_t n = 0; + if constexpr (requires { rng.size(); }) + n = rng.size(); + else + n = static_cast(std::distance(std::begin(rng), std::end(rng))); + tctx.os << " [" << n << " items]\n"; + + if (tctx.depth >= tctx.max_depth) + return; + + std::size_t idx = 0; + std::size_t total = n; + for (const auto &elem : rng) + { + if (idx >= 8) + { + auto child = tctx.child_ctx(true); + child.node("..."); + child.os << " (" << (total - idx) << " more)\n"; + break; + } + bool is_last = (idx + 1 == total || idx + 1 == 8); + auto child = tctx.child_ctx(is_last); + std::string lbl = "[" + std::to_string(idx) + "]"; + if constexpr (traits::is_map_like_v) + { + std::ostringstream ks; + inspect_context kctx{ks}; + detail::iwrite(kctx, elem.first); + twrite(child, ks.str(), elem.second); + } + else + { + twrite(child, lbl, elem); + } + ++idx; + } + } + + template + void twrite(tree_context &tctx, std::string_view label, const T &value) + { + using U = std::remove_cvref_t; + + if (tctx.depth >= tctx.max_depth) + { + twrite_leaf(tctx, label, std::string_view{""}); + return; + } + + if constexpr (traits::is_map_like_v && traits::is_range_v) + { + twrite_range(tctx, label, value); + } + else if constexpr (traits::is_range_v && !traits::is_string_like_v) + { + twrite_range(tctx, label, value); + } + else if constexpr (traits::is_optional_v) + { + if (!value.has_value()) + { + twrite_leaf(tctx, label, std::string_view{"None"}); + } + else + { + auto child = tctx.child_ctx(true); + twrite(child, std::string(label) + " (Some)", *value); + } + } + else if constexpr (traits::is_variant_v) + { + std::string vlabel = std::string(label) + + " "; + std::visit([&tctx, &vlabel](const auto &v) + { + auto child = tctx.child_ctx(true); + twrite(child, vlabel, v); }, value); + } + else if constexpr (traits::has_field_map_v) + { + tctx.node(label); + tctx.os << " [" << demangle::short_type_name() << "]\n"; + if (tctx.depth < tctx.max_depth) + { + const auto fmap = field_map::fields(); + constexpr std::size_t N = + std::tuple_size_v>; + [&](std::index_sequence) + { + (( + [&]() + { + bool is_last = (Is + 1 == N); + auto child = tctx.child_ctx(is_last); + twrite(child, std::get(fmap).name, + std::get(fmap).get(value)); + }()), + ...); + }(std::make_index_sequence{}); + } + } + else + { + twrite_leaf(tctx, label, value); + } + } + + } // namespace tree + + /** + * @brief Print a visual tree representation of any value. + * + * Uses box-drawing characters to show structure recursively. + * + * @example + * rixlib::inspect_tree(my_config, "config"); + */ + template + void inspect_tree(const T &value, std::string_view root_label = "root") + { + auto &os = *default_options().out; + tree::tree_context tctx{os}; + tree::twrite(tctx, root_label, value); + } + +} // namespace rixlib + +namespace rixlib::debug +{ + /** + * @brief Object-style inspection API mounted into rixlib::debug::Debug. + */ + class Inspect + { + public: + template + void operator()(const T &value) const + { + rixlib::inspect(value); + } + + template + void to(std::ostream &os, const T &value) const + { + rixlib::inspect_to(os, value); + } + + template + [[nodiscard]] std::string to_string(const T &value) const + { + return rixlib::inspect_to_string(value); + } + + template + [[nodiscard]] std::string compact(const T &value) const + { + return rixlib::inspect_compact(value); + } + + template + [[nodiscard]] bool check(const Expected &expected, const Actual &actual) const + { + return rixlib::inspect_check("inspect.check", expected, actual); + } + + template + [[nodiscard]] bool check(std::string_view label, + const Expected &expected, + const Actual &actual) const + { + return rixlib::inspect_check(label, expected, actual); + } + }; +} + +#endif // RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_INSPECT_HPP_INCLUDED diff --git a/include/rix/debug/log.hpp b/include/rix/debug/log.hpp new file mode 100644 index 0000000..10ff97c --- /dev/null +++ b/include/rix/debug/log.hpp @@ -0,0 +1,73 @@ +/** + * @file log.hpp + * @brief Simple logging API for rix/debug. + */ + +#ifndef RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_LOG_HPP_INCLUDED +#define RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_LOG_HPP_INCLUDED + +#include +#include +#include + +#include + +namespace rixlib::debug +{ + /** + * @brief Simple formatted logging component for rix/debug. + */ + class Log + { + public: + /** + * @brief Write a debug-level log message to stdout. + */ + template + void operator()(std::string_view fmt, const Args &...args) const + { + write(std::cout, "debug", fmt, args...); + } + + /** + * @brief Write an info-level log message to stdout. + */ + template + void info(std::string_view fmt, const Args &...args) const + { + write(std::cout, "info", fmt, args...); + } + + /** + * @brief Write a warning-level log message to stdout. + */ + template + void warn(std::string_view fmt, const Args &...args) const + { + write(std::cout, "warn", fmt, args...); + } + + /** + * @brief Write an error-level log message to stderr. + */ + template + void error(std::string_view fmt, const Args &...args) const + { + write(std::cerr, "error", fmt, args...); + } + + private: + template + void write(std::ostream &out, + std::string_view level, + std::string_view fmt, + const Args &...args) const + { + out << '[' << level << "] " << format_(fmt, args...) << '\n'; + } + + Format format_{}; + }; +} + +#endif // RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_LOG_HPP_INCLUDED diff --git a/include/rix/debug/print.hpp b/include/rix/debug/print.hpp new file mode 100644 index 0000000..6a92e1b --- /dev/null +++ b/include/rix/debug/print.hpp @@ -0,0 +1,2018 @@ +/** + * + * @file print.hpp + * @author Gaspard Kirira + * + * Copyright 2025, Gaspard Kirira. All rights reserved. + * https://github.com/rixcpp/rix + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Rix + * + * Debug printing and formatting API for rix/debug. + * + */ + +#ifndef RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_PRINT_HPP_INCLUDED +#define RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_PRINT_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__cpp_lib_expected) && __cpp_lib_expected >= 202202L +#include +#define RIXCPP_DEBUG_HAS_EXPECTED 1 +#else +#define RIXCPP_DEBUG_HAS_EXPECTED 0 +#endif + +#include +#include +#include + +#include + +namespace rixlib +{ + /// @brief Primary formatter template — specialize for custom types. + template + struct formatter; + + namespace detail + { + + template + void write(std::ostream &os, const T &value); + + } // namespace detail + + /// @brief Controls rendering behaviour of rixlib::print. + struct print_config + { + std::string separator = " "; ///< Between multiple arguments + std::string end = "\n"; ///< After all arguments + std::ostream *out = &std::cout; ///< Output stream + bool color = false; ///< ANSI color (experimental) + std::size_t max_items = 256; ///< Max items shown in a container + bool show_type = false; ///< Show type annotations + bool compact = false; ///< Compact single-line output + std::string indent_str = " "; ///< Indentation unit (unused in compact) + bool raw_strings = true; + }; + + struct options + { + std::string sep = " "; + std::string end = "\n"; + std::ostream *file = &std::cout; + bool flush = false; + + bool raw_strings = true; + std::size_t max_items = 256; + bool compact = true; + std::string indent = " "; + bool show_type = false; + bool color = false; + }; + + /// @brief Thread-local default config (modifiable by user). + inline print_config &default_config() noexcept + { + static thread_local print_config cfg; + return cfg; + } + + [[nodiscard]] inline print_config to_print_config(const options &opts) + { + print_config cfg = default_config(); + cfg.separator = opts.sep; + cfg.end = opts.end; + cfg.out = opts.file; + cfg.raw_strings = opts.raw_strings; + cfg.max_items = opts.max_items; + cfg.compact = opts.compact; + cfg.indent_str = opts.indent; + cfg.show_type = opts.show_type; + cfg.color = opts.color; + return cfg; + } + + namespace detail + { + + // write — forward declaration + template + void write(std::ostream &os, const T &value); + + // Indentation helper + struct indent_guard + { + std::ostream &os; + int level; + bool compact; + std::string_view unit; + + void nl() const + { + if (!compact) + { + os << '\n'; + for (int i = 0; i < level; ++i) + os << unit; + } + } + }; + + // write_bool + inline void write_bool(std::ostream &os, bool v) + { + os << (v ? "true" : "false"); + } + + // write_char + inline void write_char(std::ostream &os, char c) + { + os << '\'' << c << '\''; + } + + inline void write_char(std::ostream &os, signed char c) + { + os << '\'' << static_cast(c) << '\''; + } + + inline void write_char(std::ostream &os, unsigned char c) + { + os << '\'' << static_cast(c) << '\''; + } + + inline void write_char(std::ostream &os, wchar_t c) + { + // Render wchar_t as U+XXXX + os << "L'\\u" << std::hex << std::uppercase + << static_cast(c) << std::dec << "'"; + } + + inline void write_char(std::ostream &os, char8_t c) + { + os << "u8'" << static_cast(c) << '\''; + } + + inline void write_char(std::ostream &os, char16_t c) + { + os << "u'\\u" << std::hex << std::uppercase + << static_cast(c) << std::dec << "'"; + } + + inline void write_char(std::ostream &os, char32_t c) + { + os << "U'\\U" << std::hex << std::uppercase + << static_cast(c) << std::dec << "'"; + } + + // write_nullptr + inline void write_nullptr(std::ostream &os) + { + os << "nullptr"; + } + + // write_string + inline void write_string(std::ostream &os, std::string_view sv) + { + os << '"' << sv << '"'; + } + + inline void write_wstring(std::ostream &os, std::wstring_view wsv) + { + // Convert wstring to UTF-8 for display + os << "L\""; + for (wchar_t wc : wsv) + { + if (wc < 0x80) + { + os << static_cast(wc); + } + else + { + os << "\\u" << std::hex << std::uppercase + << static_cast(wc) << std::dec; + } + } + os << '"'; + } + + // write_pointer + template + void write_pointer(std::ostream &os, const T *ptr) + { + if (ptr == nullptr) + { + os << "nullptr"; + } + else + { + os << "(ptr) << std::dec << ">"; + } + } + + // write_enum + template + requires std::is_enum_v + void write_enum(std::ostream &os, E e) + { + os << static_cast>(e); + } + +#ifdef _WIN32 + inline constexpr const char *kBoxTopLeft = "+"; + inline constexpr const char *kBoxTopRight = "+"; + inline constexpr const char *kBoxBottomLeft = "+"; + inline constexpr const char *kBoxBottomRight = "+"; + inline constexpr const char *kBoxHorizontal = "-"; + inline constexpr const char *kBoxVertical = "|"; +#else + inline constexpr const char *kBoxTopLeft = "\u250C"; + inline constexpr const char *kBoxTopRight = "\u2510"; + inline constexpr const char *kBoxBottomLeft = "\u2514"; + inline constexpr const char *kBoxBottomRight = "\u2518"; + inline constexpr const char *kBoxHorizontal = "\u2500"; + inline constexpr const char *kBoxVertical = "\u2502"; +#endif + // write_duration + template + void write_duration(std::ostream &os, + const std::chrono::duration &d) + { + // Attempt to render in the most meaningful unit + using namespace std::chrono; + + if constexpr (std::is_same_v) + { + os << d.count() << "ns"; + } + else if constexpr (std::is_same_v) + { +#ifdef _WIN32 + os << d.count() << "us"; +#else + os << d.count() << "µs"; +#endif + } + else if constexpr (std::is_same_v) + { + os << d.count() << "ms"; + } + else if constexpr (std::ratio_equal_v>) + { + os << d.count() << "s"; + } + else if constexpr (std::ratio_equal_v>) + { + os << d.count() << "min"; + } + else if constexpr (std::ratio_equal_v>) + { + os << d.count() << "h"; + } + else + { + // Generic fallback: show count + ratio + os << d.count() + << " [" << Period::num << '/' << Period::den << "]s"; + } + } + + // write_time_point + template + void write_time_point(std::ostream &os, + const std::chrono::time_point &tp) + { + // Show as duration since epoch + os << ""; + } + + // write_fs_path + inline void write_fs_path(std::ostream &os, + const std::filesystem::path &p) + { + os << "path(\"" << p.string() << "\")"; + } + + // write_optional + template + void write_optional(std::ostream &os, const std::optional &opt) + { + if (opt.has_value()) + { + os << "Some("; + write(os, *opt); + os << ')'; + } + else + { + os << "None"; + } + } + + // write_variant + template + void write_variant(std::ostream &os, const std::variant &var) + { + os << "variant<"; + std::visit([&os](const auto &v) + { write(os, v); }, var); + os << '>'; + } + + // write_any + inline void write_any(std::ostream &os, const std::any &a) + { + if (!a.has_value()) + { + os << "any(empty)"; + } + else + { + os << "any(" << a.type().name() << ")"; + } + } + + // write_reference_wrapper + template + void write_ref_wrapper(std::ostream &os, + const std::reference_wrapper &rw) + { + os << "ref("; + write(os, rw.get()); + os << ')'; + } + + // Tuple helpers + template + void write_tuple_elements(std::ostream &os, + const Tuple &t, + std::index_sequence) + { + ((Is == 0 ? void(0) : void(os << ", "), + write(os, std::get(t))), + ...); + } + + template + void write_tuple(std::ostream &os, const Tuple &t) + { + os << '('; + write_tuple_elements(os, t, + std::make_index_sequence>{}); + os << ')'; + } + + // Pair + template + void write_pair(std::ostream &os, const std::pair &p) + { + os << '('; + write(os, p.first); + os << ": "; + write(os, p.second); + os << ')'; + } + + // Map key-value pair + template + void write_kv_pair(std::ostream &os, const std::pair &kv) + { + write(os, kv.first); + os << " => "; + write(os, kv.second); + } + + // Range rendering + template + void write_range( + std::ostream &os, + const Range &rng, + char open, char close, + std::size_t max_items, + bool is_map = false) + { + os << open; + std::size_t count = 0; + bool first = true; + for (const auto &elem : rng) + { + if (count >= max_items) + { + os << ", ..."; + break; + } + if (!first) + os << ", "; + first = false; + if constexpr (traits::is_map_like_v) + { + write_kv_pair(os, elem); + } + else + { + write(os, elem); + } + ++count; + } + os << close; + (void)is_map; + } + + // write_smart_ptr + template + void write_unique_ptr(std::ostream &os, + const std::unique_ptr &p) + { + if (!p) + { + os << "unique_ptr(null)"; + } + else + { + os << "unique_ptr("; + write(os, *p); + os << ')'; + } + } + + template + void write_shared_ptr(std::ostream &os, const std::shared_ptr &p) + { + if (!p) + { + os << "shared_ptr(null)"; + } + else + { + os << "shared_ptr[use=" << p.use_count() << "]("; + write(os, *p); + os << ')'; + } + } + + template + void write_weak_ptr(std::ostream &os, const std::weak_ptr &p) + { + if (p.expired()) + { + os << "weak_ptr(expired)"; + } + else if (auto sp = p.lock()) + { + os << "weak_ptr[use=" << sp.use_count() << "]("; + write(os, *sp); + os << ')'; + } + else + { + os << "weak_ptr(null)"; + } + } + + // Container adapter helpers (copy-drain approach) + template + void write_stack( + std::ostream &os, + std::stack s, // by value — intentional copy + std::size_t max_items) + { + os << "stack["; + std::vector items; + items.reserve(s.size()); + while (!s.empty()) + { + items.push_back(s.top()); + s.pop(); + } + std::reverse(items.begin(), items.end()); + std::size_t count = 0; + bool first = true; + for (const auto &v : items) + { + if (count++ >= max_items) + { + os << ", ..."; + break; + } + if (!first) + os << ", "; + first = false; + write(os, v); + } + os << "]"; + } + + template + void write_queue( + std::ostream &os, + std::queue q, // by value — intentional copy + std::size_t max_items) + { + os << "queue["; + std::size_t count = 0; + bool first = true; + while (!q.empty()) + { + if (count++ >= max_items) + { + os << ", ..."; + break; + } + if (!first) + os << ", "; + first = false; + write(os, q.front()); + q.pop(); + } + os << "]"; + } + + template + void write_priority_queue( + std::ostream &os, + std::priority_queue pq, + std::size_t max_items) + { + os << "priority_queue["; + std::size_t count = 0; + bool first = true; + while (!pq.empty()) + { + if (count++ >= max_items) + { + os << ", ..."; + break; + } + if (!first) + os << ", "; + first = false; + write(os, pq.top()); + pq.pop(); + } + os << "]"; + } + +#if RIXCPP_DEBUG_HAS_EXPECTED + // write_expected + template + void write_expected(std::ostream &os, const std::expected &exp) + { + if (exp.has_value()) + { + os << "Ok("; + write(os, *exp); + os << ')'; + } + else + { + os << "Err("; + write(os, exp.error()); + os << ')'; + } + } +#endif + + // rixlib::formatter detection + template + struct has_formatter_specialization : std::false_type + { + }; + + template + struct has_formatter_specialization::format( + std::declval(), + std::declval()))>> + : std::true_type + { + }; + + template + inline constexpr bool has_formatter_v = + has_formatter_specialization>::value; + + /** + * @brief Central dispatch function — routes any type T to the correct renderer. + * + * Priority order: + * 1. rixlib::formatter specialization + * 2. ADL rix_format hook + * 3. nullptr_t + * 4. bool + * 5. char types + * 6. wstring / wstring_view + * 7. string-like types + * 8. std::filesystem::path + * 9. std::any + * 10. std::optional + * 11. std::variant + * 12. std::reference_wrapper + * 13. chrono duration + * 14. chrono time_point + * 15. smart pointers + * 16. container adapters + * 17. std::expected (C++23) + * 18. enum types + * 19. map-like ranges + * 20. set/sequence ranges + * 21. tuple-like (pair, tuple, array) + * 22. raw pointer + * 23. function pointer + * 24. streamable via operator<< + * 25. fallback: type name + */ + template + void write(std::ostream &os, const T &value) + { + using U = std::remove_cvref_t; + + // User formatter specialization + if constexpr (has_formatter_v) + { + rixlib::formatter::format(os, value); + } + // ADL rix_format hook + else if constexpr (traits::has_rix_format_v) + { + rix_format(os, value); + } + // nullptr_t + else if constexpr (traits::is_nullptr_v) + { + write_nullptr(os); + } + // bool + else if constexpr (traits::is_bool_v) + { + write_bool(os, value); + } + // char types + else if constexpr (traits::is_char_v) + { + write_char(os, value); + } + // wstring / wstring_view / const wchar_t* + else if constexpr (std::is_same_v || + std::is_same_v) + { + write_wstring(os, value); + } + else if constexpr (std::is_same_v || + std::is_same_v) + { + if (value == nullptr) + write_nullptr(os); + else + write_wstring(os, std::wstring_view{value}); + } + // String-like + else if constexpr (traits::is_string_like_v) + { + if (default_config().raw_strings) + { + os << value; + } + else + { + write_string(os, value); + } + } + // filesystem::path + else if constexpr (traits::is_fs_path_v) + { + write_fs_path(os, value); + } + // std::any + else if constexpr (traits::is_any_v) + { + write_any(os, value); + } + // std::optional + else if constexpr (traits::is_optional_v) + { + write_optional(os, value); + } + // std::variant + else if constexpr (traits::is_variant_v) + { + write_variant(os, value); + } + // std::reference_wrapper + else if constexpr (traits::is_reference_wrapper_v) + { + write_ref_wrapper(os, value); + } + // chrono duration + else if constexpr (traits::is_duration_v) + { + write_duration(os, value); + } + // chrono time_point + else if constexpr (traits::is_time_point_v) + { + write_time_point(os, value); + } + // smart pointers + else if constexpr (traits::is_unique_ptr::value) + { + write_unique_ptr(os, value); + } + else if constexpr (traits::is_shared_ptr::value) + { + write_shared_ptr(os, value); + } + else if constexpr (traits::is_weak_ptr::value) + { + write_weak_ptr(os, value); + } + // Container adapters + else if constexpr (traits::is_stack::value) + { + write_stack(os, value, default_config().max_items); + } + else if constexpr (traits::is_queue::value) + { + write_queue(os, value, default_config().max_items); + } + else if constexpr (traits::is_priority_queue::value) + { + write_priority_queue(os, value, default_config().max_items); + } +#if RIXCPP_DEBUG_HAS_EXPECTED + // std::expected + else if constexpr (traits::is_expected_v) + { + write_expected(os, value); + } +#endif + // enum + else if constexpr (traits::is_enum_v) + { + write_enum(os, value); + } + // map-like ranges + else if constexpr (traits::is_map_like_v && traits::is_range_v) + { + write_range(os, value, '{', '}', default_config().max_items, true); + } + // other ranges (vector, list, set, ...) + else if constexpr (traits::is_range_v) + { + write_range(os, value, '[', ']', default_config().max_items, false); + } + // tuple-like (pair, tuple, std::array of non-range) + else if constexpr (traits::is_pair_v) + { + write_pair(os, value); + } + else if constexpr (traits::is_tuple_like_v) + { + write_tuple(os, value); + } + // raw pointer + else if constexpr (std::is_pointer_v && !traits::is_string_like_v) + { + if constexpr (std::is_function_v>) + { + // function pointer: show address + os << "( + reinterpret_cast(value)) + << std::dec << ">"; + } + else + { + write_pointer(os, value); + } + } + else if constexpr (traits::is_ostreamable_v) + { + os << value; + } + else + { + os << ""; + } + } + + } // namespace detail + + /** + * @brief Specialize this template to teach rixlib::print how to render your type. + * + * @example + * template<> struct rixlib::formatter { + * static void format(std::ostream& os, const MyType& v) { + * os << "MyType{" << v.x << ", " << v.y << "}"; + * } + * }; + */ + template + struct formatter + { + // No default implementation — detected at compile time. + }; + + /** + * @brief Write a single value to an ostream using the rix rendering engine. + * @param os Destination stream. + * @param value Value to render. + */ + template + void write_to(std::ostream &os, const T &value) + { + detail::write(os, value); + } + + /** + * @brief Format a single value to a std::string. + * @param value Value to render. + * @return Rendered string representation. + */ + template + [[nodiscard]] std::string to_string(const T &value) + { + std::ostringstream oss; + detail::write(oss, value); + return oss.str(); + } + + namespace detail + { + + template + void print_impl(const print_config &cfg, const Args &...args) + { + std::ostream &os = *cfg.out; + bool first = true; + (( + [&]() + { + if (!first) + os << cfg.separator; + first = false; + write(os, args); + }()), + ...); + os << cfg.end; + } + + } // namespace detail + + /** + * @brief Print any number of values to stdout, separated by spaces. + * + * Behaviour mirrors Python's print(): + * - Arguments are separated by a space by default. + * - A newline is appended at the end by default. + * - Every type supported by the rix engine is rendered intelligently. + * + * @param args Zero or more values of any supported type. + * + * @example + * rixlib::print(1, "hello", std::vector{1,2,3}, std::nullopt); + * // Output: 1 "hello" [1, 2, 3] None + */ + template + void print(const Args &...args) + { + detail::print_impl(default_config(), args...); + } + + /** + * @brief Print with a custom configuration. + * @param cfg Rendering configuration. + * @param args Values to print. + */ + template + void print(const print_config &cfg, const Args &...args) + { + detail::print_impl(cfg, args...); + } + + template + void print(const options &opts, const Args &...args) + { + print_config cfg = to_print_config(opts); + detail::print_impl(cfg, args...); + + if (opts.flush && opts.file != nullptr) + { + opts.file->flush(); + } + } + + template + void print_py(const Args &...args) + { + print_config cfg = default_config(); + cfg.raw_strings = true; + detail::print_impl(cfg, args...); + } + + /** + * @brief Print to a specific stream. + * @param os Destination stream. + * @param args Values to print. + */ + template + void print_to(std::ostream &os, const Args &...args) + { + print(options{.file = &os}, args...); + } + + /** + * @brief Print to stderr. + * @param args Values to print. + */ + template + void eprint(const Args &...args) + { + print(options{.file = &std::cerr}, args...); + } + + /** + * @brief Print without a trailing newline. + * @param args Values to print. + */ + template + void print_inline(const Args &...args) + { + print(options{.end = ""}, args...); + } + + /** + * @brief Format arguments to a std::string (no output written). + * @param args Values to format. + * @return Formatted string. + */ + template + [[nodiscard]] std::string sprint(const Args &...args) + { + std::ostringstream oss; + print_config cfg = default_config(); + cfg.out = &oss; + cfg.end = ""; + detail::print_impl(cfg, args...); + return oss.str(); + } + + /** + * @brief Print a blank line (equivalent to Python's print()). + */ + inline void print() + { + *default_config().out << default_config().end; + } + + /** + * @brief Print a named value: `label: value`. + * @param label Label string. + * @param value Any printable value. + */ + template + void print_named(std::string_view label, const T &value) + { + auto &os = *default_config().out; + os << label << ": "; + detail::write(os, value); + os << default_config().end; + } + + /** + * @brief Print a horizontal separator line. + * @param width Line width (default 60). + * @param ch Fill character (default '─'). + */ + inline void print_separator(std::size_t width = 60, char ch = '-') + { + auto &os = *default_config().out; + for (std::size_t i = 0; i < width; ++i) + os << ch; + os << '\n'; + } + + /** + * @brief Print a titled section header. + * @param title Section title. + * @param width Total width. + */ + inline void print_header(std::string_view title, + std::size_t width = 60) + { + auto &os = *default_config().out; + print_separator(width); + std::size_t padding = (width > title.size() + 2) + ? (width - title.size() - 2) / 2 + : 0; + for (std::size_t i = 0; i < padding; ++i) + os << ' '; + os << ' ' << title << ' '; + os << '\n'; + print_separator(width); + } + + /** + * @brief Print each element of a range on its own line, optionally with index. + * @param rng Iterable range. + * @param show_index Prefix each line with its zero-based index. + */ + template + requires traits::is_range_v + void print_each(const Range &rng, bool show_index = false) + { + auto &os = *default_config().out; + std::size_t idx = 0; + for (const auto &elem : rng) + { + if (show_index) + os << '[' << idx++ << "] "; + detail::write(os, elem); + os << '\n'; + } + } + + /** + * @brief Print a map as a two-column table. + * @param m Map-like container. + * @param col_width Width of the key column. + */ + template + requires traits::is_map_like_v + void print_table(const Map &m, std::size_t col_width = 20) + { + auto &os = *default_config().out; + print_separator(col_width * 2 + 3); + for (const auto &[k, v] : m) + { + std::ostringstream ks, vs; + detail::write(ks, k); + detail::write(vs, v); + std::string ks_str = ks.str(); + std::string vs_str = vs.str(); + os << ks_str; + std::size_t pad = (col_width > ks_str.size()) + ? col_width - ks_str.size() + : 1; + for (std::size_t i = 0; i < pad; ++i) + os << ' '; + os << "| " << vs_str << '\n'; + } + print_separator(col_width * 2 + 3); + } + + /** + * @brief RAII guard that temporarily overrides the default print_config. + * + * @example + * { + * rixlib::scoped_config guard{}; + * guard.cfg.separator = ", "; + * guard.cfg.end = ";\n"; + * rixlib::print(1, 2, 3); // prints: 1, 2, 3; + * } + * // original config restored + */ + class scoped_config + { + public: + print_config cfg; + + explicit scoped_config(print_config override = default_config()) + : cfg(std::move(override)), saved_(default_config()) + { + default_config() = cfg; + } + + ~scoped_config() + { + default_config() = saved_; + } + + scoped_config(const scoped_config &) = delete; + scoped_config &operator=(const scoped_config &) = delete; + + private: + print_config saved_; + }; + + /** + * @brief Convenience base for formatter specializations using operator<<. + * + * Inherit from this in a formatter specialization to delegate to operator<<: + * + * template<> struct rixlib::formatter + * : rixlib::streamable_formatter {}; + */ + template + struct streamable_formatter + { + static void format(std::ostream &os, const T &v) + { + os << v; + } + }; + + // std::monostate + template <> + struct formatter + { + static void format(std::ostream &os, const std::monostate &) + { + os << "monostate"; + } + }; + + // std::byte + template <> + struct formatter + { + static void format(std::ostream &os, const std::byte &b) + { + os << "0x" << std::hex << std::uppercase + << static_cast(b) << std::dec; + } + }; + + // std::bitset + // Generic via operator<< detection — no special case needed. + + // std::complex + // operator<< exists for std::complex — handled automatically. + + // std::error_code + template <> + struct formatter + { + static void format(std::ostream &os, const std::error_code &ec) + { + os << "error_code(" << ec.value() + << ", \"" << ec.message() << "\")"; + } + }; + + template <> + struct formatter + { + static void format(std::ostream &os, const std::error_condition &ec) + { + os << "error_condition(" << ec.value() + << ", \"" << ec.message() << "\")"; + } + }; + + /** + * @brief Returns a human-readable description of how rix would render type T. + * + * Useful during development to understand the rendering path chosen. + */ + template + [[nodiscard]] std::string_view rendering_path() noexcept + { + using U = std::remove_cvref_t; + + if constexpr (detail::has_formatter_v) + return "rixlib::formatter specialization"; + else if constexpr (traits::has_rix_format_v) + return "ADL rix_format hook"; + else if constexpr (traits::is_nullptr_v) + return "nullptr_t"; + else if constexpr (traits::is_bool_v) + return "bool"; + else if constexpr (traits::is_char_v) + return "char type"; + else if constexpr (traits::is_string_like_v) + return "string-like"; + else if constexpr (traits::is_fs_path_v) + return "filesystem::path"; + else if constexpr (traits::is_any_v) + return "std::any"; + else if constexpr (traits::is_optional_v) + return "std::optional"; + else if constexpr (traits::is_variant_v) + return "std::variant"; + else if constexpr (traits::is_reference_wrapper_v) + return "std::reference_wrapper"; + else if constexpr (traits::is_duration_v) + return "chrono::duration"; + else if constexpr (traits::is_time_point_v) + return "chrono::time_point"; + else if constexpr (traits::is_unique_ptr::value) + return "std::unique_ptr"; + else if constexpr (traits::is_shared_ptr::value) + return "std::shared_ptr"; + else if constexpr (traits::is_weak_ptr::value) + return "std::weak_ptr"; + else if constexpr (traits::is_stack::value) + return "std::stack"; + else if constexpr (traits::is_queue::value) + return "std::queue"; + else if constexpr (traits::is_priority_queue::value) + return "std::priority_queue"; +#if RIXCPP_DEBUG_HAS_EXPECTED + else if constexpr (traits::is_expected_v) + return "std::expected"; +#endif + else if constexpr (traits::is_enum_v) + return "enum"; + else if constexpr (traits::is_map_like_v && traits::is_range_v) + return "map-like range"; + else if constexpr (traits::is_range_v) + return "range"; + else if constexpr (traits::is_pair_v) + return "std::pair"; + else if constexpr (traits::is_tuple_like_v) + return "tuple-like"; + else if constexpr (std::is_pointer_v) + return "raw pointer"; + else if constexpr (traits::is_ostreamable_v) + return "operator<< (streamable)"; + else + return "fallback (unprintable)"; + } + + /** + * @brief Print the rendering path for each type in a pack. + */ + template + void print_rendering_paths() + { + auto &os = *default_config().out; + ((os << typeid(Ts).name() << " => " + << rendering_path() << '\n'), + ...); + } + + namespace pretty + { + + /** + * @brief Context carried through the recursive pretty-print traversal. + */ + struct context + { + std::ostream &os; + int depth = 0; + int max_depth = 8; + std::size_t max_items = 32; + std::string indent_str = " "; + bool color = false; + + void indent() const + { + for (int i = 0; i < depth; ++i) + os << indent_str; + } + void newline() const { os << '\n'; } + }; + + // Forward declaration + template + void pwrite(context &ctx, const T &value); + + template + void pwrite_scalar(context &ctx, const T &v) + { + detail::write(ctx.os, v); + } + + template + requires traits::is_range_v + void pwrite_range(context &ctx, const Range &rng, char open, char close) + { + ctx.os << open; + std::size_t count = 0; + + for (const auto &elem : rng) + { + if (count >= ctx.max_items) + { + ctx.newline(); + ctx.indent(); + for (int i = 0; i < ctx.depth + 1; ++i) + ctx.os << ctx.indent_str; + ctx.os << "..."; + break; + } + + ctx.newline(); + for (int i = 0; i <= ctx.depth; ++i) + ctx.os << ctx.indent_str; + + if constexpr (traits::is_map_like_v) + { + detail::write(ctx.os, elem.first); + ctx.os << " => "; + + context inner{ + ctx.os, + ctx.depth + 1, + ctx.max_depth, + ctx.max_items, + ctx.indent_str, + ctx.color}; + + pwrite(inner, elem.second); + } + else + { + context inner{ + ctx.os, + ctx.depth + 1, + ctx.max_depth, + ctx.max_items, + ctx.indent_str, + ctx.color}; + + pwrite(inner, elem); + } + + ++count; + } + + if (count > 0) + { + ctx.newline(); + ctx.indent(); + } + + ctx.os << close; + } + + template + void pwrite_tuple_elems( + context &ctx, const Tuple &t, + std::index_sequence) + { + (( + [&]() + { + ctx.newline(); + for (int i = 0; i <= ctx.depth; ++i) + ctx.os << ctx.indent_str; + context inner{ctx.os, ctx.depth + 1, ctx.max_depth, + ctx.max_items, ctx.indent_str, ctx.color}; + pwrite(inner, std::get(t)); + }()), + ...); + } + + template + void pwrite_tuple(context &ctx, const Tuple &t) + { + ctx.os << '('; + pwrite_tuple_elems(ctx, t, std::make_index_sequence>{}); + ctx.newline(); + ctx.indent(); + ctx.os << ')'; + } + + template + void pwrite_optional(context &ctx, const std::optional &opt) + { + if (!opt.has_value()) + { + ctx.os << "None"; + return; + } + ctx.os << "Some("; + context inner{ctx.os, ctx.depth + 1, ctx.max_depth, + ctx.max_items, ctx.indent_str, ctx.color}; + if constexpr (traits::is_range_v || traits::is_tuple_like_v) + { + ctx.newline(); + for (int i = 0; i <= ctx.depth; ++i) + ctx.os << ctx.indent_str; + pwrite(inner, *opt); + ctx.newline(); + ctx.indent(); + } + else + { + pwrite(inner, *opt); + } + ctx.os << ')'; + } + + template + void pwrite_variant(context &ctx, const std::variant &var) + { + ctx.os << "variant<"; + std::visit([&ctx](const auto &v) + { + context inner{ctx.os, ctx.depth, ctx.max_depth, + ctx.max_items, ctx.indent_str, ctx.color}; + pwrite(inner, v); }, var); + ctx.os << '>'; + } + + template + void pwrite_unique(context &ctx, const std::unique_ptr &p) + { + if (!p) + { + ctx.os << "unique_ptr(null)"; + return; + } + ctx.os << "unique_ptr("; + context inner{ctx.os, ctx.depth + 1, ctx.max_depth, + ctx.max_items, ctx.indent_str, ctx.color}; + pwrite(inner, *p); + ctx.os << ')'; + } + + template + void pwrite_shared(context &ctx, const std::shared_ptr &p) + { + if (!p) + { + ctx.os << "shared_ptr(null)"; + return; + } + ctx.os << "shared_ptr[use=" << p.use_count() << "]("; + context inner{ctx.os, ctx.depth + 1, ctx.max_depth, + ctx.max_items, ctx.indent_str, ctx.color}; + pwrite(inner, *p); + ctx.os << ')'; + } + + template + void pwrite(context &ctx, const T &value) + { + if (ctx.depth >= ctx.max_depth) + { + ctx.os << ""; + return; + } + + using U = std::remove_cvref_t; + + if constexpr (traits::is_map_like_v && traits::is_range_v) + { + pwrite_range(ctx, value, '{', '}'); + } + else if constexpr (traits::is_range_v) + { + pwrite_range(ctx, value, '[', ']'); + } + else if constexpr (traits::is_tuple_like_v && !traits::is_range_v) + { + pwrite_tuple(ctx, value); + } + else if constexpr (traits::is_optional_v) + { + pwrite_optional(ctx, value); + } + else if constexpr (traits::is_variant_v) + { + pwrite_variant(ctx, value); + } + else if constexpr (traits::is_unique_ptr::value) + { + pwrite_unique(ctx, value); + } + else if constexpr (traits::is_shared_ptr::value) + { + pwrite_shared(ctx, value); + } + else + { + pwrite_scalar(ctx, value); + } + } + + } // namespace pretty + + /** + * @brief Multi-line pretty-print with full indentation. + * + * Unlike rixlib::print which renders on a single line, + * rixlib::pprint formats nested structures across multiple lines. + * + * @param value Value to pretty-print. + * @param cfg Optional config override. + */ + template + void pprint(const T &value, + const print_config &cfg = default_config()) + { + pretty::context ctx{*cfg.out}; + pretty::pwrite(ctx, value); + *cfg.out << cfg.end; + } + + /** + * @brief Multi-line pretty-print for multiple values. + */ + template + void pprint_all(const Args &...args) + { + auto &os = *default_config().out; + ([&]() + { + pretty::context ctx2{os}; + pretty::pwrite(ctx2, args); + os << '\n'; }(), ...); + } + + /** + * @brief Print statistics for a numeric range (min, max, sum, average). + * + * @param rng Range of arithmetic values. + * @param label Optional label. + */ + template + requires traits::is_range_v && + std::is_arithmetic_v< + std::remove_cvref_t()))>> + void print_stats(const Range &rng, std::string_view label = "") + { + using V = std::remove_cvref_t; + auto &os = *default_config().out; + + if (label.size()) + os << label << ": "; + + if (std::begin(rng) == std::end(rng)) + { + os << "stats(empty)\n"; + return; + } + + V mn = *std::begin(rng); + V mx = mn; + double sum = 0.0; + std::size_t n = 0; + + for (const auto &v : rng) + { + if (v < mn) + mn = v; + if (v > mx) + mx = v; + sum += static_cast(v); + ++n; + } + + os << "stats{n=" << n + << ", min=" << mn + << ", max=" << mx + << ", sum=" << sum + << ", avg=" << (n ? sum / n : 0.0) + << "}\n"; + } + + /** + * @brief Print map sorted by values (ascending). + * @param m Map whose value type supports operator<. + */ + template + requires traits::is_map_like_v + void print_sorted_by_value(const Map &m) + { + using K = typename Map::key_type; + using V = typename Map::mapped_type; + std::vector> items(m.begin(), m.end()); + std::sort(items.begin(), items.end(), + [](const auto &a, const auto &b) + { return a.second < b.second; }); + detail::write_range(*default_config().out, items, '{', '}', + default_config().max_items, false); + *default_config().out << '\n'; + } + + /** + * @brief Print only elements of a range satisfying a predicate. + * @param rng Source range. + * @param pred Predicate (element -> bool). + */ + template + requires traits::is_range_v + void print_where(const Range &rng, Pred &&pred) + { + auto &os = *default_config().out; + os << '['; + bool first = true; + std::size_t count = 0; + for (const auto &elem : rng) + { + if (count >= default_config().max_items) + { + os << ", ..."; + break; + } + if (pred(elem)) + { + if (!first) + os << ", "; + first = false; + detail::write(os, elem); + ++count; + } + } + os << "]\n"; + } + + /** + * @brief Print a range transformed by a function. + * @param rng Source range. + * @param fn Transform function (element -> U). + */ + template + requires traits::is_range_v + void print_map_fn(const Range &rng, Fn &&fn) + { + auto &os = *default_config().out; + os << '['; + bool first = true; + std::size_t count = 0; + for (const auto &elem : rng) + { + if (count++ >= default_config().max_items) + { + os << ", ..."; + break; + } + if (!first) + os << ", "; + first = false; + detail::write(os, fn(elem)); + } + os << "]\n"; + } + + /** + * @brief A lightweight string builder that appends Rix-formatted tokens. + * + * @example + * rixlib::string_builder sb; + * sb.append("Result: ").append(42).append(", values: ").append(vec); + * std::string s = sb.str(); + */ + class string_builder + { + public: + string_builder() = default; + + template + string_builder &append(const T &v) + { + detail::write(buf_, v); + return *this; + } + + string_builder &append_raw(std::string_view sv) + { + buf_ << sv; + return *this; + } + + string_builder &sep(std::string_view s = " ") + { + if (has_content_) + buf_ << s; + has_content_ = true; + return *this; + } + + template + string_builder &sep_append(const T &v, std::string_view s = " ") + { + return sep(s).append(v); + } + + [[nodiscard]] std::string str() const { return buf_.str(); } + [[nodiscard]] bool empty() const { return buf_.str().empty(); } + + void clear() + { + buf_.str({}); + buf_.clear(); + has_content_ = false; + } + + /// Implicit conversion to std::string + explicit operator std::string() const { return str(); } + + friend std::ostream &operator<<(std::ostream &os, const string_builder &sb) + { + return os << sb.str(); + } + + private: + std::ostringstream buf_; + bool has_content_ = false; + }; + + /** + * @brief Print only if a condition is true. + * @param cond Condition. + * @param args Values to print if condition holds. + */ + template + void print_if(bool cond, const Args &...args) + { + if (cond) + detail::print_impl(default_config(), args...); + } + + /** + * @brief Print only in debug builds (when NDEBUG is not defined). + */ + template + void dprint([[maybe_unused]] const Args &...args) + { +#ifndef NDEBUG + print_config cfg = default_config(); + cfg.out = &std::cerr; + detail::print_impl(cfg, args...); +#endif + } + + /** + * @brief Print value and return it unchanged (inspect without disrupting flow). + * + * @example + * int result = rixlib::tap(compute_something()); + * // Prints the result and returns it for further use. + */ + template + const T &tap(const T &value, std::string_view label = "") + { + auto &os = *default_config().out; + if (!label.empty()) + os << label << ": "; + detail::write(os, value); + os << '\n'; + return value; + } + + /** + * @brief Print a value with its type name. + * + * @example + * rixlib::print_typed(42); // prints: int: 42 + */ + template + void print_typed(const T &v) + { + auto &os = *default_config().out; + os << typeid(std::remove_cvref_t).name() << ": "; + detail::write(os, v); + os << '\n'; + } + + /** + * @brief Print two values side-by-side for comparison. + */ + template + void print_diff( + const A &a, const B &b, + std::string_view label_a = "left", + std::string_view label_b = "right") + { + auto &os = *default_config().out; + os << label_a << ": "; + detail::write(os, a); + os << '\n'; + os << label_b << ": "; + detail::write(os, b); + os << '\n'; + os << "equal: " << std::boolalpha; + if constexpr (requires { a == b; }) + { + os << (a == b); + } + else + { + os << ""; + } + os << '\n'; + } + + /** + * @brief Assert-and-print: print a labelled check result. + * + * Does not throw — only prints [PASS] or [FAIL] and the values. + */ + template + bool print_check(std::string_view label, const A &a, const B &b) + { + auto &os = *default_config().out; + bool eq = false; + if constexpr (requires { a == b; }) + { + eq = (a == b); + } + os << (eq ? "[PASS] " : "[FAIL] ") << label << '\n'; + if (!eq) + { + os << " expected: "; + detail::write(os, b); + os << '\n'; + os << " got: "; + detail::write(os, a); + os << '\n'; + } + return eq; + } + + namespace ansi + { + // ANSI escape sequences + inline constexpr std::string_view RESET = "\033[0m"; + inline constexpr std::string_view BOLD = "\033[1m"; + inline constexpr std::string_view RED = "\033[31m"; + inline constexpr std::string_view GREEN = "\033[32m"; + inline constexpr std::string_view YELLOW = "\033[33m"; + inline constexpr std::string_view BLUE = "\033[34m"; + inline constexpr std::string_view MAGENTA = "\033[35m"; + inline constexpr std::string_view CYAN = "\033[36m"; + inline constexpr std::string_view WHITE = "\033[37m"; + inline constexpr std::string_view GRAY = "\033[90m"; + + /** + * @brief Wrap a string in ANSI color codes. + * @param s Content. + * @param color ANSI escape sequence. + * @return Colored string. + */ + [[nodiscard]] inline std::string colorize( + std::string_view s, + std::string_view color) + { + return std::string{color} + std::string{s} + std::string{RESET}; + } + + /** + * @brief Print a success message in green. + */ + template + void print_ok(const Args &...args) + { + *default_config().out << GREEN; + detail::print_impl(default_config(), args...); + *default_config().out << RESET; + } + + /** + * @brief Print an error message in red. + */ + template + void print_err(const Args &...args) + { + print_config cfg = default_config(); + cfg.out = &std::cerr; + *cfg.out << RED; + detail::print_impl(cfg, args...); + *cfg.out << RESET; + } + + /** + * @brief Print a warning message in yellow. + */ + template + void print_warn(const Args &...args) + { + *default_config().out << YELLOW; + detail::print_impl(default_config(), args...); + *default_config().out << RESET; + } + + } // namespace ansi + + /** + * @brief Summarize a range: show size, first N elements, and last M elements. + * + * @param rng The range to summarize. + * @param head How many elements to show from the start. + * @param tail How many elements to show from the end. + */ + template + requires traits::is_range_v + void print_summary( + const Range &rng, + std::size_t head = 3, + std::size_t tail = 3) + { + auto &os = *default_config().out; + + // Collect into vector for random access + using V = std::remove_cvref_t; + std::vector items(std::begin(rng), std::end(rng)); + const std::size_t n = items.size(); + + os << "range[n=" << n << "]("; + + if (n == 0) + { + os << ")\n"; + return; + } + + std::size_t show_head = std::min(head, n); + for (std::size_t i = 0; i < show_head; ++i) + { + if (i > 0) + os << ", "; + detail::write(os, items[i]); + } + + if (show_head < n) + { + std::size_t show_tail = std::min(tail, n - show_head); + if (show_head + show_tail < n) + { + os << ", ...[" << (n - show_head - show_tail) << " hidden]..., "; + } + else + { + os << ", "; + } + std::size_t start = n - show_tail; + for (std::size_t i = start; i < n; ++i) + { + if (i > start) + os << ", "; + detail::write(os, items[i]); + } + } + + os << ")\n"; + } + + inline void print_boxed( + std::string_view message, + std::size_t min_width = 40) + { + auto &os = *default_config().out; + const std::size_t content_width = std::max(message.size() + 4, min_width); + + // Top border + os << detail::kBoxTopLeft; + for (std::size_t i = 0; i < content_width; ++i) + os << detail::kBoxHorizontal; + os << detail::kBoxTopRight << '\n'; + + // Content line + os << detail::kBoxVertical << " " << message; + const std::size_t pad = content_width - message.size() - 2; + for (std::size_t i = 0; i < pad; ++i) + os << ' '; + os << detail::kBoxVertical << '\n'; + + // Bottom border + os << detail::kBoxBottomLeft; + for (std::size_t i = 0; i < content_width; ++i) + os << detail::kBoxHorizontal; + os << detail::kBoxBottomRight << '\n'; + } + + /** + * @brief Print a value as boxed, rendering it via vix engine. + */ + template + void print_boxed_value(std::string_view label, const T &v) + { + print_boxed(rixlib::sprint(label, ": ", v)); + } + +} // namespace rixlib + +#endif // RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_PRINT_HPP_INCLUDED diff --git a/include/rix/debug/traits.hpp b/include/rix/debug/traits.hpp new file mode 100644 index 0000000..71e963f --- /dev/null +++ b/include/rix/debug/traits.hpp @@ -0,0 +1,624 @@ +/** + * + * @file traits.hpp + * @author Gaspard Kirira + * + * Copyright 2025, Gaspard Kirira. All rights reserved. + * https://github.com/rixcpp/rix + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Rix + * + * Shared compile-time traits used by rix/debug for print, + * format, logging, and inspection. + * + */ + +#ifndef RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_TRAITS_HPP_INCLUDED +#define RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_TRAITS_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__cpp_lib_expected) && __cpp_lib_expected >= 202202L +#include +#define RIXCPP_DEBUG_HAS_EXPECTED 1 +#else +#define RIXCPP_DEBUG_HAS_EXPECTED 0 +#endif + +namespace rixlib +{ + // Forward declarations for extension-point detection. + struct inspect_context; + + template + struct formatter; + + template + struct inspector; + + template + struct field_map; + + namespace traits + { + // ------------------------------------------------------------ + // ostreamability + // ------------------------------------------------------------ + template + struct is_ostreamable : std::false_type + { + }; + + template + struct is_ostreamable< + T, + std::void_t() << std::declval())>> + : std::true_type + { + }; + + template + inline constexpr bool is_ostreamable_v = is_ostreamable::value; + + // ------------------------------------------------------------ + // string-like + // ------------------------------------------------------------ + template + struct is_string_like : std::false_type + { + }; + + template <> + struct is_string_like : std::true_type + { + }; + + template <> + struct is_string_like : std::true_type + { + }; + + template <> + struct is_string_like : std::true_type + { + }; + + template <> + struct is_string_like : std::true_type + { + }; + + template <> + struct is_string_like : std::true_type + { + }; + + template <> + struct is_string_like : std::true_type + { + }; + + template <> + struct is_string_like : std::true_type + { + }; + + template <> + struct is_string_like : std::true_type + { + }; + + template + struct is_string_like : std::true_type + { + }; + + template + struct is_string_like : std::true_type + { + }; + + template + struct is_string_like : std::true_type + { + }; + + template + struct is_string_like : std::true_type + { + }; + + template + inline constexpr bool is_string_like_v = is_string_like>::value; + + // ------------------------------------------------------------ + // range + // ------------------------------------------------------------ + template + struct is_range : std::false_type + { + }; + + template + struct is_range< + T, + std::void_t< + decltype(std::begin(std::declval())), + decltype(std::end(std::declval()))>> + : std::bool_constant> + { + }; + + template + inline constexpr bool is_range_v = is_range::value; + + // ------------------------------------------------------------ + // map-like + // ------------------------------------------------------------ + template + struct is_map_like : std::false_type + { + }; + + template + struct is_map_like> + : std::true_type + { + }; + + template + inline constexpr bool is_map_like_v = is_map_like::value; + + // ------------------------------------------------------------ + // set-like + // ------------------------------------------------------------ + template + struct is_set_like : std::false_type + { + }; + + template + struct is_set_like> + : std::bool_constant> + { + }; + + template + inline constexpr bool is_set_like_v = is_set_like::value; + + // ------------------------------------------------------------ + // tuple-like + // ------------------------------------------------------------ + template + struct is_tuple_like : std::false_type + { + }; + + template + struct is_tuple_like::value)>> + : std::true_type + { + }; + + template <> + struct is_tuple_like : std::false_type + { + }; + + template <> + struct is_tuple_like : std::false_type + { + }; + + template + inline constexpr bool is_tuple_like_v = is_tuple_like>::value; + + // ------------------------------------------------------------ + // pair + // ------------------------------------------------------------ + template + struct is_pair : std::false_type + { + }; + + template + struct is_pair> : std::true_type + { + }; + + template + inline constexpr bool is_pair_v = is_pair>::value; + + // ------------------------------------------------------------ + // smart pointers + // ------------------------------------------------------------ + template + struct is_unique_ptr : std::false_type + { + }; + + template + struct is_unique_ptr> : std::true_type + { + }; + + template + struct is_shared_ptr : std::false_type + { + }; + + template + struct is_shared_ptr> : std::true_type + { + }; + + template + struct is_weak_ptr : std::false_type + { + }; + + template + struct is_weak_ptr> : std::true_type + { + }; + + template + inline constexpr bool is_smart_ptr_v = + is_unique_ptr>::value || + is_shared_ptr>::value || + is_weak_ptr>::value; + + // ------------------------------------------------------------ + // optional / variant / any + // ------------------------------------------------------------ + template + struct is_optional : std::false_type + { + }; + + template + struct is_optional> : std::true_type + { + }; + + template + inline constexpr bool is_optional_v = is_optional>::value; + + template + struct is_variant : std::false_type + { + }; + + template + struct is_variant> : std::true_type + { + }; + + template + inline constexpr bool is_variant_v = is_variant>::value; + + template + inline constexpr bool is_any_v = + std::is_same_v, std::any>; + + // ------------------------------------------------------------ + // chrono + // ------------------------------------------------------------ + template + struct is_duration : std::false_type + { + }; + + template + struct is_duration> : std::true_type + { + }; + + template + inline constexpr bool is_duration_v = is_duration>::value; + + template + struct is_time_point : std::false_type + { + }; + + template + struct is_time_point> : std::true_type + { + }; + + template + inline constexpr bool is_time_point_v = is_time_point>::value; + + // ------------------------------------------------------------ + // filesystem::path + // ------------------------------------------------------------ + template + inline constexpr bool is_fs_path_v = + std::is_same_v, std::filesystem::path>; + + // ------------------------------------------------------------ + // reference_wrapper + // ------------------------------------------------------------ + template + struct is_reference_wrapper : std::false_type + { + }; + + template + struct is_reference_wrapper> : std::true_type + { + }; + + template + inline constexpr bool is_reference_wrapper_v = + is_reference_wrapper>::value; + + // alias kept for compatibility with existing inspect.hpp code + template + using is_ref_wrapper = is_reference_wrapper; + + template + inline constexpr bool is_ref_wrapper_v = is_reference_wrapper_v; + + // ------------------------------------------------------------ + // raw pointer / function pointer / nullptr + // ------------------------------------------------------------ + template + inline constexpr bool is_raw_pointer_v = + std::is_pointer_v && + !is_string_like_v && + !std::is_function_v>; + + template + inline constexpr bool is_function_ptr_v = + std::is_pointer_v && + std::is_function_v>; + + template + inline constexpr bool is_nullptr_v = + std::is_same_v, std::nullptr_t>; + + // ------------------------------------------------------------ + // bool / char / enum + // ------------------------------------------------------------ + template + inline constexpr bool is_bool_v = + std::is_same_v, bool>; + + template + inline constexpr bool is_char_v = + std::is_same_v, char> || + std::is_same_v, signed char> || + std::is_same_v, unsigned char> || + std::is_same_v, wchar_t> || + std::is_same_v, char8_t> || + std::is_same_v, char16_t> || + std::is_same_v, char32_t>; + + template + inline constexpr bool is_enum_v = + std::is_enum_v>; + + // ------------------------------------------------------------ + // container adapters + // ------------------------------------------------------------ + template + struct is_stack : std::false_type + { + }; + + template + struct is_stack> : std::true_type + { + }; + + template + struct is_queue : std::false_type + { + }; + + template + struct is_queue> : std::true_type + { + }; + + template + struct is_priority_queue : std::false_type + { + }; + + template + struct is_priority_queue> : std::true_type + { + }; + + template + inline constexpr bool is_adapter_v = + is_stack>::value || + is_queue>::value || + is_priority_queue>::value; + + // ------------------------------------------------------------ + // formatter detection + // ------------------------------------------------------------ + template + struct has_formatter_specialization : std::false_type + { + }; + + template + struct has_formatter_specialization< + T, + std::void_t::format( + std::declval(), + std::declval()))>> : std::true_type + { + }; + + template + inline constexpr bool has_formatter_v = + has_formatter_specialization>::value; + + // ------------------------------------------------------------ + // ADL rix_format detection + // ------------------------------------------------------------ + template + struct has_rix_format : std::false_type + { + }; + + template + struct has_rix_format< + T, + std::void_t(), + std::declval()))>> : std::true_type + { + }; + + template + inline constexpr bool has_rix_format_v = has_rix_format::value; + + // ------------------------------------------------------------ + // inspect-specific extension points + // ------------------------------------------------------------ + template + struct has_rix_inspect_hook : std::false_type + { + }; + + template + struct has_rix_inspect_hook< + T, + std::void_t(), + std::declval()))>> : std::true_type + { + }; + + template + inline constexpr bool has_rix_inspect_hook_v = + has_rix_inspect_hook>::value; + + template + struct has_inspector_specialization : std::false_type + { + }; + + template + struct has_inspector_specialization< + T, + std::void_t::inspect( + std::declval(), + std::declval()))>> : std::true_type + { + }; + + template + inline constexpr bool has_inspector_v = + has_inspector_specialization>::value; + + template + struct has_field_map : std::false_type + { + }; + + template + struct has_field_map::fields())>> + : std::true_type + { + }; + + template + inline constexpr bool has_field_map_v = + has_field_map>::value; + +#if RIXCPP_DEBUG_HAS_EXPECTED + // ------------------------------------------------------------ + // std::expected + // ------------------------------------------------------------ + template + struct is_expected : std::false_type + { + }; + + template + struct is_expected> : std::true_type + { + }; + + template + inline constexpr bool is_expected_v = + is_expected>::value; +#endif + + } // namespace traits + + namespace concepts + { + template + concept Streamable = traits::is_ostreamable_v; + + template + concept Ostreamable = traits::is_ostreamable_v; + + template + concept StringLike = traits::is_string_like_v; + + template + concept Range = traits::is_range_v; + + template + concept MapLike = traits::is_map_like_v; + + template + concept SetLike = traits::is_set_like_v && !MapLike; + + template + concept TupleLike = traits::is_tuple_like_v && !Range; + + template + concept HasRixFormat = traits::has_rix_format_v; + + template + concept HasRixInspect = traits::has_rix_inspect_hook_v; + + template + concept HasInspector = traits::has_inspector_v; + + template + concept HasFieldMap = traits::has_field_map_v; + + template + concept SmartPointer = traits::is_smart_ptr_v; + + template + concept Duration = traits::is_duration_v; + + template + concept TimePoint = traits::is_time_point_v; + } // namespace concepts + +} // namespace rixlib + +#endif // RIXCPP_DEBUG_INCLUDE_RIX_DEBUG_TRAITS_HPP_INCLUDED diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..c3d5204 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,13 @@ +add_executable(rix_debug_tests + debug_tests.cpp +) + +target_link_libraries(rix_debug_tests + PRIVATE + rix::debug +) + +add_test( + NAME rix_debug_tests + COMMAND rix_debug_tests +) diff --git a/tests/debug_tests.cpp b/tests/debug_tests.cpp new file mode 100644 index 0000000..9b45049 --- /dev/null +++ b/tests/debug_tests.cpp @@ -0,0 +1,149 @@ +/** + * @file debug_tests.cpp + * @brief Basic tests for rix/debug. + * + * @author Gaspard Kirira + */ + +#include + +#include +#include +#include +#include + +namespace +{ + void expect_true(bool condition, const std::string &message) + { + if (!condition) + { + std::cerr << "FAILED: " << message << '\n'; + std::exit(1); + } + } + + void test_format_auto_placeholders() + { + const rixlib::debug::Debug debug; + + const std::string output = debug.format("Hello {}", "Rix"); + + expect_true( + output == "Hello Rix", + "debug.format should support automatic placeholders"); + } + + void test_format_explicit_placeholders() + { + const rixlib::debug::Debug debug; + + const std::string output = debug.format("{0} + {0} = {1}", 2, 4); + + expect_true( + output == "2 + 2 = 4", + "debug.format should support explicit placeholders"); + } + + void test_format_escaped_braces() + { + const rixlib::debug::Debug debug; + + const std::string output = debug.format("{{ value }} = {}", 42); + + expect_true( + output == "{ value } = 42", + "debug.format should support escaped braces"); + } + + void test_format_append() + { + const rixlib::debug::Debug debug; + + std::string output = "prefix: "; + debug.format.append(output, "{}", "ready"); + + expect_true( + output == "prefix: ready", + "debug.format.append should append formatted text"); + } + + void test_format_to() + { + const rixlib::debug::Debug debug; + + std::string output = "old"; + debug.format.to(output, "status: {}", "ok"); + + expect_true( + output == "status: ok", + "debug.format.to should replace destination content"); + } + + void test_print_to_stream() + { + std::ostringstream out; + + rixlib::print_to(out, "Hello", "Rix"); + + expect_true( + out.str() == "Hello Rix\n", + "rixlib::print_to should write arguments separated by spaces with newline"); + } + + void test_inspect_to_string() + { + const rixlib::debug::Debug debug; + + expect_true( + debug.inspect.to_string(42) == "42", + "debug.inspect.to_string should render integers"); + + expect_true( + debug.inspect.to_string(true) == "true", + "debug.inspect.to_string should render booleans"); + } + + void test_inspect_check_pass() + { + const rixlib::debug::Debug debug; + + const bool ok = debug.inspect.check(42, 42); + + expect_true( + ok, + "debug.inspect.check should return true when values are equal"); + } + + void test_inspect_check_fail() + { + const rixlib::debug::Debug debug; + + const bool ok = debug.inspect.check(42, 24); + + expect_true( + !ok, + "debug.inspect.check should return false when values are different"); + } + + void run_tests() + { + test_format_auto_placeholders(); + test_format_explicit_placeholders(); + test_format_escaped_braces(); + test_format_append(); + test_format_to(); + test_print_to_stream(); + test_inspect_to_string(); + test_inspect_check_pass(); + test_inspect_check_fail(); + } +} + +int main() +{ + run_tests(); + + std::cout << "debug tests passed\n"; + return 0; +} diff --git a/vix.json b/vix.json new file mode 100644 index 0000000..e21d73a --- /dev/null +++ b/vix.json @@ -0,0 +1,30 @@ +{ + "name": "debug", + "namespace": "rix", + "version": "0.2.0", + "type": "header-only", + "include": "include", + "license": "MIT", + "description": "Debug printing, formatting, logging, and inspection utilities for Rix.", + "keywords": [ + "cpp", + "debug", + "print", + "format", + "log", + "inspect", + "rix", + "vix" + ], + "repository": "https://github.com/rixcpp/debug", + "maintainers": [ + { + "name": "Gaspard Kirira", + "github": "rixcpp" + } + ], + "cmake": { + "target": "rix::debug" + }, + "deps": [] +}