diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a05d13..e149cae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.25) project(ros - VERSION 0.2.0 + VERSION 0.2.1 LANGUAGES CXX DESCRIPTION "A header-only C++ library that provides safe diff --git a/include/ros/eval.hpp b/include/ros/eval.hpp index a01852a..8f03441 100644 --- a/include/ros/eval.hpp +++ b/include/ros/eval.hpp @@ -6,149 +6,6 @@ #include namespace ros { -namespace detail { - -template struct return_reads { - using type = void; -}; - -template struct return_reads> { - using type = typename Op::type::value_type; -}; - -template struct return_reads> { - using type = std::tuple; -}; - -template -using return_reads_t = typename return_reads::type; - -template -constexpr auto get_write_value_helper(Tuple tup, std::index_sequence) -> T { - return (std::tuple_element_t::type::to_reg( - T{0}, std::get(tup).value) | - ...); -} - -template -constexpr auto get_write_value(T value, T mask, std::tuple const &tup) -> T { - return (value & ~mask) | - get_write_value_helper(tup, - std::make_index_sequence{}); -} - -template -constexpr auto get_write_value(T value, T mask, std::tuple<> const &tup) -> T { - return value; -} - -template -constexpr auto get_write_mask_helper(Tuple const &tup, - std::index_sequence) { - return (std::tuple_element_t::type::mask | ...); -}; - -template constexpr auto get_write_mask(std::tuple<> const &tup) -> T { - return 0; -} - -template -constexpr auto get_write_mask(std::tuple const &tup) -> T { - return get_write_mask_helper(tup, - std::make_index_sequence{}); -} - -template -constexpr auto get_invocable_write_fields_helper(T value, InvocableWrite iw, - TupleFields tup, - std::index_sequence) - -> T { - // get each field - return iw(std::tuple_element_t::to_field(value)...); -} - -template -constexpr auto get_invocable_write_value_helper(T value, TupleInvocableWrites tup, - std::index_sequence) - -> T { - return ( - std::tuple_element_t< - Idx, TupleInvocableWrites>::type::to_reg( // wrap back everything to - // reg value - T{0}, // pass in zero, final value will assigned with a compound - // mask - std::tuple_element_t< - Idx, TupleInvocableWrites>::type::runtime_check( // safety check - get_invocable_write_fields_helper( // make invocable call with - // each field value - value, // original reg value - std::get(tup), // invocable lambda wrapper - typename std::tuple_element_t< - Idx, TupleInvocableWrites>::fields{}, // tuple of fields - std::make_index_sequence< - std::tuple_size_v::fields>>{}))) | - ...); -} - -template -constexpr auto -get_invocable_write_value(T value, T mask, - std::tuple const &tup) - -> T { - return (value & ~mask) | - get_invocable_write_value_helper( - value, tup, - std::make_index_sequence{}); -} - -template -constexpr auto get_invocable_write_value(T value, T mask, - std::tuple<> const &tup) - -> T { - return value; -} - -template -constexpr void -evaluate_invocable_assignment_helper(InvocableWrite iw, - std::index_sequence) { - using registerOp = typename InvocableWrite::registerOp; - using busOp = registerOp::bus; - using registers = typename InvocableWrite::registers; - - busOp::write( - iw( - // call bus::read that corresponds to each register - std::tuple_element_t::bus::template read< - typename std::tuple_element_t::value_type>( - std::tuple_element_t::address::value)...), - registerOp::address::value); -} - -template -constexpr void evaluate_invocable_assignment(InvocableWrite iw) { - using registers = typename InvocableWrite::registers; - evaluate_invocable_assignment_helper( - iw, std::make_index_sequence>{}); -} - -template -constexpr void -evaluate_invocable_assignments_helper(std::tuple writes, - std::index_sequence) { - (evaluate_invocable_assignment(std::get(writes)), ...); -} - -template -constexpr void -evaluate_invocable_assignments(std::tuple writes) { - evaluate_invocable_assignments_helper( - writes, std::make_index_sequence{}); -} - -} // namespace detail template requires detail::field_constraints @@ -201,18 +58,14 @@ auto eval(Op op, Ops... ops) -> detail::return_reads_t< value = bus::template read(reg::address::value); } - // evaluate invocables at the beginning - // it doesn't make much sense to evaluate it at the end because it will - // have newly assigned values. this way just literals could be provided - // in the lambda - value = detail::get_invocable_write_value(value, write_mask_inv, - writes_inv); + // compose monadic chain + const auto chain + = detail::chain(writes_inv) + | detail::chain(writes_ct) + | detail::chain(writes_rt) + ; - // [TODO] study efficiency of bundling together all writes - // compile time - value = detail::get_write_value(value, write_mask_ct, writes_ct); - // runtime - value = detail::get_write_value(value, write_mask_rt, writes_rt); + value = chain(value); // registers are not guaranteed to be idempotent, (the stored // value can hold value that may trigger clear/set/toggle. @@ -295,44 +148,9 @@ auto eval(Op op, Ops... ops) -> detail::return_reads_t< Rs::type::address::value)...); }(reads); - if constexpr (has_writes_inv) { - detail::evaluate_invocable_assignments(writes_inv); - } - - // third, cluster adjacent writes into separate tuples - // call write bundled for each tuple - - // writes are peformed separately. the intent is to get jem - // out of compile-time accessible writes, and then let - // the compiler to optimize the runtime writes - - if constexpr (has_writes_ct) { - if constexpr (std::tuple_size_v == 1) { - // if there's only one write, just call write without bundling - using write = std::tuple_element_t<0, decltype(writes_ct)>; - write::type::bus::write(write::value, write::type::address::value); - } else { - [](std::tuple) -> void { - (Ws::type::bus::write(Ws::value, Ws::type::address::value), - ...); - }(writes_ct); - } - } - - if constexpr (has_writes_rt) { - if constexpr (std::tuple_size_v == 1) { - // if there's only one write, just call write without bundling - using write = std::tuple_element_t<0, decltype(writes_rt)>; - write::type::bus::write(std::get(writes_rt).value, - write::type::address::value); - } else { - [](std::tuple ws) -> void { - (Ws::type::bus::write(std::get(ws).value, - Ws::type::address::value), - ...); - }(writes_rt); - } - } + detail::evaluate(writes_inv); + detail::evaluate(writes_ct); + detail::evaluate(writes_rt); // if there's only one read, just return the reg value without tuple if constexpr (std::tuple_size_v == 1) { diff --git a/include/ros/operations.hpp b/include/ros/operations.hpp index 5e6d975..c87716b 100644 --- a/include/ros/operations.hpp +++ b/include/ros/operations.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include @@ -14,27 +15,58 @@ template struct field_assignment { template struct field_assignment_ct : field_assignment { - constexpr static typename Field::value_type value = val; + using value_type = typename Field::value_type; + using value_type_r = typename Field::value_type_r; + + constexpr auto operator() (const value_type_r register_value) const + -> value_type_r { + return Field::to_reg(register_value, value); + } + +private: + constexpr static value_type value = val; }; template struct field_assignment_rt : field_assignment { using value_type = typename Field::value_type; + using value_type_r = typename Field::value_type_r; - constexpr explicit field_assignment_rt(value_type v) : value{v} {} + constexpr explicit field_assignment_rt(const value_type v) : value{v} {} + + constexpr auto operator() (const value_type_r register_value) const + -> value_type_r { + return Field::to_reg(register_value, value); + } +private: value_type value; }; template struct field_assignment_invocable : field_assignment { + using value_type = typename FieldOp::value_type; + using value_type_r = typename FieldOp::value_type_r; using fields = std::tuple; - - explicit field_assignment_invocable(F f) : lambda_{f} {} - - constexpr auto operator()(typename Field0::value_type f0, - typename Fields::value_type... fs) -> - typename FieldOp::value_type { - return lambda_(f0, fs...); + + constexpr explicit field_assignment_invocable(F f) : lambda_{f} {} + + constexpr auto operator() (const value_type_r register_value) const + -> value_type_r { + return FieldOp::to_reg( + register_value, + FieldOp::runtime_check( + invoke(register_value, fields{}, + std::make_index_sequence<1 + sizeof...(Fields)>{}))); + } + +private: + template + [[nodiscard]] constexpr auto invoke(const value_type_r value, + const FieldsTuple tup, + const std::index_sequence) const + -> value_type { + return lambda_( + std::tuple_element_t::to_field(value)...); } F lambda_; @@ -46,16 +78,14 @@ template struct unsafe_field_operations_handler { // NOLINTNEXTLINE(cppcoreguidelines-c-copy-assignment-signature) constexpr auto operator=(auto const &rhs) const -> field_assignment_rt { - static_assert(Field::access != access_type::RO, - "cannot write read-only field"); + static_assert(Field::writable(), "cannot write read-only field"); return field_assignment_rt{static_cast(rhs)}; } // NOLINTNEXTLINE(cppcoreguidelines-c-copy-assignment-signature) constexpr auto operator=(auto &&rhs) const -> field_assignment_rt { - static_assert(Field::access != access_type::RO, - "cannot write read-only field"); + static_assert(Field::writable(), "cannot write read-only field"); return field_assignment_rt{static_cast(rhs)}; } @@ -63,6 +93,30 @@ template struct unsafe_field_operations_handler { // TODO: add compile time unsafe operations }; +template +struct pipe { +public: + constexpr pipe(const L lhs, const R rhs) + : lhs_{lhs}, rhs_{rhs} + {} + + constexpr auto operator() (const auto ...vs) const { + return call_rhs(lhs_(vs...)); + } + +private: + constexpr auto call_rhs(auto && ...vs) const { + return rhs_(std::forward(vs)...); + } + + L lhs_; + R rhs_; +}; + +template +constexpr auto operator| (L l, R r) { + return pipe{l, r}; +} } // namespace detail // reg operations @@ -73,30 +127,56 @@ template struct register_assignment { template struct register_assignment_ct : register_assignment { - constexpr static typename Register::value_type value = val; + using value_type = typename Register::value_type; + using bus = typename Register::bus; + + constexpr auto operator() () const -> void { + return bus::template write(val, + Register::address::value); + }; }; template struct register_assignment_rt : register_assignment { using value_type = typename Register::value_type; + using bus = typename Register::bus; - constexpr explicit register_assignment_rt(value_type v) : value{v} {} + constexpr explicit register_assignment_rt(const value_type v) : value_{v} {} - value_type value; + constexpr auto operator() () const -> void { + return bus::template write(value_, + Register::address::value); + }; + +private: + value_type value_; }; template struct register_assignment_invocable : register_assignment { + using value_type = typename RegisterOp::value_type; + using bus = typename RegisterOp::bus; using registerOp = RegisterOp; using registers = std::tuple; explicit register_assignment_invocable(F f) : lambda_{f} {} - constexpr auto operator()(typename Register0::value_type r0, - typename Registers::value_type... rs) -> - typename RegisterOp::value_type { - return lambda_(r0, rs...); + constexpr auto operator() () const -> void { + bus::template write( + invoke(registers{}, + std::make_index_sequence{}), + RegisterOp::address::value); + } + +private: + template + [[nodiscard]] constexpr auto invoke(const RegistersTuple tup, + std::index_sequence) const + -> value_type { + return lambda_( + bus::template read::value_type>( + std::tuple_element_t::address::value)...); } F lambda_; @@ -124,6 +204,21 @@ template struct unsafe_register_operations_handler { // TODO: Add compile time unsafe operations support }; +template struct return_reads { + using type = void; +}; + +template struct return_reads> { + using type = typename Op::type::value_type; +}; + +template struct return_reads> { + using type = std::tuple; +}; + +template +using return_reads_t = typename return_reads::type; + template constexpr auto get_rmw_mask_helper(std::tuple const &t, std::index_sequence) -> @@ -193,5 +288,51 @@ constexpr auto get_ro_mask(reg const &r) -> T { constexpr std::size_t tup_size = std::tuple_size_v; return get_ro_mask_helper(tup, std::make_index_sequence{}); } + +template +constexpr auto get_write_mask_helper(Tuple const &tup, + std::index_sequence) { + return (std::tuple_element_t::type::mask | ...); +}; + +template constexpr auto get_write_mask(std::tuple<> const &tup) -> T { + return 0; +} + +template +constexpr auto get_write_mask(std::tuple const &tup) -> T { + return get_write_mask_helper(tup, + std::make_index_sequence{}); +} + +template +constexpr auto chain_helper(std::tuple const &ops, + std::index_sequence) { + return (std::get(ops) | ...); +} + +template +constexpr auto chain(std::tuple const &ops) { + return chain_helper(ops, std::make_index_sequence{}); +} + +constexpr auto chain(std::tuple<> const &tup) { + return [] (auto value) { return value; }; +} + +template +constexpr auto evaluate_helper(std::tuple const &ops, + std::index_sequence) { + return (std::get(ops)(), ...); +} + +template +constexpr auto evaluate(std::tuple const &ops) { + return evaluate_helper(ops, std::make_index_sequence{}); +} + +constexpr auto evaluate(std::tuple<> const &tup) { + // no-op +} } // namespace detail } // namespace ros \ No newline at end of file diff --git a/include/ros/type_traits.hpp b/include/ros/type_traits.hpp index 11393c2..47c8513 100644 --- a/include/ros/type_traits.hpp +++ b/include/ros/type_traits.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include diff --git a/test/unit/test_apply_field.cpp b/test/unit/test_apply_field.cpp index f17a39a..19d7a7b 100644 --- a/test/unit/test_apply_field.cpp +++ b/test/unit/test_apply_field.cpp @@ -173,6 +173,36 @@ TEST_F(ApplyFieldTest, InvocableWrite_PlusCTWrite_CoversAllRMW) { EXPECT_EQ(bus_log[1].value, 0xB4u); } +TEST_F(ApplyFieldTest, InvocableWrite_MultipleInvocables_LaterSeesEarlierWrite) { + constexpr simple_reg r{}; + bus_read_value = 0x21; // low_nibble = 1, high_nibble = 2 + + // Invocables are applied as a sequential chain in argument order, NOT against + // a single pre-read snapshot. The 2nd invocable (writing low_nibble) reads + // high_nibble, which the 1st invocable has already incremented. + eval(r.high_nibble([](uint8_t h) -> uint8_t { return h + 1; }), // high: 2 -> 3 + r.low_nibble([](uint8_t h) -> uint8_t { return h; }, // low = current high + r.high_nibble)); + + ASSERT_EQ(bus_log.size(), 2u); + // low reads high AFTER it became 3 -> 0x33 (a snapshot semantics would give 0x32) + EXPECT_EQ(bus_log[1].value, 0x33u); +} + +TEST_F(ApplyFieldTest, InvocableWrite_MultipleInvocables_OrderDependent) { + constexpr simple_reg r{}; + bus_read_value = 0x21; // low_nibble = 1, high_nibble = 2 + + // Same two invocables, reversed order: low_nibble's invocable now runs first, + // so it reads high_nibble's original value (2) before it is incremented. + eval(r.low_nibble([](uint8_t h) -> uint8_t { return h; }, r.high_nibble), + r.high_nibble([](uint8_t h) -> uint8_t { return h + 1; })); + + ASSERT_EQ(bus_log.size(), 2u); + // low reads high BEFORE the increment -> low=2; high then becomes 3 -> 0x32 + EXPECT_EQ(bus_log[1].value, 0x32u); +} + TEST_F(ApplyFieldTest, InvocableWrite_FullWidthField) { constexpr full_reg r{}; bus_read_value = 0xDEADBEEF;