From eb755374de6db36cc79bbb34ec217824c40811ca Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Wed, 22 Jul 2026 16:04:14 +0800 Subject: [PATCH] Add value-only matrix snapshot bridge --- include/pineforge/generic_matrix.hpp | 58 ++++++++++- include/pineforge/matrix.hpp | 20 ++++ src/matrix.cpp | 11 +++ tests/CMakeLists.txt | 1 + tests/test_matrix_snapshot_compat.cpp | 132 ++++++++++++++++++++++++++ 5 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 tests/test_matrix_snapshot_compat.cpp diff --git a/include/pineforge/generic_matrix.hpp b/include/pineforge/generic_matrix.hpp index f542890..26cd686 100644 --- a/include/pineforge/generic_matrix.hpp +++ b/include/pineforge/generic_matrix.hpp @@ -126,7 +126,9 @@ inline void sort_impl(std::vector& data, int column, bool ascending) { template class PineGenericMatrix { - std::vector> data_; + using Data = std::vector>; + + Data data_; bool valid_{false}; void require_valid() const { @@ -134,6 +136,20 @@ class PineGenericMatrix { } public: + class Snapshot { + Data state_; + + explicit Snapshot(const Data& state) : state_(state) {} + + friend class PineGenericMatrix; + + public: + Snapshot(const Snapshot&) = default; + Snapshot& operator=(const Snapshot&) = default; + Snapshot(Snapshot&&) = default; + Snapshot& operator=(Snapshot&&) = default; + }; + ~PineGenericMatrix() = default; [[nodiscard]] static PineGenericMatrix new_(int rows, int cols, T init) { @@ -347,6 +363,17 @@ class PineGenericMatrix { } [[nodiscard]] bool is_na() const noexcept { return !valid_; } + + [[nodiscard]] Snapshot snapshot() const { + require_valid(); + return Snapshot(data_); + } + + void restore(const Snapshot& snapshot) { + Data replacement(snapshot.state_); + data_.swap(replacement); + valid_ = true; + } }; // PineGenericMatrix uses std::vector as the row container because @@ -356,7 +383,9 @@ class PineGenericMatrix { // to the same detail:: helpers used by the primary template. template <> class PineGenericMatrix { - std::vector> data_; + using Data = std::vector>; + + Data data_; bool valid_{false}; void require_valid() const { @@ -364,6 +393,20 @@ class PineGenericMatrix { } public: + class Snapshot { + Data state_; + + explicit Snapshot(const Data& state) : state_(state) {} + + friend class PineGenericMatrix; + + public: + Snapshot(const Snapshot&) = default; + Snapshot& operator=(const Snapshot&) = default; + Snapshot(Snapshot&&) = default; + Snapshot& operator=(Snapshot&&) = default; + }; + [[nodiscard]] static PineGenericMatrix new_(int rows, int cols, bool init) { if (rows < 0 || cols < 0) throw std::invalid_argument("matrix.new: negative dimensions"); @@ -562,6 +605,17 @@ class PineGenericMatrix { } [[nodiscard]] bool is_na() const noexcept { return !valid_; } + + [[nodiscard]] Snapshot snapshot() const { + require_valid(); + return Snapshot(data_); + } + + void restore(const Snapshot& snapshot) { + Data replacement(snapshot.state_); + data_.swap(replacement); + valid_ = true; + } }; template diff --git a/include/pineforge/matrix.hpp b/include/pineforge/matrix.hpp index 96a2863..950d83c 100644 --- a/include/pineforge/matrix.hpp +++ b/include/pineforge/matrix.hpp @@ -12,6 +12,23 @@ class PineMatrix { void require_valid() const; public: + // Opaque value checkpoint used by generated rollback code. This API is + // intentionally identity-neutral: matrix assignment retains its existing + // value semantics until the separate Pine ID-semantics change lands. + class Snapshot { + Eigen::MatrixXd state_; + + explicit Snapshot(const Eigen::MatrixXd& state) : state_(state) {} + + friend class PineMatrix; + + public: + Snapshot(const Snapshot&) = default; + Snapshot& operator=(const Snapshot&) = default; + Snapshot(Snapshot&&) = default; + Snapshot& operator=(Snapshot&&) = default; + }; + // Construction static PineMatrix new_(int rows, int cols, double init_val = 0.0); @@ -74,6 +91,9 @@ class PineMatrix { // matrix.new() returns a valid ID even when its dimensions are 0x0. [[nodiscard]] bool is_na() const noexcept { return !valid_; } + [[nodiscard]] Snapshot snapshot() const; + void restore(const Snapshot& snapshot); + // Properties bool is_square() const; bool is_identity() const; diff --git a/src/matrix.cpp b/src/matrix.cpp index 2351b88..315f1b5 100644 --- a/src/matrix.cpp +++ b/src/matrix.cpp @@ -22,6 +22,17 @@ void PineMatrix::require_valid() const { if (!valid_) throw std::runtime_error("matrix operation on na ID"); } +PineMatrix::Snapshot PineMatrix::snapshot() const { + require_valid(); + return Snapshot(data_); +} + +void PineMatrix::restore(const Snapshot& snapshot) { + Eigen::MatrixXd replacement(snapshot.state_); + data_.swap(replacement); + valid_ = true; +} + // ── Access ────────────────────────────────────────────────────────────────── double PineMatrix::get(int row, int col) const { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 86145d4..9d915f7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,6 +16,7 @@ set(TEST_SOURCES test_ta_na_source_rules test_matrix test_matrix_na + test_matrix_snapshot_compat test_session_time test_time_tradingday test_chart_timezone diff --git a/tests/test_matrix_snapshot_compat.cpp b/tests/test_matrix_snapshot_compat.cpp new file mode 100644 index 0000000..d56962b --- /dev/null +++ b/tests/test_matrix_snapshot_compat.cpp @@ -0,0 +1,132 @@ +#include +#include + +#include +#include +#include +#include +#include + +using pineforge::PineGenericMatrix; +using pineforge::PineMatrix; + +template +void require_na_snapshot_error(const Matrix& matrix) { + try { + (void)matrix.snapshot(); + assert(false && "snapshot() on na must throw"); + } catch (const std::runtime_error& error) { + assert(std::string(error.what()) == "matrix operation on na ID"); + } +} + +template +void check_generic_snapshot_round_trip(const T& initial, const T& changed) { + using Matrix = PineGenericMatrix; + using Snapshot = typename Matrix::Snapshot; + + static_assert(std::is_copy_constructible_v); + static_assert(std::is_copy_assignable_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_move_assignable_v); + static_assert(!std::is_default_constructible_v); + + Matrix matrix = Matrix::new_(1, 1, initial); + Snapshot snapshot = matrix.snapshot(); + Snapshot copied = snapshot; + Snapshot moved = std::move(copied); + + matrix.set(0, 0, changed); + matrix.restore(snapshot); + assert(matrix.get(0, 0) == initial); + + matrix = Matrix::new_(2, 1, changed); + matrix.restore(moved); + assert(matrix.rows() == 1); + assert(matrix.columns() == 1); + assert(matrix.get(0, 0) == initial); + + Matrix receiver; + receiver.restore(snapshot); + assert(!receiver.is_na()); + assert(receiver.get(0, 0) == initial); + + receiver.set(0, 0, changed); + receiver.restore(snapshot); + assert(receiver.get(0, 0) == initial); + + Matrix empty = Matrix::new_(0, 0, initial); + Snapshot empty_snapshot = empty.snapshot(); + Matrix empty_receiver; + empty_receiver.restore(empty_snapshot); + assert(!empty_receiver.is_na()); + assert(empty_receiver.rows() == 0); + assert(empty_receiver.columns() == 0); + + require_na_snapshot_error(Matrix{}); +} + +int main() { + using Snapshot = PineMatrix::Snapshot; + static_assert(std::is_copy_constructible_v); + static_assert(std::is_copy_assignable_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_move_assignable_v); + static_assert(!std::is_default_constructible_v); + + PineMatrix matrix = PineMatrix::new_(1, 1, 7.0); + PineMatrix assigned = matrix; + assigned.set(0, 0, 13.0); + assert(matrix.get(0, 0) == 7.0); + assert(assigned.get(0, 0) == 13.0); + + PineMatrix explicit_copy = matrix.copy(); + explicit_copy.set(0, 0, 99.0); + assert(matrix.get(0, 0) == 7.0); + assert(explicit_copy.get(0, 0) == 99.0); + + Snapshot snapshot = matrix.snapshot(); + Snapshot copied = snapshot; + Snapshot moved = std::move(copied); + matrix.set(0, 0, 3.0); + matrix.restore(snapshot); + assert(matrix.get(0, 0) == 7.0); + + matrix = PineMatrix::new_(2, 1, 5.0); + matrix.restore(moved); + assert(matrix.rows() == 1); + assert(matrix.columns() == 1); + assert(matrix.get(0, 0) == 7.0); + + PineMatrix receiver; + receiver.restore(snapshot); + assert(!receiver.is_na()); + assert(receiver.get(0, 0) == 7.0); + receiver.set(0, 0, 8.0); + receiver.restore(snapshot); + assert(receiver.get(0, 0) == 7.0); + + PineMatrix empty = PineMatrix::new_(0, 0, 0.0); + Snapshot empty_snapshot = empty.snapshot(); + PineMatrix empty_receiver; + empty_receiver.restore(empty_snapshot); + assert(!empty_receiver.is_na()); + assert(empty_receiver.rows() == 0); + assert(empty_receiver.columns() == 0); + + require_na_snapshot_error(PineMatrix{}); + + PineGenericMatrix generic = PineGenericMatrix::new_(1, 1, 7); + PineGenericMatrix generic_assigned = generic; + generic_assigned.set(0, 0, 13); + assert(generic.get(0, 0) == 7); + assert(generic_assigned.get(0, 0) == 13); + PineGenericMatrix generic_copy = generic.copy(); + generic_copy.set(0, 0, 99); + assert(generic.get(0, 0) == 7); + assert(generic_copy.get(0, 0) == 99); + + check_generic_snapshot_round_trip(7, 13); + check_generic_snapshot_round_trip("alpha", "beta"); + check_generic_snapshot_round_trip(true, false); +}