diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a5b76d..174be64 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,7 @@ target_sources( include/stdx/cx_set.hpp include/stdx/cx_vector.hpp include/stdx/detail/bitset_common.hpp + include/stdx/detail/fmt.hpp include/stdx/detail/list_common.hpp include/stdx/env.hpp include/stdx/for_each_n_args.hpp diff --git a/docs/ct_format.adoc b/docs/ct_format.adoc index 535f782..8b92c51 100644 --- a/docs/ct_format.adoc +++ b/docs/ct_format.adoc @@ -9,7 +9,8 @@ provides `ct_format`, a compile-time function for formatting strings. NOTE: Like xref:ct_string.adoc#_ct_string_hpp[`ct_string`], `ct_format` is available only in C++20 and later. -IMPORTANT: `ct_format` is not yet available on freestanding implementations. +IMPORTANT: `ct_format` is limited on freestanding implementations: format +specifiers are limited to `{}` and `{:x}`. The format string is provided as a template argument, and the arguments to be formatted as regular function arguments. diff --git a/docs/static_assert.adoc b/docs/static_assert.adoc index 6df4730..944fac6 100644 --- a/docs/static_assert.adoc +++ b/docs/static_assert.adoc @@ -3,7 +3,8 @@ `STATIC_ASSERT` is a way to produce compile-time errors using formatted strings. -IMPORTANT: `STATIC_ASSERT` is not yet available on freestanding implementations. +IMPORTANT: `STATIC_ASSERT` is limited on freestanding implementations in the +same way as xref:ct_format.adoc#_ct_format_hpp[`ct_format`]. [source,cpp] ---- diff --git a/include/stdx/ct_format.hpp b/include/stdx/ct_format.hpp index 3427bdb..c2b3d15 100644 --- a/include/stdx/ct_format.hpp +++ b/include/stdx/ct_format.hpp @@ -6,15 +6,13 @@ #include #include #include +#include #include #include #include #include #include -#include -#include - #include #include #include @@ -207,7 +205,7 @@ CONSTEVAL auto perform_format(auto s, auto v) -> ct_string { template constexpr auto format1(Arg arg) { if constexpr (requires { arg_value(arg); }) { - constexpr auto fmtstr = FMT_COMPILE(std::string_view{Fmt}); + constexpr auto fmtstr = STDX_FMT_COMPILE(Fmt); constexpr auto a = arg_value(arg); if constexpr (is_specialization_of_v, format_result>) { diff --git a/include/stdx/detail/fmt.hpp b/include/stdx/detail/fmt.hpp new file mode 100644 index 0000000..3d2abe4 --- /dev/null +++ b/include/stdx/detail/fmt.hpp @@ -0,0 +1,174 @@ +#pragma once + +#if not STDX_FMT_FREESTANDING + +#include +#include + +#include + +#define STDX_FMT_COMPILE(Fmt) FMT_COMPILE(std::string_view{Fmt}) + +#else + +#include +#include +#include + +#include + +#define STDX_FMT_COMPILE(X) [] { return X; } + +namespace stdx { +inline namespace v1 { +namespace fmt { + +namespace detail { +struct fmt_spec { + int len{}; + int base{}; + bool well_formed{true}; +}; + +CONSTEVAL auto parse_fmt_spec(auto fmtstr) -> fmt_spec { + constexpr auto fmt = fmtstr(); + auto i = fmt.begin(); + while (*i != '{') { + ++i; + } + ++i; + if (*i == '}') { + return {.len = 2, .base = 10}; + } else if (*i++ == ':' and *i++ == 'x' and *i == '}') { + return {.len = 4, .base = 16}; + } + return {.well_formed = false}; +} + +template +CONSTEVAL auto formatted_size(fmt_spec, R const &r) -> std::size_t { + return r.size(); +} + +CONSTEVAL auto formatted_size(fmt_spec, char const *str) -> std::size_t { + std::size_t sz{}; + while (*str) { + ++sz; + ++str; + } + return sz; +} + +CONSTEVAL auto formatted_size(fmt_spec, char) -> std::size_t { return 1; } + +template +CONSTEVAL auto formatted_size(fmt_spec s, I i) -> std::size_t { + if (i == 0) { + return 1; + } else { + std::size_t sz{}; + if (i < 0) { + ++sz; + } + + while (i != 0) { + ++sz; + i /= s.base; + } + return sz; + } +} +} // namespace detail + +CONSTEVAL auto formatted_size(auto fmtstr, auto v) -> std::size_t { + constexpr auto spec = detail::parse_fmt_spec(fmtstr); + static_assert( + spec.well_formed, + "Freestanding fmt implementation does not support that format " + "specifier"); + + return fmtstr().size() - spec.len + detail::formatted_size(spec, v); +} + +namespace detail { +template +CONSTEVAL auto format_to(fmt_spec, It dest, R const &r) -> It { + for (auto const c : r) { + *dest++ = c; + } + return dest; +} + +template +CONSTEVAL auto format_to(fmt_spec, It dest, char const *str) -> It { + while (*str) { + *dest++ = *str++; + } + return dest; +} + +template +CONSTEVAL auto format_to(fmt_spec, It dest, char c) -> It { + *dest++ = c; + return dest; +} + +template +CONSTEVAL auto format_to(fmt_spec s, It dest, I i) -> It { + if (i == 0) { + *dest++ = '0'; + } else { + auto abs = [](T v) -> T { return v < 0 ? -v : v; }; + auto to_digit = [](auto n) -> char { return "0123456789abcdef"[n]; }; + + if (i < 0) { + *dest++ = '-'; + } + + auto b = dest; + while (i != 0) { + auto digit = to_digit(abs(i % s.base)); + i /= s.base; + *dest++ = digit; + } + + auto e = dest - 1; + while (b < e) { + auto tmp = *b; + *b++ = *e; + *e-- = tmp; + } + } + return dest; +} + +} // namespace detail + +template +CONSTEVAL auto format_to(It dest, auto fmtstr, auto v) -> void { + constexpr auto fmt = fmtstr(); + constexpr auto spec = detail::parse_fmt_spec(fmtstr); + + // copy to opening { + auto src = fmt.begin(); + while (*src != '{') { + *dest++ = *src++; + } + + // skip fmt spec + src += spec.len; + + // format the value into the buffer + dest = detail::format_to(spec, dest, v); + + // copy the rest of src + while (*src) { + *dest++ = *src++; + } +} + +} // namespace fmt +} // namespace v1 +} // namespace stdx + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0b5bc68..2bef2bf 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -85,6 +85,17 @@ if(${CMAKE_CXX_STANDARD} GREATER_EQUAL 20) indexed_tuple tuple tuple_algorithms) + + add_unit_test( + "ct_format_freestanding_test" + CATCH2 + FILES + "ct_format.cpp" + LIBRARIES + warnings + stdx) + target_compile_definitions(ct_format_freestanding_test + PRIVATE -DSTDX_FMT_FREESTANDING=1) endif() add_subdirectory(fail) diff --git a/test/ct_format.cpp b/test/ct_format.cpp index df6f87c..af34730 100644 --- a/test/ct_format.cpp +++ b/test/ct_format.cpp @@ -3,6 +3,11 @@ #include +#include +#include +#include +#include + using namespace stdx::ct_string_literals; TEST_CASE("detect string format specifiers", "[ct_format]") { @@ -58,6 +63,29 @@ TEST_CASE("format a compile-time integral argument (CX_VALUE)", "[ct_format]") { "Hello 42"_fmt_res); } +TEST_CASE("format a negative compile-time integral argument (CX_VALUE)", + "[ct_format]") { + STATIC_REQUIRE(stdx::ct_format<"Hello {}">(CX_VALUE(-42)) == + "Hello -42"_fmt_res); +} + +TEST_CASE("format most negative compile-time integral argument (CX_VALUE)", + "[ct_format]") { + STATIC_REQUIRE(stdx::ct_format<"Hello {}">( + CX_VALUE(std::numeric_limits::min())) == + "Hello -2147483648"_fmt_res); +} + +TEST_CASE("format zero (CX_VALUE)", "[ct_format]") { + STATIC_REQUIRE(stdx::ct_format<"Hello {}">(CX_VALUE(0)) == + "Hello 0"_fmt_res); +} + +TEST_CASE("format a char (CX_VALUE)", "[ct_format]") { + STATIC_REQUIRE(stdx::ct_format<"Hello {}orld">(CX_VALUE('w')) == + "Hello world"_fmt_res); +} + TEST_CASE("format a compile-time integral argument (ct)", "[ct_format]") { STATIC_REQUIRE(stdx::ct_format<"Hello {}">(stdx::ct<42>()) == "Hello 42"_fmt_res); @@ -73,10 +101,17 @@ TEST_CASE("format a type argument (ct)", "[ct_format]") { "Hello int"_fmt_res); } +TEST_CASE("format a compile-time argument with different base", "[ct_format]") { + STATIC_REQUIRE(stdx::ct_format<"Hello 0x{:x}">(CX_VALUE(42)) == + "Hello 0x2a"_fmt_res); +} + +#if not STDX_FMT_FREESTANDING TEST_CASE("format a compile-time argument with fmt spec", "[ct_format]") { STATIC_REQUIRE(stdx::ct_format<"Hello {:*>#6x}">(CX_VALUE(42)) == "Hello **0x2a"_fmt_res); } +#endif namespace { enum struct E { A }; diff --git a/usage_test/CMakeLists.txt b/usage_test/CMakeLists.txt index 0ff84f4..d490908 100644 --- a/usage_test/CMakeLists.txt +++ b/usage_test/CMakeLists.txt @@ -12,6 +12,6 @@ cpmaddpackage(NAME stdx SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/.." GIT_TAG HEAD) add_executable(app main.cpp) target_link_libraries(app PRIVATE stdx) if(CPP_IMPLEMENTATION STREQUAL "FREESTANDING") - target_compile_definitions(app PRIVATE SIMULATE_FREESTANDING) + target_compile_definitions(app PRIVATE STDX_FMT_FREESTANDING) target_compile_options(app PRIVATE -ffreestanding) endif() diff --git a/usage_test/main.cpp b/usage_test/main.cpp index 4792cb5..06dcc39 100644 --- a/usage_test/main.cpp +++ b/usage_test/main.cpp @@ -9,9 +9,7 @@ #include #include #include -#ifndef SIMULATE_FREESTANDING #include -#endif #include #include #include @@ -35,9 +33,7 @@ #include #include #include -#ifndef SIMULATE_FREESTANDING #include -#endif #include #include #include