xo-umbrella2/xo-ppsink/include/xo/ppsink/scope.hpp
Roland Conybeare 82252666f2
All checks were successful
cmake-docker / cmake-build (clang, clang++) (push) Successful in 32m4s
cmake-docker / cmake-build (gcc, g++) (push) Successful in 29m34s
CI / smoke-test (push) Successful in 52m15s
xo-ppsink: allow lpos-relative begin [FEATURE]
2026-08-22 14:11:30 -04:00

339 lines
14 KiB
C++

/** @file scope.hpp
*
* @author Roland Conybeare, Jul 2026
*
* Minimal indenting scope-logger POC, built on xo::pp::PpSink.
*
* This is the arena-free logging front-end: it composes output through a
* PpSink (see xo::pp::LogState for the per-thread active sink), so the
* same scope code drives flat output (FlatSink, here) and, later,
* arena-backed pretty output (PrettySink, in xo-indentlog2).
*
* POC scope: entry/exit banners + per-nesting-level indentation + log() +
* log-level gating. Deliberately omits (for now) color, timestamps, code
* location, and the function-name styling of the legacy xo-indentlog scope.
*
* NOTE: this header is intentionally macro-free. The terse XO_SCOPE_/
* XO_ENTER0_ convenience macros live in the opt-in header scope_macros.hpp
* so that scope.hpp can coexist in one translation unit with the legacy
* <xo/indentlog/scope.hpp> (which defines its own XO_* macros).
**/
#pragma once
#include "LogState.hpp"
#include "PpSink.hpp"
#include "color.hpp"
#include "function_name.hpp"
#include "log_level.hpp"
#include "pretty_ostream.hpp" /* pretty(): scope logs arbitrary types, so it needs the operator<< fallback */
#include <cstdint>
#include <string_view>
#include <utility>
namespace xo::pp {
/** @brief process-wide logging configuration (POC subset) **/
struct scope_config {
/** a scope logs iff its level is at least this severe **/
static inline log_level min_log_level = log_level::default_level;
/** spaces of indentation per nesting level **/
static inline std::uint32_t indent_width = 2;
/** cap on total indentation (columns): indent stops growing past this,
* so deeply-nested scopes don't march off the right side
**/
static inline std::uint32_t max_indent_width = 32;
/** if true, prefix each line with a UTC time-of-day timestamp
* (banner lines show the time; mid-scope log() lines show a
* same-width blank pad so they align under the banner)
**/
static inline bool time_enabled = true;
/** true => local time-of-day; false => UTC **/
static inline bool time_local_flag = true;
/** microsecond precision "HH:MM:SS.uuuuuu" vs millisecond "HH:MM:SS.mmm" **/
static inline bool time_usec_flag = true;
/** if true, show the nesting depth "(N)" after the +/- banner marker **/
static inline bool nesting_level_enabled = true;
/** color for the "(N)" nesting-depth display (when color enabled);
* xterm 153 = light blue (legacy used a near-white xterm 195)
**/
static inline color_spec_type nesting_level_color = color_spec_type::xterm(153);
// TODO: static inline bool pretty_print_enabled = true;
/** how to style a __PRETTY_FUNCTION__ banner name (see XO_ENTER0_);
* streamlined => "Class::method"
**/
static inline xo::FunctionStyle function_style = xo::FunctionStyle::streamlined;
/** color for the "+name" entry banner (default none => uncolored) **/
static inline color_spec_type function_entry_color = color_spec_type::none();
/** color for the "-name" exit banner (default none => uncolored) **/
static inline color_spec_type function_exit_color = color_spec_type::none();
/** if true, append the scope's source location "[file:line]" to the
* entry banner (file/line captured by XO_ENTER0_)
**/
static inline bool location_enabled = false;
/** column at which to right-align "[file:line]" -- honored by sinks
* that report a column (PrettySink); FlatSink falls back to inline
**/
static inline std::uint32_t location_tab = 80;
/** color for the "[file:line]" code location **/
static inline color_spec_type code_location_color = color_spec_type::red();
};
/** @brief captured scope-entry information (POC subset)
*
* Produced by the XO_ENTER0_ macro (see scope_macros.hpp) and consumed by
* the scope constructor. For now it carries the scope name and level;
* source location and function-name styling are deferred to the
* feature-parity pass.
**/
struct scope_setup {
/** scope name -- typically __PRETTY_FUNCTION__ (see XO_ENTER0_), styled
* per @ref style_ when the banner is printed
**/
std::string_view name_;
/** severity of this scope; gated against scope_config::min_log_level **/
log_level level_ = log_level::always;
/** how to style @ref name_ in the banner (default literal => verbatim) **/
xo::FunctionStyle style_ = xo::FunctionStyle::literal;
/** source file of the entry site (__FILE__); empty => no location **/
std::string_view file_ = {};
/** source line of the entry site (__LINE__) **/
std::uint32_t line_ = 0;
/** optional second banner name, appended verbatim after @ref name_ with
* no separator -- so a caller supplies its own "::" (e.g. name_="Foo",
* name2_="::ctor" banners as "Foo::ctor"). Unlike @ref name_ it is
* neither styled nor colored, matching legacy scope_setup's name2.
*
* Declared LAST on purpose: the macros in scope_macros.hpp initialize
* scope_setup positionally, so inserting a field earlier would silently
* shift their arguments.
**/
std::string_view name2_ = {};
/** true iff a scope entered with this setup should log **/
bool is_enabled() const { return level_ >= scope_config::min_log_level; }
};
/** @brief RAII scope logger: logs entry on construction, exit on destruction,
* and log() lines in between, each at the current nesting indentation.
*
* A scope whose level is below scope_config::min_log_level is *disabled*:
* it emits nothing and does not affect nesting. The idiom
* @code log && log(...) @endcode uses operator bool() to skip the log()
* call (and its argument evaluation) for a disabled scope.
**/
class scope {
public:
/** enter a scope named @p name (always enabled; no entry-banner args) **/
explicit scope(std::string_view name)
: name_{name}, finalized_{false}
{
begin_scope();
}
/** enter the scope described by @p setup, logging @p args on the entry
* banner (e.g. XO_ENTER0_(info), ":n ", n)
**/
template <typename... Ts>
explicit scope(scope_setup setup, Ts &&... args)
: name_{setup.name_}, name2_{setup.name2_}, style_{setup.style_},
file_{setup.file_}, line_{setup.line_},
finalized_{!setup.is_enabled()}
{
begin_scope(std::forward<Ts>(args)...);
}
~scope() {
if (!finalized_)
end_scope();
}
scope(const scope &) = delete;
scope & operator=(const scope &) = delete;
/** true while this scope is enabled (and not yet finalized) **/
bool enabled() const { return !finalized_; }
operator bool() const { return enabled(); }
/** current scope nesting depth for the calling thread.
*
* Counterpart of legacy xo::scope::nesting_level(). Useful when a
* caller hand-indents output that a sink is not laying out for it --
* e.g. xo-ordinaltree's tree dumps do
* @code
* if (log) this->print(std::clog, log.nesting_level() + 2);
* @endcode
*
* NB prefer letting the sink do it (begin()/split()/end() carry a
* running indent and adapt to the margin); this exists for printers
* that predate that and write to a bare ostream.
**/
std::uint32_t nesting_level() const {
return ThreadLogState::thread_log_state().nesting_level();
}
/** log one line: current indent, then each argument via pretty.
* No-op (returns false) if the scope is disabled.
**/
template <typename... Ts>
bool log(Ts &&... args) {
if (!enabled())
return false;
xo::pp::LogState & st = xo::pp::ThreadLogState::thread_log_state();
xo::pp::PpSink & sink = st.sink();
emit_time(sink, false /*real_time: log() lines get a blank time pad*/);
emit_indent(st);
sink.begin_here(0);
(xo::pp::pretty(sink, args), ...);
sink.end();
sink.complete();
return true;
}
/** log(args...) spelled as a call, for the @c log && log(...) idiom **/
template <typename... Ts>
bool operator()(Ts &&... args) {
return log(std::forward<Ts>(args)...);
}
/** optionally end the scope early (before dtor), logging @p args on the
* exit banner (e.g. end_scope("<- :retval ", retval)). Idempotent.
**/
template <typename... Ts>
void end_scope(Ts &&... args) {
if (finalized_)
return;
finalized_ = true;
xo::pp::LogState & st = xo::pp::ThreadLogState::thread_log_state();
xo::pp::PpSink & sink = st.sink();
st.decr_nesting();
emit_time(sink, true /*real_time*/);
emit_indent(st);
sink.put("-");
emit_nesting_level(sink, st.nesting_level());
{
color_guard g(sink, scope_config::function_exit_color);
put_function_name(sink, style_, name_);
}
/* outside the color guard: name2 is verbatim, matching legacy
* (function_name(style, color, name1) << name2)
*/
if (!name2_.empty())
sink.put(name2_);
if constexpr (sizeof...(args) > 0) {
sink.put(" ");
sink.begin_here(0);
(xo::pp::pretty(sink, args), ...);
sink.end();
}
sink.complete();
}
/** re-enable a disabled scope and emit its (deferred) entry banner
* now, logging @p args on it. No-op if the scope is already enabled.
*
* Use to turn logging on only when something goes wrong: create the
* scope below the log threshold (silent), then call this on the error
* path so the banner + details appear (cf. a verify_ok() that logs
* only on failure).
**/
template <typename... Ts>
void retroactively_enable(Ts &&... args) {
if (finalized_) {
finalized_ = false;
begin_scope(std::forward<Ts>(args)...);
}
}
private:
/** log the entry banner ("+name" + optional args) and increase nesting.
* No-op if the scope is disabled.
**/
template <typename... Ts>
void begin_scope(Ts &&... args) {
if (!enabled())
return;
xo::pp::LogState & st = xo::pp::ThreadLogState::thread_log_state();
xo::pp::PpSink & sink = st.sink();
emit_time(sink, true /*real_time*/);
emit_indent(st);
sink.put("+");
emit_nesting_level(sink, st.nesting_level());
{
color_guard g(sink, scope_config::function_entry_color);
put_function_name(sink, style_, name_);
}
/* outside the color guard: name2 is verbatim, matching legacy
* (function_name(style, color, name1) << name2)
*/
if (!name2_.empty())
sink.put(name2_);
if constexpr (sizeof...(args) > 0) {
sink.put(" ");
sink.begin_here(0);
(xo::pp::pretty(sink, args), ...);
sink.end();
}
emit_location(sink, file_, line_);
sink.complete();
st.incr_nesting();
}
/** write (nesting_level * indent_width, capped) spaces to the active sink **/
static void emit_indent(xo::pp::LogState & st);
/** if timestamps are enabled, write the leftmost time field to @p sink:
* the real UTC time when @p real_time, else a same-width blank pad
* (so mid-scope log() lines align under the timestamped banner)
**/
static void emit_time(xo::pp::PpSink & sink, bool real_time);
/** if nesting_level_enabled, write "(N) " (N = @p level, colored) to
* @p sink -- the depth display shown after the +/- banner marker
**/
static void emit_nesting_level(xo::pp::PpSink & sink, std::uint32_t level);
/** if location_enabled and @p file is non-empty, append the code
* location "[file:line]" -- right-aligned at scope_config::location_tab
* when @p sink reports a column (PrettySink), else inline (FlatSink)
**/
static void emit_location(xo::pp::PpSink & sink,
std::string_view file, std::uint32_t line);
private:
/** scope name (e.g. function name); printed in the +/- entry/exit banners **/
std::string_view name_;
/** optional second banner name, appended verbatim (unstyled, uncolored)
* immediately after @ref name_; empty => nothing extra printed
**/
std::string_view name2_ = {};
/** how to style @ref name_ in the banner (literal for the string_view
* ctor; from scope_setup / scope_config::function_style via XO_ENTER0_)
**/
xo::FunctionStyle style_ = xo::FunctionStyle::literal;
/** entry-site __FILE__ (empty for the string_view ctor => no location) **/
std::string_view file_ = {};
/** entry-site __LINE__ **/
std::uint32_t line_ = 0;
/** once true, logging is disabled; set at entry (level gating) or by
* end_scope(); guards against a double exit banner from the dtor
**/
bool finalized_ = false;
};
} /*namespace xo::pp*/
/* end scope.hpp */