summaryrefslogtreecommitdiffstats
path: root/src/libixion/cell.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/libixion/cell.cpp')
-rw-r--r--src/libixion/cell.cpp578
1 files changed, 578 insertions, 0 deletions
diff --git a/src/libixion/cell.cpp b/src/libixion/cell.cpp
new file mode 100644
index 0000000..22e8dc3
--- /dev/null
+++ b/src/libixion/cell.cpp
@@ -0,0 +1,578 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <ixion/cell.hpp>
+#include <ixion/address.hpp>
+#include <ixion/exceptions.hpp>
+#include <ixion/formula_result.hpp>
+#include <ixion/formula_tokens.hpp>
+#include <ixion/interface/session_handler.hpp>
+#include <ixion/global.hpp>
+#include <ixion/matrix.hpp>
+#include <ixion/formula_name_resolver.hpp>
+#include <ixion/formula.hpp>
+#include <ixion/model_context.hpp>
+
+#include "formula_interpreter.hpp"
+#include "debug.hpp"
+
+#include <cassert>
+#include <string>
+#include <sstream>
+#include <iostream>
+#include <algorithm>
+#include <functional>
+
+#include "calc_status.hpp"
+
+#define FORMULA_CIRCULAR_SAFE 0x01
+#define FORMULA_SHARED_TOKENS 0x02
+
+using namespace std;
+
+namespace ixion {
+
+namespace {
+
+#if IXION_LOGGING
+
+[[maybe_unused]] std::string gen_trace_output(const formula_cell& fc, const model_context& cxt, const abs_address_t& pos)
+{
+ auto resolver = formula_name_resolver::get(formula_name_resolver_t::excel_a1, &cxt);
+ std::ostringstream os;
+ os << "pos=" << pos.get_name() << "; formula='" << print_formula_tokens(cxt, pos, *resolver, fc.get_tokens()->get()) << "'";
+ return os.str();
+}
+
+#endif
+
+}
+
+struct formula_cell::impl
+{
+ mutable calc_status_ptr_t m_calc_status;
+ formula_tokens_store_ptr_t m_tokens;
+ rc_address_t m_group_pos;
+
+ impl() : impl(-1, -1, new calc_status, formula_tokens_store_ptr_t()) {}
+
+ impl(const formula_tokens_store_ptr_t& tokens) : impl(-1, -1, new calc_status, tokens) {}
+
+ impl(row_t row, col_t col, const calc_status_ptr_t& cs,
+ const formula_tokens_store_ptr_t& tokens) :
+ m_calc_status(cs),
+ m_tokens(tokens),
+ m_group_pos(row, col, false, false) {}
+
+ /**
+ * Block until the result becomes available.
+ *
+ * @param lock mutex lock associated with the result cache data.
+ */
+ void wait_for_interpreted_result(std::unique_lock<std::mutex>& lock) const
+ {
+ IXION_TRACE("Wait for the interpreted result");
+
+ while (!m_calc_status->result)
+ {
+ IXION_TRACE("Waiting...");
+ m_calc_status->cond.wait(lock);
+ }
+ }
+
+ void reset_flag()
+ {
+ m_calc_status->circular_safe = false;
+ }
+
+ /**
+ * Check if this cell contains a circular reference.
+ *
+ * @return true if this cell contains no circular reference, hence
+ * considered "safe", false otherwise.
+ */
+ bool is_circular_safe() const
+ {
+ return m_calc_status->circular_safe;
+ }
+
+ bool check_ref_for_circular_safety(const formula_cell& ref, const abs_address_t& pos)
+ {
+ if (!ref.mp_impl->is_circular_safe())
+ {
+ // Circular dependency detected !!
+ IXION_DEBUG("Circular dependency detected !!");
+ assert(!m_calc_status->result);
+ m_calc_status->result =
+ std::make_unique<formula_result>(formula_error_t::ref_result_not_available);
+
+ return false;
+ }
+ return true;
+ }
+
+ void check_calc_status_or_throw() const
+ {
+ if (!m_calc_status->result)
+ {
+ // Result not cached yet. Reference error.
+ IXION_DEBUG("Result not cached yet. This is a reference error.");
+ throw formula_error(formula_error_t::ref_result_not_available);
+ }
+
+ if (m_calc_status->result->get_type() == formula_result::result_type::error)
+ {
+ // Error condition.
+ IXION_DEBUG("Error in result.");
+ throw formula_error(m_calc_status->result->get_error());
+ }
+ }
+
+ double fetch_value_from_result() const
+ {
+ check_calc_status_or_throw();
+
+ switch (m_calc_status->result->get_type())
+ {
+ case formula_result::result_type::boolean:
+ return m_calc_status->result->get_boolean() ? 1.0 : 0.0;
+ case formula_result::result_type::value:
+ return m_calc_status->result->get_value();
+ case formula_result::result_type::matrix:
+ {
+ IXION_TRACE("Fetching a matrix result.");
+ const matrix& m = m_calc_status->result->get_matrix();
+ row_t row_size = m.row_size();
+ col_t col_size = m.col_size();
+
+ if (m_group_pos.row >= row_size || m_group_pos.column >= col_size)
+ throw formula_error(formula_error_t::invalid_value_type);
+
+ matrix::element elem = m.get(m_group_pos.row, m_group_pos.column);
+
+ switch (elem.type)
+ {
+ case matrix::element_type::numeric:
+ return std::get<double>(elem.value);
+ case matrix::element_type::empty:
+ return 0.0;
+ case matrix::element_type::boolean:
+ return std::get<bool>(elem.value) ? 1.0 : 0.0;
+ case matrix::element_type::string:
+ case matrix::element_type::error:
+ default:
+ throw formula_error(formula_error_t::invalid_value_type);
+ }
+ }
+ default:
+ {
+ std::ostringstream os;
+ os << "numeric result was requested, but the actual result is of "
+ << m_calc_status->result->get_type() << " type.";
+ throw formula_error(
+ formula_error_t::invalid_value_type,
+ os.str()
+ );
+ }
+ }
+ }
+
+ std::string_view fetch_string_from_result() const
+ {
+ check_calc_status_or_throw();
+
+ switch (m_calc_status->result->get_type())
+ {
+ case formula_result::result_type::string:
+ return m_calc_status->result->get_string();
+ case formula_result::result_type::matrix:
+ {
+ const matrix& m = m_calc_status->result->get_matrix();
+ row_t row_size = m.row_size();
+ col_t col_size = m.col_size();
+
+ if (m_group_pos.row >= row_size || m_group_pos.column >= col_size)
+ throw formula_error(formula_error_t::invalid_value_type);
+
+ matrix::element elem = m.get(m_group_pos.row, m_group_pos.column);
+
+ switch (elem.type)
+ {
+ case matrix::element_type::string:
+ return std::get<std::string_view>(elem.value);
+ case matrix::element_type::numeric:
+ case matrix::element_type::empty:
+ case matrix::element_type::boolean:
+ case matrix::element_type::error:
+ default:
+ throw formula_error(formula_error_t::invalid_value_type);
+ }
+ }
+ default:
+ {
+ std::ostringstream os;
+ os << "string result was requested, but the actual result is of "
+ << m_calc_status->result->get_type() << " type.";
+ throw formula_error(
+ formula_error_t::invalid_value_type,
+ os.str()
+ );
+ }
+ }
+
+ return std::string_view{};
+ }
+
+ bool is_grouped() const
+ {
+ return m_group_pos.column >= 0 && m_group_pos.row >= 0;
+ }
+
+ bool is_group_parent() const
+ {
+ return m_group_pos.column == 0 && m_group_pos.row == 0;
+ }
+
+ bool calc_allowed() const
+ {
+ if (!is_grouped())
+ return true;
+
+ return is_group_parent();
+ }
+
+ formula_result get_single_formula_result(const formula_result& src) const
+ {
+ if (!is_grouped())
+ return src; // returns a copy.
+
+ if (src.get_type() != formula_result::result_type::matrix)
+ // A grouped cell should have a matrix result whose size equals the
+ // size of the group. But in case of anything else, just return the
+ // stored value.
+ return src;
+
+ const matrix& m = src.get_matrix();
+ row_t row_size = m.row_size();
+ col_t col_size = m.col_size();
+
+ if (m_group_pos.row >= row_size || m_group_pos.column >= col_size)
+ return formula_result(formula_error_t::invalid_value_type);
+
+ matrix::element elem = m.get(m_group_pos.row, m_group_pos.column);
+
+ switch (elem.type)
+ {
+ case matrix::element_type::numeric:
+ return formula_result(std::get<double>(elem.value));
+ case matrix::element_type::string:
+ {
+ std::string s{std::get<std::string_view>(elem.value)};
+ return formula_result(std::move(s));
+ }
+ case matrix::element_type::error:
+ return formula_result(std::get<formula_error_t>(elem.value));
+ case matrix::element_type::empty:
+ return formula_result();
+ case matrix::element_type::boolean:
+ return formula_result(std::get<bool>(elem.value) ? 1.0 : 0.0);
+ default:
+ throw std::logic_error("unhandled element type of a matrix result value.");
+ }
+ }
+
+ void set_single_formula_result(formula_result result)
+ {
+ if (is_grouped())
+ {
+ std::unique_lock<std::mutex> lock(m_calc_status->mtx);
+
+ if (!m_calc_status->result)
+ {
+ m_calc_status->result =
+ std::make_unique<formula_result>(
+ matrix(m_calc_status->group_size.row, m_calc_status->group_size.column));
+ }
+
+ matrix& m = m_calc_status->result->get_matrix();
+ assert(m_group_pos.row < row_t(m.row_size()));
+ assert(m_group_pos.column < col_t(m.col_size()));
+
+ switch (result.get_type())
+ {
+ case formula_result::result_type::boolean:
+ m.set(m_group_pos.row, m_group_pos.column, result.get_boolean());
+ break;
+ case formula_result::result_type::value:
+ m.set(m_group_pos.row, m_group_pos.column, result.get_value());
+ break;
+ case formula_result::result_type::string:
+ m.set(m_group_pos.row, m_group_pos.column, result.get_string());
+ break;
+ case formula_result::result_type::error:
+ m.set(m_group_pos.row, m_group_pos.column, result.get_error());
+ break;
+ case formula_result::result_type::matrix:
+ throw std::logic_error("setting a cached result of matrix value directly is not yet supported.");
+ }
+
+ return;
+ }
+
+ std::unique_lock<std::mutex> lock(m_calc_status->mtx);
+ m_calc_status->result = std::make_unique<formula_result>(std::move(result));
+ }
+};
+
+formula_cell::formula_cell() : mp_impl(std::make_unique<impl>()) {}
+
+formula_cell::formula_cell(const formula_tokens_store_ptr_t& tokens) :
+ mp_impl(std::make_unique<impl>(tokens)) {}
+
+formula_cell::formula_cell(
+ row_t group_row, col_t group_col,
+ const calc_status_ptr_t& cs,
+ const formula_tokens_store_ptr_t& tokens) :
+ mp_impl(std::make_unique<impl>(group_row, group_col, cs, tokens)) {}
+
+formula_cell::~formula_cell()
+{
+}
+
+const formula_tokens_store_ptr_t& formula_cell::get_tokens() const
+{
+ return mp_impl->m_tokens;
+}
+
+void formula_cell::set_tokens(const formula_tokens_store_ptr_t& tokens)
+{
+ mp_impl->m_tokens = tokens;
+}
+
+double formula_cell::get_value(formula_result_wait_policy_t policy) const
+{
+ std::unique_lock<std::mutex> lock(mp_impl->m_calc_status->mtx);
+ if (policy == formula_result_wait_policy_t::block_until_done)
+ mp_impl->wait_for_interpreted_result(lock);
+ return mp_impl->fetch_value_from_result();
+}
+
+std::string_view formula_cell::get_string(formula_result_wait_policy_t policy) const
+{
+ std::unique_lock<std::mutex> lock(mp_impl->m_calc_status->mtx);
+ if (policy == formula_result_wait_policy_t::block_until_done)
+ mp_impl->wait_for_interpreted_result(lock);
+ return mp_impl->fetch_string_from_result();
+}
+
+void formula_cell::interpret(model_context& context, const abs_address_t& pos)
+{
+ IXION_TRACE(gen_trace_output(*this, context, pos));
+
+ if (!mp_impl->calc_allowed())
+ throw std::logic_error("Calculation on this formula cell is not allowed.");
+
+ calc_status& status = *mp_impl->m_calc_status;
+
+ {
+ std::lock_guard<std::mutex> lock(status.mtx);
+
+ if (mp_impl->m_calc_status->result)
+ {
+ // When the result is already cached before the cell is interpreted,
+ // it can mean the cell has circular dependency.
+ if (status.result->get_type() == formula_result::result_type::error)
+ {
+ auto handler = context.create_session_handler();
+ if (handler)
+ {
+ handler->begin_cell_interpret(pos);
+ std::string_view msg = get_formula_error_name(status.result->get_error());
+ handler->set_formula_error(msg);
+ handler->end_cell_interpret();
+ }
+ }
+ return;
+ }
+
+ formula_interpreter fin(this, context);
+ fin.set_origin(pos);
+ status.result = std::make_unique<formula_result>();
+ if (fin.interpret())
+ {
+ // Successful interpretation.
+ *status.result = fin.transfer_result();
+ }
+ else
+ {
+ // Interpretation ended with an error condition.
+ status.result->set_error(fin.get_error());
+ }
+ }
+
+ status.cond.notify_all();
+}
+
+void formula_cell::check_circular(const model_context& cxt, const abs_address_t& pos)
+{
+ // TODO: Check to make sure this is being run on the main thread only.
+ for (const formula_token& t : mp_impl->m_tokens->get())
+ {
+ switch (t.opcode)
+ {
+ case fop_single_ref:
+ {
+ abs_address_t addr = std::get<address_t>(t.value).to_abs(pos);
+ const formula_cell* ref = cxt.get_formula_cell(addr);
+
+ if (!ref)
+ continue;
+
+ if (!mp_impl->check_ref_for_circular_safety(*ref, addr))
+ return;
+
+ break;
+ }
+ case fop_range_ref:
+ {
+ abs_range_t range = std::get<range_t>(t.value).to_abs(pos);
+ for (sheet_t sheet = range.first.sheet; sheet <= range.last.sheet; ++sheet)
+ {
+ rc_size_t sheet_size = cxt.get_sheet_size();
+ col_t col_first = range.first.column, col_last = range.last.column;
+ if (range.all_columns())
+ {
+ col_first = 0;
+ col_last = sheet_size.column - 1;
+ }
+
+ for (col_t col = col_first; col <= col_last; ++col)
+ {
+ row_t row_first = range.first.row, row_last = range.last.row;
+ if (range.all_rows())
+ {
+ assert(row_last == row_unset);
+ row_first = 0;
+ row_last = sheet_size.row - 1;
+ }
+
+ for (row_t row = row_first; row <= row_last; ++row)
+ {
+ abs_address_t addr(sheet, row, col);
+ if (cxt.get_celltype(addr) != celltype_t::formula)
+ continue;
+
+ if (!mp_impl->check_ref_for_circular_safety(*cxt.get_formula_cell(addr), addr))
+ return;
+ }
+ }
+ }
+
+ break;
+ }
+ default:
+ ;
+ }
+
+ }
+
+ // No circular dependencies. Good.
+ mp_impl->m_calc_status->circular_safe = true;
+}
+
+void formula_cell::reset()
+{
+ std::lock_guard<std::mutex> lock(mp_impl->m_calc_status->mtx);
+ mp_impl->m_calc_status->result.reset();
+ mp_impl->reset_flag();
+}
+
+std::vector<const formula_token*> formula_cell::get_ref_tokens(
+ const model_context& cxt, const abs_address_t& pos) const
+{
+ std::vector<const formula_token*> ret;
+
+ std::function<void(const formula_tokens_t::value_type&)> get_refs = [&](const formula_tokens_t::value_type& t)
+ {
+ switch (t.opcode)
+ {
+ case fop_single_ref:
+ case fop_range_ref:
+ ret.push_back(&t);
+ break;
+ case fop_named_expression:
+ {
+ const named_expression_t* named_exp =
+ cxt.get_named_expression(pos.sheet, std::get<std::string>(t.value));
+
+ if (!named_exp)
+ // silently ignore non-existing names.
+ break;
+
+ // recursive call.
+ std::for_each(named_exp->tokens.begin(), named_exp->tokens.end(), get_refs);
+ break;
+ }
+ default:
+ ; // ignore the rest.
+ }
+ };
+
+ const formula_tokens_t& this_tokens = mp_impl->m_tokens->get();
+
+ std::for_each(this_tokens.begin(), this_tokens.end(), get_refs);
+
+ return ret;
+}
+
+const formula_result& formula_cell::get_raw_result_cache(formula_result_wait_policy_t policy) const
+{
+ std::unique_lock<std::mutex> lock(mp_impl->m_calc_status->mtx);
+
+ if (policy == formula_result_wait_policy_t::block_until_done)
+ mp_impl->wait_for_interpreted_result(lock);
+
+ if (!mp_impl->m_calc_status->result)
+ {
+ IXION_DEBUG("Result not yet available.");
+ throw formula_error(formula_error_t::ref_result_not_available);
+ }
+
+ return *mp_impl->m_calc_status->result;
+}
+
+formula_result formula_cell::get_result_cache(formula_result_wait_policy_t policy) const
+{
+ const formula_result& src = get_raw_result_cache(policy);
+ return mp_impl->get_single_formula_result(src);
+}
+
+void formula_cell::set_result_cache(formula_result result)
+{
+ mp_impl->set_single_formula_result(result);
+}
+
+formula_group_t formula_cell::get_group_properties() const
+{
+ uintptr_t identity = reinterpret_cast<uintptr_t>(mp_impl->m_calc_status.get());
+ return formula_group_t(mp_impl->m_calc_status->group_size, identity, mp_impl->is_grouped());
+}
+
+abs_address_t formula_cell::get_parent_position(const abs_address_t& pos) const
+{
+ if (!mp_impl->is_grouped())
+ return pos;
+
+ abs_address_t parent_pos = pos;
+ parent_pos.column -= mp_impl->m_group_pos.column;
+ parent_pos.row -= mp_impl->m_group_pos.row;
+ return parent_pos;
+}
+
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */