From 9d8365472c10b753efa27898c80eee8e4e925480 Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Thu, 9 Jul 2026 18:07:16 +0500 Subject: [PATCH 1/2] update formatters --- CMakeLists.txt | 2 + src/aether-miscpp/format/default_formatters.h | 277 ++++++++------ src/aether-miscpp/format/format.h | 4 +- src/aether-miscpp/format/format_impl.h | 350 ++++++++++-------- src/aether-miscpp/format/format_time.h | 271 +++++++++----- src/aether-miscpp/format/formatter.h | 17 +- src/aether-miscpp/format/numeric_helpers.h | 90 +++++ tests/test-format/CMakeLists.txt | 4 +- tests/test-format/test-format-str.cpp | 150 +++++++- tests/test-format/test-format-time.cpp | 127 ++++++- tests/test-format/test-format-types.cpp | 128 ++++++- tests/test-meta/CMakeLists.txt | 4 +- tests/test-reflect/CMakeLists.txt | 4 +- tests/test-types/CMakeLists.txt | 4 +- 14 files changed, 1028 insertions(+), 404 deletions(-) create mode 100644 src/aether-miscpp/format/numeric_helpers.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e0850d4..2722fbe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,8 @@ option(AE_INSTALL "Install aether-miscpp library" ${IS_ROOT_PROJECT} ) add_library(${PROJECT_NAME} INTERFACE) add_library(aether::miscpp ALIAS ${PROJECT_NAME}) +target_compile_features(${PROJECT_NAME} INTERFACE cxx_std_20) + target_include_directories(${PROJECT_NAME} INTERFACE $ $) diff --git a/src/aether-miscpp/format/default_formatters.h b/src/aether-miscpp/format/default_formatters.h index 634cb75..ee43c68 100644 --- a/src/aether-miscpp/format/default_formatters.h +++ b/src/aether-miscpp/format/default_formatters.h @@ -17,157 +17,220 @@ #ifndef AETHER_MISCPP_FORMAT_DEFAULT_FORMATTERS_H_ #define AETHER_MISCPP_FORMAT_DEFAULT_FORMATTERS_H_ -#include -#include +#include +#include +#include +#include #include #include -#include -#include -#include +#include +#include #include +#include +#include -#include "aether-miscpp/format/formatter.h" -#include "aether-miscpp/meta/time_traits.h" -#include "aether-miscpp/meta/container_traits.h" #include "aether-miscpp/format/format_impl.h" +#include "aether-miscpp/format/formatter.h" +#include "aether-miscpp/format/numeric_helpers.h" namespace ae { -using format_internal::FormatEntry; -using format_internal::FormatScheme; +namespace format_internal { -template -struct IsStreamOutputSpecified : std::false_type {}; +template +using BareT = std::remove_cvref_t; template -struct IsStreamOutputSpecified< - T, std::void_t() - << std::declval())>> : std::true_type {}; +concept StringLike = std::same_as, std::string> || + std::same_as, std::string_view>; -template -struct IstextSpecified : std::false_type {}; +template +concept HasValueType = requires { typename BareT::value_type; }; template -struct IstextSpecified()))>> - : std::true_type {}; +concept ContainerLike = !StringLike && HasValueType && + std::ranges::input_range const&>; -// for any with operator<< to std::ostream& template -struct Formatter< - T, std::enable_if_t::value && - !IstextSpecified::value && !std::is_enum_v && - !IsTimePoint::value && !IsDuration::value>> { +concept ByteBufferContainer = + ContainerLike && + std::same_as::value_type, std::uint8_t>; + +template +void WriteUnsignedIntegral(Writer& writer, std::uint64_t value) { + WriteUnsigned(writer, value); +} + +template +void WriteSignedIntegral(Writer& writer, std::int64_t value) { + if (value < 0) { + writer.write('-'); + auto magnitude = std::uint64_t{0} - static_cast(value); + WriteUnsignedIntegral(writer, magnitude); + } else { + WriteUnsignedIntegral(writer, static_cast(value)); + } +} + +template +void WriteIntegral(Writer& writer, T value) { + if constexpr (std::is_signed_v) { + WriteSignedIntegral(writer, static_cast(value)); + } else { + WriteUnsignedIntegral(writer, static_cast(value)); + } +} + +template +void WriteFloat(Writer& writer, T value) { + constexpr auto kFloatBufferSize = std::size_t{64}; + auto buff = std::array{}; + auto [ptr, ec] = std::to_chars(buff.data(), buff.data() + buff.size(), value, + std::chars_format::general); + if (ec == std::errc{}) { + writer.write(std::string_view{buff.data(), + static_cast(ptr - buff.data())}); + return; + } + writer.write("{float error "); + WriteSignedIntegral(writer, static_cast(static_cast(ec))); + writer.write('}'); +} + +} // namespace format_internal + +template <> +struct Formatter { template - void Format(T const& value, FormatContext& ctx) const { - ctx.out().stream() << value; + void Format(std::nullptr_t, FormatContext& ctx) const { + ctx.out().write("(null)"); } }; -// for any with text method -template -struct Formatter::value>> { +template <> +struct Formatter { template - void Format(T const& value, FormatContext& ctx) const { - ctx.out().write(T::text(value)); + void Format(std::string const& value, FormatContext& ctx) const { + ctx.out().write(value); } }; -// for any enum -template -struct Formatter>> - : Formatter { +template <> +struct Formatter { template - void Format(T const& value, FormatContext& ctx) const { - Formatter::Format(static_cast(value), ctx); + void Format(std::string_view value, FormatContext& ctx) const { + ctx.out().write(value); + } +}; + +template <> +struct Formatter { + template + void Format(char const* value, FormatContext& ctx) const { + ctx.out().write(value != nullptr ? std::string_view{value} + : std::string_view{"(null)"}); + } +}; + +template <> +struct Formatter : Formatter {}; + +template <> +struct Formatter { + template + void Format(char value, FormatContext& ctx) const { + ctx.out().write(value); + } +}; + +template <> +struct Formatter { + template + void Format(bool value, FormatContext& ctx) const { + ctx.out().write(value ? "true" : "false"); } }; -// that can be iterated template -struct Formatter>::value || - IsStringView>::value) && - IsContainer>::value>> { +struct Formatter>> { template - void Format(T const& value, FormatContext& ctx) const { - if constexpr (std::is_same_v> || - std::is_same_v>) { - FormatBuffer(value, ctx); - } else { - FormatContainer(value, ctx); - } + void Format(T value, FormatContext& ctx) const { + format_internal::WriteIntegral( + ctx.out(), static_cast>(value)); } +}; +template +struct Formatter< + T, std::enable_if_t && !std::is_same_v && + !std::is_same_v>> { template - void FormatContainer(T const& value, FormatContext& ctx) const { - auto format = FormatScheme{std::array{ - FormatEntry{ctx.options, 0, 0, - static_cast(ctx.options.size()), 0}, - FormatEntry{{", "}, 2, 0, 0, 1}, - }}; - auto format_last = FormatScheme{std::array{ - FormatEntry{ctx.options, 0, 0, - static_cast(ctx.options.size()), 0}, - }}; - - for (auto it = std::begin(value); it != std::end(value); ++it) { - if (std::next(it) == std::end(value)) { - ae::Format(ctx.out(), format_last, *it); - } else { - ae::Format(ctx.out(), format, *it); - } - } + void Format(T value, FormatContext& ctx) const { + format_internal::WriteIntegral(ctx.out(), value); } +}; +template +struct Formatter>> { template - void FormatBuffer(T const& value, FormatContext& ctx) const { - static_assert(sizeof(typename T::value_type) == 1, - "Print buffer only for one byte size values"); - - constexpr std::size_t kLocalBuffSize = 128; - constexpr std::size_t kTwoMinCharValue = 0x10; - constexpr int kPrintBase = 16; - std::size_t v_size = 2; // 2 chars on byte - std::size_t buff_size = v_size * value.size(); - - std::array local_buff; - std::unique_ptr alloc_buff; // NOLINT(*avoid-c-arrays) - - char* buff; // NOLINT(*init-variables) - if (buff_size > local_buff.size()) { - alloc_buff = - std::make_unique(buff_size); // NOLINT(*avoid-c-arrays) - buff = alloc_buff.get(); - } else { - buff = local_buff.data(); - } - std::size_t wp = 0; - for (auto const& v : value) { - // convert value with leading 0 - if (v < kTwoMinCharValue) { - *(buff + wp) = '0'; - std::to_chars(buff + wp + 1, buff + wp + v_size, v, kPrintBase); - } else { - std::to_chars(buff + wp, buff + wp + v_size, v, kPrintBase); - } - wp += v_size; - } - ctx.out().stream().write(buff, static_cast(wp)); + void Format(T value, FormatContext& ctx) const { + format_internal::WriteFloat(ctx.out(), value); } }; -// for std::optional template -struct Formatter> : Formatter { +struct Formatter> { template + requires format_internal::HasFormatterFor void Format(std::optional const& value, FormatContext& ctx) const { if (!value) { - static constexpr std::string_view null_str = "nullopt"; - ctx.out().stream().write(null_str.data(), null_str.size()); + ctx.out().write("nullopt"); } else { - Formatter::Format(value.value(), ctx); + format_internal::FormatValue(ctx.out(), *value, {}); + } + } +}; + +template +struct Formatter>> { + template + void Format(T const& value, FormatContext& ctx) const { + static constexpr auto kHex = std::string_view{"0123456789abcdef"}; + static constexpr auto kHexNibbleMask = std::uint8_t{0x0f}; + ctx.out().write("0x"); + for (auto byte : value) { + auto const byte_value = static_cast(byte); + auto buffer = std::array{ + kHex[(byte_value >> 4) & kHexNibbleMask], // NOLINT(*bounds*) + kHex[byte_value & kHexNibbleMask]}; // NOLINT(*bounds*) + ctx.out().write(std::string_view{buffer.data(), buffer.size()}); + } + } +}; + +template +struct Formatter && + !format_internal::ByteBufferContainer>> { + template + requires format_internal::HasFormatterFor< + std::ranges::range_reference_t const&>, + TStream> + void Format(T const& value, FormatContext& ctx) const { + bool first = true; + for (auto const& item : value) { + if (!first) { + ctx.out().write(", "); + } + first = false; + if constexpr (format_internal::ContainerLike && + !format_internal::ByteBufferContainer) { + ctx.out().write('['); + format_internal::FormatValue(ctx.out(), item, {}); + ctx.out().write(']'); + } else { + format_internal::FormatValue(ctx.out(), item, {}); + } } } }; diff --git a/src/aether-miscpp/format/format.h b/src/aether-miscpp/format/format.h index ddf0c89..bf48cff 100644 --- a/src/aether-miscpp/format/format.h +++ b/src/aether-miscpp/format/format.h @@ -17,11 +17,13 @@ #ifndef AETHER_MISCPP_FORMAT_FORMAT_H_ #define AETHER_MISCPP_FORMAT_FORMAT_H_ +// User code should include only this header; formatter subheaders are +// implementation/export details. // IWYU pragma: begin_exports #include "aether-miscpp/format/formatter.h" #include "aether-miscpp/format/format_impl.h" -#include "aether-miscpp/format/format_time.h" #include "aether-miscpp/format/default_formatters.h" +#include "aether-miscpp/format/format_time.h" // IWYU pragma: end_exports #endif // AETHER_MISCPP_FORMAT_FORMAT_H_ diff --git a/src/aether-miscpp/format/format_impl.h b/src/aether-miscpp/format/format_impl.h index 005a7d2..83a745b 100644 --- a/src/aether-miscpp/format/format_impl.h +++ b/src/aether-miscpp/format/format_impl.h @@ -17,218 +17,248 @@ #ifndef AETHER_MISCPP_FORMAT_FORMAT_IMPL_H_ #define AETHER_MISCPP_FORMAT_FORMAT_IMPL_H_ -#include #include -#include -#include +#include +#include +#include #include -#include -#include +#include #include #include -#include -#include +#include #include "aether-miscpp/format/formatter.h" namespace ae { namespace format_internal { -template -struct IsStream : std::false_type {}; - -template -struct IsStream() << int{0})>> - : std::true_type {}; -template -struct FormatWriter; - -template <> -struct FormatWriter { - explicit FormatWriter(std::ostream& stream) : ostr{&stream} {} - - void write(std::uint8_t const* data, std::size_t size) { - ostr->write(reinterpret_cast(data), - static_cast(size)); - } +struct StringWriter { + explicit StringWriter(std::string& out) noexcept : out_{&out} {} + void write(std::string_view str) { out_->append(str.data(), str.size()); } + void write(char ch) { out_->push_back(ch); } + std::string* out_; +}; +struct OStreamWriter { + explicit OStreamWriter(std::ostream& out) noexcept : out_{&out} {} void write(std::string_view str) { - ostr->write(str.data(), static_cast(str.size())); - } - - auto& stream() const { return *ostr; } - - std::ostream* ostr; -}; -FormatWriter(std::ostream& stream) -> FormatWriter; - -// list of args to format -template -struct FormatArgs { - constexpr explicit FormatArgs(T const&... args) : arguments{&args...} {} - - template - constexpr void Format(TFormatContext& ctx) { - using arg_type = std::decay_t>>; - auto const& arg = *std::get(arguments); - Formatter{}.Format(arg, ctx); + out_->write(str.data(), static_cast(str.size())); } - - std::tuple arguments; + void write(char ch) { out_->put(ch); } + std::ostream* out_; }; -struct FormatEntry { - constexpr std::string_view before_format() const { - if (entry_string.empty()) { - return {}; - } - return entry_string.substr(0, before_format_size); - } - constexpr std::string_view options() const { - if (entry_string.empty()) { - return {}; - } - return entry_string.substr(options_offset, options_size); - } - - std::string_view entry_string; - std::uint16_t before_format_size; - std::uint16_t options_offset; - std::uint16_t options_size; - std::uint8_t index = std::numeric_limits::max(); +struct FormatPart { + std::size_t offset{}; + std::size_t size{}; + bool placeholder{}; }; struct FormatScheme { - static constexpr std::size_t kCount = 10; - - constexpr FormatScheme(char const* format) - : FormatScheme{std::string_view{format}} {} + // Hard limit for parsed parts, including both placeholders and literal runs. + // The current layout supports the common maximum pattern of 10 placeholders + // interleaved with 11 literals. If parsing would exceed this 21-part limit, + // overflow is recorded; formatting then writes the original source followed + // by " OVERFLOW" and does not format any arguments. + static constexpr std::size_t kMaxFormatParts = 21; + + template + // NOLINTNEXTLINE(*explicit-constructor*) + constexpr FormatScheme(char const (&format)[N]) noexcept + : FormatScheme{std::string_view{format, N ? N - 1 : 0}} {} + // NOLINTNEXTLINE(*explicit-constructor*) + constexpr FormatScheme(std::string_view format) noexcept : source{format} { + Parse(); + } - template - constexpr FormatScheme(char const (&format)[Size]) - : FormatScheme{std::string_view{format, Size}} {} + constexpr std::size_t reserve_hint() const noexcept { return source.size(); } - FormatScheme(std::string const& format) - : FormatScheme{std::string_view{format}} {} + // Non-owning format source. Referenced characters must remain alive and + // unchanged while formatting. + std::string_view source; + std::array parts{}; + std::size_t part_count{}; + bool overflow{}; - constexpr FormatScheme(std::string_view format) { - std::uint8_t index = 0; + private: + constexpr void AddPart(std::size_t offset, std::size_t size, + bool placeholder) noexcept { + if (size == 0) { + return; + } + if (part_count == parts.size()) { + overflow = true; + return; + } + parts[part_count++] = FormatPart{offset, size, placeholder}; + } - while (!format.empty()) { - auto format_begin = FormatBegin(format); - if (format_begin == std::string_view::npos) { - break; - } - auto format_end = format.find_first_of('}', format_begin); - if (format_end == std::string_view::npos) { - break; - } - auto index_end = format.find_first_of(':', format_begin); - if (index_end > format_end) { - index_end = format_begin; + constexpr void Parse() noexcept { + std::size_t literal_begin = 0; + std::size_t i = 0; + while (i < source.size()) { + auto const escaped_brace = i + 1 < source.size() && + (source[i] == '{' || source[i] == '}') && + source[i + 1] == source[i]; + if (escaped_brace) { + AddPart(literal_begin, i - literal_begin, false); + AddPart(i, 1, false); + i += 2; + literal_begin = i; + continue; } - format_entries[index] = FormatEntry{ - format.substr(0, format_end + 1), - static_cast(format_begin), - static_cast(index_end + 1), - static_cast(format_end - index_end - 1), - index, - }; - index += 1; - format = format.substr(format_end + 1, format.size() - format_end - 1); - } - if (!format.empty()) { - format_entries[index] = FormatEntry{ - format, static_cast(format.size()), 0, - 0, std::numeric_limits::max(), - }; + if (source[i] == '{') { + auto close = i + 1; + while (close < source.size() && source[close] != '}') { + ++close; + } + if (close == source.size()) { + break; + } + AddPart(literal_begin, i - literal_begin, false); + AddPart(i, close - i + 1, true); + i = close + 1; + literal_begin = i; + } else { + ++i; + } } + AddPart(literal_begin, source.size() - literal_begin, false); } +}; - constexpr FormatScheme(FormatScheme const& format_string) - : FormatScheme{format_string.format_entries} {} +template +concept HasFormatterFor = + requires(Formatter> formatter, T const& value, + FormatContext& ctx) { formatter.Format(value, ctx); }; - template - constexpr explicit FormatScheme(std::array const& fe_arr) { - std::copy_n(std::begin(fe_arr), std::min(kCount, Size), - std::begin(format_entries)); - } +template +concept WriterLike = requires(Writer& writer) { + writer.write(std::string_view{}); + writer.write('a'); +}; - static constexpr std::size_t FormatBegin(std::string_view const format) { - std::size_t format_begin{}; - format_begin = format.find_first_of('{', format_begin); - if ((format_begin != std::string_view::npos) && - (format_begin + 1) != format.size() && - (format[format_begin + 1] == '{')) { // escaped '{' - format_begin += 1; - } - return format_begin; +template +void FormatValue(Writer& writer, T const& value, std::string_view options) { + using U = std::decay_t; + if constexpr (HasFormatterFor) { + auto ctx = FormatContext{writer, options}; + Formatter{}.Format(value, ctx); + } else { + static_assert(sizeof(U) == 0, "Unsupported type for ae::Format"); } +} - std::array format_entries{}; +template +struct FormatArg { + using FormatFn = void (*)(Writer&, void const*, std::string_view); + void const* value{}; + FormatFn format{}; }; -template -struct Index { - static constexpr auto value = I; -}; +template +void FormatArgThunk(Writer& writer, void const* value, + std::string_view options) { + FormatValue(writer, *static_cast(value), options); +} -template -void DispatchImpl(std::size_t index, [[maybe_unused]] Func func, - std::index_sequence const& /* seq */) { - bool res = ((index == Is ? (func(Index{}), true) : false) || ...); - (void)(res); +template +FormatArg MakeFormatArg(T const& value) { + return FormatArg{ + static_cast(std::addressof(value)), + &FormatArgThunk, + }; } -// Dispatch runtime index to compile time Index -template -void Dispatch(std::size_t index, Func&& func) { - DispatchImpl(index, std::forward(func), - std::make_index_sequence{}); +constexpr std::string_view PlaceholderOptions( + std::string_view placeholder) noexcept { + if ((placeholder.size() >= 3) && (placeholder[0] == '{') && + (placeholder[1] == ':') && (placeholder.back() == '}')) { + return std::string_view{placeholder.data() + 2, placeholder.size() - 3}; + } + return {}; } -template -void FormatToStream(TStream& out, FormatScheme const& format_scheme, - FormatArgs args) { - for (auto const& fmt : format_scheme.format_entries) { - if (auto before = fmt.before_format(); !before.empty()) { - out.write(before); - } - if (fmt.index < sizeof...(T)) { - Dispatch(fmt.index, [&](auto index) { - auto ctx = FormatContext{out, fmt.options()}; - args.template Format(ctx); - }); +template +void FormatToWriterErased(Writer& writer, FormatScheme const& scheme, + FormatArg const* args, + std::size_t arg_count) { + if (scheme.overflow) { + writer.write(scheme.source); + writer.write(" OVERFLOW"); + return; + } + + std::size_t next_arg = 0; + for (std::size_t i = 0; i < scheme.part_count; ++i) { + auto const part = scheme.parts[i]; + auto const text = scheme.source.substr(part.offset, part.size); + if (part.placeholder) { + auto const arg_index = next_arg; + auto const options = PlaceholderOptions(text); + if (next_arg < arg_count) { + ++next_arg; + args[arg_index].format(writer, args[arg_index].value, options); + } else { + writer.write(text); + } + } else { + writer.write(text); } } } +template +void FormatToWriter(Writer& writer, FormatScheme const& scheme, + Args&&... args) { + auto erased_args = std::array, sizeof...(Args)>{ + MakeFormatArg(args)..., + }; + FormatToWriterErased(writer, scheme, erased_args.data(), erased_args.size()); +} + } // namespace format_internal +using FormatScheme = format_internal::FormatScheme; + template -void Format(format_internal::FormatWriter& out_writer, - format_internal::FormatScheme const& format, Args&&... args) { - format_internal::FormatToStream( - out_writer, format, - format_internal::FormatArgs...>{args...}); + requires std::derived_from, std::ostream> +void FormatTo(TStream& stream, FormatScheme const& format, Args&&... args) { + auto writer = format_internal::OStreamWriter{stream}; + format_internal::FormatToWriter(writer, format, std::forward(args)...); +} + +template +void FormatTo(Writer& writer, FormatScheme const& format, Args&&... args) { + format_internal::FormatToWriter(writer, format, std::forward(args)...); +} + +template +void FormatTo(std::string& out, FormatScheme const& format, Args&&... args) { + // Precondition: format.source and formatted arguments must not reference + // storage owned by out. This includes string_view, char const*, containers, + // ranges, strings, or views into out. This overload may reserve/append to out + // before or while reading inputs, which can invalidate or overlap those + // reads. No runtime aliasing checks are performed. + out.reserve(out.size() + format.reserve_hint()); + auto writer = format_internal::StringWriter{out}; + format_internal::FormatToWriter(writer, format, std::forward(args)...); } template -std::enable_if_t::value> Format( - TStream& stream, format_internal::FormatScheme const& format, - Args&&... args) { - auto format_writer = format_internal::FormatWriter{stream}; - Format(format_writer, format, std::forward(args)...); + requires std::derived_from, std::ostream> +void Format(TStream& stream, FormatScheme const& format, Args&&... args) { + FormatTo(stream, format, std::forward(args)...); } template -std::string Format(format_internal::FormatScheme const& format, - Args&&... args) { - auto stream = std::stringstream{}; - Format(stream, format, std::forward(args)...); - return stream.str(); +std::string Format(FormatScheme const& format, Args&&... args) { + std::string out; + FormatTo(out, format, std::forward(args)...); + return out; } + } // namespace ae #endif // AETHER_MISCPP_FORMAT_FORMAT_IMPL_H_ diff --git a/src/aether-miscpp/format/format_time.h b/src/aether-miscpp/format/format_time.h index 8651712..315d20b 100644 --- a/src/aether-miscpp/format/format_time.h +++ b/src/aether-miscpp/format/format_time.h @@ -13,125 +13,198 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + #ifndef AETHER_MISCPP_FORMAT_FORMAT_TIME_H_ #define AETHER_MISCPP_FORMAT_FORMAT_TIME_H_ #include -#include -#include +#include +#include +#include #include "aether-miscpp/format/formatter.h" +#include "aether-miscpp/format/numeric_helpers.h" namespace ae { namespace format_internal { -inline std::string FormatTimeWithOptions(std::string_view options, - auto duration_us) { - // Convert to sys_days for calendar decomposition - auto tp_us = std::chrono::system_clock::time_point{duration_us}; - auto sd = std::chrono::floor(tp_us); - auto ymd = std::chrono::year_month_day{sd}; - std::chrono::hh_mm_ss hms{tp_us - sd}; - - auto h = hms.hours().count(); - auto m = hms.minutes().count(); - auto s = hms.seconds().count(); - auto sub = - std::chrono::duration_cast(hms.subseconds()) - .count(); - - auto yr = static_cast(ymd.year()); - auto mo = static_cast(ymd.month()); - auto dy = static_cast(ymd.day()); - - std::string result; - auto inserter = std::back_inserter(result); - - for (std::size_t i = 0; i < options.size(); ++i) { - if (options[i] == '%' && i + 1 < options.size()) { - ++i; - switch (options[i]) { - case 'H': - std::format_to(inserter, "{:02}", h); - break; - case 'M': - std::format_to(inserter, "{:02}", m); - break; - case 'S': - std::format_to(inserter, "{:02}.{:06}", s, sub); - break; - case 'Y': - std::format_to(inserter, "{:04}", yr); - break; - case 'm': - std::format_to(inserter, "{:02}", mo); - break; - case 'd': - std::format_to(inserter, "{:02}", dy); - break; - case 'f': - std::format_to(inserter, "{:06}", sub); - break; - case 'F': - std::format_to(inserter, "{:04}-{:02}-{:02}", yr, mo, dy); - break; - case 'T': - std::format_to(inserter, "{:02}:{:02}:{:02}.{:06}", h, m, s, sub); - break; - case '%': - result += '%'; - break; - default: - result += '%'; - result += options[i]; - break; - } - } else { - result += options[i]; - } +inline constexpr auto kSubsecondBase = 1000; +inline constexpr auto kSecondsPerMinute = 60; +inline constexpr auto kMinutesPerHour = 60; +inline constexpr auto kSecondsPerHour = 3600; +inline constexpr auto kTwoDigitWidth = std::size_t{2}; +inline constexpr auto kSubsecondChunkWidth = std::size_t{3}; +using HoursPeriod = std::ratio; +using MinutesPeriod = std::ratio; +using SecondsPeriod = std::ratio<1>; + +template +// Supported builtin duration periods: +// ratio<3600> - hours +// ratio<60> - minutes +// ratio<1> - seconds +// std::milli - milliseconds +// std::micro - microseconds +// std::nano - nanoseconds +concept SupportedDuration = + std::is_integral_v && !std::is_same_v && + (std::ratio_equal_v || + std::ratio_equal_v || + std::ratio_equal_v || + std::ratio_equal_v || + std::ratio_equal_v || + std::ratio_equal_v); + +template +void WriteHourMinuteSecond(Writer& writer, Magnitude hours, Magnitude minutes, + Magnitude seconds, bool pad_hours) { + if (pad_hours) { + WritePaddedUnsigned(writer, hours, kTwoDigitWidth); + } else { + WriteUnsigned(writer, hours); } - return result; + writer.write(':'); + WritePaddedUnsigned(writer, minutes, kTwoDigitWidth); + writer.write(':'); + WritePaddedUnsigned(writer, seconds, kTwoDigitWidth); } -} // namespace format_internal +template +void WriteTimeOfDayMicros(Writer& writer, + std::chrono::hh_mm_ss const& tod) { + WriteHourMinuteSecond(writer, static_cast(tod.hours().count()), + static_cast(tod.minutes().count()), + static_cast(tod.seconds().count()), true); + writer.write('.'); + WritePaddedUnsigned( + writer, + static_cast( + std::chrono::duration_cast(tod.subseconds()) + .count()), + 2 * kSubsecondChunkWidth); +} -/** - * \brief Format TimePoint to string. - * Uses C++20 std::chrono facilities (hh_mm_ss) for decomposition - * and std::format_to for formatting individual components. - */ -template -struct Formatter> { - template - void Format(std::chrono::time_point const& value, - FormatContext& ctx) const { - auto tp_us = std::chrono::time_point_cast(value); - auto result = format_internal::FormatTimeWithOptions( - ctx.options, tp_us.time_since_epoch()); - ctx.out().write(result); +template +void WriteChronoDateTime(Writer& writer, + std::chrono::system_clock::time_point tp) { + auto const us = std::chrono::time_point_cast(tp); + auto const days = std::chrono::floor(us); + auto const ymd = std::chrono::year_month_day{days}; + auto const tod = std::chrono::hh_mm_ss{us - days}; + WritePaddedUnsigned(writer, + static_cast(static_cast(ymd.year())), 4); + writer.write('-'); + WritePaddedUnsigned(writer, static_cast(ymd.month()), kTwoDigitWidth); + writer.write('-'); + WritePaddedUnsigned(writer, static_cast(ymd.day()), kTwoDigitWidth); + writer.write(' '); + WriteTimeOfDayMicros(writer, tod); +} + +template +void WriteChronoTimeOfDay(Writer& writer, + std::chrono::system_clock::time_point tp) { + auto const us = std::chrono::time_point_cast(tp); + auto const days = std::chrono::floor(us); + auto const tod = std::chrono::hh_mm_ss{us - days}; + WriteTimeOfDayMicros(writer, tod); +} + +template + requires SupportedDuration +void WriteChronoDuration(Writer& writer, + std::chrono::duration value) { + using Magnitude = std::make_unsigned_t; + auto negative = false; + auto abs = Magnitude{}; + if constexpr (std::is_signed_v) { + auto const count = value.count(); + negative = count < 0; + abs = negative ? Magnitude{0} - static_cast(count) + : static_cast(count); + } else { + abs = value.count(); } -}; + auto hours = Magnitude{}; + auto minutes = Magnitude{}; + auto seconds = Magnitude{}; + auto millis = unsigned{}; + auto micros = unsigned{}; + auto nanos = unsigned{}; -template -struct Formatter> { - template - void Format(std::chrono::duration const& value, - FormatContext& ctx) const { - bool has_spec = !ctx.options.empty(); - if (!has_spec) { - auto count = - std::chrono::duration_cast>(value) - .count(); - ctx.out().write(std::to_string(count)); + if constexpr (std::ratio_equal_v) { + nanos = static_cast(TakeRemainder(abs)); + } + if constexpr (std::ratio_equal_v || + std::ratio_equal_v) { + micros = static_cast(TakeRemainder(abs)); + } + if constexpr (std::ratio_equal_v || + std::ratio_equal_v || + std::ratio_equal_v) { + millis = static_cast(TakeRemainder(abs)); + } + if constexpr (!std::ratio_equal_v && + !std::ratio_equal_v) { + seconds = TakeRemainder(abs); + } + if constexpr (!std::ratio_equal_v) { + minutes = TakeRemainder(abs); + } + hours = abs; + + if (negative) { + writer.write('-'); + } + WriteHourMinuteSecond(writer, hours, minutes, seconds, false); + if constexpr (std::ratio_equal_v || + std::ratio_equal_v || + std::ratio_equal_v) { + writer.write('.'); + WritePaddedUnsigned(writer, millis, kSubsecondChunkWidth); + } + if constexpr (std::ratio_equal_v || + std::ratio_equal_v) { + WritePaddedUnsigned(writer, micros, kSubsecondChunkWidth); + } + if constexpr (std::ratio_equal_v) { + WritePaddedUnsigned(writer, nanos, kSubsecondChunkWidth); + } +} + +} // namespace format_internal + +// Formats std::chrono::system_clock::time_point only. Supported values are +// non-negative offsets since the Unix epoch. Output precision is microseconds; +// higher precision inputs are truncated, not rounded. The {:time} option prints +// only the time of day, while all other options print the full date and time. +template +struct Formatter> { + template + void Format( + std::chrono::time_point const& value, + FormatContext& ctx) const { + auto const us = std::chrono::duration_cast( + value.time_since_epoch()); + auto const tp = std::chrono::system_clock::time_point{us}; + if (ctx.options == "time") { + format_internal::WriteChronoTimeOfDay(ctx.out(), tp); } else { - auto us = std::chrono::duration_cast(value); - auto tp = std::chrono::system_clock::time_point{ - std::chrono::duration_cast(us)}; - auto result = format_internal::FormatTimeWithOptions(ctx.options, tp); - ctx.out().write(result); + format_internal::WriteChronoDateTime(ctx.out(), tp); } } }; +template + requires format_internal::SupportedDuration +struct Formatter> { + template + void Format(std::chrono::duration const& value, + FormatContext& ctx) const { + format_internal::WriteChronoDuration(ctx.out(), value); + } +}; + } // namespace ae + #endif // AETHER_MISCPP_FORMAT_FORMAT_TIME_H_ diff --git a/src/aether-miscpp/format/formatter.h b/src/aether-miscpp/format/formatter.h index 3c0ec18..7a91e28 100644 --- a/src/aether-miscpp/format/formatter.h +++ b/src/aether-miscpp/format/formatter.h @@ -20,11 +20,13 @@ #include namespace ae { -// Format context for each variable -template +// Context passed to Formatter::Format. Custom formatters should write via +// ctx.out().write(...). ctx.options contains text from placeholders written as +// {:...}; placeholders without ':' provide an empty options string. +template class FormatContext { public: - constexpr FormatContext(TStream& out, std::string_view opt_string) + constexpr FormatContext(Writer& out, std::string_view opt_string) : options{opt_string}, out_{&out} {} constexpr auto& out() { return *out_; } @@ -32,13 +34,14 @@ class FormatContext { std::string_view options; private: - TStream* out_; + Writer* out_; }; +// To format a custom type, specialize ae::Formatter and implement: +// template +// void Format(MyType const&, ae::FormatContext& ctx) const; template -struct Formatter { - // provide Format method for your type -}; +struct Formatter {}; } // namespace ae diff --git a/src/aether-miscpp/format/numeric_helpers.h b/src/aether-miscpp/format/numeric_helpers.h new file mode 100644 index 0000000..52f9157 --- /dev/null +++ b/src/aether-miscpp/format/numeric_helpers.h @@ -0,0 +1,90 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_MISCPP_FORMAT_NUMERIC_HELPERS_H_ +#define AETHER_MISCPP_FORMAT_NUMERIC_HELPERS_H_ + +#include +#include +#include +#include +#include +#include +#include + +namespace ae::format_internal { + +inline constexpr auto kDecimalBase = 10; + +template +Magnitude TakeRemainder(Magnitude& value) noexcept { + static_assert(std::is_unsigned_v); + if constexpr (std::cmp_less(std::numeric_limits::max(), Base)) { + auto const rem = value; + value = 0; + return rem; + } else { + auto const divisor = static_cast(Base); + auto const rem = static_cast(value % divisor); + value = static_cast(value / divisor); + return rem; + } +} + +template +void WriteUnsigned(Writer& out, Magnitude value) { + static_assert(std::is_unsigned_v); + if (value == 0) { + out.write(std::string_view{"0"}); + return; + } + auto buff = std::array::digits10 + 2>{}; + auto wp = std::size_t{}; + while (value != 0) { + auto const digit = TakeRemainder(value); + buff[wp++] = static_cast('0' + digit); // NOLINT(*bounds*) + } + std::reverse(buff.begin(), buff.begin() + static_cast(wp)); + out.write(std::string_view{buff.data(), wp}); +} + +// Writes an unsigned magnitude with zero padding. width is a minimum, not a +// maximum; values longer than width are not truncated. Callers must pass an +// unsigned Magnitude and keep width <= numeric_limits::digits10 + 2 +// because this function uses a fixed local buffer. No runtime width check is +// performed. +template +void WritePaddedUnsigned(Writer& out, Magnitude value, std::size_t width) { + static_assert(std::is_unsigned_v); + auto buff = std::array::digits10 + 2>{}; + auto wp = std::size_t{}; + if (value == 0) { + buff[wp++] = '0'; // NOLINT(*bounds*) + } + while (value != 0) { + auto const digit = TakeRemainder(value); + buff[wp++] = static_cast('0' + digit); // NOLINT(*bounds*) + } + while (wp < width) { + buff[wp++] = '0'; // NOLINT(*bounds*) + } + std::reverse(buff.begin(), buff.begin() + static_cast(wp)); + out.write(std::string_view{buff.data(), wp}); +} + +} // namespace ae::format_internal + +#endif // AETHER_MISCPP_FORMAT_NUMERIC_HELPERS_H_ diff --git a/tests/test-format/CMakeLists.txt b/tests/test-format/CMakeLists.txt index b5b3a7a..454c16d 100644 --- a/tests/test-format/CMakeLists.txt +++ b/tests/test-format/CMakeLists.txt @@ -26,9 +26,7 @@ if(NOT CM_PLATFORM) add_executable(${PROJECT_NAME}) target_sources(${PROJECT_NAME} PRIVATE ${test_srcs}) - # for aether - target_include_directories(${PROJECT_NAME} PRIVATE ${INLCUDE_DIR}) - target_link_libraries(${PROJECT_NAME} PRIVATE unity) + target_link_libraries(${PROJECT_NAME} PRIVATE aether::miscpp unity) add_test(NAME ${PROJECT_NAME} COMMAND $) else() diff --git a/tests/test-format/test-format-str.cpp b/tests/test-format/test-format-str.cpp index bf4b341..1974e17 100644 --- a/tests/test-format/test-format-str.cpp +++ b/tests/test-format/test-format-str.cpp @@ -16,14 +16,58 @@ #include +#include +#include +#include +#include + #include "aether-miscpp/format/format.h" namespace ae::test_format_str { + +static_assert(FormatScheme::kMaxFormatParts == 21); + +constexpr auto kConstexprScheme = FormatScheme{"{} {{}}"}; +static_assert(kConstexprScheme.source.size() == 7); +static_assert(kConstexprScheme.part_count == 4); +static_assert(!kConstexprScheme.overflow); +static_assert(kConstexprScheme.parts[0].placeholder); +static_assert(!kConstexprScheme.parts[1].placeholder); + +constexpr auto kEnvFormatScheme = FormatScheme{ + "Platform:{}\n" + "Compiler:{}\n" + "Compiler version:{}\n" + "Library version:{}\n" + "Api version:{}\n" + "CPU arch:{}\n" + "Endianness:{}\n" + "UTMid:{}\n"}; +constexpr auto kEnvPlaceholderOffsets = + std::array{9, 21, 41, 60, 75, 87, 101, 110}; + +constexpr bool EnvPlaceholdersMatch() { + for (std::size_t i = 0; i < kEnvPlaceholderOffsets.size(); ++i) { + auto const part = kEnvFormatScheme.parts[(i * 2) + 1]; + if (!part.placeholder || part.offset != kEnvPlaceholderOffsets[i] || + part.size != 2 || kEnvFormatScheme.source[part.offset] != '{' || + kEnvFormatScheme.source[part.offset + 1] != '}') { + return false; + } + } + return true; +} + +static_assert(kEnvFormatScheme.source.size() == 113); +static_assert(kEnvFormatScheme.part_count == 17); +static_assert(!kEnvFormatScheme.overflow); +static_assert(EnvPlaceholdersMatch()); + void test_BracketInTheBegin() { auto str1 = Format("{ kek"); TEST_ASSERT_EQUAL_STRING("{ kek", str1.c_str()); auto str2 = Format("{} kek"); - TEST_ASSERT_EQUAL_STRING(" kek", str2.c_str()); + TEST_ASSERT_EQUAL_STRING("{} kek", str2.c_str()); auto str3 = Format("} kek {"); TEST_ASSERT_EQUAL_STRING("} kek {", str3.c_str()); @@ -33,7 +77,7 @@ void test_BracketInTheEnd() { auto str1 = Format("kek {"); TEST_ASSERT_EQUAL_STRING("kek {", str1.c_str()); auto str2 = Format("kek {}"); - TEST_ASSERT_EQUAL_STRING("kek ", str2.c_str()); + TEST_ASSERT_EQUAL_STRING("kek {}", str2.c_str()); } void test_BracketEscape() { @@ -41,10 +85,102 @@ void test_BracketEscape() { TEST_ASSERT_EQUAL_STRING("kek {}", str1.c_str()); auto str2 = Format("{} kek {{}}"); - TEST_ASSERT_EQUAL_STRING(" kek {}", str2.c_str()); + TEST_ASSERT_EQUAL_STRING("{} kek {}", str2.c_str()); - auto str3 = Format("{} kek {{}}", 12, 42); + auto str3 = Format("{} kek {{{}}}", 12, 42); TEST_ASSERT_EQUAL_STRING("12 kek {42}", str3.c_str()); + + auto str4 = Format("{{}}"); + TEST_ASSERT_EQUAL_STRING("{}", str4.c_str()); + + auto str5 = Format("{{}"); + TEST_ASSERT_EQUAL_STRING("{}", str5.c_str()); + + auto str6 = Format("{}}", 1); + TEST_ASSERT_EQUAL_STRING("1}", str6.c_str()); + + auto str7 = Format("{ {{"); + TEST_ASSERT_EQUAL_STRING("{ {{", str7.c_str()); + + auto str8 = Format("}} {"); + TEST_ASSERT_EQUAL_STRING("} {", str8.c_str()); + + auto str9 = Format("x { y {{ z", 123); + TEST_ASSERT_EQUAL_STRING("x { y {{ z", str9.c_str()); +} + +void test_MissingArgsAndLiteralNul() { + auto str = Format(std::string_view{"a{b}c"}); + TEST_ASSERT_EQUAL_UINT32(5, static_cast(str.size())); + TEST_ASSERT_EQUAL_STRING("a{b}c", str.c_str()); + + auto option_missing_arg = Format("{:time}"); + TEST_ASSERT_EQUAL_STRING("{:time}", option_missing_arg.c_str()); + + auto lit = Format("abc\0def"); + TEST_ASSERT_EQUAL_UINT32(7, static_cast(lit.size())); + TEST_ASSERT_EQUAL_CHAR('a', lit[0]); + TEST_ASSERT_EQUAL_CHAR('b', lit[1]); + TEST_ASSERT_EQUAL_CHAR('c', lit[2]); + TEST_ASSERT_EQUAL_CHAR('\0', lit[3]); + TEST_ASSERT_EQUAL_CHAR('d', lit[4]); + TEST_ASSERT_EQUAL_CHAR('e', lit[5]); + TEST_ASSERT_EQUAL_CHAR('f', lit[6]); +} + +void test_FormatTo() { + std::string out{"x"}; + FormatTo(out, "-{}-", 42); + TEST_ASSERT_EQUAL_STRING("x-42-", out.c_str()); + + std::ostringstream oss; + FormatTo(oss, "{}:{}", 1, 2); + TEST_ASSERT_EQUAL_STRING("1:2", oss.str().c_str()); +} + +void test_FormatSchemeRuntimeAndOverflow() { + constexpr auto scheme = FormatScheme{"{} {{}}"}; + static_assert(scheme.source.size() == 7); + TEST_ASSERT_EQUAL_STRING("1 {}", Format(scheme, 1).c_str()); + + auto runtime_format = std::string{"{}:{}"}; + auto runtime = Format(std::string_view{runtime_format}, 1, 2); + TEST_ASSERT_EQUAL_STRING("1:2", runtime.c_str()); + + auto many = std::string{}; + for (auto i = 0; i < 70; ++i) { + many += "{}"; + } + auto overflow = Format(std::string_view{many}, 1, 2, 3); + auto expected = many + " OVERFLOW"; + TEST_ASSERT_EQUAL_STRING(expected.c_str(), overflow.c_str()); +} + +void test_FormatSchemeTenPlaceholdersFit() { + constexpr auto scheme = FormatScheme{"a{}b{}c{}d{}e{}f{}g{}h{}i{}j{}k"}; + static_assert(scheme.part_count == 21); + static_assert(!scheme.overflow); + + auto result = Format(scheme, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + TEST_ASSERT_EQUAL_STRING("a1b2c3d4e5f6g7h8i9j10k", result.c_str()); +} + +void test_FormatSchemeElevenPlaceholdersOverflow() { + constexpr auto scheme = FormatScheme{"a{}b{}c{}d{}e{}f{}g{}h{}i{}j{}k{}l"}; + static_assert(scheme.overflow); + + auto result = Format(scheme, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + TEST_ASSERT_EQUAL_STRING("a{}b{}c{}d{}e{}f{}g{}h{}i{}j{}k{}l OVERFLOW", + result.c_str()); +} + +void test_FormatToRuntimeFormatSource() { + auto out = std::string{"prefix:"}; + auto format = std::string{"{}"}; + auto scheme = FormatScheme{std::string_view{format}}; + FormatTo(out, scheme, std::string(128, 'x')); + auto expected = std::string{"prefix:"} + std::string(128, 'x'); + TEST_ASSERT_EQUAL_STRING(expected.c_str(), out.c_str()); } } // namespace ae::test_format_str @@ -53,5 +189,11 @@ int test_format_str() { RUN_TEST(ae::test_format_str::test_BracketInTheBegin); RUN_TEST(ae::test_format_str::test_BracketInTheEnd); RUN_TEST(ae::test_format_str::test_BracketEscape); + RUN_TEST(ae::test_format_str::test_MissingArgsAndLiteralNul); + RUN_TEST(ae::test_format_str::test_FormatTo); + RUN_TEST(ae::test_format_str::test_FormatSchemeRuntimeAndOverflow); + RUN_TEST(ae::test_format_str::test_FormatSchemeTenPlaceholdersFit); + RUN_TEST(ae::test_format_str::test_FormatSchemeElevenPlaceholdersOverflow); + RUN_TEST(ae::test_format_str::test_FormatToRuntimeFormatSource); return UNITY_END(); } diff --git a/tests/test-format/test-format-time.cpp b/tests/test-format/test-format-time.cpp index 1e57435..e1aad18 100644 --- a/tests/test-format/test-format-time.cpp +++ b/tests/test-format/test-format-time.cpp @@ -16,37 +16,146 @@ #include -#include #include +#include +#include +#include #include "aether-miscpp/format/format.h" namespace ae::test_format_time { -using TimePoint = std::chrono::time_point; +using TimePoint = + std::chrono::time_point; +using SteadyTimePoint = + std::chrono::time_point; +using UnsupportedSteadyTimePoint = + std::chrono::time_point; + +template +concept HasStringFormatter = requires(ae::Formatter formatter, T const& value, + ae::FormatContext< + ae::format_internal::StringWriter>& ctx) { + formatter.Format(value, ctx); +}; + +static_assert(HasStringFormatter); +static_assert(!HasStringFormatter); +static_assert(!HasStringFormatter); +static_assert(!HasStringFormatter); +static_assert(!HasStringFormatter>); +static_assert(!HasStringFormatter>>); +static_assert(HasStringFormatter>>); + +} // namespace ae::test_format_time + +template <> +struct ae::Formatter { + template + void Format(ae::test_format_time::SteadyTimePoint const&, + ae::FormatContext& ctx) const { + ctx.out().write("steady"); + } +}; + +namespace ae::test_format_time { void test_FormatTimePoint() { - auto tp = TimePoint{std::chrono::milliseconds{9913675}}; - auto tp_str = Format("=:-{:%H:%M:%S}", tp); - TEST_ASSERT_EQUAL_STRING("=:-02:45:13.675000", tp_str.data()); + auto tp = + std::chrono::time_point{ + std::chrono::microseconds{1234567}}; + auto tp_str = Format("{}", tp); + TEST_ASSERT_EQUAL_STRING("1970-01-01 00:00:01.234567", tp_str.data()); + auto tp_time_str = Format("{:time}", tp); + TEST_ASSERT_EQUAL_STRING("00:00:01.234567", tp_time_str.data()); + auto tp_unknown_str = Format("{time}", tp); + TEST_ASSERT_EQUAL_STRING("1970-01-01 00:00:01.234567", tp_unknown_str.data()); auto tp2 = TimePoint{std::chrono::milliseconds{9913675}}; - auto tp2_str = Format("[{:%H:%M:%S}]", tp2); - TEST_ASSERT_EQUAL_STRING("[02:45:13.675000]", tp2_str.data()); + auto tp2_str = Format("[{}]", tp2); + TEST_ASSERT_EQUAL_STRING("[1970-01-01 02:45:13.675000]", tp2_str.data()); + auto tp2_time_str = Format("{:time}", tp2); + TEST_ASSERT_EQUAL_STRING("02:45:13.675000", tp2_time_str.data()); + + auto steady_str = Format("{}", SteadyTimePoint{}); + TEST_ASSERT_EQUAL_STRING("steady", steady_str.data()); +} + +void test_FormatNanosecondTimePointTruncatesToMicros() { + auto tp = + std::chrono::time_point{ + std::chrono::nanoseconds{1234567890}}; + + auto tp_str = Format("{}", tp); + TEST_ASSERT_EQUAL_STRING("1970-01-01 00:00:01.234567", tp_str.data()); + + auto tp_time_str = Format("{:time}", tp); + TEST_ASSERT_EQUAL_STRING("00:00:01.234567", tp_time_str.data()); } void test_FormatListOfTimePoint() { auto tp_list = std::list{TimePoint{std::chrono::milliseconds{13675}}, TimePoint{std::chrono::milliseconds{14000}}, TimePoint{std::chrono::milliseconds{15888}}}; - auto tp_list_str = Format("{:o=%S}", tp_list); - TEST_ASSERT_EQUAL_STRING("o=13.675000, o=14.000000, o=15.888000", + auto tp_list_str = Format("{}", tp_list); + TEST_ASSERT_EQUAL_STRING("1970-01-01 00:00:13.675000, 1970-01-01 00:00:14.000000, 1970-01-01 00:00:15.888000", tp_list_str.data()); } + +void test_Duration() { + TEST_ASSERT_EQUAL_STRING("1:00:00", Format("{}", std::chrono::hours{1}).data()); + TEST_ASSERT_EQUAL_STRING("15:00:00", Format("{}", std::chrono::hours{15}).data()); + TEST_ASSERT_EQUAL_STRING("25:00:00", Format("{}", std::chrono::hours{25}).data()); + TEST_ASSERT_EQUAL_STRING("100:00:00", Format("{}", std::chrono::hours{100}).data()); + TEST_ASSERT_EQUAL_STRING("1:01:00", Format("{}", std::chrono::minutes{61}).data()); + TEST_ASSERT_EQUAL_STRING("25:01:00", Format("{}", std::chrono::minutes{1501}).data()); + TEST_ASSERT_EQUAL_STRING("0:01:01", Format("{}", std::chrono::seconds{61}).data()); + TEST_ASSERT_EQUAL_STRING("25:01:01", Format("{}", std::chrono::seconds{90061}).data()); + TEST_ASSERT_EQUAL_STRING("0:00:00.001", Format("{}", std::chrono::milliseconds{1}).data()); + TEST_ASSERT_EQUAL_STRING("25:01:01.002", Format("{}", std::chrono::milliseconds{90061002}).data()); + auto d = std::chrono::microseconds{-3723004005LL}; + auto s = Format("{}", d); + TEST_ASSERT_EQUAL_STRING("-1:02:03.004005", s.data()); + TEST_ASSERT_EQUAL_STRING("25:01:01.002003004", + Format("{}", std::chrono::nanoseconds{90061002003004LL}).data()); + + using TwoHourDuration = std::chrono::duration>; + TEST_ASSERT_EQUAL_STRING("25:00:00", Format("{}", TwoHourDuration{25}).data()); + + auto min_s = Format("{}", std::chrono::microseconds::min()); + TEST_ASSERT_EQUAL_STRING("-2562047788:00:54.775808", min_s.data()); + + using Int8Millis = std::chrono::duration; + using Int16Micros = std::chrono::duration; + using Int16Nanos = std::chrono::duration; + using Int32Micros = std::chrono::duration; + using Int32Nanos = std::chrono::duration; + TEST_ASSERT_EQUAL_STRING("0:00:00.127", Format("{}", Int8Millis{127}).data()); + TEST_ASSERT_EQUAL_STRING("-0:00:00.128", + Format("{}", Int8Millis{std::numeric_limits::min()}).data()); + TEST_ASSERT_EQUAL_STRING("0:00:00.032767", + Format("{}", Int16Micros{std::numeric_limits::max()}).data()); + TEST_ASSERT_EQUAL_STRING("-0:00:00.032768", + Format("{}", Int16Micros{std::numeric_limits::min()}).data()); + TEST_ASSERT_EQUAL_STRING("0:00:00.000032767", + Format("{}", Int16Nanos{std::numeric_limits::max()}).data()); + TEST_ASSERT_EQUAL_STRING("-0:00:00.000032768", + Format("{}", Int16Nanos{std::numeric_limits::min()}).data()); + TEST_ASSERT_EQUAL_STRING("0:00:02.147483647", + Format("{}", Int32Nanos{std::numeric_limits::max()}).data()); + TEST_ASSERT_EQUAL_STRING("-0:00:02.147483648", + Format("{}", Int32Nanos{std::numeric_limits::min()}).data()); + TEST_ASSERT_EQUAL_STRING("0:35:47.483647", + Format("{}", Int32Micros{std::numeric_limits::max()}).data()); + TEST_ASSERT_EQUAL_STRING("-0:35:47.483648", + Format("{}", Int32Micros{std::numeric_limits::min()}).data()); +} } // namespace ae::test_format_time int test_format_time() { UNITY_BEGIN(); RUN_TEST(ae::test_format_time::test_FormatTimePoint); + RUN_TEST(ae::test_format_time::test_FormatNanosecondTimePointTruncatesToMicros); RUN_TEST(ae::test_format_time::test_FormatListOfTimePoint); + RUN_TEST(ae::test_format_time::test_Duration); return UNITY_END(); } diff --git a/tests/test-format/test-format-types.cpp b/tests/test-format/test-format-types.cpp index 65610ab..edfef18 100644 --- a/tests/test-format/test-format-types.cpp +++ b/tests/test-format/test-format-types.cpp @@ -16,37 +16,132 @@ #include -#include #include -#include -#include #include +#include +#include +#include #include +#include +#include #include +#include #include "aether-miscpp/format/format.h" +namespace ae::test_format_types { +struct CustomType { + int value; +}; + +struct UnsupportedType {}; + +using TestWriter = ae::format_internal::StringWriter; + +static_assert( + ae::format_internal::HasFormatterFor, TestWriter>); +static_assert( + !ae::format_internal::HasFormatterFor, + TestWriter>); +static_assert(ae::format_internal::HasFormatterFor, + TestWriter>); +static_assert(ae::format_internal::HasFormatterFor>, + TestWriter>); +static_assert(!ae::format_internal::HasFormatterFor, + TestWriter>); + +struct ByteProxy { + operator std::uint8_t() const { return value; } + + ByteProxy& operator=(std::uint8_t) { + *mutated = true; + return *this; + } + + std::uint8_t value{}; + bool* mutated{}; +}; + +struct ProxyByteIterator { + using difference_type = std::ptrdiff_t; + using value_type = std::uint8_t; + using iterator_concept = std::input_iterator_tag; + + ByteProxy operator*() const { return ByteProxy{data[index], mutated}; } + ProxyByteIterator& operator++() { + ++index; + return *this; + } + void operator++(int) { ++(*this); } + + friend bool operator==(ProxyByteIterator const& lhs, + ProxyByteIterator const& rhs) { + return lhs.index == rhs.index; + } + + std::uint8_t const* data{}; + std::size_t index{}; + bool* mutated{}; +}; + +struct ProxyByteRange { + using value_type = std::uint8_t; + + ProxyByteIterator begin() const { + return ProxyByteIterator{data.data(), 0, mutated}; + } + ProxyByteIterator end() const { + return ProxyByteIterator{data.data(), data.size(), mutated}; + } + + std::array data{}; + bool* mutated{}; +}; + +} // namespace ae::test_format_types + +template <> +struct ae::Formatter { + template + void Format(ae::test_format_types::CustomType const& value, + ae::FormatContext& ctx) const { + if (value.value == 43) { + TEST_ASSERT_EQUAL_STRING("custom", std::string{ctx.options}.c_str()); + } else { + TEST_ASSERT_EQUAL_UINT32(0, static_cast(ctx.options.size())); + } + auto nested = ae::FormatScheme{"custom:{}"}; + ae::FormatTo(ctx.out(), nested, value.value); + } +}; + namespace ae::test_format_types { void test_FormatNumbers() { auto int8 = Format("{}", static_cast(std::int8_t{42})); auto int16 = Format("{}", std::int16_t{42}); auto int32 = Format("{}", std::int32_t{42}); auto int64 = Format("{}", std::int64_t{42}); + auto negative = Format("{}", std::int64_t{-42}); + auto int64_min = Format("{}", std::numeric_limits::min()); TEST_ASSERT_EQUAL_STRING("42", int8.data()); TEST_ASSERT_EQUAL_STRING("42", int16.data()); TEST_ASSERT_EQUAL_STRING("42", int32.data()); TEST_ASSERT_EQUAL_STRING("42", int64.data()); + TEST_ASSERT_EQUAL_STRING("-42", negative.data()); + TEST_ASSERT_EQUAL_STRING("-9223372036854775808", int64_min.data()); auto uint8 = Format("{}", static_cast(std::uint8_t{42})); auto uint16 = Format("{}", std::uint16_t{42}); auto uint32 = Format("{}", std::uint32_t{42}); auto uint64 = Format("{}", std::uint64_t{42}); + auto uint64_max = Format("{}", std::numeric_limits::max()); TEST_ASSERT_EQUAL_STRING("42", uint8.data()); TEST_ASSERT_EQUAL_STRING("42", uint16.data()); TEST_ASSERT_EQUAL_STRING("42", uint32.data()); TEST_ASSERT_EQUAL_STRING("42", uint64.data()); + TEST_ASSERT_EQUAL_STRING("18446744073709551615", uint64_max.data()); } void test_FormatFloats() { @@ -96,7 +191,11 @@ void test_FormatStrings() { void test_Containers() { std::vector vec_data = {0x7f, 0x01, 0x42}; auto vec_data_str = Format("{}", vec_data); - TEST_ASSERT_EQUAL_STRING("7f0142", vec_data_str.data()); + TEST_ASSERT_EQUAL_STRING("127, 1, 66", vec_data_str.data()); + + std::vector bytes = {0x7f, 0x01, 0x42}; + auto bytes_str = Format("{}", bytes); + TEST_ASSERT_EQUAL_STRING("0x7f0142", bytes_str.data()); std::vector vec_messages = {"hello", "beautiful", "world"}; auto vec_messages_str = Format("[{}]", vec_messages); @@ -105,7 +204,7 @@ void test_Containers() { std::list list_data = {0xff, 0x01, 0x42}; auto list_data_str = Format("{}", list_data); - TEST_ASSERT_EQUAL_STRING("ff0142", list_data_str.data()); + TEST_ASSERT_EQUAL_STRING("0xff0142", list_data_str.data()); std::list list_enum = { Int8EnumType::kOne, Int8EnumType::kFortyTwo, Int8EnumType::kOne}; @@ -116,9 +215,19 @@ void test_Containers() { auto arr_data_str = Format("{}", arr_data); TEST_ASSERT_EQUAL_STRING("255, 1, 66", arr_data_str.data()); + std::vector> nested{{1, 2}, {3, 4}}; + auto nested_str = Format("{}", nested); + TEST_ASSERT_EQUAL_STRING("[1, 2], [3, 4]", nested_str.data()); + auto arr_floats = std::array{25.5F, 4.2F, 35.4F}; auto arr_floats_str = Format("{}", arr_floats); TEST_ASSERT_EQUAL_STRING("25.5, 4.2, 35.4", arr_floats_str.data()); + + auto mutated = false; + auto proxy_bytes = ProxyByteRange{{0x7f, 0x01, 0x42}, &mutated}; + auto proxy_bytes_str = Format("{}", proxy_bytes); + TEST_ASSERT_EQUAL_STRING("0x7f0142", proxy_bytes_str.data()); + TEST_ASSERT_FALSE(mutated); } void test_Optional() { @@ -131,6 +240,14 @@ void test_Optional() { TEST_ASSERT_EQUAL_STRING("nullopt", opt_no_value_str.data()); } +void test_CustomFormatterAndEnums() { + auto out = Format("{}", CustomType{42}); + TEST_ASSERT_EQUAL_STRING("custom:42", out.data()); + + auto out_with_options = Format("{:custom}", CustomType{43}); + TEST_ASSERT_EQUAL_STRING("custom:43", out_with_options.data()); +} + } // namespace ae::test_format_types int test_format_types() { @@ -141,5 +258,6 @@ int test_format_types() { RUN_TEST(ae::test_format_types::test_FormatStrings); RUN_TEST(ae::test_format_types::test_Containers); RUN_TEST(ae::test_format_types::test_Optional); + RUN_TEST(ae::test_format_types::test_CustomFormatterAndEnums); return UNITY_END(); } diff --git a/tests/test-meta/CMakeLists.txt b/tests/test-meta/CMakeLists.txt index 8bc7722..4edf6e6 100644 --- a/tests/test-meta/CMakeLists.txt +++ b/tests/test-meta/CMakeLists.txt @@ -28,9 +28,7 @@ if(NOT CM_PLATFORM) add_executable(${PROJECT_NAME}) target_sources(${PROJECT_NAME} PRIVATE ${test_srcs}) - # for aether - target_include_directories(${PROJECT_NAME} PRIVATE ${INLCUDE_DIR}) - target_link_libraries(${PROJECT_NAME} PRIVATE unity) + target_link_libraries(${PROJECT_NAME} PRIVATE aether::miscpp unity) if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") target_compile_options(${PROJECT_NAME} PUBLIC /Zc:preprocessor) endif() diff --git a/tests/test-reflect/CMakeLists.txt b/tests/test-reflect/CMakeLists.txt index 1635717..8c0b208 100644 --- a/tests/test-reflect/CMakeLists.txt +++ b/tests/test-reflect/CMakeLists.txt @@ -25,9 +25,7 @@ if(NOT CM_PLATFORM) add_executable(${PROJECT_NAME}) target_sources(${PROJECT_NAME} PRIVATE ${test_srcs}) - # for aether - target_include_directories(${PROJECT_NAME} PRIVATE ${INLCUDE_DIR}) - target_link_libraries(${PROJECT_NAME} PRIVATE unity) + target_link_libraries(${PROJECT_NAME} PRIVATE aether::miscpp unity) add_test(NAME ${PROJECT_NAME} COMMAND $) diff --git a/tests/test-types/CMakeLists.txt b/tests/test-types/CMakeLists.txt index d8b2056..e323243 100644 --- a/tests/test-types/CMakeLists.txt +++ b/tests/test-types/CMakeLists.txt @@ -28,9 +28,7 @@ else() add_executable(${PROJECT_NAME}) target_sources(${PROJECT_NAME} PRIVATE ${test_srcs}) - # for aether - target_include_directories(${PROJECT_NAME} PRIVATE ${INLCUDE_DIR}) - target_link_libraries(${PROJECT_NAME} PRIVATE unity) + target_link_libraries(${PROJECT_NAME} PRIVATE aether::miscpp unity) if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") target_compile_options(${PROJECT_NAME} PUBLIC /Zc:preprocessor) endif() From 11a78d808d20b12c8e3817f6c43c7326ccf1292c Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Thu, 9 Jul 2026 18:07:25 +0500 Subject: [PATCH 2/2] add clang-tidy --- .clang-tidy | 45 +++++++++++++++++ opencode.json | 131 +++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 153 insertions(+), 23 deletions(-) create mode 100644 .clang-tidy diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..c437351 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,45 @@ +Checks: + - core* + - cppcoreguidelines-* + - modernize* + - bugprone-* + - performance-* + - readability-* + - portability-* + - clang-diagnostic-* + - misc-* + - google-build-namespaces + - google-build-using-namespace + - google-default-arguments + - google-explicit-constructor + - google-readability-casting + - -modernize-use-trailing-return-type + - -modernize-use-nodiscard + - -modernize-concat-nested-namespaces + - -modernize-use-default-member-init + - -modernize-use-ranges + - -modernize-use-designated-initializers + - -misc-non-private-member-variables-in-classes + - -misc-use-internal-linkage + - -misc-use-anonymous-namespace + - -cppcoreguidelines-pro-type-vararg + - -cppcoreguidelines-non-private-member-variables-in-classes + - -cppcoreguidelines-special-member-functions + - -cppcoreguidelines-pro-type-reinterpret-cast + - -cppcoreguidelines-pro-type-union-access + - -cppcoreguidelines-pro-bounds-pointer-arithmetic + - -cppcoreguidelines-pro-type-member-init + - -cppcoreguidelines-owning-memory + - -cppcoreguidelines-avoid-const-or-ref-data-members + - -cppcoreguidelines-macro-usage + - -cppcoreguidelines-pro-bounds-constant-array-index + - -cppcoreguidelines-pro-bounds-array-to-pointer-decay + - -cppcoreguidelines-pro-type-static-cast-downcast + - -bugprone-easily-swappable-parameters + - -readability-named-parameter + - -readability-identifier-length + +CheckOptions: + bugprone-assert-side-effect.CheckFunctionCalls: true + performance-move-const-arg.CheckTriviallyCopyableMove: false + cppcoreguidelines-avoid-do-while.IgnoreMacros: true diff --git a/opencode.json b/opencode.json index 819eeee..69b4a21 100644 --- a/opencode.json +++ b/opencode.json @@ -1,10 +1,86 @@ { "$schema": "https://opencode.ai/config.json", + "model": "openai/gpt-5.5-fast", + "small_model": "openai/gpt-5.4-mini-fast", + "default_agent": "team-lead", "agent": { - "builder_cpp": { + "team-lead": { + "mode": "primary", + "model": "openai/gpt-5.5-fast", + "reasoningEffort": "medium", + "description": "The main agent to rule the others on the way to work on code.", + "prompt": "You are team-manager. You do not write code, build, or test directly. You coordinate specialized agents: @explorer finds relevant files and facts; @architect designs the solution and writes implementation instructions; @coder edits code from precise instructions; @builder validates CMake/Ninja builds and reports compiler or linker errors; @tester runs unit/smoke tests and reports results; @sanity-reviewer performs a fast task/architecture match check; @code-reviewer performs deep final code review. Coordination rule: run workflow stages sequentially. Never start @tester in the same step or same tool batch as @builder. Start @tester only after you have received a successful @builder report for the current implementation. Workflow: analyze the request; use @explorer when code context is needed; ask @architect for design and coder checklist; require user approval for medium/high-risk changes; ask @coder to implement; ask @builder to validate the build; if @builder reports build failure, send the report back to @coder and repeat; after build validation succeeds, ask @tester to run tests; if tests fail, send the report back to @coder and repeat build and tests; after tests pass, ask @sanity-reviewer for a fast task/architecture match check using the changed-file list reported by @coder, and tell @sanity-reviewer to ignore unrelated worktree changes outside that list; if sanity review blocks, route the issue to @coder for implementation mismatch or @architect for architecture mismatch, then rebuild and retest; only after sanity review approves, ask @code-reviewer for deep final review; if deep review blocks, route directly to @architect for revised instructions, then continue with @coder, @builder, @tester, @sanity-reviewer, and @code-reviewer again. Loop control: allow at most three implementation fix cycles for the same task. A fix cycle is @coder -> @builder -> @tester -> @sanity-reviewer -> @code-reviewer. After three failed cycles, or immediately when failures indicate design/API/ownership/lifetime/CMake/requirement mismatch, escalate to @architect to rethink the solution before more coding. If @code-reviewer reports the same issue twice, escalate to @architect. If @coder says the instructions are ambiguous or the fix would change the approved design, escalate to @architect.", + "permission": { + "edit": "deny", + "bash": "deny", + "task": "allow" + }, + "temperature": 0.1 + }, + "explorer": { + "mode": "subagent", + "model": "openai/gpt-5.4-mini-fast", + "reasoningEffort": "low", + "description": "Read-only codebase exploration before architecture or implementation work", + "prompt": "You are a read-only C++ codebase explorer. Your task is to find relevant files, APIs, existing patterns, build targets, tests, and constraints for the requested change. Do not design the solution, do not edit files, do not build, and do not test. Return concise facts with file paths and symbols that @architect and @coder can rely on.", + "permission": { + "edit": "deny", + "bash": "deny", + "grep": "allow", + "glob": "allow", + "list": "allow", + "read": "allow", + "external_directory": "deny" + }, + "temperature": 0.1, + }, + "architect": { + "mode": "all", + "model": "openai/gpt-5.5", + "reasoningEffort": "high", + "description": "Analyze requirements and produce C++ architecture and implementation instructions", + "prompt": "You are a solution architect. You do not write code, build, or test. Use @explorer first for broad codebase discovery when relevant context is missing. After @explorer reports, read files directly only to verify exact APIs, invariants, ownership/lifetime behavior, or details needed for precise implementation instructions. Avoid repeating broad exploration already completed by @explorer. Produce practical architecture and precise implementation instructions for @coder. Include: files to inspect or modify, exact API or behavior changes, invariants to preserve, tests or build commands expected, risks, and things not to change. For medium or high-risk changes involving public APIs, persistence, async/task flow, crypto/security, platform behavior, or CMake structure, explicitly request user approval before implementation.", + "permission": { + "edit": "deny", + "task": { + "*": "deny", + "explorer": "allow" + }, + "bash": { + "*": "deny", + "git log*": "allow", + "git diff*": "allow" + } + } + }, + "coder": { "mode": "subagent", + "model": "openai/gpt-5.5-fast", + "reasoningEffort": "low", + "description": "Write c++ code", + "prompt": "You are a focused C++ implementation agent. You receive precise instructions from @team-lead or @architect and implement only those instructions. Follow AGENTS.md, preserve existing style, avoid unrelated refactors, and keep changes minimal. If instructions are ambiguous, ask for clarification instead of inventing architecture. If a fix requires changing the architect-approved design or you are making a repeated attempt at the same failed issue, stop and ask @architect for revised instructions. Do not run or request build/test validation. After implementation, report what changed and let @team-lead coordinate validation. At the end of your response, report the actual files you changed under: Changed files. Include added, modified, deleted, or renamed files. Do not include files changed by other agents or the user.", + "permission": { + "edit": "allow", + "grep": "allow", + "bash": { + "*": "deny", + "rm *": "ask", + "rm *.txt": "ask", + "rm *.cpp": "allow", + "rm *.h": "allow", + "rm *.hpp": "allow", + "rm *.cmake": "allow" + }, + "external_directory": "deny", + "repo_clone": "deny" + } + }, + "builder": { + "mode": "subagent", + "model": "openai/gpt-5.4-mini-fast", + "reasoningEffort": "low", "description": "Validate project build and analyze compiler logs", - "prompt": "You are c++ code builder specialists. You know how to configure cmake build, run ninja, understand compiler logs for clang and gcc. Your task is to run project build, either full (`ninja`), or for specific target (`ninja `). If build succeeds report succeed, if fails analyze build errors and make a report for other agents to fix the issues. Do not try to fix the issues yourself.", + "prompt": "You are a C++ build validation specialist. Run the requested CMake/Ninja build: full build, specific target, or configured build command. If the build succeeds, report the command, build directory, and success. If the build fails, analyze the full build log and report root-cause errors only. Collapse cascaded diagnostics into the real underlying issue. Group independent failures by file, target, or symbol. For each issue, report the location, root cause, and brief supporting diagnostic. End every report with exactly one marker: Build validation: SUCCESS or Build validation: FAILURE. Do not edit files and do not fix issues yourself.", "permission": { "edit": "deny", "bash": { @@ -14,55 +90,64 @@ } }, "temperature": 0.1, - "steps": 10 + "steps": 5 }, "tester": { "mode": "subagent", + "model": "openai/gpt-5.4-mini-fast", + "reasoningEffort": "low", "description": "Run tests and analyze results", - "prompt": "You are highly qualified quality assurance specialist. Your task is to run unit tests and smoke tests. If unit test fails, analyze the log and point the cases are failed. If smoke test fails, analyze the log and make a report. Do not try to fix the issues yourself or provide solutions. There are other agents to fix them.", + "prompt": "You are a fast test runner and test result reporter. Run tests only after @builder has reported build success for the current implementation. If build success is not confirmed in the current workflow, report that testing is blocked by missing build validation and do not run tests. Your task is to run unit tests and separately run smoke tests, then report what passed and what failed. Before running smoke tests, inspect project instructions such as AGENTS.md to identify what this project defines as smoke tests, where they must be run from, and whether any cleanup is required. If a test fails, report the failing command, relevant output, exit status if available, and a short likely cause. Do not edit files and do not design new tests unless explicitly asked.", "permission": { "edit": "deny", + "read": "allow", + "grep": "allow", + "glob": "allow", "bash": { "*": "deny", + "rm -rf *state": "allow", + "*aether-client-cpp-cloud*": "allow", "ninja test": "allow", "ctest *": "allow" } }, "temperature": 0.1, - "steps": 10 + "steps": 5 }, - "coder": { + "sanity-reviewer": { "mode": "subagent", - "description": "Write c++ code", - "prompt": "You are a highly qualified c++ developer. Your task is to write c++ code that solves the given problem. You get instruction from the team lead/architect. You only implement the ideas in code.", + "model": "openai/gpt-5.4-mini", + "reasoningEffort": "low", + "description": "Fast check that implementation matches the task and proposed architecture", + "prompt": "You are a fast implementation sanity reviewer. Review only the files listed in the changed-file list provided by @coder for the current task. Use the actual diff for those files to verify what changed. Ignore other modified files in the worktree unless they are explicitly included in @coder's changed-file list or explicitly assigned to this task. Check whether the reviewed changes match the user request and architect instructions. Check for missing requested behavior, unrelated changes within the reviewed files, obvious mismatches with the proposed architecture, and incomplete implementation. Also run clang-tidy on changed files - list generated warnings from critical to style. Do not perform deep C++ review. Do not review generated build artifacts, temporary files, logs, or unrelated files. Never edit files. Report: Reviewed files, Matches task, Matches architecture, Blocking mismatches, Approve or Block.", "permission": { - "edit": "allow", - "bash": "ask", - "external_directory": "deny", - "repo_clone": "deny" - } + "grep": "allow", + "edit": "deny", + "bash": { + "*": "deny", + "git log*": "allow", + "git diff*": "allow", + "clang-tidy*": "allow" + } + }, + "temperature": 0.1 }, "code-reviewer": { "mode": "all", + "model": "openai/gpt-5.5", + "reasoningEffort": "high", "description": "Review code and validate if it solves the problem", - "prompt": "You are trained as a code reviewer. Your task is to review the code written by the other agents and validate if it solves the problem. Notice we are working on a cross-platform project for desktop and IoT devices, so pay attention to platform-specific, performance, and security concerns. Never try to fix the issues yourself, just report.", + "prompt": "You are a strict C++ code reviewer. Review the current code diff against the user request and architect instructions. Focus on correctness, undefined behavior, object lifetime, ownership, async/task usage, persistence, CMake target propagation, cross-platform desktop/IoT behavior, performance, and security. Do not review generated build artifacts, temporary files, logs, or unrelated files. Never edit files. Mark repeated or design-level issues as Block. When you Block, route the issue to @architect for revised instructions rather than recommending a local coder fix. Report: Findings, Missing tests, Risk assessment, Approve or Block.", "permission": { + "grep": "allow", "edit": "deny", "bash": { + "*": "deny", "git log*": "allow", "git diff*": "allow" } }, "temperature": 0.1 - }, - "team-lead-architect": { - "mode": "primary", - "description": "The main agent to rule the others on the way to work on code.", - "prompt": "You are team-lead architect. You don't write code, you don't build, you don't test. You manage agent team and architect solutions. You have @coder - to write actual code by your detailed instructions; @builder_cpp to validate builds, analyze compiler errors; @tester to run tests and analyze test logs; @code-reviewer to work in pair with @coder and check if everything made as it's intended.", - "permission": { - "edit": "deny", - "bash": "deny" - } } } }