From ec6c1bd7eb84f3692cfbce668de328442b5fdb75 Mon Sep 17 00:00:00 2001 From: Kaleb Barrett Date: Sat, 20 Jun 2026 11:59:08 -0400 Subject: [PATCH 1/5] Make resolve(WEAK) throw on metavalues The WEAK resolve method used to map W to X and pass U/X/Z/- through unchanged. That left WEAK as a "best effort" pass that quietly returned non-resolvable values, which is at odds with the obvious reading of "the method either resolves to 0/1 or fails." After this change, WEAK accepts only 0/1/L/H (the strengths that have a meaningful 0/1 interpretation) and throws std::invalid_argument on everything else. Callers that want X/Z/U/W/- to silently pick a value already have ZEROS/ONES/RANDOM. Callers that want strict 0/1 already have ERROR. WEAK now slots between them as "accept HDL weak strengths, reject metavalues." --- cpp/src/logic.cpp | 4 +--- nanobind/src/types/bind_logic.cpp | 22 +++++++++++++++++++++- tests/cpp/test_logic.cpp | 13 +++++++------ tests/cpp/test_logic_array.cpp | 14 +++++++++++--- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/cpp/src/logic.cpp b/cpp/src/logic.cpp index c638349..fd27a44 100644 --- a/cpp/src/logic.cpp +++ b/cpp/src/logic.cpp @@ -27,10 +27,8 @@ Logic Logic::resolve(ResolveMethod method) const { return _0; case H: return _1; - case W: - return X; default: - return *this; + throw std::invalid_argument("Logic value is not resolvable under WEAK"); } case ResolveMethod::ZEROS: switch (value_) { diff --git a/nanobind/src/types/bind_logic.cpp b/nanobind/src/types/bind_logic.cpp index c3dd029..c239706 100644 --- a/nanobind/src/types/bind_logic.cpp +++ b/nanobind/src/types/bind_logic.cpp @@ -130,7 +130,27 @@ void register_logic(nb::module_& m) { .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++ throws + // 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; + } + } + return self.resolve(m); } ) .def("resolve", &Logic::resolve) diff --git a/tests/cpp/test_logic.cpp b/tests/cpp/test_logic.cpp index a8f3e72..613bade 100644 --- a/tests/cpp/test_logic.cpp +++ b/tests/cpp/test_logic.cpp @@ -226,16 +226,17 @@ 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); + // Test WEAK resolution: 0/1 pass through; L/H map to 0/1; everything else + // throws (the "not resolvable under WEAK" tier matches L/H plus 0/1). 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); + EXPECT_THROW((void)'U'_l.resolve(ResolveMethod::WEAK), std::invalid_argument); + EXPECT_THROW((void)'X'_l.resolve(ResolveMethod::WEAK), std::invalid_argument); + EXPECT_THROW((void)'Z'_l.resolve(ResolveMethod::WEAK), std::invalid_argument); + EXPECT_THROW((void)'W'_l.resolve(ResolveMethod::WEAK), std::invalid_argument); + EXPECT_THROW((void)'-'_l.resolve(ResolveMethod::WEAK), std::invalid_argument); // Test ZEROS resolution EXPECT_EQ('U'_l.resolve(ResolveMethod::ZEROS), '0'_l); diff --git a/tests/cpp/test_logic_array.cpp b/tests/cpp/test_logic_array.cpp index 48d6aa7..ec4fa41 100644 --- a/tests/cpp/test_logic_array.cpp +++ b/tests/cpp/test_logic_array.cpp @@ -337,10 +337,18 @@ TEST(TestLogicArray, ResolveOnes) { 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 throws on the rest. The input must + // contain only resolvable-under-WEAK values for the call to succeed. + 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, ResolveWeakThrowsOnMetavalue) { + // Even a single non-resolvable value makes the whole array throw. + auto a = to_logic_array("01X"); + EXPECT_THROW(a.resolve(ResolveMethod::WEAK), std::invalid_argument); } TEST(TestLogicArray, ResolveError) { From c9a6ced62580cdbb20f38a4ff8bb22f8b6af0f42 Mon Sep 17 00:00:00 2001 From: Kaleb Barrett Date: Sat, 20 Jun 2026 12:06:47 -0400 Subject: [PATCH 2/5] Support passing a ResolveMethod to is_resolvable is_resolvable() previously hard-coded WEAK. This makes it not particularly useful as a predicate for "will resolve() throw?" Also it wasn't a good predicate for WEAK either since WEAK still returned a value when it got an unresolvable metavalue. --- cpp/include/coconext/types/logic.hpp | 6 +- cpp/include/coconext/types/logic_array.hpp | 16 ++--- cpp/src/logic.cpp | 14 ++++ nanobind/src/types/bind_logic.cpp | 10 ++- tests/cpp/test_logic.cpp | 76 +++++++++++++++------- tests/cpp/test_logic_array.cpp | 42 ++++++------ 6 files changed, 107 insertions(+), 57 deletions(-) diff --git a/cpp/include/coconext/types/logic.hpp b/cpp/include/coconext/types/logic.hpp index 1174ae5..0cb2fca 100644 --- a/cpp/include/coconext/types/logic.hpp +++ b/cpp/include/coconext/types/logic.hpp @@ -37,9 +37,7 @@ 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; - } + bool is_resolvable(ResolveMethod method) const noexcept; Logic resolve(ResolveMethod method) const; @@ -59,7 +57,7 @@ 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 bool is_resolvable(ResolveMethod) const noexcept { return true; } constexpr Bit resolve(ResolveMethod) const noexcept { return *this; } diff --git a/cpp/include/coconext/types/logic_array.hpp b/cpp/include/coconext/types/logic_array.hpp index 5d38a9e..6749127 100644 --- a/cpp/include/coconext/types/logic_array.hpp +++ b/cpp/include/coconext/types/logic_array.hpp @@ -81,18 +81,18 @@ 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. +// so `Array::is_resolvable(method)` and friends don't exist. template struct LogicArrayMixin { - bool is_resolvable() const noexcept { + bool is_resolvable(ResolveMethod method) 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. + // Every Bit is resolvable under every method; skip the walk. return true; } else { - return std::ranges::all_of(self, [](auto const& v) { - return v.is_resolvable(); + return std::ranges::all_of(self, [method](auto const& v) { + return v.is_resolvable(method); }); } } @@ -160,9 +160,9 @@ struct LogicArrayMixin { // // These specializations make `Array`, `Array`, // `Vector`, `Vector`, and slices over Logic/Bit owners -// inherit `LogicArrayMixin`, gaining `is_resolvable()` and `resolve(method)` +// inherit `LogicArrayMixin`, gaining `is_resolvable(method)` and `resolve(method)` // as members. The primary templates remain unchanged for non-Logic element -// types -- e.g., `Array` has no `is_resolvable()`. +// types -- e.g., `Array` has no `is_resolvable(method)`. namespace detail { @@ -762,7 +762,7 @@ Vector to_bit_array(R const& range) { Vector result(Range{n - 1, Direction::DOWNTO, 0}); auto out = result.begin(); for (Logic const& v : range) { - if (!v.is_resolvable()) { + if (!v.is_resolvable(ResolveMethod::WEAK)) { throw std::invalid_argument( "Cannot convert non-resolvable Logic values to BitArray" ); diff --git a/cpp/src/logic.cpp b/cpp/src/logic.cpp index fd27a44..0705a16 100644 --- a/cpp/src/logic.cpp +++ b/cpp/src/logic.cpp @@ -8,6 +8,20 @@ using namespace coconext::types; namespace coconext::types { +bool Logic::is_resolvable(ResolveMethod method) const noexcept { + switch (method) { + case ResolveMethod::ERROR: + return value_ == _0 || value_ == _1; + case ResolveMethod::WEAK: + return value_ == _0 || value_ == _1 || value_ == L || value_ == H; + case ResolveMethod::ZEROS: + case ResolveMethod::ONES: + case ResolveMethod::RANDOM: + return true; + } + return false; +} + Logic Logic::resolve(ResolveMethod method) const { switch (method) { case ResolveMethod::ERROR: diff --git a/nanobind/src/types/bind_logic.cpp b/nanobind/src/types/bind_logic.cpp index c239706..cd853c0 100644 --- a/nanobind/src/types/bind_logic.cpp +++ b/nanobind/src/types/bind_logic.cpp @@ -126,7 +126,10 @@ 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.is_resolvable(ResolveMethod::WEAK); } + ) .def( "resolve", [](Logic const& self, std::string_view method) { @@ -245,7 +248,10 @@ 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& self) { return self.is_resolvable(ResolveMethod::WEAK); } + ) .def( "resolve", [](Bit const& self, std::string_view method) { diff --git a/tests/cpp/test_logic.cpp b/tests/cpp/test_logic.cpp index 613bade..5687889 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.is_resolvable(ResolveMethod::WEAK), true); + EXPECT_EQ('H'_l.is_resolvable(ResolveMethod::WEAK), true); // Convertible to false - EXPECT_EQ('0'_l.is_resolvable(), true); - EXPECT_EQ('L'_l.is_resolvable(), true); + EXPECT_EQ('0'_l.is_resolvable(ResolveMethod::WEAK), true); + EXPECT_EQ('L'_l.is_resolvable(ResolveMethod::WEAK), 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.is_resolvable(ResolveMethod::WEAK), false); + EXPECT_EQ('Z'_l.is_resolvable(ResolveMethod::WEAK), false); + EXPECT_EQ('U'_l.is_resolvable(ResolveMethod::WEAK), false); + EXPECT_EQ('W'_l.is_resolvable(ResolveMethod::WEAK), false); + EXPECT_EQ('-'_l.is_resolvable(ResolveMethod::WEAK), false); } TEST(TestBit, BitBoolConversions) { - EXPECT_EQ('0'_b.is_resolvable(), true); - EXPECT_EQ('1'_b.is_resolvable(), true); + EXPECT_EQ('0'_b.is_resolvable(ResolveMethod::WEAK), true); + EXPECT_EQ('1'_b.is_resolvable(ResolveMethod::WEAK), true); +} + +TEST(TestLogic, LogicIsResolvableUnderEachMethod) { + // ERROR: only 0/1 are resolvable. + EXPECT_TRUE('0'_l.is_resolvable(ResolveMethod::ERROR)); + EXPECT_TRUE('1'_l.is_resolvable(ResolveMethod::ERROR)); + EXPECT_FALSE('L'_l.is_resolvable(ResolveMethod::ERROR)); + EXPECT_FALSE('H'_l.is_resolvable(ResolveMethod::ERROR)); + EXPECT_FALSE('X'_l.is_resolvable(ResolveMethod::ERROR)); + EXPECT_FALSE('Z'_l.is_resolvable(ResolveMethod::ERROR)); + EXPECT_FALSE('U'_l.is_resolvable(ResolveMethod::ERROR)); + EXPECT_FALSE('W'_l.is_resolvable(ResolveMethod::ERROR)); + EXPECT_FALSE('-'_l.is_resolvable(ResolveMethod::ERROR)); + + // WEAK: 0/1/L/H pass; metavalues (incl. W) do not. + EXPECT_TRUE('0'_l.is_resolvable(ResolveMethod::WEAK)); + EXPECT_TRUE('1'_l.is_resolvable(ResolveMethod::WEAK)); + EXPECT_TRUE('L'_l.is_resolvable(ResolveMethod::WEAK)); + EXPECT_TRUE('H'_l.is_resolvable(ResolveMethod::WEAK)); + EXPECT_FALSE('X'_l.is_resolvable(ResolveMethod::WEAK)); + EXPECT_FALSE('Z'_l.is_resolvable(ResolveMethod::WEAK)); + EXPECT_FALSE('U'_l.is_resolvable(ResolveMethod::WEAK)); + EXPECT_FALSE('W'_l.is_resolvable(ResolveMethod::WEAK)); + EXPECT_FALSE('-'_l.is_resolvable(ResolveMethod::WEAK)); + + // ZEROS / ONES / RANDOM: always resolvable (no value can throw). + for (auto m : {ResolveMethod::ZEROS, ResolveMethod::ONES, ResolveMethod::RANDOM}) { + EXPECT_TRUE('0'_l.is_resolvable(m)); + EXPECT_TRUE('X'_l.is_resolvable(m)); + EXPECT_TRUE('U'_l.is_resolvable(m)); + EXPECT_TRUE('-'_l.is_resolvable(m)); + } } // Test Logic string conversions @@ -344,21 +376,21 @@ TEST(TestLogic, RuntimeIntAndBitConversions) { // Test Logic is_resolvable 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.is_resolvable(ResolveMethod::WEAK)); + EXPECT_TRUE('1'_l.is_resolvable(ResolveMethod::WEAK)); + EXPECT_TRUE('L'_l.is_resolvable(ResolveMethod::WEAK)); + EXPECT_TRUE('H'_l.is_resolvable(ResolveMethod::WEAK)); - 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.is_resolvable(ResolveMethod::WEAK)); + EXPECT_FALSE('X'_l.is_resolvable(ResolveMethod::WEAK)); + EXPECT_FALSE('Z'_l.is_resolvable(ResolveMethod::WEAK)); + EXPECT_FALSE('W'_l.is_resolvable(ResolveMethod::WEAK)); + EXPECT_FALSE('-'_l.is_resolvable(ResolveMethod::WEAK)); } TEST(TestBit, BitIsResolvable) { - EXPECT_TRUE('0'_b.is_resolvable()); - EXPECT_TRUE('1'_b.is_resolvable()); + EXPECT_TRUE('0'_b.is_resolvable(ResolveMethod::WEAK)); + EXPECT_TRUE('1'_b.is_resolvable(ResolveMethod::WEAK)); } TEST(TestLogic, LogicIsHashable) { diff --git a/tests/cpp/test_logic_array.cpp b/tests/cpp/test_logic_array.cpp index ec4fa41..7aa08db 100644 --- a/tests/cpp/test_logic_array.cpp +++ b/tests/cpp/test_logic_array.cpp @@ -307,20 +307,20 @@ TEST(TestLogicArray, ToStringEmpty) { TEST(TestLogicArray, IsResolvableTrue) { auto a = to_logic_array("01LH"); - EXPECT_TRUE(a.is_resolvable()); + EXPECT_TRUE(a.is_resolvable(ResolveMethod::WEAK)); } 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()); + EXPECT_FALSE(to_logic_array("01X0").is_resolvable(ResolveMethod::WEAK)); + EXPECT_FALSE(to_logic_array("Z").is_resolvable(ResolveMethod::WEAK)); + EXPECT_FALSE(to_logic_array("U").is_resolvable(ResolveMethod::WEAK)); + EXPECT_FALSE(to_logic_array("W").is_resolvable(ResolveMethod::WEAK)); + EXPECT_FALSE(to_logic_array("-").is_resolvable(ResolveMethod::WEAK)); } TEST(TestLogicArray, IsResolvableEmpty) { auto a = to_logic_array(""); - EXPECT_TRUE(a.is_resolvable()); + EXPECT_TRUE(a.is_resolvable(ResolveMethod::WEAK)); } // -- resolve on arrays ------------------------------------------------------ @@ -376,7 +376,7 @@ TEST(TestLogicArray, ResolveStaticReturnsStaticArray) { // // 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 is_resolvable(method) and resolve(method) members. Sub-slicing preserves // the mixin via outer-name resolution in the slice impl. TEST(TestLogicArray, DynSliceIsResolvable) { @@ -384,9 +384,9 @@ 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.is_resolvable(ResolveMethod::WEAK)); // 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.is_resolvable(ResolveMethod::WEAK)); // covers '0' and '1' only } TEST(TestLogicArray, DynSliceResolveReturnsVector) { @@ -406,15 +406,15 @@ TEST(TestLogicArray, StaticSliceResolveReturnsStaticArray) { } 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>::is_resolvable(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 + // .is_resolvable(method) 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.is_resolvable(ResolveMethod::WEAK)); // contains '1' and 'X' auto s_clean = a.slice(); - EXPECT_TRUE(s_clean.is_resolvable()); // '0' and '1' + EXPECT_TRUE(s_clean.is_resolvable(ResolveMethod::WEAK)); // '0' and '1' } TEST(TestLogicArray, ConstOwnerDynSliceHasMixin) { @@ -430,7 +430,7 @@ 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.is_resolvable(ResolveMethod::WEAK)); // all elements are 0/1/L/H auto r = s.resolve(ResolveMethod::WEAK); EXPECT_EQ(to_string(r), "0101"); // L->0, H->1 } @@ -439,10 +439,10 @@ 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 is_resolvable(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.is_resolvable(ResolveMethod::WEAK)); } // -- index / rindex on Logic/Bit arrays ----------------------------------- @@ -1218,9 +1218,9 @@ TEST(TestBitArray, ToStringStaticBit) { TEST(TestBitArray, IsResolvableAlwaysTrue) { auto a = to_bit_array("0110"); - EXPECT_TRUE(a.is_resolvable()); + EXPECT_TRUE(a.is_resolvable(ResolveMethod::WEAK)); auto empty = to_bit_array(""); - EXPECT_TRUE(empty.is_resolvable()); + EXPECT_TRUE(empty.is_resolvable(ResolveMethod::WEAK)); } // -- resolve on BitArray --------------------------------------------------- From 9679f698e29986a1a3656208b47a7ecd141e73cd Mon Sep 17 00:00:00 2001 From: Kaleb Barrett Date: Sat, 20 Jun 2026 12:15:41 -0400 Subject: [PATCH 3/5] Change resolve() to return Bit and BitArray/BitVector Previously this returned Self, now since all methods resolve to 0/1 this can return Bit and BitArray/BitVector. These types have implicit casts to bool and Bit has an implicit int cast. This also is a good jumping off point to cast to Unsigned, Signed, Sfixed, Ufixed, or Float. --- cpp/include/coconext/types/logic.hpp | 6 +- cpp/include/coconext/types/logic_array.hpp | 54 +++++++++++------- cpp/src/logic.cpp | 53 +++++++++-------- nanobind/src/types/bind_logic.cpp | 13 +++-- tests/cpp/test_logic.cpp | 66 +++++++++++----------- tests/cpp/test_logic_array.cpp | 9 +-- 6 files changed, 113 insertions(+), 88 deletions(-) diff --git a/cpp/include/coconext/types/logic.hpp b/cpp/include/coconext/types/logic.hpp index 0cb2fca..d241c98 100644 --- a/cpp/include/coconext/types/logic.hpp +++ b/cpp/include/coconext/types/logic.hpp @@ -18,6 +18,8 @@ enum class ResolveMethod { RANDOM, }; +class Bit; + class Logic { public: enum class value_type : uint8_t { @@ -39,7 +41,7 @@ class Logic { bool is_resolvable(ResolveMethod method) const noexcept; - Logic resolve(ResolveMethod method) const; + Bit resolve(ResolveMethod method) const; private: value_type value_ = _0; @@ -233,6 +235,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 6749127..c3a5f4c 100644 --- a/cpp/include/coconext/types/logic_array.hpp +++ b/cpp/include/coconext/types/logic_array.hpp @@ -78,6 +78,16 @@ constexpr Range make_logic_static_range() { } } +} // namespace detail + +template <> +class Vector; + +namespace detail { + +template +class Array; + // 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, @@ -97,27 +107,7 @@ struct LogicArrayMixin { } } - // 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; // Reductions: fold over the array with the corresponding bitwise op. // Empty arrays return the operation's identity (1 for AND, 0 for OR/XOR), @@ -252,6 +242,28 @@ class StaticArraySlice using detail::StaticArraySliceImpl::operator=; }; +namespace detail { + +template +auto LogicArrayMixin::resolve(ResolveMethod method) const { + auto const& self = *static_cast(this); + if constexpr (StaticRangedSequence) { + ::coconext::types::detail::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; + } +} + +} // namespace detail + using LogicVector = Vector; using BitVector = Vector; diff --git a/cpp/src/logic.cpp b/cpp/src/logic.cpp index 0705a16..f25e876 100644 --- a/cpp/src/logic.cpp +++ b/cpp/src/logic.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include "./random.hpp" @@ -22,70 +23,72 @@ bool Logic::is_resolvable(ResolveMethod method) const noexcept { return false; } -Logic Logic::resolve(ResolveMethod method) const { +Bit Logic::resolve(ResolveMethod method) const { 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"); + throw std::invalid_argument( + "Logic value '" + std::string(to_string(*this)) + + "' is not resolvable under ERROR" + ); } case ResolveMethod::WEAK: 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: - throw std::invalid_argument("Logic value is not resolvable under WEAK"); + throw std::invalid_argument( + "Logic value '" + std::string(to_string(*this)) + + "' is not resolvable under WEAK" + ); } 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: { 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"); } + throw std::invalid_argument("Unknown resolve method"); } } // namespace coconext::types diff --git a/nanobind/src/types/bind_logic.cpp b/nanobind/src/types/bind_logic.cpp index cd853c0..2c8ffb2 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__", @@ -153,10 +153,15 @@ void register_logic(nb::module_& m) { return self; } } - return self.resolve(m); + return Logic(self.resolve(m)); + } + ) + .def( + "resolve", + [](Logic const& self, ResolveMethod method) { + return Logic(self.resolve(method)); } ) - .def("resolve", &Logic::resolve) .def("__copy__", [](Logic const& self) { return Logic(self); }) .def("__deepcopy__", [](Logic const& self, nb::dict /* memo */) { return Logic(self); @@ -173,7 +178,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__", diff --git a/tests/cpp/test_logic.cpp b/tests/cpp/test_logic.cpp index 5687889..2c3a2b2 100644 --- a/tests/cpp/test_logic.cpp +++ b/tests/cpp/test_logic.cpp @@ -260,10 +260,10 @@ TEST(TestBit, BitInvert) { TEST(TestLogic, LogicResolve) { // Test WEAK resolution: 0/1 pass through; L/H map to 0/1; everything else // throws (the "not resolvable under WEAK" tier matches L/H plus 0/1). - EXPECT_EQ('0'_l.resolve(ResolveMethod::WEAK), '0'_l); - EXPECT_EQ('1'_l.resolve(ResolveMethod::WEAK), '1'_l); - EXPECT_EQ('L'_l.resolve(ResolveMethod::WEAK), '0'_l); - EXPECT_EQ('H'_l.resolve(ResolveMethod::WEAK), '1'_l); + 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_THROW((void)'U'_l.resolve(ResolveMethod::WEAK), std::invalid_argument); EXPECT_THROW((void)'X'_l.resolve(ResolveMethod::WEAK), std::invalid_argument); EXPECT_THROW((void)'Z'_l.resolve(ResolveMethod::WEAK), std::invalid_argument); @@ -271,52 +271,52 @@ TEST(TestLogic, LogicResolve) { EXPECT_THROW((void)'-'_l.resolve(ResolveMethod::WEAK), std::invalid_argument); // 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_EQ('0'_l.resolve(ResolveMethod::ERROR), '0'_b); + EXPECT_EQ('1'_l.resolve(ResolveMethod::ERROR), '1'_b); 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); diff --git a/tests/cpp/test_logic_array.cpp b/tests/cpp/test_logic_array.cpp index 7aa08db..ac68588 100644 --- a/tests/cpp/test_logic_array.cpp +++ b/tests/cpp/test_logic_array.cpp @@ -367,8 +367,9 @@ 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>); + // preserves Self's static range when available, and resolve always returns + // a Bit-valued container. + static_assert(std::is_same_v>); EXPECT_EQ(to_string(b), "0100"); } @@ -393,7 +394,7 @@ 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>); + static_assert(std::is_same_v); EXPECT_EQ(to_string(r), "0100"); } @@ -401,7 +402,7 @@ TEST(TestLogicArray, StaticSliceResolveReturnsStaticArray) { auto a = "01XZ"_l; // LogicArray auto s = a.slice(); auto r = s.resolve(ResolveMethod::ZEROS); - static_assert(std::is_same_v>); + static_assert(std::is_same_v>); EXPECT_EQ(to_string(r), "10"); // X->0, 1->1; slice was {X, 1} in storage order } From 72f9bef1df1e3a234c0dd322e40b194c9ad38a10 Mon Sep 17 00:00:00 2001 From: Kaleb Barrett Date: Sat, 20 Jun 2026 16:26:29 -0400 Subject: [PATCH 4/5] Unify resolve and is_resolvable into std::optional return resolve(method) now returns std::optional (Logic/Bit) or std::optional> / std::optional> (LogicArrayMixin). This was done because the previous method required a two-pass approach which is usually a performance smell. --- cpp/include/coconext/types/logic.hpp | 17 ++- cpp/include/coconext/types/logic_array.hpp | 83 ++++------- cpp/src/logic.cpp | 34 +---- nanobind/src/types/bind_logic.cpp | 32 ++--- tests/cpp/test_logic.cpp | 132 +++++++++--------- tests/cpp/test_logic_array.cpp | 155 ++++++++------------- 6 files changed, 186 insertions(+), 267 deletions(-) diff --git a/cpp/include/coconext/types/logic.hpp b/cpp/include/coconext/types/logic.hpp index d241c98..afcdc00 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 @@ -39,9 +40,12 @@ class Logic { constexpr Logic(value_type value) noexcept : value_(value) {} constexpr value_type value() const noexcept { return value_; } - bool is_resolvable(ResolveMethod method) const noexcept; - - Bit resolve(ResolveMethod method) const; + // 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; private: value_type value_ = _0; @@ -59,9 +63,10 @@ class Bit { constexpr Bit(value_type value) noexcept : value_(value) {} constexpr value_type value() const noexcept { return value_; } - constexpr bool is_resolvable(ResolveMethod) 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; } // Implicit conversion from Bit to Logic mimics subtype upcasting. constexpr operator Logic() const noexcept { diff --git a/cpp/include/coconext/types/logic_array.hpp b/cpp/include/coconext/types/logic_array.hpp index c3a5f4c..a2e2d7a 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 @@ -85,27 +86,12 @@ class Vector; namespace detail { -template -class Array; - -// 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(method)` and friends don't exist. +// 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(ResolveMethod method) const noexcept { - auto const& self = *static_cast(this); - using Elem = std::ranges::range_value_t; - if constexpr (std::same_as) { - // Every Bit is resolvable under every method; skip the walk. - return true; - } else { - return std::ranges::all_of(self, [method](auto const& v) { - return v.is_resolvable(method); - }); - } - } auto resolve(ResolveMethod method) const; @@ -150,9 +136,9 @@ struct LogicArrayMixin { // // These specializations make `Array`, `Array`, // `Vector`, `Vector`, and slices over Logic/Bit owners -// inherit `LogicArrayMixin`, gaining `is_resolvable(method)` and `resolve(method)` -// as members. The primary templates remain unchanged for non-Logic element -// types -- e.g., `Array` has no `is_resolvable(method)`. +// 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 { @@ -248,16 +234,28 @@ template auto LogicArrayMixin::resolve(ResolveMethod method) const { auto const& self = *static_cast(this); if constexpr (StaticRangedSequence) { - ::coconext::types::detail::Array result{}; - std::ranges::transform(self, result.begin(), [method](auto const& v) { - return v.resolve(method); - }); + 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 { - ::coconext::types::Vector result(self.range()); - std::ranges::transform(self, result.begin(), [method](auto const& v) { - return v.resolve(method); - }); + 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; } } @@ -759,31 +757,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(ResolveMethod::WEAK)) { - 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 f25e876..47c7bf3 100644 --- a/cpp/src/logic.cpp +++ b/cpp/src/logic.cpp @@ -1,7 +1,6 @@ #include +#include #include -#include -#include #include "./random.hpp" @@ -9,21 +8,7 @@ using namespace coconext::types; namespace coconext::types { -bool Logic::is_resolvable(ResolveMethod method) const noexcept { - switch (method) { - case ResolveMethod::ERROR: - return value_ == _0 || value_ == _1; - case ResolveMethod::WEAK: - return value_ == _0 || value_ == _1 || value_ == L || value_ == H; - case ResolveMethod::ZEROS: - case ResolveMethod::ONES: - case ResolveMethod::RANDOM: - return true; - } - return false; -} - -Bit Logic::resolve(ResolveMethod method) const { +std::optional Logic::resolve(ResolveMethod method) const noexcept { switch (method) { case ResolveMethod::ERROR: switch (value_) { @@ -32,10 +17,7 @@ Bit Logic::resolve(ResolveMethod method) const { case _1: return Bit::_1; default: - throw std::invalid_argument( - "Logic value '" + std::string(to_string(*this)) - + "' is not resolvable under ERROR" - ); + return std::nullopt; } case ResolveMethod::WEAK: switch (value_) { @@ -46,10 +28,7 @@ Bit Logic::resolve(ResolveMethod method) const { case H: return Bit::_1; default: - throw std::invalid_argument( - "Logic value '" + std::string(to_string(*this)) - + "' is not resolvable under WEAK" - ); + return std::nullopt; } case ResolveMethod::ZEROS: switch (value_) { @@ -73,7 +52,7 @@ Bit Logic::resolve(ResolveMethod method) const { default: return Bit::_1; } - case ResolveMethod::RANDOM: { + case ResolveMethod::RANDOM: switch (value_) { case _0: case L: @@ -87,8 +66,7 @@ Bit Logic::resolve(ResolveMethod method) const { } } } - } - 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 2c8ffb2..2537583 100644 --- a/nanobind/src/types/bind_logic.cpp +++ b/nanobind/src/types/bind_logic.cpp @@ -128,16 +128,17 @@ void register_logic(nb::module_& m) { ) .def_prop_ro( "is_resolvable", - [](Logic const& self) { return self.is_resolvable(ResolveMethod::WEAK); } + [](Logic const& self) { return self.resolve(ResolveMethod::WEAK).has_value(); } ) .def( "resolve", [](Logic const& self, std::string_view 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++ throws - // on metavalues under WEAK. Reproduce the old mapping here so - // upstream Python (and its integration tests) don't break. + // 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: @@ -153,13 +154,13 @@ void register_logic(nb::module_& m) { return self; } } - return Logic(self.resolve(m)); - } - ) - .def( - "resolve", - [](Logic const& self, ResolveMethod method) { - return Logic(self.resolve(method)); + auto r = self.resolve(m); + if (!r) { + throw std::invalid_argument( + "Logic value is not resolvable under the given method" + ); + } + return Logic(*r); } ) .def("__copy__", [](Logic const& self) { return Logic(self); }) @@ -253,17 +254,14 @@ void register_logic(nb::module_& m) { .def( "__invert__", [](Bit const& self) { return ~self; }, nb::is_operator() ) - .def_prop_ro( - "is_resolvable", - [](Bit const& self) { return self.is_resolvable(ResolveMethod::WEAK); } - ) + .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 2c3a2b2..96b35fb 100644 --- a/tests/cpp/test_logic.cpp +++ b/tests/cpp/test_logic.cpp @@ -95,55 +95,55 @@ TEST(TestBit, BitConversions) { // Test Logic bool conversions TEST(TestLogic, LogicBoolConversions) { // Convertible to true - EXPECT_EQ('1'_l.is_resolvable(ResolveMethod::WEAK), true); - EXPECT_EQ('H'_l.is_resolvable(ResolveMethod::WEAK), 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(ResolveMethod::WEAK), true); - EXPECT_EQ('L'_l.is_resolvable(ResolveMethod::WEAK), 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(ResolveMethod::WEAK), false); - EXPECT_EQ('Z'_l.is_resolvable(ResolveMethod::WEAK), false); - EXPECT_EQ('U'_l.is_resolvable(ResolveMethod::WEAK), false); - EXPECT_EQ('W'_l.is_resolvable(ResolveMethod::WEAK), false); - EXPECT_EQ('-'_l.is_resolvable(ResolveMethod::WEAK), 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(ResolveMethod::WEAK), true); - EXPECT_EQ('1'_b.is_resolvable(ResolveMethod::WEAK), true); + EXPECT_EQ('0'_b.resolve(ResolveMethod::WEAK).has_value(), true); + EXPECT_EQ('1'_b.resolve(ResolveMethod::WEAK).has_value(), true); } -TEST(TestLogic, LogicIsResolvableUnderEachMethod) { +TEST(TestLogic, LogicResolvabilityUnderEachMethod) { // ERROR: only 0/1 are resolvable. - EXPECT_TRUE('0'_l.is_resolvable(ResolveMethod::ERROR)); - EXPECT_TRUE('1'_l.is_resolvable(ResolveMethod::ERROR)); - EXPECT_FALSE('L'_l.is_resolvable(ResolveMethod::ERROR)); - EXPECT_FALSE('H'_l.is_resolvable(ResolveMethod::ERROR)); - EXPECT_FALSE('X'_l.is_resolvable(ResolveMethod::ERROR)); - EXPECT_FALSE('Z'_l.is_resolvable(ResolveMethod::ERROR)); - EXPECT_FALSE('U'_l.is_resolvable(ResolveMethod::ERROR)); - EXPECT_FALSE('W'_l.is_resolvable(ResolveMethod::ERROR)); - EXPECT_FALSE('-'_l.is_resolvable(ResolveMethod::ERROR)); + 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.is_resolvable(ResolveMethod::WEAK)); - EXPECT_TRUE('1'_l.is_resolvable(ResolveMethod::WEAK)); - EXPECT_TRUE('L'_l.is_resolvable(ResolveMethod::WEAK)); - EXPECT_TRUE('H'_l.is_resolvable(ResolveMethod::WEAK)); - EXPECT_FALSE('X'_l.is_resolvable(ResolveMethod::WEAK)); - EXPECT_FALSE('Z'_l.is_resolvable(ResolveMethod::WEAK)); - EXPECT_FALSE('U'_l.is_resolvable(ResolveMethod::WEAK)); - EXPECT_FALSE('W'_l.is_resolvable(ResolveMethod::WEAK)); - EXPECT_FALSE('-'_l.is_resolvable(ResolveMethod::WEAK)); + 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.is_resolvable(m)); - EXPECT_TRUE('X'_l.is_resolvable(m)); - EXPECT_TRUE('U'_l.is_resolvable(m)); - EXPECT_TRUE('-'_l.is_resolvable(m)); + 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()); } } @@ -259,16 +259,17 @@ TEST(TestBit, BitInvert) { // Test Logic resolve with different methods TEST(TestLogic, LogicResolve) { // Test WEAK resolution: 0/1 pass through; L/H map to 0/1; everything else - // throws (the "not resolvable under WEAK" tier matches L/H plus 0/1). + // 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_THROW((void)'U'_l.resolve(ResolveMethod::WEAK), std::invalid_argument); - EXPECT_THROW((void)'X'_l.resolve(ResolveMethod::WEAK), std::invalid_argument); - EXPECT_THROW((void)'Z'_l.resolve(ResolveMethod::WEAK), std::invalid_argument); - EXPECT_THROW((void)'W'_l.resolve(ResolveMethod::WEAK), std::invalid_argument); - EXPECT_THROW((void)'-'_l.resolve(ResolveMethod::WEAK), std::invalid_argument); + 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'_b); @@ -317,24 +318,23 @@ TEST(TestLogic, LogicResolve) { // Test ERROR resolution EXPECT_EQ('0'_l.resolve(ResolveMethod::ERROR), '0'_b); EXPECT_EQ('1'_l.resolve(ResolveMethod::ERROR), '1'_b); - 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); + 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 `default:` arm. - EXPECT_THROW( - (void)'0'_l.resolve(static_cast(99)), std::invalid_argument - ); + // 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); @@ -374,23 +374,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(ResolveMethod::WEAK)); - EXPECT_TRUE('1'_l.is_resolvable(ResolveMethod::WEAK)); - EXPECT_TRUE('L'_l.is_resolvable(ResolveMethod::WEAK)); - EXPECT_TRUE('H'_l.is_resolvable(ResolveMethod::WEAK)); + 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(ResolveMethod::WEAK)); - EXPECT_FALSE('X'_l.is_resolvable(ResolveMethod::WEAK)); - EXPECT_FALSE('Z'_l.is_resolvable(ResolveMethod::WEAK)); - EXPECT_FALSE('W'_l.is_resolvable(ResolveMethod::WEAK)); - EXPECT_FALSE('-'_l.is_resolvable(ResolveMethod::WEAK)); + 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(ResolveMethod::WEAK)); - EXPECT_TRUE('1'_b.is_resolvable(ResolveMethod::WEAK)); + 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 ac68588..f01e943 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(ResolveMethod::WEAK)); + EXPECT_TRUE(a.resolve(ResolveMethod::WEAK).has_value()); } -TEST(TestLogicArray, IsResolvableFalse) { - EXPECT_FALSE(to_logic_array("01X0").is_resolvable(ResolveMethod::WEAK)); - EXPECT_FALSE(to_logic_array("Z").is_resolvable(ResolveMethod::WEAK)); - EXPECT_FALSE(to_logic_array("U").is_resolvable(ResolveMethod::WEAK)); - EXPECT_FALSE(to_logic_array("W").is_resolvable(ResolveMethod::WEAK)); - EXPECT_FALSE(to_logic_array("-").is_resolvable(ResolveMethod::WEAK)); +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(ResolveMethod::WEAK)); + EXPECT_TRUE(a.resolve(ResolveMethod::WEAK).has_value()); } // -- resolve on arrays ------------------------------------------------------ @@ -328,38 +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, ResolveWeakAcceptsResolvable) { - // WEAK passes 0/1/L/H -> 0/1/0/1 and throws on the rest. The input must - // contain only resolvable-under-WEAK values for the call to succeed. + // 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), "0101"); + EXPECT_EQ(to_string(*b), "0101"); } -TEST(TestLogicArray, ResolveWeakThrowsOnMetavalue) { - // Even a single non-resolvable value makes the whole array throw. +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_THROW(a.resolve(ResolveMethod::WEAK), std::invalid_argument); + 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) { @@ -368,16 +370,18 @@ TEST(TestLogicArray, ResolveStaticReturnsStaticArray) { // 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, and resolve always returns - // a Bit-valued container. - static_assert(std::is_same_v>); - EXPECT_EQ(to_string(b), "0100"); + // an optional Bit-valued container. + static_assert( + std::is_same_v>> + ); + EXPECT_EQ(to_string(*b), "0100"); } // -- Slice resolvability --------------------------------------------------- // // The constrained partial specs of StaticArraySlice and ArraySlice inherit the // LogicArrayMixin too, so slices of LogicArray/BitArray/LogicVector/etc -// have is_resolvable(method) 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) { @@ -385,37 +389,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(ResolveMethod::WEAK)); // 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(ResolveMethod::WEAK)); // 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(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 - // .is_resolvable(method) 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(ResolveMethod::WEAK)); // 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(ResolveMethod::WEAK)); // '0' and '1' + EXPECT_TRUE(s_clean.resolve(ResolveMethod::WEAK).has_value()); // '0' and '1' } TEST(TestLogicArray, ConstOwnerDynSliceHasMixin) { @@ -431,19 +442,19 @@ TEST(TestLogicArray, ConstOwnerDynSliceHasMixin) { std::same_as const>>, "const Logic owner must produce ArraySlice>" ); - EXPECT_TRUE(s.is_resolvable(ResolveMethod::WEAK)); // 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(method) -- 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(ResolveMethod::WEAK)); + EXPECT_FALSE(sub.resolve(ResolveMethod::WEAK).has_value()); } // -- index / rindex on Logic/Bit arrays ----------------------------------- @@ -1155,54 +1166,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) { @@ -1215,20 +1178,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(ResolveMethod::WEAK)); + EXPECT_TRUE(a.resolve(ResolveMethod::WEAK).has_value()); auto empty = to_bit_array(""); - EXPECT_TRUE(empty.is_resolvable(ResolveMethod::WEAK)); + 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, @@ -1236,7 +1201,7 @@ TEST(TestBitArray, ResolveBitIsIdentity) { ResolveMethod::ERROR, ResolveMethod::RANDOM}) { - EXPECT_EQ(to_string(a.resolve(method)), "0110"); + EXPECT_EQ(to_string(*a.resolve(method)), "0110"); } } From ea7d2bb79a00ee9f4420b6e112f60eb421829f78 Mon Sep 17 00:00:00 2001 From: Kaleb Barrett Date: Sat, 20 Jun 2026 16:30:24 -0400 Subject: [PATCH 5/5] Add no-arg resolve() defaulting to WEAK --- cpp/include/coconext/types/logic.hpp | 10 ++++++++++ cpp/include/coconext/types/logic_array.hpp | 2 ++ tests/cpp/test_logic.cpp | 20 ++++++++++++++++++++ tests/cpp/test_logic_array.cpp | 12 ++++++++++++ 4 files changed, 44 insertions(+) diff --git a/cpp/include/coconext/types/logic.hpp b/cpp/include/coconext/types/logic.hpp index afcdc00..adc8c58 100644 --- a/cpp/include/coconext/types/logic.hpp +++ b/cpp/include/coconext/types/logic.hpp @@ -47,6 +47,9 @@ class Logic { // `r.value()` extracts the Bit. std::optional resolve(ResolveMethod method) const noexcept; + // Default to WEAK. + std::optional resolve() const noexcept; + private: value_type value_ = _0; }; @@ -67,6 +70,7 @@ class Bit { // 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 { @@ -85,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(); } diff --git a/cpp/include/coconext/types/logic_array.hpp b/cpp/include/coconext/types/logic_array.hpp index a2e2d7a..8e22122 100644 --- a/cpp/include/coconext/types/logic_array.hpp +++ b/cpp/include/coconext/types/logic_array.hpp @@ -94,6 +94,8 @@ template struct LogicArrayMixin { 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), diff --git a/tests/cpp/test_logic.cpp b/tests/cpp/test_logic.cpp index 96b35fb..a2e3bcb 100644 --- a/tests/cpp/test_logic.cpp +++ b/tests/cpp/test_logic.cpp @@ -356,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) { diff --git a/tests/cpp/test_logic_array.cpp b/tests/cpp/test_logic_array.cpp index f01e943..9b24235 100644 --- a/tests/cpp/test_logic_array.cpp +++ b/tests/cpp/test_logic_array.cpp @@ -377,6 +377,18 @@ TEST(TestLogicArray, ResolveStaticReturnsStaticArray) { 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