// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module Crafter.Debug:Registry_impl; import std; import Crafter.Debug.Core; import :Registry; using namespace Crafter::Debug; using namespace Crafter::Debug::Detail; namespace { std::atomic frame{ 0 }; // Points kept per sample path, enough for `trace` over a few seconds of // frames without an app logging every frame growing without bound. constexpr std::size_t sampleHistory = 512; bool ValidName(std::string_view name) { return !name.empty() && std::ranges::none_of(name, [](char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; }); } // The registered name that `path` starts with, longest first, ending at a // step boundary: "bodies" for "bodies[3].pos", not "bod". template auto LongestPrefix(const Map& map, std::string_view path) -> decltype(map.end()) { auto best = map.end(); std::size_t bestLength = 0; for (auto it = map.begin(); it != map.end(); ++it) { const std::string_view name = it->first; if (!path.starts_with(name)) continue; if (path.size() > name.size() && path[name.size()] != '.' && path[name.size()] != '[') continue; if (name.size() >= bestLength) { best = it; bestLength = name.size(); } } return best; } std::vector MemberNames(const Value& object) { std::vector names; for (const auto& [key, member] : object.Members()) names.push_back(key); return names; } std::string JoinNames(std::span names) { std::string joined; for (const std::string_view name : names) { if (!joined.empty()) joined += ", "; joined += name; } return joined; } // Follows `rest` (".name" and "[index]" steps) from `root`. `where` names // the root in errors. Returns nullptr and sets `error` when a step fails. template V* Walk(V& root, std::string_view rest, std::string where, std::string& error) { V* current = &root; while (!rest.empty()) { if (rest.front() == '.') { rest.remove_prefix(1); const std::size_t end = std::min(rest.find('.'), rest.find('[')); const std::string_view name = rest.substr(0, end); rest.remove_prefix(name.size()); if (!current->IsObject()) { error = std::format("'{}' is {}, so it has no member '{}'", where, KindName(current->Kind()), name); return nullptr; } V* next = current->Find(name); if (next == nullptr) { const std::vector names = MemberNames(*current); const auto suggestion = Suggest(name, names); error = suggestion ? std::format("'{}' has no member '{}'; did you mean '{}'?", where, name, *suggestion) : std::format("'{}' has no member '{}' (members: {})", where, name, JoinNames(names)); return nullptr; } where += '.'; where += name; current = next; } else if (rest.front() == '[') { const std::size_t close = rest.find(']'); std::size_t index = 0; const std::string_view digits = rest.substr(1, close == std::string_view::npos ? std::string_view::npos : close - 1); const auto [end, failure] = std::from_chars(digits.data(), digits.data() + digits.size(), index); if (close == std::string_view::npos || failure != std::errc() || end != digits.data() + digits.size()) { error = std::format("'{}' is followed by a malformed index: '{}'", where, rest); return nullptr; } rest.remove_prefix(close + 1); if (!current->IsArray()) { error = std::format("'{}' is {}, not an array", where, KindName(current->Kind())); return nullptr; } if (index >= current->Size()) { error = std::format("'{}' has {} items, so [{}] is out of range", where, current->Size(), index); return nullptr; } where += std::format("[{}]", index); current = ¤t->Items()[index]; } else { error = std::format("cannot read '{}' after '{}'", rest, where); return nullptr; } } return current; } } std::uint64_t Crafter::Debug::Detail::CurrentFrame() { return frame.load(std::memory_order_relaxed); } void Crafter::Debug::Detail::SetFrame(std::uint64_t value) { frame.store(value, std::memory_order_relaxed); } bool Crafter::Debug::Detail::GlobMatch(std::string_view glob, std::string_view text) { std::size_t g = 0; std::size_t t = 0; std::size_t star = std::string_view::npos; std::size_t mark = 0; while (t < text.size()) { if (g < glob.size() && (glob[g] == '?' || glob[g] == text[t])) { ++g; ++t; } else if (g < glob.size() && glob[g] == '*') { star = g++; mark = t; } else if (star != std::string_view::npos) { g = star + 1; t = ++mark; } else { return false; } } while (g < glob.size() && glob[g] == '*') ++g; return g == glob.size(); } std::string Crafter::Debug::Detail::Joined(std::string_view prefix, std::string_view name) { if (prefix.empty()) return std::string(name); if (name.empty()) return std::string(prefix); return std::format("{}.{}", prefix, name); } Registry& Registry::Instance() { static Registry registry; return registry; } std::uint64_t Registry::NewOwner() { return nextOwner.fetch_add(1, std::memory_order_relaxed); } void Registry::AddCommand(CommandEntry entry) { if (!ValidName(entry.name)) throw std::invalid_argument(std::format("Crafter.Debug: '{}' is not a command name: it must be non-empty, without whitespace", entry.name)); std::string name = entry.name; std::lock_guard lock(mutex); commands.insert_or_assign(std::move(name), std::make_shared(std::move(entry))); } void Registry::AddWatch(WatchEntry entry) { if (!ValidName(entry.path)) throw std::invalid_argument(std::format("Crafter.Debug: '{}' is not a watch path: it must be non-empty, without whitespace", entry.path)); std::string path = entry.path; std::lock_guard lock(mutex); watches.insert_or_assign(std::move(path), std::make_shared(std::move(entry))); } void Registry::AddSample(std::string_view path, Value value, std::uint64_t sampleFrame, std::uint64_t owner) { std::lock_guard lock(mutex); auto found = samples.find(path); if (found == samples.end()) found = samples.emplace(std::string(path), Series{}).first; Series& series = found->second; series.owner = owner; series.points.push_back({ sampleFrame, std::move(value) }); if (series.points.size() > sampleHistory) series.points.pop_front(); } void Registry::RemoveOwner(std::uint64_t owner) { std::lock_guard lock(mutex); std::erase_if(commands, [&](const auto& item) { return item.second->owner == owner; }); std::erase_if(watches, [&](const auto& item) { return item.second->owner == owner; }); std::erase_if(samples, [&](const auto& item) { return item.second.owner == owner; }); } std::shared_ptr Registry::FindCommand(std::string_view name) const { std::lock_guard lock(mutex); const auto found = commands.find(name); return found == commands.end() ? nullptr : found->second; } std::vector> Registry::Commands(std::string_view glob) const { std::lock_guard lock(mutex); std::vector> matched; for (const auto& [name, entry] : commands) { if (GlobMatch(glob, name)) matched.push_back(entry); } return matched; } std::vector> Registry::Watches(std::string_view glob) const { std::lock_guard lock(mutex); std::vector> matched; for (const auto& [path, entry] : watches) { if (GlobMatch(glob, path)) matched.push_back(entry); } return matched; } std::vector Registry::SamplePaths(std::string_view glob) const { std::lock_guard lock(mutex); std::vector matched; for (const auto& [path, series] : samples) { if (GlobMatch(glob, path)) matched.push_back(path); } return matched; } std::vector Registry::SampleHistory(std::string_view path) const { std::lock_guard lock(mutex); const auto found = samples.find(path); if (found == samples.end()) return {}; return std::vector(found->second.points.begin(), found->second.points.end()); } Resolved Registry::Resolve(std::string_view path) const { std::shared_ptr watch; std::optional latest; std::string base; { std::lock_guard lock(mutex); const auto foundWatch = LongestPrefix(watches, path); const auto foundSample = LongestPrefix(samples, path); const std::size_t watchLength = foundWatch == watches.end() ? 0 : foundWatch->first.size(); const std::size_t sampleLength = foundSample == samples.end() ? 0 : foundSample->first.size(); if (foundWatch != watches.end() && watchLength >= sampleLength) { watch = foundWatch->second; base = foundWatch->first; } else if (foundSample != samples.end() && !foundSample->second.points.empty()) { latest = foundSample->second.points.back(); base = foundSample->first; } } if (!watch && !latest) { const auto suggestion = SuggestPath(path); return { Value(), suggestion ? std::format("no watch or sample '{}'; did you mean '{}'?", path, *suggestion) : std::format("no watch or sample '{}'; list them with: crafter-debug get '*'", path) }; } Value root = watch ? watch->get() : std::move(latest->value); std::string error; const Value* reached = Walk(std::as_const(root), path.substr(base.size()), base, error); if (reached == nullptr) return { Value(), error }; return { *reached, {} }; } std::optional Registry::Set(std::string_view path, const Value& value) const { std::shared_ptr watch; { std::lock_guard lock(mutex); const auto found = LongestPrefix(watches, path); if (found != watches.end()) watch = found->second; } if (!watch) { const auto suggestion = SuggestPath(path); return suggestion ? std::format("no watch '{}'; did you mean '{}'?", path, *suggestion) : std::format("no watch '{}'", path); } if (!watch->set) return std::format("'{}' is read-only", watch->path); const std::string_view rest = path.substr(watch->path.size()); if (rest.empty()) return watch->set(value); Value whole = watch->get(); std::string error; Value* part = Walk(whole, rest, watch->path, error); if (part == nullptr) return error; *part = value; return watch->set(whole); } std::optional Registry::SuggestCommand(std::string_view name) const { std::lock_guard lock(mutex); std::vector names; for (const auto& [command, entry] : commands) names.push_back(command); if (const auto suggestion = Suggest(name, names)) return std::string(*suggestion); return std::nullopt; } std::optional Registry::SuggestPath(std::string_view path) const { std::lock_guard lock(mutex); std::vector names; for (const auto& [watch, entry] : watches) names.push_back(watch); for (const auto& [sample, series] : samples) names.push_back(sample); if (const auto suggestion = Suggest(path, names)) return std::string(*suggestion); return std::nullopt; } Group::Group(std::string prefix) : prefix(std::move(prefix)), owner(Registry::Instance().NewOwner()) {} Group::~Group() { Registry::Instance().RemoveOwner(owner); }