diff --git a/src/stan/io/json/json_data.hpp b/src/stan/io/json/json_data.hpp index 13c771c3112..7a33a8d7905 100644 --- a/src/stan/io/json/json_data.hpp +++ b/src/stan/io/json/json_data.hpp @@ -5,9 +5,9 @@ #include #include #include +#include #include #include -#include #include #include #include @@ -35,6 +35,19 @@ namespace json { * stored as a vector of ints, else the array will be stored * 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": [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 * to read a single JSON text from an input stream. @@ -61,6 +74,44 @@ class json_data : public stan::io::var_context { return vars_r_.find(name) != vars_r_.end(); } + /** + * Pair the real and imaginary components of a complex variable. + * + * 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`. + * @param name Variable name. + * @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 &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 (!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 = entry.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 < half_block; ++i) { + result.emplace_back(values[start + i], values[start + half_block + i]); + } + } + return result; + } + public: /** * Construct a json_data object from the specified input stream. @@ -108,14 +159,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_; } @@ -126,35 +173,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>{}; } @@ -168,9 +193,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_; } @@ -184,7 +209,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_; } @@ -198,7 +223,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_; } @@ -246,66 +271,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 70177a0bda9..77c8a3c2fac 100644 --- a/src/stan/io/json/json_data_handler.hpp +++ b/src/stan/io/json/json_data_handler.hpp @@ -6,15 +6,17 @@ #include #include #include +#include #include +#include #include #include #include #include -#include #include #include #include +#include #include #include @@ -22,11 +24,75 @@ 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()); +} -typedef std::map vars_map_r; -typedef std::map vars_map_i; +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. + * + * An array of 2 tuples whose first slot is an array of 3 reals, + * + * ``` + * "x": [{"1": [1.0, 2.0, 3.0], "2": 4}, {"1": [5.0, 6.0, 7.0], "2": 8}] + * ``` + * + * 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 { + std::vector values; + std::vector dims; + size_t num_outer_arrays = 0; + + /** True if the variable itself is an array rather than a scalar. */ + 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(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 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>; + +using var_r = var_entry; +using var_i = var_entry; + +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 @@ -125,11 +191,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::map slot_types_map; // all slots all vars parsed - 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 @@ -169,7 +235,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; @@ -236,6 +302,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& entry, const std::vector& values, + const std::vector& 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. * Converts multi-dim arrays from row-major to column major. * For arrays of tuples we need to check that new elements are consistent @@ -264,23 +341,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; @@ -295,28 +368,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."; @@ -324,76 +380,69 @@ 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 how many were prepended in + * `num_outer_arrays`, since the merged `dims` no longer shows the split. */ 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]); - 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_arrays = all_dims.size() - entry.dims.size(); + entry.dims = all_dims; + } + }; + 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()); } } } template - void to_column_major(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) { - 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()); @@ -404,7 +453,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()); @@ -436,7 +485,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(); @@ -451,21 +500,21 @@ 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; * 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); } @@ -494,7 +543,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; @@ -519,7 +568,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()) @@ -557,7 +606,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."); } @@ -595,7 +644,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) @@ -633,7 +682,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; @@ -642,7 +691,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; @@ -651,7 +700,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; @@ -675,14 +724,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()]) { @@ -692,7 +741,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 @@ -705,14 +754,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 @@ -723,7 +772,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(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/stan/io/read_from_context.hpp b/src/stan/io/read_from_context.hpp new file mode 100644 index 00000000000..244465d4422 --- /dev/null +++ b/src/stan/io/read_from_context.hpp @@ -0,0 +1,441 @@ +#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 +#include +#include +#include +#include +#include + +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. + * `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. + * + * 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. + * + * 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`. + * + * 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 + * + * ```stan + * array[3] tuple(real, real) x; + * ``` + * + * 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. + * + * 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. + * + * 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; + * ``` + * + * The input JSON will look like the following: + * + * ``` + * "x": [{"1": 7, "2": [[1, 2], [3, 4]]}, + * {"1": 8, "2": [[5, 6], [7, 8]]}] + * ``` + * + * The context holds two names, one per tuple slot: + * + * ``` + * "x.1" {7, 8} int + * | \__ x[2].1 + * \_____ x[1].1 + * + * "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 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) { + 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 (is_flat_vector_v) { + const std::size_t size = x.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(); + 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); + } + } +} + +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 (is_flat_vector_v) { + const std::size_t size = x.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(); + 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); + } + } +} + +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 (is_flat_vector_v) { + const std::size_t size = x.size(); + flat_map_t dst(x.data(), size); + dst = map_t(values.data(), size, 1, 1); + } else { + 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); + } + } +} + +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 (is_flat_vector_v) { + const std::size_t size = x.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); + } 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_complex(x[i], values, offset + i * stride, child_stride, + imaginary_offset); + } + } +} + +template +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 (is_flat_vector_v) { + const std::size_t size = x.size(); + 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; + for (std::size_t i = 0; i < size; ++i) { + read_payload_complex(x[i], values, i, child_stride, imaginary_offset); + } + } +} + +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)); + } + if (size > 0) { + if constexpr (is_complex::value) { + read_payload_complex(payload, values, cursor, 1, size); + } else { + read_payload(payload, values, cursor); + } + } + cursor += needed; + } +} + +} // namespace internal + +/** + * 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] 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 >* = nullptr> +inline void read_from_context(T& x, const Context& context, + const std::string& name) { + using scalar_t = scalar_type_t; + static_assert( + 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; + if (values.size() != expected) { + throw std::runtime_error("read_from_context: " + name + " expected " + + std::to_string(expected) + " values, got " + + std::to_string(values.size())); + } + 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 (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]}; + } + } else { + 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); + } +} + +/** + * 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 + * 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. + * + * 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. + * @param[in,out] x Destination with every dimension already allocated. It is + * 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. + * @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, require_t>* = nullptr> +inline void read_from_context(T& x, const Context& context, + 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, name + "." + std::to_string(Slot.value + 1), + std::index_sequence{}), + ...); + }); + } else { + 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: " + 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/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 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..02b51ca1ae6 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) { @@ -230,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")); +} 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; } 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..ca954ec0127 --- /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::io::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::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); +} 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": [] +}