diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml index 5f2ab55..3cad064 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -25,10 +25,10 @@ jobs: with: submodules: recursive - - name: Install Graphviz + - name: Install Graphviz, gcc-14, g++-14 run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends graphviz + sudo apt-get install -y --no-install-recommends graphviz gcc-14 g++-14 - name: Restore Doxygen cache id: cache-doxygen @@ -49,7 +49,7 @@ jobs: tar -C "${{ github.workspace }}/.cache" -xf "${RUNNER_TEMP}/${archive}" - name: Configure - run: cmake -S ${{github.workspace}} -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=OFF -DFE_BUILD_DOCS=ON -DDOXYGEN_EXECUTABLE=${{ env.DOXYGEN_CACHE_DIR }}/bin/doxygen + run: CC=gcc-14 CXX=g++-14 cmake -S ${{github.workspace}} -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=OFF -DFE_BUILD_DOCS=ON -DDOXYGEN_EXECUTABLE=${{ env.DOXYGEN_CACHE_DIR }}/bin/doxygen - name: Build run: cmake --build ${{github.workspace}}/build -v --target docs diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index a8396d4..74a3027 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -19,13 +19,13 @@ jobs: with: submodules: recursive - - name: Install Valgrind + - name: Install Valgrind, gcc-14, g++-14 run: | sudo apt-get update - sudo apt-get install -y valgrind + sudo apt-get install -y valgrind gcc-14 g++-14 - name: Configure - run: cmake -S ${{github.workspace}} -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{matrix.build-type}} -DBUILD_TESTING=ON + run: CC=gcc-14 CXX=g++-14 cmake -S ${{github.workspace}} -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{matrix.build-type}} -DBUILD_TESTING=ON - name: Build run: cmake --build ${{github.workspace}}/build -v diff --git a/CMakeLists.txt b/CMakeLists.txt index 11af91d..35d7497 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,7 +39,7 @@ target_sources(fe include/fe/utf8.h ) target_include_directories(fe INTERFACE $) -target_compile_features(fe INTERFACE cxx_std_20) +target_compile_features(fe INTERFACE cxx_std_23) if(MSVC) target_compile_definitions(fe INTERFACE _CTYPE_DISABLE_MACROS) diff --git a/docs/README.md b/docs/README.md index c95af54..01f6408 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,12 +10,12 @@ [![Doxygen](https://img.shields.io/github/actions/workflow/status/leissa/fe/doxygen.yml?style=flat-square&logo=gitbook&logoColor=white&label=doxygen&branch=main)](https://github.com/leissa/fe/actions/workflows/doxygen.yml) [![Documentation](https://img.shields.io/badge/docs-main-green?style=flat-square&logo=gitbook&logoColor=white)](https://leissa.github.io/fe) -[![C++20](https://img.shields.io/badge/C%2B%2B-20-blue?style=flat-square&logo=cplusplus)](https://en.wikipedia.org/wiki/C%2B%2B#Standardization) -[![License](https://img.shields.io/github/license/leissa/fe?style=flat-square&color=yellowgreen&logo=opensourceinitiative&logoColor=white)](https://github.com/leissa/fe/blob/main/LICENSE.TXT) +[![C++23](https://img.shields.io/badge/C%2B%2B-23-blue?style=flat-square&logo=cplusplus)](https://en.wikipedia.org/wiki/C%2B%2B#Standardization) +[![License: MIT](https://img.shields.io/github/license/leissa/fe?style=flat-square&color=yellowgreen&logo=opensourceinitiative&logoColor=white)](https://github.com/leissa/fe/blob/main/LICENSE.TXT) [TOC] -**FE** is a header-only C++20 toolkit for building handwritten compiler and interpreter frontends. +**FE** is a header-only C++23 toolkit for building handwritten compiler and interpreter frontends. Rather than generating lexers or parsers for you, FE focuses on the infrastructure that every frontend needs anyway: source locations, diagnostics, interning, parsing support, and efficient memory management. The goal is simple: keep handwritten frontends lightweight, explicit, and pleasant to maintain. diff --git a/include/fe/enum.h b/include/fe/enum.h index 47dd414..8ff899a 100644 --- a/include/fe/enum.h +++ b/include/fe/enum.h @@ -1,62 +1,42 @@ #pragma once -#include - #include namespace fe { -/// @name is_enum -///@{ -template -struct is_bit_enum : std::false_type {}; -template -inline constexpr bool is_bit_enum_v = is_bit_enum::value; -template -concept BitEnum = std::is_enum_v && is_bit_enum_v; -///@} - /// @name Bit operations for enum classs /// Provides all kind of bit and comparison operators for an `enum class` @p E. -/// Note that the bit operators return @p E's underlying type and not the original `enum` @p E. -/// This is because the result may not be a valid `enum` value. -/// For the same reason, it doesn't make sense to declare operators such as `&=`. /// Use like this: /// ``` -/// using fe::operator&; -/// using fe::operator|; -/// using fe::operator^; -/// using fe::operator<=>; -/// using fe::operator==; -/// using fe::operator!=; -/// /// enum class MyEnum : unsigned { /// A = 1 << 0, /// B = 1 << 1, /// C = 1 << 2, /// }; /// -/// template<> struct fe::is_bit_enum : std::true_type { }; +/// template<> struct fe::is_bit_enum : std::true_type {}; /// ``` -///@{ -// clang-format off -template constexpr auto operator&(E x, E y) { return std::underlying_type_t(x) & std::underlying_type_t(y); } -template constexpr auto operator&(std::underlying_type_t x, E y) { return x & std::underlying_type_t(y); } -template constexpr auto operator&( E x, std::underlying_type_t y) { return std::underlying_type_t(x) & y ; } -template constexpr auto operator|( E x, E y) { return std::underlying_type_t(x) | std::underlying_type_t(y); } -template constexpr auto operator|(std::underlying_type_t x, E y) { return x | std::underlying_type_t(y); } -template constexpr auto operator|( E x, std::underlying_type_t y) { return std::underlying_type_t(x) | y ; } -template constexpr auto operator^( E x, E y) { return std::underlying_type_t(x) ^ std::underlying_type_t(y); } -template constexpr auto operator^(std::underlying_type_t x, E y) { return x ^ std::underlying_type_t(y); } -template constexpr auto operator^( E x, std::underlying_type_t y) { return std::underlying_type_t(x) ^ y ; } -template constexpr std::strong_ordering operator<=>(std::underlying_type_t x, E y) { return x <=> std::underlying_type_t(y); } -template constexpr std::strong_ordering operator<=>(E x, std::underlying_type_t y) { return std::underlying_type_t(x) <=> y; } -template constexpr bool operator==(std::underlying_type_t x, E y) { return x == std::underlying_type_t(y); } -template constexpr bool operator!=(std::underlying_type_t x, E y) { return x != std::underlying_type_t(y); } -template constexpr bool operator==(E x, std::underlying_type_t y) { return std::underlying_type_t(x) == y; } -template constexpr bool operator!=(E x, std::underlying_type_t y) { return std::underlying_type_t(x) != y; } -// clang-format on +template +struct is_bit_enum : std::false_type {}; -///@} +template +concept BitEnum = std::is_enum_v && is_bit_enum::value; +template constexpr auto to_underlying(E e) noexcept { return static_cast>(e); } + +} // namespace fe + +// clang-format off +template constexpr E operator|(E a, E b) noexcept { return static_cast(fe::to_underlying(a) | fe::to_underlying(b)); } +template constexpr E operator&(E a, E b) noexcept { return static_cast(fe::to_underlying(a) & fe::to_underlying(b)); } +template constexpr E operator^(E a, E b) noexcept { return static_cast(fe::to_underlying(a) ^ fe::to_underlying(b)); } +template constexpr E operator~(E a) noexcept { return static_cast(~fe::to_underlying(a)); } +template constexpr E& operator|=(E& a, E b) noexcept { return a = (a | b); } +template constexpr E& operator&=(E& a, E b) noexcept { return a = (a & b); } +template constexpr E& operator^=(E& a, E b) noexcept { return a = (a ^ b); } + +namespace fe { +template constexpr bool has_flag(E value, E flag) noexcept { return (value & flag) == flag; } } // namespace fe + +// clang-format on diff --git a/include/fe/format.h b/include/fe/format.h index 4d5b2c9..f65e3fe 100644 --- a/include/fe/format.h +++ b/include/fe/format.h @@ -1,12 +1,50 @@ #pragma once +#include + #include +#include +#include +#include +#include +#include +#include +#include +#include #include "fe/loc.h" #include "fe/utf8.h" namespace fe { +/// Wrap a callable `f(std::ostream&) -> std::ostream&` so it streams via `operator<<` and `std::format`. +/// Useful for inline ad-hoc formatting: +/// ``` +/// auto greet = fe::StreamFn{[](std::ostream& os) { os << "hi"; }}; +/// std::cout << greet; +/// std::format("{}", greet); +/// ``` +template +requires std::invocable +class StreamFn { +public: + constexpr StreamFn(F f) + : f_(std::move(f)) {} + + friend std::ostream& operator<<(std::ostream& os, StreamFn const& s) { + if constexpr (std::same_as, std::ostream&>) + return std::invoke(s.f_, os); + else + return std::invoke(s.f_, os), os; + } + +private: + F f_; +}; + +template +StreamFn(F) -> StreamFn; + /// Make types that support ostream operators available for `std::format`. /// Use like this: /// ``` @@ -15,90 +53,129 @@ namespace fe { /// @sa [Stack Overflow](https://stackoverflow.com/a/75738462). template struct basic_ostream_formatter : std::formatter, Char> { - template - O format(const T& value, std::basic_format_context& ctx) const { + template + auto format(T const& value, FormatContext& ctx) const { std::basic_stringstream ss; ss << value; -#if defined(_LIBCPP_VERSION) && _LIBCPP_VERSION < 170000 - return std::formatter, Char>::format(ss.str(), ctx); -#else return std::formatter, Char>::format(ss.view(), ctx); -#endif } }; using ostream_formatter = basic_ostream_formatter; -/// @name out/outln/err/errln -/// Print to `std::cout`/`std::cerr` via `std::format`; the `*ln` variants conclude with `std::endl`. -///@{ -// clang-format off -template void err (std::format_string fmt, Args&&... args) { std::cerr << std::format(fmt, std::forward(args)...); } -template void out (std::format_string fmt, Args&&... args) { std::cout << std::format(fmt, std::forward(args)...); } -template void errln(std::format_string fmt, Args&&... args) { std::cerr << std::format(fmt, std::forward(args)...) << std::endl; } -template void outln(std::format_string fmt, Args&&... args) { std::cout << std::format(fmt, std::forward(args)...) << std::endl; } -// clang-format on - /// Keeps track of indentation level during output class Tab { public: + /// @name Construction + ///@{ Tab(const Tab&) = default; - Tab(std::string_view tab = {"\t"}, size_t indent = 0) + Tab(std::string_view tab = {"\t"}, int indent = 0) : tab_(tab) - , indent_(indent) {} + , indent_(indent) { + assert(indent >= 0); + } - /// @name Getters - ///@{ - constexpr size_t indent() const noexcept { return indent_; } - constexpr std::string_view tab() const noexcept { return tab_; } + static Tab spaces() { return Tab(std::string_view(" ")); } ///@} - /// @name Setters + /// @name Getters ///@{ - Tab& operator=(size_t indent) { - indent_ = indent; - return *this; - } - Tab& operator=(std::string tab) { - tab_ = tab; - return *this; - } + constexpr int indent() const noexcept { return indent_; } + constexpr std::string_view tab() const noexcept { return tab_; } ///@} // clang-format off /// @name Creates a new Tab ///@{ - [[nodiscard]] constexpr Tab operator++(int) noexcept { return {tab_, indent_++}; } - [[nodiscard]] constexpr Tab operator--(int) noexcept { assert(indent_ > 0); return {tab_, indent_--}; } - [[nodiscard]] constexpr Tab operator+(size_t indent) const noexcept { return {tab_, indent_ + indent}; } - [[nodiscard]] constexpr Tab operator-(size_t indent) const noexcept { assert(indent_ > 0); return {tab_, indent_ - indent}; } + /// + [[nodiscard]] Tab operator+(int indent) const noexcept { assert(indent >= 0); return {tab_, indent_ + indent}; } + [[nodiscard]] Tab operator-(int indent) const noexcept { assert(indent >= 0 && indent_ >= indent); return {tab_, indent_ - indent}; } ///@} /// @name Modifies this Tab ///@{ constexpr Tab& operator++() noexcept { ++indent_; return *this; } constexpr Tab& operator--() noexcept { assert(indent_ > 0); --indent_; return *this; } - constexpr Tab& operator+=(size_t indent) noexcept { indent_ += indent; return *this; } - constexpr Tab& operator-=(size_t indent) noexcept { assert(indent_ > 0); indent_ -= indent; return *this; } - constexpr Tab& operator=(size_t indent) noexcept { indent_ = indent; return *this; } - constexpr Tab& operator=(std::string_view tab) noexcept { tab_ = tab; return *this; } + constexpr Tab& operator+=(int indent) noexcept { assert(indent >= 0); indent_ += indent; return *this; } + constexpr Tab& operator-=(int indent) noexcept { assert(indent >= 0 && indent_ >= indent); indent_ -= indent; return *this; } ///@} // clang-format on friend std::ostream& operator<<(std::ostream& os, Tab tab) { - for (size_t i = 0; i != tab.indent_; ++i) + for (int i = 0; i != tab.indent_; ++i) os << tab.tab_; return os; } private: std::string_view tab_; - size_t indent_ = 0; + int indent_ = 0; }; +template +concept Formattable + = requires(std::basic_format_context>, CharT>& ctx, T const& v) { + std::formatter, CharT>{}.format(v, ctx); + }; + +/// Join elements of @p range with @p sep. +/// Use as a `std::format` or `operator<<` argument: `std::format("{}", fe::Join(v, ", "))`. +template +requires Formattable>>> +class Join { +public: + using View = std::views::all_t; + + Join(R&& range, std::string_view sep = ", ") + : range_(std::views::all(std::forward(range))) + , sep_(sep) {} + + const auto& range() const { return range_; } + std::string_view sep() const { return sep_; } + + friend std::ostream& operator<<(std::ostream& os, const Join& j) { + for (std::string_view sep{}; const auto& elem : j.range_) { + os << sep << elem; + sep = j.sep_; + } + return os; + } + +private: + View range_; + std::string_view sep_; +}; + +template +Join(R&&, std::string_view = ", ") -> Join; + } // namespace fe #ifndef DOXYGEN +template +struct std::formatter> : fe::ostream_formatter {}; + +template +struct std::formatter> { + using elem_t = std::remove_cvref_t>>; + std::formatter elem_fmt; + + constexpr auto parse(std::format_parse_context& ctx) { return elem_fmt.parse(ctx); } + + template + auto format(const fe::Join& j, FormatContext& ctx) const { + auto out = ctx.out(); + for (std::string_view sep = {}; const auto& elem : j.range()) { + out = std::ranges::copy(sep, out).out; + ctx.advance_to(out); + out = elem_fmt.format(elem, ctx); + ctx.advance_to(out); + sep = j.sep(); + } + return out; + } +}; + // clang-format off template<> struct std::formatter : fe::ostream_formatter {}; template<> struct std::formatter : fe::ostream_formatter {}; @@ -107,3 +184,20 @@ template<> struct std::formatter : fe::ostream_formatter {}; template<> struct std::formatter : fe::ostream_formatter {}; // clang-format on #endif + +#ifdef NDEBUG +# define assertf(condition, ...) \ + do { \ + (void)sizeof(condition); \ + __VA_OPT__((void)sizeof((__VA_ARGS__));) \ + } while (false) +#else +# define assertf(condition, ...) \ + do { \ + if (!(condition)) { \ + std::print(std::cerr, "{}:{}: assertion `{}` failed", __FILE__, __LINE__, #condition); \ + std::println(std::cerr __VA_OPT__(, ": " __VA_ARGS__)); \ + fe::breakpoint(); \ + } \ + } while (false) +#endif diff --git a/tests/test.cpp b/tests/test.cpp index 06172ab..3bbd115 100644 --- a/tests/test.cpp +++ b/tests/test.cpp @@ -208,15 +208,12 @@ enum class MyEnum : unsigned { template<> struct fe::is_bit_enum : std::true_type {}; -using fe::operator&; -using fe::operator|; -using fe::operator^; TEST_CASE("enum") { - static_assert((MyEnum::A & MyEnum::A) == 1); - static_assert((MyEnum::A & MyEnum::B) == 0); - static_assert((MyEnum::A | MyEnum::B) == 3); - static_assert((MyEnum::A ^ MyEnum::A) == 0); + static_assert(fe::to_underlying(MyEnum::A & MyEnum::A) == 1); + static_assert(fe::to_underlying(MyEnum::A & MyEnum::B) == 0); + static_assert(fe::to_underlying(MyEnum::A | MyEnum::B) == 3); + static_assert(fe::to_underlying(MyEnum::A ^ MyEnum::A) == 0); } TEST_CASE("term") { @@ -246,3 +243,14 @@ TEST_CASE("term") { CHECK(oss.str() == "x"); } } + +TEST_CASE("format") { + std::vector v0; + std::vector v1 = {23}; + std::vector v2 = {23, 42}; + std::vector v3 = {23, 42, 17}; + CHECK(std::format("{}", fe::Join(v0)) == ""); + CHECK(std::format("{}", fe::Join(v1)) == "23"); + CHECK(std::format("{}", fe::Join(v2)) == "23, 42"); + CHECK(std::format("{}", fe::Join(v3)) == "23, 42, 17"); +}