diff --git a/cpp/include/coconext/types/logic.hpp b/cpp/include/coconext/types/logic.hpp index 1174ae5..adc8c58 100644 --- a/cpp/include/coconext/types/logic.hpp +++ b/cpp/include/coconext/types/logic.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -18,6 +19,8 @@ enum class ResolveMethod { RANDOM, }; +class Bit; + class Logic { public: enum class value_type : uint8_t { @@ -37,11 +40,15 @@ class Logic { constexpr Logic(value_type value) noexcept : value_(value) {} constexpr value_type value() const noexcept { return value_; } - constexpr bool is_resolvable() const noexcept { - return value_ == _0 || value_ == _1 || value_ == L || value_ == H; - } + // Resolve under `method`. Returns nullopt iff the value is not resolvable + // under `method` -- ERROR accepts only 0/1; WEAK additionally accepts L/H; + // ZEROS, ONES, RANDOM accept anything. This unifies the old separate + // is_resolvable/resolve pair: `r.has_value()` answers the predicate, + // `r.value()` extracts the Bit. + std::optional resolve(ResolveMethod method) const noexcept; - Logic resolve(ResolveMethod method) const; + // Default to WEAK. + std::optional resolve() const noexcept; private: value_type value_ = _0; @@ -59,9 +66,11 @@ class Bit { constexpr Bit(value_type value) noexcept : value_(value) {} constexpr value_type value() const noexcept { return value_; } - constexpr bool is_resolvable() const noexcept { return true; } - - constexpr Bit resolve(ResolveMethod) const noexcept { return *this; } + // Every Bit is resolvable under every method, so the optional is always + // engaged. Kept for uniformity with Logic::resolve so generic code over + // LogicType can treat both the same way. + constexpr std::optional resolve(ResolveMethod) const noexcept { return *this; } + constexpr std::optional resolve() const noexcept { return *this; } // Implicit conversion from Bit to Logic mimics subtype upcasting. constexpr operator Logic() const noexcept { @@ -80,6 +89,12 @@ class Bit { value_type value_ = _0; }; +// Out-of-line: needs the Bit definition above to instantiate +// std::optional. +inline std::optional Logic::resolve() const noexcept { + return resolve(ResolveMethod::WEAK); +} + constexpr bool operator==(Logic const& lhs, Logic const& rhs) noexcept { return lhs.value() == rhs.value(); } @@ -235,6 +250,8 @@ constexpr int to_int(Logic const& value) { } } +constexpr int to_int(Bit const& value) noexcept { return value.value() == Bit::_0 ? 0 : 1; } + constexpr Logic operator|(Logic const& lhs, Logic const& rhs) noexcept { using enum Logic::value_type; constexpr Logic const table[9][9] = { diff --git a/cpp/include/coconext/types/logic_array.hpp b/cpp/include/coconext/types/logic_array.hpp index 5d38a9e..8e22122 100644 --- a/cpp/include/coconext/types/logic_array.hpp +++ b/cpp/include/coconext/types/logic_array.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -78,46 +79,23 @@ constexpr Range make_logic_static_range() { } } -// CRTP mixin providing the Logic/Bit-specific query and resolution members. -// Inherited by the Logic/Bit specializations of Array, Vector, StaticArraySlice, -// and ArraySlice below. The non-Logic/Bit primaries do NOT inherit this, -// so `Array::is_resolvable()` and friends don't exist. +} // namespace detail + +template <> +class Vector; + +namespace detail { + +// CRTP mixin providing the Logic/Bit-specific resolve member. Inherited by +// the Logic/Bit specializations of Array, Vector, StaticArraySlice, and +// ArraySlice below. The non-Logic/Bit primaries do NOT inherit this, so +// `Array::resolve(method)` and friends don't exist. template struct LogicArrayMixin { - bool is_resolvable() const noexcept { - auto const& self = *static_cast(this); - using Elem = std::ranges::range_value_t; - if constexpr (std::same_as) { - // Every Bit is resolvable; skip the walk. - return true; - } else { - return std::ranges::all_of(self, [](auto const& v) { - return v.is_resolvable(); - }); - } - } - // Element-wise resolve. Returns a static `Array` when Self has a - // compile-time range, a heap-allocated `Vector` otherwise. The - // returned array preserves Self's range (an owner returns the same shape; - // a slice returns an owner sized like the slice's range). - auto resolve(ResolveMethod method) const { - auto const& self = *static_cast(this); - using Elem = std::ranges::range_value_t; - if constexpr (StaticRangedSequence) { - ::coconext::types::Array result{}; - std::ranges::transform(self, result.begin(), [method](auto const& v) { - return v.resolve(method); - }); - return result; - } else { - ::coconext::types::Vector result(self.range()); - std::ranges::transform(self, result.begin(), [method](auto const& v) { - return v.resolve(method); - }); - return result; - } - } + auto resolve(ResolveMethod method) const; + // Default to WEAK. + auto resolve() const { return resolve(ResolveMethod::WEAK); } // Reductions: fold over the array with the corresponding bitwise op. // Empty arrays return the operation's identity (1 for AND, 0 for OR/XOR), @@ -160,9 +138,9 @@ struct LogicArrayMixin { // // These specializations make `Array`, `Array`, // `Vector`, `Vector`, and slices over Logic/Bit owners -// inherit `LogicArrayMixin`, gaining `is_resolvable()` and `resolve(method)` -// as members. The primary templates remain unchanged for non-Logic element -// types -- e.g., `Array` has no `is_resolvable()`. +// inherit `LogicArrayMixin`, gaining `resolve(method)` as a member. The +// primary templates remain unchanged for non-Logic element types -- e.g., +// `Array` has no `resolve(method)`. namespace detail { @@ -252,6 +230,40 @@ class StaticArraySlice using detail::StaticArraySliceImpl::operator=; }; +namespace detail { + +template +auto LogicArrayMixin::resolve(ResolveMethod method) const { + auto const& self = *static_cast(this); + if constexpr (StaticRangedSequence) { + std::optional<::coconext::types::detail::Array> result{ + std::in_place + }; + auto out = result->begin(); + for (auto const& v : self) { + auto r = v.resolve(method); + if (!r) { + return decltype(result){std::nullopt}; + } + *out++ = *r; + } + return result; + } else { + std::optional<::coconext::types::Vector> result{std::in_place, self.range()}; + auto out = result->begin(); + for (auto const& v : self) { + auto r = v.resolve(method); + if (!r) { + return decltype(result){std::nullopt}; + } + *out++ = *r; + } + return result; + } +} + +} // namespace detail + using LogicVector = Vector; using BitVector = Vector; @@ -747,31 +759,6 @@ inline Vector to_bit_array(std::string_view s) { return detail::parse_logic_string(s, [](char c) { return to_bit(c); }); } -// Convert a range of Logic to a Vector. Throws if any element is not -// 0/1/L/H (i.e. every element must be resolvable). Constrained to sized_range -// so the result can be sized up-front from std::ranges::size in O(1) and the -// resolvability check can be fused with the fill into a single pass. -template - requires std::same_as, Logic> -Vector to_bit_array(R const& range) { - auto const sz = std::ranges::size(range); - if (sz > static_cast(std::numeric_limits::max())) { - throw std::length_error("range too long for Range::value_type"); - } - auto const n = static_cast(sz); - Vector result(Range{n - 1, Direction::DOWNTO, 0}); - auto out = result.begin(); - for (Logic const& v : range) { - if (!v.is_resolvable()) { - throw std::invalid_argument( - "Cannot convert non-resolvable Logic values to BitArray" - ); - } - *out++ = to_int(v) ? Bit::_1 : Bit::_0; - } - return result; -} - // -- String-literal UDL ---------------------------------------------------- template diff --git a/cpp/src/logic.cpp b/cpp/src/logic.cpp index c638349..47c7bf3 100644 --- a/cpp/src/logic.cpp +++ b/cpp/src/logic.cpp @@ -1,6 +1,6 @@ #include +#include #include -#include #include "./random.hpp" @@ -8,72 +8,65 @@ using namespace coconext::types; namespace coconext::types { -Logic Logic::resolve(ResolveMethod method) const { +std::optional Logic::resolve(ResolveMethod method) const noexcept { switch (method) { case ResolveMethod::ERROR: switch (value_) { case _0: + return Bit::_0; case _1: - return *this; + return Bit::_1; default: - throw std::invalid_argument("Logic value is not resolvable"); + return std::nullopt; } case ResolveMethod::WEAK: switch (value_) { case _0: - case _1: - return *this; case L: - return _0; + return Bit::_0; + case _1: case H: - return _1; - case W: - return X; + return Bit::_1; default: - return *this; + return std::nullopt; } case ResolveMethod::ZEROS: switch (value_) { case _0: - case _1: - return *this; case L: - return _0; + return Bit::_0; + case _1: case H: - return _1; + return Bit::_1; default: - return _0; + return Bit::_0; } case ResolveMethod::ONES: switch (value_) { case _0: - case _1: - return *this; case L: - return _0; + return Bit::_0; + case _1: case H: - return _1; + return Bit::_1; default: - return _1; + return Bit::_1; } - case ResolveMethod::RANDOM: { + case ResolveMethod::RANDOM: switch (value_) { case _0: - case _1: - return *this; case L: - return _0; + return Bit::_0; + case _1: case H: - return _1; + return Bit::_1; default: { auto& rng = get_rng(); - return (rng() % 2 == 0) ? _0 : _1; + return (rng() % 2 == 0) ? Bit::_0 : Bit::_1; } } } - default: - throw std::invalid_argument("Unknown resolve method"); - } + return std::nullopt; } } // namespace coconext::types diff --git a/nanobind/src/types/bind_logic.cpp b/nanobind/src/types/bind_logic.cpp index c3dd029..2537583 100644 --- a/nanobind/src/types/bind_logic.cpp +++ b/nanobind/src/types/bind_logic.cpp @@ -55,7 +55,7 @@ void register_logic(nb::module_& m) { [](Logic* self, long long value) { new (self) Logic(to_logic(value)); } ) .def("__str__", [](Logic const& self) { return to_string(self); }) - .def("__index__", &to_int) + .def("__index__", [](Logic const& self) { return to_int(self); }) .def("__bool__", [](Logic const& self) { return to_int(self) != 0; }) .def( "__repr__", @@ -126,14 +126,43 @@ void register_logic(nb::module_& m) { .def( "__invert__", [](Logic const& self) { return ~self; }, nb::is_operator() ) - .def_prop_ro("is_resolvable", &Logic::is_resolvable) + .def_prop_ro( + "is_resolvable", + [](Logic const& self) { return self.resolve(ResolveMethod::WEAK).has_value(); } + ) .def( "resolve", [](Logic const& self, std::string_view method) { - return self.resolve(string_to_resolve_method(method)); + auto m = string_to_resolve_method(method); + // Compat shim: pre-branch C++ WEAK mapped L/H -> 0/1, W -> X, + // and left other metavalues unchanged. Post-branch C++ returns + // nullopt on metavalues under WEAK. Reproduce the old mapping + // here so upstream Python (and its integration tests) don't + // break. + if (m == ResolveMethod::WEAK) { + switch (self.value()) { + case Logic::_0: + case Logic::_1: + return self; + case Logic::L: + return Logic(Logic::_0); + case Logic::H: + return Logic(Logic::_1); + case Logic::W: + return Logic(Logic::X); + default: + return self; + } + } + auto r = self.resolve(m); + if (!r) { + throw std::invalid_argument( + "Logic value is not resolvable under the given method" + ); + } + return Logic(*r); } ) - .def("resolve", &Logic::resolve) .def("__copy__", [](Logic const& self) { return Logic(self); }) .def("__deepcopy__", [](Logic const& self, nb::dict /* memo */) { return Logic(self); @@ -150,7 +179,7 @@ void register_logic(nb::module_& m) { ) .def("__init__", [](Bit* self, long long value) { new (self) Bit(to_bit(value)); }) .def("__str__", [](Bit const& self) { return to_string(self); }) - .def("__index__", &to_int) + .def("__index__", [](Bit const& self) { return to_int(self); }) .def("__bool__", [](Bit const& self) { return to_int(self) != 0; }) .def( "__repr__", @@ -225,14 +254,14 @@ void register_logic(nb::module_& m) { .def( "__invert__", [](Bit const& self) { return ~self; }, nb::is_operator() ) - .def_prop_ro("is_resolvable", &Bit::is_resolvable) + .def_prop_ro("is_resolvable", [](Bit const&) { return true; }) .def( "resolve", [](Bit const& self, std::string_view method) { - return self.resolve(string_to_resolve_method(method)); + (void)string_to_resolve_method(method); + return self; } ) - .def("resolve", &Bit::resolve) .def("__copy__", [](Bit const& self) { return Bit(self); }) .def("__deepcopy__", [](Bit const& self, nb::dict /* memo */) { return Bit(self); diff --git a/tests/cpp/test_logic.cpp b/tests/cpp/test_logic.cpp index a8f3e72..a2e3bcb 100644 --- a/tests/cpp/test_logic.cpp +++ b/tests/cpp/test_logic.cpp @@ -95,24 +95,56 @@ TEST(TestBit, BitConversions) { // Test Logic bool conversions TEST(TestLogic, LogicBoolConversions) { // Convertible to true - EXPECT_EQ('1'_l.is_resolvable(), true); - EXPECT_EQ('H'_l.is_resolvable(), true); + EXPECT_EQ('1'_l.resolve(ResolveMethod::WEAK).has_value(), true); + EXPECT_EQ('H'_l.resolve(ResolveMethod::WEAK).has_value(), true); // Convertible to false - EXPECT_EQ('0'_l.is_resolvable(), true); - EXPECT_EQ('L'_l.is_resolvable(), true); + EXPECT_EQ('0'_l.resolve(ResolveMethod::WEAK).has_value(), true); + EXPECT_EQ('L'_l.resolve(ResolveMethod::WEAK).has_value(), true); // Non-convertible values - EXPECT_EQ('X'_l.is_resolvable(), false); - EXPECT_EQ('Z'_l.is_resolvable(), false); - EXPECT_EQ('U'_l.is_resolvable(), false); - EXPECT_EQ('W'_l.is_resolvable(), false); - EXPECT_EQ('-'_l.is_resolvable(), false); + EXPECT_EQ('X'_l.resolve(ResolveMethod::WEAK).has_value(), false); + EXPECT_EQ('Z'_l.resolve(ResolveMethod::WEAK).has_value(), false); + EXPECT_EQ('U'_l.resolve(ResolveMethod::WEAK).has_value(), false); + EXPECT_EQ('W'_l.resolve(ResolveMethod::WEAK).has_value(), false); + EXPECT_EQ('-'_l.resolve(ResolveMethod::WEAK).has_value(), false); } TEST(TestBit, BitBoolConversions) { - EXPECT_EQ('0'_b.is_resolvable(), true); - EXPECT_EQ('1'_b.is_resolvable(), true); + EXPECT_EQ('0'_b.resolve(ResolveMethod::WEAK).has_value(), true); + EXPECT_EQ('1'_b.resolve(ResolveMethod::WEAK).has_value(), true); +} + +TEST(TestLogic, LogicResolvabilityUnderEachMethod) { + // ERROR: only 0/1 are resolvable. + EXPECT_TRUE('0'_l.resolve(ResolveMethod::ERROR).has_value()); + EXPECT_TRUE('1'_l.resolve(ResolveMethod::ERROR).has_value()); + EXPECT_FALSE('L'_l.resolve(ResolveMethod::ERROR).has_value()); + EXPECT_FALSE('H'_l.resolve(ResolveMethod::ERROR).has_value()); + EXPECT_FALSE('X'_l.resolve(ResolveMethod::ERROR).has_value()); + EXPECT_FALSE('Z'_l.resolve(ResolveMethod::ERROR).has_value()); + EXPECT_FALSE('U'_l.resolve(ResolveMethod::ERROR).has_value()); + EXPECT_FALSE('W'_l.resolve(ResolveMethod::ERROR).has_value()); + EXPECT_FALSE('-'_l.resolve(ResolveMethod::ERROR).has_value()); + + // WEAK: 0/1/L/H pass; metavalues (incl. W) do not. + EXPECT_TRUE('0'_l.resolve(ResolveMethod::WEAK).has_value()); + EXPECT_TRUE('1'_l.resolve(ResolveMethod::WEAK).has_value()); + EXPECT_TRUE('L'_l.resolve(ResolveMethod::WEAK).has_value()); + EXPECT_TRUE('H'_l.resolve(ResolveMethod::WEAK).has_value()); + EXPECT_FALSE('X'_l.resolve(ResolveMethod::WEAK).has_value()); + EXPECT_FALSE('Z'_l.resolve(ResolveMethod::WEAK).has_value()); + EXPECT_FALSE('U'_l.resolve(ResolveMethod::WEAK).has_value()); + EXPECT_FALSE('W'_l.resolve(ResolveMethod::WEAK).has_value()); + EXPECT_FALSE('-'_l.resolve(ResolveMethod::WEAK).has_value()); + + // ZEROS / ONES / RANDOM: always resolvable (no value can throw). + for (auto m : {ResolveMethod::ZEROS, ResolveMethod::ONES, ResolveMethod::RANDOM}) { + EXPECT_TRUE('0'_l.resolve(m).has_value()); + EXPECT_TRUE('X'_l.resolve(m).has_value()); + EXPECT_TRUE('U'_l.resolve(m).has_value()); + EXPECT_TRUE('-'_l.resolve(m).has_value()); + } } // Test Logic string conversions @@ -226,82 +258,83 @@ TEST(TestBit, BitInvert) { // Test Logic resolve with different methods TEST(TestLogic, LogicResolve) { - // Test WEAK resolution - EXPECT_EQ('U'_l.resolve(ResolveMethod::WEAK), 'U'_l); - EXPECT_EQ('X'_l.resolve(ResolveMethod::WEAK), 'X'_l); - EXPECT_EQ('0'_l.resolve(ResolveMethod::WEAK), '0'_l); - EXPECT_EQ('1'_l.resolve(ResolveMethod::WEAK), '1'_l); - EXPECT_EQ('Z'_l.resolve(ResolveMethod::WEAK), 'Z'_l); - EXPECT_EQ('W'_l.resolve(ResolveMethod::WEAK), 'X'_l); - EXPECT_EQ('L'_l.resolve(ResolveMethod::WEAK), '0'_l); - EXPECT_EQ('H'_l.resolve(ResolveMethod::WEAK), '1'_l); - EXPECT_EQ('-'_l.resolve(ResolveMethod::WEAK), '-'_l); + // Test WEAK resolution: 0/1 pass through; L/H map to 0/1; everything else + // returns nullopt (the "not resolvable under WEAK" tier matches L/H plus + // 0/1). optional compares equal to Bit when engaged. + EXPECT_EQ('0'_l.resolve(ResolveMethod::WEAK), '0'_b); + EXPECT_EQ('1'_l.resolve(ResolveMethod::WEAK), '1'_b); + EXPECT_EQ('L'_l.resolve(ResolveMethod::WEAK), '0'_b); + EXPECT_EQ('H'_l.resolve(ResolveMethod::WEAK), '1'_b); + EXPECT_EQ('U'_l.resolve(ResolveMethod::WEAK), std::nullopt); + EXPECT_EQ('X'_l.resolve(ResolveMethod::WEAK), std::nullopt); + EXPECT_EQ('Z'_l.resolve(ResolveMethod::WEAK), std::nullopt); + EXPECT_EQ('W'_l.resolve(ResolveMethod::WEAK), std::nullopt); + EXPECT_EQ('-'_l.resolve(ResolveMethod::WEAK), std::nullopt); // Test ZEROS resolution - EXPECT_EQ('U'_l.resolve(ResolveMethod::ZEROS), '0'_l); - EXPECT_EQ('X'_l.resolve(ResolveMethod::ZEROS), '0'_l); - EXPECT_EQ('0'_l.resolve(ResolveMethod::ZEROS), '0'_l); - EXPECT_EQ('1'_l.resolve(ResolveMethod::ZEROS), '1'_l); - EXPECT_EQ('Z'_l.resolve(ResolveMethod::ZEROS), '0'_l); - EXPECT_EQ('W'_l.resolve(ResolveMethod::ZEROS), '0'_l); - EXPECT_EQ('L'_l.resolve(ResolveMethod::ZEROS), '0'_l); - EXPECT_EQ('H'_l.resolve(ResolveMethod::ZEROS), '1'_l); - EXPECT_EQ('-'_l.resolve(ResolveMethod::ZEROS), '0'_l); + EXPECT_EQ('U'_l.resolve(ResolveMethod::ZEROS), '0'_b); + EXPECT_EQ('X'_l.resolve(ResolveMethod::ZEROS), '0'_b); + EXPECT_EQ('0'_l.resolve(ResolveMethod::ZEROS), '0'_b); + EXPECT_EQ('1'_l.resolve(ResolveMethod::ZEROS), '1'_b); + EXPECT_EQ('Z'_l.resolve(ResolveMethod::ZEROS), '0'_b); + EXPECT_EQ('W'_l.resolve(ResolveMethod::ZEROS), '0'_b); + EXPECT_EQ('L'_l.resolve(ResolveMethod::ZEROS), '0'_b); + EXPECT_EQ('H'_l.resolve(ResolveMethod::ZEROS), '1'_b); + EXPECT_EQ('-'_l.resolve(ResolveMethod::ZEROS), '0'_b); // Test ONES resolution - EXPECT_EQ('U'_l.resolve(ResolveMethod::ONES), '1'_l); - EXPECT_EQ('X'_l.resolve(ResolveMethod::ONES), '1'_l); - EXPECT_EQ('0'_l.resolve(ResolveMethod::ONES), '0'_l); - EXPECT_EQ('1'_l.resolve(ResolveMethod::ONES), '1'_l); - EXPECT_EQ('Z'_l.resolve(ResolveMethod::ONES), '1'_l); - EXPECT_EQ('W'_l.resolve(ResolveMethod::ONES), '1'_l); - EXPECT_EQ('L'_l.resolve(ResolveMethod::ONES), '0'_l); - EXPECT_EQ('H'_l.resolve(ResolveMethod::ONES), '1'_l); - EXPECT_EQ('-'_l.resolve(ResolveMethod::ONES), '1'_l); + EXPECT_EQ('U'_l.resolve(ResolveMethod::ONES), '1'_b); + EXPECT_EQ('X'_l.resolve(ResolveMethod::ONES), '1'_b); + EXPECT_EQ('0'_l.resolve(ResolveMethod::ONES), '0'_b); + EXPECT_EQ('1'_l.resolve(ResolveMethod::ONES), '1'_b); + EXPECT_EQ('Z'_l.resolve(ResolveMethod::ONES), '1'_b); + EXPECT_EQ('W'_l.resolve(ResolveMethod::ONES), '1'_b); + EXPECT_EQ('L'_l.resolve(ResolveMethod::ONES), '0'_b); + EXPECT_EQ('H'_l.resolve(ResolveMethod::ONES), '1'_b); + EXPECT_EQ('-'_l.resolve(ResolveMethod::ONES), '1'_b); // Test RANDOM resolution auto resolved_u = 'U'_l.resolve(ResolveMethod::RANDOM); - EXPECT_TRUE(resolved_u == '0'_l || resolved_u == '1'_l); + EXPECT_TRUE(resolved_u == '0'_b || resolved_u == '1'_b); auto resolved_x = 'X'_l.resolve(ResolveMethod::RANDOM); - EXPECT_TRUE(resolved_x == '0'_l || resolved_x == '1'_l); + EXPECT_TRUE(resolved_x == '0'_b || resolved_x == '1'_b); - EXPECT_EQ('0'_l.resolve(ResolveMethod::RANDOM), '0'_l); - EXPECT_EQ('1'_l.resolve(ResolveMethod::RANDOM), '1'_l); + EXPECT_EQ('0'_l.resolve(ResolveMethod::RANDOM), '0'_b); + EXPECT_EQ('1'_l.resolve(ResolveMethod::RANDOM), '1'_b); auto resolved_z = 'Z'_l.resolve(ResolveMethod::RANDOM); - EXPECT_TRUE(resolved_z == '0'_l || resolved_z == '1'_l); + EXPECT_TRUE(resolved_z == '0'_b || resolved_z == '1'_b); auto resolved_w = 'W'_l.resolve(ResolveMethod::RANDOM); - EXPECT_TRUE(resolved_w == '0'_l || resolved_w == '1'_l); + EXPECT_TRUE(resolved_w == '0'_b || resolved_w == '1'_b); - EXPECT_EQ('L'_l.resolve(ResolveMethod::RANDOM), '0'_l); - EXPECT_EQ('H'_l.resolve(ResolveMethod::RANDOM), '1'_l); + EXPECT_EQ('L'_l.resolve(ResolveMethod::RANDOM), '0'_b); + EXPECT_EQ('H'_l.resolve(ResolveMethod::RANDOM), '1'_b); auto resolved_dc = '-'_l.resolve(ResolveMethod::RANDOM); - EXPECT_TRUE(resolved_dc == '0'_l || resolved_dc == '1'_l); + EXPECT_TRUE(resolved_dc == '0'_b || resolved_dc == '1'_b); // Test ERROR resolution - EXPECT_EQ('0'_l.resolve(ResolveMethod::ERROR), '0'_l); - EXPECT_EQ('1'_l.resolve(ResolveMethod::ERROR), '1'_l); - EXPECT_THROW((void)'U'_l.resolve(ResolveMethod::ERROR), std::invalid_argument); - EXPECT_THROW((void)'X'_l.resolve(ResolveMethod::ERROR), std::invalid_argument); - EXPECT_THROW((void)'Z'_l.resolve(ResolveMethod::ERROR), std::invalid_argument); - EXPECT_THROW((void)'W'_l.resolve(ResolveMethod::ERROR), std::invalid_argument); - EXPECT_THROW((void)'L'_l.resolve(ResolveMethod::ERROR), std::invalid_argument); - EXPECT_THROW((void)'H'_l.resolve(ResolveMethod::ERROR), std::invalid_argument); - EXPECT_THROW((void)'-'_l.resolve(ResolveMethod::ERROR), std::invalid_argument); - - // Out-of-range ResolveMethod hits the outer `default:` arm. - EXPECT_THROW( - (void)'0'_l.resolve(static_cast(99)), std::invalid_argument - ); + EXPECT_EQ('0'_l.resolve(ResolveMethod::ERROR), '0'_b); + EXPECT_EQ('1'_l.resolve(ResolveMethod::ERROR), '1'_b); + EXPECT_EQ('U'_l.resolve(ResolveMethod::ERROR), std::nullopt); + EXPECT_EQ('X'_l.resolve(ResolveMethod::ERROR), std::nullopt); + EXPECT_EQ('Z'_l.resolve(ResolveMethod::ERROR), std::nullopt); + EXPECT_EQ('W'_l.resolve(ResolveMethod::ERROR), std::nullopt); + EXPECT_EQ('L'_l.resolve(ResolveMethod::ERROR), std::nullopt); + EXPECT_EQ('H'_l.resolve(ResolveMethod::ERROR), std::nullopt); + EXPECT_EQ('-'_l.resolve(ResolveMethod::ERROR), std::nullopt); + + // Out-of-range ResolveMethod hits the outer fall-through arm and returns + // nullopt (the unified API doesn't throw on bad methods either). + EXPECT_EQ('0'_l.resolve(static_cast(99)), std::nullopt); } TEST(TestBit, BitResolve) { - // Every Bit value is resolvable, so resolve() is a no-op regardless of - // method -- including ERROR (which throws on Logic for non-binary values) - // and out-of-range method values. + // Every Bit value is resolvable, so resolve() always returns an engaged + // optional equal to the input -- including ERROR (which is nullopt on + // Logic for non-binary values) and out-of-range method values. EXPECT_EQ('0'_b.resolve(ResolveMethod::ERROR), '0'_b); EXPECT_EQ('1'_b.resolve(ResolveMethod::ERROR), '1'_b); @@ -323,6 +356,26 @@ TEST(TestBit, BitResolve) { EXPECT_EQ('1'_b.resolve(static_cast(99)), '1'_b); } +// No-arg resolve() defaults to WEAK -- mirroring the LogicArrayMixin shortcut +// so user code can write `r.resolve()` for the common synthesizable-values +// case. +TEST(TestLogic, LogicResolveNoArgDefaultsToWeak) { + EXPECT_EQ('0'_l.resolve(), '0'_b); + EXPECT_EQ('1'_l.resolve(), '1'_b); + EXPECT_EQ('L'_l.resolve(), '0'_b); + EXPECT_EQ('H'_l.resolve(), '1'_b); + EXPECT_EQ('X'_l.resolve(), std::nullopt); + EXPECT_EQ('U'_l.resolve(), std::nullopt); + EXPECT_EQ('Z'_l.resolve(), std::nullopt); + EXPECT_EQ('W'_l.resolve(), std::nullopt); + EXPECT_EQ('-'_l.resolve(), std::nullopt); +} + +TEST(TestBit, BitResolveNoArgAlwaysEngaged) { + EXPECT_EQ('0'_b.resolve(), '0'_b); + EXPECT_EQ('1'_b.resolve(), '1'_b); +} + // Stores values in std::vector to force runtime evaluation of conversions. // These can be evaluated at compile time and reduce observed coverage. TEST(TestLogic, RuntimeIntAndBitConversions) { @@ -341,23 +394,23 @@ TEST(TestLogic, RuntimeIntAndBitConversions) { EXPECT_EQ(bits[1].resolve(ResolveMethod::ZEROS), '1'_b); } -// Test Logic is_resolvable +// Test Logic resolvability via resolve(...).has_value() TEST(TestLogic, LogicIsResolvable) { - EXPECT_TRUE('0'_l.is_resolvable()); - EXPECT_TRUE('1'_l.is_resolvable()); - EXPECT_TRUE('L'_l.is_resolvable()); - EXPECT_TRUE('H'_l.is_resolvable()); + EXPECT_TRUE('0'_l.resolve(ResolveMethod::WEAK).has_value()); + EXPECT_TRUE('1'_l.resolve(ResolveMethod::WEAK).has_value()); + EXPECT_TRUE('L'_l.resolve(ResolveMethod::WEAK).has_value()); + EXPECT_TRUE('H'_l.resolve(ResolveMethod::WEAK).has_value()); - EXPECT_FALSE('U'_l.is_resolvable()); - EXPECT_FALSE('X'_l.is_resolvable()); - EXPECT_FALSE('Z'_l.is_resolvable()); - EXPECT_FALSE('W'_l.is_resolvable()); - EXPECT_FALSE('-'_l.is_resolvable()); + EXPECT_FALSE('U'_l.resolve(ResolveMethod::WEAK).has_value()); + EXPECT_FALSE('X'_l.resolve(ResolveMethod::WEAK).has_value()); + EXPECT_FALSE('Z'_l.resolve(ResolveMethod::WEAK).has_value()); + EXPECT_FALSE('W'_l.resolve(ResolveMethod::WEAK).has_value()); + EXPECT_FALSE('-'_l.resolve(ResolveMethod::WEAK).has_value()); } TEST(TestBit, BitIsResolvable) { - EXPECT_TRUE('0'_b.is_resolvable()); - EXPECT_TRUE('1'_b.is_resolvable()); + EXPECT_TRUE('0'_b.resolve(ResolveMethod::WEAK).has_value()); + EXPECT_TRUE('1'_b.resolve(ResolveMethod::WEAK).has_value()); } TEST(TestLogic, LogicIsHashable) { diff --git a/tests/cpp/test_logic_array.cpp b/tests/cpp/test_logic_array.cpp index 48d6aa7..9b24235 100644 --- a/tests/cpp/test_logic_array.cpp +++ b/tests/cpp/test_logic_array.cpp @@ -303,24 +303,24 @@ TEST(TestLogicArray, ToStringEmpty) { EXPECT_EQ(to_string(a), ""); } -// -- is_resolvable on arrays ------------------------------------------------ +// -- resolvability query on arrays ------------------------------------------ -TEST(TestLogicArray, IsResolvableTrue) { +TEST(TestLogicArray, ResolveEngagedOnResolvable) { auto a = to_logic_array("01LH"); - EXPECT_TRUE(a.is_resolvable()); + EXPECT_TRUE(a.resolve(ResolveMethod::WEAK).has_value()); } -TEST(TestLogicArray, IsResolvableFalse) { - EXPECT_FALSE(to_logic_array("01X0").is_resolvable()); - EXPECT_FALSE(to_logic_array("Z").is_resolvable()); - EXPECT_FALSE(to_logic_array("U").is_resolvable()); - EXPECT_FALSE(to_logic_array("W").is_resolvable()); - EXPECT_FALSE(to_logic_array("-").is_resolvable()); +TEST(TestLogicArray, ResolveNulloptOnMetavalue) { + EXPECT_FALSE(to_logic_array("01X0").resolve(ResolveMethod::WEAK).has_value()); + EXPECT_FALSE(to_logic_array("Z").resolve(ResolveMethod::WEAK).has_value()); + EXPECT_FALSE(to_logic_array("U").resolve(ResolveMethod::WEAK).has_value()); + EXPECT_FALSE(to_logic_array("W").resolve(ResolveMethod::WEAK).has_value()); + EXPECT_FALSE(to_logic_array("-").resolve(ResolveMethod::WEAK).has_value()); } -TEST(TestLogicArray, IsResolvableEmpty) { +TEST(TestLogicArray, ResolveEngagedOnEmpty) { auto a = to_logic_array(""); - EXPECT_TRUE(a.is_resolvable()); + EXPECT_TRUE(a.resolve(ResolveMethod::WEAK).has_value()); } // -- resolve on arrays ------------------------------------------------------ @@ -328,30 +328,40 @@ TEST(TestLogicArray, IsResolvableEmpty) { TEST(TestLogicArray, ResolveZeros) { auto a = to_logic_array("01XZULWH-"); auto b = a.resolve(ResolveMethod::ZEROS); - EXPECT_EQ(to_string(b), "010000010"); + EXPECT_EQ(to_string(*b), "010000010"); } TEST(TestLogicArray, ResolveOnes) { auto a = to_logic_array("01XZULWH-"); auto b = a.resolve(ResolveMethod::ONES); - EXPECT_EQ(to_string(b), "011110111"); + EXPECT_EQ(to_string(*b), "011110111"); } -TEST(TestLogicArray, ResolveWeak) { - auto a = to_logic_array("01XZULWH-"); +TEST(TestLogicArray, ResolveWeakAcceptsResolvable) { + // WEAK passes 0/1/L/H -> 0/1/0/1 and returns nullopt on the rest. The + // input must contain only resolvable-under-WEAK values for the result to + // be engaged. + auto a = to_logic_array("01LH"); auto b = a.resolve(ResolveMethod::WEAK); - EXPECT_EQ(to_string(b), "01XZU0X1-"); + EXPECT_EQ(to_string(*b), "0101"); +} + +TEST(TestLogicArray, ResolveWeakReturnsNulloptOnMetavalue) { + // Even a single non-resolvable value makes the whole array's resolve + // return nullopt (build-then-drop). + auto a = to_logic_array("01X"); + EXPECT_FALSE(a.resolve(ResolveMethod::WEAK).has_value()); } TEST(TestLogicArray, ResolveError) { auto a = to_logic_array("01X"); - EXPECT_THROW(a.resolve(ResolveMethod::ERROR), std::invalid_argument); + EXPECT_FALSE(a.resolve(ResolveMethod::ERROR).has_value()); } TEST(TestLogicArray, ResolveErrorPass) { auto a = to_logic_array("01"); auto b = a.resolve(ResolveMethod::ERROR); - EXPECT_EQ(to_string(b), "01"); + EXPECT_EQ(to_string(*b), "01"); } TEST(TestLogicArray, ResolveStaticReturnsStaticArray) { @@ -359,16 +369,31 @@ TEST(TestLogicArray, ResolveStaticReturnsStaticArray) { auto b = a.resolve(ResolveMethod::ZEROS); // Static-bound input -> static-bound output of matching range. This is the // payoff of memberizing resolve on the specializations: the result type - // preserves Self's static range when available. - static_assert(std::is_same_v>); - EXPECT_EQ(to_string(b), "0100"); + // preserves Self's static range when available, and resolve always returns + // an optional Bit-valued container. + static_assert( + std::is_same_v>> + ); + EXPECT_EQ(to_string(*b), "0100"); +} + +// No-arg resolve() defaults to WEAK -- engages iff every element is 0/1/L/H, +// matching the scalar Logic::resolve() shortcut. +TEST(TestLogicArray, ResolveNoArgDefaultsToWeak) { + auto resolvable = to_logic_array("01LH"); + auto a = resolvable.resolve(); + ASSERT_TRUE(a.has_value()); + EXPECT_EQ(to_string(*a), "0101"); + + auto mixed = to_logic_array("01X"); + EXPECT_FALSE(mixed.resolve().has_value()); } // -- Slice resolvability --------------------------------------------------- // // The constrained partial specs of StaticArraySlice and ArraySlice inherit the // LogicArrayMixin too, so slices of LogicArray/BitArray/LogicVector/etc -// have is_resolvable() and resolve(method) members. Sub-slicing preserves +// have resolve(method) members returning std::optional. Sub-slicing preserves // the mixin via outer-name resolution in the slice impl. TEST(TestLogicArray, DynSliceIsResolvable) { @@ -376,37 +401,44 @@ TEST(TestLogicArray, DynSliceIsResolvable) { // a[2]='0', a[1]='1', a[0]='X'. auto a = to_logic_array("01X"); auto s_full = a[{2, 0}]; - EXPECT_FALSE(s_full.is_resolvable()); // covers the X at a[0] + EXPECT_FALSE(s_full.resolve(ResolveMethod::WEAK).has_value()); // covers the X at a[0] auto s_excl_x = a[{2, 1}]; - EXPECT_TRUE(s_excl_x.is_resolvable()); // covers '0' and '1' only + EXPECT_TRUE( + s_excl_x.resolve(ResolveMethod::WEAK).has_value() + ); // covers '0' and '1' only } TEST(TestLogicArray, DynSliceResolveReturnsVector) { auto a = to_logic_array("01XZ"); auto s = a[{3, 0}]; auto r = s.resolve(ResolveMethod::ZEROS); - static_assert(std::is_same_v>); - EXPECT_EQ(to_string(r), "0100"); + static_assert(std::is_same_v>); + EXPECT_EQ(to_string(*r), "0100"); } TEST(TestLogicArray, StaticSliceResolveReturnsStaticArray) { auto a = "01XZ"_l; // LogicArray auto s = a.slice(); auto r = s.resolve(ResolveMethod::ZEROS); - static_assert(std::is_same_v>); - EXPECT_EQ(to_string(r), "10"); // X->0, 1->1; slice was {X, 1} in storage order + static_assert( + std::is_same_v>> + ); + EXPECT_EQ(to_string(*r), "10"); // X->0, 1->1; slice was {X, 1} in storage order } TEST(TestLogicArray, StaticSliceIsResolvable) { - // Exercises StaticArraySlice, R2>::is_resolvable() -- the constrained - // partial spec from logic_array.hpp. Without this test a regression that drops - // the mixin from the static slice spec would only be caught when user code - // calls .is_resolvable() on a sliced LogicArray and fails to compile. + // Exercises StaticArraySlice, R2>::resolve(method) -- the + // constrained partial spec from logic_array.hpp. Without this test a + // regression that drops the mixin from the static slice spec would only be + // caught when user code calls .resolve(method).has_value() on a sliced + // LogicArray and fails to compile. auto a = "01XZ"_l; // a[3]='0', a[2]='1', a[1]='X', a[0]='Z' auto s_with_x = a.slice(); - EXPECT_FALSE(s_with_x.is_resolvable()); // contains '1' and 'X' + EXPECT_FALSE( + s_with_x.resolve(ResolveMethod::WEAK).has_value() + ); // contains '1' and 'X' auto s_clean = a.slice(); - EXPECT_TRUE(s_clean.is_resolvable()); // '0' and '1' + EXPECT_TRUE(s_clean.resolve(ResolveMethod::WEAK).has_value()); // '0' and '1' } TEST(TestLogicArray, ConstOwnerDynSliceHasMixin) { @@ -422,19 +454,19 @@ TEST(TestLogicArray, ConstOwnerDynSliceHasMixin) { std::same_as const>>, "const Logic owner must produce ArraySlice>" ); - EXPECT_TRUE(s.is_resolvable()); // all elements are 0/1/L/H + EXPECT_TRUE(s.resolve(ResolveMethod::WEAK).has_value()); // all elements are 0/1/L/H auto r = s.resolve(ResolveMethod::WEAK); - EXPECT_EQ(to_string(r), "0101"); // L->0, H->1 + EXPECT_EQ(to_string(*r), "0101"); // L->0, H->1 } TEST(TestLogicArray, SubSlicePreservesMixin) { auto a = to_logic_array("01XZ"); auto s = a[{3, 0}]; auto sub = s[{2, 1}]; // sub-slice via ArraySliceImpl::operator[] - // The sub-slice still has is_resolvable() -- the impl returns + // The sub-slice still has resolve(method) -- the impl returns // ArraySlice by outer name, which resolves to the constrained // partial spec when the element type is Logic. - EXPECT_FALSE(sub.is_resolvable()); + EXPECT_FALSE(sub.resolve(ResolveMethod::WEAK).has_value()); } // -- index / rindex on Logic/Bit arrays ----------------------------------- @@ -1146,54 +1178,6 @@ TEST(TestBitArray, ToBitArrayInvalidChar) { EXPECT_THROW(to_bit_array("2"), std::invalid_argument); } -// -- to_bit_array from range of Logic -------------------------------------- - -TEST(TestBitArray, ToBitArrayFromLogicRange) { - auto a = to_logic_array("01LH"); - auto b = to_bit_array(a); - static_assert(std::is_same_v); - EXPECT_EQ(b.range(), Range(3, Direction::DOWNTO, 0)); - EXPECT_EQ(b[3], '0'_b); - EXPECT_EQ(b[2], '1'_b); - EXPECT_EQ(b[1], '0'_b); - EXPECT_EQ(b[0], '1'_b); -} - -TEST(TestBitArray, ToBitArrayFromLogicRangeNonResolvable) { - EXPECT_THROW(to_bit_array(to_logic_array("X")), std::invalid_argument); - EXPECT_THROW(to_bit_array(to_logic_array("Z")), std::invalid_argument); - EXPECT_THROW(to_bit_array(to_logic_array("U")), std::invalid_argument); - EXPECT_THROW(to_bit_array(to_logic_array("W")), std::invalid_argument); - EXPECT_THROW(to_bit_array(to_logic_array("-")), std::invalid_argument); -} - -TEST(TestBitArray, ToBitArrayFromStdVector) { - // Exercises the sized_range overload with a non-coconext range. std::vector - // is the canonical non-coconext sized range; this verifies the constraint - // accepts it and the single-pass fused walk yields the same result as the - // coconext-array form above. - std::vector const v{'0'_l, '1'_l, 'L'_l, 'H'_l}; - auto b = to_bit_array(v); - static_assert(std::is_same_v); - EXPECT_EQ(b.range(), Range(3, Direction::DOWNTO, 0)); - EXPECT_EQ(b[3], '0'_b); - EXPECT_EQ(b[2], '1'_b); - EXPECT_EQ(b[1], '0'_b); // L -> 0 - EXPECT_EQ(b[0], '1'_b); // H -> 1 -} - -TEST(TestBitArray, ToBitArrayFromStdVectorNonResolvable) { - std::vector const v{'0'_l, '1'_l, 'X'_l}; - EXPECT_THROW(to_bit_array(v), std::invalid_argument); -} - -TEST(TestBitArray, ToBitArrayFromStaticLogicArray) { - auto a = "01LH"_l; - auto b = to_bit_array(a); - static_assert(std::is_same_v); - EXPECT_EQ(to_string(b), "0101"); -} - // -- to_string on BitArray ------------------------------------------------- TEST(TestBitArray, ToStringBit) { @@ -1206,20 +1190,22 @@ TEST(TestBitArray, ToStringStaticBit) { EXPECT_EQ(to_string(a), "0101"); } -// -- is_resolvable on BitArray --------------------------------------------- +// -- resolve on BitArray (always engaged) ---------------------------------- -TEST(TestBitArray, IsResolvableAlwaysTrue) { +TEST(TestBitArray, ResolveAlwaysEngaged) { auto a = to_bit_array("0110"); - EXPECT_TRUE(a.is_resolvable()); + EXPECT_TRUE(a.resolve(ResolveMethod::WEAK).has_value()); auto empty = to_bit_array(""); - EXPECT_TRUE(empty.is_resolvable()); + EXPECT_TRUE(empty.resolve(ResolveMethod::WEAK).has_value()); } // -- resolve on BitArray --------------------------------------------------- TEST(TestBitArray, ResolveBitIsIdentity) { auto a = to_bit_array("0110"); - static_assert(std::is_same_v); + static_assert( + std::is_same_v> + ); for (auto method : {ResolveMethod::ZEROS, ResolveMethod::ONES, @@ -1227,7 +1213,7 @@ TEST(TestBitArray, ResolveBitIsIdentity) { ResolveMethod::ERROR, ResolveMethod::RANDOM}) { - EXPECT_EQ(to_string(a.resolve(method)), "0110"); + EXPECT_EQ(to_string(*a.resolve(method)), "0110"); } }