From 982eb2c97cc3600ae788d604e76b405c64168480 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Fri, 25 Sep 2026 12:19:59 -0400 Subject: [PATCH 01/18] fix json reader bug for reading in complex and tuple combinations in arrays --- src/stan/io/json/json_data.hpp | 68 ++++++++----- src/stan/io/json/json_data_handler.hpp | 21 ++++ src/test/unit/io/json/json_data_test.cpp | 21 ++++ .../unit/io/json/json_data_tuples_test.cpp | 99 +++++++++++++++++++ 4 files changed, 184 insertions(+), 25 deletions(-) diff --git a/src/stan/io/json/json_data.hpp b/src/stan/io/json/json_data.hpp index 13c771c3112..d29feab625e 100644 --- a/src/stan/io/json/json_data.hpp +++ b/src/stan/io/json/json_data.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,7 @@ class json_data : public stan::io::var_context { private: vars_map_r vars_r_; vars_map_i vars_i_; + boost::unordered_flat_map array_block_sizes_; std::vector const empty_vec_r_; std::vector const empty_vec_i_; @@ -61,6 +63,42 @@ class json_data : public stan::io::var_context { return vars_r_.find(name) != vars_r_.end(); } + /** + * Decode complex components within each tuple-free array block. + * + * @tparam T Stored scalar type, either int or double. + * @param name Variable name. + * @param data Flat values and combined dimensions for the variable. + * @return Complex values in the order of the input blocks. + * @throw json_error if nonempty data has no trailing component dimension 2. + */ + template + std::vector> vals_c_impl( + const std::string &name, + const std::pair, std::vector> &data) const { + const auto &values = data.first; + if (values.empty()) + return {}; + const auto block = array_block_sizes_.find(name); + if (block == array_block_sizes_.end() || data.second.empty() + || data.second.back() != 2) { + throw json_error("Variable: " + name + + ", expected a trailing dimension of 2 for complex " + "values."); + } + const size_t block_size = block->second; + const size_t offset = block_size / 2; + std::vector> result(values.size() / 2); + for (size_t start = 0; start < values.size(); start += block_size) { + for (size_t i = 0; i < offset; ++i) { + result[start / 2 + i] + = {static_cast(values[start + i]), + static_cast(values[start + offset + i])}; + } + } + return result; + } + public: /** * Construct a json_data object from the specified input stream. @@ -73,6 +111,7 @@ class json_data : public stan::io::var_context { explicit json_data(std::istream &in) : vars_r_(), vars_i_() { json_data_handler handler(vars_r_, vars_i_); rapidjson_parse(in, handler); + array_block_sizes_ = handler.array_block_sizes(); } /** @@ -126,35 +165,13 @@ class json_data : public stan::io::var_context { * * @param name Name of Variable of type string. * @return Vector of complex numbers with values equal to the read input. + * @throw json_error if nonempty data has no trailing component dimension 2. */ std::vector> vals_c(const std::string &name) const { if (contains_r_only(name)) { - auto &&vec_r = (vars_r_.find(name)->second); - auto &&val_r = vec_r.first; - auto &&dim_r = vec_r.second; - std::vector> vec_c(val_r.size() / 2); - int offset = 1; - for (int i = 0; i < dim_r.size() - 1; ++i) { - offset *= dim_r[i]; - } - for (int i = 0; i < vec_c.size(); ++i) { - vec_c[i] = std::complex{val_r[i], val_r[i + offset]}; - } - return vec_c; + return vals_c_impl(name, vars_r_.find(name)->second); } else if (contains_i(name)) { - auto &&vec_i = (vars_i_.find(name)->second); - auto &&val_i = vec_i.first; - auto &&dim_i = vec_i.second; - std::vector> vec_c(val_i.size() / 2); - int offset = 1; - for (int i = 0; i < dim_i.size() - 1; ++i) { - offset *= dim_i[i]; - } - for (int i = 0; i < vec_c.size(); ++i) { - vec_c[i] = std::complex{static_cast(val_i[i]), - static_cast(val_i[i + offset])}; - } - return vec_c; + return vals_c_impl(name, vars_i_.find(name)->second); } return std::vector>{}; } @@ -237,6 +254,7 @@ class json_data : public stan::io::var_context { * returns false. */ bool remove(const std::string &name) { + array_block_sizes_.erase(name); return (vars_i_.erase(name) > 0) || (vars_r_.erase(name) > 0); } diff --git a/src/stan/io/json/json_data_handler.hpp b/src/stan/io/json/json_data_handler.hpp index 70177a0bda9..5364971f915 100644 --- a/src/stan/io/json/json_data_handler.hpp +++ b/src/stan/io/json/json_data_handler.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -453,6 +454,26 @@ class json_data_handler : public stan::json::json_handler { */ void end_text() { update_array_dims(); } + /** + * Return the number of scalar values in each tuple-free array block. + * Unlike the dimensions in vars_r and vars_i after end_text(), these + * sizes exclude enclosing arrays of tuples. + * + * @return Map from leaf names to the size of one array block. + */ + boost::unordered_flat_map array_block_sizes() const { + boost::unordered_flat_map sizes; + for (const auto& slot : slot_dims_map) { + if (slot_types_map.at(slot.first) != meta_type::ARRAY) + continue; + size_t size = 1; + for (size_t dim : slot.second.dims) + size *= dim; + sizes.emplace(slot.first, size); + } + return sizes; + } + /** A key is either a top-level Stan variable name or a tuple slot id. * Logic handles edge case where key is the first slot of a tuple; * the name of the enclosing object is not used in the generated C++, diff --git a/src/test/unit/io/json/json_data_test.cpp b/src/test/unit/io/json/json_data_test.cpp index 2e4b2d38102..25072facc29 100644 --- a/src/test/unit/io/json/json_data_test.cpp +++ b/src/test/unit/io/json/json_data_test.cpp @@ -43,6 +43,27 @@ TEST(ioJson, jsonData_scalar_complex) { test_complex_var(jdata, "foo", expected_vals, expected_dims); } +TEST(ioJson, jsonData_complex_invalid_dimensions) { + std::stringstream in(R"({"scalar": 1, "odd": [1, 2, 3], + "even": [1.0, 2.0, 3.0, 4.0], + "tuples": [{"1": 1.0}, {"1": 2.0}]})"); + stan::json::json_data jdata(in); + + EXPECT_THROW(jdata.vals_c("scalar"), stan::json::json_error); + EXPECT_THROW(jdata.vals_c("odd"), stan::json::json_error); + EXPECT_THROW(jdata.vals_c("even"), stan::json::json_error); + EXPECT_THROW(jdata.vals_c("tuples.1"), stan::json::json_error); +} + +TEST(ioJson, jsonData_complex_removed_variable) { + std::stringstream in(R"({"x": [[1, 2], [3, 4]]})"); + stan::json::json_data jdata(in); + + EXPECT_TRUE(jdata.remove("x")); + EXPECT_FALSE(jdata.remove("x")); + EXPECT_TRUE(jdata.vals_c("x").empty()); +} + TEST(ioJson, jsonData_mult_vars) { std::string txt = "{ \"foo\" : 1, \"bar\" : 0.1 }"; std::stringstream in(txt); diff --git a/src/test/unit/io/json/json_data_tuples_test.cpp b/src/test/unit/io/json/json_data_tuples_test.cpp index 006a16cca96..4dde6087909 100644 --- a/src/test/unit/io/json/json_data_tuples_test.cpp +++ b/src/test/unit/io/json/json_data_tuples_test.cpp @@ -8,6 +8,105 @@ #include #include +#include +#include + +TEST(ioJsonTuples, jsonData_array_tuple_complex_scalar) { + std::stringstream in(R"({ + "d": [{"1": 1.0, "2": [2.0, 3.0]}, + {"1": 4.0, "2": [5.0, 6.0]}], + "e": [{"1": [7.0, 8.0], "2": 9.0}, + {"1": [10.0, 11.0], "2": 12.0}], + "f": {"1": 13.0, "2": [14.0, 15.0]} + })"); + stan::json::json_data jdata(in); + + test_real_var(jdata, "d.1", {1.0, 4.0}, {2}); + test_real_var(jdata, "e.2", {9.0, 12.0}, {2}); + test_real_var(jdata, "f.1", {13.0}, {}); + + EXPECT_EQ((std::vector{2, 2}), jdata.dims_r("d.2")); + EXPECT_EQ((std::vector{2, 2}), jdata.dims_r("e.1")); + EXPECT_EQ((std::vector{2}), jdata.dims_r("f.2")); + + // Real and imaginary components must belong to the same tuple instance. + EXPECT_EQ((std::vector>{{2.0, 3.0}, {5.0, 6.0}}), + jdata.vals_c("d.2")); + EXPECT_EQ((std::vector>{{7.0, 8.0}, {10.0, 11.0}}), + jdata.vals_c("e.1")); + EXPECT_EQ((std::vector>{{14.0, 15.0}}), + jdata.vals_c("f.2")); +} + +TEST(ioJsonTuples, jsonData_nested_array_tuple_complex_integer) { + std::stringstream in(R"({"x": [ + {"1": [{"1": [[1, 101], [2, 102], [3, 103]]}, + {"1": [[4, 104], [5, 105], [6, 106]]}]}, + {"1": [{"1": [[7, 107], [8, 108], [9, 109]]}, + {"1": [[10, 110], [11, 111], [12, 112]]}]} + ]})"); + stan::json::json_data jdata(in); + + EXPECT_TRUE(jdata.contains_i("x.1.1")); + EXPECT_EQ((std::vector{2, 2, 3, 2}), jdata.dims_r("x.1.1")); + EXPECT_EQ((std::vector>{{1, 101}, + {2, 102}, + {3, 103}, + {4, 104}, + {5, 105}, + {6, 106}, + {7, 107}, + {8, 108}, + {9, 109}, + {10, 110}, + {11, 111}, + {12, 112}}), + jdata.vals_c("x.1.1")); + EXPECT_EQ( + (std::vector{1, 2, 3, 101, 102, 103, 4, 5, 6, 104, 105, 106, + 7, 8, 9, 107, 108, 109, 10, 11, 12, 110, 111, 112}), + jdata.vals_i("x.1.1")); +} + +TEST(ioJsonTuples, jsonData_array_tuple_complex_matrix) { + std::stringstream in(R"({"x": [ + {"1": [[[1, 101], [2, 102], [3, 103]], + [[4, 104], [5, 105], [6, 106]]]}, + {"1": [[[7.5, 107.5], [8, 108], [9, 109]], + [[10, 110], [11, 111], [12, 112]]]} + ]})"); + stan::json::json_data jdata(in); + + EXPECT_FALSE(jdata.contains_i("x.1")); + EXPECT_EQ((std::vector{2, 2, 3, 2}), jdata.dims_r("x.1")); + EXPECT_EQ((std::vector>{{1, 101}, + {4, 104}, + {2, 102}, + {5, 105}, + {3, 103}, + {6, 106}, + {7.5, 107.5}, + {10, 110}, + {8, 108}, + {11, 111}, + {9, 109}, + {12, 112}}), + jdata.vals_c("x.1")); + EXPECT_EQ((std::vector{1, 4, 2, 5, 3, 6, 101, 104, + 102, 105, 103, 106, 7.5, 10, 8, 11, + 9, 12, 107.5, 110, 108, 111, 109, 112}), + jdata.vals_r("x.1")); +} + +TEST(ioJsonTuples, jsonData_array_tuple_complex_empty) { + std::stringstream in(R"({"x": [{"1": []}, {"1": []}], + "y": [{"1": [[], []]}, {"1": [[], []]}]})"); + stan::json::json_data jdata(in); + + EXPECT_TRUE(jdata.vals_c("x.1").empty()); + EXPECT_TRUE(jdata.vals_c("y.1").empty()); + EXPECT_TRUE(jdata.vals_c("missing").empty()); +} // tuple(int, real) x - also real y; TEST(ioJsonTuples, jsonData_tuple_int_real) { From decdd5eb27099972b6256a899e763be82e358814 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Fri, 25 Sep 2026 16:57:34 -0400 Subject: [PATCH 02/18] adds tests for for parsing complex tuple array combinations --- src/stan/io/read_from_context.hpp | 391 ++++++++++++++ src/test/unit/io/read_from_context_test.cpp | 419 +++++++++++++++ .../read_from_context/arr_tuple.json | 32 ++ .../arr_tuple_arr_tuple.json | 68 +++ .../read_from_context/arr_tuple_tuple.json | 34 ++ .../array_array_array_real.json | 44 ++ .../array_array_complex.json | 32 ++ .../read_from_context/array_array_real.json | 14 + .../array_complex_eigen_matrix.json | 116 +++++ .../read_from_context/array_eigen_matrix.json | 44 ++ .../read_from_context/basic.json | 10 + .../read_from_context/complex.json | 6 + .../complex_vector_deep.json | 260 ++++++++++ .../read_from_context/double.json | 3 + .../eigen_complex_matrix.json | 32 ++ .../read_from_context/eigen_matrix.json | 14 + .../read_from_context/eigen_row_vector.json | 8 + .../read_from_context/eigen_vector.json | 8 + .../read_from_context/empty_inner_array.json | 6 + .../read_from_context/empty_inner_tuple.json | 12 + .../read_from_context/empty_outer_tuple.json | 3 + .../read_from_context/empty_vector.json | 3 + .../read_from_context/int.json | 3 + .../integer_backed_complex.json | 6 + .../integer_backed_real.json | 3 + .../read_from_context/nested_array_tuple.json | 32 ++ .../read_from_context/tuple_arr_tuple.json | 30 ++ .../read_from_context/tuple_tuple.json | 13 + .../read_from_context/vector_complex.json | 20 + .../read_from_context/vector_double.json | 8 + .../read_from_context/vector_int.json | 7 + .../read_from_context/very_deep.json | 476 ++++++++++++++++++ .../read_from_context/zero_cols_matrix.json | 6 + .../read_from_context/zero_rows_matrix.json | 3 + 34 files changed, 2166 insertions(+) create mode 100644 src/stan/io/read_from_context.hpp create mode 100644 src/test/unit/io/read_from_context_test.cpp create mode 100644 src/test/unit/io/test_json_files/read_from_context/arr_tuple.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/arr_tuple_arr_tuple.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/arr_tuple_tuple.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/array_array_array_real.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/array_array_complex.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/array_array_real.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/array_complex_eigen_matrix.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/array_eigen_matrix.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/basic.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/complex.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/complex_vector_deep.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/double.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/eigen_complex_matrix.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/eigen_matrix.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/eigen_row_vector.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/eigen_vector.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/empty_inner_array.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/empty_inner_tuple.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/empty_outer_tuple.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/empty_vector.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/int.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/integer_backed_complex.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/integer_backed_real.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/nested_array_tuple.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/tuple_arr_tuple.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/tuple_tuple.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/vector_complex.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/vector_double.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/vector_int.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/very_deep.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/zero_cols_matrix.json create mode 100644 src/test/unit/io/test_json_files/read_from_context/zero_rows_matrix.json diff --git a/src/stan/io/read_from_context.hpp b/src/stan/io/read_from_context.hpp new file mode 100644 index 00000000000..b1c4242391f --- /dev/null +++ b/src/stan/io/read_from_context.hpp @@ -0,0 +1,391 @@ +#ifndef STAN_IO_READ_FROM_CONTEXT_HPP +#define STAN_IO_READ_FROM_CONTEXT_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace stan { + +namespace internal { + +/** + * @brief Read one integer without advancing a consumption cursor. + * @param[out] x Destination integer. + * @param[in] values Buffer of integer values for a named leaf. + * @param[in] idx Zero-based index of the value to read. + * @throw std::runtime_error if idx is outside values. + */ +inline void read_from_buffer(int& x, const std::vector& values, + std::size_t idx) { + if (idx >= values.size()) { + throw std::runtime_error("read_from_context: not enough integer values"); + } + x = values[idx]; +} + +/** + * @brief Read one real value without advancing a consumption cursor. + * @param[out] x Destination real value. + * @param[in] values Buffer of real values for a named leaf. + * @param[in] idx Zero-based index of the value to read. + * @throw std::runtime_error if idx is outside values. + */ +inline void read_from_buffer(double& x, const std::vector& values, + std::size_t idx) { + if (idx >= values.size()) { + throw std::runtime_error("read_from_context: not enough real values"); + } + x = values[idx]; +} + +/** + * @brief Read one complex value from separate component indices. + * @param[out] x Destination complex value. + * @param[in] values Buffer of real and imaginary components for a named leaf. + * @param[in] real_idx Zero-based index of the real component. + * @param[in] imag_idx Zero-based index of the imaginary component. + * @throw std::runtime_error if either index is outside values. + */ +inline void read_from_buffer(std::complex& x, + const std::vector& values, + std::size_t real_idx, std::size_t imag_idx) { + if (real_idx >= values.size() || imag_idx >= values.size()) { + throw std::runtime_error( + "read_from_context: not enough complex components"); + } + x = {values[real_idx], values[imag_idx]}; +} + +/** + * @brief Read an Eigen object in logical column-major order without advancing + * a consumption cursor. + * @tparam EigMat Writable Eigen destination type with int, double, or + * std::complex coefficients. + * @tparam InVec Contiguous input buffer type with int or double coefficients, + * possibly const. Integer destinations use integer buffers; real and complex + * destinations use real buffers. + * @param[in,out] x Destination with its rows and columns already allocated. + * @param[in] values Buffer containing the tuple-free payload to read. + * @param[in] offset Zero-based source index of the first coefficient's value + * or real component. + * @param[in] stride Positive distance in buffer elements between successive + * logical column-major coefficients. + * @param[in] imaginary_offset Distance in buffer elements from each real + * component to its imaginary component; ignored for non-complex destinations. + * @pre The caller has validated that all source indices are within values. + */ +template * = nullptr> +void read_from_buffer(EigMat& x, InVec& values, std::size_t offset, + std::size_t stride, std::size_t imaginary_offset) { + using Scalar = scalar_type_t; + const std::size_t size = x.size(); + if (size == 0) { + return; + } + using Source + = Eigen::Matrix, Eigen::Dynamic, Eigen::Dynamic>; + using Stride = Eigen::InnerStride; + using SourceMap = Eigen::Map; + if constexpr (stan::is_complex::value) { + x.real() + = SourceMap(values.data() + offset, x.rows(), x.cols(), Stride(stride)); + x.imag() = SourceMap(values.data() + offset + imaginary_offset, x.rows(), + x.cols(), Stride(stride)); + } else { + x = SourceMap(values.data() + offset, x.rows(), x.cols(), Stride(stride)); + } +} + +/** + * @brief Read a tuple-free array using column-major source strides. + * Each child uses stride * x.size(); imaginary_offset stays constant within + * the payload. This function does not advance a consumption cursor. + * @tparam StdVec Destination std::vector type containing int, double, + * std::complex, Eigen objects, or nested tuple-free std::vectors. + * @tparam InVec Input std::vector type holding int values for integer + * destinations or double values for real and complex destinations. + * @param[in,out] x Rectangular destination with every dimension allocated. + * @param[in] values Buffer containing the tuple-free payload to read. + * @param[in] offset Zero-based source index of the first value or real + * component in this array. + * @param[in] stride Positive distance in buffer elements between the starting + * indices of consecutive elements of x. + * @param[in] imaginary_offset Distance in buffer elements from each real + * component to its imaginary component; ignored for non-complex destinations. + * @pre The caller has validated that all source indices are within values. + */ +template * = nullptr> +void read_from_buffer(StdVec& x, const InVec& values, std::size_t offset, + std::size_t stride, std::size_t imaginary_offset) { + using T = value_type_t; + if constexpr (stan::is_complex::value) { + for (std::size_t i = 0; i < x.size(); ++i) { + const std::size_t idx = offset + i * stride; + internal::read_from_buffer(x[i], values, idx, idx + imaginary_offset); + } + } else if constexpr (std::is_same_v || std::is_same_v) { + using Vec = Eigen::Matrix; + using Stride = Eigen::InnerStride; + Eigen::Map(x.data(), x.size()) + = Eigen::Map( + values.data() + offset, x.size(), Stride(stride)); + } else { + for (std::size_t i = 0; i < x.size(); ++i) { + const std::size_t idx = offset + i * stride; + internal::read_from_buffer(x[i], values, idx, stride * x.size(), + imaginary_offset); + } + } +} + +/** + * @brief Read one tuple-free payload and advance its consumption cursor. + * Validate the full payload before reading. Complex payloads use separate + * real and imaginary component blocks and consume two stored values per + * destination value. Non-complex payloads consume one stored value per + * destination value. + * @tparam T Tuple-free destination type: int, double, std::complex, + * an Eigen object, or a rectangular std::vector nesting of these types. + * @tparam InputVec Input std::vector type, or a reference to it, holding int + * values for integer destinations or double values for real and complex ones. + * @param[in,out] x Destination with every container dimension allocated. + * @param[in] values Buffer for the named leaf containing this payload; + * neither modified nor moved from. + * @param[in,out] position Zero-based start of the payload in values. Advances + * by the number of stored int or double values on success, counting two per + * complex value. An empty payload leaves position unchanged. + * @throw std::runtime_error if position exceeds the buffer size or there are + * insufficient values for the payload. + */ +template +void read_from_buffer_block(T& x, InputVec&& values, std::size_t& position) { + const std::size_t size = math::num_elements(x); + if (position > values.size()) { + throw std::runtime_error( + "read_from_context: not enough values for payload"); + } + if (size == 0) { + return; + } + if constexpr (stan::is_complex>::value) { + if (size > (values.size() - position) / 2) { + throw std::runtime_error( + "read_from_context: not enough values for complex payload"); + } + if constexpr (stan::is_complex::value) { + internal::read_from_buffer(x, values, position, position + 1); + } else { + internal::read_from_buffer(x, values, position, 1, size); + } + position += 2 * size; + } else { + if (size > values.size() - position) { + throw std::runtime_error( + "read_from_context: not enough values for payload"); + } + if constexpr (std::is_integral_v || std::is_floating_point_v) { + internal::read_from_buffer(x, values, position); + } else { + internal::read_from_buffer(x, values, position, 1, 0); + } + position += size; + } +} + +/** + * @brief Read a complete tuple-free payload once the field path is exhausted. + * @tparam T Tuple-free scalar, Eigen, or rectangular std::vector destination + * type with int, double, or std::complex scalar values. + * @tparam InVec Input std::vector type holding int values for integer + * destinations or double values for real and complex destinations. + * @param[in,out] x Destination payload with its dimensions already allocated. + * @param[in] values Buffer for the named leaf containing this payload. + * @param[in,out] position Zero-based payload start in values; advances past + * this payload on success, counting both components of complex values. + * @param[in] fields Empty tuple-field path selecting x itself as the payload. + * @throw std::runtime_error if the buffer cannot supply the complete payload. + */ +template +void read_from_buffer(T& x, const InVec& values, std::size_t& position, + std::index_sequence<> fields) { + internal::read_from_buffer_block(x, values, position); +} + +/** + * @brief Follow a tuple-field path through the destination for one named leaf. + * Tuples consume one path index. Arrays visit elements in order without + * consuming an index, sharing the input buffer and consumption cursor. + * @tparam T Destination tuple or std::vector nesting containing tuples. + * @tparam InVec Input std::vector type holding int values for integer + * destinations or double values for real and complex destinations. + * @tparam First Zero-based field index in the next tuple encountered. + * @tparam Rest Remaining zero-based field indices in successive nested tuples. + * @param[in,out] x Destination subtree with every dimension already allocated. + * @param[in] values Buffer for the named leaf selected by fields. + * @param[in,out] position Zero-based index of the next unread buffer element; + * advances through each selected payload in traversal order. + * @param[in] fields Compile-time tuple-field path, excluding array indices. + * @throw std::runtime_error if the buffer cannot supply a selected payload. + */ +template +void read_from_buffer(T& x, const InVec& values, std::size_t& position, + std::index_sequence fields) { + if constexpr (is_tuple_v) { + internal::read_from_buffer(std::get(x), values, position, + std::index_sequence{}); + } else { + for (auto& element : x) { + internal::read_from_buffer(element, values, position, fields); + } + } +} + +/** + * @brief Check that a named leaf buffer has been consumed exactly. + * @tparam InVec Input buffer type providing size(). + * @param[in] values Loaded buffer for one named leaf. + * @param[in] position Number of consumed buffer elements, counting both + * components of complex values. + * @param[in] path Variable name, including one-based dotted tuple-field + * suffixes, used in the error message. + * @throw std::runtime_error if position differs from values.size(). + */ +template +void check_consumed(const InVec& values, std::size_t position, + const std::string& path) { + if (position != values.size()) { + throw std::runtime_error("read_from_context: unexpected value count for " + + path); + } +} + +/** + * @brief Load and write one named leaf at a time, releasing each buffer before + * loading the next. Discover tuple paths from types, including for empty + * arrays. + * @tparam Field Type at the current tuple-field path, possibly wrapped in + * arrays. Its scalar_type_t determines the next tuple or the leaf scalar type. + * @tparam Root Complete destination type with int, double, or + * std::complex scalar values, Eigen objects, std::vectors, and tuples. + * @tparam Context Source type providing const vals_i(name) and vals_r(name) + * methods returning std::vector and std::vector, respectively. + * @tparam Fields Zero-based tuple-field indices from the root, excluding array + * indices. + * @param[in,out] x Complete rectangular destination with all dimensions + * allocated. Earlier writes remain if a subsequent leaf fails. + * @param[in] context Source of the named integer and real buffers. + * @param[in] path Variable name including the one-based dotted tuple-field + * suffixes corresponding to Fields. + * @param[in] fields Compile-time tuple-field path into x corresponding to + * Field and path; empty at the root. + * @throw std::runtime_error if a leaf has too few or too many values for its + * destinations. Exceptions from the context propagate to the caller. + */ +template +void load_from_context(Root& x, const Context& context, const std::string& path, + std::index_sequence fields) { + using S = scalar_type_t; + if constexpr (is_tuple_v) { + math::index_apply>([&x, &context, &path](auto... i) { + (internal::load_from_context>( + x, context, path + "." + std::to_string(decltype(i)::value + 1), + std::index_sequence{}), + ...); + }); + } else { + std::size_t position = 0; + if constexpr (std::is_same_v) { + const auto values = context.vals_i(path); + internal::read_from_buffer(x, values, position, fields); + internal::check_consumed(values, position, path); + } else { + static_assert( + std::is_same_v || std::is_same_v>, + "read_from_context requires int, double or complex " + "scalar types"); + const auto values = context.vals_r(path); + internal::read_from_buffer(x, values, position, fields); + internal::check_consumed(values, position, path); + } + } +} + +} // namespace internal + +/** + * Read a named variable into an already-sized destination. + * + * Supported destinations are int, double, std::complex, Eigen + * vectors, row vectors and matrices, and nested std::vectors and std::tuples + * of these types. Arrays must be rectangular. Declared dimensions must be + * validated before this call; empty containers cannot describe the sizes of + * their missing elements. + * + * Each dotted tuple-field name is fetched once, written, and its buffer + * released before the next name is fetched. Arrays containing tuples are + * traversed in element order, while each tuple-free payload is decoded in + * column-major order. Complex components are paired within that payload. + * If a later leaf fails, previously written leaves remain modified. + * + * @tparam T Destination type: int, double, std::complex, an Eigen + * object, or a nesting of std::vectors and std::tuples of supported types. + * @tparam Context Source type providing const vals_i(name) and vals_r(name) + * methods returning std::vector and std::vector, respectively. + * @param[in,out] x Rectangular destination with its declared dimensions already + * allocated. Scalar values are overwritten; container dimensions are preserved. + * @param[in] context Source of the named integer and real buffers. + * @param[in] name Root variable name, without array indices or tuple suffixes; + * converted to an owned string for this call. + * @throw std::runtime_error if the input contains an unexpected number of + * values. + */ +template +inline void read_from_context(T& x, const Context& context, + std::string_view name) { + const std::string path(name); + if constexpr (stan::is_complex::value) { + static_assert(std::is_same_v, + "read_from_context requires complex scalar types"); + const auto values = context.vals_r(path); + if (values.size() != 2) { + throw std::runtime_error("read_from_context: expected two components for " + + path); + } + x = {values[0], values[1]}; + } else if constexpr (std::is_same_v) { + const auto values = context.vals_i(path); + if (values.size() != 1) { + throw std::runtime_error("read_from_context: expected one value for " + + path); + } + x = values[0]; + } else if constexpr (std::is_same_v) { + const auto values = context.vals_r(path); + if (values.size() != 1) { + throw std::runtime_error("read_from_context: expected one value for " + + path); + } + x = values[0]; + } else { + internal::load_from_context(x, context, path, std::index_sequence<>{}); + } +} + +} // namespace stan + +#endif diff --git a/src/test/unit/io/read_from_context_test.cpp b/src/test/unit/io/read_from_context_test.cpp new file mode 100644 index 00000000000..029c10dac8b --- /dev/null +++ b/src/test/unit/io/read_from_context_test.cpp @@ -0,0 +1,419 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// Keep fixtures in logical JSON order. Expectations below use destination +// coordinates, independently of the parser's flat-buffer layout. +std::ifstream fixture_stream(const std::string& fixture) { + const std::string path = "src/test/unit/io/test_json_files/read_from_context/" + + fixture + ".json"; + std::ifstream in(path); + if (!in.is_open()) { + throw std::runtime_error("Cannot open JSON fixture: " + path); + } + return in; +} + +template +void read_fixture(T& x, const std::string& fixture, + const std::string& name = "x") { + auto in = fixture_stream(fixture); + const stan::json::json_data context(in); + stan::read_from_context(x, context, name); +} + +void expect_complex(const std::complex& actual, double real) { + EXPECT_DOUBLE_EQ(real, actual.real()); + EXPECT_DOUBLE_EQ(-real - 0.5, actual.imag()); +} + +using complex_vector = Eigen::Matrix, -1, 1>; +using complex_matrix = Eigen::Matrix, -1, -1>; + +} // namespace + +TEST(ioReadFromContext, int_scalar) { + int x = -999; + ASSERT_NO_THROW(read_fixture(x, "int")); + EXPECT_EQ(-17, x); +} + +TEST(ioReadFromContext, real_scalar) { + double x = -999; + ASSERT_NO_THROW(read_fixture(x, "double")); + EXPECT_DOUBLE_EQ(12.25, x); +} + +TEST(ioReadFromContext, complex_scalar) { + std::complex x(-999, -999); + ASSERT_NO_THROW(read_fixture(x, "complex")); + EXPECT_DOUBLE_EQ(-2.25, x.real()); + EXPECT_DOUBLE_EQ(7.5, x.imag()); +} + +TEST(ioReadFromContext, integer_backed_real) { + double x = -999; + ASSERT_NO_THROW(read_fixture(x, "integer_backed_real")); + EXPECT_DOUBLE_EQ(17, x); +} + +TEST(ioReadFromContext, integer_backed_complex) { + std::complex x(-999, -999); + ASSERT_NO_THROW(read_fixture(x, "integer_backed_complex")); + EXPECT_DOUBLE_EQ(7, x.real()); + EXPECT_DOUBLE_EQ(-19, x.imag()); +} + +TEST(ioReadFromContext, vector_double) { + std::vector x(4, -999); + ASSERT_NO_THROW(read_fixture(x, "vector_double")); + EXPECT_EQ((std::vector{1.25, -2.5, 3.75, 40.125}), x); +} + +TEST(ioReadFromContext, vector_int) { + std::vector x(3, -999); + ASSERT_NO_THROW(read_fixture(x, "vector_int")); + EXPECT_EQ((std::vector{-4, 7, 19}), x); +} + +TEST(ioReadFromContext, vector_complex) { + std::vector> x(4, {-999, -999}); + ASSERT_NO_THROW(read_fixture(x, "vector_complex")); + ASSERT_EQ(4, x.size()); + for (int i = 0; i < 4; ++i) { + expect_complex(x[i], i + 0.25); + } +} + +TEST(ioReadFromContext, eigen_vector) { + Eigen::VectorXd x = Eigen::VectorXd::Constant(4, -999); + ASSERT_NO_THROW(read_fixture(x, "eigen_vector")); + ASSERT_EQ(4, x.size()); + for (int i = 0; i < 4; ++i) { + EXPECT_DOUBLE_EQ(i + 0.25, x(i)); + } +} + +TEST(ioReadFromContext, eigen_row_vector) { + Eigen::RowVectorXd x = Eigen::RowVectorXd::Constant(4, -999); + ASSERT_NO_THROW(read_fixture(x, "eigen_row_vector")); + ASSERT_EQ(4, x.size()); + for (int i = 0; i < 4; ++i) { + EXPECT_DOUBLE_EQ(i + 0.25, x(i)); + } +} + +TEST(ioReadFromContext, eigen_matrix) { + Eigen::MatrixXd x = Eigen::MatrixXd::Constant(2, 3, -999); + ASSERT_NO_THROW(read_fixture(x, "eigen_matrix")); + ASSERT_EQ(2, x.rows()); + ASSERT_EQ(3, x.cols()); + for (int r = 0; r < 2; ++r) { + for (int c = 0; c < 3; ++c) { + EXPECT_DOUBLE_EQ(10 * r + c + 0.25, x(r, c)); + } + } +} + +TEST(ioReadFromContext, eigen_complex_matrix) { + complex_matrix x = complex_matrix::Constant(2, 3, {-999, -999}); + ASSERT_NO_THROW(read_fixture(x, "eigen_complex_matrix")); + ASSERT_EQ(2, x.rows()); + ASSERT_EQ(3, x.cols()); + for (int r = 0; r < 2; ++r) { + for (int c = 0; c < 3; ++c) { + expect_complex(x(r, c), 10 * r + c + 0.25); + } + } +} + +TEST(ioReadFromContext, array_array_real) { + std::vector> x(2, std::vector(3, -999)); + ASSERT_NO_THROW(read_fixture(x, "array_array_real")); + ASSERT_EQ(2, x.size()); + for (int a = 0; a < 2; ++a) { + ASSERT_EQ(3, x[a].size()); + for (int b = 0; b < 3; ++b) { + EXPECT_DOUBLE_EQ(10 * a + b + 0.25, x[a][b]); + } + } +} + +TEST(ioReadFromContext, array_array_complex) { + std::vector>> x( + 2, std::vector>(3, {-999, -999})); + ASSERT_NO_THROW(read_fixture(x, "array_array_complex")); + ASSERT_EQ(2, x.size()); + for (int a = 0; a < 2; ++a) { + ASSERT_EQ(3, x[a].size()); + for (int b = 0; b < 3; ++b) { + expect_complex(x[a][b], 10 * a + b + 0.25); + } + } +} + +TEST(ioReadFromContext, array_array_array_real) { + std::vector>> x( + 2, std::vector>(3, std::vector(4, -999))); + ASSERT_NO_THROW(read_fixture(x, "array_array_array_real")); + for (int a = 0; a < 2; ++a) { + for (int b = 0; b < 3; ++b) { + for (int c = 0; c < 4; ++c) { + EXPECT_DOUBLE_EQ(100 * a + 10 * b + c + 0.25, x[a][b][c]); + } + } + } +} + +TEST(ioReadFromContext, array_eigen_matrix) { + std::vector x(2, Eigen::MatrixXd::Constant(3, 4, -999)); + ASSERT_NO_THROW(read_fixture(x, "array_eigen_matrix")); + // Array entries are interleaved in the column-major context buffer; + // a single matrix's values are not a contiguous source block. + for (int a = 0; a < 2; ++a) { + for (int r = 0; r < 3; ++r) { + for (int c = 0; c < 4; ++c) { + EXPECT_DOUBLE_EQ(100 * a + 10 * r + c + 0.25, x[a](r, c)); + } + } + } +} + +TEST(ioReadFromContext, array_complex_eigen_matrix) { + std::vector x(2, + complex_matrix::Constant(3, 4, {-999, -999})); + ASSERT_NO_THROW(read_fixture(x, "array_complex_eigen_matrix")); + for (int a = 0; a < 2; ++a) { + for (int r = 0; r < 3; ++r) { + for (int c = 0; c < 4; ++c) { + expect_complex(x[a](r, c), 100 * a + 10 * r + c + 0.25); + } + } + } +} + +TEST(ioReadFromContext, basic) { + std::tuple, int> x{std::vector(3, -999), -999}; + ASSERT_NO_THROW(read_fixture(x, "basic", "basic")); + EXPECT_EQ((std::vector{1.25, -2.5, 3.75}), std::get<0>(x)); + EXPECT_EQ(-17, std::get<1>(x)); +} + +TEST(ioReadFromContext, tuple_tuple) { + std::tuple> x{ + -999, {-999, Eigen::VectorXd::Constant(3, -999)}}; + ASSERT_NO_THROW(read_fixture(x, "tuple_tuple", "tuple_tuple")); + EXPECT_EQ(-17, std::get<0>(x)); + EXPECT_DOUBLE_EQ(8.25, std::get<0>(std::get<1>(x))); + for (int i = 0; i < 3; ++i) { + EXPECT_DOUBLE_EQ(10 + i + 0.5, std::get<1>(std::get<1>(x))(i)); + } +} + +TEST(ioReadFromContext, arr_tuple) { + std::vector, Eigen::VectorXd>> x( + 2, + {-999, std::vector(3, -999), Eigen::VectorXd::Constant(4, -999)}); + ASSERT_NO_THROW(read_fixture(x, "arr_tuple", "arr_tuple")); + for (int a = 0; a < 2; ++a) { + SCOPED_TRACE(a); + EXPECT_EQ(a + 7, std::get<0>(x[a])); + for (int i = 0; i < 3; ++i) { + EXPECT_DOUBLE_EQ(100 * a + i + 0.25, std::get<1>(x[a])[i]); + } + for (int i = 0; i < 4; ++i) { + EXPECT_DOUBLE_EQ(100 * a + 10 + i + 0.5, std::get<2>(x[a])(i)); + } + } +} + +TEST(ioReadFromContext, tuple_arr_tuple) { + std::tuple, int, + std::vector>>> + x{std::vector(3, -999), -999, + std::vector>>( + 2, {-999, std::vector(4, -999)})}; + ASSERT_NO_THROW(read_fixture(x, "tuple_arr_tuple", "tuple_arr_tuple")); + EXPECT_EQ((std::vector{1.25, 2.5, 3.75}), std::get<0>(x)); + EXPECT_EQ(-9, std::get<1>(x)); + for (int a = 0; a < 2; ++a) { + const auto& field = std::get<2>(x)[a]; + EXPECT_DOUBLE_EQ(10 * a + 0.25, std::get<0>(field)); + for (int i = 0; i < 4; ++i) { + EXPECT_EQ(100 * a + i, std::get<1>(field)[i]); + } + } +} + +TEST(ioReadFromContext, arr_tuple_tuple) { + std::vector>>> + x(2, {-999, -999, {-999, {-999, Eigen::VectorXd::Constant(3, -999)}}}); + ASSERT_NO_THROW(read_fixture(x, "arr_tuple_tuple", "arr_tuple_tuple")); + for (int a = 0; a < 2; ++a) { + SCOPED_TRACE(a); + EXPECT_DOUBLE_EQ(100 * a + 0.25, std::get<0>(x[a])); + EXPECT_EQ(100 * a + 1, std::get<1>(x[a])); + const auto& inner = std::get<2>(x[a]); + EXPECT_DOUBLE_EQ(100 * a + 2.5, std::get<0>(inner)); + EXPECT_EQ(100 * a + 3, std::get<0>(std::get<1>(inner))); + for (int i = 0; i < 3; ++i) { + EXPECT_DOUBLE_EQ(100 * a + 10 + i + 0.75, + std::get<1>(std::get<1>(inner))(i)); + } + } +} + +TEST(ioReadFromContext, arr_tuple_arr_tuple) { + using inner = std::tuple; + std::vector>> x( + 2, {-999, + std::vector(3, {-999, Eigen::VectorXd::Constant(4, -999)})}); + ASSERT_NO_THROW( + read_fixture(x, "arr_tuple_arr_tuple", "arr_tuple_arr_tuple")); + for (int a = 0; a < 2; ++a) { + EXPECT_EQ(a + 7, std::get<0>(x[a])); + for (int b = 0; b < 3; ++b) { + const auto& field = std::get<1>(x[a])[b]; + EXPECT_DOUBLE_EQ(100 * a + 10 * b + 0.25, std::get<0>(field)); + for (int i = 0; i < 4; ++i) { + EXPECT_DOUBLE_EQ(100 * a + 10 * b + i + 0.5, std::get<1>(field)(i)); + } + } + } +} + +TEST(ioReadFromContext, complex_vector_deep) { + using inner = std::tuple>; + std::vector>> x( + 2, {-999, std::vector(3, {-999, std::vector( + 2, complex_vector::Constant( + 4, {-999, -999}))})}); + ASSERT_NO_THROW( + read_fixture(x, "complex_vector_deep", "complex_vector_deep")); + for (int a = 0; a < 2; ++a) { + SCOPED_TRACE(a); + EXPECT_EQ(a + 7, std::get<0>(x[a])); + for (int b = 0; b < 3; ++b) { + SCOPED_TRACE(b); + const auto& field = std::get<1>(x[a])[b]; + EXPECT_DOUBLE_EQ(100 * a + 10 * b + 0.25, std::get<0>(field)); + for (int c = 0; c < 2; ++c) { + for (int d = 0; d < 4; ++d) { + expect_complex(std::get<1>(field)[c](d), + 1000 * a + 100 * b + 10 * c + d + 0.25); + } + } + } + } +} + +TEST(ioReadFromContext, very_deep) { + using leaf = std::tuple, Eigen::MatrixXd>; + using inner = std::tuple>; + std::vector>> x( + 2, {-999, std::vector( + 3, {-999, std::vector(4, {{-999, -999}, + Eigen::MatrixXd::Constant( + 2, 3, -999)})})}); + ASSERT_NO_THROW(read_fixture(x, "very_deep", "very_deep")); + for (int a = 0; a < 2; ++a) { + SCOPED_TRACE(a); + EXPECT_EQ(a + 7, std::get<0>(x[a])); + for (int b = 0; b < 3; ++b) { + SCOPED_TRACE(b); + const auto& field = std::get<1>(x[a])[b]; + EXPECT_DOUBLE_EQ(100 * a + 10 * b + 0.25, std::get<0>(field)); + for (int c = 0; c < 4; ++c) { + SCOPED_TRACE(c); + const auto& nested = std::get<1>(field)[c]; + expect_complex(std::get<0>(nested), 1000 * a + 100 * b + 10 * c + 0.25); + for (int r = 0; r < 2; ++r) { + for (int s = 0; s < 3; ++s) { + EXPECT_DOUBLE_EQ(10000 * a + 1000 * b + 100 * c + 10 * r + s + 0.5, + std::get<1>(nested)(r, s)); + } + } + } + } + } +} + +TEST(ioReadFromContext, nested_array_tuple) { + using element = std::tuple; + std::vector> x(2, std::vector(3, {-999, -999})); + ASSERT_NO_THROW(read_fixture(x, "nested_array_tuple")); + // Multiple array dimensions outside a tuple use tuple-instance order, + // unlike the identically sized ordinary array_array_real fixture. + for (int a = 0; a < 2; ++a) { + for (int b = 0; b < 3; ++b) { + EXPECT_EQ(10 * a + b, std::get<0>(x[a][b])); + EXPECT_DOUBLE_EQ(100 * a + 10 * b + 0.25, std::get<1>(x[a][b])); + } + } +} + +TEST(ioReadFromContext, empty_vector) { + std::vector x; + ASSERT_NO_THROW(read_fixture(x, "empty_vector")); + EXPECT_TRUE(x.empty()); +} + +TEST(ioReadFromContext, empty_outer_tuple) { + std::vector>> x; + ASSERT_NO_THROW(read_fixture(x, "empty_outer_tuple")); + EXPECT_TRUE(x.empty()); +} + +TEST(ioReadFromContext, empty_inner_array) { + std::vector> x(2); + ASSERT_NO_THROW(read_fixture(x, "empty_inner_array")); + ASSERT_EQ(2, x.size()); + EXPECT_TRUE(x[0].empty()); + EXPECT_TRUE(x[1].empty()); +} + +TEST(ioReadFromContext, empty_inner_tuple) { + std::vector>>> x(2); + ASSERT_NO_THROW(read_fixture(x, "empty_inner_tuple")); + ASSERT_EQ(2, x.size()); + for (int a = 0; a < 2; ++a) { + EXPECT_EQ(a + 7, std::get<0>(x[a])); + EXPECT_TRUE(std::get<1>(x[a]).empty()); + } +} + +TEST(ioReadFromContext, zero_rows_matrix) { + Eigen::MatrixXd x(0, 3); + ASSERT_NO_THROW(read_fixture(x, "zero_rows_matrix")); + EXPECT_EQ(0, x.rows()); + EXPECT_EQ(3, x.cols()); +} + +TEST(ioReadFromContext, zero_cols_matrix) { + Eigen::MatrixXd x(2, 0); + ASSERT_NO_THROW(read_fixture(x, "zero_cols_matrix")); + EXPECT_EQ(2, x.rows()); + EXPECT_EQ(0, x.cols()); +} + +TEST(ioReadFromContext, repeated_read_has_independent_positions) { + auto in = fixture_stream("basic"); + const stan::json::json_data context(in); + std::tuple, int> first{std::vector(3, -999), + -999}; + auto second = first; + ASSERT_NO_THROW(stan::read_from_context(first, context, "basic")); + ASSERT_NO_THROW(stan::read_from_context(second, context, "basic")); + EXPECT_EQ((std::vector{1.25, -2.5, 3.75}), std::get<0>(first)); + EXPECT_EQ(-17, std::get<1>(first)); + EXPECT_EQ(first, second); +} diff --git a/src/test/unit/io/test_json_files/read_from_context/arr_tuple.json b/src/test/unit/io/test_json_files/read_from_context/arr_tuple.json new file mode 100644 index 00000000000..26897d2f0e8 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/arr_tuple.json @@ -0,0 +1,32 @@ +{ + "arr_tuple": [ + { + "1": 7, + "2": [ + 0.25, + 1.25, + 2.25 + ], + "3": [ + 10.5, + 11.5, + 12.5, + 13.5 + ] + }, + { + "1": 8, + "2": [ + 100.25, + 101.25, + 102.25 + ], + "3": [ + 110.5, + 111.5, + 112.5, + 113.5 + ] + } + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/arr_tuple_arr_tuple.json b/src/test/unit/io/test_json_files/read_from_context/arr_tuple_arr_tuple.json new file mode 100644 index 00000000000..2047079ba0d --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/arr_tuple_arr_tuple.json @@ -0,0 +1,68 @@ +{ + "arr_tuple_arr_tuple": [ + { + "1": 7, + "2": [ + { + "1": 0.25, + "2": [ + 0.5, + 1.5, + 2.5, + 3.5 + ] + }, + { + "1": 10.25, + "2": [ + 10.5, + 11.5, + 12.5, + 13.5 + ] + }, + { + "1": 20.25, + "2": [ + 20.5, + 21.5, + 22.5, + 23.5 + ] + } + ] + }, + { + "1": 8, + "2": [ + { + "1": 100.25, + "2": [ + 100.5, + 101.5, + 102.5, + 103.5 + ] + }, + { + "1": 110.25, + "2": [ + 110.5, + 111.5, + 112.5, + 113.5 + ] + }, + { + "1": 120.25, + "2": [ + 120.5, + 121.5, + 122.5, + 123.5 + ] + } + ] + } + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/arr_tuple_tuple.json b/src/test/unit/io/test_json_files/read_from_context/arr_tuple_tuple.json new file mode 100644 index 00000000000..90dcc5336f9 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/arr_tuple_tuple.json @@ -0,0 +1,34 @@ +{ + "arr_tuple_tuple": [ + { + "1": 0.25, + "2": 1, + "3": { + "1": 2.5, + "2": { + "1": 3, + "2": [ + 10.75, + 11.75, + 12.75 + ] + } + } + }, + { + "1": 100.25, + "2": 101, + "3": { + "1": 102.5, + "2": { + "1": 103, + "2": [ + 110.75, + 111.75, + 112.75 + ] + } + } + } + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/array_array_array_real.json b/src/test/unit/io/test_json_files/read_from_context/array_array_array_real.json new file mode 100644 index 00000000000..1958a933698 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/array_array_array_real.json @@ -0,0 +1,44 @@ +{ + "x": [ + [ + [ + 0.25, + 1.25, + 2.25, + 3.25 + ], + [ + 10.25, + 11.25, + 12.25, + 13.25 + ], + [ + 20.25, + 21.25, + 22.25, + 23.25 + ] + ], + [ + [ + 100.25, + 101.25, + 102.25, + 103.25 + ], + [ + 110.25, + 111.25, + 112.25, + 113.25 + ], + [ + 120.25, + 121.25, + 122.25, + 123.25 + ] + ] + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/array_array_complex.json b/src/test/unit/io/test_json_files/read_from_context/array_array_complex.json new file mode 100644 index 00000000000..625517ddb43 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/array_array_complex.json @@ -0,0 +1,32 @@ +{ + "x": [ + [ + [ + 0.25, + -0.75 + ], + [ + 1.25, + -1.75 + ], + [ + 2.25, + -2.75 + ] + ], + [ + [ + 10.25, + -10.75 + ], + [ + 11.25, + -11.75 + ], + [ + 12.25, + -12.75 + ] + ] + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/array_array_real.json b/src/test/unit/io/test_json_files/read_from_context/array_array_real.json new file mode 100644 index 00000000000..730b6b34fde --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/array_array_real.json @@ -0,0 +1,14 @@ +{ + "x": [ + [ + 0.25, + 1.25, + 2.25 + ], + [ + 10.25, + 11.25, + 12.25 + ] + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/array_complex_eigen_matrix.json b/src/test/unit/io/test_json_files/read_from_context/array_complex_eigen_matrix.json new file mode 100644 index 00000000000..9f66ff45dd3 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/array_complex_eigen_matrix.json @@ -0,0 +1,116 @@ +{ + "x": [ + [ + [ + [ + 0.25, + -0.75 + ], + [ + 1.25, + -1.75 + ], + [ + 2.25, + -2.75 + ], + [ + 3.25, + -3.75 + ] + ], + [ + [ + 10.25, + -10.75 + ], + [ + 11.25, + -11.75 + ], + [ + 12.25, + -12.75 + ], + [ + 13.25, + -13.75 + ] + ], + [ + [ + 20.25, + -20.75 + ], + [ + 21.25, + -21.75 + ], + [ + 22.25, + -22.75 + ], + [ + 23.25, + -23.75 + ] + ] + ], + [ + [ + [ + 100.25, + -100.75 + ], + [ + 101.25, + -101.75 + ], + [ + 102.25, + -102.75 + ], + [ + 103.25, + -103.75 + ] + ], + [ + [ + 110.25, + -110.75 + ], + [ + 111.25, + -111.75 + ], + [ + 112.25, + -112.75 + ], + [ + 113.25, + -113.75 + ] + ], + [ + [ + 120.25, + -120.75 + ], + [ + 121.25, + -121.75 + ], + [ + 122.25, + -122.75 + ], + [ + 123.25, + -123.75 + ] + ] + ] + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/array_eigen_matrix.json b/src/test/unit/io/test_json_files/read_from_context/array_eigen_matrix.json new file mode 100644 index 00000000000..1958a933698 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/array_eigen_matrix.json @@ -0,0 +1,44 @@ +{ + "x": [ + [ + [ + 0.25, + 1.25, + 2.25, + 3.25 + ], + [ + 10.25, + 11.25, + 12.25, + 13.25 + ], + [ + 20.25, + 21.25, + 22.25, + 23.25 + ] + ], + [ + [ + 100.25, + 101.25, + 102.25, + 103.25 + ], + [ + 110.25, + 111.25, + 112.25, + 113.25 + ], + [ + 120.25, + 121.25, + 122.25, + 123.25 + ] + ] + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/basic.json b/src/test/unit/io/test_json_files/read_from_context/basic.json new file mode 100644 index 00000000000..d6b3cd9c327 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/basic.json @@ -0,0 +1,10 @@ +{ + "basic": { + "1": [ + 1.25, + -2.5, + 3.75 + ], + "2": -17 + } +} diff --git a/src/test/unit/io/test_json_files/read_from_context/complex.json b/src/test/unit/io/test_json_files/read_from_context/complex.json new file mode 100644 index 00000000000..472d8162803 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/complex.json @@ -0,0 +1,6 @@ +{ + "x": [ + -2.25, + 7.5 + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/complex_vector_deep.json b/src/test/unit/io/test_json_files/read_from_context/complex_vector_deep.json new file mode 100644 index 00000000000..9a8afe8d097 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/complex_vector_deep.json @@ -0,0 +1,260 @@ +{ + "complex_vector_deep": [ + { + "1": 7, + "2": [ + { + "1": 0.25, + "2": [ + [ + [ + 0.25, + -0.75 + ], + [ + 1.25, + -1.75 + ], + [ + 2.25, + -2.75 + ], + [ + 3.25, + -3.75 + ] + ], + [ + [ + 10.25, + -10.75 + ], + [ + 11.25, + -11.75 + ], + [ + 12.25, + -12.75 + ], + [ + 13.25, + -13.75 + ] + ] + ] + }, + { + "1": 10.25, + "2": [ + [ + [ + 100.25, + -100.75 + ], + [ + 101.25, + -101.75 + ], + [ + 102.25, + -102.75 + ], + [ + 103.25, + -103.75 + ] + ], + [ + [ + 110.25, + -110.75 + ], + [ + 111.25, + -111.75 + ], + [ + 112.25, + -112.75 + ], + [ + 113.25, + -113.75 + ] + ] + ] + }, + { + "1": 20.25, + "2": [ + [ + [ + 200.25, + -200.75 + ], + [ + 201.25, + -201.75 + ], + [ + 202.25, + -202.75 + ], + [ + 203.25, + -203.75 + ] + ], + [ + [ + 210.25, + -210.75 + ], + [ + 211.25, + -211.75 + ], + [ + 212.25, + -212.75 + ], + [ + 213.25, + -213.75 + ] + ] + ] + } + ] + }, + { + "1": 8, + "2": [ + { + "1": 100.25, + "2": [ + [ + [ + 1000.25, + -1000.75 + ], + [ + 1001.25, + -1001.75 + ], + [ + 1002.25, + -1002.75 + ], + [ + 1003.25, + -1003.75 + ] + ], + [ + [ + 1010.25, + -1010.75 + ], + [ + 1011.25, + -1011.75 + ], + [ + 1012.25, + -1012.75 + ], + [ + 1013.25, + -1013.75 + ] + ] + ] + }, + { + "1": 110.25, + "2": [ + [ + [ + 1100.25, + -1100.75 + ], + [ + 1101.25, + -1101.75 + ], + [ + 1102.25, + -1102.75 + ], + [ + 1103.25, + -1103.75 + ] + ], + [ + [ + 1110.25, + -1110.75 + ], + [ + 1111.25, + -1111.75 + ], + [ + 1112.25, + -1112.75 + ], + [ + 1113.25, + -1113.75 + ] + ] + ] + }, + { + "1": 120.25, + "2": [ + [ + [ + 1200.25, + -1200.75 + ], + [ + 1201.25, + -1201.75 + ], + [ + 1202.25, + -1202.75 + ], + [ + 1203.25, + -1203.75 + ] + ], + [ + [ + 1210.25, + -1210.75 + ], + [ + 1211.25, + -1211.75 + ], + [ + 1212.25, + -1212.75 + ], + [ + 1213.25, + -1213.75 + ] + ] + ] + } + ] + } + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/double.json b/src/test/unit/io/test_json_files/read_from_context/double.json new file mode 100644 index 00000000000..19911c9f08e --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/double.json @@ -0,0 +1,3 @@ +{ + "x": 12.25 +} diff --git a/src/test/unit/io/test_json_files/read_from_context/eigen_complex_matrix.json b/src/test/unit/io/test_json_files/read_from_context/eigen_complex_matrix.json new file mode 100644 index 00000000000..625517ddb43 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/eigen_complex_matrix.json @@ -0,0 +1,32 @@ +{ + "x": [ + [ + [ + 0.25, + -0.75 + ], + [ + 1.25, + -1.75 + ], + [ + 2.25, + -2.75 + ] + ], + [ + [ + 10.25, + -10.75 + ], + [ + 11.25, + -11.75 + ], + [ + 12.25, + -12.75 + ] + ] + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/eigen_matrix.json b/src/test/unit/io/test_json_files/read_from_context/eigen_matrix.json new file mode 100644 index 00000000000..730b6b34fde --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/eigen_matrix.json @@ -0,0 +1,14 @@ +{ + "x": [ + [ + 0.25, + 1.25, + 2.25 + ], + [ + 10.25, + 11.25, + 12.25 + ] + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/eigen_row_vector.json b/src/test/unit/io/test_json_files/read_from_context/eigen_row_vector.json new file mode 100644 index 00000000000..336a550b37a --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/eigen_row_vector.json @@ -0,0 +1,8 @@ +{ + "x": [ + 0.25, + 1.25, + 2.25, + 3.25 + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/eigen_vector.json b/src/test/unit/io/test_json_files/read_from_context/eigen_vector.json new file mode 100644 index 00000000000..336a550b37a --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/eigen_vector.json @@ -0,0 +1,8 @@ +{ + "x": [ + 0.25, + 1.25, + 2.25, + 3.25 + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/empty_inner_array.json b/src/test/unit/io/test_json_files/read_from_context/empty_inner_array.json new file mode 100644 index 00000000000..ae9a95b7f8e --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/empty_inner_array.json @@ -0,0 +1,6 @@ +{ + "x": [ + [], + [] + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/empty_inner_tuple.json b/src/test/unit/io/test_json_files/read_from_context/empty_inner_tuple.json new file mode 100644 index 00000000000..f3013ff53e6 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/empty_inner_tuple.json @@ -0,0 +1,12 @@ +{ + "x": [ + { + "1": 7, + "2": [] + }, + { + "1": 8, + "2": [] + } + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/empty_outer_tuple.json b/src/test/unit/io/test_json_files/read_from_context/empty_outer_tuple.json new file mode 100644 index 00000000000..5679c7135a2 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/empty_outer_tuple.json @@ -0,0 +1,3 @@ +{ + "x": [] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/empty_vector.json b/src/test/unit/io/test_json_files/read_from_context/empty_vector.json new file mode 100644 index 00000000000..5679c7135a2 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/empty_vector.json @@ -0,0 +1,3 @@ +{ + "x": [] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/int.json b/src/test/unit/io/test_json_files/read_from_context/int.json new file mode 100644 index 00000000000..e7593a7b205 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/int.json @@ -0,0 +1,3 @@ +{ + "x": -17 +} diff --git a/src/test/unit/io/test_json_files/read_from_context/integer_backed_complex.json b/src/test/unit/io/test_json_files/read_from_context/integer_backed_complex.json new file mode 100644 index 00000000000..327db2fc3d9 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/integer_backed_complex.json @@ -0,0 +1,6 @@ +{ + "x": [ + 7, + -19 + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/integer_backed_real.json b/src/test/unit/io/test_json_files/read_from_context/integer_backed_real.json new file mode 100644 index 00000000000..d67f9df10ed --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/integer_backed_real.json @@ -0,0 +1,3 @@ +{ + "x": 17 +} diff --git a/src/test/unit/io/test_json_files/read_from_context/nested_array_tuple.json b/src/test/unit/io/test_json_files/read_from_context/nested_array_tuple.json new file mode 100644 index 00000000000..dc7e615132e --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/nested_array_tuple.json @@ -0,0 +1,32 @@ +{ + "x": [ + [ + { + "1": 0, + "2": 0.25 + }, + { + "1": 1, + "2": 10.25 + }, + { + "1": 2, + "2": 20.25 + } + ], + [ + { + "1": 10, + "2": 100.25 + }, + { + "1": 11, + "2": 110.25 + }, + { + "1": 12, + "2": 120.25 + } + ] + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/tuple_arr_tuple.json b/src/test/unit/io/test_json_files/read_from_context/tuple_arr_tuple.json new file mode 100644 index 00000000000..0d2ad16e8af --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/tuple_arr_tuple.json @@ -0,0 +1,30 @@ +{ + "tuple_arr_tuple": { + "1": [ + 1.25, + 2.5, + 3.75 + ], + "2": -9, + "3": [ + { + "1": 0.25, + "2": [ + 0, + 1, + 2, + 3 + ] + }, + { + "1": 10.25, + "2": [ + 100, + 101, + 102, + 103 + ] + } + ] + } +} diff --git a/src/test/unit/io/test_json_files/read_from_context/tuple_tuple.json b/src/test/unit/io/test_json_files/read_from_context/tuple_tuple.json new file mode 100644 index 00000000000..0b04a5a5949 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/tuple_tuple.json @@ -0,0 +1,13 @@ +{ + "tuple_tuple": { + "1": -17, + "2": { + "1": 8.25, + "2": [ + 10.5, + 11.5, + 12.5 + ] + } + } +} diff --git a/src/test/unit/io/test_json_files/read_from_context/vector_complex.json b/src/test/unit/io/test_json_files/read_from_context/vector_complex.json new file mode 100644 index 00000000000..66a8d2e6477 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/vector_complex.json @@ -0,0 +1,20 @@ +{ + "x": [ + [ + 0.25, + -0.75 + ], + [ + 1.25, + -1.75 + ], + [ + 2.25, + -2.75 + ], + [ + 3.25, + -3.75 + ] + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/vector_double.json b/src/test/unit/io/test_json_files/read_from_context/vector_double.json new file mode 100644 index 00000000000..9812fa5afbb --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/vector_double.json @@ -0,0 +1,8 @@ +{ + "x": [ + 1.25, + -2.5, + 3.75, + 40.125 + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/vector_int.json b/src/test/unit/io/test_json_files/read_from_context/vector_int.json new file mode 100644 index 00000000000..6f2251345cb --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/vector_int.json @@ -0,0 +1,7 @@ +{ + "x": [ + -4, + 7, + 19 + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/very_deep.json b/src/test/unit/io/test_json_files/read_from_context/very_deep.json new file mode 100644 index 00000000000..712aa968f88 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/very_deep.json @@ -0,0 +1,476 @@ +{ + "very_deep": [ + { + "1": 7, + "2": [ + { + "1": 0.25, + "2": [ + { + "1": [ + 0.25, + -0.75 + ], + "2": [ + [ + 0.5, + 1.5, + 2.5 + ], + [ + 10.5, + 11.5, + 12.5 + ] + ] + }, + { + "1": [ + 10.25, + -10.75 + ], + "2": [ + [ + 100.5, + 101.5, + 102.5 + ], + [ + 110.5, + 111.5, + 112.5 + ] + ] + }, + { + "1": [ + 20.25, + -20.75 + ], + "2": [ + [ + 200.5, + 201.5, + 202.5 + ], + [ + 210.5, + 211.5, + 212.5 + ] + ] + }, + { + "1": [ + 30.25, + -30.75 + ], + "2": [ + [ + 300.5, + 301.5, + 302.5 + ], + [ + 310.5, + 311.5, + 312.5 + ] + ] + } + ] + }, + { + "1": 10.25, + "2": [ + { + "1": [ + 100.25, + -100.75 + ], + "2": [ + [ + 1000.5, + 1001.5, + 1002.5 + ], + [ + 1010.5, + 1011.5, + 1012.5 + ] + ] + }, + { + "1": [ + 110.25, + -110.75 + ], + "2": [ + [ + 1100.5, + 1101.5, + 1102.5 + ], + [ + 1110.5, + 1111.5, + 1112.5 + ] + ] + }, + { + "1": [ + 120.25, + -120.75 + ], + "2": [ + [ + 1200.5, + 1201.5, + 1202.5 + ], + [ + 1210.5, + 1211.5, + 1212.5 + ] + ] + }, + { + "1": [ + 130.25, + -130.75 + ], + "2": [ + [ + 1300.5, + 1301.5, + 1302.5 + ], + [ + 1310.5, + 1311.5, + 1312.5 + ] + ] + } + ] + }, + { + "1": 20.25, + "2": [ + { + "1": [ + 200.25, + -200.75 + ], + "2": [ + [ + 2000.5, + 2001.5, + 2002.5 + ], + [ + 2010.5, + 2011.5, + 2012.5 + ] + ] + }, + { + "1": [ + 210.25, + -210.75 + ], + "2": [ + [ + 2100.5, + 2101.5, + 2102.5 + ], + [ + 2110.5, + 2111.5, + 2112.5 + ] + ] + }, + { + "1": [ + 220.25, + -220.75 + ], + "2": [ + [ + 2200.5, + 2201.5, + 2202.5 + ], + [ + 2210.5, + 2211.5, + 2212.5 + ] + ] + }, + { + "1": [ + 230.25, + -230.75 + ], + "2": [ + [ + 2300.5, + 2301.5, + 2302.5 + ], + [ + 2310.5, + 2311.5, + 2312.5 + ] + ] + } + ] + } + ] + }, + { + "1": 8, + "2": [ + { + "1": 100.25, + "2": [ + { + "1": [ + 1000.25, + -1000.75 + ], + "2": [ + [ + 10000.5, + 10001.5, + 10002.5 + ], + [ + 10010.5, + 10011.5, + 10012.5 + ] + ] + }, + { + "1": [ + 1010.25, + -1010.75 + ], + "2": [ + [ + 10100.5, + 10101.5, + 10102.5 + ], + [ + 10110.5, + 10111.5, + 10112.5 + ] + ] + }, + { + "1": [ + 1020.25, + -1020.75 + ], + "2": [ + [ + 10200.5, + 10201.5, + 10202.5 + ], + [ + 10210.5, + 10211.5, + 10212.5 + ] + ] + }, + { + "1": [ + 1030.25, + -1030.75 + ], + "2": [ + [ + 10300.5, + 10301.5, + 10302.5 + ], + [ + 10310.5, + 10311.5, + 10312.5 + ] + ] + } + ] + }, + { + "1": 110.25, + "2": [ + { + "1": [ + 1100.25, + -1100.75 + ], + "2": [ + [ + 11000.5, + 11001.5, + 11002.5 + ], + [ + 11010.5, + 11011.5, + 11012.5 + ] + ] + }, + { + "1": [ + 1110.25, + -1110.75 + ], + "2": [ + [ + 11100.5, + 11101.5, + 11102.5 + ], + [ + 11110.5, + 11111.5, + 11112.5 + ] + ] + }, + { + "1": [ + 1120.25, + -1120.75 + ], + "2": [ + [ + 11200.5, + 11201.5, + 11202.5 + ], + [ + 11210.5, + 11211.5, + 11212.5 + ] + ] + }, + { + "1": [ + 1130.25, + -1130.75 + ], + "2": [ + [ + 11300.5, + 11301.5, + 11302.5 + ], + [ + 11310.5, + 11311.5, + 11312.5 + ] + ] + } + ] + }, + { + "1": 120.25, + "2": [ + { + "1": [ + 1200.25, + -1200.75 + ], + "2": [ + [ + 12000.5, + 12001.5, + 12002.5 + ], + [ + 12010.5, + 12011.5, + 12012.5 + ] + ] + }, + { + "1": [ + 1210.25, + -1210.75 + ], + "2": [ + [ + 12100.5, + 12101.5, + 12102.5 + ], + [ + 12110.5, + 12111.5, + 12112.5 + ] + ] + }, + { + "1": [ + 1220.25, + -1220.75 + ], + "2": [ + [ + 12200.5, + 12201.5, + 12202.5 + ], + [ + 12210.5, + 12211.5, + 12212.5 + ] + ] + }, + { + "1": [ + 1230.25, + -1230.75 + ], + "2": [ + [ + 12300.5, + 12301.5, + 12302.5 + ], + [ + 12310.5, + 12311.5, + 12312.5 + ] + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/zero_cols_matrix.json b/src/test/unit/io/test_json_files/read_from_context/zero_cols_matrix.json new file mode 100644 index 00000000000..ae9a95b7f8e --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/zero_cols_matrix.json @@ -0,0 +1,6 @@ +{ + "x": [ + [], + [] + ] +} diff --git a/src/test/unit/io/test_json_files/read_from_context/zero_rows_matrix.json b/src/test/unit/io/test_json_files/read_from_context/zero_rows_matrix.json new file mode 100644 index 00000000000..5679c7135a2 --- /dev/null +++ b/src/test/unit/io/test_json_files/read_from_context/zero_rows_matrix.json @@ -0,0 +1,3 @@ +{ + "x": [] +} From fedbe211282cc5b0d5da4cf0cfe7170079d087f8 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Mon, 5 Oct 2026 14:31:20 -0700 Subject: [PATCH 03/18] simplify json reader complex/tuple handling Replace the array_block_sizes() accessor and its parallel side map with a field on the per-variable record. The JSON format erases where the enclosing array-of-tuples dimensions stop and a leaf variable's own dimensions begin, so that split has to be stored. var_entry::num_outer_dims records it, set once in update_array_dims() at the point the two are concatenated. block_size() and the leaf shape derive from it. This drops the boost unordered_flat_map dependency, the public accessor, and the remove() sync line. Other cleanups in the same area: - json_data::validate_dims delegates to stan::io::validate_dims, as every other var_context subclass already does; only the JSON '[]'-ambiguity guard stays local - document the mixed array-of-tuples layout on the json_data class, which claimed column-major with no carve-out - vals_c rejects a trailing 2 that belongs to an enclosing array rather than to the variable itself, which is what the old block_size == 0 test meant - rename var_value to var_entry and num_elements to size_from_dims, both of which collided with differently-behaving names in stan::math - replace per-element map lookups in the append path with a single lookup and a range insert; move the column-major temporaries instead of copying them; pass variable names by const reference --- src/stan/io/json/json_data.hpp | 129 ++++-------- src/stan/io/json/json_data_handler.hpp | 183 +++++++++--------- .../unit/io/json/json_data_tuples_test.cpp | 134 +++++++++++++ 3 files changed, 267 insertions(+), 179 deletions(-) diff --git a/src/stan/io/json/json_data.hpp b/src/stan/io/json/json_data.hpp index d29feab625e..7408a4ca50a 100644 --- a/src/stan/io/json/json_data.hpp +++ b/src/stan/io/json/json_data.hpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include #include #include @@ -36,6 +36,17 @@ namespace json { * stored as a vector of ints, else the array will be stored * as a vector of type double. * + *

A variable nested inside one or more arrays of tuples is named by the + * dotted path to its innermost tuple slot. Its dimensions are the dimensions + * of the enclosing arrays followed by the dimensions of the variable itself, + * and its values hold one contiguous block per enclosing tuple element, laid + * out back to back. The enclosing array dimensions are in element (row-major) + * order, while the values within each block are column-major as above; this + * mixed layout is the convention assumed by + * stan::io::read_from_context. The dimension vector alone does + * not say where the enclosing dimensions end and the variable's own begin, + * so var_entry::num_outer_dims records that split. + * *

json_data objects are created by using the * json_parser and a json_data_handler * to read a single JSON text from an input stream. @@ -44,7 +55,6 @@ class json_data : public stan::io::var_context { private: vars_map_r vars_r_; vars_map_i vars_i_; - boost::unordered_flat_map array_block_sizes_; std::vector const empty_vec_r_; std::vector const empty_vec_i_; @@ -64,36 +74,37 @@ class json_data : public stan::io::var_context { } /** - * Decode complex components within each tuple-free array block. + * Decode complex components within each innermost array block. + * + * Values are stored as one block per enclosing tuple element, laid out + * back to back. Within a block the real components precede the imaginary + * ones, so the component of a value is a half block away from its real part. * * @tparam T Stored scalar type, either int or double. * @param name Variable name. - * @param data Flat values and combined dimensions for the variable. + * @param var Stored values and dimensions for the variable. * @return Complex values in the order of the input blocks. * @throw json_error if nonempty data has no trailing component dimension 2. */ template - std::vector> vals_c_impl( - const std::string &name, - const std::pair, std::vector> &data) const { - const auto &values = data.first; + std::vector> vals_c_impl(const std::string &name, + const var_entry &var) const { + const auto &values = var.values; if (values.empty()) return {}; - const auto block = array_block_sizes_.find(name); - if (block == array_block_sizes_.end() || data.second.empty() - || data.second.back() != 2) { + // The trailing 2 must be the variable's own, not an enclosing array dim. + if (!var.has_own_dims() || var.dims.back() != 2) { throw json_error("Variable: " + name + ", expected a trailing dimension of 2 for complex " "values."); } - const size_t block_size = block->second; - const size_t offset = block_size / 2; - std::vector> result(values.size() / 2); + const size_t block_size = var.block_size(); + const size_t half_block = block_size / 2; + std::vector> result; + result.reserve(values.size() / 2); for (size_t start = 0; start < values.size(); start += block_size) { - for (size_t i = 0; i < offset; ++i) { - result[start / 2 + i] - = {static_cast(values[start + i]), - static_cast(values[start + offset + i])}; + for (size_t i = 0; i < half_block; ++i) { + result.emplace_back(values[start + i], values[start + half_block + i]); } } return result; @@ -111,7 +122,6 @@ class json_data : public stan::io::var_context { explicit json_data(std::istream &in) : vars_r_(), vars_i_() { json_data_handler handler(vars_r_, vars_i_); rapidjson_parse(in, handler); - array_block_sizes_ = handler.array_block_sizes(); } /** @@ -147,14 +157,10 @@ class json_data : public stan::io::var_context { */ std::vector vals_r(const std::string &name) const { if (contains_r_only(name)) { - return (vars_r_.find(name)->second).first; + return vars_r_.find(name)->second.values; } else if (contains_i(name)) { - std::vector vec_int = (vars_i_.find(name)->second).first; - std::vector vec_r(vec_int.size()); - for (size_t ii = 0; ii < vec_int.size(); ii++) { - vec_r[ii] = vec_int[ii]; - } - return vec_r; + const std::vector &vec_int = vars_i_.find(name)->second.values; + return std::vector(vec_int.begin(), vec_int.end()); } return empty_vec_r_; } @@ -185,9 +191,9 @@ class json_data : public stan::io::var_context { */ std::vector dims_r(const std::string &name) const { if (contains_r_only(name)) { - return (vars_r_.find(name)->second).second; + return vars_r_.find(name)->second.dims; } else if (contains_i(name)) { - return (vars_i_.find(name)->second).second; + return vars_i_.find(name)->second.dims; } return empty_vec_ui_; } @@ -201,7 +207,7 @@ class json_data : public stan::io::var_context { */ std::vector vals_i(const std::string &name) const { if (contains_i(name)) { - return (vars_i_.find(name)->second).first; + return vars_i_.find(name)->second.values; } return empty_vec_i_; } @@ -215,7 +221,7 @@ class json_data : public stan::io::var_context { */ std::vector dims_i(const std::string &name) const { if (contains_i(name)) { - return (vars_i_.find(name)->second).second; + return vars_i_.find(name)->second.dims; } return empty_vec_ui_; } @@ -254,7 +260,6 @@ class json_data : public stan::io::var_context { * returns false. */ bool remove(const std::string &name) { - array_block_sizes_.erase(name); return (vars_i_.erase(name) > 0) || (vars_r_.erase(name) > 0); } @@ -264,66 +269,12 @@ class json_data : public stan::io::var_context { std::vector dims = dims_r(name); // JSON '[ ]' is ambiguous - any multi-dim variable with len 0 dim - size_t num_elements = 1; - if (dims.size() == 0) { - // treat non-existent variables as size-0 objects - num_elements = 0; - } else { - for (size_t i = 0; i < dims.size(); ++i) { - num_elements *= dims[i]; - } - } - - size_t num_elements_expected = 1; - for (size_t i = 0; i < dims_declared.size(); ++i) { - num_elements_expected *= dims_declared[i]; - } - - if (num_elements == 0 && num_elements_expected == 0) + // treat non-existent variables as size-0 objects + size_t num_values = dims.empty() ? 0 : size_from_dims(dims); + if (num_values == 0 && size_from_dims(dims_declared) == 0) return; - if (dims.size() != dims_declared.size()) { - std::stringstream msg; - msg << "mismatch in number dimensions declared and found in context" - << "; processing stage=" << stage << "; variable name=" << name - << "; dims declared="; - dims_msg(msg, dims_declared); - msg << "; dims found="; - dims_msg(msg, dims); - throw std::runtime_error(msg.str()); - } - for (size_t i = 0; i < dims.size(); ++i) { - if (dims_declared[i] != dims[i]) { - std::stringstream msg; - msg << "mismatch in dimension declared and found in context" - << "; processing stage=" << stage << "; variable name=" << name - << "; position=" << i << "; dims declared="; - dims_msg(msg, dims_declared); - msg << "; dims found="; - dims_msg(msg, dims); - throw std::runtime_error(msg.str()); - } - } - - bool is_int_type = base_type == "int"; - if (is_int_type) { - if (!contains_i(name)) { - std::stringstream msg; - msg << (contains_r(name) ? "int variable contained non-int values" - : "variable does not exist") - << "; processing stage=" << stage << "; variable name=" << name - << "; base type=" << base_type; - throw std::runtime_error(msg.str()); - } - } else { - if (!contains_r(name)) { - std::stringstream msg; - msg << "variable does not exist" - << "; processing stage=" << stage << "; variable name=" << name - << "; base type=" << base_type; - throw std::runtime_error(msg.str()); - } - } + stan::io::validate_dims(*this, stage, name, base_type, dims_declared); } }; diff --git a/src/stan/io/json/json_data_handler.hpp b/src/stan/io/json/json_data_handler.hpp index 5364971f915..70aa190878c 100644 --- a/src/stan/io/json/json_data_handler.hpp +++ b/src/stan/io/json/json_data_handler.hpp @@ -6,8 +6,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -23,8 +23,48 @@ namespace stan { namespace json { -typedef std::pair, std::vector> var_r; -typedef std::pair, std::vector> var_i; +/** Number of scalar values in an array with the given dimensions. */ +inline size_t size_from_dims(std::vector::const_iterator first, + std::vector::const_iterator last) { + return std::accumulate(first, last, static_cast(1), + std::multiplies()); +} + +inline size_t size_from_dims(const std::vector& dims) { + return size_from_dims(dims.cbegin(), dims.cend()); +} + +/** The values and dimensions of a single Stan variable. + * + * For a variable nested in one or more arrays of tuples, `dims` is the + * dimensions of those enclosing arrays followed by the dimensions of the + * variable itself, and `values` holds one contiguous block per enclosing + * tuple element, laid out back to back. `num_outer_dims` is how many + * leading entries of `dims` belong to the enclosing arrays, which `dims` + * alone does not determine. + */ +template +struct var_entry { + std::vector values; + std::vector dims; + size_t num_outer_dims = 0; + + /** Dimensions of the variable itself, without the enclosing array dims. */ + std::vector::const_iterator own_dims_begin() const { + return dims.cbegin() + num_outer_dims; + } + + /** True if the variable itself is an array rather than a scalar. */ + bool has_own_dims() const { return num_outer_dims < dims.size(); } + + /** Number of values in one enclosing tuple element's block. */ + size_t block_size() const { + return size_from_dims(own_dims_begin(), dims.cend()); + } +}; + +typedef var_entry var_r; +typedef var_entry var_i; typedef std::map vars_map_r; typedef std::map vars_map_i; @@ -237,6 +277,17 @@ class json_data_handler : public stan::json::json_handler { } } + /* Append one tuple element's block of values to an existing variable. + * `dims` is the variable's own shape; update_array_dims() prepends the + * dimensions of the enclosing arrays once all elements have been seen. + */ + template + static void append_block(var_entry& var, const std::vector& values, + const std::vector& dims) { + var.values.insert(var.values.end(), values.begin(), values.end()); + var.dims = dims; + } + /* Save non-tuple vars and innermost tuple slots to vars_i and vars_r. * Converts multi-dim arrays from row-major to column major. * For arrays of tuples we need to check that new elements are consistent @@ -265,23 +316,19 @@ class json_data_handler : public stan::json::json_handler { if (is_int) { std::vector cm_values_i(values_i.size()); to_column_major(key, cm_values_i, values_i, dims); - values_i.assign(cm_values_i.begin(), cm_values_i.end()); + values_i = std::move(cm_values_i); } else { std::vector cm_values_r(values_r.size()); to_column_major(key, cm_values_r, values_r, dims); - values_r.assign(cm_values_r.begin(), cm_values_r.end()); + values_r = std::move(cm_values_r); } } if (is_new) { var_types_map[key] = slot_types_map[key]; if (is_int) { - std::pair, std::vector> pair; - pair = make_pair(values_i, dims); - vars_i[key] = pair; + vars_i[key] = var_i{values_i, dims, 0}; } else { - std::pair, std::vector> pair; - pair = make_pair(values_r, dims); - vars_r[key] = pair; + vars_r[key] = var_r{values_r, dims, 0}; } } else { bool is_aot = false; @@ -296,28 +343,11 @@ class json_data_handler : public stan::json::json_handler { } if (!is_aot) unexpected_error(key, "not array of tuples"); - bool consistent = true; - if (is_int || was_int) { - auto expect_dims = vars_i[key].second; - size_t expect_vals_len = 1; - for (auto& x : expect_dims) - expect_vals_len *= x; - if (is_int) { - if (expect_vals_len != values_i.size()) - consistent = false; - } else { - if (expect_vals_len != values_r.size()) - consistent = false; - } - } else { - auto expect_dims = vars_r[key].second; - size_t expect_vals_len = 1; - for (auto& x : expect_dims) - expect_vals_len *= x; - if (expect_vals_len != values_r.size()) - consistent = false; - } - if (!consistent) { + size_t expect_vals_len = (is_int || was_int) + ? size_from_dims(vars_i[key].dims) + : size_from_dims(vars_r[key].dims); + size_t found_vals_len = is_int ? values_i.size() : values_r.size(); + if (expect_vals_len != found_vals_len) { std::stringstream errorMsg; errorMsg << "Variable " << key << ": size mismatch between tuple elements."; @@ -325,32 +355,27 @@ class json_data_handler : public stan::json::json_handler { } var_types_map[key] = meta_type::ARRAY; if ((!is_int && was_int) || (is_int && is_real)) { // promote to double + const std::vector& prev_values = vars_i[key].values; std::vector values_tmp; - for (auto& x : vars_i[key].first) { - values_tmp.push_back(x); - } - for (auto& x : values_r) - values_tmp.push_back(x); - std::pair, std::vector> pair; - pair = make_pair(values_tmp, dims); - vars_r[key] = pair; + values_tmp.reserve(prev_values.size() + values_r.size()); + values_tmp.insert(values_tmp.end(), prev_values.begin(), + prev_values.end()); + values_tmp.insert(values_tmp.end(), values_r.begin(), values_r.end()); + vars_r[key] = var_r{std::move(values_tmp), dims, 0}; vars_i.erase(key); } else if (is_int) { - for (auto& x : values_i) - vars_i[key].first.push_back(x); - vars_i[key].second = dims; + append_block(vars_i[key], values_i, dims); } else { - for (auto& x : values_r) - vars_r[key].first.push_back(x); - vars_r[key].second = dims; + append_block(vars_r[key], values_r, dims); } } } key_stack.pop_back(); } - /* For array of tuples, concatenate dimensions - * Update vars_i and vars_r dimensions accordingly. + /* For array of tuples, prepend the dimensions of the enclosing arrays + * onto the variable's own dimensions and record where the two meet. + * This is the point at which the split is otherwise lost. */ void update_array_dims() { for (auto const& var : var_types_map) { @@ -362,23 +387,24 @@ class json_data_handler : public stan::json::json_handler { std::string slot; for (size_t i = 0; i < slots.size(); ++i) { slot.append(slots[i]); - if (slot_dims_map.count(slot) == 1 - && !slot_dims_map[slot].dims.empty()) { - for (auto& x : slot_dims_map[slot].dims) - all_dims.push_back(x); - } + auto it = slot_dims_map.find(slot); + if (it != slot_dims_map.end()) + all_dims.insert(all_dims.end(), it->second.dims.begin(), + it->second.dims.end()); slot.append("."); } - if (vars_i.count(var.first) == 1) { - if (all_dims.size() == vars_i[var.first].second.size()) - continue; - else - vars_i[var.first].second.assign(all_dims.begin(), all_dims.end()); - } else if (vars_r.count(var.first) == 1) { - if (all_dims.size() == vars_r[var.first].second.size()) - continue; - else - vars_r[var.first].second.assign(all_dims.begin(), all_dims.end()); + auto set_dims = [&all_dims](auto& entry) { + if (all_dims.size() > entry.dims.size()) { + entry.num_outer_dims = all_dims.size() - entry.dims.size(); + entry.dims = all_dims; + } + }; + auto it_i = vars_i.find(var.first); + auto it_r = vars_r.find(var.first); + if (it_i != vars_i.end()) { + set_dims(it_i->second); + } else if (it_r != vars_r.end()) { + set_dims(it_r->second); } else { std::stringstream errorMsg; errorMsg << "Variable: " << var.first << ", ill-formed JSON."; @@ -388,13 +414,10 @@ class json_data_handler : public stan::json::json_handler { } template - void to_column_major(std::string vname, std::vector& cm_vals, + void to_column_major(const std::string& vname, std::vector& cm_vals, const std::vector& rm_vals, const std::vector& dims) { - size_t expected_size = 1; - for (auto& x : dims) - expected_size *= x; - if (expected_size != rm_vals.size()) { + if (size_from_dims(dims) != rm_vals.size()) { std::stringstream errorMsg; errorMsg << "Variable: " << vname << ", error: ill-formed array."; throw json_error(errorMsg.str()); @@ -454,26 +477,6 @@ class json_data_handler : public stan::json::json_handler { */ void end_text() { update_array_dims(); } - /** - * Return the number of scalar values in each tuple-free array block. - * Unlike the dimensions in vars_r and vars_i after end_text(), these - * sizes exclude enclosing arrays of tuples. - * - * @return Map from leaf names to the size of one array block. - */ - boost::unordered_flat_map array_block_sizes() const { - boost::unordered_flat_map sizes; - for (const auto& slot : slot_dims_map) { - if (slot_types_map.at(slot.first) != meta_type::ARRAY) - continue; - size_t size = 1; - for (size_t dim : slot.second.dims) - size *= dim; - sizes.emplace(slot.first, size); - } - return sizes; - } - /** A key is either a top-level Stan variable name or a tuple slot id. * Logic handles edge case where key is the first slot of a tuple; * the name of the enclosing object is not used in the generated C++, @@ -744,7 +747,7 @@ class json_data_handler : public stan::json::json_handler { /** This function provides the column-major offset of an array element * given its row-major offset and the array dimensions. */ - size_t convert_offset_rtl_2_ltr(std::string vname, size_t rtl_offset, + size_t convert_offset_rtl_2_ltr(const std::string& vname, size_t rtl_offset, const std::vector& dims) { size_t rtl_dsize = 1; for (size_t i = 1; i < dims.size(); i++) diff --git a/src/test/unit/io/json/json_data_tuples_test.cpp b/src/test/unit/io/json/json_data_tuples_test.cpp index 4dde6087909..02b51ca1ae6 100644 --- a/src/test/unit/io/json/json_data_tuples_test.cpp +++ b/src/test/unit/io/json/json_data_tuples_test.cpp @@ -329,3 +329,137 @@ TEST(ioJsonTuples, jsonData_plus_comments_2) { std::vector expected_dims_y; test_real_var(jdata, "y", expected_vals_y, expected_dims_y); } + +/* --------------------------------------------------------------------------- + * N-dimensional arrays of tuples. + * + * Layout convention: the arrays enclosing a tuple are traversed in element + * order, while each leaf payload is converted to column-major. A tuple slot + * therefore does NOT match a plain array of the same declared shape, whose + * every dimension is column-major. One-dimensional outer arrays cannot tell + * the two apart, so these cases start at two dimensions. + * --------------------------------------------------------------------------- + */ + +TEST(ioJsonTuples, jsonData_array_2d_tuple_dims) { + std::stringstream in(R"({ + "x": [[{"1": 1.0, "2": 11}, {"1": 2.0, "2": 12}, {"1": 3.0, "2": 13}], + [{"1": 4.0, "2": 14}, {"1": 5.0, "2": 15}, {"1": 6.0, "2": 16}]] + })"); + stan::json::json_data jdata(in); + + EXPECT_EQ((std::vector{2, 3}), jdata.dims_r("x.1")); + EXPECT_EQ((std::vector{2, 3}), jdata.dims_i("x.2")); + EXPECT_TRUE(jdata.contains_i("x.2")); + EXPECT_FALSE(jdata.contains_i("x.1")); + EXPECT_EQ(6u, jdata.vals_r("x.1").size()); + EXPECT_EQ(6u, jdata.vals_i("x.2").size()); +} + +TEST(ioJsonTuples, jsonData_array_3d_tuple_dims) { + std::stringstream in(R"({ + "x": [[[{"1": 1.0}, {"1": 2.0}], [{"1": 3.0}, {"1": 4.0}]], + [[{"1": 5.0}, {"1": 6.0}], [{"1": 7.0}, {"1": 8.0}]]] + })"); + stan::json::json_data jdata(in); + + EXPECT_EQ((std::vector{2, 2, 2}), jdata.dims_r("x.1")); + EXPECT_EQ(8u, jdata.vals_r("x.1").size()); +} + +TEST(ioJsonTuples, jsonData_array_2d_tuple_array_slot_dims) { + std::stringstream in(R"({ + "x": [[{"1": [1.0, 2.0]}, {"1": [3.0, 4.0]}], + [{"1": [5.0, 6.0]}, {"1": [7.0, 8.0]}]] + })"); + stan::json::json_data jdata(in); + + EXPECT_EQ((std::vector{2, 2, 2}), jdata.dims_r("x.1")); + EXPECT_EQ(8u, jdata.vals_r("x.1").size()); +} + +TEST(ioJsonTuples, jsonData_array_2d_nested_tuple_dims) { + std::stringstream in(R"({ + "x": [[{"1": {"1": 1.0}}, {"1": {"1": 2.0}}], + [{"1": {"1": 3.0}}, {"1": {"1": 4.0}}]] + })"); + stan::json::json_data jdata(in); + + EXPECT_EQ((std::vector{2, 2}), jdata.dims_r("x.1.1")); + EXPECT_EQ(4u, jdata.vals_r("x.1.1").size()); +} + +TEST(ioJsonTuples, jsonData_array_2d_tuple_int_and_promotion) { + std::stringstream in(R"({ + "a": [[{"1": 1}, {"1": 2}], [{"1": 3}, {"1": 4}]], + "b": [[{"1": 1}, {"1": 2}], [{"1": 3}, {"1": 4.5}]] + })"); + stan::json::json_data jdata(in); + + EXPECT_TRUE(jdata.contains_i("a.1")); + EXPECT_FALSE(jdata.contains_i("b.1")); + EXPECT_EQ((std::vector{2, 2}), jdata.dims_i("a.1")); + EXPECT_EQ((std::vector{2, 2}), jdata.dims_r("b.1")); +} + +TEST(ioJsonTuples, jsonData_array_2d_tuple_ragged_outer) { + test_exception(R"({"x": [[{"1": 1.0}, {"1": 2.0}], [{"1": 3.0}]]})", + "Variable: x, error: non-rectangular array."); +} + +TEST(ioJsonTuples, jsonData_array_2d_tuple_slot_count_mismatch) { + test_exception(R"({"x": [[{"1": 1.0, "2": 9.0}, {"1": 2.0, "2": 9.0}], + [{"1": 3.0}, {"1": 4.0, "2": 9.0}]]})", + "Variable x: size mismatch between tuple elements."); +} + +TEST(ioJsonTuples, jsonData_array_2d_tuple_slot_size_mismatch) { + test_exception(R"({"x": [[{"1": [1.0, 2.0]}, {"1": [3.0, 4.0]}], + [{"1": [5.0]}, {"1": [7.0, 8.0]}]]})", + "Variable: x.1, error: non-rectangular array."); +} + +TEST(ioJsonTuples, jsonData_array_2d_tuple_uses_element_order) { + std::stringstream in(R"({ + "x": [[{"1": 1.0}, {"1": 2.0}, {"1": 3.0}], + [{"1": 4.0}, {"1": 5.0}, {"1": 6.0}]], + "plain": [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]] + })"); + stan::json::json_data jdata(in); + + EXPECT_EQ(jdata.dims_r("plain"), jdata.dims_r("x.1")); + EXPECT_EQ((std::vector{1, 2, 3, 4, 5, 6}), jdata.vals_r("x.1")); + EXPECT_EQ((std::vector{1, 4, 2, 5, 3, 6}), jdata.vals_r("plain")); +} + +TEST(ioJsonTuples, jsonData_array_3d_tuple_uses_element_order) { + std::stringstream in(R"({ + "x": [[[{"1": 1.0}, {"1": 2.0}], [{"1": 3.0}, {"1": 4.0}]], + [[{"1": 5.0}, {"1": 6.0}], [{"1": 7.0}, {"1": 8.0}]]], + "plain": [[[1.0, 2.0], [3.0, 4.0]], [[5.0, 6.0], [7.0, 8.0]]] + })"); + stan::json::json_data jdata(in); + + EXPECT_EQ(jdata.dims_r("plain"), jdata.dims_r("x.1")); + EXPECT_EQ((std::vector{1, 2, 3, 4, 5, 6, 7, 8}), jdata.vals_r("x.1")); + EXPECT_EQ((std::vector{1, 5, 3, 7, 2, 6, 4, 8}), + jdata.vals_r("plain")); +} + +TEST(ioJsonTuples, jsonData_array_2d_tuple_complex_uses_element_order) { + std::stringstream in(R"({ + "x": [[{"1": [1.0, 101.0]}, {"1": [2.0, 102.0]}, {"1": [3.0, 103.0]}], + [{"1": [4.0, 104.0]}, {"1": [5.0, 105.0]}, {"1": [6.0, 106.0]}]], + "plain": [[[1.0, 101.0], [2.0, 102.0], [3.0, 103.0]], + [[4.0, 104.0], [5.0, 105.0], [6.0, 106.0]]] + })"); + stan::json::json_data jdata(in); + + EXPECT_EQ(jdata.dims_r("plain"), jdata.dims_r("x.1")); + EXPECT_EQ((std::vector>{ + {1, 101}, {2, 102}, {3, 103}, {4, 104}, {5, 105}, {6, 106}}), + jdata.vals_c("x.1")); + EXPECT_EQ((std::vector>{ + {1, 101}, {4, 104}, {2, 102}, {5, 105}, {3, 103}, {6, 106}}), + jdata.vals_c("plain")); +} From 3cbc103234a462ed800cc3ab26fa12017827015c Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Mon, 5 Oct 2026 15:16:13 -0700 Subject: [PATCH 04/18] take json handler keys and strings as string_view The rapidjson callbacks were passing a bare const char* into a const std::string& parameter, discarding the length rapidjson already provides. That cost a strlen and a heap allocation for every key and every string token in the input, for text that was then copied again into the key stack. json_handler::key and json_handler::string now take std::string_view and the parser passes {str, length}. Also take string_view in valid_varname, unexpected_error, to_column_major and convert_offset_rtl_2_ltr. All seven unexpected_error call sites pass string literals, each of which was building a std::string only to stream it into a stringstream and destroy it. slot_types_map gets a std::less<> comparator so key() can look up a view without converting it back to a string; it is the only map looked up that way. The two signatures that changed are marked override in both handler subclasses, since otherwise a missed subclass silently becomes a non-overriding overload and stops receiving events. The std::string parameters left in json_data are var_context virtuals, which cannot change without changing the interface and its other implementations. --- src/stan/io/json/json_data_handler.hpp | 20 +++++++++++--------- src/stan/io/json/json_handler.hpp | 12 +++++++++--- src/stan/io/json/rapidjson_parser.hpp | 7 ++++--- src/test/unit/io/json/util.hpp | 5 +++-- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/stan/io/json/json_data_handler.hpp b/src/stan/io/json/json_data_handler.hpp index 70aa190878c..0942a10de3c 100644 --- a/src/stan/io/json/json_data_handler.hpp +++ b/src/stan/io/json/json_data_handler.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -166,8 +167,9 @@ class json_data_handler : public stan::json::json_handler { vars_map_r& vars_r; vars_map_i& vars_i; std::vector key_stack; - std::map var_types_map; // vars_r and vars_i entries - std::map slot_types_map; // all slots all vars parsed + std::map var_types_map; // vars_r and vars_i entries + // std::less<> so that key() can look up a string_view without copying it. + std::map> slot_types_map; std::map slot_dims_map; std::map tuple_slots_map; std::map int_slots_map; @@ -210,7 +212,7 @@ class json_data_handler : public stan::json::json_handler { /** Stan variable names must start with a letter * and contain only letters, numbers, or an underscore. */ - bool valid_varname(const std::string& name) { + bool valid_varname(std::string_view name) { static const std::locale& locale = std::locale::classic(); if (name.empty() || !std::isalpha(name[0], locale)) { return false; @@ -414,7 +416,7 @@ class json_data_handler : public stan::json::json_handler { } template - void to_column_major(const std::string& vname, std::vector& cm_vals, + void to_column_major(std::string_view vname, std::vector& cm_vals, const std::vector& rm_vals, const std::vector& dims) { if (size_from_dims(dims) != rm_vals.size()) { @@ -428,7 +430,7 @@ class json_data_handler : public stan::json::json_handler { } } - void unexpected_error(const std::string& where, const std::string& what) { + void unexpected_error(std::string_view where, std::string_view what) { std::stringstream errorMsg; errorMsg << "Variable " << where << ", " << what << "."; throw json_error(errorMsg.str()); @@ -482,14 +484,14 @@ class json_data_handler : public stan::json::json_handler { * the name of the enclosing object is not used in the generated C++, * but we still need to track the number of tuple slots. */ - void key(const std::string& key) { + void key(std::string_view key) override { if (event != meta_event::OBJ_OPEN) { save_key_value_pair(); } event = meta_event::KEY; reset_values(); std::string outer = key_str(); - key_stack.push_back(key); + key_stack.emplace_back(key); if (key_stack.size() == 1) { not_stan_var = !valid_varname(key); } @@ -675,7 +677,7 @@ class json_data_handler : public stan::json::json_handler { throw json_error(errorMsg.str()); } - void string(const std::string& s) { + void string(std::string_view s) override { if (not_stan_var) return; double tmp; @@ -747,7 +749,7 @@ class json_data_handler : public stan::json::json_handler { /** This function provides the column-major offset of an array element * given its row-major offset and the array dimensions. */ - size_t convert_offset_rtl_2_ltr(const std::string& vname, size_t rtl_offset, + size_t convert_offset_rtl_2_ltr(std::string_view vname, size_t rtl_offset, const std::vector& dims) { size_t rtl_dsize = 1; for (size_t i = 1; i < dims.size(); i++) diff --git a/src/stan/io/json/json_handler.hpp b/src/stan/io/json/json_handler.hpp index ea6a2f072a3..328c4cc3c3a 100644 --- a/src/stan/io/json/json_handler.hpp +++ b/src/stan/io/json/json_handler.hpp @@ -2,7 +2,7 @@ #define STAN_IO_JSON_JSON_HANDLER_HPP #include -#include +#include namespace stan { @@ -100,16 +100,22 @@ class json_handler { /** * Handle the specified string value. * + * The view is only valid for the duration of the call; a handler which + * needs to retain the text must copy it. + * * @param s String value to handle. */ - virtual void string(const std::string &s) {} + virtual void string(std::string_view s) {} /** * Handle the specified object key. * + * The view is only valid for the duration of the call; a handler which + * needs to retain the text must copy it. + * * @param s String object key to handle. */ - virtual void key(const std::string &s) {} + virtual void key(std::string_view s) {} }; } // namespace json diff --git a/src/stan/io/json/rapidjson_parser.hpp b/src/stan/io/json/rapidjson_parser.hpp index 7341b3df64d..d02f257b2bf 100644 --- a/src/stan/io/json/rapidjson_parser.hpp +++ b/src/stan/io/json/rapidjson_parser.hpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace stan { namespace json { @@ -64,7 +65,7 @@ struct RapidJSONHandler { return true; } bool String(const char *str, rapidjson::SizeType length, bool copy) { - h_.string(str); + h_.string(std::string_view(str, length)); return check_start(); } bool StartObject() { @@ -74,8 +75,8 @@ struct RapidJSONHandler { return true; } bool Key(const char *str, rapidjson::SizeType length, bool copy) { - h_.key(str); - last_key_ = str; + h_.key(std::string_view(str, length)); + last_key_.assign(str, length); return check_start(); } bool EndObject(rapidjson::SizeType memberCount) { diff --git a/src/test/unit/io/json/util.hpp b/src/test/unit/io/json/util.hpp index f695e9eb9f6..92a11d5c9f6 100644 --- a/src/test/unit/io/json/util.hpp +++ b/src/test/unit/io/json/util.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -26,8 +27,8 @@ class recording_handler : public stan::json::json_handler { void end_object() { os_ << "E:obj"; } void null() { os_ << "NULL:null"; } void boolean(bool p) { os_ << "BOOL:" << p; } - void string(const std::string &s) { os_ << "STR:\"" << s << "\""; } - void key(const std::string &key) { os_ << "KEY:\"" << key << "\""; } + void string(std::string_view s) override { os_ << "STR:\"" << s << "\""; } + void key(std::string_view key) override { os_ << "KEY:\"" << key << "\""; } void number_double(double x) { os_ << "D(REAL):" << x; } void number_int(int n) { os_ << "I(INT):" << n; } void number_unsigned_int(unsigned n) { os_ << "U(INT):" << n; } From 28ed04e20d93fe64679023b24a5656084d2a8100 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Mon, 5 Oct 2026 15:51:52 -0700 Subject: [PATCH 05/18] clarify json var_entry naming and use unordered maps Rename num_outer_dims to num_outer_arrays, since it counts enclosing array depth rather than dimensions in general, and has_own_dims to is_array, which is what it actually asks. That makes own_dims_begin() redundant. Replace the prose describing the array-of-tuples layout with a worked example in both the var_entry and json_data docs, and show the complex block layout as a decode of six values rather than describing it. The handler's five bookkeeping maps are keyed by name and only ever looked up, apart from update_array_dims, which does not depend on iteration order. Use boost::unordered_flat_map with a transparent hasher so that key() can still look up a string_view without building a string. vars_r and vars_i stay ordered because names_r and names_i report their iteration order. Rename objects called var, which collides with stan::math::var. --- src/stan/io/json/json_data.hpp | 36 ++++++------- src/stan/io/json/json_data_handler.hpp | 73 +++++++++++++++----------- 2 files changed, 60 insertions(+), 49 deletions(-) diff --git a/src/stan/io/json/json_data.hpp b/src/stan/io/json/json_data.hpp index 7408a4ca50a..ba6eefff7bd 100644 --- a/src/stan/io/json/json_data.hpp +++ b/src/stan/io/json/json_data.hpp @@ -36,16 +36,13 @@ namespace json { * stored as a vector of ints, else the array will be stored * as a vector of type double. * - *

A variable nested inside one or more arrays of tuples is named by the - * dotted path to its innermost tuple slot. Its dimensions are the dimensions - * of the enclosing arrays followed by the dimensions of the variable itself, - * and its values hold one contiguous block per enclosing tuple element, laid - * out back to back. The enclosing array dimensions are in element (row-major) - * order, while the values within each block are column-major as above; this - * mixed layout is the convention assumed by - * stan::io::read_from_context. The dimension vector alone does - * not say where the enclosing dimensions end and the variable's own begin, - * so var_entry::num_outer_dims records that split. + *

A variable inside an array of tuples is named by the dotted path to its + * tuple slot. Given "x": [ [[1, 2, 3], 4], [[5, 6, 7], 8] ], + * slot x.1 has dims {2, 3} and values + * {1, 2, 3, 5, 6, 7}. The leading 2 is the enclosing array, + * which is in element order; the 3 values of each block are column-major as + * above. var_entry::num_outer_arrays records how many leading + * dimensions are enclosing arrays, which dims alone cannot say. * *

json_data objects are created by using the * json_parser and a json_data_handler @@ -76,29 +73,30 @@ class json_data : public stan::io::var_context { /** * Decode complex components within each innermost array block. * - * Values are stored as one block per enclosing tuple element, laid out - * back to back. Within a block the real components precede the imaginary - * ones, so the component of a value is a half block away from its real part. + * A block holds all the real components first, then all the imaginary + * ones, so a block of 6 values {r0, r1, r2, i0, i1, i2} + * decodes to {(r0,i0), (r1,i1), (r2,i2)}. An array of tuples + * contributes one such block per tuple element, laid out back to back. * * @tparam T Stored scalar type, either int or double. * @param name Variable name. - * @param var Stored values and dimensions for the variable. + * @param entry Stored values and dimensions for the variable. * @return Complex values in the order of the input blocks. * @throw json_error if nonempty data has no trailing component dimension 2. */ template - std::vector> vals_c_impl(const std::string &name, - const var_entry &var) const { - const auto &values = var.values; + std::vector> vals_c_impl( + const std::string &name, const var_entry &entry) const { + const auto &values = entry.values; if (values.empty()) return {}; // The trailing 2 must be the variable's own, not an enclosing array dim. - if (!var.has_own_dims() || var.dims.back() != 2) { + if (!entry.is_array() || entry.dims.back() != 2) { throw json_error("Variable: " + name + ", expected a trailing dimension of 2 for complex " "values."); } - const size_t block_size = var.block_size(); + const size_t block_size = entry.block_size(); const size_t half_block = block_size / 2; std::vector> result; result.reserve(values.size() / 2); diff --git a/src/stan/io/json/json_data_handler.hpp b/src/stan/io/json/json_data_handler.hpp index 0942a10de3c..1a22bff1658 100644 --- a/src/stan/io/json/json_data_handler.hpp +++ b/src/stan/io/json/json_data_handler.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -37,33 +38,46 @@ inline size_t size_from_dims(const std::vector& dims) { /** The values and dimensions of a single Stan variable. * - * For a variable nested in one or more arrays of tuples, `dims` is the - * dimensions of those enclosing arrays followed by the dimensions of the - * variable itself, and `values` holds one contiguous block per enclosing - * tuple element, laid out back to back. `num_outer_dims` is how many - * leading entries of `dims` belong to the enclosing arrays, which `dims` - * alone does not determine. + * For `x` given as `[ [[1, 2, 3], 4], [[5, 6, 7], 8] ]`, an array of 2 + * tuples whose first slot is an array of 3 reals, slot `x.1` holds + * + * values = {1, 2, 3, 5, 6, 7} dims = {2, 3} num_outer_arrays = 1 + * + * The leading 2 is the enclosing array; the trailing 3 is the slot's own + * shape, one block per tuple element. `dims` alone cannot say where the + * first ends and the second begins, so `num_outer_arrays` records it. */ template struct var_entry { std::vector values; std::vector dims; - size_t num_outer_dims = 0; - - /** Dimensions of the variable itself, without the enclosing array dims. */ - std::vector::const_iterator own_dims_begin() const { - return dims.cbegin() + num_outer_dims; - } + size_t num_outer_arrays = 0; /** True if the variable itself is an array rather than a scalar. */ - bool has_own_dims() const { return num_outer_dims < dims.size(); } + bool is_array() const { return num_outer_arrays < dims.size(); } /** Number of values in one enclosing tuple element's block. */ size_t block_size() const { - return size_from_dims(own_dims_begin(), dims.cend()); + return size_from_dims(dims.cbegin() + num_outer_arrays, dims.cend()); + } +}; + +/** Hashes `std::string` and `std::string_view` alike so that lookups by + * view do not have to build a string. + */ +struct string_hash { + using is_transparent = void; + using is_avalanching = void; + size_t operator()(std::string_view key) const noexcept { + return boost::hash{}(key); } }; +/** Name-keyed map used for the handler's parse-time bookkeeping. */ +template +using string_map + = boost::unordered_flat_map>; + typedef var_entry var_r; typedef var_entry var_i; @@ -167,12 +181,11 @@ class json_data_handler : public stan::json::json_handler { vars_map_r& vars_r; vars_map_i& vars_i; std::vector key_stack; - std::map var_types_map; // vars_r and vars_i entries - // std::less<> so that key() can look up a string_view without copying it. - std::map> slot_types_map; - std::map slot_dims_map; - std::map tuple_slots_map; - std::map int_slots_map; + string_map var_types_map; // vars_r and vars_i entries + string_map slot_types_map; // all slots all vars parsed + string_map slot_dims_map; + string_map tuple_slots_map; + string_map int_slots_map; std::vector values_r; // accumulates real var values std::vector values_i; // accumulates int var values size_t array_start_i; // index into values_i @@ -284,10 +297,10 @@ class json_data_handler : public stan::json::json_handler { * dimensions of the enclosing arrays once all elements have been seen. */ template - static void append_block(var_entry& var, const std::vector& values, + static void append_block(var_entry& entry, const std::vector& values, const std::vector& dims) { - var.values.insert(var.values.end(), values.begin(), values.end()); - var.dims = dims; + entry.values.insert(entry.values.end(), values.begin(), values.end()); + entry.dims = dims; } /* Save non-tuple vars and innermost tuple slots to vars_i and vars_r. @@ -380,12 +393,12 @@ class json_data_handler : public stan::json::json_handler { * This is the point at which the split is otherwise lost. */ void update_array_dims() { - for (auto const& var : var_types_map) { - if (var.second != meta_type::ARRAY) { + for (auto const& [name, type] : var_types_map) { + if (type != meta_type::ARRAY) { continue; } std::vector all_dims; - std::vector slots = stan::io::split(var.first, ".", true); + std::vector slots = stan::io::split(name, ".", true); std::string slot; for (size_t i = 0; i < slots.size(); ++i) { slot.append(slots[i]); @@ -397,19 +410,19 @@ class json_data_handler : public stan::json::json_handler { } auto set_dims = [&all_dims](auto& entry) { if (all_dims.size() > entry.dims.size()) { - entry.num_outer_dims = all_dims.size() - entry.dims.size(); + entry.num_outer_arrays = all_dims.size() - entry.dims.size(); entry.dims = all_dims; } }; - auto it_i = vars_i.find(var.first); - auto it_r = vars_r.find(var.first); + auto it_i = vars_i.find(name); + auto it_r = vars_r.find(name); if (it_i != vars_i.end()) { set_dims(it_i->second); } else if (it_r != vars_r.end()) { set_dims(it_r->second); } else { std::stringstream errorMsg; - errorMsg << "Variable: " << var.first << ", ill-formed JSON."; + errorMsg << "Variable: " << name << ", ill-formed JSON."; throw json_error(errorMsg.str()); } } From f7022a81486ee93e7e1bd08879c1ea0bd42c2164 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Mon, 5 Oct 2026 15:51:52 -0700 Subject: [PATCH 06/18] dispatch std::vector and Eigen reads without the tuple path A destination holding no tuples consumes its buffer as a single payload, so add if constexpr arms for std::vector, std::vector and Eigen destinations with int or double scalars alongside the existing scalar and complex arms. read_whole_payload is read_from_buffer_block followed by check_consumed, which is exactly what the general path does once the tuple-field path is empty, so errors and behavior are unchanged while the index_sequence plumbing is not instantiated. Eigen destinations with complex scalars and nested std::vectors still take the general path. --- src/stan/io/read_from_context.hpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/stan/io/read_from_context.hpp b/src/stan/io/read_from_context.hpp index b1c4242391f..3d34cdf1336 100644 --- a/src/stan/io/read_from_context.hpp +++ b/src/stan/io/read_from_context.hpp @@ -273,6 +273,25 @@ void check_consumed(const InVec& values, std::size_t position, } } +/** + * @brief Read a destination that holds no tuples, so its buffer is one + * payload consumed in full. Equivalent to walking an empty tuple-field path, + * without instantiating the path machinery. + * @tparam T Tuple-free destination type with its dimensions already set. + * @tparam InVec Input buffer type holding int values for integer + * destinations or double values for real and complex destinations. + * @param[in,out] x Destination with every container dimension allocated. + * @param[in] values Buffer loaded for this variable. + * @param[in] path Variable name, used in the error message. + * @throw std::runtime_error if the buffer does not match the destination. + */ +template +inline void read_whole_payload(T& x, const InVec& values, const std::string& path) { + std::size_t position = 0; + internal::read_from_buffer_block(x, values, position); + internal::check_consumed(values, position, path); +} + /** * @brief Load and write one named leaf at a time, releasing each buffer before * loading the next. Discover tuple paths from types, including for empty @@ -381,6 +400,16 @@ inline void read_from_context(T& x, const Context& context, + path); } x = values[0]; + } else if constexpr (std::is_same_v>) { + internal::read_whole_payload(x, context.vals_i(path), path); + } else if constexpr (std::is_same_v>) { + internal::read_whole_payload(x, context.vals_r(path), path); + } else if constexpr (is_eigen::value + && std::is_same_v, int>) { + internal::read_whole_payload(x, context.vals_i(path), path); + } else if constexpr (is_eigen::value + && std::is_same_v, double>) { + internal::read_whole_payload(x, context.vals_r(path), path); } else { internal::load_from_context(x, context, path, std::index_sequence<>{}); } From e25872cf8f1945483637e5268ef63ffc16e809e7 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Mon, 5 Oct 2026 16:09:12 -0700 Subject: [PATCH 07/18] store json results in unordered maps vars_map_r and vars_map_i do not need to be ordered. names_r and names_i are their only consumers of iteration order, var_context does not document one, and random_var_context already reports insertion order rather than sorted order, so no caller could rely on it. Nothing in the tree reads the order. Reference stability still holds under open addressing: the reference into vars_i in the promote branch is last read before vars_r is written and before the erase, and append_block consumes its reference within the call. Drops the now unused includes from both headers, and converts the remaining typedefs in the header to using aliases. --- src/stan/io/json/json_data.hpp | 1 - src/stan/io/json/json_data_handler.hpp | 14 ++++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/stan/io/json/json_data.hpp b/src/stan/io/json/json_data.hpp index ba6eefff7bd..92c51ce9bff 100644 --- a/src/stan/io/json/json_data.hpp +++ b/src/stan/io/json/json_data.hpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include diff --git a/src/stan/io/json/json_data_handler.hpp b/src/stan/io/json/json_data_handler.hpp index 1a22bff1658..c41d99ae946 100644 --- a/src/stan/io/json/json_data_handler.hpp +++ b/src/stan/io/json/json_data_handler.hpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include @@ -73,16 +72,19 @@ struct string_hash { } }; -/** Name-keyed map used for the handler's parse-time bookkeeping. */ +/** Name-keyed map used for the handler's bookkeeping and for its results. + * Nothing depends on the iteration order: names_r and names_i are its only + * consumers and var_context does not promise one. + */ template using string_map = boost::unordered_flat_map>; -typedef var_entry var_r; -typedef var_entry var_i; +using var_r = var_entry; +using var_i = var_entry; -typedef std::map vars_map_r; -typedef std::map vars_map_i; +using vars_map_r = string_map; +using vars_map_i = string_map; /** Enum of the kinds of structures the handler needs to manage. * Determined by the initial sequence of start elements following From 1a1ef09b034cd5f642159b450e810cca933ffdc4 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Mon, 5 Oct 2026 16:09:12 -0700 Subject: [PATCH 08/18] split real and complex payload readers The Eigen and std::vector readers each took an imaginary_offset that only meant anything for complex destinations, documented as "ignored for non-complex destinations" in both. Split each into read_real and read_complex and dispatch at the call site, where read_from_buffer_block already branches on is_complex> to pick the size check and the cursor advance. The recursion stays complex-aware throughout, so imaginary_offset is gone from the real path end to end rather than being threaded through unused. Factors the strided source map the Eigen readers share into source_block, which also removes the Stride and SourceMap aliases from each caller. --- src/stan/io/read_from_context.hpp | 146 +++++++++++++++++++++--------- 1 file changed, 102 insertions(+), 44 deletions(-) diff --git a/src/stan/io/read_from_context.hpp b/src/stan/io/read_from_context.hpp index 3d34cdf1336..01106d98f50 100644 --- a/src/stan/io/read_from_context.hpp +++ b/src/stan/io/read_from_context.hpp @@ -70,84 +70,141 @@ inline void read_from_buffer(std::complex& x, } /** - * @brief Read an Eigen object in logical column-major order without advancing - * a consumption cursor. - * @tparam EigMat Writable Eigen destination type with int, double, or - * std::complex coefficients. + * @brief Map a strided column-major source block onto a destination. + * @tparam Source Dense source matrix type matching the buffer's scalar type. + * @tparam InVec Contiguous input buffer type, possibly const. + */ +template +inline auto source_block(const InVec& values, std::size_t offset, + Eigen::Index rows, Eigen::Index cols, + std::size_t stride) { + using Stride = Eigen::InnerStride; + return Eigen::Map( + values.data() + offset, rows, cols, Stride(stride)); +} + +/** + * @brief Read an int or real Eigen object in logical column-major order + * without advancing a consumption cursor. + * @tparam EigMat Writable Eigen destination type with int or double + * coefficients. * @tparam InVec Contiguous input buffer type with int or double coefficients, - * possibly const. Integer destinations use integer buffers; real and complex - * destinations use real buffers. + * possibly const. Integer destinations use integer buffers. + * @param[in,out] x Destination with its rows and columns already allocated. + * @param[in] values Buffer containing the tuple-free payload to read. + * @param[in] offset Zero-based source index of the first coefficient's value. + * @param[in] stride Positive distance in buffer elements between successive + * logical column-major coefficients. + * @pre The caller has validated that all source indices are within values. + */ +template * = nullptr> +void read_real(EigMat& x, const InVec& values, std::size_t offset, + std::size_t stride) { + if (x.size() == 0) { + return; + } + using Source + = Eigen::Matrix, Eigen::Dynamic, Eigen::Dynamic>; + x = internal::source_block(values, offset, x.rows(), x.cols(), + stride); +} + +/** + * @brief Read a complex Eigen object in logical column-major order without + * advancing a consumption cursor. The real components of the payload precede + * the imaginary ones, so each component block is read separately. + * @tparam EigMat Writable Eigen destination type with std::complex + * coefficients. + * @tparam InVec Contiguous input buffer type with double coefficients, + * possibly const. * @param[in,out] x Destination with its rows and columns already allocated. * @param[in] values Buffer containing the tuple-free payload to read. - * @param[in] offset Zero-based source index of the first coefficient's value - * or real component. + * @param[in] offset Zero-based source index of the first real component. * @param[in] stride Positive distance in buffer elements between successive * logical column-major coefficients. * @param[in] imaginary_offset Distance in buffer elements from each real - * component to its imaginary component; ignored for non-complex destinations. + * component to its imaginary component. * @pre The caller has validated that all source indices are within values. */ template * = nullptr> -void read_from_buffer(EigMat& x, InVec& values, std::size_t offset, - std::size_t stride, std::size_t imaginary_offset) { - using Scalar = scalar_type_t; - const std::size_t size = x.size(); - if (size == 0) { +void read_complex(EigMat& x, const InVec& values, std::size_t offset, + std::size_t stride, std::size_t imaginary_offset) { + if (x.size() == 0) { return; } using Source = Eigen::Matrix, Eigen::Dynamic, Eigen::Dynamic>; - using Stride = Eigen::InnerStride; - using SourceMap = Eigen::Map; - if constexpr (stan::is_complex::value) { - x.real() - = SourceMap(values.data() + offset, x.rows(), x.cols(), Stride(stride)); - x.imag() = SourceMap(values.data() + offset + imaginary_offset, x.rows(), - x.cols(), Stride(stride)); + x.real() = internal::source_block(values, offset, x.rows(), x.cols(), + stride); + x.imag() = internal::source_block(values, offset + imaginary_offset, + x.rows(), x.cols(), stride); +} + +/** + * @brief Read an int or real tuple-free array using column-major source + * strides. Each child uses stride * x.size(). This function does not advance + * a consumption cursor. + * @tparam StdVec Destination std::vector type containing int, double, Eigen + * objects, or nested tuple-free std::vectors of these. + * @tparam InVec Input std::vector type holding int values for integer + * destinations or double values for real ones. + * @param[in,out] x Rectangular destination with every dimension allocated. + * @param[in] values Buffer containing the tuple-free payload to read. + * @param[in] offset Zero-based source index of the first value in this array. + * @param[in] stride Positive distance in buffer elements between the starting + * indices of consecutive elements of x. + * @pre The caller has validated that all source indices are within values. + */ +template * = nullptr> +void read_real(StdVec& x, const InVec& values, std::size_t offset, + std::size_t stride) { + using T = value_type_t; + if constexpr (std::is_same_v || std::is_same_v) { + using Vec = Eigen::Matrix; + using Stride = Eigen::InnerStride; + Eigen::Map(x.data(), x.size()) + = Eigen::Map( + values.data() + offset, x.size(), Stride(stride)); } else { - x = SourceMap(values.data() + offset, x.rows(), x.cols(), Stride(stride)); + for (std::size_t i = 0; i < x.size(); ++i) { + internal::read_real(x[i], values, offset + i * stride, stride * x.size()); + } } } /** - * @brief Read a tuple-free array using column-major source strides. + * @brief Read a complex tuple-free array using column-major source strides. * Each child uses stride * x.size(); imaginary_offset stays constant within * the payload. This function does not advance a consumption cursor. - * @tparam StdVec Destination std::vector type containing int, double, - * std::complex, Eigen objects, or nested tuple-free std::vectors. - * @tparam InVec Input std::vector type holding int values for integer - * destinations or double values for real and complex destinations. + * @tparam StdVec Destination std::vector type containing + * std::complex, Eigen objects, or nested tuple-free std::vectors of + * these. + * @tparam InVec Input std::vector type holding double values. * @param[in,out] x Rectangular destination with every dimension allocated. * @param[in] values Buffer containing the tuple-free payload to read. - * @param[in] offset Zero-based source index of the first value or real - * component in this array. + * @param[in] offset Zero-based source index of the first real component in + * this array. * @param[in] stride Positive distance in buffer elements between the starting * indices of consecutive elements of x. * @param[in] imaginary_offset Distance in buffer elements from each real - * component to its imaginary component; ignored for non-complex destinations. + * component to its imaginary component. * @pre The caller has validated that all source indices are within values. */ template * = nullptr> -void read_from_buffer(StdVec& x, const InVec& values, std::size_t offset, - std::size_t stride, std::size_t imaginary_offset) { +void read_complex(StdVec& x, const InVec& values, std::size_t offset, + std::size_t stride, std::size_t imaginary_offset) { using T = value_type_t; if constexpr (stan::is_complex::value) { for (std::size_t i = 0; i < x.size(); ++i) { const std::size_t idx = offset + i * stride; internal::read_from_buffer(x[i], values, idx, idx + imaginary_offset); } - } else if constexpr (std::is_same_v || std::is_same_v) { - using Vec = Eigen::Matrix; - using Stride = Eigen::InnerStride; - Eigen::Map(x.data(), x.size()) - = Eigen::Map( - values.data() + offset, x.size(), Stride(stride)); } else { for (std::size_t i = 0; i < x.size(); ++i) { - const std::size_t idx = offset + i * stride; - internal::read_from_buffer(x[i], values, idx, stride * x.size(), - imaginary_offset); + internal::read_complex(x[i], values, offset + i * stride, + stride * x.size(), imaginary_offset); } } } @@ -189,7 +246,7 @@ void read_from_buffer_block(T& x, InputVec&& values, std::size_t& position) { if constexpr (stan::is_complex::value) { internal::read_from_buffer(x, values, position, position + 1); } else { - internal::read_from_buffer(x, values, position, 1, size); + internal::read_complex(x, values, position, 1, size); } position += 2 * size; } else { @@ -200,7 +257,7 @@ void read_from_buffer_block(T& x, InputVec&& values, std::size_t& position) { if constexpr (std::is_integral_v || std::is_floating_point_v) { internal::read_from_buffer(x, values, position); } else { - internal::read_from_buffer(x, values, position, 1, 0); + internal::read_real(x, values, position, 1); } position += size; } @@ -286,7 +343,8 @@ void check_consumed(const InVec& values, std::size_t position, * @throw std::runtime_error if the buffer does not match the destination. */ template -inline void read_whole_payload(T& x, const InVec& values, const std::string& path) { +inline void read_whole_payload(T& x, const InVec& values, + const std::string& path) { std::size_t position = 0; internal::read_from_buffer_block(x, values, position); internal::check_consumed(values, position, path); From 11dad61e3c364fc99f14a8bb31d88331a0b04d58 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Tue, 6 Oct 2026 00:17:33 -0700 Subject: [PATCH 09/18] dispatch read_from_context on whether the destination holds a tuple The entry point funneled every destination through the same traversal, so an int and an array of tuples both paid for the index_sequence path, the dotted-name walk and the field-path plumbing. Hand-tracing one leaf of very_deep crossed eleven instantiations. A name maps to more than one destination only when an array encloses a tuple. std::get(x) on a plain tuple is a real subobject, so that case never needs a path. Split the entry point into two overloads on contains_tuple. Without a tuple the variable is one name and one whole buffer, fetched once, checked against num_elements and copied in place. With one, SlotType walks the tuple skeleton that scalar_type_t leaves behind, producing one context name per leaf, and fill_slots walks the destination for that name off a shared cursor. x is never descended by the name walk, only forwarded. Replaces read_leaves, load_from_context, read_from_buffer, source_block, read_whole_payload and check_consumed, and drops index_apply from the leaf path. The slot path is never empty at a leaf, since the overload is only selected when scalar_type_t is a tuple, so fill_slots terminates on sizeof...(Rest) and no longer needs an index_sequence<> overload to pair against. read_real and read_complex become read_payload and read_payload_complex with offset-zero overloads for the top-level call, and the strided source map they share is named source_map_t. Adds a file comment working through std::vector>>> as a name tree and a slot walk, and records that an array enclosing a tuple is filled in element order while a tuple-free payload is column-major. That fact only existed in test comments. Doxygen on both overloads documents SlotType and the slot path as recursion parameters rather than arguments a caller supplies. --- src/stan/io/read_from_context.hpp | 707 +++++++++----------- src/test/unit/io/read_from_context_test.cpp | 6 +- 2 files changed, 309 insertions(+), 404 deletions(-) diff --git a/src/stan/io/read_from_context.hpp b/src/stan/io/read_from_context.hpp index 01106d98f50..07b8a205ab7 100644 --- a/src/stan/io/read_from_context.hpp +++ b/src/stan/io/read_from_context.hpp @@ -3,15 +3,19 @@ #include #include +#include +#include #include +#include #include -#include +#include #include +#include +#include #include #include #include #include -#include #include #include #include @@ -19,460 +23,361 @@ namespace stan { -namespace internal { +namespace io { -/** - * @brief Read one integer without advancing a consumption cursor. - * @param[out] x Destination integer. - * @param[in] values Buffer of integer values for a named leaf. - * @param[in] idx Zero-based index of the value to read. - * @throw std::runtime_error if idx is outside values. +/* A var_context stores one value array per NAME, never per array element. For + * + * array[2] tuple(real, real) x; + * + * it holds exactly two entries, "x.1" -> {a, b} and "x.2" -> {c, d}. There is + * no "x[0].1". Reading is therefore name-major: fetch a name once, then visit + * every element that has that slot. Element-major would refetch each buffer + * once per element, and vals_r returns its vector by value. + * + * Worked example: + * + * using Leaf = std::tuple; + * using Mid = std::tuple>; + * std::vector x; + * + * scalar_type_t erases every std::vector and Eigen layer and keeps the tuples, + * so SlotType starts as the name tree alone: + * + * SlotType = std::tuple, double>> + * + * read_from_context walks that tree at compile time and never touches x. The + * std::tuple_element_t below indexes a TYPE; the only runtime work in that + * branch is building name strings. + * + * SlotType path name + * tuple> <> "x" + * \__ tuple <0> "x.1" + * |__ complex <0,0> "x.1.1" leaf + * \__ double <0,1> "x.1.2" leaf + * + * Each leaf fetches its own buffer, then fill_slots walks the VALUE to every + * destination that name feeds, off one shared cursor. For "x.1.1", path <0,0>: + * + * x std::vector array -> loop, path stays <0,0> + * \__ x[i] std::tuple slot 0 -> get<0>, path <0> + * \__ v std::vector array -> loop, path stays <0> + * \__ v[j] std::tuple slot 0, path empty -> the payload + * + * One buffer fills the matrix of every x[i] -> v[j], laid back to back. + * + * The two array orderings are opposite, and both are deliberate: + * an array ENCLOSING a tuple is element order (fill_slots) + * a tuple-free payload is column-major (read_payload) + * array_array_real and nested_array_tuple are both 2x3 and disagree. */ -inline void read_from_buffer(int& x, const std::vector& values, - std::size_t idx) { - if (idx >= values.size()) { - throw std::runtime_error("read_from_context: not enough integer values"); - } - x = values[idx]; -} -/** - * @brief Read one real value without advancing a consumption cursor. - * @param[out] x Destination real value. - * @param[in] values Buffer of real values for a named leaf. - * @param[in] idx Zero-based index of the value to read. - * @throw std::runtime_error if idx is outside values. - */ -inline void read_from_buffer(double& x, const std::vector& values, - std::size_t idx) { - if (idx >= values.size()) { - throw std::runtime_error("read_from_context: not enough real values"); - } - x = values[idx]; -} +namespace internal { -/** - * @brief Read one complex value from separate component indices. - * @param[out] x Destination complex value. - * @param[in] values Buffer of real and imaginary components for a named leaf. - * @param[in] real_idx Zero-based index of the real component. - * @param[in] imag_idx Zero-based index of the imaginary component. - * @throw std::runtime_error if either index is outside values. - */ -inline void read_from_buffer(std::complex& x, - const std::vector& values, - std::size_t real_idx, std::size_t imag_idx) { - if (real_idx >= values.size() || imag_idx >= values.size()) { - throw std::runtime_error( - "read_from_context: not enough complex components"); +template +inline auto get_values(const ContextT& context, const Name& name) { + if constexpr (std::is_same_v) { + return context.vals_i(name); + } else { + return context.vals_r(name); } - x = {values[real_idx], values[imag_idx]}; -} - -/** - * @brief Map a strided column-major source block onto a destination. - * @tparam Source Dense source matrix type matching the buffer's scalar type. - * @tparam InVec Contiguous input buffer type, possibly const. - */ -template -inline auto source_block(const InVec& values, std::size_t offset, - Eigen::Index rows, Eigen::Index cols, - std::size_t stride) { - using Stride = Eigen::InnerStride; - return Eigen::Map( - values.data() + offset, rows, cols, Stride(stride)); } -/** - * @brief Read an int or real Eigen object in logical column-major order - * without advancing a consumption cursor. - * @tparam EigMat Writable Eigen destination type with int or double - * coefficients. - * @tparam InVec Contiguous input buffer type with int or double coefficients, - * possibly const. Integer destinations use integer buffers. - * @param[in,out] x Destination with its rows and columns already allocated. - * @param[in] values Buffer containing the tuple-free payload to read. - * @param[in] offset Zero-based source index of the first coefficient's value. - * @param[in] stride Positive distance in buffer elements between successive - * logical column-major coefficients. - * @pre The caller has validated that all source indices are within values. - */ -template * = nullptr> -void read_real(EigMat& x, const InVec& values, std::size_t offset, - std::size_t stride) { - if (x.size() == 0) { - return; - } - using Source - = Eigen::Matrix, Eigen::Dynamic, Eigen::Dynamic>; - x = internal::source_block(values, offset, x.rows(), x.cols(), - stride); -} +template +using source_map_t = Eigen::Map< + const Eigen::Matrix, Eigen::Dynamic, Eigen::Dynamic>, + Eigen::Unaligned, Eigen::InnerStride>; -/** - * @brief Read a complex Eigen object in logical column-major order without - * advancing a consumption cursor. The real components of the payload precede - * the imaginary ones, so each component block is read separately. - * @tparam EigMat Writable Eigen destination type with std::complex - * coefficients. - * @tparam InVec Contiguous input buffer type with double coefficients, - * possibly const. - * @param[in,out] x Destination with its rows and columns already allocated. - * @param[in] values Buffer containing the tuple-free payload to read. - * @param[in] offset Zero-based source index of the first real component. - * @param[in] stride Positive distance in buffer elements between successive - * logical column-major coefficients. - * @param[in] imaginary_offset Distance in buffer elements from each real - * component to its imaginary component. - * @pre The caller has validated that all source indices are within values. - */ -template * = nullptr> -void read_complex(EigMat& x, const InVec& values, std::size_t offset, - std::size_t stride, std::size_t imaginary_offset) { - if (x.size() == 0) { - return; +template +inline void read_payload(T& x, const InVec& values, std::size_t offset, + std::size_t stride) { + using map_t = source_map_t; + if constexpr (std::is_arithmetic_v) { + x = values[offset]; + } else if constexpr (is_eigen_v) { + x = map_t(values.data() + offset, x.rows(), x.cols(), stride); + } else if constexpr (std::is_same_v, scalar_type_t>) { + const std::size_t size = x.size(); + Eigen::Map, Eigen::Dynamic, 1>> dst(x.data(), + size); + dst = map_t(values.data() + offset, size, 1, stride); + } else { + const std::size_t size = x.size(); + const std::size_t child_stride = stride * size; + for (std::size_t i = 0; i < size; ++i) { + read_payload(x[i], values, offset + i * stride, child_stride); + } } - using Source - = Eigen::Matrix, Eigen::Dynamic, Eigen::Dynamic>; - x.real() = internal::source_block(values, offset, x.rows(), x.cols(), - stride); - x.imag() = internal::source_block(values, offset + imaginary_offset, - x.rows(), x.cols(), stride); } -/** - * @brief Read an int or real tuple-free array using column-major source - * strides. Each child uses stride * x.size(). This function does not advance - * a consumption cursor. - * @tparam StdVec Destination std::vector type containing int, double, Eigen - * objects, or nested tuple-free std::vectors of these. - * @tparam InVec Input std::vector type holding int values for integer - * destinations or double values for real ones. - * @param[in,out] x Rectangular destination with every dimension allocated. - * @param[in] values Buffer containing the tuple-free payload to read. - * @param[in] offset Zero-based source index of the first value in this array. - * @param[in] stride Positive distance in buffer elements between the starting - * indices of consecutive elements of x. - * @pre The caller has validated that all source indices are within values. - */ -template * = nullptr> -void read_real(StdVec& x, const InVec& values, std::size_t offset, - std::size_t stride) { - using T = value_type_t; - if constexpr (std::is_same_v || std::is_same_v) { - using Vec = Eigen::Matrix; - using Stride = Eigen::InnerStride; - Eigen::Map(x.data(), x.size()) - = Eigen::Map( - values.data() + offset, x.size(), Stride(stride)); +template +inline void read_payload(T& x, const InVec& values, std::size_t offset) { + using map_t = source_map_t; + if constexpr (std::is_arithmetic_v) { + x = values[offset]; + } else if constexpr (is_eigen_v) { + x = map_t(values.data() + offset, x.rows(), x.cols(), 1); + } else if constexpr (std::is_same_v, scalar_type_t>) { + const std::size_t size = x.size(); + Eigen::Map, Eigen::Dynamic, 1>> dst(x.data(), + size); + dst = map_t(values.data() + offset, size, 1, 1); } else { - for (std::size_t i = 0; i < x.size(); ++i) { - internal::read_real(x[i], values, offset + i * stride, stride * x.size()); + const std::size_t size = x.size(); + const std::size_t child_stride = size; + for (std::size_t i = 0; i < size; ++i) { + read_payload(x[i], values, offset + i, child_stride); } } } -/** - * @brief Read a complex tuple-free array using column-major source strides. - * Each child uses stride * x.size(); imaginary_offset stays constant within - * the payload. This function does not advance a consumption cursor. - * @tparam StdVec Destination std::vector type containing - * std::complex, Eigen objects, or nested tuple-free std::vectors of - * these. - * @tparam InVec Input std::vector type holding double values. - * @param[in,out] x Rectangular destination with every dimension allocated. - * @param[in] values Buffer containing the tuple-free payload to read. - * @param[in] offset Zero-based source index of the first real component in - * this array. - * @param[in] stride Positive distance in buffer elements between the starting - * indices of consecutive elements of x. - * @param[in] imaginary_offset Distance in buffer elements from each real - * component to its imaginary component. - * @pre The caller has validated that all source indices are within values. - */ -template * = nullptr> -void read_complex(StdVec& x, const InVec& values, std::size_t offset, - std::size_t stride, std::size_t imaginary_offset) { - using T = value_type_t; - if constexpr (stan::is_complex::value) { - for (std::size_t i = 0; i < x.size(); ++i) { - const std::size_t idx = offset + i * stride; - internal::read_from_buffer(x[i], values, idx, idx + imaginary_offset); - } +template +inline void read_payload(T& x, const InVec& values) { + using map_t = source_map_t; + if constexpr (std::is_arithmetic_v) { + x = values[0]; + } else if constexpr (is_eigen_v) { + x = map_t(values.data(), x.rows(), x.cols(), 1); + } else if constexpr (std::is_same_v, scalar_type_t>) { + const std::size_t size = x.size(); + Eigen::Map, Eigen::Dynamic, 1>> dst(x.data(), + size); + dst = map_t(values.data(), size, 1, 1); } else { - for (std::size_t i = 0; i < x.size(); ++i) { - internal::read_complex(x[i], values, offset + i * stride, - stride * x.size(), imaginary_offset); + const std::size_t size = x.size(); + const std::size_t child_stride = size; + for (std::size_t i = 0; i < size; ++i) { + read_payload(x[i], values, i, child_stride); } } } -/** - * @brief Read one tuple-free payload and advance its consumption cursor. - * Validate the full payload before reading. Complex payloads use separate - * real and imaginary component blocks and consume two stored values per - * destination value. Non-complex payloads consume one stored value per - * destination value. - * @tparam T Tuple-free destination type: int, double, std::complex, - * an Eigen object, or a rectangular std::vector nesting of these types. - * @tparam InputVec Input std::vector type, or a reference to it, holding int - * values for integer destinations or double values for real and complex ones. - * @param[in,out] x Destination with every container dimension allocated. - * @param[in] values Buffer for the named leaf containing this payload; - * neither modified nor moved from. - * @param[in,out] position Zero-based start of the payload in values. Advances - * by the number of stored int or double values on success, counting two per - * complex value. An empty payload leaves position unchanged. - * @throw std::runtime_error if position exceeds the buffer size or there are - * insufficient values for the payload. - */ -template -void read_from_buffer_block(T& x, InputVec&& values, std::size_t& position) { - const std::size_t size = math::num_elements(x); - if (position > values.size()) { - throw std::runtime_error( - "read_from_context: not enough values for payload"); - } - if (size == 0) { - return; - } - if constexpr (stan::is_complex>::value) { - if (size > (values.size() - position) / 2) { - throw std::runtime_error( - "read_from_context: not enough values for complex payload"); - } - if constexpr (stan::is_complex::value) { - internal::read_from_buffer(x, values, position, position + 1); - } else { - internal::read_complex(x, values, position, 1, size); - } - position += 2 * size; +template +inline void read_payload_complex(T& x, const InVec& values, std::size_t offset, + std::size_t stride, + std::size_t imaginary_offset) { + using map_t = source_map_t; + if constexpr (is_complex::value) { + x = {values[offset], values[offset + imaginary_offset]}; + } else if constexpr (is_eigen_v) { + x.real() = map_t(values.data() + offset, x.rows(), x.cols(), stride); + x.imag() = map_t(values.data() + offset + imaginary_offset, x.rows(), + x.cols(), stride); + } else if constexpr (std::is_same_v, scalar_type_t>) { + const std::size_t size = x.size(); + Eigen::Map, Eigen::Dynamic, 1>> dst(x.data(), + size); + dst.real() = map_t(values.data() + offset, size, 1, stride); + dst.imag() + = map_t(values.data() + offset + imaginary_offset, size, 1, stride); } else { - if (size > values.size() - position) { - throw std::runtime_error( - "read_from_context: not enough values for payload"); - } - if constexpr (std::is_integral_v || std::is_floating_point_v) { - internal::read_from_buffer(x, values, position); - } else { - internal::read_real(x, values, position, 1); + const std::size_t size = x.size(); + const std::size_t child_stride = stride * size; + for (std::size_t i = 0; i < size; ++i) { + read_payload_complex(x[i], values, offset + i * stride, child_stride, + imaginary_offset); } - position += size; } } -/** - * @brief Read a complete tuple-free payload once the field path is exhausted. - * @tparam T Tuple-free scalar, Eigen, or rectangular std::vector destination - * type with int, double, or std::complex scalar values. - * @tparam InVec Input std::vector type holding int values for integer - * destinations or double values for real and complex destinations. - * @param[in,out] x Destination payload with its dimensions already allocated. - * @param[in] values Buffer for the named leaf containing this payload. - * @param[in,out] position Zero-based payload start in values; advances past - * this payload on success, counting both components of complex values. - * @param[in] fields Empty tuple-field path selecting x itself as the payload. - * @throw std::runtime_error if the buffer cannot supply the complete payload. - */ template -void read_from_buffer(T& x, const InVec& values, std::size_t& position, - std::index_sequence<> fields) { - internal::read_from_buffer_block(x, values, position); -} - -/** - * @brief Follow a tuple-field path through the destination for one named leaf. - * Tuples consume one path index. Arrays visit elements in order without - * consuming an index, sharing the input buffer and consumption cursor. - * @tparam T Destination tuple or std::vector nesting containing tuples. - * @tparam InVec Input std::vector type holding int values for integer - * destinations or double values for real and complex destinations. - * @tparam First Zero-based field index in the next tuple encountered. - * @tparam Rest Remaining zero-based field indices in successive nested tuples. - * @param[in,out] x Destination subtree with every dimension already allocated. - * @param[in] values Buffer for the named leaf selected by fields. - * @param[in,out] position Zero-based index of the next unread buffer element; - * advances through each selected payload in traversal order. - * @param[in] fields Compile-time tuple-field path, excluding array indices. - * @throw std::runtime_error if the buffer cannot supply a selected payload. - */ -template -void read_from_buffer(T& x, const InVec& values, std::size_t& position, - std::index_sequence fields) { - if constexpr (is_tuple_v) { - internal::read_from_buffer(std::get(x), values, position, - std::index_sequence{}); +inline void read_payload_complex(T& x, const InVec& values, + std::size_t imaginary_offset) { + using map_t = source_map_t; + if constexpr (is_complex::value) { + x = {values[0], values[imaginary_offset]}; + } else if constexpr (is_eigen_v) { + x.real() = map_t(values.data(), x.rows(), x.cols(), 1); + x.imag() = map_t(values.data() + imaginary_offset, x.rows(), x.cols(), 1); + } else if constexpr (std::is_same_v, scalar_type_t>) { + const std::size_t size = x.size(); + Eigen::Map, Eigen::Dynamic, 1>> dst(x.data(), + size); + dst.real() = map_t(values.data() + 0, size, 1, 1); + dst.imag() = map_t(values.data() + 0 + imaginary_offset, size, 1, 1); } else { - for (auto& element : x) { - internal::read_from_buffer(element, values, position, fields); + const std::size_t size = x.size(); + const std::size_t child_stride = size; + for (std::size_t i = 0; i < size; ++i) { + read_payload_complex(x[i], values, i, child_stride, imaginary_offset); } } } -/** - * @brief Check that a named leaf buffer has been consumed exactly. - * @tparam InVec Input buffer type providing size(). - * @param[in] values Loaded buffer for one named leaf. - * @param[in] position Number of consumed buffer elements, counting both - * components of complex values. - * @param[in] path Variable name, including one-based dotted tuple-field - * suffixes, used in the error message. - * @throw std::runtime_error if position differs from values.size(). - */ -template -void check_consumed(const InVec& values, std::size_t position, - const std::string& path) { - if (position != values.size()) { - throw std::runtime_error("read_from_context: unexpected value count for " - + path); +template +inline void fill_slots(T& x, const InVec& values, std::size_t& cursor, + std::index_sequence path) { + if constexpr (!is_tuple_v) { + for (auto& element : x) { + fill_slots(element, values, cursor, path); + } + } else if constexpr (sizeof...(Rest) > 0) { + fill_slots(std::get(x), values, cursor, + std::index_sequence{}); + } else { + using scalar_t = scalar_type_t>; + auto& payload = std::get(x); + const std::size_t size = math::num_elements(payload); + const std::size_t needed = is_complex::value ? 2 * size : size; + if (needed > values.size() - cursor) { + throw std::runtime_error( + "read_from_context: ran out of values filling " + "slot " + + std::to_string(Slot + 1)); + } else if (size > 0) { + if constexpr (is_complex::value) { + read_payload_complex(payload, values, cursor, 1, size); + } else { + read_payload(payload, values, cursor); + } + } + cursor += needed; } } -/** - * @brief Read a destination that holds no tuples, so its buffer is one - * payload consumed in full. Equivalent to walking an empty tuple-field path, - * without instantiating the path machinery. - * @tparam T Tuple-free destination type with its dimensions already set. - * @tparam InVec Input buffer type holding int values for integer - * destinations or double values for real and complex destinations. - * @param[in,out] x Destination with every container dimension allocated. - * @param[in] values Buffer loaded for this variable. - * @param[in] path Variable name, used in the error message. - * @throw std::runtime_error if the buffer does not match the destination. - */ -template -inline void read_whole_payload(T& x, const InVec& values, - const std::string& path) { - std::size_t position = 0; - internal::read_from_buffer_block(x, values, position); - internal::check_consumed(values, position, path); -} +} // namespace internal /** - * @brief Load and write one named leaf at a time, releasing each buffer before - * loading the next. Discover tuple paths from types, including for empty - * arrays. - * @tparam Field Type at the current tuple-field path, possibly wrapped in - * arrays. Its scalar_type_t determines the next tuple or the leaf scalar type. - * @tparam Root Complete destination type with int, double, or - * std::complex scalar values, Eigen objects, std::vectors, and tuples. - * @tparam Context Source type providing const vals_i(name) and vals_r(name) - * methods returning std::vector and std::vector, respectively. - * @tparam Fields Zero-based tuple-field indices from the root, excluding array - * indices. - * @param[in,out] x Complete rectangular destination with all dimensions - * allocated. Earlier writes remain if a subsequent leaf fails. + * Read a tuple-free variable out of a var_context into an already-sized + * destination. + * + * The whole destination is one name and one whole buffer, so the buffer is + * fetched once and must supply exactly the number of values the destination + * holds. A complex destination takes two values per coefficient, every real + * component before every imaginary one. + * + * @tparam T Destination type with no std::tuple anywhere inside: int, double, + * std::complex, an Eigen vector, row vector or matrix, or a + * rectangular std::vector nesting of these. Its scalar type must be int, + * double or std::complex. + * @tparam Context Source providing vals_i(name) and vals_r(name) as const + * members returning std::vector and std::vector by value. + * Integer destinations read vals_i, real and complex ones read vals_r. + * @param[in,out] x Destination with every dimension already allocated. + * Declared dimensions must be validated before this call, because an empty + * container cannot describe the sizes of the elements it does not have. * @param[in] context Source of the named integer and real buffers. - * @param[in] path Variable name including the one-based dotted tuple-field - * suffixes corresponding to Fields. - * @param[in] fields Compile-time tuple-field path into x corresponding to - * Field and path; empty at the root. - * @throw std::runtime_error if a leaf has too few or too many values for its - * destinations. Exceptions from the context propagate to the caller. + * @param[in] name Variable name, with no array indices or tuple suffixes. + * @throw std::runtime_error if the context supplies a number of values other + * than the number the destination holds. Exceptions from the context + * propagate unchanged. */ -template -void load_from_context(Root& x, const Context& context, const std::string& path, - std::index_sequence fields) { - using S = scalar_type_t; - if constexpr (is_tuple_v) { - math::index_apply>([&x, &context, &path](auto... i) { - (internal::load_from_context>( - x, context, path + "." + std::to_string(decltype(i)::value + 1), - std::index_sequence{}), - ...); - }); - } else { - std::size_t position = 0; - if constexpr (std::is_same_v) { - const auto values = context.vals_i(path); - internal::read_from_buffer(x, values, position, fields); - internal::check_consumed(values, position, path); +template >* = nullptr> +inline void read_from_context(T& x, const Context& context, + const std::string& name) { + using scalar_t = scalar_type_t; + static_assert(std::is_same_v + || std::is_same_v + || std::is_same_v>, + "read_from_context requires int, double or complex " + "scalars"); + const auto values = internal::get_values(context, name); + const std::size_t size = math::num_elements(x); + const std::size_t expected = is_complex::value ? 2 * size : size; + if (values.size() != expected) { + throw std::runtime_error("read_from_context: " + name + " expected " + + std::to_string(expected) + " values, got " + + std::to_string(values.size())); + } else if (size == 0) { + return; + } + if constexpr (std::is_arithmetic_v) { + x = values[0]; + } else if constexpr (is_complex::value) { + x = {values[0], values[1]}; + } else if constexpr (is_eigen_v) { + using map_t + = Eigen::Map, + Eigen::Dynamic, Eigen::Dynamic>>; + if constexpr (is_complex::value) { + x.real() = map_t(values.data(), x.rows(), x.cols()); + x.imag() = map_t(values.data() + size, x.rows(), x.cols()); + } else { + x = map_t(values.data(), x.rows(), x.cols()); + } + } else if constexpr (std::is_same_v, scalar_t>) { + if constexpr (is_complex::value) { + for (std::size_t i = 0; i < size; ++i) { + x[i] = {values[i], values[i + size]}; + } } else { - static_assert( - std::is_same_v || std::is_same_v>, - "read_from_context requires int, double or complex " - "scalar types"); - const auto values = context.vals_r(path); - internal::read_from_buffer(x, values, position, fields); - internal::check_consumed(values, position, path); + std::copy(values.begin(), values.end(), x.begin()); } + } else if constexpr (is_complex::value) { + internal::read_payload_complex(x, values, size); + } else { + internal::read_payload(x, values); } } -} // namespace internal - /** - * Read a named variable into an already-sized destination. + * Read a variable containing std::tuple out of a var_context into an + * already-sized destination. * - * Supported destinations are int, double, std::complex, Eigen - * vectors, row vectors and matrices, and nested std::vectors and std::tuples - * of these types. Arrays must be rectangular. Declared dimensions must be - * validated before this call; empty containers cannot describe the sizes of - * their missing elements. + * Every tuple slot is a separate name, so this reads one name per leaf of the + * tuple skeleton, fetching and releasing each buffer before the next. If a + * later name fails, the names already read remain written. See the file + * comment above for a worked example of the two traversals. * - * Each dotted tuple-field name is fetched once, written, and its buffer - * released before the next name is fetched. Arrays containing tuples are - * traversed in element order, while each tuple-free payload is decoded in - * column-major order. Complex components are paired within that payload. - * If a later leaf fails, previously written leaves remain modified. + * Call it with three arguments; SlotType and path carry the recursion and + * default to the root of the tuple skeleton and the empty path. * - * @tparam T Destination type: int, double, std::complex, an Eigen - * object, or a nesting of std::vectors and std::tuples of supported types. - * @tparam Context Source type providing const vals_i(name) and vals_r(name) - * methods returning std::vector and std::vector, respectively. - * @param[in,out] x Rectangular destination with its declared dimensions already - * allocated. Scalar values are overwritten; container dimensions are preserved. + * @tparam T Destination type with a std::tuple somewhere inside: a + * std::tuple, or a rectangular std::vector nesting around one. Every leaf + * scalar must be int, double or std::complex. + * @tparam SlotType Position in the tuple skeleton, scalar_type_t at the + * root. A std::tuple here means the name has more slots below it; anything + * else means the name is a leaf and is read. Never a type of x itself, since + * scalar_type_t has erased the std::vector and Eigen layers. + * @tparam Context Source providing vals_i(name) and vals_r(name) as const + * members returning std::vector and std::vector by value. + * @tparam Slots Tuple slot indices from the root to SlotType, excluding array + * indices. The path that fill_slots follows through the destination. + * @param[in,out] x Destination with every dimension already allocated. It is + * always the root: the recursion descends SlotType and path, never x. * @param[in] context Source of the named integer and real buffers. - * @param[in] name Root variable name, without array indices or tuple suffixes; - * converted to an owned string for this call. - * @throw std::runtime_error if the input contains an unexpected number of - * values. + * @param[in] name Variable name, with the dotted one-based suffix for Slots + * already appended. + * @param[in] path Slots as a value, so the pack is deduced rather than given. + * @throw std::runtime_error if a name supplies a number of values other than + * the number its destinations hold. Exceptions from the context propagate + * unchanged. */ -template +template , typename Context, + std::size_t... Slots, + stan::require_t>* = nullptr> inline void read_from_context(T& x, const Context& context, - std::string_view name) { - const std::string path(name); - if constexpr (stan::is_complex::value) { - static_assert(std::is_same_v, - "read_from_context requires complex scalar types"); - const auto values = context.vals_r(path); - if (values.size() != 2) { - throw std::runtime_error("read_from_context: expected two components for " - + path); - } - x = {values[0], values[1]}; - } else if constexpr (std::is_same_v) { - const auto values = context.vals_i(path); - if (values.size() != 1) { - throw std::runtime_error("read_from_context: expected one value for " - + path); - } - x = values[0]; - } else if constexpr (std::is_same_v) { - const auto values = context.vals_r(path); - if (values.size() != 1) { - throw std::runtime_error("read_from_context: expected one value for " - + path); - } - x = values[0]; - } else if constexpr (std::is_same_v>) { - internal::read_whole_payload(x, context.vals_i(path), path); - } else if constexpr (std::is_same_v>) { - internal::read_whole_payload(x, context.vals_r(path), path); - } else if constexpr (is_eigen::value - && std::is_same_v, int>) { - internal::read_whole_payload(x, context.vals_i(path), path); - } else if constexpr (is_eigen::value - && std::is_same_v, double>) { - internal::read_whole_payload(x, context.vals_r(path), path); + std::string_view name, + std::index_sequence path = {}) { + if constexpr (is_tuple_v) { + math::index_apply>([&x, &context, + &name](auto... Slot) { + (read_from_context>( + x, context, std::string(name) + "." + std::to_string(Slot.value + 1), + std::index_sequence{}), + ...); + }); } else { - internal::load_from_context(x, context, path, std::index_sequence<>{}); + static_assert(std::is_same_v + || std::is_same_v + || std::is_same_v>, + "read_from_context requires int, double or complex " + "scalars"); + const std::string leaf_name(name); + const auto values = internal::get_values(context, leaf_name); + std::size_t cursor = 0; + internal::fill_slots(x, values, cursor, path); + if (cursor != values.size()) { + throw std::runtime_error("read_from_context: " + leaf_name + " supplied " + + std::to_string(values.size()) + + " values but the destination holds " + + std::to_string(cursor)); + } } } +} // namespace io + } // namespace stan #endif diff --git a/src/test/unit/io/read_from_context_test.cpp b/src/test/unit/io/read_from_context_test.cpp index 029c10dac8b..0ae9ec056f0 100644 --- a/src/test/unit/io/read_from_context_test.cpp +++ b/src/test/unit/io/read_from_context_test.cpp @@ -27,7 +27,7 @@ void read_fixture(T& x, const std::string& fixture, const std::string& name = "x") { auto in = fixture_stream(fixture); const stan::json::json_data context(in); - stan::read_from_context(x, context, name); + stan::io::read_from_context(x, context, name); } void expect_complex(const std::complex& actual, double real) { @@ -411,8 +411,8 @@ TEST(ioReadFromContext, repeated_read_has_independent_positions) { std::tuple, int> first{std::vector(3, -999), -999}; auto second = first; - ASSERT_NO_THROW(stan::read_from_context(first, context, "basic")); - ASSERT_NO_THROW(stan::read_from_context(second, context, "basic")); + ASSERT_NO_THROW(stan::io::read_from_context(first, context, "basic")); + ASSERT_NO_THROW(stan::io::read_from_context(second, context, "basic")); EXPECT_EQ((std::vector{1.25, -2.5, 3.75}), std::get<0>(first)); EXPECT_EQ(-17, std::get<1>(first)); EXPECT_EQ(first, second); From 6d9b01b5f6566051071155100d9e568006257f1f Mon Sep 17 00:00:00 2001 From: Stan Jenkins Date: Tue, 6 Oct 2026 03:18:36 -0400 Subject: [PATCH 10/18] [Jenkins] auto-formatting by clang-format version 10.0.0-4ubuntu1 --- src/stan/io/read_from_context.hpp | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/stan/io/read_from_context.hpp b/src/stan/io/read_from_context.hpp index 07b8a205ab7..237b6c96381 100644 --- a/src/stan/io/read_from_context.hpp +++ b/src/stan/io/read_from_context.hpp @@ -266,11 +266,12 @@ template ; - static_assert(std::is_same_v - || std::is_same_v - || std::is_same_v>, - "read_from_context requires int, double or complex " - "scalars"); + static_assert( + std::is_same_v< + scalar_t, + int> || std::is_same_v || std::is_same_v>, + "read_from_context requires int, double or complex " + "scalars"); const auto values = internal::get_values(context, name); const std::size_t size = math::num_elements(x); const std::size_t expected = is_complex::value ? 2 * size : size; @@ -358,11 +359,12 @@ inline void read_from_context(T& x, const Context& context, ...); }); } else { - static_assert(std::is_same_v - || std::is_same_v - || std::is_same_v>, - "read_from_context requires int, double or complex " - "scalars"); + static_assert( + std::is_same_v< + SlotType, + int> || std::is_same_v || std::is_same_v>, + "read_from_context requires int, double or complex " + "scalars"); const std::string leaf_name(name); const auto values = internal::get_values(context, leaf_name); std::size_t cursor = 0; From c491f16abf4e2cf246ecc856235d5675d0c7d25b Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Tue, 6 Oct 2026 00:27:40 -0700 Subject: [PATCH 11/18] update --- src/stan/io/read_from_context.hpp | 33 +++++++++++++------------------ 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/stan/io/read_from_context.hpp b/src/stan/io/read_from_context.hpp index 237b6c96381..3bd1788cb9e 100644 --- a/src/stan/io/read_from_context.hpp +++ b/src/stan/io/read_from_context.hpp @@ -41,15 +41,15 @@ namespace io { * std::vector x; * * scalar_type_t erases every std::vector and Eigen layer and keeps the tuples, - * so SlotType starts as the name tree alone: + * so SlotT starts as the name tree alone: * - * SlotType = std::tuple, double>> + * SlotT = std::tuple, double>> * * read_from_context walks that tree at compile time and never touches x. The * std::tuple_element_t below indexes a TYPE; the only runtime work in that * branch is building name strings. * - * SlotType path name + * SlotT path name * tuple> <> "x" * \__ tuple <0> "x.1" * |__ complex <0,0> "x.1.1" leaf @@ -267,9 +267,7 @@ inline void read_from_context(T& x, const Context& context, const std::string& name) { using scalar_t = scalar_type_t; static_assert( - std::is_same_v< - scalar_t, - int> || std::is_same_v || std::is_same_v>, + std::is_arithmetic_v || stan::is_complex_v, "read_from_context requires int, double or complex " "scalars"); const auto values = internal::get_values(context, name); @@ -320,22 +318,22 @@ inline void read_from_context(T& x, const Context& context, * later name fails, the names already read remain written. See the file * comment above for a worked example of the two traversals. * - * Call it with three arguments; SlotType and path carry the recursion and + * Call it with three arguments; SlotT and path carry the recursion and * default to the root of the tuple skeleton and the empty path. * * @tparam T Destination type with a std::tuple somewhere inside: a * std::tuple, or a rectangular std::vector nesting around one. Every leaf * scalar must be int, double or std::complex. - * @tparam SlotType Position in the tuple skeleton, scalar_type_t at the + * @tparam SlotT Position in the tuple skeleton, scalar_type_t at the * root. A std::tuple here means the name has more slots below it; anything * else means the name is a leaf and is read. Never a type of x itself, since * scalar_type_t has erased the std::vector and Eigen layers. * @tparam Context Source providing vals_i(name) and vals_r(name) as const * members returning std::vector and std::vector by value. - * @tparam Slots Tuple slot indices from the root to SlotType, excluding array + * @tparam Slots Tuple slot indices from the root to SlotT, excluding array * indices. The path that fill_slots follows through the destination. * @param[in,out] x Destination with every dimension already allocated. It is - * always the root: the recursion descends SlotType and path, never x. + * always the root: the recursion descends SlotT and path, never x. * @param[in] context Source of the named integer and real buffers. * @param[in] name Variable name, with the dotted one-based suffix for Slots * already appended. @@ -344,29 +342,26 @@ inline void read_from_context(T& x, const Context& context, * the number its destinations hold. Exceptions from the context propagate * unchanged. */ -template , typename Context, +template , typename Context, std::size_t... Slots, stan::require_t>* = nullptr> inline void read_from_context(T& x, const Context& context, std::string_view name, std::index_sequence path = {}) { - if constexpr (is_tuple_v) { - math::index_apply>([&x, &context, + if constexpr (is_tuple_v) { + math::index_apply>([&x, &context, &name](auto... Slot) { - (read_from_context>( + (read_from_context>( x, context, std::string(name) + "." + std::to_string(Slot.value + 1), std::index_sequence{}), ...); }); } else { - static_assert( - std::is_same_v< - SlotType, - int> || std::is_same_v || std::is_same_v>, + static_assert(std::is_arithmetic_v || stan::is_complex_v, "read_from_context requires int, double or complex " "scalars"); const std::string leaf_name(name); - const auto values = internal::get_values(context, leaf_name); + const auto values = internal::get_values(context, leaf_name); std::size_t cursor = 0; internal::fill_slots(x, values, cursor, path); if (cursor != values.size()) { From c368e15fdcc9796ba4795d5e71090cf4a00d1a67 Mon Sep 17 00:00:00 2001 From: Stan Jenkins Date: Tue, 6 Oct 2026 03:28:43 -0400 Subject: [PATCH 12/18] [Jenkins] auto-formatting by clang-format version 10.0.0-4ubuntu1 --- src/stan/io/read_from_context.hpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/stan/io/read_from_context.hpp b/src/stan/io/read_from_context.hpp index 3bd1788cb9e..2b45c463a0a 100644 --- a/src/stan/io/read_from_context.hpp +++ b/src/stan/io/read_from_context.hpp @@ -266,10 +266,9 @@ template ; - static_assert( - std::is_arithmetic_v || stan::is_complex_v, - "read_from_context requires int, double or complex " - "scalars"); + static_assert(std::is_arithmetic_v || stan::is_complex_v, + "read_from_context requires int, double or complex " + "scalars"); const auto values = internal::get_values(context, name); const std::size_t size = math::num_elements(x); const std::size_t expected = is_complex::value ? 2 * size : size; @@ -350,7 +349,7 @@ inline void read_from_context(T& x, const Context& context, std::index_sequence path = {}) { if constexpr (is_tuple_v) { math::index_apply>([&x, &context, - &name](auto... Slot) { + &name](auto... Slot) { (read_from_context>( x, context, std::string(name) + "." + std::to_string(Slot.value + 1), std::index_sequence{}), @@ -358,8 +357,8 @@ inline void read_from_context(T& x, const Context& context, }); } else { static_assert(std::is_arithmetic_v || stan::is_complex_v, - "read_from_context requires int, double or complex " - "scalars"); + "read_from_context requires int, double or complex " + "scalars"); const std::string leaf_name(name); const auto values = internal::get_values(context, leaf_name); std::size_t cursor = 0; From af813d36783712bf898e9d398f3028a48d26d950 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Tue, 6 Oct 2026 00:39:23 -0700 Subject: [PATCH 13/18] update cpplint --- src/stan/io/json/json_data_handler.hpp | 26 +++++++++++++------------- src/stan/io/read_from_context.hpp | 12 +++++++----- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/stan/io/json/json_data_handler.hpp b/src/stan/io/json/json_data_handler.hpp index c41d99ae946..a621180b2f4 100644 --- a/src/stan/io/json/json_data_handler.hpp +++ b/src/stan/io/json/json_data_handler.hpp @@ -477,7 +477,7 @@ class json_data_handler : public stan::json::json_handler { * This means that we don't accumulate variable definitions * across calls to the parser. */ - void start_text() { + void start_text() override { vars_i.clear(); vars_r.clear(); var_types_map.clear(); @@ -492,7 +492,7 @@ class json_data_handler : public stan::json::json_handler { /** Once all variable definitions have been processed, * update dimensions for array of tuple variables. */ - void end_text() { update_array_dims(); } + void end_text() override { update_array_dims(); } /** A key is either a top-level Stan variable name or a tuple slot id. * Logic handles edge case where key is the first slot of a tuple; @@ -535,7 +535,7 @@ class json_data_handler : public stan::json::json_handler { * A start object ("{") event changes the meta-type of the current key. * Initialize or update tuple slots. */ - void start_object() { + void start_object() override { event = meta_event::OBJ_OPEN; if (is_init() || not_stan_var) return; @@ -560,7 +560,7 @@ class json_data_handler : public stan::json::json_handler { * If this is an array of tuples, track or check the number tuple slots * and the array size. */ - void end_object() { + void end_object() override { event = meta_event::OBJ_CLOSE; if (not_stan_var) { if (!key_stack.empty()) @@ -598,7 +598,7 @@ class json_data_handler : public stan::json::json_handler { * doesn't distinguish lists of heterogeneous elements and arrays. * Then we add or update the dimensions of the array variable. */ - void start_array() { + void start_array() override { if (key_stack.empty()) { throw json_error("Expecting JSON object, found array."); } @@ -636,7 +636,7 @@ class json_data_handler : public stan::json::json_handler { * If processing the first row of an array, record the size of this row, * else check that the size of this row matches recorded row size. */ - void end_array() { + void end_array() override { if (not_stan_var) return; if (slot_dims_map.count(key_str()) == 0) @@ -674,7 +674,7 @@ class json_data_handler : public stan::json::json_handler { slot_dims_map[key] = dims; } - void null() { + void null() override { if (not_stan_var) return; std::stringstream errorMsg; @@ -683,7 +683,7 @@ class json_data_handler : public stan::json::json_handler { throw json_error(errorMsg.str()); } - void boolean(bool p) { + void boolean(bool p) override { if (not_stan_var) return; std::stringstream errorMsg; @@ -716,14 +716,14 @@ class json_data_handler : public stan::json::json_handler { values_r.push_back(tmp); } - void number_double(double x) { + void number_double(double x) override { if (not_stan_var) return; promote_to_double(); values_r.push_back(x); } - void number_int(int n) { + void number_int(int n) override { if (not_stan_var) return; if (int_slots_map[key_str()]) { @@ -733,7 +733,7 @@ class json_data_handler : public stan::json::json_handler { } } - void number_unsigned_int(unsigned n) { + void number_unsigned_int(unsigned n) override { if (not_stan_var) return; // if integer overflow, promote numeric data to double @@ -746,14 +746,14 @@ class json_data_handler : public stan::json::json_handler { } } - void number_int64(int64_t n) { + void number_int64(int64_t n) override { if (not_stan_var) return; // the number doesn't fit in int (otherwise number_int() would be called) number_double(n); } - void number_unsigned_int64(uint64_t n) { + void number_unsigned_int64(uint64_t n) override { if (not_stan_var) return; // the number doesn't fit in int (otherwise number_unsigned_int() would be diff --git a/src/stan/io/read_from_context.hpp b/src/stan/io/read_from_context.hpp index 3bd1788cb9e..59efeb4faf7 100644 --- a/src/stan/io/read_from_context.hpp +++ b/src/stan/io/read_from_context.hpp @@ -267,7 +267,7 @@ inline void read_from_context(T& x, const Context& context, const std::string& name) { using scalar_t = scalar_type_t; static_assert( - std::is_arithmetic_v || stan::is_complex_v, + std::is_arithmetic_v || stan::is_complex::value, "read_from_context requires int, double or complex " "scalars"); const auto values = internal::get_values(context, name); @@ -350,16 +350,18 @@ inline void read_from_context(T& x, const Context& context, std::index_sequence path = {}) { if constexpr (is_tuple_v) { math::index_apply>([&x, &context, - &name](auto... Slot) { + &name](auto... Slot) { (read_from_context>( x, context, std::string(name) + "." + std::to_string(Slot.value + 1), std::index_sequence{}), ...); }); } else { - static_assert(std::is_arithmetic_v || stan::is_complex_v, - "read_from_context requires int, double or complex " - "scalars"); + constexpr bool is_valid_slot_type + = std::is_arithmetic_v || stan::is_complex::value; + static_assert(is_valid_slot_type, + "read_from_context requires int, double or complex " + "scalars"); const std::string leaf_name(name); const auto values = internal::get_values(context, leaf_name); std::size_t cursor = 0; From 9b053ed8a509e4983b34ef4bc47235052f4c9d5b Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Tue, 6 Oct 2026 00:50:27 -0700 Subject: [PATCH 14/18] retry the unit stage checkout after wiping the workspace disableConcurrentBuilds(abortPrevious) aborts the running build on every push to a PR branch. When the abort lands while the git plugin is inside "git submodule foreach --recursive git reset --hard", git is killed holding .git/modules/lib/stan_math/index.lock, and every later build in that workspace fails while cleaning, before it fetches anything. Neither "git reset --hard" nor "git clean -fdx" can recover, since both want the same lock. runIntegration already deleteDir()s before its checkouts, so only the unit stages are exposed. They reuse their workspace on purpose, so that math-libs is not rebuilt every run, and wiping unconditionally would pay that cost on every build rather than on the broken ones. Wrap their checkout instead and wipe only after it throws. Aborting a superseded build is worth keeping, so this recovers from the abort rather than removing it. A workspace already holding a stale lock still has to be cleared once by hand before a build gets far enough to run this. --- Jenkinsfile | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 1b9e2911971..209565a0151 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -100,6 +100,19 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) if (runRemainingStages) { stage('Unit & integration tests') { + // The unit stages reuse their workspace so math-libs is not rebuilt + // every run, so an aborted build can leave a stale index.lock behind + // and every later checkout into that workspace fails while cleaning. + def checkoutResilient = { + try { + checkout scm + } catch (e) { + echo "checkout failed (${e.message}); wiping workspace and retrying" + deleteDir() + checkout scm + } + } + def runUnit = { args -> def local = "CXX=$args.cxx\n$stanc3_bin_url" if (args.local) @@ -144,7 +157,7 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) windowsUnit: { node('windows') { stage('Windows Headers & Unit') { - checkout scm + checkoutResilient() bat """$WINSETENV make -f lib/stan_math/make/standalone math-libs """ @@ -167,7 +180,7 @@ LDFLAGS_OPENCL=-L/usr/local/cuda/targets/x86_64-linux/lib }, macUnit: { node('macos') { stage('Mac Unit') { - checkout scm + checkoutResilient() runUnit(cxx: MAC_CXX) } } From 7fb39ac1654f447a622afe6885ac91acc969e135 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Tue, 6 Oct 2026 13:51:27 -0400 Subject: [PATCH 15/18] Update model_header.hpp --- src/stan/model/model_header.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stan/model/model_header.hpp b/src/stan/model/model_header.hpp index a3896ecc6cb..5a7e8b1e99f 100644 --- a/src/stan/model/model_header.hpp +++ b/src/stan/model/model_header.hpp @@ -7,6 +7,7 @@ #include #include +#include #include #include From 71253ce6c6910ea865d1a2122be6b5f337169968 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Tue, 6 Oct 2026 11:16:20 -0700 Subject: [PATCH 16/18] fix docs and simplify the code a bit --- src/stan/io/json/json_data.hpp | 29 ++- src/stan/io/json/json_data_handler.hpp | 30 ++- src/stan/io/read_from_context.hpp | 265 ++++++++++++-------- src/test/unit/io/read_from_context_test.cpp | 2 +- 4 files changed, 199 insertions(+), 127 deletions(-) diff --git a/src/stan/io/json/json_data.hpp b/src/stan/io/json/json_data.hpp index 92c51ce9bff..7a33a8d7905 100644 --- a/src/stan/io/json/json_data.hpp +++ b/src/stan/io/json/json_data.hpp @@ -36,12 +36,17 @@ namespace json { * as a vector of type double. * *

A variable inside an array of tuples is named by the dotted path to its - * tuple slot. Given "x": [ [[1, 2, 3], 4], [[5, 6, 7], 8] ], - * slot x.1 has dims {2, 3} and values - * {1, 2, 3, 5, 6, 7}. The leading 2 is the enclosing array, - * which is in element order; the 3 values of each block are column-major as - * above. var_entry::num_outer_arrays records how many leading - * dimensions are enclosing arrays, which dims alone cannot say. + * tuple slot. Given + * + * ``` + * "x": [{"1": [1.0, 2.0, 3.0], "2": 4}, {"1": [5.0, 6.0, 7.0], "2": 8}] + * ``` + * + * slot `x.1` has dims `{2, 3}` and values `{1, 2, 3, 5, 6, 7}`. The leading 2 + * is the enclosing array, with one block per tuple element in element order; + * the 3 values of each block are column-major as above. + * `var_entry::num_outer_arrays` records how many leading dimensions are + * enclosing arrays, which `dims` alone cannot say. * *

json_data objects are created by using the * json_parser and a json_data_handler @@ -70,14 +75,14 @@ class json_data : public stan::io::var_context { } /** - * Decode complex components within each innermost array block. + * Pair the real and imaginary components of a complex variable. * - * A block holds all the real components first, then all the imaginary - * ones, so a block of 6 values {r0, r1, r2, i0, i1, i2} - * decodes to {(r0,i0), (r1,i1), (r2,i2)}. An array of tuples - * contributes one such block per tuple element, laid out back to back. + * The values are one block per enclosing tuple element, or a single block + * when no array of tuples encloses the variable. A block holds all the real + * components first, then all the imaginary ones, so a block of 6 values + * `{r0, r1, r2, i0, i1, i2}` decodes to `{(r0, i0), (r1, i1), (r2, i2)}`. * - * @tparam T Stored scalar type, either int or double. + * @tparam T Stored scalar type, either `int` or `double`. * @param name Variable name. * @param entry Stored values and dimensions for the variable. * @return Complex values in the order of the input blocks. diff --git a/src/stan/io/json/json_data_handler.hpp b/src/stan/io/json/json_data_handler.hpp index a621180b2f4..77c8a3c2fac 100644 --- a/src/stan/io/json/json_data_handler.hpp +++ b/src/stan/io/json/json_data_handler.hpp @@ -37,14 +37,22 @@ inline size_t size_from_dims(const std::vector& dims) { /** The values and dimensions of a single Stan variable. * - * For `x` given as `[ [[1, 2, 3], 4], [[5, 6, 7], 8] ]`, an array of 2 - * tuples whose first slot is an array of 3 reals, slot `x.1` holds + * An array of 2 tuples whose first slot is an array of 3 reals, * - * values = {1, 2, 3, 5, 6, 7} dims = {2, 3} num_outer_arrays = 1 + * ``` + * "x": [{"1": [1.0, 2.0, 3.0], "2": 4}, {"1": [5.0, 6.0, 7.0], "2": 8}] + * ``` * - * The leading 2 is the enclosing array; the trailing 3 is the slot's own - * shape, one block per tuple element. `dims` alone cannot say where the - * first ends and the second begins, so `num_outer_arrays` records it. + * stores slot `x.1` as + * + * ``` + * values = {1, 2, 3, 5, 6, 7} dims = {2, 3} num_outer_arrays = 1 + * ``` + * + * The leading 2 is the enclosing array, with one block per tuple element in + * element order. The trailing 3 is the slot's own shape, column-major within + * each block. `dims` alone cannot say where the first ends and the second + * begins, so `num_outer_arrays` records it. */ template struct var_entry { @@ -73,8 +81,8 @@ struct string_hash { }; /** Name-keyed map used for the handler's bookkeeping and for its results. - * Nothing depends on the iteration order: names_r and names_i are its only - * consumers and var_context does not promise one. + * Nothing depends on the iteration order: `names_r` and `names_i` are its + * only consumers and `var_context` does not promise one. */ template using string_map @@ -295,7 +303,7 @@ class json_data_handler : public stan::json::json_handler { } /* Append one tuple element's block of values to an existing variable. - * `dims` is the variable's own shape; update_array_dims() prepends the + * `dims` is the variable's own shape; `update_array_dims()` prepends the * dimensions of the enclosing arrays once all elements have been seen. */ template @@ -391,8 +399,8 @@ class json_data_handler : public stan::json::json_handler { } /* For array of tuples, prepend the dimensions of the enclosing arrays - * onto the variable's own dimensions and record where the two meet. - * This is the point at which the split is otherwise lost. + * onto the variable's own dimensions and record how many were prepended in + * `num_outer_arrays`, since the merged `dims` no longer shows the split. */ void update_array_dims() { for (auto const& [name, type] : var_types_map) { diff --git a/src/stan/io/read_from_context.hpp b/src/stan/io/read_from_context.hpp index 59efeb4faf7..8454d35cf65 100644 --- a/src/stan/io/read_from_context.hpp +++ b/src/stan/io/read_from_context.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -25,68 +26,134 @@ namespace stan { namespace io { -/* A var_context stores one value array per NAME, never per array element. For +/** + * `read_from_context` is used by the stan compiler to read in user data + * from a `var_context`. This code looks a bit messy due to the indexing + * issues that arise from serializing arrays of tuples and complex number + * types. + * `read_from_context` fills one already-sized model variable from a + * `var_context`. The context maps a name to a flat `std::vector` or + * `std::vector`, while the destination can be any nesting of + * `std::vector`, Eigen types and `std::tuple`. The stan io model has three + * non-trivial rules for arrays of tuples and complex numbers that make + * the io challenging. * - * array[2] tuple(real, real) x; + * 1. Arrays are column-major, first index fastest. For `array[2] vector[3] v` + * the buffer is `v[0][0], v[1][0], v[0][1], v[1][1], ...`, so `v[0]` is + * every second value rather than a contiguous run. `read_payload` tracks + * this with an `offset` and a `stride` that is multiplied by each array + * length it descends through. * - * it holds exactly two entries, "x.1" -> {a, b} and "x.2" -> {c, d}. There is - * no "x[0].1". Reading is therefore name-major: fetch a name once, then visit - * every element that has that slot. Element-major would refetch each buffer - * once per element, and vals_r returns its vector by value. + * 2. Complex values are stored as every real component followed by every + * imaginary one. A complex payload of `n` coefficients takes `2n` values, + * and each imaginary part sits `n` after its real part. + * `read_payload_complex` carries that `n` as `imaginary_offset`. * - * Worked example: + * 3. A tuple is not one entry. Each tuple slot is its own name (`"x.1"`, + * `"x.2.1"`), and array indices never appear in a name. For * - * using Leaf = std::tuple; - * using Mid = std::tuple>; - * std::vector x; + * ```stan + * array[3] tuple(real, real) x; + * ``` * - * scalar_type_t erases every std::vector and Eigen layer and keeps the tuples, - * so SlotT starts as the name tree alone: + * the context holds only `"x.1"` and `"x.2"`, with 3 values each. One name + * feeds that slot in every element of the enclosing array, and each + * element's share is a contiguous block in element order. This is the + * opposite of rule 1: an array enclosing a tuple is element order, while an + * array inside a tuple slot is column-major. * - * SlotT = std::tuple, double>> + * Rule 3 sets the structure of the file. A destination with no tuple is one + * name and one buffer, so the first overload reads it directly. A destination + * containing a tuple is read one name at a time. The second overload walks the + * tuple types to build each leaf name and fetches that name's buffer once. + * `fill_slots` then walks the destination to every place the name feeds, + * taking one block per place off a shared `cursor`. Walking elements first + * would instead fetch every buffer once per element, and `vals_r` returns by + * value. * - * read_from_context walks that tree at compile time and never touches x. The - * std::tuple_element_t below indexes a TYPE; the only runtime work in that - * branch is building name strings. + * Example: + * Our example will go over the array of tuples with nested arrays and vectors. + * ```stan + * array[2] tuple(int, array[2] vector[2]) x; + * ``` + * This is the data structure below with nested arrays in a tuple. + * ```cpp + * std::vector>> x; + * ``` * - * SlotT path name - * tuple> <> "x" - * \__ tuple <0> "x.1" - * |__ complex <0,0> "x.1.1" leaf - * \__ double <0,1> "x.1.2" leaf + * The input JSON will look like the following: * - * Each leaf fetches its own buffer, then fill_slots walks the VALUE to every - * destination that name feeds, off one shared cursor. For "x.1.1", path <0,0>: + * ``` + * "x": [{"1": 7, "2": [[1, 2], [3, 4]]}, + * {"1": 8, "2": [[5, 6], [7, 8]]}] + * ``` * - * x std::vector array -> loop, path stays <0,0> - * \__ x[i] std::tuple slot 0 -> get<0>, path <0> - * \__ v std::vector array -> loop, path stays <0> - * \__ v[j] std::tuple slot 0, path empty -> the payload + * The context holds two names, one per tuple slot: * - * One buffer fills the matrix of every x[i] -> v[j], laid back to back. + * ``` + * "x.1" {7, 8} int + * | \__ x[2].1 + * \_____ x[1].1 * - * The two array orderings are opposite, and both are deliberate: - * an array ENCLOSING a tuple is element order (fill_slots) - * a tuple-free payload is column-major (read_payload) - * array_array_real and nested_array_tuple are both 2x3 and disagree. + * "x.2" {1, 3, 2, 4, 5, 7, 6, 8} array[2] vector[2] + * \________/ \________/ + * x[1].2 x[2].2 element order (rule 3) + * + * Inside the x[1].2 block, first index fastest (rule 1): + * + * 1 -> x[1].2[1][1] + * 2 -> x[1].2[1][2] + * 3 -> x[1].2[2][1] + * 4 -> x[1].2[2][2] + * + * so x[1].2[1] = [1, 2]' and x[1].2[2] = [3, 4]'. x[2].2 is read the + * same way from {5, 7, 6, 8}. + * ``` + * + * `scalar_type_t` is `std::tuple`, so the second + * overload reads `"x.1"` through `vals_i` and `"x.2"` through `vals_r`. For + * `"x.2"`, `fill_slots` visits `x[0]` and then `x[1]`, takes slot 2 of each, + * and hands `read_payload` the next block of 4 values. The slot's array has + * length 2, so the stride is 2 (rule 1): + * + * ``` + * block {1, 3, 2, 4} + * slot[0] = (1, 2) from offsets 0 and 2 + * slot[1] = (3, 4) from offsets 1 and 3 + * ``` + * + * `x[1]` takes `{5, 7, 6, 8}` the same way. A name that runs out of values, + * or has values left over, throws. */ namespace internal { -template -inline auto get_values(const ContextT& context, const Name& name) { - if constexpr (std::is_same_v) { +template +inline auto get_values(const Context& context, const std::string& name) { + if constexpr (std::is_same_v) { return context.vals_i(name); } else { return context.vals_r(name); } } +template +inline constexpr bool is_supported_scalar_v + = std::is_arithmetic_v || is_complex::value; + +template +inline constexpr bool is_flat_vector_v + = std::is_same_v, scalar_type_t>; + template using source_map_t = Eigen::Map< const Eigen::Matrix, Eigen::Dynamic, Eigen::Dynamic>, Eigen::Unaligned, Eigen::InnerStride>; +template +using flat_map_t + = Eigen::Map, Eigen::Dynamic, 1>>; + template inline void read_payload(T& x, const InVec& values, std::size_t offset, std::size_t stride) { @@ -95,10 +162,9 @@ inline void read_payload(T& x, const InVec& values, std::size_t offset, x = values[offset]; } else if constexpr (is_eigen_v) { x = map_t(values.data() + offset, x.rows(), x.cols(), stride); - } else if constexpr (std::is_same_v, scalar_type_t>) { + } else if constexpr (is_flat_vector_v) { const std::size_t size = x.size(); - Eigen::Map, Eigen::Dynamic, 1>> dst(x.data(), - size); + flat_map_t dst(x.data(), size); dst = map_t(values.data() + offset, size, 1, stride); } else { const std::size_t size = x.size(); @@ -116,10 +182,9 @@ inline void read_payload(T& x, const InVec& values, std::size_t offset) { x = values[offset]; } else if constexpr (is_eigen_v) { x = map_t(values.data() + offset, x.rows(), x.cols(), 1); - } else if constexpr (std::is_same_v, scalar_type_t>) { + } else if constexpr (is_flat_vector_v) { const std::size_t size = x.size(); - Eigen::Map, Eigen::Dynamic, 1>> dst(x.data(), - size); + flat_map_t dst(x.data(), size); dst = map_t(values.data() + offset, size, 1, 1); } else { const std::size_t size = x.size(); @@ -137,10 +202,9 @@ inline void read_payload(T& x, const InVec& values) { x = values[0]; } else if constexpr (is_eigen_v) { x = map_t(values.data(), x.rows(), x.cols(), 1); - } else if constexpr (std::is_same_v, scalar_type_t>) { + } else if constexpr (is_flat_vector_v) { const std::size_t size = x.size(); - Eigen::Map, Eigen::Dynamic, 1>> dst(x.data(), - size); + flat_map_t dst(x.data(), size); dst = map_t(values.data(), size, 1, 1); } else { const std::size_t size = x.size(); @@ -162,10 +226,9 @@ inline void read_payload_complex(T& x, const InVec& values, std::size_t offset, x.real() = map_t(values.data() + offset, x.rows(), x.cols(), stride); x.imag() = map_t(values.data() + offset + imaginary_offset, x.rows(), x.cols(), stride); - } else if constexpr (std::is_same_v, scalar_type_t>) { + } else if constexpr (is_flat_vector_v) { const std::size_t size = x.size(); - Eigen::Map, Eigen::Dynamic, 1>> dst(x.data(), - size); + flat_map_t dst(x.data(), size); dst.real() = map_t(values.data() + offset, size, 1, stride); dst.imag() = map_t(values.data() + offset + imaginary_offset, size, 1, stride); @@ -188,12 +251,11 @@ inline void read_payload_complex(T& x, const InVec& values, } else if constexpr (is_eigen_v) { x.real() = map_t(values.data(), x.rows(), x.cols(), 1); x.imag() = map_t(values.data() + imaginary_offset, x.rows(), x.cols(), 1); - } else if constexpr (std::is_same_v, scalar_type_t>) { + } else if constexpr (is_flat_vector_v) { const std::size_t size = x.size(); - Eigen::Map, Eigen::Dynamic, 1>> dst(x.data(), - size); - dst.real() = map_t(values.data() + 0, size, 1, 1); - dst.imag() = map_t(values.data() + 0 + imaginary_offset, size, 1, 1); + flat_map_t dst(x.data(), size); + dst.real() = map_t(values.data(), size, 1, 1); + dst.imag() = map_t(values.data() + imaginary_offset, size, 1, 1); } else { const std::size_t size = x.size(); const std::size_t child_stride = size; @@ -220,10 +282,10 @@ inline void fill_slots(T& x, const InVec& values, std::size_t& cursor, const std::size_t needed = is_complex::value ? 2 * size : size; if (needed > values.size() - cursor) { throw std::runtime_error( - "read_from_context: ran out of values filling " - "slot " + "read_from_context: ran out of values filling slot " + std::to_string(Slot + 1)); - } else if (size > 0) { + } + if (size > 0) { if constexpr (is_complex::value) { read_payload_complex(payload, values, cursor, 1, size); } else { @@ -237,7 +299,7 @@ inline void fill_slots(T& x, const InVec& values, std::size_t& cursor, } // namespace internal /** - * Read a tuple-free variable out of a var_context into an already-sized + * Read a tuple-free variable out of a `var_context` into an already-sized * destination. * * The whole destination is one name and one whole buffer, so the buffer is @@ -245,13 +307,13 @@ inline void fill_slots(T& x, const InVec& values, std::size_t& cursor, * holds. A complex destination takes two values per coefficient, every real * component before every imaginary one. * - * @tparam T Destination type with no std::tuple anywhere inside: int, double, - * std::complex, an Eigen vector, row vector or matrix, or a - * rectangular std::vector nesting of these. Its scalar type must be int, - * double or std::complex. - * @tparam Context Source providing vals_i(name) and vals_r(name) as const - * members returning std::vector and std::vector by value. - * Integer destinations read vals_i, real and complex ones read vals_r. + * @tparam T Destination type with no `std::tuple` anywhere inside: `int`, + * `double`, `std::complex`, an Eigen vector, row vector or matrix, or + * a rectangular `std::vector` nesting of these. Its scalar type must be + * `int`, `double` or `std::complex`. + * @tparam Context Source providing `vals_i(name)` and `vals_r(name)` as const + * members returning `std::vector` and `std::vector` by value. + * Integer destinations read `vals_i`, real and complex ones read `vals_r`. * @param[in,out] x Destination with every dimension already allocated. * Declared dimensions must be validated before this call, because an empty * container cannot describe the sizes of the elements it does not have. @@ -262,14 +324,13 @@ inline void fill_slots(T& x, const InVec& values, std::size_t& cursor, * propagate unchanged. */ template >* = nullptr> + require_not_t>* = nullptr> inline void read_from_context(T& x, const Context& context, const std::string& name) { using scalar_t = scalar_type_t; static_assert( - std::is_arithmetic_v || stan::is_complex::value, - "read_from_context requires int, double or complex " - "scalars"); + internal::is_supported_scalar_v, + "read_from_context requires int, double or complex scalars"); const auto values = internal::get_values(context, name); const std::size_t size = math::num_elements(x); const std::size_t expected = is_complex::value ? 2 * size : size; @@ -277,7 +338,8 @@ inline void read_from_context(T& x, const Context& context, throw std::runtime_error("read_from_context: " + name + " expected " + std::to_string(expected) + " values, got " + std::to_string(values.size())); - } else if (size == 0) { + } + if (size == 0) { return; } if constexpr (std::is_arithmetic_v) { @@ -294,7 +356,7 @@ inline void read_from_context(T& x, const Context& context, } else { x = map_t(values.data(), x.rows(), x.cols()); } - } else if constexpr (std::is_same_v, scalar_t>) { + } else if constexpr (internal::is_flat_vector_v) { if constexpr (is_complex::value) { for (std::size_t i = 0; i < size; ++i) { x[i] = {values[i], values[i + size]}; @@ -310,7 +372,7 @@ inline void read_from_context(T& x, const Context& context, } /** - * Read a variable containing std::tuple out of a var_context into an + * Read a variable containing `std::tuple` out of a `var_context` into an * already-sized destination. * * Every tuple slot is a separate name, so this reads one name per leaf of the @@ -318,56 +380,53 @@ inline void read_from_context(T& x, const Context& context, * later name fails, the names already read remain written. See the file * comment above for a worked example of the two traversals. * - * Call it with three arguments; SlotT and path carry the recursion and + * Call it with three arguments; `SlotT` and `path` carry the recursion and * default to the root of the tuple skeleton and the empty path. * - * @tparam T Destination type with a std::tuple somewhere inside: a - * std::tuple, or a rectangular std::vector nesting around one. Every leaf - * scalar must be int, double or std::complex. - * @tparam SlotT Position in the tuple skeleton, scalar_type_t at the - * root. A std::tuple here means the name has more slots below it; anything - * else means the name is a leaf and is read. Never a type of x itself, since - * scalar_type_t has erased the std::vector and Eigen layers. - * @tparam Context Source providing vals_i(name) and vals_r(name) as const - * members returning std::vector and std::vector by value. - * @tparam Slots Tuple slot indices from the root to SlotT, excluding array - * indices. The path that fill_slots follows through the destination. + * @tparam T Destination type with a `std::tuple` somewhere inside: a + * `std::tuple`, or a rectangular `std::vector` nesting around one. Every leaf + * scalar must be `int`, `double` or `std::complex`. + * @tparam SlotT Position in the tuple skeleton, `scalar_type_t` at the + * root. A `std::tuple` here means the name has more slots below it; anything + * else means the name is a leaf and is read. Never a type of `x` itself, + * since `scalar_type_t` has erased the `std::vector` and Eigen layers. + * @tparam Context Source providing `vals_i(name)` and `vals_r(name)` as const + * members returning `std::vector` and `std::vector` by value. + * @tparam Slots Tuple slot indices from the root to `SlotT`, excluding array + * indices. The path that `fill_slots` follows through the destination. * @param[in,out] x Destination with every dimension already allocated. It is - * always the root: the recursion descends SlotT and path, never x. + * always the root: the recursion descends `SlotT` and `path`, never `x`. * @param[in] context Source of the named integer and real buffers. - * @param[in] name Variable name, with the dotted one-based suffix for Slots + * @param[in] name Variable name, with the dotted one-based suffix for `Slots` * already appended. - * @param[in] path Slots as a value, so the pack is deduced rather than given. + * @param[in] path `Slots` as a value, so the pack is deduced rather than + * given. * @throw std::runtime_error if a name supplies a number of values other than * the number its destinations hold. Exceptions from the context propagate * unchanged. */ template , typename Context, - std::size_t... Slots, - stan::require_t>* = nullptr> + std::size_t... Slots, require_t>* = nullptr> inline void read_from_context(T& x, const Context& context, - std::string_view name, + const std::string& name, std::index_sequence path = {}) { if constexpr (is_tuple_v) { - math::index_apply>([&x, &context, - &name](auto... Slot) { - (read_from_context>( - x, context, std::string(name) + "." + std::to_string(Slot.value + 1), - std::index_sequence{}), - ...); - }); + math::index_apply>( + [&x, &context, &name](auto... Slot) { + (read_from_context>( + x, context, name + "." + std::to_string(Slot.value + 1), + std::index_sequence{}), + ...); + }); } else { - constexpr bool is_valid_slot_type - = std::is_arithmetic_v || stan::is_complex::value; - static_assert(is_valid_slot_type, - "read_from_context requires int, double or complex " - "scalars"); - const std::string leaf_name(name); - const auto values = internal::get_values(context, leaf_name); + static_assert( + internal::is_supported_scalar_v, + "read_from_context requires int, double or complex scalars"); + const auto values = internal::get_values(context, name); std::size_t cursor = 0; internal::fill_slots(x, values, cursor, path); if (cursor != values.size()) { - throw std::runtime_error("read_from_context: " + leaf_name + " supplied " + throw std::runtime_error("read_from_context: " + name + " supplied " + std::to_string(values.size()) + " values but the destination holds " + std::to_string(cursor)); diff --git a/src/test/unit/io/read_from_context_test.cpp b/src/test/unit/io/read_from_context_test.cpp index 0ae9ec056f0..ca954ec0127 100644 --- a/src/test/unit/io/read_from_context_test.cpp +++ b/src/test/unit/io/read_from_context_test.cpp @@ -352,7 +352,7 @@ TEST(ioReadFromContext, nested_array_tuple) { std::vector> x(2, std::vector(3, {-999, -999})); ASSERT_NO_THROW(read_fixture(x, "nested_array_tuple")); // Multiple array dimensions outside a tuple use tuple-instance order, - // unlike the identically sized ordinary array_array_real fixture. + // unlike the identically sized ordinary `array_array_real` fixture. for (int a = 0; a < 2; ++a) { for (int b = 0; b < 3; ++b) { EXPECT_EQ(10 * a + b, std::get<0>(x[a][b])); From 2ed67bb7130712fc1a8219a44c7cbeaac34c397a Mon Sep 17 00:00:00 2001 From: Stan Jenkins Date: Tue, 6 Oct 2026 14:18:13 -0400 Subject: [PATCH 17/18] [Jenkins] auto-formatting by clang-format version 10.0.0-4ubuntu1 --- src/stan/io/read_from_context.hpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/stan/io/read_from_context.hpp b/src/stan/io/read_from_context.hpp index 8454d35cf65..244465d4422 100644 --- a/src/stan/io/read_from_context.hpp +++ b/src/stan/io/read_from_context.hpp @@ -26,11 +26,11 @@ namespace stan { namespace io { -/** +/** * `read_from_context` is used by the stan compiler to read in user data * from a `var_context`. This code looks a bit messy due to the indexing - * issues that arise from serializing arrays of tuples and complex number - * types. + * issues that arise from serializing arrays of tuples and complex number + * types. * `read_from_context` fills one already-sized model variable from a * `var_context`. The context maps a name to a flat `std::vector` or * `std::vector`, while the destination can be any nesting of @@ -101,15 +101,15 @@ namespace io { * * Inside the x[1].2 block, first index fastest (rule 1): * - * 1 -> x[1].2[1][1] + * 1 -> x[1].2[1][1] * 2 -> x[1].2[1][2] - * 3 -> x[1].2[2][1] + * 3 -> x[1].2[2][1] * 4 -> x[1].2[2][2] * * so x[1].2[1] = [1, 2]' and x[1].2[2] = [3, 4]'. x[2].2 is read the * same way from {5, 7, 6, 8}. * ``` - * + * * `scalar_type_t` is `std::tuple`, so the second * overload reads `"x.1"` through `vals_i` and `"x.2"` through `vals_r`. For * `"x.2"`, `fill_slots` visits `x[0]` and then `x[1]`, takes slot 2 of each, From 6ec8508115c327be54d19eda24e0f8bcdeae200d Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Wed, 7 Oct 2026 14:21:20 -0400 Subject: [PATCH 18/18] Revert "retry the unit stage checkout after wiping the workspace" This reverts commit 9b053ed8a509e4983b34ef4bc47235052f4c9d5b. --- Jenkinsfile | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 209565a0151..1b9e2911971 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -100,19 +100,6 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) if (runRemainingStages) { stage('Unit & integration tests') { - // The unit stages reuse their workspace so math-libs is not rebuilt - // every run, so an aborted build can leave a stale index.lock behind - // and every later checkout into that workspace fails while cleaning. - def checkoutResilient = { - try { - checkout scm - } catch (e) { - echo "checkout failed (${e.message}); wiping workspace and retrying" - deleteDir() - checkout scm - } - } - def runUnit = { args -> def local = "CXX=$args.cxx\n$stanc3_bin_url" if (args.local) @@ -157,7 +144,7 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) windowsUnit: { node('windows') { stage('Windows Headers & Unit') { - checkoutResilient() + checkout scm bat """$WINSETENV make -f lib/stan_math/make/standalone math-libs """ @@ -180,7 +167,7 @@ LDFLAGS_OPENCL=-L/usr/local/cuda/targets/x86_64-linux/lib }, macUnit: { node('macos') { stage('Mac Unit') { - checkoutResilient() + checkout scm runUnit(cxx: MAC_CXX) } }