From cb71e001e5088e9006c00634ce833f5659af855c Mon Sep 17 00:00:00 2001 From: Markus Mittendrein Date: Fri, 7 Jan 2022 18:40:42 +0100 Subject: Add runtime implementation and refactor format_spec checks in formatters --- include/cxxformat/core.hpp | 27 +++-- include/cxxformat/cxxformat | 1 + include/cxxformat/formatters.hpp | 247 +++++++++++++++++++++++---------------- include/cxxformat/runtime.hpp | 219 ++++++++++++++++++++++++++++++++++ 4 files changed, 382 insertions(+), 112 deletions(-) create mode 100644 include/cxxformat/runtime.hpp (limited to 'include') diff --git a/include/cxxformat/core.hpp b/include/cxxformat/core.hpp index 8669f1c..993060d 100644 --- a/include/cxxformat/core.hpp +++ b/include/cxxformat/core.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -155,17 +156,17 @@ namespace format { template struct formatter; - template - concept has_formatter = requires(T t, format_specifier spec, const NullOutput& out, optional_int z) { - { formatter::format(out, t, spec, z, z) } -> std::same_as; - { formatter::template format(out, t, z, z) } -> std::same_as; + template + concept is_formatter = requires(Value t, format_specifier spec, const NullOutput& out, optional_int z) { + { Formatter::format(out, t, spec, z, z) } -> std::same_as; + { Formatter::conversionSupported(spec) } -> std::same_as; }; template - concept has_fallback_formatter = requires(T t, format_specifier spec, const NullOutput& out, optional_int z) { - { formatter::format(out, t, spec, z, z) } -> std::same_as; - { formatter::template format(out, t, z, z) } -> std::same_as; - }; + concept has_formatter = is_formatter, T>; + + template + concept has_fallback_formatter = is_formatter, T>; template concept has_some_formatter = has_formatter || has_fallback_formatter; @@ -464,8 +465,16 @@ namespace format { static_assert(sizeof(Arg) <= sizeof(std::size_t), "Precision argument type must be at most as big as std::size_t"); precision = nth_argument<*spec.precision>(std::forward(args)...); } + auto&& arg = nth_argument(std::forward(args)...); - return formatter_with_fallback>::template format(out, arg, minWidth, precision); + using formatter = formatter_with_fallback>; + + []() consteval { + // throws on error + formatter::conversionSupported(spec); + }(); + + return formatter::format(out, arg, spec, minWidth, precision); }; (([&]{ diff --git a/include/cxxformat/cxxformat b/include/cxxformat/cxxformat index 6da253a..72bef6f 100644 --- a/include/cxxformat/cxxformat +++ b/include/cxxformat/cxxformat @@ -4,4 +4,5 @@ #include #include #include +#include #include diff --git a/include/cxxformat/formatters.hpp b/include/cxxformat/formatters.hpp index a713018..ea0c08b 100644 --- a/include/cxxformat/formatters.hpp +++ b/include/cxxformat/formatters.hpp @@ -9,10 +9,18 @@ #include #include #include +#include #include +#include namespace format { namespace { + template + constexpr auto operator ""_contains() noexcept + { + return [](char c) constexpr noexcept { return std::string_view{what}.find(c) != std::string_view::npos; }; + } + constexpr void formatPadded(const format_output auto& out, std::string_view subject, char padding, bool leftJustified, optional_int minWidth) { if(subject.length() < minWidth.value_or(0) && !leftJustified) @@ -27,6 +35,51 @@ namespace format { out(padding, *minWidth - subject.length()); } } + + constexpr void simpleConversionSpecifierCheck(std::string_view supported, char c, std::string_view typeDesc) + { + using namespace std::string_literals; + if (supported.find(c) == std::string_view::npos) + { + throw std::invalid_argument{"Unsupported conversion specifier ‘"s + c + "’ for "s + std::string{typeDesc} + "; Supported are "s + std::string{supported}}; + } + } + + constexpr void noAlternative(bool alternative, std::string_view typeDesc) + { + using namespace std::string_literals; + if (alternative) + { + throw std::invalid_argument{"‘#’ (alternative) flag is not supported for "s + std::string{typeDesc}}; + } + } + + constexpr void noSignFlags(char addSign, std::string_view typeDesc) + { + using namespace std::string_literals; + if (addSign != '\0') + { + throw std::invalid_argument{"‘+’ and ‘ ’ (sign related) flags are not supported for %s"s + std::string{typeDesc}}; + } + } + + constexpr void noZeroPadding(char padding, std::string_view typeDesc) + { + using namespace std::string_literals; + if (padding == '0') + { + throw std::invalid_argument{"Zero padding (‘0’ flag) is not supported for %s"s + std::string{typeDesc}}; + } + } + + constexpr void noPrecision(optional_int precision, std::string_view typeDesc) + { + using namespace std::string_literals; + if (precision) + { + throw std::invalid_argument{"Specifying a precision is not supported for "s + std::string{typeDesc}}; + } + } } using invalid_argument = std::invalid_argument; @@ -35,54 +88,25 @@ namespace format { struct formatter { static constexpr void format(const format_output auto& out, T t, format_specifier spec, optional_int minWidth, optional_int precision) { - int base = 10; - switch(spec.conversion) + const auto conv = spec.conversion; + if (conv == 'c') { - case 'v': - break; - case 'c': - { - // TODO: see static_assert version - char converted = static_cast(static_cast(t)); - formatPadded(out, {&converted, 1}, ' ', spec.leftJustified, minWidth); - return; - } - case 'i': case 'd': - if constexpr (std::unsigned_integral) - { - throw invalid_argument{"%i and %d are not supported for unsigned integrals"}; - } - break; - case 'o': - base = 8; - if constexpr (std::signed_integral) - { - throw invalid_argument{"%o is not supported for signed integrals"}; - } - break; - case 'x': case 'X': - base = 16; - if constexpr (std::signed_integral) - { - throw invalid_argument{"%x and %X are not supported for signed integrals"}; - } - break; - case 'u': - if constexpr (std::signed_integral) - { - throw invalid_argument{"%u is not supported for signed integrals"}; - } - break; - default: - throw invalid_argument{std::string{"The given integral type does not support conversion specifier ‘"} + spec.conversion + std::string{"’"}}; + char converted = static_cast(static_cast(t)); + formatPadded(out, {&converted, 1}, ' ', spec.leftJustified, minWidth); + return; } + const int base = std::unsigned_integral ? (conv == 'o' ? 8 : "xX"_contains(conv) ? 16 : 10) : 10; + std::string_view converted; std::array::digits + 2) / 3> result; if (precision != 0 || t != 0) { const auto [end, ec] = std::to_chars(result.data(), result.data() + result.size(), t, base); - assert(ec == std::errc{}); + if (ec != std::errc{}) + { + throw std::runtime_error{"Unexpected error from to_chars: " + std::make_error_condition(ec).message()}; + } converted = {result.data(), end}; } const bool hasMinusSign = !std::unsigned_integral && converted.starts_with('-'); @@ -175,77 +199,96 @@ namespace format { return; } - template - static constexpr void format(const format_output auto& out, T t, optional_int minWidth, optional_int precision) + static constexpr void conversionSupported(format_specifier spec) { - constexpr auto conv = spec.conversion; - if constexpr (conv == 'c') + const auto conv = spec.conversion; + + // TODO: what is the right thing to do for %c? + // static_assert(std::same_as, "%c only accepts int as argument"); + + if constexpr (std::signed_integral) { - // TODO: what is the right thing to do here? - // static_assert(std::same_as, "%c only accepts int as argument"); + simpleConversionSpecifierCheck("cidv", conv, "signed integral type"); } - else if constexpr (conv == 'i' || conv == 'd') + else { - static_assert(std::signed_integral, "%i and %d only accept signed integral types"); + static_assert(std::unsigned_integral, "Integral type is neither signed nor unsigned?"); + simpleConversionSpecifierCheck("coxXuv", conv, "unsigned integral type"); } - else if constexpr (conv == 'o' || conv == 'x' || conv == 'X' || conv == 'u') + + if (spec.alternative && !"oxX"_contains(conv)) { - static_assert(std::unsigned_integral, "%o, %x, %X and %u only accept unsigned integral types"); + throw std::invalid_argument{"‘#’ (alternative) flag is only allowed for %x, %X and %o"}; } - else if constexpr (conv != 'v') + + if (conv == 'c') { - static_assert(sizeof(T) == 0, "Unsupported conversion for integral type specified"); + noPrecision(spec.precision, "integral types with %c"); } - static_assert(spec.addSign == '\0' || conv == 'i' || conv == 'd', "‘+’ and ‘ ’ (sign related) flags are only allowed for %i and %d"); - static_assert(!spec.alternative || conv == 'o' || conv == 'x' || conv == 'X', "‘#’ (alternative) flag is only allowed for %x, %X and %o"); - - return format(out, t, spec, minWidth, precision); + if (spec.addSign != '\0' && !(std::signed_integral && "idv"_contains(conv))) + { + throw std::invalid_argument{"‘+’ and ‘ ’ (sign related) flags are only allowed for signed integral types and conversions %i, %d and %v"}; + } } }; // NOTE: Difference to printf: uses std::to_chars’ minimal width for exact representation when no precision is specified instead of default precision of 6 template struct formatter { - static constexpr void format(const format_output auto& out, T t, format_specifier spec, optional_int minWidth, optional_int precision) + static void format(const format_output auto& out, T t, format_specifier spec, optional_int minWidth, optional_int precision) { std::chars_format format; - bool uppercase = false; + const auto uppercase = "AEFG"_contains(spec.conversion); switch(spec.conversion) { case 'A': - uppercase = true; - [[fallthrough]]; case 'a': format = std::chars_format::hex; break; case 'E': - uppercase = true; - [[fallthrough]]; case 'e': format = std::chars_format::scientific; break; case 'F': - uppercase = true; - [[fallthrough]]; case 'f': format = std::chars_format::fixed; break; case 'G': - uppercase = true; - [[fallthrough]]; case 'g': case 'v': format = std::chars_format::general; break; default: - throw invalid_argument{std::string{"The given floating point type does not support conversion specifier ‘"} + spec.conversion + std::string{"’"}}; + assert(!"Unsupported conversion; should have been caught by formatter::conversionSupported"); } + char* data, *end; + std::errc ec; + const auto call_to_chars = [format, precision, t, &data, &end, &ec](char* data_, std::size_t size) + { + data = data_; + const auto [end_, ec_] = precision ? std::to_chars(data, data + size, t, format, *precision) : std::to_chars(data, data + size, t, format); + end = end_; + ec = ec_; + }; + std::array::digits10 * 2 + 6)> result; - const auto [end, ec] = precision ? std::to_chars(result.data(), result.data() + result.size(), t, format ,*precision) : std::to_chars(result.data(), result.data() + result.size(), t, format); - assert(ec == std::errc{}); - std::string_view converted{result.data(), end}; + std::unique_ptr largeResult; + call_to_chars(result.data(), result.size()); + if (ec == std::errc::value_too_large && precision) + { + const auto size = result.size() + *precision; + largeResult = std::make_unique_for_overwrite(size); + call_to_chars(largeResult.get(), size); + } + + if (ec != std::errc{}) + { + throw std::runtime_error{"Unexpected error from to_chars: " + std::make_error_condition(ec).message()}; + } + + std::string_view converted{data, end}; const bool hasMinusSign = converted.starts_with('-'); if (hasMinusSign) @@ -337,11 +380,9 @@ namespace format { return; } - template - static constexpr void format(const format_output auto& out, T t, optional_int minWidth, optional_int precision) + static constexpr void conversionSupported(format_specifier spec) { - static_assert(std::string_view{"fFeEaAgGv"}.find(spec.conversion) != std::string_view::npos, "Unsupported conversion specified for floating point type"); - return format(out, t, spec, minWidth, precision); + simpleConversionSpecifierCheck("fFeEaAgGv", spec.conversion, "floating point type"); } }; @@ -365,11 +406,11 @@ namespace format { formatPadded(out, val, ' ', spec.leftJustified, minWidth); } - template - static constexpr void format(const format_output auto& out, const S& s, optional_int minWidth, optional_int precision) + static constexpr void conversionSupported(format_specifier spec) { - static_assert(spec.conversion == 's' || spec.conversion == 'v', "The given stringy type only supports the ‘s’ and ‘v’ conversions"); - return format(out, s, spec, minWidth, precision); + simpleConversionSpecifierCheck("sv", spec.conversion, "stringy type"); + noAlternative(spec.alternative, "stringy types"); + noSignFlags(spec.addSign, "stringy types"); } }; @@ -378,7 +419,10 @@ namespace format { template struct formatter { - static constexpr void format(const format_output auto& out, const std::remove_pointer_t* t, format_specifier spec, optional_int minWidth, optional_int precision) + static constexpr inline format_specifier delegateSpec{.conversion = 'x', .alternative = true}; + using delegated_formatter = formatter; + + static constexpr void format(const format_output auto& out, const std::remove_pointer_t* t, format_specifier spec, optional_int minWidth, optional_int) { if (t == nullptr) { @@ -386,31 +430,30 @@ namespace format { } else { - constexpr format_specifier spec{.conversion = 'x', .alternative = true}; - formatter::template format(out, std::bit_cast(t), minWidth, std::nullopt); + delegated_formatter::format(out, std::bit_cast(t), delegateSpec, minWidth, std::nullopt); } } - template - static constexpr void format(const format_output auto& out, const std::remove_pointer_t* t, optional_int minWidth, optional_int precision) + static constexpr void conversionSupported(format_specifier spec) { - static_assert(spec.conversion == 'p' || spec.conversion == 'v', "The given pointer type only supports the ‘p’ and ‘v’ conversions"); - return format(out, t, spec, minWidth, precision); + simpleConversionSpecifierCheck("pv", spec.conversion, "pointer type"); + noAlternative(spec.alternative, "pointer types"); + noSignFlags(spec.addSign, "pointer types"); + noPrecision(spec.precision, "pointer types"); + delegated_formatter::conversionSupported(delegateSpec); } }; template<> struct formatter { - static constexpr void format(const format_output auto& out, std::nullptr_t, format_specifier spec, optional_int minWidth, optional_int precision) + static constexpr void format(const format_output auto& out, std::nullptr_t, format_specifier spec, optional_int minWidth, optional_int) { formatPadded(out, "(nil)", ' ', spec.leftJustified, minWidth); } - template - static constexpr void format(const format_output auto& out, std::nullptr_t, optional_int minWidth, optional_int precision) + static constexpr void conversionSupported(format_specifier spec) { - static_assert(spec.conversion == 'p' || spec.conversion == 'v', "The given pointer type only supports the ‘p’ and ‘v’ conversions"); - return format(out, nullptr, spec, minWidth, precision); + return formatter::conversionSupported(spec); } }; @@ -437,11 +480,11 @@ namespace format { } } - template - static constexpr void format(const format_output auto& out, const char* s, optional_int minWidth, optional_int precision) + static constexpr void conversionSupported(format_specifier spec) { - static_assert(spec.conversion == 'p' || spec.conversion == 's' || spec.conversion == 'v', "const char* only supports the ‘p’, ‘s’ and ‘v’ conversions"); - return format(out, s, spec, minWidth, precision); + simpleConversionSpecifierCheck("spv", spec.conversion, "char pointer"); + noAlternative(spec.alternative, "char pointer"); + noSignFlags(spec.addSign, "char pointer"); } }; @@ -472,16 +515,14 @@ namespace format { formatPadded(out, stream.view(), ' ', spec.leftJustified, minWidth); } - template - static constexpr void format(const format_output auto& out, const T& t, optional_int minWidth, optional_int precision) + static constexpr void conversionSupported(format_specifier spec) { - static_assert(spec.conversion == 'v' || spec.conversion == 's', "The operator<<(std::ostream&) fallback only supports conversions ‘s’ and ‘v’"); - static_assert(!spec.precision, "The operator<<(std::ostream&) fallback does not support specifying any precision"); - static_assert(spec.addSign == '\0', "The operator<<(std::ostream&) fallback does not support the add sign (‘+‘) flag"); - static_assert(!spec.alternative, "The operator<<(std::ostream&) fallback does not support the alternative (‘#‘) flag"); - static_assert(spec.padding == ' ', "The operator<<(std::ostream&) fallback does not support padding with 0"); - static_assert(spec.length == format_specifier::Length::None, "The operator<<(std::ostream&) fallback does not support specifying any length"); - return format(out, t, spec, minWidth, precision); + constexpr std::string_view fallbackName{"operator<<(std::ostream&) fallback"}; + simpleConversionSpecifierCheck("sv", spec.conversion, fallbackName); + noAlternative(spec.alternative, fallbackName); + noSignFlags(spec.addSign, fallbackName); + noPrecision(spec.precision, fallbackName); + noZeroPadding(spec.padding, fallbackName); } }; } diff --git a/include/cxxformat/runtime.hpp b/include/cxxformat/runtime.hpp new file mode 100644 index 0000000..cc09b7e --- /dev/null +++ b/include/cxxformat/runtime.hpp @@ -0,0 +1,219 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace format { + namespace { + namespace run_time { + class format_template { + std::vector> parts; + std::size_t argCount; + + public: + format_template(std::vector>&& parts, std::size_t argCount) noexcept : parts{std::move(parts)}, argCount{argCount} {} + + template requires (has_some_formatter> && ...) + void operator()(Output&& output, Args&&... args) const + { + checkArgCount(sizeof...(Args)); + + std::array, sizeof...(Args)> indexArgs{[](Arg&& arg) constexpr -> optional_int { + if constexpr (std::unsigned_integral && sizeof(Arg) <= sizeof(std::size_t)) + { + return static_cast(arg); + } + else + { + return std::nullopt; + } + }(std::forward(args))...}; + + const auto checkPart = [&args..., &indexArgs](std::index_sequence) + { + return [&args..., &indexArgs](format_specifier spec) + { + ([spec, &indexArgs](Arg&&, std::size_t index) + { + if (index == spec.argIndex) + { + if (spec.widthAsArg) + { + if (!indexArgs[*spec.minWidth]) + { + throw std::invalid_argument{"Width argument must be an unsigned integral at most as big as std::size_t"}; + } + } + if (spec.precisionAsArg) + { + if (!indexArgs[*spec.precision]) + { + throw std::invalid_argument{"Precision argument must be an unsigned integral at most as big as std::size_t"}; + } + } + formatter_with_fallback>::conversionSupported(spec); + } + }(std::forward(args), indices), ...); + }; + }(std::make_index_sequence()); + + for (const auto& part : parts) + { + if (std::holds_alternative(part)) + { + checkPart(std::get(part)); + } + } + + const auto out = make_output(std::forward(output)); + const overloaded_callable visitor{ + [&args..., &out, &indexArgs](std::index_sequence) + { + return [&args..., &out, &indexArgs](format_specifier spec) + { + ([&out, spec, &indexArgs](Arg&& arg, std::size_t index) + { + if (index == spec.argIndex) + { + optional_int minWidth = spec.minWidth; + if (spec.widthAsArg) + { + minWidth = indexArgs[*minWidth]; + assert(minWidth); + } + optional_int precision = spec.precision; + if (spec.precisionAsArg) + { + precision = indexArgs[*precision]; + assert(precision); + } + return formatter_with_fallback>::format(out, std::forward(arg), spec, minWidth, precision); + } + }(std::forward(args), indices), ...); + }; + }(std::make_index_sequence()), + [&out](std::string text) { out(text); } + }; + for (const auto& part : parts) + { + std::visit(visitor, part); + } + } + + private: + void checkArgCount(std::size_t args) const + { + if (args > argCount) + { + throw std::invalid_argument{"Too many arguments passed!"}; + } + else if (args < argCount) + { + throw std::invalid_argument{"Not enough arguments passed!"}; + } + } + }; + + format_template parseFormat(std::string_view fmt) + { + ArgIndex nextArg{0}; + ArgIndex maxArg{0}; + + std::vector> parts; + + const auto addString = [&parts](std::string_view part) + { + if (!parts.empty() && std::holds_alternative(parts.back())) + { + std::get(parts.back()).append(part); + } + else + { + parts.emplace_back(std::string{part}); + } + }; + + for (std::size_t pos{0}; pos < fmt.size(); ) + { + const auto nextSpec = fmt.find('%', pos); + if (nextSpec == std::string_view::npos) + { + parts.emplace_back(std::string{fmt.substr(pos)}); + break; + } + + if (nextSpec + 1 >= fmt.size()) + { + throw std::invalid_argument{"Incomplete format specifier at end of format string"}; + } + + const auto specStart = fmt[nextSpec + 1]; + if (specStart == '%') + { + // + 1 to include the first % + addString(fmt.substr(pos, nextSpec - pos + 1)); + pos = nextSpec + 2; + } + else + { + const auto [newPos, spec, newNextArg, specMaxArg] = parseSpec(fmt, nextSpec + 1, nextArg); + if (!checkValidConversion(spec.conversion)) + { + throw std::invalid_argument{"Invalid conversion specifier ‘%c’ in format specifier “%s”"_format(spec.conversion, fmt.substr(nextSpec, newPos - nextSpec))}; + } + + addString(fmt.substr(pos, nextSpec - pos)); + + nextArg = newNextArg; + maxArg = std::max(maxArg, specMaxArg); + pos = newPos; + + parts.emplace_back(spec); + } + } + + return {std::move(parts), maxArg}; + } + } + } + + template requires (has_some_formatter> && ...) + void format_to(std::string_view fmt, Output&& output, Args&&... args) + { + run_time::parseFormat(fmt)(std::forward(output), std::forward(args)...); + } + + template requires (has_some_formatter> && ...) + void format_nothrow_to(std::string_view fmt, Output&& output, Args&&... args) noexcept + { + try + { + format_to(fmt, std::forward(output), std::forward(args)...); + } + catch(const std::exception& e) + { + "Formatting error: %s"_format_to(output, e.what()); + } + } + + template requires (has_some_formatter> && ...) + std::string format(std::string_view fmt, Args&&... args) + { + std::ostringstream out; + format_to(fmt, out, std::forward(args)...); + return out.str(); + } + + template requires (has_some_formatter> && ...) + std::string format_nothrow(std::string_view fmt, Args&&... args) + { + std::ostringstream out; + format_nothrow_to(fmt, out, std::forward(args)...); + return out.str(); + } +} -- cgit v1.2.3-54-g00ecf