From ed0e41c49fadf497871386765e95204fbfe513eb Mon Sep 17 00:00:00 2001 From: Kaleb Barrett Date: Mon, 8 Jun 2026 09:21:21 -0400 Subject: [PATCH 1/3] Add fixed-width Unsigned and DynUnsigned integer types --- cpp/include/coconext/types.hpp | 4 +- cpp/include/coconext/types/int_common.hpp | 116 +++++ cpp/include/coconext/types/unsigned.hpp | 556 ++++++++++++++++++++++ tests/cpp/CMakeLists.txt | 4 +- tests/cpp/test_unsigned.cpp | 323 +++++++++++++ 5 files changed, 1001 insertions(+), 2 deletions(-) create mode 100644 cpp/include/coconext/types/int_common.hpp create mode 100644 cpp/include/coconext/types/unsigned.hpp create mode 100644 tests/cpp/test_unsigned.cpp diff --git a/cpp/include/coconext/types.hpp b/cpp/include/coconext/types.hpp index 297802b..527ca08 100644 --- a/cpp/include/coconext/types.hpp +++ b/cpp/include/coconext/types.hpp @@ -7,10 +7,12 @@ #include "./types/concepts.hpp" #include "./types/direction.hpp" #include "./types/int.hpp" +#include "./types/vector.hpp" +#include "./types/int_common.hpp" #include "./types/logic.hpp" #include "./types/logic_array.hpp" #include "./types/range.hpp" -#include "./types/vector.hpp" +#include "./types/unsigned.hpp" // NOLINTEND(unused-includes) #endif // COCONEXT_TYPES_HPP diff --git a/cpp/include/coconext/types/int_common.hpp b/cpp/include/coconext/types/int_common.hpp new file mode 100644 index 0000000..96c50e3 --- /dev/null +++ b/cpp/include/coconext/types/int_common.hpp @@ -0,0 +1,116 @@ +#ifndef COCONEXT_INT_COMMON_HPP +#define COCONEXT_INT_COMMON_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace coconext::types::detail { + +// Storage for Unsigned/Signed is a single native 64-bit word, so widths are +// limited to 1..64 for now. Wider types will need a different storage strategy. +constexpr unsigned int_max_width = std::numeric_limits::digits; + +constexpr void check_width(unsigned w) { + if (w < 1 || w > int_max_width) { + throw std::out_of_range( + "integer width " + std::to_string(w) + " out of range (1.." + + std::to_string(int_max_width) + ")" + ); + } +} + +// Mask with the low `w` bits set. w must be in 1..64 (see check_width). The +// w==64 case is special-cased because `1ull << 64` is undefined behavior. +constexpr uint64_t uint_mask(unsigned w) noexcept { + return w >= int_max_width ? ~uint64_t{0} : ((uint64_t{1} << w) - 1); +} + +// Reduce an unsigned value to its low `w` bits (two's-complement wrap for the +// Unsigned interpretation). Collapses to identity at w==64. +constexpr uint64_t uint_wrap(uint64_t v, unsigned w) noexcept { return v & uint_mask(w); } + +// Interpret the low `w` bits of `bits` as a two's-complement signed value and +// sign-extend into a full int64_t. Collapses to a plain reinterpret at w==64. +constexpr int64_t sint_wrap(uint64_t bits, unsigned w) noexcept { + auto const masked = uint_wrap(bits, w); + auto const sign_bit = uint64_t{1} << (w - 1); + // If the sign bit is set, set all bits above w-1. + return static_cast((masked ^ sign_bit) - sign_bit); +} + +// Build a {n-1 DOWNTO 0} Range from a length, the HDL convention for numeric +// types. Used by Unsigned/Signed/DynUnsigned/DynSigned constructors that take +// just a width. +constexpr Range int_downto_range(size_t n) { + return Range{static_cast(n) - 1, Direction::DOWNTO, 0}; +} + +// Range NTTP dispatcher for the Unsigned<...>/Signed<...> template aliases. +// Same shape as the logic_array `make_logic_static_range`: defaults to DOWNTO +// when the user didn't pick a direction explicitly. +// `Unsigned<8>` -> {7 DOWNTO 0} +// `Unsigned` -> R (passthrough) +// `Unsigned<7, 0>` -> {7 DOWNTO 0} (auto) +// `Unsigned<3, 3>` -> {3 DOWNTO 3} (default DOWNTO when L == R) +// `Unsigned<0, 7>` -> {0 TO 7} (auto) +// `Unsigned` -> {L D R} (explicit) +template +constexpr Range make_int_range() { + static_assert( + sizeof...(Args) >= 1 && sizeof...(Args) <= 3, + "Unsigned/Signed takes 1 to 3 range args" + ); + constexpr auto t = std::tuple{Args...}; + if constexpr (sizeof...(Args) == 1) { + using First = std::remove_cvref_t(t))>; + if constexpr (std::is_same_v) { + return std::get<0>(t); + } else { + static_assert( + std::integral, + "single template arg must be a Range value or an integral length" + ); + static_assert(std::get<0>(t) >= 0, "length must be non-negative"); + return int_downto_range(static_cast(std::get<0>(t))); + } + } else if constexpr (sizeof...(Args) == 2) { + constexpr Range r{ + static_cast(std::get<0>(t)), + static_cast(std::get<1>(t)) + }; + if constexpr (r.left == r.right) { + return Range{r.left, Direction::DOWNTO, r.right}; + } else { + return r; + } + } else { // 3 + static_assert( + std::is_same_v(t))>, Direction>, + "three-arg form requires (left, Direction, right)" + ); + return Range{ + static_cast(std::get<0>(t)), + std::get<1>(t), + static_cast(std::get<2>(t)) + }; + } +} + +// Result range for a binary op between two fixed-width numeric types: width +// max(width(a), width(b)), normalized to {N-1 DOWNTO 0} (VHDL numeric_std +// convention). +template +inline constexpr Range int_result_range = + int_downto_range(std::max(A.length(), B.length())); + +} // namespace coconext::types::detail + +#endif // COCONEXT_INT_COMMON_HPP diff --git a/cpp/include/coconext/types/unsigned.hpp b/cpp/include/coconext/types/unsigned.hpp new file mode 100644 index 0000000..5380654 --- /dev/null +++ b/cpp/include/coconext/types/unsigned.hpp @@ -0,0 +1,556 @@ +#ifndef COCONEXT_UNSIGNED_HPP +#define COCONEXT_UNSIGNED_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace coconext::types { + +namespace detail { + +template +class Unsigned; + +// Wrapping factory: build an Unsigned from raw bits, reducing modulo +// 2^R.length(). Used by arithmetic operators (which wrap rather than throw). +template +constexpr Unsigned make_unsigned(uint64_t bits) noexcept; + +// Fixed-width unsigned integer with two's-complement wrap-on-overflow. The +// indexing range R carries HDL coordinates; only its length (in bits) matters +// for arithmetic. Backed by a single uint64_t, so length is limited to 1..64. +// The stored value is always kept masked to the low N bits. +template +class Unsigned { + static_assert( + R.length() >= 1 && R.length() <= int_max_width, "Unsigned width must be 1..64" + ); + + public: + using storage_type = uint64_t; + + static constexpr Range range() noexcept { return R; } + static constexpr size_t width() noexcept { return R.length(); } + + constexpr Unsigned() noexcept = default; + + // Construct from a native integer. Throws std::out_of_range if the value is + // negative or does not fit in N bits. + template + explicit constexpr Unsigned(T v) { + if constexpr (std::is_signed_v) { + if (v < 0) { + throw std::out_of_range("negative value in Unsigned construction"); + } + } + auto const u = static_cast(v); + if (u > uint_mask(width())) { + throw std::out_of_range("value does not fit in Unsigned width"); + } + value_ = u; + } + + // Cross-width conversion. Throws if the source value doesn't fit in N bits. + template + explicit constexpr Unsigned(Unsigned other) { + if (other.value() > uint_mask(width())) { + throw std::out_of_range("value does not fit in Unsigned width"); + } + value_ = other.value(); + } + + constexpr uint64_t value() const noexcept { return value_; } + + // Convert to a native integer. Throws std::out_of_range if the value + // exceeds the target type's range. + template + constexpr T to() const { + if (value_ > static_cast(std::numeric_limits::max())) { + throw std::out_of_range("Unsigned value does not fit in target type"); + } + return static_cast(value_); + } + + // -- unary ---------------------------------------------------------------- + constexpr Unsigned operator+() const noexcept { return *this; } + constexpr Unsigned operator-() const noexcept { return make_unsigned(~value_ + 1); } + constexpr Unsigned operator~() const noexcept { return make_unsigned(~value_); } + + constexpr Unsigned& operator++() noexcept { + value_ = uint_wrap(value_ + 1, width()); + return *this; + } + constexpr Unsigned operator++(int) noexcept { + auto const old = *this; + ++*this; + return old; + } + constexpr Unsigned& operator--() noexcept { + value_ = uint_wrap(value_ - 1, width()); + return *this; + } + constexpr Unsigned operator--(int) noexcept { + auto const old = *this; + --*this; + return old; + } + + // -- shifts (amount is a native integer) ---------------------------------- + constexpr Unsigned operator<<(int amount) const { + if (amount < 0) { + throw std::invalid_argument("negative shift amount"); + } + if (amount >= static_cast(int_max_width)) { + return make_unsigned(0); + } + return make_unsigned(value_ << amount); + } + constexpr Unsigned operator>>(int amount) const { + if (amount < 0) { + throw std::invalid_argument("negative shift amount"); + } + if (amount >= static_cast(int_max_width)) { + return make_unsigned(0); + } + return make_unsigned(value_ >> amount); + } + + // -- compound assignment (result wrapped to this width) ------------------- + template + constexpr Unsigned& operator+=(Unsigned rhs) noexcept { + value_ = uint_wrap(value_ + rhs.value(), width()); + return *this; + } + template + constexpr Unsigned& operator-=(Unsigned rhs) noexcept { + value_ = uint_wrap(value_ - rhs.value(), width()); + return *this; + } + template + constexpr Unsigned& operator*=(Unsigned rhs) noexcept { + value_ = uint_wrap(value_ * rhs.value(), width()); + return *this; + } + template + constexpr Unsigned& operator/=(Unsigned rhs) { + if (rhs.value() == 0) { + throw std::domain_error("division by zero"); + } + value_ = uint_wrap(value_ / rhs.value(), width()); + return *this; + } + template + constexpr Unsigned& operator%=(Unsigned rhs) { + if (rhs.value() == 0) { + throw std::domain_error("modulo by zero"); + } + value_ = uint_wrap(value_ % rhs.value(), width()); + return *this; + } + template + constexpr Unsigned& operator&=(Unsigned rhs) noexcept { + value_ = uint_wrap(value_ & rhs.value(), width()); + return *this; + } + template + constexpr Unsigned& operator|=(Unsigned rhs) noexcept { + value_ = uint_wrap(value_ | rhs.value(), width()); + return *this; + } + template + constexpr Unsigned& operator^=(Unsigned rhs) noexcept { + value_ = uint_wrap(value_ ^ rhs.value(), width()); + return *this; + } + constexpr Unsigned& operator<<=(int amount) { + *this = *this << amount; + return *this; + } + constexpr Unsigned& operator>>=(int amount) { + *this = *this >> amount; + return *this; + } + + private: + struct raw_tag {}; + constexpr Unsigned(raw_tag, uint64_t bits) noexcept + : value_(uint_wrap(bits, width())) {} + + uint64_t value_ = 0; + + friend constexpr Unsigned detail::make_unsigned(uint64_t bits) noexcept; +}; + +template +constexpr Unsigned make_unsigned(uint64_t bits) noexcept { + return Unsigned(typename Unsigned::raw_tag{}, bits); +} + +template +inline constexpr size_t max_width = A > B ? A : B; + +} // namespace detail + +// User-facing alias: accepts the same NTTP forms as Array, with HDL +// DOWNTO defaulting (see detail::make_int_range for the rules). +template +using Unsigned = detail::Unsigned()>; + +namespace detail { + +// -- binary arithmetic: result range is {max(width)-1 DOWNTO 0} -------------- +// Defined inside detail:: so ADL finds them from detail::Unsigned arguments +// (ADL looks in the namespace of the argument type, not enclosing namespaces). + +template +constexpr Unsigned> operator+( + Unsigned a, Unsigned b +) noexcept { + return make_unsigned>(a.value() + b.value()); +} +template +constexpr Unsigned> operator-( + Unsigned a, Unsigned b +) noexcept { + return make_unsigned>(a.value() - b.value()); +} +template +constexpr Unsigned> operator*( + Unsigned a, Unsigned b +) noexcept { + return make_unsigned>(a.value() * b.value()); +} +template +constexpr Unsigned> operator/(Unsigned a, Unsigned b) { + if (b.value() == 0) { + throw std::domain_error("division by zero"); + } + return make_unsigned>(a.value() / b.value()); +} +template +constexpr Unsigned> operator%(Unsigned a, Unsigned b) { + if (b.value() == 0) { + throw std::domain_error("modulo by zero"); + } + return make_unsigned>(a.value() % b.value()); +} + +template +constexpr Unsigned> operator&( + Unsigned a, Unsigned b +) noexcept { + return make_unsigned>(a.value() & b.value()); +} +template +constexpr Unsigned> operator|( + Unsigned a, Unsigned b +) noexcept { + return make_unsigned>(a.value() | b.value()); +} +template +constexpr Unsigned> operator^( + Unsigned a, Unsigned b +) noexcept { + return make_unsigned>(a.value() ^ b.value()); +} + +template +constexpr bool operator==(Unsigned a, Unsigned b) noexcept { + return a.value() == b.value(); +} +template +constexpr bool operator!=(Unsigned a, Unsigned b) noexcept { + return a.value() != b.value(); +} +template +constexpr bool operator<(Unsigned a, Unsigned b) noexcept { + return a.value() < b.value(); +} +template +constexpr bool operator<=(Unsigned a, Unsigned b) noexcept { + return a.value() <= b.value(); +} +template +constexpr bool operator>(Unsigned a, Unsigned b) noexcept { + return a.value() > b.value(); +} +template +constexpr bool operator>=(Unsigned a, Unsigned b) noexcept { + return a.value() >= b.value(); +} + +} // namespace detail + +// -- DynUnsigned: runtime-range counterpart --------------------------------- +// +// Same uint64_t storage and modular semantics as Unsigned, but the indexing +// Range is a runtime value. Binary ops produce a result of range {N-1 DOWNTO +// 0} where N = max(width(a), width(b)). Mixing a DynUnsigned with a static +// Unsigned is out of scope for now -- convert explicitly via value()/to<>() +// at the boundary. + +class DynUnsigned { + public: + using storage_type = uint64_t; + + constexpr Range const& range() const noexcept { return range_; } + constexpr size_t width() const noexcept { return range_.length(); } + + // Construct from a value plus an explicit Range. + template + constexpr DynUnsigned(T v, Range range) : range_(range) { + detail::check_width(static_cast(range.length())); + if constexpr (std::is_signed_v) { + if (v < 0) { + throw std::out_of_range("negative value in DynUnsigned construction"); + } + } + auto const u = static_cast(v); + if (u > detail::uint_mask(width())) { + throw std::out_of_range("value does not fit in DynUnsigned width"); + } + value_ = u; + } + + // Length-only sugar: produces a {length-1 DOWNTO 0} range (HDL convention). + template + constexpr DynUnsigned(T v, unsigned length) + : DynUnsigned(v, detail::int_downto_range(length)) {} + + constexpr uint64_t value() const noexcept { return value_; } + + template + constexpr T to() const { + if (value_ > static_cast(std::numeric_limits::max())) { + throw std::out_of_range("DynUnsigned value does not fit in target type"); + } + return static_cast(value_); + } + + constexpr DynUnsigned operator+() const noexcept { return *this; } + constexpr DynUnsigned operator-() const noexcept { + return DynUnsigned(raw_tag{}, ~value_ + 1, range_); + } + constexpr DynUnsigned operator~() const noexcept { + return DynUnsigned(raw_tag{}, ~value_, range_); + } + + constexpr DynUnsigned& operator++() noexcept { + value_ = detail::uint_wrap(value_ + 1, width()); + return *this; + } + constexpr DynUnsigned operator++(int) noexcept { + auto const old = *this; + ++*this; + return old; + } + constexpr DynUnsigned& operator--() noexcept { + value_ = detail::uint_wrap(value_ - 1, width()); + return *this; + } + constexpr DynUnsigned operator--(int) noexcept { + auto const old = *this; + --*this; + return old; + } + + constexpr DynUnsigned operator<<(int amount) const { + if (amount < 0) { + throw std::invalid_argument("negative shift amount"); + } + if (amount >= static_cast(detail::int_max_width)) { + return DynUnsigned(raw_tag{}, 0, range_); + } + return DynUnsigned(raw_tag{}, value_ << amount, range_); + } + constexpr DynUnsigned operator>>(int amount) const { + if (amount < 0) { + throw std::invalid_argument("negative shift amount"); + } + if (amount >= static_cast(detail::int_max_width)) { + return DynUnsigned(raw_tag{}, 0, range_); + } + return DynUnsigned(raw_tag{}, value_ >> amount, range_); + } + + constexpr DynUnsigned& operator+=(DynUnsigned rhs) noexcept { + value_ = detail::uint_wrap(value_ + rhs.value_, width()); + return *this; + } + constexpr DynUnsigned& operator-=(DynUnsigned rhs) noexcept { + value_ = detail::uint_wrap(value_ - rhs.value_, width()); + return *this; + } + constexpr DynUnsigned& operator*=(DynUnsigned rhs) noexcept { + value_ = detail::uint_wrap(value_ * rhs.value_, width()); + return *this; + } + constexpr DynUnsigned& operator/=(DynUnsigned rhs) { + if (rhs.value_ == 0) { + throw std::domain_error("division by zero"); + } + value_ = detail::uint_wrap(value_ / rhs.value_, width()); + return *this; + } + constexpr DynUnsigned& operator%=(DynUnsigned rhs) { + if (rhs.value_ == 0) { + throw std::domain_error("modulo by zero"); + } + value_ = detail::uint_wrap(value_ % rhs.value_, width()); + return *this; + } + constexpr DynUnsigned& operator&=(DynUnsigned rhs) noexcept { + value_ = detail::uint_wrap(value_ & rhs.value_, width()); + return *this; + } + constexpr DynUnsigned& operator|=(DynUnsigned rhs) noexcept { + value_ = detail::uint_wrap(value_ | rhs.value_, width()); + return *this; + } + constexpr DynUnsigned& operator^=(DynUnsigned rhs) noexcept { + value_ = detail::uint_wrap(value_ ^ rhs.value_, width()); + return *this; + } + constexpr DynUnsigned& operator<<=(int amount) { + *this = *this << amount; + return *this; + } + constexpr DynUnsigned& operator>>=(int amount) { + *this = *this >> amount; + return *this; + } + + private: + struct raw_tag {}; + constexpr DynUnsigned(raw_tag, uint64_t bits, Range range) noexcept + : value_(detail::uint_wrap(bits, static_cast(range.length()))), + range_(range) {} + + uint64_t value_ = 0; + Range range_ = detail::int_downto_range(1); + + friend constexpr DynUnsigned operator+(DynUnsigned, DynUnsigned) noexcept; + friend constexpr DynUnsigned operator-(DynUnsigned, DynUnsigned) noexcept; + friend constexpr DynUnsigned operator*(DynUnsigned, DynUnsigned) noexcept; + friend constexpr DynUnsigned operator/(DynUnsigned, DynUnsigned); + friend constexpr DynUnsigned operator%(DynUnsigned, DynUnsigned); + friend constexpr DynUnsigned operator&(DynUnsigned, DynUnsigned) noexcept; + friend constexpr DynUnsigned operator|(DynUnsigned, DynUnsigned) noexcept; + friend constexpr DynUnsigned operator^(DynUnsigned, DynUnsigned) noexcept; +}; + +inline constexpr DynUnsigned operator+(DynUnsigned a, DynUnsigned b) noexcept { + auto const r = detail::int_downto_range(std::max(a.width(), b.width())); + return DynUnsigned(DynUnsigned::raw_tag{}, a.value_ + b.value_, r); +} +inline constexpr DynUnsigned operator-(DynUnsigned a, DynUnsigned b) noexcept { + auto const r = detail::int_downto_range(std::max(a.width(), b.width())); + return DynUnsigned(DynUnsigned::raw_tag{}, a.value_ - b.value_, r); +} +inline constexpr DynUnsigned operator*(DynUnsigned a, DynUnsigned b) noexcept { + auto const r = detail::int_downto_range(std::max(a.width(), b.width())); + return DynUnsigned(DynUnsigned::raw_tag{}, a.value_ * b.value_, r); +} +inline constexpr DynUnsigned operator/(DynUnsigned a, DynUnsigned b) { + if (b.value_ == 0) { + throw std::domain_error("division by zero"); + } + auto const r = detail::int_downto_range(std::max(a.width(), b.width())); + return DynUnsigned(DynUnsigned::raw_tag{}, a.value_ / b.value_, r); +} +inline constexpr DynUnsigned operator%(DynUnsigned a, DynUnsigned b) { + if (b.value_ == 0) { + throw std::domain_error("modulo by zero"); + } + auto const r = detail::int_downto_range(std::max(a.width(), b.width())); + return DynUnsigned(DynUnsigned::raw_tag{}, a.value_ % b.value_, r); +} +inline constexpr DynUnsigned operator&(DynUnsigned a, DynUnsigned b) noexcept { + auto const r = detail::int_downto_range(std::max(a.width(), b.width())); + return DynUnsigned(DynUnsigned::raw_tag{}, a.value_ & b.value_, r); +} +inline constexpr DynUnsigned operator|(DynUnsigned a, DynUnsigned b) noexcept { + auto const r = detail::int_downto_range(std::max(a.width(), b.width())); + return DynUnsigned(DynUnsigned::raw_tag{}, a.value_ | b.value_, r); +} +inline constexpr DynUnsigned operator^(DynUnsigned a, DynUnsigned b) noexcept { + auto const r = detail::int_downto_range(std::max(a.width(), b.width())); + return DynUnsigned(DynUnsigned::raw_tag{}, a.value_ ^ b.value_, r); +} + +inline constexpr bool operator==(DynUnsigned a, DynUnsigned b) noexcept { + return a.value() == b.value(); +} +inline constexpr bool operator!=(DynUnsigned a, DynUnsigned b) noexcept { + return a.value() != b.value(); +} +inline constexpr bool operator<(DynUnsigned a, DynUnsigned b) noexcept { + return a.value() < b.value(); +} +inline constexpr bool operator<=(DynUnsigned a, DynUnsigned b) noexcept { + return a.value() <= b.value(); +} +inline constexpr bool operator>(DynUnsigned a, DynUnsigned b) noexcept { + return a.value() > b.value(); +} +inline constexpr bool operator>=(DynUnsigned a, DynUnsigned b) noexcept { + return a.value() >= b.value(); +} + +} // namespace coconext::types + +template +struct std::formatter> { + constexpr auto parse(std::format_parse_context& ctx) { + auto it = ctx.begin(); + if (it != ctx.end() && *it != '}') { + throw std::format_error("Unsigned formatter takes no format spec"); + } + return it; + } + auto format( + coconext::types::detail::Unsigned const& v, std::format_context& ctx + ) const { + return std::format_to(ctx.out(), "{}", v.value()); + } +}; + +template <> +struct std::formatter { + constexpr auto parse(std::format_parse_context& ctx) { + auto it = ctx.begin(); + if (it != ctx.end() && *it != '}') { + throw std::format_error("DynUnsigned formatter takes no format spec"); + } + return it; + } + auto format(coconext::types::DynUnsigned const& v, std::format_context& ctx) const { + return std::format_to(ctx.out(), "{}", v.value()); + } +}; + +template +struct std::hash> { + size_t operator()(coconext::types::detail::Unsigned const& v) const noexcept { + return std::hash{}(v.value()); + } +}; + +template <> +struct std::hash { + size_t operator()(coconext::types::DynUnsigned const& v) const noexcept { + return std::hash{}(v.value()); + } +}; + +#endif // COCONEXT_UNSIGNED_HPP diff --git a/tests/cpp/CMakeLists.txt b/tests/cpp/CMakeLists.txt index ee2a05d..274a70c 100644 --- a/tests/cpp/CMakeLists.txt +++ b/tests/cpp/CMakeLists.txt @@ -22,7 +22,9 @@ add_executable(coconext_tests test_array.cpp test_static_array.cpp test_logic_array.cpp - test_int.cpp) + test_int.cpp + test_unsigned.cpp) + target_link_libraries(coconext_tests PRIVATE coconext::coconext GTest::gtest_main) gtest_discover_tests(coconext_tests) diff --git a/tests/cpp/test_unsigned.cpp b/tests/cpp/test_unsigned.cpp new file mode 100644 index 0000000..49cb669 --- /dev/null +++ b/tests/cpp/test_unsigned.cpp @@ -0,0 +1,323 @@ +// LCOV_EXCL_BR_START -- gtest macros generate noisy uncovered branches +#include + +#include +#include +#include +#include +#include +#include + +using namespace coconext::types; + +// -- Construction ---------------------------------------------------------- + +TEST(TestUnsigned, ConstructInRange) { + Unsigned<4> a(10); + EXPECT_EQ(a.value(), 10U); + EXPECT_EQ(a.width(), 4U); +} + +TEST(TestUnsigned, RangeAccessor) { + // Length-only sugar defaults to DOWNTO (HDL convention). + EXPECT_EQ(Unsigned<8>::range(), (Range{7, Direction::DOWNTO, 0})); +} + +TEST(TestUnsigned, RangeFormsMirrorLogicArray) { + // Length-only -> DOWNTO. + static_assert( + std::is_same_v, detail::Unsigned> + ); + // 2-arg with L > R: auto-DOWNTO (same as generic). + static_assert( + std::is_same_v, detail::Unsigned> + ); + // 2-arg L == R: defaults to DOWNTO (the case where it differs from generic). + static_assert( + std::is_same_v, detail::Unsigned> + ); + // 2-arg L < R: keeps generic TO auto-direction. + static_assert( + std::is_same_v, detail::Unsigned> + ); + // 3-arg explicit direction is respected. + static_assert(std::is_same_v< + Unsigned<0, Direction::TO, 7>, + detail::Unsigned>); + // Explicit Range NTTP passes through. + static_assert(std::is_same_v< + Unsigned, + detail::Unsigned>); +} + +TEST(TestUnsigned, ConstructZeroDefault) { + Unsigned<8> a; + EXPECT_EQ(a.value(), 0U); +} + +TEST(TestUnsigned, ConstructBoundary) { + Unsigned<4> a(15); + EXPECT_EQ(a.value(), 15U); +} + +TEST(TestUnsigned, ConstructOverflowThrows) { + EXPECT_THROW(Unsigned<4>(16), std::out_of_range); + EXPECT_THROW(Unsigned<4>(100), std::out_of_range); +} + +TEST(TestUnsigned, ConstructNegativeThrows) { + EXPECT_THROW(Unsigned<4>(-1), std::out_of_range); +} + +TEST(TestUnsigned, ConstructFullWidth) { + Unsigned<64> a(std::numeric_limits::max()); + EXPECT_EQ(a.value(), std::numeric_limits::max()); +} + +TEST(TestUnsigned, CrossWidthConstruct) { + Unsigned<4> small(5); + Unsigned<8> big(small); + EXPECT_EQ(big.value(), 5U); + EXPECT_EQ(big.width(), 8U); +} + +TEST(TestUnsigned, CrossWidthNarrowingThrows) { + Unsigned<8> big(200); + EXPECT_THROW(Unsigned<4>{big}, std::out_of_range); +} + +// -- Conversion out -------------------------------------------------------- + +TEST(TestUnsigned, ToNativeInt) { + Unsigned<8> a(200); + EXPECT_EQ(a.to(), 200); + EXPECT_EQ(a.to(), 200U); +} + +TEST(TestUnsigned, ToNativeOverflowThrows) { + Unsigned<16> a(40000); + EXPECT_THROW((void)a.to(), std::out_of_range); + EXPECT_THROW((void)a.to(), std::out_of_range); +} + +// -- Arithmetic wrap ------------------------------------------------------- + +TEST(TestUnsigned, AddWraps) { + EXPECT_EQ((Unsigned<4>(15) + Unsigned<4>(1)).value(), 0U); + EXPECT_EQ((Unsigned<4>(8) + Unsigned<4>(9)).value(), 1U); +} + +TEST(TestUnsigned, SubWraps) { EXPECT_EQ((Unsigned<4>(0) - Unsigned<4>(1)).value(), 15U); } + +TEST(TestUnsigned, MulWraps) { + EXPECT_EQ((Unsigned<4>(6) * Unsigned<4>(3)).value(), 2U); // 18 mod 16 +} + +TEST(TestUnsigned, DivFloor) { EXPECT_EQ((Unsigned<8>(17) / Unsigned<8>(5)).value(), 3U); } + +TEST(TestUnsigned, ModRemainder) { + EXPECT_EQ((Unsigned<8>(17) % Unsigned<8>(5)).value(), 2U); +} + +TEST(TestUnsigned, DivByZeroThrows) { + EXPECT_THROW(Unsigned<8>(1) / Unsigned<8>(0), std::domain_error); +} + +TEST(TestUnsigned, ModByZeroThrows) { + EXPECT_THROW(Unsigned<8>(1) % Unsigned<8>(0), std::domain_error); +} + +TEST(TestUnsigned, FullWidthArithmeticNoOp) { + Unsigned<64> a(std::numeric_limits::max()); + EXPECT_EQ((a + Unsigned<64>(1)).value(), 0U); // wraps at 2^64 +} + +// -- Mixed-width result type ----------------------------------------------- + +TEST(TestUnsigned, MixedWidthResultIsMax) { + auto c = Unsigned<4>(3) + Unsigned<8>(200); + static_assert(std::is_same_v>); + EXPECT_EQ(c.value(), 203U); + EXPECT_EQ(c.range(), (Range{7, Direction::DOWNTO, 0})); +} + +TEST(TestUnsigned, MixedWidthWrapsToMaxWidth) { + auto c = Unsigned<4>(15) + Unsigned<8>(250); + static_assert(std::is_same_v>); + EXPECT_EQ(c.value(), 9U); // 265 mod 256 +} + +// -- Unary ----------------------------------------------------------------- + +TEST(TestUnsigned, UnaryNegateWraps) { + EXPECT_EQ((-Unsigned<4>(1)).value(), 15U); + EXPECT_EQ((-Unsigned<4>(0)).value(), 0U); +} + +TEST(TestUnsigned, UnaryPlus) { EXPECT_EQ((+Unsigned<4>(7)).value(), 7U); } + +TEST(TestUnsigned, BitwiseNot) { + EXPECT_EQ((~Unsigned<4>(0)).value(), 15U); + EXPECT_EQ((~Unsigned<4>(10)).value(), 5U); +} + +TEST(TestUnsigned, IncrementDecrement) { + Unsigned<4> a(15); + EXPECT_EQ((++a).value(), 0U); + EXPECT_EQ((a++).value(), 0U); + EXPECT_EQ(a.value(), 1U); + Unsigned<4> b(0); + EXPECT_EQ((--b).value(), 15U); + EXPECT_EQ((b--).value(), 15U); + EXPECT_EQ(b.value(), 14U); +} + +// -- Shifts ---------------------------------------------------------------- + +TEST(TestUnsigned, ShiftLeftDropsHighBits) { + EXPECT_EQ((Unsigned<4>(0b0011) << 2).value(), 0b1100U); + EXPECT_EQ((Unsigned<4>(0b0011) << 3).value(), 0b1000U); // top bit dropped +} + +TEST(TestUnsigned, ShiftRightLogical) { + EXPECT_EQ((Unsigned<4>(0b1100) >> 2).value(), 0b0011U); +} + +TEST(TestUnsigned, ShiftPastWidthIsZero) { + EXPECT_EQ((Unsigned<4>(15) << 64).value(), 0U); + EXPECT_EQ((Unsigned<4>(15) >> 64).value(), 0U); +} + +TEST(TestUnsigned, ShiftNegativeThrows) { + EXPECT_THROW(Unsigned<4>(1) << -1, std::invalid_argument); + EXPECT_THROW(Unsigned<4>(1) >> -1, std::invalid_argument); +} + +// -- Bitwise --------------------------------------------------------------- + +TEST(TestUnsigned, BitwiseOps) { + EXPECT_EQ((Unsigned<4>(0b1100) & Unsigned<4>(0b1010)).value(), 0b1000U); + EXPECT_EQ((Unsigned<4>(0b1100) | Unsigned<4>(0b1010)).value(), 0b1110U); + EXPECT_EQ((Unsigned<4>(0b1100) ^ Unsigned<4>(0b1010)).value(), 0b0110U); +} + +// -- Compound assignment --------------------------------------------------- + +TEST(TestUnsigned, CompoundAssign) { + Unsigned<4> a(10); + a += Unsigned<4>(7); + EXPECT_EQ(a.value(), 1U); // 17 mod 16 + a -= Unsigned<4>(2); + EXPECT_EQ(a.value(), 15U); + a *= Unsigned<4>(2); + EXPECT_EQ(a.value(), 14U); // 30 mod 16 + a <<= 1; + EXPECT_EQ(a.value(), 12U); // 28 mod 16 + a >>= 2; + EXPECT_EQ(a.value(), 3U); +} + +TEST(TestUnsigned, CompoundAssignKeepsLhsWidth) { + Unsigned<4> a(15); + a += Unsigned<8>(250); // result wrapped to width 4 + EXPECT_EQ(a.value(), 9U); + EXPECT_EQ(a.width(), 4U); +} + +// -- Comparisons ----------------------------------------------------------- + +TEST(TestUnsigned, Comparisons) { + EXPECT_TRUE(Unsigned<4>(3) == Unsigned<8>(3)); + EXPECT_TRUE(Unsigned<4>(3) != Unsigned<4>(4)); + EXPECT_TRUE(Unsigned<4>(3) < Unsigned<4>(4)); + EXPECT_TRUE(Unsigned<4>(4) <= Unsigned<8>(4)); + EXPECT_TRUE(Unsigned<8>(200) > Unsigned<4>(15)); + EXPECT_TRUE(Unsigned<4>(15) >= Unsigned<4>(15)); +} + +// -- Formatter / hash ------------------------------------------------------ + +TEST(TestUnsigned, Formatter) { EXPECT_EQ(std::format("{}", Unsigned<8>(200)), "200"); } + +TEST(TestUnsigned, Hashable) { + std::hash> h; + EXPECT_EQ(h(Unsigned<8>(5)), h(Unsigned<8>(5))); + std::unordered_set> s{Unsigned<8>(1), Unsigned<8>(1), Unsigned<8>(2)}; + EXPECT_EQ(s.size(), 2U); +} + +// -- DynUnsigned ----------------------------------------------------------- + +TEST(TestDynUnsigned, Construct) { + DynUnsigned a(10, 4); + EXPECT_EQ(a.value(), 10U); + EXPECT_EQ(a.width(), 4U); + // Length-only sugar produces DOWNTO range. + EXPECT_EQ(a.range(), (Range{3, Direction::DOWNTO, 0})); +} + +TEST(TestDynUnsigned, ConstructFromExplicitRange) { + DynUnsigned a(10, Range(15, Direction::DOWNTO, 12)); + EXPECT_EQ(a.value(), 10U); + EXPECT_EQ(a.width(), 4U); + EXPECT_EQ(a.range(), (Range{15, Direction::DOWNTO, 12})); +} + +TEST(TestDynUnsigned, ConstructOverflowThrows) { + EXPECT_THROW(DynUnsigned(16, 4), std::out_of_range); +} + +TEST(TestDynUnsigned, ConstructNegativeThrows) { + EXPECT_THROW(DynUnsigned(-1, 4), std::out_of_range); +} + +TEST(TestDynUnsigned, BadWidthThrows) { + EXPECT_THROW(DynUnsigned(0, 0), std::out_of_range); + EXPECT_THROW(DynUnsigned(0, 65), std::out_of_range); +} + +TEST(TestDynUnsigned, ArithmeticWrapAndMaxWidth) { + auto c = DynUnsigned(15, 4) + DynUnsigned(250, 8); + EXPECT_EQ(c.width(), 8U); + EXPECT_EQ(c.value(), 9U); // 265 mod 256 +} + +TEST(TestDynUnsigned, SubWraps) { + EXPECT_EQ((DynUnsigned(0, 4) - DynUnsigned(1, 4)).value(), 15U); +} + +TEST(TestDynUnsigned, DivModByZeroThrows) { + EXPECT_THROW(DynUnsigned(1, 8) / DynUnsigned(0, 8), std::domain_error); + EXPECT_THROW(DynUnsigned(1, 8) % DynUnsigned(0, 8), std::domain_error); +} + +TEST(TestDynUnsigned, Bitwise) { + EXPECT_EQ((DynUnsigned(0b1100, 4) & DynUnsigned(0b1010, 4)).value(), 0b1000U); + EXPECT_EQ((~DynUnsigned(0, 4)).value(), 15U); +} + +TEST(TestDynUnsigned, Shifts) { + EXPECT_EQ((DynUnsigned(0b0011, 4) << 2).value(), 0b1100U); + EXPECT_EQ((DynUnsigned(15, 4) << 64).value(), 0U); + EXPECT_THROW(DynUnsigned(1, 4) << -1, std::invalid_argument); +} + +TEST(TestDynUnsigned, CompoundAssign) { + DynUnsigned a(10, 4); + a += DynUnsigned(7, 4); + EXPECT_EQ(a.value(), 1U); + EXPECT_EQ(a.width(), 4U); +} + +TEST(TestDynUnsigned, Comparisons) { + EXPECT_TRUE(DynUnsigned(3, 4) == DynUnsigned(3, 8)); + EXPECT_TRUE(DynUnsigned(3, 4) < DynUnsigned(4, 4)); +} + +TEST(TestDynUnsigned, ToNative) { + EXPECT_EQ(DynUnsigned(200, 8).to(), 200); + EXPECT_THROW((void)DynUnsigned(40000, 16).to(), std::out_of_range); +} + +TEST(TestDynUnsigned, Formatter) { EXPECT_EQ(std::format("{}", DynUnsigned(42, 8)), "42"); } +// LCOV_EXCL_BR_STOP From 5529b4baab1f83cb7c7671e4cd6fe3c52c594439 Mon Sep 17 00:00:00 2001 From: Kaleb Barrett Date: Mon, 8 Jun 2026 09:25:43 -0400 Subject: [PATCH 2/3] Add fixed-width Signed and DynSigned integer types --- cpp/include/coconext/types.hpp | 1 + cpp/include/coconext/types/signed.hpp | 612 ++++++++++++++++++++++++ cpp/include/coconext/types/unsigned.hpp | 3 - tests/cpp/CMakeLists.txt | 3 +- tests/cpp/test_signed.cpp | 311 ++++++++++++ 5 files changed, 925 insertions(+), 5 deletions(-) create mode 100644 cpp/include/coconext/types/signed.hpp create mode 100644 tests/cpp/test_signed.cpp diff --git a/cpp/include/coconext/types.hpp b/cpp/include/coconext/types.hpp index 527ca08..d02152e 100644 --- a/cpp/include/coconext/types.hpp +++ b/cpp/include/coconext/types.hpp @@ -12,6 +12,7 @@ #include "./types/logic.hpp" #include "./types/logic_array.hpp" #include "./types/range.hpp" +#include "./types/signed.hpp" #include "./types/unsigned.hpp" // NOLINTEND(unused-includes) diff --git a/cpp/include/coconext/types/signed.hpp b/cpp/include/coconext/types/signed.hpp new file mode 100644 index 0000000..af72ed3 --- /dev/null +++ b/cpp/include/coconext/types/signed.hpp @@ -0,0 +1,612 @@ +#ifndef COCONEXT_SIGNED_HPP +#define COCONEXT_SIGNED_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace coconext::types { + +namespace detail { + +template +class Signed; + +template +constexpr Signed make_signed(uint64_t bits) noexcept; + +// Smallest/largest values representable in a width-w two's-complement field. +constexpr int64_t sint_min(unsigned w) noexcept { + return w >= int_max_width ? std::numeric_limits::min() + : -(int64_t{1} << (w - 1)); +} +constexpr int64_t sint_max(unsigned w) noexcept { + return w >= int_max_width ? std::numeric_limits::max() + : (int64_t{1} << (w - 1)) - 1; +} + +// Fixed-width two's-complement signed integer with wrap-on-overflow. The +// indexing range R carries HDL coordinates; only its length (in bits) matters +// for arithmetic. Backed by a single int64_t, so length is limited to 1..64. +// The stored value is always kept sign-extended from bit N-1. +template +class Signed { + static_assert( + R.length() >= 1 && R.length() <= int_max_width, "Signed width must be 1..64" + ); + + public: + using storage_type = int64_t; + + static constexpr Range range() noexcept { return R; } + static constexpr size_t width() noexcept { return R.length(); } + + constexpr Signed() noexcept = default; + + // Construct from a native integer. Throws std::out_of_range if the value + // does not fit in the N-bit signed range. + template + explicit constexpr Signed(T v) { + auto const s = static_cast(v); + // For unsigned T larger than int64_t range, the cast above could go + // negative; guard that explicitly. + if constexpr (std::is_unsigned_v) { + if (static_cast(v) > static_cast(sint_max(width()))) { + throw std::out_of_range("value does not fit in Signed width"); + } + } else { + if (s < sint_min(width()) || s > sint_max(width())) { + throw std::out_of_range("value does not fit in Signed width"); + } + } + value_ = s; + } + + // Cross-width conversion. Throws if the source value doesn't fit in N bits. + template + explicit constexpr Signed(Signed other) { + if (other.value() < sint_min(width()) || other.value() > sint_max(width())) { + throw std::out_of_range("value does not fit in Signed width"); + } + value_ = other.value(); + } + + constexpr int64_t value() const noexcept { return value_; } + + template + constexpr T to() const { + if (value_ < static_cast(std::numeric_limits::min()) + || value_ > static_cast(std::numeric_limits::max())) + { + throw std::out_of_range("Signed value does not fit in target type"); + } + return static_cast(value_); + } + + constexpr Signed operator+() const noexcept { return *this; } + constexpr Signed operator-() const noexcept { + return make_signed(~static_cast(value_) + 1); + } + constexpr Signed operator~() const noexcept { + return make_signed(~static_cast(value_)); + } + + constexpr Signed& operator++() noexcept { + value_ = sint_wrap(static_cast(value_) + 1, width()); + return *this; + } + constexpr Signed operator++(int) noexcept { + auto const old = *this; + ++*this; + return old; + } + constexpr Signed& operator--() noexcept { + value_ = sint_wrap(static_cast(value_) - 1, width()); + return *this; + } + constexpr Signed operator--(int) noexcept { + auto const old = *this; + --*this; + return old; + } + + // << drops bits beyond N; >> is arithmetic (sign-extending). + constexpr Signed operator<<(int amount) const { + if (amount < 0) { + throw std::invalid_argument("negative shift amount"); + } + if (amount >= static_cast(int_max_width)) { + return make_signed(0); + } + return make_signed(static_cast(value_) << amount); + } + constexpr Signed operator>>(int amount) const { + if (amount < 0) { + throw std::invalid_argument("negative shift amount"); + } + // Arithmetic right shift: shifting by >= width collapses to the sign. + auto const shift = + amount >= static_cast(width()) ? static_cast(width()) - 1 : amount; + return make_signed(static_cast(value_ >> shift)); + } + + template + constexpr Signed& operator+=(Signed rhs) noexcept { + value_ = sint_wrap( + static_cast(value_) + static_cast(rhs.value()), width() + ); + return *this; + } + template + constexpr Signed& operator-=(Signed rhs) noexcept { + value_ = sint_wrap( + static_cast(value_) - static_cast(rhs.value()), width() + ); + return *this; + } + template + constexpr Signed& operator*=(Signed rhs) noexcept { + value_ = sint_wrap( + static_cast(value_) * static_cast(rhs.value()), width() + ); + return *this; + } + template + constexpr Signed& operator/=(Signed rhs) { + if (rhs.value() == 0) { + throw std::domain_error("division by zero"); + } + value_ = sint_wrap(static_cast(value_ / rhs.value()), width()); + return *this; + } + template + constexpr Signed& operator%=(Signed rhs) { + if (rhs.value() == 0) { + throw std::domain_error("modulo by zero"); + } + value_ = sint_wrap(static_cast(value_ % rhs.value()), width()); + return *this; + } + template + constexpr Signed& operator&=(Signed rhs) noexcept { + value_ = sint_wrap( + static_cast(value_) & static_cast(rhs.value()), width() + ); + return *this; + } + template + constexpr Signed& operator|=(Signed rhs) noexcept { + value_ = sint_wrap( + static_cast(value_) | static_cast(rhs.value()), width() + ); + return *this; + } + template + constexpr Signed& operator^=(Signed rhs) noexcept { + value_ = sint_wrap( + static_cast(value_) ^ static_cast(rhs.value()), width() + ); + return *this; + } + constexpr Signed& operator<<=(int amount) { + *this = *this << amount; + return *this; + } + constexpr Signed& operator>>=(int amount) { + *this = *this >> amount; + return *this; + } + + private: + struct raw_tag {}; + constexpr Signed(raw_tag, uint64_t bits) noexcept : value_(sint_wrap(bits, width())) {} + + int64_t value_ = 0; + + friend constexpr Signed detail::make_signed(uint64_t bits) noexcept; +}; + +template +constexpr Signed make_signed(uint64_t bits) noexcept { + return Signed(typename Signed::raw_tag{}, bits); +} + +} // namespace detail + +template +using Signed = detail::Signed()>; + +namespace detail { + +// -- binary arithmetic: result range is {max(width)-1 DOWNTO 0} -------------- +// Operands are widened to the result width (sign-preserving) before the op via +// the make_signed wrap step, which sign-extends from the result width. +// Defined inside detail:: so ADL finds them from detail::Signed arguments. + +template +constexpr Signed> operator+(Signed a, Signed b) noexcept { + return make_signed>( + static_cast(a.value()) + static_cast(b.value()) + ); +} +template +constexpr Signed> operator-(Signed a, Signed b) noexcept { + return make_signed>( + static_cast(a.value()) - static_cast(b.value()) + ); +} +template +constexpr Signed> operator*(Signed a, Signed b) noexcept { + return make_signed>( + static_cast(a.value()) * static_cast(b.value()) + ); +} +template +constexpr Signed> operator/(Signed a, Signed b) { + if (b.value() == 0) { + throw std::domain_error("division by zero"); + } + return make_signed>( + static_cast(a.value() / b.value()) + ); +} +template +constexpr Signed> operator%(Signed a, Signed b) { + if (b.value() == 0) { + throw std::domain_error("modulo by zero"); + } + return make_signed>( + static_cast(a.value() % b.value()) + ); +} + +template +constexpr Signed> operator&(Signed a, Signed b) noexcept { + return make_signed>( + static_cast(a.value()) & static_cast(b.value()) + ); +} +template +constexpr Signed> operator|(Signed a, Signed b) noexcept { + return make_signed>( + static_cast(a.value()) | static_cast(b.value()) + ); +} +template +constexpr Signed> operator^(Signed a, Signed b) noexcept { + return make_signed>( + static_cast(a.value()) ^ static_cast(b.value()) + ); +} + +template +constexpr bool operator==(Signed a, Signed b) noexcept { + return a.value() == b.value(); +} +template +constexpr bool operator!=(Signed a, Signed b) noexcept { + return a.value() != b.value(); +} +template +constexpr bool operator<(Signed a, Signed b) noexcept { + return a.value() < b.value(); +} +template +constexpr bool operator<=(Signed a, Signed b) noexcept { + return a.value() <= b.value(); +} +template +constexpr bool operator>(Signed a, Signed b) noexcept { + return a.value() > b.value(); +} +template +constexpr bool operator>=(Signed a, Signed b) noexcept { + return a.value() >= b.value(); +} + +} // namespace detail + +// -- DynSigned: runtime-range counterpart ----------------------------------- + +class DynSigned { + public: + using storage_type = int64_t; + + constexpr Range const& range() const noexcept { return range_; } + constexpr size_t width() const noexcept { return range_.length(); } + + template + constexpr DynSigned(T v, Range range) : range_(range) { + detail::check_width(static_cast(range.length())); + auto const s = static_cast(v); + if constexpr (std::is_unsigned_v) { + if (static_cast(v) > static_cast(detail::sint_max(width()))) + { + throw std::out_of_range("value does not fit in DynSigned width"); + } + } else { + if (s < detail::sint_min(width()) || s > detail::sint_max(width())) { + throw std::out_of_range("value does not fit in DynSigned width"); + } + } + value_ = s; + } + + // Length-only sugar: produces a {length-1 DOWNTO 0} range (HDL convention). + template + constexpr DynSigned(T v, unsigned length) + : DynSigned(v, detail::int_downto_range(length)) {} + + constexpr int64_t value() const noexcept { return value_; } + + template + constexpr T to() const { + if (value_ < static_cast(std::numeric_limits::min()) + || value_ > static_cast(std::numeric_limits::max())) + { + throw std::out_of_range("DynSigned value does not fit in target type"); + } + return static_cast(value_); + } + + constexpr DynSigned operator+() const noexcept { return *this; } + constexpr DynSigned operator-() const noexcept { + return DynSigned(raw_tag{}, ~static_cast(value_) + 1, range_); + } + constexpr DynSigned operator~() const noexcept { + return DynSigned(raw_tag{}, ~static_cast(value_), range_); + } + + constexpr DynSigned& operator++() noexcept { + value_ = detail::sint_wrap(static_cast(value_) + 1, width()); + return *this; + } + constexpr DynSigned operator++(int) noexcept { + auto const old = *this; + ++*this; + return old; + } + constexpr DynSigned& operator--() noexcept { + value_ = detail::sint_wrap(static_cast(value_) - 1, width()); + return *this; + } + constexpr DynSigned operator--(int) noexcept { + auto const old = *this; + --*this; + return old; + } + + constexpr DynSigned operator<<(int amount) const { + if (amount < 0) { + throw std::invalid_argument("negative shift amount"); + } + if (amount >= static_cast(detail::int_max_width)) { + return DynSigned(raw_tag{}, 0, range_); + } + return DynSigned(raw_tag{}, static_cast(value_) << amount, range_); + } + constexpr DynSigned operator>>(int amount) const { + if (amount < 0) { + throw std::invalid_argument("negative shift amount"); + } + auto const shift = + amount >= static_cast(width()) ? static_cast(width()) - 1 : amount; + return DynSigned(raw_tag{}, static_cast(value_ >> shift), range_); + } + + constexpr DynSigned& operator+=(DynSigned rhs) noexcept { + value_ = detail::sint_wrap( + static_cast(value_) + static_cast(rhs.value_), width() + ); + return *this; + } + constexpr DynSigned& operator-=(DynSigned rhs) noexcept { + value_ = detail::sint_wrap( + static_cast(value_) - static_cast(rhs.value_), width() + ); + return *this; + } + constexpr DynSigned& operator*=(DynSigned rhs) noexcept { + value_ = detail::sint_wrap( + static_cast(value_) * static_cast(rhs.value_), width() + ); + return *this; + } + constexpr DynSigned& operator/=(DynSigned rhs) { + if (rhs.value_ == 0) { + throw std::domain_error("division by zero"); + } + value_ = detail::sint_wrap(static_cast(value_ / rhs.value_), width()); + return *this; + } + constexpr DynSigned& operator%=(DynSigned rhs) { + if (rhs.value_ == 0) { + throw std::domain_error("modulo by zero"); + } + value_ = detail::sint_wrap(static_cast(value_ % rhs.value_), width()); + return *this; + } + constexpr DynSigned& operator&=(DynSigned rhs) noexcept { + value_ = detail::sint_wrap( + static_cast(value_) & static_cast(rhs.value_), width() + ); + return *this; + } + constexpr DynSigned& operator|=(DynSigned rhs) noexcept { + value_ = detail::sint_wrap( + static_cast(value_) | static_cast(rhs.value_), width() + ); + return *this; + } + constexpr DynSigned& operator^=(DynSigned rhs) noexcept { + value_ = detail::sint_wrap( + static_cast(value_) ^ static_cast(rhs.value_), width() + ); + return *this; + } + constexpr DynSigned& operator<<=(int amount) { + *this = *this << amount; + return *this; + } + constexpr DynSigned& operator>>=(int amount) { + *this = *this >> amount; + return *this; + } + + private: + struct raw_tag {}; + constexpr DynSigned(raw_tag, uint64_t bits, Range range) noexcept + : value_(detail::sint_wrap(bits, static_cast(range.length()))), + range_(range) {} + + int64_t value_ = 0; + Range range_ = detail::int_downto_range(1); + + friend constexpr DynSigned operator+(DynSigned, DynSigned) noexcept; + friend constexpr DynSigned operator-(DynSigned, DynSigned) noexcept; + friend constexpr DynSigned operator*(DynSigned, DynSigned) noexcept; + friend constexpr DynSigned operator/(DynSigned, DynSigned); + friend constexpr DynSigned operator%(DynSigned, DynSigned); + friend constexpr DynSigned operator&(DynSigned, DynSigned) noexcept; + friend constexpr DynSigned operator|(DynSigned, DynSigned) noexcept; + friend constexpr DynSigned operator^(DynSigned, DynSigned) noexcept; +}; + +inline constexpr DynSigned operator+(DynSigned a, DynSigned b) noexcept { + auto const r = detail::int_downto_range(std::max(a.width(), b.width())); + return DynSigned( + DynSigned::raw_tag{}, + static_cast(a.value_) + static_cast(b.value_), + r + ); +} +inline constexpr DynSigned operator-(DynSigned a, DynSigned b) noexcept { + auto const r = detail::int_downto_range(std::max(a.width(), b.width())); + return DynSigned( + DynSigned::raw_tag{}, + static_cast(a.value_) - static_cast(b.value_), + r + ); +} +inline constexpr DynSigned operator*(DynSigned a, DynSigned b) noexcept { + auto const r = detail::int_downto_range(std::max(a.width(), b.width())); + return DynSigned( + DynSigned::raw_tag{}, + static_cast(a.value_) * static_cast(b.value_), + r + ); +} +inline constexpr DynSigned operator/(DynSigned a, DynSigned b) { + if (b.value_ == 0) { + throw std::domain_error("division by zero"); + } + auto const r = detail::int_downto_range(std::max(a.width(), b.width())); + return DynSigned(DynSigned::raw_tag{}, static_cast(a.value_ / b.value_), r); +} +inline constexpr DynSigned operator%(DynSigned a, DynSigned b) { + if (b.value_ == 0) { + throw std::domain_error("modulo by zero"); + } + auto const r = detail::int_downto_range(std::max(a.width(), b.width())); + return DynSigned(DynSigned::raw_tag{}, static_cast(a.value_ % b.value_), r); +} +inline constexpr DynSigned operator&(DynSigned a, DynSigned b) noexcept { + auto const r = detail::int_downto_range(std::max(a.width(), b.width())); + return DynSigned( + DynSigned::raw_tag{}, + static_cast(a.value_) & static_cast(b.value_), + r + ); +} +inline constexpr DynSigned operator|(DynSigned a, DynSigned b) noexcept { + auto const r = detail::int_downto_range(std::max(a.width(), b.width())); + return DynSigned( + DynSigned::raw_tag{}, + static_cast(a.value_) | static_cast(b.value_), + r + ); +} +inline constexpr DynSigned operator^(DynSigned a, DynSigned b) noexcept { + auto const r = detail::int_downto_range(std::max(a.width(), b.width())); + return DynSigned( + DynSigned::raw_tag{}, + static_cast(a.value_) ^ static_cast(b.value_), + r + ); +} + +inline constexpr bool operator==(DynSigned a, DynSigned b) noexcept { + return a.value() == b.value(); +} +inline constexpr bool operator!=(DynSigned a, DynSigned b) noexcept { + return a.value() != b.value(); +} +inline constexpr bool operator<(DynSigned a, DynSigned b) noexcept { + return a.value() < b.value(); +} +inline constexpr bool operator<=(DynSigned a, DynSigned b) noexcept { + return a.value() <= b.value(); +} +inline constexpr bool operator>(DynSigned a, DynSigned b) noexcept { + return a.value() > b.value(); +} +inline constexpr bool operator>=(DynSigned a, DynSigned b) noexcept { + return a.value() >= b.value(); +} + +} // namespace coconext::types + +template +struct std::formatter> { + constexpr auto parse(std::format_parse_context& ctx) { + auto it = ctx.begin(); + if (it != ctx.end() && *it != '}') { + throw std::format_error("Signed formatter takes no format spec"); + } + return it; + } + auto format( + coconext::types::detail::Signed const& v, std::format_context& ctx + ) const { + return std::format_to(ctx.out(), "{}", v.value()); + } +}; + +template <> +struct std::formatter { + constexpr auto parse(std::format_parse_context& ctx) { + auto it = ctx.begin(); + if (it != ctx.end() && *it != '}') { + throw std::format_error("DynSigned formatter takes no format spec"); + } + return it; + } + auto format(coconext::types::DynSigned const& v, std::format_context& ctx) const { + return std::format_to(ctx.out(), "{}", v.value()); + } +}; + +template +struct std::hash> { + size_t operator()(coconext::types::detail::Signed const& v) const noexcept { + return std::hash{}(v.value()); + } +}; + +template <> +struct std::hash { + size_t operator()(coconext::types::DynSigned const& v) const noexcept { + return std::hash{}(v.value()); + } +}; + +#endif // COCONEXT_SIGNED_HPP diff --git a/cpp/include/coconext/types/unsigned.hpp b/cpp/include/coconext/types/unsigned.hpp index 5380654..7245677 100644 --- a/cpp/include/coconext/types/unsigned.hpp +++ b/cpp/include/coconext/types/unsigned.hpp @@ -196,9 +196,6 @@ constexpr Unsigned make_unsigned(uint64_t bits) noexcept { return Unsigned(typename Unsigned::raw_tag{}, bits); } -template -inline constexpr size_t max_width = A > B ? A : B; - } // namespace detail // User-facing alias: accepts the same NTTP forms as Array, with HDL diff --git a/tests/cpp/CMakeLists.txt b/tests/cpp/CMakeLists.txt index 274a70c..6b72dfa 100644 --- a/tests/cpp/CMakeLists.txt +++ b/tests/cpp/CMakeLists.txt @@ -23,8 +23,7 @@ add_executable(coconext_tests test_static_array.cpp test_logic_array.cpp test_int.cpp - test_unsigned.cpp) - + test_signed.cpp) target_link_libraries(coconext_tests PRIVATE coconext::coconext GTest::gtest_main) gtest_discover_tests(coconext_tests) diff --git a/tests/cpp/test_signed.cpp b/tests/cpp/test_signed.cpp new file mode 100644 index 0000000..c999101 --- /dev/null +++ b/tests/cpp/test_signed.cpp @@ -0,0 +1,311 @@ +// LCOV_EXCL_BR_START -- gtest macros generate noisy uncovered branches +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace coconext::types; + +// -- Construction ---------------------------------------------------------- + +TEST(TestSigned, ConstructInRange) { + Signed<4> a(-3); + EXPECT_EQ(a.value(), -3); + EXPECT_EQ(a.width(), 4U); +} + +TEST(TestSigned, RangeAccessor) { + EXPECT_EQ(Signed<8>::range(), (Range{7, Direction::DOWNTO, 0})); +} + +TEST(TestSigned, RangeFormsMirrorLogicArray) { + static_assert( + std::is_same_v, detail::Signed> + ); + static_assert( + std::is_same_v, detail::Signed> + ); + static_assert( + std::is_same_v, detail::Signed> + ); + static_assert(std::is_same_v, detail::Signed>); +} + +TEST(TestSigned, ConstructZeroDefault) { + Signed<8> a; + EXPECT_EQ(a.value(), 0); +} + +TEST(TestSigned, ConstructBoundaries) { + EXPECT_EQ(Signed<4>(7).value(), 7); // max + EXPECT_EQ(Signed<4>(-8).value(), -8); // min +} + +TEST(TestSigned, ConstructOverflowThrows) { + EXPECT_THROW(Signed<4>(8), std::out_of_range); + EXPECT_THROW(Signed<4>(-9), std::out_of_range); +} + +TEST(TestSigned, ConstructFullWidth) { + Signed<64> a(std::numeric_limits::min()); + EXPECT_EQ(a.value(), std::numeric_limits::min()); + Signed<64> b(std::numeric_limits::max()); + EXPECT_EQ(b.value(), std::numeric_limits::max()); +} + +TEST(TestSigned, ConstructFromLargeUnsignedThrows) { + auto const big = std::numeric_limits::max(); + EXPECT_THROW(Signed<8>{big}, std::out_of_range); +} + +TEST(TestSigned, CrossWidthConstruct) { + Signed<4> small(-3); + Signed<8> big(small); + EXPECT_EQ(big.value(), -3); + EXPECT_EQ(big.width(), 8U); +} + +TEST(TestSigned, CrossWidthNarrowingThrows) { + Signed<8> big(100); + EXPECT_THROW(Signed<4>{big}, std::out_of_range); +} + +// -- Conversion out -------------------------------------------------------- + +TEST(TestSigned, ToNativeInt) { + Signed<8> a(-100); + EXPECT_EQ(a.to(), -100); + EXPECT_EQ(a.to(), -100); +} + +TEST(TestSigned, ToNativeOverflowThrows) { + Signed<16> a(-30000); + EXPECT_THROW((void)a.to(), std::out_of_range); + EXPECT_THROW((void)a.to(), std::out_of_range); // negative into unsigned +} + +// -- Arithmetic wrap ------------------------------------------------------- + +TEST(TestSigned, AddWraps) { + EXPECT_EQ((Signed<4>(7) + Signed<4>(1)).value(), -8); // overflow wraps + EXPECT_EQ((Signed<4>(3) + Signed<4>(2)).value(), 5); +} + +TEST(TestSigned, SubWraps) { + EXPECT_EQ((Signed<4>(-8) - Signed<4>(1)).value(), 7); // underflow wraps +} + +TEST(TestSigned, MulWraps) { + EXPECT_EQ((Signed<4>(4) * Signed<4>(4)).value(), 0); // 16 wraps to 0 + EXPECT_EQ((Signed<4>(-2) * Signed<4>(3)).value(), -6); +} + +TEST(TestSigned, DivTruncatesTowardZero) { + EXPECT_EQ((Signed<8>(-7) / Signed<8>(2)).value(), -3); +} + +TEST(TestSigned, ModFollowsCpp) { EXPECT_EQ((Signed<8>(-7) % Signed<8>(2)).value(), -1); } + +TEST(TestSigned, DivByZeroThrows) { + EXPECT_THROW(Signed<8>(1) / Signed<8>(0), std::domain_error); +} + +TEST(TestSigned, ModByZeroThrows) { + EXPECT_THROW(Signed<8>(1) % Signed<8>(0), std::domain_error); +} + +TEST(TestSigned, MinDivNegOneWraps) { + // -8 / -1 = 8, which wraps to -8 in 4-bit two's complement. + EXPECT_EQ((Signed<4>(-8) / Signed<4>(-1)).value(), -8); +} + +TEST(TestSigned, FullWidthArithmeticNoOp) { + Signed<64> a(std::numeric_limits::max()); + EXPECT_EQ((a + Signed<64>(1)).value(), std::numeric_limits::min()); +} + +// -- Mixed-width result type ----------------------------------------------- + +TEST(TestSigned, MixedWidthResultIsMax) { + auto c = Signed<4>(-3) + Signed<8>(100); + static_assert(std::is_same_v>); + EXPECT_EQ(c.value(), 97); +} + +TEST(TestSigned, MixedWidthNegativePreserved) { + // Narrower negative operand sign-extends into the wider result. + auto c = Signed<4>(-1) + Signed<8>(0); + static_assert(std::is_same_v>); + EXPECT_EQ(c.value(), -1); +} + +// -- Unary ----------------------------------------------------------------- + +TEST(TestSigned, UnaryNegate) { + EXPECT_EQ((-Signed<4>(3)).value(), -3); + EXPECT_EQ((-Signed<4>(-8)).value(), -8); // negating min wraps to itself +} + +TEST(TestSigned, UnaryPlus) { EXPECT_EQ((+Signed<4>(-5)).value(), -5); } + +TEST(TestSigned, BitwiseNot) { + EXPECT_EQ((~Signed<4>(0)).value(), -1); // ~0 = all ones = -1 + EXPECT_EQ((~Signed<4>(-1)).value(), 0); +} + +TEST(TestSigned, IncrementDecrement) { + Signed<4> a(7); + EXPECT_EQ((++a).value(), -8); // wraps + Signed<4> b(-8); + EXPECT_EQ((--b).value(), 7); // wraps + EXPECT_EQ((b--).value(), 7); + EXPECT_EQ(b.value(), 6); +} + +// -- Shifts ---------------------------------------------------------------- + +TEST(TestSigned, ShiftLeftDropsHighBits) { + EXPECT_EQ((Signed<4>(3) << 1).value(), 6); + EXPECT_EQ((Signed<4>(3) << 2).value(), -4); // 0b1100 = -4 +} + +TEST(TestSigned, ShiftRightArithmetic) { + EXPECT_EQ((Signed<8>(-8) >> 1).value(), -4); // sign-extends + EXPECT_EQ((Signed<8>(-1) >> 3).value(), -1); // stays all ones + EXPECT_EQ((Signed<8>(16) >> 2).value(), 4); +} + +TEST(TestSigned, ShiftRightPastWidthIsSign) { + EXPECT_EQ((Signed<8>(-1) >> 64).value(), -1); // negative -> all ones + EXPECT_EQ((Signed<8>(5) >> 64).value(), 0); // positive -> 0 +} + +TEST(TestSigned, ShiftNegativeThrows) { + EXPECT_THROW(Signed<4>(1) << -1, std::invalid_argument); + EXPECT_THROW(Signed<4>(1) >> -1, std::invalid_argument); +} + +// -- Bitwise --------------------------------------------------------------- + +TEST(TestSigned, BitwiseOps) { + EXPECT_EQ((Signed<4>(0b0110) & Signed<4>(0b0011)).value(), 0b0010); + EXPECT_EQ((Signed<4>(0b0100) | Signed<4>(0b0001)).value(), 0b0101); + EXPECT_EQ((Signed<4>(0b0110) ^ Signed<4>(0b0011)).value(), 0b0101); +} + +// -- Compound assignment --------------------------------------------------- + +TEST(TestSigned, CompoundAssign) { + Signed<8> a(10); + a += Signed<8>(5); + EXPECT_EQ(a.value(), 15); + a -= Signed<8>(20); + EXPECT_EQ(a.value(), -5); + a *= Signed<8>(-2); + EXPECT_EQ(a.value(), 10); +} + +TEST(TestSigned, CompoundAssignKeepsLhsWidth) { + Signed<4> a(7); + a += Signed<8>(1); // wraps to width 4 + EXPECT_EQ(a.value(), -8); + EXPECT_EQ(a.width(), 4U); +} + +// -- Comparisons ----------------------------------------------------------- + +TEST(TestSigned, Comparisons) { + EXPECT_TRUE(Signed<4>(-3) == Signed<8>(-3)); + EXPECT_TRUE(Signed<4>(-3) != Signed<4>(3)); + EXPECT_TRUE(Signed<4>(-3) < Signed<4>(2)); + EXPECT_TRUE(Signed<8>(-100) < Signed<4>(0)); + EXPECT_TRUE(Signed<4>(7) > Signed<4>(-8)); + EXPECT_TRUE(Signed<4>(5) >= Signed<4>(5)); +} + +// -- Formatter / hash ------------------------------------------------------ + +TEST(TestSigned, Formatter) { EXPECT_EQ(std::format("{}", Signed<8>(-100)), "-100"); } + +TEST(TestSigned, Hashable) { + std::hash> h; + EXPECT_EQ(h(Signed<8>(-5)), h(Signed<8>(-5))); + std::unordered_set> s{Signed<8>(-1), Signed<8>(-1), Signed<8>(2)}; + EXPECT_EQ(s.size(), 2U); +} + +// -- DynSigned ------------------------------------------------------------- + +TEST(TestDynSigned, Construct) { + DynSigned a(-3, 4); + EXPECT_EQ(a.value(), -3); + EXPECT_EQ(a.width(), 4U); + EXPECT_EQ(a.range(), (Range{3, Direction::DOWNTO, 0})); +} + +TEST(TestDynSigned, ConstructFromExplicitRange) { + DynSigned a(-3, Range(15, Direction::DOWNTO, 12)); + EXPECT_EQ(a.value(), -3); + EXPECT_EQ(a.range(), (Range{15, Direction::DOWNTO, 12})); +} + +TEST(TestDynSigned, ConstructOverflowThrows) { + EXPECT_THROW(DynSigned(8, 4), std::out_of_range); + EXPECT_THROW(DynSigned(-9, 4), std::out_of_range); +} + +TEST(TestDynSigned, BadWidthThrows) { + EXPECT_THROW(DynSigned(0, 0), std::out_of_range); + EXPECT_THROW(DynSigned(0, 65), std::out_of_range); +} + +TEST(TestDynSigned, ArithmeticWrapAndMaxWidth) { + auto c = DynSigned(7, 4) + DynSigned(1, 8); + EXPECT_EQ(c.width(), 8U); + EXPECT_EQ(c.value(), 8); // no wrap at width 8 +} + +TEST(TestDynSigned, AddWrapsAtOwnWidth) { + EXPECT_EQ((DynSigned(7, 4) + DynSigned(1, 4)).value(), -8); +} + +TEST(TestDynSigned, DivModByZeroThrows) { + EXPECT_THROW(DynSigned(1, 8) / DynSigned(0, 8), std::domain_error); + EXPECT_THROW(DynSigned(1, 8) % DynSigned(0, 8), std::domain_error); +} + +TEST(TestDynSigned, ShiftRightArithmetic) { + EXPECT_EQ((DynSigned(-8, 8) >> 1).value(), -4); + EXPECT_EQ((DynSigned(-1, 8) >> 64).value(), -1); + EXPECT_THROW(DynSigned(1, 4) << -1, std::invalid_argument); +} + +TEST(TestDynSigned, Bitwise) { + EXPECT_EQ((~DynSigned(0, 4)).value(), -1); + EXPECT_EQ((DynSigned(0b0110, 4) & DynSigned(0b0011, 4)).value(), 0b0010); +} + +TEST(TestDynSigned, CompoundAssign) { + DynSigned a(7, 4); + a += DynSigned(1, 4); + EXPECT_EQ(a.value(), -8); + EXPECT_EQ(a.width(), 4U); +} + +TEST(TestDynSigned, Comparisons) { + EXPECT_TRUE(DynSigned(-3, 4) == DynSigned(-3, 8)); + EXPECT_TRUE(DynSigned(-3, 4) < DynSigned(2, 4)); +} + +TEST(TestDynSigned, ToNative) { + EXPECT_EQ(DynSigned(-100, 8).to(), -100); + EXPECT_THROW((void)DynSigned(-30000, 16).to(), std::out_of_range); +} + +TEST(TestDynSigned, Formatter) { EXPECT_EQ(std::format("{}", DynSigned(-42, 8)), "-42"); } +// LCOV_EXCL_BR_STOP From 8bd0455f8985ced36823ca91ae96dbc94645d49e Mon Sep 17 00:00:00 2001 From: ZiaCheemaGit Date: Sat, 20 Jun 2026 12:22:57 +0500 Subject: [PATCH 3/3] resolve pre-commit issues left out by re-base --- cpp/include/coconext/types.hpp | 2 +- tests/cpp/CMakeLists.txt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cpp/include/coconext/types.hpp b/cpp/include/coconext/types.hpp index d02152e..4195ab9 100644 --- a/cpp/include/coconext/types.hpp +++ b/cpp/include/coconext/types.hpp @@ -7,13 +7,13 @@ #include "./types/concepts.hpp" #include "./types/direction.hpp" #include "./types/int.hpp" -#include "./types/vector.hpp" #include "./types/int_common.hpp" #include "./types/logic.hpp" #include "./types/logic_array.hpp" #include "./types/range.hpp" #include "./types/signed.hpp" #include "./types/unsigned.hpp" +#include "./types/vector.hpp" // NOLINTEND(unused-includes) #endif // COCONEXT_TYPES_HPP diff --git a/tests/cpp/CMakeLists.txt b/tests/cpp/CMakeLists.txt index 6b72dfa..28f86fa 100644 --- a/tests/cpp/CMakeLists.txt +++ b/tests/cpp/CMakeLists.txt @@ -23,7 +23,8 @@ add_executable(coconext_tests test_static_array.cpp test_logic_array.cpp test_int.cpp - test_signed.cpp) + test_signed.cpp + test_unsigned.cpp) target_link_libraries(coconext_tests PRIVATE coconext::coconext GTest::gtest_main) gtest_discover_tests(coconext_tests)