diff --git a/include/pineforge/generic_matrix.hpp b/include/pineforge/generic_matrix.hpp index 26cd686..8694e55 100644 --- a/include/pineforge/generic_matrix.hpp +++ b/include/pineforge/generic_matrix.hpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include namespace pineforge { @@ -128,18 +130,60 @@ template class PineGenericMatrix { using Data = std::vector>; - Data data_; - bool valid_{false}; + struct Storage { + Data data; - void require_valid() const { - if (!valid_) throw std::runtime_error("matrix operation on na ID"); + Storage() = default; + explicit Storage(Data value) : data(std::move(value)) {} + }; + + static constexpr const char* kNaIdError = + "matrix operation on na ID"; + static constexpr const char* kInvalidSnapshotError = + "matrix restore from invalid snapshot"; + + std::shared_ptr storage_; + + explicit PineGenericMatrix(Data data) + : storage_(std::make_shared(std::move(data))) {} + + Storage& require_storage() { + if (!storage_) throw std::runtime_error(kNaIdError); + return *storage_; + } + + const Storage& require_storage() const { + if (!storage_) throw std::runtime_error(kNaIdError); + return *storage_; } + Data& data() { return require_storage().data; } + const Data& data() const { return require_storage().data; } + public: + // Pine matrices are IDs. Copies and assignments alias their backing store; + // copy() is the explicit operation that creates an independent outer ID. + PineGenericMatrix() noexcept = default; + PineGenericMatrix(const PineGenericMatrix&) noexcept = default; + PineGenericMatrix& operator=(const PineGenericMatrix&) noexcept = default; + + // Preserve the source handle across C++ moves, matching Pine assignment. + PineGenericMatrix(PineGenericMatrix&& other) noexcept + : storage_(other.storage_) {} + PineGenericMatrix& operator=(PineGenericMatrix&& other) noexcept { + if (this != &other) storage_ = other.storage_; + return *this; + } + + // Snapshot copies the outer matrix state with ordinary T copy semantics. + // Primitive elements are detached values; UDT/collection handles remain + // shallow aliases so generated checkpoints can recurse through them. class Snapshot { + std::shared_ptr identity_; Data state_; - explicit Snapshot(const Data& state) : state_(state) {} + Snapshot(std::shared_ptr identity, const Data& state) + : identity_(std::move(identity)), state_(state) {} friend class PineGenericMatrix; @@ -155,11 +199,9 @@ class PineGenericMatrix { [[nodiscard]] static PineGenericMatrix new_(int rows, int cols, T init) { if (rows < 0 || cols < 0) throw std::invalid_argument("matrix.new: negative dimensions"); - PineGenericMatrix m; - m.data_.assign(static_cast(rows), - std::vector(static_cast(cols), init)); - m.valid_ = true; - return m; + Data data(static_cast(rows), + std::vector(static_cast(cols), init)); + return PineGenericMatrix(std::move(data)); } [[nodiscard]] static PineGenericMatrix new_(int rows, int cols) { @@ -167,139 +209,131 @@ class PineGenericMatrix { "matrix.new: no-init overload requires default-constructible T"); if (rows < 0 || cols < 0) throw std::invalid_argument("matrix.new: negative dimensions"); - PineGenericMatrix m; - m.data_.assign(static_cast(rows), - std::vector(static_cast(cols), T{})); - m.valid_ = true; - return m; + Data data(static_cast(rows), + std::vector(static_cast(cols), T{})); + return PineGenericMatrix(std::move(data)); } T get(int row, int col) const { - require_valid(); + const Data& values = data(); if (row < 0 || row >= rows()) throw std::out_of_range("matrix.get: row index out of range"); if (col < 0 || col >= columns()) throw std::out_of_range("matrix.get: column index out of range"); - return data_[static_cast(row)][static_cast(col)]; + return values[static_cast(row)][static_cast(col)]; } void set(int row, int col, T val) { - require_valid(); + Data& values = data(); if (row < 0 || row >= rows()) throw std::out_of_range("matrix.set: row index out of range"); if (col < 0 || col >= columns()) throw std::out_of_range("matrix.set: column index out of range"); - data_[static_cast(row)][static_cast(col)] = val; + values[static_cast(row)][static_cast(col)] = val; } void fill(T val) { - require_valid(); - for (auto& r : data_) std::fill(r.begin(), r.end(), val); + for (auto& r : data()) std::fill(r.begin(), r.end(), val); } int rows() const { - require_valid(); - return static_cast(data_.size()); + return static_cast(data().size()); } int columns() const { - require_valid(); - return data_.empty() ? 0 : static_cast(data_[0].size()); + const Data& values = data(); + return values.empty() ? 0 : static_cast(values[0].size()); } std::vector row(int idx) const { - require_valid(); + const Data& values = data(); if (idx < 0 || idx >= rows()) throw std::out_of_range("matrix.row: row index out of range"); - return data_[static_cast(idx)]; + return values[static_cast(idx)]; } std::vector col(int idx) const { - require_valid(); + const Data& values = data(); if (idx < 0 || idx >= columns()) throw std::out_of_range("matrix.col: column index out of range"); std::vector out; - out.reserve(data_.size()); - for (const auto& r : data_) out.push_back(r[static_cast(idx)]); + out.reserve(values.size()); + for (const auto& r : values) out.push_back(r[static_cast(idx)]); return out; } template >> const std::vector& row_ref(int idx) const { - require_valid(); + const Data& values = data(); if (idx < 0 || idx >= rows()) throw std::out_of_range("matrix.row_ref: row index out of range"); - return data_[static_cast(idx)]; + return values[static_cast(idx)]; } void add_row(int idx, const std::vector& values) { - require_valid(); + Data& matrix_data = data(); if (idx < 0 || idx > rows()) throw std::out_of_range("matrix.add_row: row index out of range"); - if (!data_.empty() && values.size() != static_cast(columns())) + if (!matrix_data.empty() && values.size() != static_cast(columns())) throw std::runtime_error("matrix.add_row: values size must equal columns()"); - data_.reserve(data_.size() + 1); - data_.insert(data_.begin() + idx, values); + matrix_data.reserve(matrix_data.size() + 1); + matrix_data.insert(matrix_data.begin() + idx, values); } void add_col(int idx, const std::vector& values) { - require_valid(); - if (data_.empty()) + Data& matrix_data = data(); + if (matrix_data.empty()) throw std::logic_error("matrix.add_col on empty matrix: use add_row first"); if (idx < 0 || idx > columns()) throw std::out_of_range("matrix.add_col: column index out of range"); - if (values.size() != data_.size()) + if (values.size() != matrix_data.size()) throw std::runtime_error("matrix.add_col: values size must equal rows()"); // Strong guarantee: build a new buffer, swap on success. - std::vector> next; - next.reserve(data_.size()); - for (size_t r = 0; r < data_.size(); ++r) { - std::vector row = data_[r]; + Data next; + next.reserve(matrix_data.size()); + for (size_t r = 0; r < matrix_data.size(); ++r) { + std::vector row = matrix_data[r]; row.insert(row.begin() + idx, values[r]); next.push_back(std::move(row)); } - data_.swap(next); + matrix_data.swap(next); } void remove_row(int idx) { - require_valid(); + (void)data(); if (idx < 0 || idx >= rows()) throw std::out_of_range("matrix.remove_row: row index out of range"); - detail::erase_row(data_, idx); + detail::erase_row(data(), idx); } void remove_col(int idx) { - require_valid(); + (void)data(); if (idx < 0 || idx >= columns()) throw std::out_of_range("matrix.remove_col: column index out of range"); - detail::erase_col(data_, idx); + detail::erase_col(data(), idx); } void swap_rows(int i, int j) { - require_valid(); + (void)data(); if (i < 0 || i >= rows() || j < 0 || j >= rows()) throw std::out_of_range("matrix.swap_rows: row index out of range"); - detail::swap_rows_impl(data_, i, j); + detail::swap_rows_impl(data(), i, j); } void swap_columns(int i, int j) { - require_valid(); + (void)data(); if (i < 0 || i >= columns() || j < 0 || j >= columns()) throw std::out_of_range("matrix.swap_columns: column index out of range"); - detail::swap_cols_impl(data_, i, j); + detail::swap_cols_impl(data(), i, j); } [[nodiscard]] PineGenericMatrix copy() const { - require_valid(); - PineGenericMatrix m; - m.data_ = data_; - m.valid_ = true; - return m; + return PineGenericMatrix(data()); } [[nodiscard]] PineGenericMatrix submatrix(int from_row, int to_row, int from_col, int to_col) const { - require_valid(); + (void)data(); if (from_row < 0 || to_row > rows()) throw std::out_of_range("matrix.submatrix: row index out of range"); if (from_col < 0 || to_col > columns()) @@ -308,25 +342,21 @@ class PineGenericMatrix { throw std::invalid_argument("matrix.submatrix: from_row must be <= to_row"); if (from_col > to_col) throw std::invalid_argument("matrix.submatrix: from_col must be <= to_col"); - PineGenericMatrix m; - m.data_ = detail::copy_submatrix(data_, from_row, to_row, from_col, to_col); - m.valid_ = true; - return m; + return PineGenericMatrix( + detail::copy_submatrix(data(), from_row, to_row, + from_col, to_col)); } [[nodiscard]] PineGenericMatrix transpose() const { static_assert(std::is_default_constructible_v, "matrix.transpose: requires default-constructible element type"); - require_valid(); - PineGenericMatrix m; - m.data_ = detail::transpose_impl(data_, rows(), columns(), T{}); - m.valid_ = true; - return m; + return PineGenericMatrix( + detail::transpose_impl(data(), rows(), columns(), T{})); } [[nodiscard]] PineGenericMatrix concat(const PineGenericMatrix& other, bool horizontal) const { - require_valid(); - other.require_valid(); + (void)data(); + (void)other.data(); if (horizontal) { if (rows() != other.rows()) throw std::invalid_argument("matrix.concat: row count mismatch"); @@ -335,44 +365,47 @@ class PineGenericMatrix { throw std::invalid_argument("matrix.concat: column count mismatch"); } PineGenericMatrix m = copy(); - detail::concat_impl(m.data_, other.data_, horizontal); + detail::concat_impl(m.data(), other.data(), horizontal); return m; } void reshape(int new_rows, int new_cols) { static_assert(std::is_default_constructible_v, "matrix.reshape: requires default-constructible element type"); - require_valid(); - detail::reshape_impl(data_, new_rows, new_cols, T{}); + detail::reshape_impl(data(), new_rows, new_cols, T{}); } - void reverse() { require_valid(); std::reverse(data_.begin(), data_.end()); } + void reverse() { + Data& values = data(); + std::reverse(values.begin(), values.end()); + } void sort(int column, bool ascending = true) { static_assert(std::is_same_v || std::is_same_v || std::is_same_v, "matrix.sort: requires int, bool, or std::string element type"); - require_valid(); - detail::sort_impl(data_, column, ascending); + detail::sort_impl(data(), column, ascending); } int elements_count() const { - require_valid(); - return detail::elements_count_impl(data_); + return detail::elements_count_impl(data()); } - [[nodiscard]] bool is_na() const noexcept { return !valid_; } + [[nodiscard]] bool is_na() const noexcept { return !storage_; } [[nodiscard]] Snapshot snapshot() const { - require_valid(); - return Snapshot(data_); + const Storage& storage = require_storage(); + return Snapshot(storage_, storage.data); } void restore(const Snapshot& snapshot) { + if (!snapshot.identity_) { + throw std::runtime_error(kInvalidSnapshotError); + } Data replacement(snapshot.state_); - data_.swap(replacement); - valid_ = true; + snapshot.identity_->data.swap(replacement); + storage_ = snapshot.identity_; } }; @@ -385,18 +418,54 @@ template <> class PineGenericMatrix { using Data = std::vector>; - Data data_; - bool valid_{false}; + struct Storage { + Data data; + + Storage() = default; + explicit Storage(Data value) : data(std::move(value)) {} + }; + + static constexpr const char* kNaIdError = + "matrix operation on na ID"; + static constexpr const char* kInvalidSnapshotError = + "matrix restore from invalid snapshot"; + + std::shared_ptr storage_; + + explicit PineGenericMatrix(Data data) + : storage_(std::make_shared(std::move(data))) {} - void require_valid() const { - if (!valid_) throw std::runtime_error("matrix operation on na ID"); + Storage& require_storage() { + if (!storage_) throw std::runtime_error(kNaIdError); + return *storage_; } + const Storage& require_storage() const { + if (!storage_) throw std::runtime_error(kNaIdError); + return *storage_; + } + + Data& data() { return require_storage().data; } + const Data& data() const { return require_storage().data; } + public: + PineGenericMatrix() noexcept = default; + PineGenericMatrix(const PineGenericMatrix&) noexcept = default; + PineGenericMatrix& operator=(const PineGenericMatrix&) noexcept = default; + + PineGenericMatrix(PineGenericMatrix&& other) noexcept + : storage_(other.storage_) {} + PineGenericMatrix& operator=(PineGenericMatrix&& other) noexcept { + if (this != &other) storage_ = other.storage_; + return *this; + } + class Snapshot { + std::shared_ptr identity_; Data state_; - explicit Snapshot(const Data& state) : state_(state) {} + Snapshot(std::shared_ptr identity, const Data& state) + : identity_(std::move(identity)), state_(state) {} friend class PineGenericMatrix; @@ -410,11 +479,9 @@ class PineGenericMatrix { [[nodiscard]] static PineGenericMatrix new_(int rows, int cols, bool init) { if (rows < 0 || cols < 0) throw std::invalid_argument("matrix.new: negative dimensions"); - PineGenericMatrix m; - m.data_.assign(static_cast(rows), - std::vector(static_cast(cols), init ? 1 : 0)); - m.valid_ = true; - return m; + Data data(static_cast(rows), + std::vector(static_cast(cols), init ? 1 : 0)); + return PineGenericMatrix(std::move(data)); } [[nodiscard]] static PineGenericMatrix new_(int rows, int cols) { @@ -422,56 +489,54 @@ class PineGenericMatrix { } bool get(int row, int col) const { - require_valid(); + const Data& values = data(); if (row < 0 || row >= rows()) throw std::out_of_range("matrix.get: row index out of range"); if (col < 0 || col >= columns()) throw std::out_of_range("matrix.get: column index out of range"); - return data_[static_cast(row)][static_cast(col)] != 0; + return values[static_cast(row)][static_cast(col)] != 0; } void set(int row, int col, bool val) { - require_valid(); + Data& values = data(); if (row < 0 || row >= rows()) throw std::out_of_range("matrix.set: row index out of range"); if (col < 0 || col >= columns()) throw std::out_of_range("matrix.set: column index out of range"); - data_[static_cast(row)][static_cast(col)] = val ? 1 : 0; + values[static_cast(row)][static_cast(col)] = val ? 1 : 0; } void fill(bool val) { - require_valid(); char c = val ? 1 : 0; - for (auto& r : data_) std::fill(r.begin(), r.end(), c); + for (auto& r : data()) std::fill(r.begin(), r.end(), c); } int rows() const { - require_valid(); - return static_cast(data_.size()); + return static_cast(data().size()); } int columns() const { - require_valid(); - return data_.empty() ? 0 : static_cast(data_[0].size()); + const Data& values = data(); + return values.empty() ? 0 : static_cast(values[0].size()); } std::vector row(int idx) const { - require_valid(); + const Data& values = data(); if (idx < 0 || idx >= rows()) throw std::out_of_range("matrix.row: row index out of range"); std::vector out; - const auto& src = data_[static_cast(idx)]; + const auto& src = values[static_cast(idx)]; out.reserve(src.size()); for (char c : src) out.push_back(c != 0); return out; } std::vector col(int idx) const { - require_valid(); + const Data& values = data(); if (idx < 0 || idx >= columns()) throw std::out_of_range("matrix.col: column index out of range"); std::vector out; - out.reserve(data_.size()); - for (const auto& r : data_) out.push_back(r[static_cast(idx)] != 0); + out.reserve(values.size()); + for (const auto& r : values) out.push_back(r[static_cast(idx)] != 0); return out; } @@ -479,75 +544,71 @@ class PineGenericMatrix { // returning const std::vector&. void add_row(int idx, const std::vector& values) { - require_valid(); + Data& matrix_data = data(); if (idx < 0 || idx > rows()) throw std::out_of_range("matrix.add_row: row index out of range"); - if (!data_.empty() && values.size() != static_cast(columns())) + if (!matrix_data.empty() && values.size() != static_cast(columns())) throw std::runtime_error("matrix.add_row: values size must equal columns()"); std::vector row; row.reserve(values.size()); for (bool v : values) row.push_back(v ? 1 : 0); - data_.reserve(data_.size() + 1); - data_.insert(data_.begin() + idx, std::move(row)); + matrix_data.reserve(matrix_data.size() + 1); + matrix_data.insert(matrix_data.begin() + idx, std::move(row)); } void add_col(int idx, const std::vector& values) { - require_valid(); - if (data_.empty()) + Data& matrix_data = data(); + if (matrix_data.empty()) throw std::logic_error("matrix.add_col on empty matrix: use add_row first"); if (idx < 0 || idx > columns()) throw std::out_of_range("matrix.add_col: column index out of range"); - if (values.size() != data_.size()) + if (values.size() != matrix_data.size()) throw std::runtime_error("matrix.add_col: values size must equal rows()"); - std::vector> next; - next.reserve(data_.size()); - for (size_t r = 0; r < data_.size(); ++r) { - std::vector row = data_[r]; + Data next; + next.reserve(matrix_data.size()); + for (size_t r = 0; r < matrix_data.size(); ++r) { + std::vector row = matrix_data[r]; row.insert(row.begin() + idx, values[r] ? 1 : 0); next.push_back(std::move(row)); } - data_.swap(next); + matrix_data.swap(next); } void remove_row(int idx) { - require_valid(); + (void)data(); if (idx < 0 || idx >= rows()) throw std::out_of_range("matrix.remove_row: row index out of range"); - detail::erase_row(data_, idx); + detail::erase_row(data(), idx); } void remove_col(int idx) { - require_valid(); + (void)data(); if (idx < 0 || idx >= columns()) throw std::out_of_range("matrix.remove_col: column index out of range"); - detail::erase_col(data_, idx); + detail::erase_col(data(), idx); } void swap_rows(int i, int j) { - require_valid(); + (void)data(); if (i < 0 || i >= rows() || j < 0 || j >= rows()) throw std::out_of_range("matrix.swap_rows: row index out of range"); - detail::swap_rows_impl(data_, i, j); + detail::swap_rows_impl(data(), i, j); } void swap_columns(int i, int j) { - require_valid(); + (void)data(); if (i < 0 || i >= columns() || j < 0 || j >= columns()) throw std::out_of_range("matrix.swap_columns: column index out of range"); - detail::swap_cols_impl(data_, i, j); + detail::swap_cols_impl(data(), i, j); } [[nodiscard]] PineGenericMatrix copy() const { - require_valid(); - PineGenericMatrix m; - m.data_ = data_; - m.valid_ = true; - return m; + return PineGenericMatrix(data()); } [[nodiscard]] PineGenericMatrix submatrix(int from_row, int to_row, int from_col, int to_col) const { - require_valid(); + (void)data(); if (from_row < 0 || to_row > rows()) throw std::out_of_range("matrix.submatrix: row index out of range"); if (from_col < 0 || to_col > columns()) @@ -556,30 +617,29 @@ class PineGenericMatrix { throw std::invalid_argument("matrix.submatrix: from_row must be <= to_row"); if (from_col > to_col) throw std::invalid_argument("matrix.submatrix: from_col must be <= to_col"); - PineGenericMatrix m; - m.data_ = detail::copy_submatrix(data_, from_row, to_row, from_col, to_col); - m.valid_ = true; - return m; + return PineGenericMatrix( + detail::copy_submatrix(data(), from_row, to_row, + from_col, to_col)); } void reshape(int new_rows, int new_cols) { - require_valid(); - detail::reshape_impl(data_, new_rows, new_cols, static_cast(0)); + detail::reshape_impl(data(), new_rows, new_cols, static_cast(0)); } - void reverse() { require_valid(); std::reverse(data_.begin(), data_.end()); } + void reverse() { + Data& values = data(); + std::reverse(values.begin(), values.end()); + } [[nodiscard]] PineGenericMatrix transpose() const { - require_valid(); - PineGenericMatrix m; - m.data_ = detail::transpose_impl(data_, rows(), columns(), static_cast(0)); - m.valid_ = true; - return m; + return PineGenericMatrix( + detail::transpose_impl(data(), rows(), columns(), + static_cast(0))); } [[nodiscard]] PineGenericMatrix concat(const PineGenericMatrix& other, bool horizontal) const { - require_valid(); - other.require_valid(); + (void)data(); + (void)other.data(); if (horizontal) { if (rows() != other.rows()) throw std::invalid_argument("matrix.concat: row count mismatch"); @@ -588,33 +648,35 @@ class PineGenericMatrix { throw std::invalid_argument("matrix.concat: column count mismatch"); } PineGenericMatrix m = copy(); - detail::concat_impl(m.data_, other.data_, horizontal); + detail::concat_impl(m.data(), other.data(), horizontal); return m; } template void sort(int /*column*/, bool /*ascending*/ = true) { - require_valid(); + (void)data(); static_assert(!std::is_same_v && std::is_same_v, "matrix.sort: not supported on bool element type"); } int elements_count() const { - require_valid(); - return detail::elements_count_impl(data_); + return detail::elements_count_impl(data()); } - [[nodiscard]] bool is_na() const noexcept { return !valid_; } + [[nodiscard]] bool is_na() const noexcept { return !storage_; } [[nodiscard]] Snapshot snapshot() const { - require_valid(); - return Snapshot(data_); + const Storage& storage = require_storage(); + return Snapshot(storage_, storage.data); } void restore(const Snapshot& snapshot) { + if (!snapshot.identity_) { + throw std::runtime_error(kInvalidSnapshotError); + } Data replacement(snapshot.state_); - data_.swap(replacement); - valid_ = true; + snapshot.identity_->data.swap(replacement); + storage_ = snapshot.identity_; } }; diff --git a/include/pineforge/matrix.hpp b/include/pineforge/matrix.hpp index 950d83c..22b2944 100644 --- a/include/pineforge/matrix.hpp +++ b/include/pineforge/matrix.hpp @@ -1,24 +1,61 @@ #pragma once #include +#include #include #include +#include namespace pineforge { class PineMatrix { - Eigen::MatrixXd data_; - bool valid_{false}; + struct Storage { + Eigen::MatrixXd data; - void require_valid() const; + Storage() = default; + explicit Storage(Eigen::MatrixXd value) + : data(std::move(value)) {} + }; + + static constexpr const char* kNaIdError = + "matrix operation on na ID"; + static constexpr const char* kInvalidSnapshotError = + "matrix restore from invalid snapshot"; + + std::shared_ptr storage_; + + explicit PineMatrix(Eigen::MatrixXd data) + : storage_(std::make_shared(std::move(data))) {} + + [[nodiscard]] Storage& require_storage(); + [[nodiscard]] const Storage& require_storage() 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. + // Pine matrices are reference IDs. Ordinary C++ copies and assignments + // therefore alias one backing store; matrix.copy() is the operation that + // allocates an independent outer ID. + PineMatrix() noexcept = default; + PineMatrix(const PineMatrix&) noexcept = default; + PineMatrix& operator=(const PineMatrix&) noexcept = default; + + // Treat C++ moves like Pine assignment as well. A compiler-generated move + // must not turn the source handle into na. + PineMatrix(PineMatrix&& other) noexcept : storage_(other.storage_) {} + PineMatrix& operator=(PineMatrix&& other) noexcept { + if (this != &other) storage_ = other.storage_; + return *this; + } + + // Opaque generated-checkpoint hook. The snapshot owns a detached value + // copy of the matrix contents and retains the original ID. restore() + // mutates that ID in place before reattaching the receiving handle, so all + // aliases observe rollback even if the receiver was rebound or set to na. class Snapshot { + std::shared_ptr identity_; Eigen::MatrixXd state_; - explicit Snapshot(const Eigen::MatrixXd& state) : state_(state) {} + Snapshot(std::shared_ptr identity, + const Eigen::MatrixXd& state) + : identity_(std::move(identity)), state_(state) {} friend class PineMatrix; @@ -89,7 +126,7 @@ class PineMatrix { // Pine matrix ID state. A default-constructed value represents ``na``; // matrix.new() returns a valid ID even when its dimensions are 0x0. - [[nodiscard]] bool is_na() const noexcept { return !valid_; } + [[nodiscard]] bool is_na() const noexcept { return !storage_; } [[nodiscard]] Snapshot snapshot() const; void restore(const Snapshot& snapshot); @@ -107,8 +144,8 @@ class PineMatrix { bool is_zero() const; // Access to internal data for friend operations - const Eigen::MatrixXd& data() const { require_valid(); return data_; } - Eigen::MatrixXd& data() { require_valid(); return data_; } + const Eigen::MatrixXd& data() const { return require_storage().data; } + Eigen::MatrixXd& data() { return require_storage().data; } }; inline bool is_na(const PineMatrix& matrix) noexcept { diff --git a/src/matrix.cpp b/src/matrix.cpp index 315f1b5..2329e44 100644 --- a/src/matrix.cpp +++ b/src/matrix.cpp @@ -12,133 +12,127 @@ static constexpr double EPS = 1e-10; // ── Construction ──────────────────────────────────────────────────────────── PineMatrix PineMatrix::new_(int rows, int cols, double init_val) { - PineMatrix m; - m.data_ = Eigen::MatrixXd::Constant(rows, cols, init_val); - m.valid_ = true; - return m; + return PineMatrix(Eigen::MatrixXd::Constant(rows, cols, init_val)); } -void PineMatrix::require_valid() const { - if (!valid_) throw std::runtime_error("matrix operation on na ID"); +PineMatrix::Storage& PineMatrix::require_storage() { + if (!storage_) throw std::runtime_error(kNaIdError); + return *storage_; +} + +const PineMatrix::Storage& PineMatrix::require_storage() const { + if (!storage_) throw std::runtime_error(kNaIdError); + return *storage_; } PineMatrix::Snapshot PineMatrix::snapshot() const { - require_valid(); - return Snapshot(data_); + const Storage& storage = require_storage(); + return Snapshot(storage_, storage.data); } void PineMatrix::restore(const Snapshot& snapshot) { + if (!snapshot.identity_) { + throw std::runtime_error(kInvalidSnapshotError); + } + // Clone before touching the live ID, then swap the fully-built state into + // the original backing store. Existing aliases keep that exact identity. Eigen::MatrixXd replacement(snapshot.state_); - data_.swap(replacement); - valid_ = true; + using std::swap; + swap(snapshot.identity_->data, replacement); + storage_ = snapshot.identity_; } // ── Access ────────────────────────────────────────────────────────────────── double PineMatrix::get(int row, int col) const { - require_valid(); - return data_(row, col); + return data()(row, col); } void PineMatrix::set(int row, int col, double val) { - require_valid(); - data_(row, col) = val; + data()(row, col) = val; } void PineMatrix::fill(double val) { - require_valid(); - data_.setConstant(val); + data().setConstant(val); } std::vector PineMatrix::row(int idx) const { - require_valid(); - Eigen::VectorXd r = data_.row(idx); + Eigen::VectorXd r = data().row(idx); return std::vector(r.data(), r.data() + r.size()); } std::vector PineMatrix::col(int idx) const { - require_valid(); - Eigen::VectorXd c = data_.col(idx); + Eigen::VectorXd c = data().col(idx); return std::vector(c.data(), c.data() + c.size()); } // ── Row/Col ops ───────────────────────────────────────────────────────────── int PineMatrix::rows() const { - require_valid(); - return static_cast(data_.rows()); + return static_cast(data().rows()); } int PineMatrix::columns() const { - require_valid(); - return static_cast(data_.cols()); + return static_cast(data().cols()); } void PineMatrix::add_row(int idx, const std::vector& values) { int r = rows(), c = columns(); if (static_cast(values.size()) != c) throw std::invalid_argument("add_row: values size mismatch"); - data_.conservativeResize(r + 1, c); + data().conservativeResize(r + 1, c); // shift rows down from bottom to idx for (int i = r; i > idx; --i) - data_.row(i) = data_.row(i - 1); + data().row(i) = data().row(i - 1); for (int j = 0; j < c; ++j) - data_(idx, j) = values[j]; + data()(idx, j) = values[j]; } void PineMatrix::add_col(int idx, const std::vector& values) { int r = rows(), c = columns(); if (static_cast(values.size()) != r) throw std::invalid_argument("add_col: values size mismatch"); - data_.conservativeResize(r, c + 1); + data().conservativeResize(r, c + 1); for (int j = c; j > idx; --j) - data_.col(j) = data_.col(j - 1); + data().col(j) = data().col(j - 1); for (int i = 0; i < r; ++i) - data_(i, idx) = values[i]; + data()(i, idx) = values[i]; } void PineMatrix::remove_row(int idx) { int r = rows(), c = columns(); for (int i = idx; i < r - 1; ++i) - data_.row(i) = data_.row(i + 1); - data_.conservativeResize(r - 1, c); + data().row(i) = data().row(i + 1); + data().conservativeResize(r - 1, c); } void PineMatrix::remove_col(int idx) { int r = rows(), c = columns(); for (int j = idx; j < c - 1; ++j) - data_.col(j) = data_.col(j + 1); - data_.conservativeResize(r, c - 1); + data().col(j) = data().col(j + 1); + data().conservativeResize(r, c - 1); } // ── Swap ──────────────────────────────────────────────────────────────────── void PineMatrix::swap_rows(int i, int j) { - require_valid(); - data_.row(i).swap(data_.row(j)); + data().row(i).swap(data().row(j)); } void PineMatrix::swap_columns(int i, int j) { - require_valid(); - data_.col(i).swap(data_.col(j)); + data().col(i).swap(data().col(j)); } // ── Transform ─────────────────────────────────────────────────────────────── PineMatrix PineMatrix::copy() const { - require_valid(); - PineMatrix m; - m.data_ = data_; - m.valid_ = true; - return m; + return PineMatrix(data()); } PineMatrix PineMatrix::submatrix(int from_row, int to_row, int from_col, int to_col) const { - require_valid(); - PineMatrix m; - m.data_ = data_.block(from_row, from_col, to_row - from_row, to_col - from_col); - m.valid_ = true; - return m; + return PineMatrix(Eigen::MatrixXd( + data().block(from_row, from_col, + to_row - from_row, to_col - from_col))); } void PineMatrix::reshape(int r, int c) { @@ -149,26 +143,21 @@ void PineMatrix::reshape(int r, int c) { flat.reserve(r * c); for (int i = 0; i < rows(); ++i) for (int j = 0; j < columns(); ++j) - flat.push_back(data_(i, j)); - data_.resize(r, c); + flat.push_back(data()(i, j)); + data().resize(r, c); int k = 0; for (int i = 0; i < r; ++i) for (int j = 0; j < c; ++j) - data_(i, j) = flat[k++]; + data()(i, j) = flat[k++]; } void PineMatrix::reverse() { - require_valid(); - Eigen::MatrixXd tmp = data_.colwise().reverse(); - data_ = tmp.rowwise().reverse(); + Eigen::MatrixXd tmp = data().colwise().reverse(); + data() = tmp.rowwise().reverse(); } PineMatrix PineMatrix::transpose() const { - require_valid(); - PineMatrix m; - m.data_ = data_.transpose(); - m.valid_ = true; - return m; + return PineMatrix(Eigen::MatrixXd(data().transpose())); } void PineMatrix::sort(int column, bool ascending) { @@ -177,43 +166,39 @@ void PineMatrix::sort(int column, bool ascending) { std::vector indices(r); for (int i = 0; i < r; ++i) indices[i] = i; std::sort(indices.begin(), indices.end(), [&](int a, int b) { - return ascending ? data_(a, column) < data_(b, column) - : data_(a, column) > data_(b, column); + return ascending ? data()(a, column) < data()(b, column) + : data()(a, column) > data()(b, column); }); Eigen::MatrixXd sorted(r, columns()); for (int i = 0; i < r; ++i) - sorted.row(i) = data_.row(indices[i]); - data_ = sorted; + sorted.row(i) = data().row(indices[i]); + data() = sorted; } PineMatrix PineMatrix::concat(const PineMatrix& other, bool horizontal) const { - require_valid(); - other.require_valid(); - PineMatrix m; + Eigen::MatrixXd result; if (horizontal) { - m.data_.resize(rows(), columns() + other.columns()); - m.data_ << data_, other.data_; + result.resize(rows(), columns() + other.columns()); + result << data(), other.data(); } else { - m.data_.resize(rows() + other.rows(), columns()); - m.data_ << data_, other.data_; + result.resize(rows() + other.rows(), columns()); + result << data(), other.data(); } - m.valid_ = true; - return m; + return PineMatrix(std::move(result)); } // ── Aggregation ───────────────────────────────────────────────────────────── -double PineMatrix::avg() const { require_valid(); return data_.mean(); } -double PineMatrix::min() const { require_valid(); return data_.minCoeff(); } -double PineMatrix::max() const { require_valid(); return data_.maxCoeff(); } -double PineMatrix::sum() const { require_valid(); return data_.sum(); } +double PineMatrix::avg() const { return data().mean(); } +double PineMatrix::min() const { return data().minCoeff(); } +double PineMatrix::max() const { return data().maxCoeff(); } +double PineMatrix::sum() const { return data().sum(); } double PineMatrix::mode() const { - require_valid(); std::map freq; for (int i = 0; i < rows(); ++i) for (int j = 0; j < columns(); ++j) - freq[data_(i, j)]++; + freq[data()(i, j)]++; int best = 0; double result = 0; for (auto& [val, cnt] : freq) { @@ -236,88 +221,66 @@ bool all_coeffs_finite(const Eigen::MatrixXd& m) { // ── Arithmetic ────────────────────────────────────────────────────────────── PineMatrix PineMatrix::diff(const PineMatrix& other) const { - require_valid(); - other.require_valid(); - PineMatrix m; - m.data_ = data_ - other.data_; - m.valid_ = true; - return m; + return PineMatrix(Eigen::MatrixXd(data() - other.data())); } PineMatrix PineMatrix::mult(const PineMatrix& other) const { - require_valid(); - other.require_valid(); - PineMatrix m; - m.data_ = data_ * other.data_; - m.valid_ = true; - return m; + return PineMatrix(Eigen::MatrixXd(data() * other.data())); } PineMatrix PineMatrix::pow(int n) const { if (!is_square()) return PineMatrix::new_(0, 0, 0.0); - if (!all_coeffs_finite(data_)) + if (!all_coeffs_finite(data())) return PineMatrix::new_(0, 0, 0.0); // start with identity - PineMatrix result; - result.data_ = Eigen::MatrixXd::Identity(rows(), rows()); - result.valid_ = true; + PineMatrix result(Eigen::MatrixXd::Identity(rows(), rows())); for (int i = 0; i < n; ++i) - result.data_ = result.data_ * data_; + result.data() = result.data() * data(); return result; } // ── Linear algebra ────────────────────────────────────────────────────────── double PineMatrix::det() const { - require_valid(); if (!is_square()) return std::numeric_limits::quiet_NaN(); - if (!all_coeffs_finite(data_)) + if (!all_coeffs_finite(data())) return std::numeric_limits::quiet_NaN(); - return data_.determinant(); + return data().determinant(); } PineMatrix PineMatrix::inv() const { - require_valid(); if (!is_square()) return PineMatrix::new_(0, 0, 0.0); - if (!all_coeffs_finite(data_)) + if (!all_coeffs_finite(data())) return PineMatrix::new_(0, 0, 0.0); - PineMatrix m; - m.data_ = data_.inverse(); - m.valid_ = true; - return m; + return PineMatrix(Eigen::MatrixXd(data().inverse())); } PineMatrix PineMatrix::pinv() const { - require_valid(); - if (!all_coeffs_finite(data_)) + if (!all_coeffs_finite(data())) return PineMatrix::new_(0, 0, 0.0); - PineMatrix m; - m.data_ = data_.completeOrthogonalDecomposition().pseudoInverse(); - m.valid_ = true; - return m; + return PineMatrix(Eigen::MatrixXd( + data().completeOrthogonalDecomposition().pseudoInverse())); } int PineMatrix::rank() const { - require_valid(); - if (!all_coeffs_finite(data_)) + if (!all_coeffs_finite(data())) return 0; - return static_cast(Eigen::FullPivLU(data_).rank()); + return static_cast(Eigen::FullPivLU(data()).rank()); } -double PineMatrix::trace() const { require_valid(); return data_.trace(); } +double PineMatrix::trace() const { return data().trace(); } std::vector PineMatrix::eigenvalues() const { - require_valid(); if (!is_square()) return {}; - if (!all_coeffs_finite(data_)) + if (!all_coeffs_finite(data())) return {}; if (is_symmetric()) { Eigen::SelfAdjointEigenSolver solver( - data_, Eigen::EigenvaluesOnly); + data(), Eigen::EigenvaluesOnly); if (solver.info() != Eigen::Success) return {}; auto ev = solver.eigenvalues(); @@ -327,7 +290,7 @@ std::vector PineMatrix::eigenvalues() const { std::sort(result.begin(), result.end(), std::greater()); return result; } - Eigen::EigenSolver solver(data_); + Eigen::EigenSolver solver(data()); if (solver.info() != Eigen::Success) return {}; auto ev = solver.eigenvalues(); @@ -341,52 +304,43 @@ std::vector PineMatrix::eigenvalues() const { PineMatrix PineMatrix::eigenvectors() const { if (!is_square()) return PineMatrix::new_(0, 0, 0.0); - if (!all_coeffs_finite(data_)) + if (!all_coeffs_finite(data())) return PineMatrix::new_(0, 0, 0.0); if (is_symmetric()) { Eigen::SelfAdjointEigenSolver solver( - data_, Eigen::ComputeEigenvectors); + data(), Eigen::ComputeEigenvectors); if (solver.info() != Eigen::Success) return PineMatrix::new_(0, 0, 0.0); - PineMatrix m; - m.data_ = solver.eigenvectors(); - m.valid_ = true; - return m; + return PineMatrix(Eigen::MatrixXd(solver.eigenvectors())); } - Eigen::EigenSolver solver(data_); + Eigen::EigenSolver solver(data()); if (solver.info() != Eigen::Success) return PineMatrix::new_(0, 0, 0.0); auto ev = solver.eigenvectors(); - PineMatrix m; - m.data_.resize(ev.rows(), ev.cols()); - m.valid_ = true; + Eigen::MatrixXd result(ev.rows(), ev.cols()); for (Eigen::Index i = 0; i < ev.rows(); ++i) for (Eigen::Index j = 0; j < ev.cols(); ++j) - m.data_(i, j) = ev(i, j).real(); - return m; + result(i, j) = ev(i, j).real(); + return PineMatrix(std::move(result)); } // ── Kronecker ─────────────────────────────────────────────────────────────── PineMatrix PineMatrix::kron(const PineMatrix& other) const { - require_valid(); - other.require_valid(); int ar = rows(), ac = columns(); int br = other.rows(), bc = other.columns(); - PineMatrix m; - m.data_.resize(ar * br, ac * bc); - m.valid_ = true; + Eigen::MatrixXd result(ar * br, ac * bc); for (int i = 0; i < ar * br; ++i) for (int j = 0; j < ac * bc; ++j) - m.data_(i, j) = data_(i / br, j / bc) * other.data_(i % br, j % bc); - return m; + result(i, j) = data()(i / br, j / bc) * + other.data()(i % br, j % bc); + return PineMatrix(std::move(result)); } // ── Count ─────────────────────────────────────────────────────────────────── int PineMatrix::elements_count() const { - require_valid(); - return static_cast(data_.size()); + return static_cast(data().size()); } // ── Properties ────────────────────────────────────────────────────────────── @@ -395,13 +349,13 @@ bool PineMatrix::is_square() const { return rows() == columns(); } bool PineMatrix::is_identity() const { if (!is_square()) return false; - return data_.isApprox(Eigen::MatrixXd::Identity(rows(), rows()), EPS); + return data().isApprox(Eigen::MatrixXd::Identity(rows(), rows()), EPS); } bool PineMatrix::is_diagonal() const { for (int i = 0; i < rows(); ++i) for (int j = 0; j < columns(); ++j) - if (i != j && std::abs(data_(i, j)) > EPS) return false; + if (i != j && std::abs(data()(i, j)) > EPS) return false; return true; } @@ -410,26 +364,26 @@ bool PineMatrix::is_antidiagonal() const { int n = rows(); for (int i = 0; i < n; ++i) for (int j = 0; j < n; ++j) - if (i + j != n - 1 && std::abs(data_(i, j)) > EPS) return false; + if (i + j != n - 1 && std::abs(data()(i, j)) > EPS) return false; return true; } bool PineMatrix::is_symmetric() const { if (!is_square()) return false; - return data_.isApprox(data_.transpose(), EPS); + return data().isApprox(data().transpose(), EPS); } bool PineMatrix::is_antisymmetric() const { if (!is_square()) return false; - return data_.isApprox(-data_.transpose(), EPS); + return data().isApprox(-data().transpose(), EPS); } bool PineMatrix::is_triangular() const { bool upper = true, lower = true; for (int i = 0; i < rows(); ++i) for (int j = 0; j < columns(); ++j) { - if (i > j && std::abs(data_(i, j)) > EPS) upper = false; - if (i < j && std::abs(data_(i, j)) > EPS) lower = false; + if (i > j && std::abs(data()(i, j)) > EPS) upper = false; + if (i < j && std::abs(data()(i, j)) > EPS) lower = false; } return upper || lower; } @@ -438,8 +392,8 @@ bool PineMatrix::is_stochastic() const { for (int i = 0; i < rows(); ++i) { double s = 0; for (int j = 0; j < columns(); ++j) { - if (data_(i, j) < -EPS) return false; - s += data_(i, j); + if (data()(i, j) < -EPS) return false; + s += data()(i, j); } if (std::abs(s - 1.0) > EPS) return false; } @@ -449,14 +403,14 @@ bool PineMatrix::is_stochastic() const { bool PineMatrix::is_binary() const { for (int i = 0; i < rows(); ++i) for (int j = 0; j < columns(); ++j) - if (std::abs(data_(i, j)) > EPS && std::abs(data_(i, j) - 1.0) > EPS) + if (std::abs(data()(i, j)) > EPS && + std::abs(data()(i, j) - 1.0) > EPS) return false; return true; } bool PineMatrix::is_zero() const { - require_valid(); - return data_.isZero(EPS); + return data().isZero(EPS); } } // namespace pineforge diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9d915f7..6c151c6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,6 +15,7 @@ set(TEST_SOURCES test_ta_rma_warmup test_ta_na_source_rules test_matrix + test_matrix_identity test_matrix_na test_matrix_snapshot_compat test_session_time diff --git a/tests/test_generic_matrix_series.cpp b/tests/test_generic_matrix_series.cpp index e63cc8b..055cd61 100644 --- a/tests/test_generic_matrix_series.cpp +++ b/tests/test_generic_matrix_series.cpp @@ -38,26 +38,30 @@ static void test_series_ring_buffer_wraparound() { assert(s[2].get(0, 0) == 3); } -static void test_series_value_semantic_aliasing() { +static void test_series_preserves_matrix_id_aliasing() { Series> s; auto m1 = PineGenericMatrix::new_(1, 1, 10); s.push(m1); auto m2 = PineGenericMatrix::new_(1, 1, 20); s.push(m2); - // mutate the source m2 after push — series must hold its own copy. + // Series stores the matrix ID assigned on each bar. Mutating that ID after + // the push is visible through the current history slot. m2.set(0, 0, 999); - assert(s[0].get(0, 0) == 20); - // mutating s[0] copy must not bleed into s[1]. + assert(s[0].get(0, 0) == 999); + + // Reading a history slot returns another handle to the same ID. auto cur = s[0]; cur.set(0, 0, 777); + assert(s[0].get(0, 0) == 777); + + // The prior bar was assigned a different matrix ID and stays independent. assert(s[1].get(0, 0) == 10); - assert(s[0].get(0, 0) == 20); // s itself unchanged by local mutation } int main() { test_series_of_generic_matrix(); test_series_ring_buffer_wraparound(); - test_series_value_semantic_aliasing(); + test_series_preserves_matrix_id_aliasing(); std::printf("All test_generic_matrix_series tests passed.\n"); return 0; } diff --git a/tests/test_matrix_identity.cpp b/tests/test_matrix_identity.cpp new file mode 100644 index 0000000..7016288 --- /dev/null +++ b/tests/test_matrix_identity.cpp @@ -0,0 +1,263 @@ +#include +#include +#include + +#include +#include +#include +#include +#include + +using pineforge::PineGenericMatrix; +using pineforge::PineMatrix; + +static void require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template +static void require_runtime_error(Fn&& fn, const char* expected) { + try { + fn(); + } catch (const std::runtime_error& error) { + require(std::string(error.what()) == expected, + "runtime error message mismatch"); + return; + } + throw std::runtime_error("expected runtime error was not thrown"); +} + +static void test_float_assignment_aliases_and_explicit_copy_detaches() { + static_assert(std::is_nothrow_default_constructible_v); + static_assert(std::is_nothrow_copy_constructible_v); + static_assert(std::is_nothrow_copy_assignable_v); + static_assert(std::is_nothrow_move_constructible_v); + static_assert(std::is_nothrow_move_assignable_v); + + auto original = PineMatrix::new_(1, 1, 1.0); + auto alias = original; + alias.set(0, 0, 2.0); + require(original.get(0, 0) == 2.0, + "ordinary float-matrix copy must alias"); + + auto assigned = PineMatrix::new_(1, 1, 90.0); + auto discarded_identity = assigned; + assigned = original; + assigned.set(0, 0, 3.0); + require(original.get(0, 0) == 3.0, + "ordinary float-matrix assignment must alias"); + require(discarded_identity.get(0, 0) == 90.0, + "rebinding must not mutate the discarded identity"); + + auto detached = original.copy(); + detached.set(0, 0, 4.0); + require(original.get(0, 0) == 3.0, + "matrix.copy must allocate an independent outer ID"); + require(detached.get(0, 0) == 4.0, + "matrix.copy result must remain mutable"); +} + +static void test_float_moves_preserve_source_handle() { + auto source = PineMatrix::new_(1, 1, 5.0); + PineMatrix constructed(std::move(source)); + constructed.set(0, 0, 6.0); + require(!source.is_na() && source.get(0, 0) == 6.0, + "move construction must preserve the source matrix ID"); + + auto assigned = PineMatrix::new_(1, 1, 70.0); + auto old_assigned_identity = assigned; + assigned = std::move(source); + assigned.set(0, 0, 8.0); + require(source.get(0, 0) == 8.0 && constructed.get(0, 0) == 8.0, + "move assignment must preserve all aliases"); + require(old_assigned_identity.get(0, 0) == 70.0, + "move assignment must only rebind the receiver"); +} + +template +static void check_generic_assignment_copy_and_move( + Value initial, Value aliased, Value detached_value, Factory&& make) { + static_assert(std::is_nothrow_default_constructible_v); + static_assert(std::is_nothrow_copy_constructible_v); + static_assert(std::is_nothrow_copy_assignable_v); + static_assert(std::is_nothrow_move_constructible_v); + static_assert(std::is_nothrow_move_assignable_v); + + auto original = make(initial); + auto alias = original; + alias.set(0, 0, aliased); + require(original.get(0, 0) == aliased, + "ordinary generic-matrix copy must alias"); + + auto detached = original.copy(); + detached.set(0, 0, detached_value); + require(original.get(0, 0) == aliased, + "generic matrix.copy must detach the outer ID"); + require(detached.get(0, 0) == detached_value, + "detached generic matrix must hold its own value"); + + Matrix moved(std::move(original)); + moved.set(0, 0, initial); + require(!original.is_na() && original.get(0, 0) == initial, + "generic-matrix move must preserve the source ID"); + require(alias.get(0, 0) == initial, + "generic-matrix move must preserve existing aliases"); +} + +static void test_generic_assignment_copy_and_move_semantics() { + check_generic_assignment_copy_and_move>( + 1, 2, 3, + [](int value) { return PineGenericMatrix::new_(1, 1, value); }); + check_generic_assignment_copy_and_move>( + false, true, false, + [](bool value) { return PineGenericMatrix::new_(1, 1, value); }); + check_generic_assignment_copy_and_move>( + std::string("a"), std::string("b"), std::string("c"), + [](const std::string& value) { + return PineGenericMatrix::new_(1, 1, value); + }); +} + +template +static void check_snapshot_identity(Value initial, Value mutated, + Value replacement_value, + Factory&& make) { + Matrix null_matrix; + require_runtime_error( + [&] { (void)null_matrix.snapshot(); }, + "matrix operation on na ID"); + + auto live = make(initial); + auto alias = live; + auto snapshot = live.snapshot(); + + alias.set(0, 0, mutated); + alias.add_row(1, {mutated}); + require(alias.rows() == 2, "snapshot test mutation must change shape"); + + auto replacement = make(replacement_value); + auto replacement_alias = replacement; + live = replacement; + live.restore(snapshot); + + require(live.rows() == 1 && live.columns() == 1, + "restore must recover the snapshotted shape"); + require(live.get(0, 0) == initial && alias.get(0, 0) == initial, + "restore must mutate the original identity in place"); + live.set(0, 0, mutated); + require(alias.get(0, 0) == mutated, + "restored receiver must reattach to the original identity"); + require(replacement_alias.get(0, 0) == replacement_value, + "restore must not mutate a replacement identity"); + + live = Matrix{}; + require(live.is_na(), "test receiver must be rebound to na"); + live.restore(snapshot); + require(!live.is_na() && live.get(0, 0) == initial, + "restore must reattach a receiver rebound to na"); + require(alias.get(0, 0) == initial, + "reused snapshot must roll back existing aliases again"); + + auto active_snapshot = std::move(snapshot); + require_runtime_error( + [&] { live.restore(snapshot); }, + "matrix restore from invalid snapshot"); + live.restore(active_snapshot); + require(live.get(0, 0) == initial, + "moved snapshot must retain its rollback state"); +} + +static void test_snapshot_restores_contents_and_identity() { + check_snapshot_identity( + 1.0, 2.0, 9.0, + [](double value) { return PineMatrix::new_(1, 1, value); }); + check_snapshot_identity>( + 1, 2, 9, + [](int value) { return PineGenericMatrix::new_(1, 1, value); }); + check_snapshot_identity>( + false, true, true, + [](bool value) { return PineGenericMatrix::new_(1, 1, value); }); +} + +template +static void check_two_alias_snapshots_restore_one_identity( + Value initial, Value changed, Factory&& make) { + auto first = make(initial); + auto second = first; + auto first_snapshot = first.snapshot(); + auto second_snapshot = second.snapshot(); + + first.set(0, 0, changed); + first = make(changed); + second = make(changed); + + first.restore(first_snapshot); + second.restore(second_snapshot); + second.set(0, 0, changed); + require(first.get(0, 0) == changed, + "two restored aliases must converge on one original identity"); +} + +static void test_two_alias_snapshots_restore_one_identity() { + check_two_alias_snapshots_restore_one_identity( + 1.0, 2.0, + [](double value) { return PineMatrix::new_(1, 1, value); }); + check_two_alias_snapshots_restore_one_identity>( + 1, 2, + [](int value) { return PineGenericMatrix::new_(1, 1, value); }); +} + +static void test_snapshot_keeps_original_identity_alive() { + auto snapshot = [] { + auto ephemeral = PineMatrix::new_(1, 1, 7.0); + return ephemeral.snapshot(); + }(); + + PineMatrix restored; + restored.restore(snapshot); + require(restored.get(0, 0) == 7.0, + "snapshot must keep an otherwise-unreferenced ID alive"); +} + +struct Holder { + int scalar{0}; + PineMatrix nested; +}; + +static void test_generic_copy_is_outer_deep_and_nested_handle_shallow() { + auto nested = PineMatrix::new_(1, 1, 10.0); + auto outer = PineGenericMatrix::new_( + 1, 1, Holder{1, nested}); + auto copied = outer.copy(); + + Holder copied_holder = copied.get(0, 0); + copied_holder.scalar = 2; + copied_holder.nested.set(0, 0, 20.0); + copied.set(0, 0, copied_holder); + + const Holder original_holder = outer.get(0, 0); + require(original_holder.scalar == 1, + "matrix.copy must detach the outer element buffer"); + require(original_holder.nested.get(0, 0) == 20.0, + "matrix.copy must shallow-copy nested matrix handles"); + + auto replacement = PineMatrix::new_(1, 1, 30.0); + copied_holder.nested = replacement; + copied.set(0, 0, copied_holder); + require(outer.get(0, 0).nested.get(0, 0) == 20.0, + "rebinding a copied cell must not replace the original cell"); + require(copied.get(0, 0).nested.get(0, 0) == 30.0, + "copied outer matrix must accept an independent nested binding"); +} + +int main() { + test_float_assignment_aliases_and_explicit_copy_detaches(); + test_float_moves_preserve_source_handle(); + test_generic_assignment_copy_and_move_semantics(); + test_snapshot_restores_contents_and_identity(); + test_two_alias_snapshots_restore_one_identity(); + test_snapshot_keeps_original_identity_alive(); + test_generic_copy_is_outer_deep_and_nested_handle_shallow(); + std::printf("All matrix identity tests passed.\n"); + return 0; +} diff --git a/tests/test_matrix_snapshot_compat.cpp b/tests/test_matrix_snapshot_compat.cpp index cab46f5..05c03ba 100644 --- a/tests/test_matrix_snapshot_compat.cpp +++ b/tests/test_matrix_snapshot_compat.cpp @@ -82,12 +82,12 @@ int main() { 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(matrix.get(0, 0) == 13.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(matrix.get(0, 0) == 13.0); assert(explicit_copy.get(0, 0) == 99.0); Snapshot snapshot = matrix.snapshot(); @@ -95,21 +95,24 @@ int main() { Snapshot moved = std::move(copied); matrix.set(0, 0, 3.0); matrix.restore(snapshot); - assert(matrix.get(0, 0) == 7.0); + assert(matrix.get(0, 0) == 13.0); + assert(assigned.get(0, 0) == 13.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); + assert(matrix.get(0, 0) == 13.0); + assert(assigned.get(0, 0) == 13.0); PineMatrix receiver; receiver.restore(snapshot); assert(!receiver.is_na()); - assert(receiver.get(0, 0) == 7.0); + assert(receiver.get(0, 0) == 13.0); receiver.set(0, 0, 8.0); receiver.restore(snapshot); - assert(receiver.get(0, 0) == 7.0); + assert(receiver.get(0, 0) == 13.0); + assert(matrix.get(0, 0) == 13.0); PineMatrix empty = PineMatrix::new_(0, 0, 0.0); Snapshot empty_snapshot = empty.snapshot(); @@ -124,11 +127,11 @@ int main() { 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.get(0, 0) == 13); 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.get(0, 0) == 13); assert(generic_copy.get(0, 0) == 99); check_generic_snapshot_round_trip(7, 13);