/*
 * Copyright © 2003-2026 Dynare Team
 *
 * This file is part of Dynare.
 *
 * Dynare is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Dynare is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Dynare.  If not, see <https://www.gnu.org/licenses/>.
 */

#include <cassert>
#include <cmath>
#include <fstream>
#include <iostream>
#include <numeric>
#include <ranges>
#include <sstream>

#include "ExprNode.hh"
#include "ParsingDriver.hh"
#include "Statement.hh"
#include "Utils.hh"
/* NB: the following also imports our specialization of operator<< for location class,
   used below in error() and undeclared_model_variable_error() */
#include "WarningConsolidation.hh"

bool
ParsingDriver::symbol_exists_and_is_not_modfile_local_or_external_function(const string& s) const
{
  if (!mod_file->symbol_table.exists(s))
    return false;

  SymbolType type = mod_file->symbol_table.getType(s);

  return type != SymbolType::modFileLocalVariable && type != SymbolType::externalFunction;
}

void
ParsingDriver::check_symbol_existence_in_model_block(const string& name)
{
  if (!mod_file->symbol_table.exists(name) || undeclared_model_vars.contains(name))
    undeclared_model_variable_error("Unknown symbol: " + name, name);
}

void
ParsingDriver::check_symbol_existence(const string& name)
{
  if (!mod_file->symbol_table.exists(name))
    error("Unknown symbol: " + name
          + ".\nIf referenced from the 'initval', 'endval', 'histval', or 'shocks' block, you can "
            "pass the 'nostrict' option to dynare to have this line ignored.");
}

void
ParsingDriver::check_symbol_is_parameter(const string& name)
{
  check_symbol_existence(name);
  int symb_id = mod_file->symbol_table.getID(name);
  if (mod_file->symbol_table.getType(symb_id) != SymbolType::parameter)
    error(name + " is not a parameter");
}

void
ParsingDriver::set_current_data_tree(DataTree* data_tree_arg)
{
  data_tree = data_tree_arg;
  model_tree = dynamic_cast<ModelTree*>(data_tree_arg);
  dynamic_model = dynamic_cast<DynamicModel*>(data_tree_arg);
  heterogeneous_model = dynamic_cast<HeterogeneousModel*>(data_tree_arg);
}

void
ParsingDriver::reset_data_tree()
{
  set_current_data_tree(&mod_file->expressions_tree);
}

unique_ptr<ModFile>
ParsingDriver::parse(istream& in, bool debug)
{
  mod_file = make_unique<ModFile>(warnings);

  reset_data_tree();
  estim_params.init(*data_tree);
  osr_params.init(*data_tree);

  lexer = make_unique<DynareFlex>(&in);
  lexer->set_debug(debug);

  Dynare::parser parser(*this);
  parser.set_debug_level(debug);
  parser.parse();

  return move(mod_file);
}

void
ParsingDriver::error(const Dynare::parser::location_type& l, const string& m)
{
  cerr << "ERROR: " << l << ": " << m << '\n';
  exit(EXIT_FAILURE);
}

void
ParsingDriver::error(const string& m)
{
  error(location, m);
}

void
ParsingDriver::undeclared_model_variable_error(const string& m, const string& var)
{
  ostringstream stream;
  if (!nostrict)
    stream << "ERROR: " << location << ": ";
  stream << m;
  if (nostrict)
    stream << " automatically declared exogenous.";
  undeclared_model_variable_errors.emplace_back(var, stream.str());
}

void
ParsingDriver::warning(const string& m)
{
  warnings << "WARNING: " << location << ": " << m << '\n';
}

int
ParsingDriver::declare_symbol(const string& name, SymbolType type, const string& tex_name,
                              const vector<pair<string, string>>& partition_value,
                              const optional<int>& heterogeneity_dimension)
{
  int symb_id;
  try
    {
      symb_id = mod_file->symbol_table.addSymbol(name, type, tex_name, partition_value,
                                                 heterogeneity_dimension);
    }
  catch (SymbolTable::AlreadyDeclaredException& e)
    {
      if (e.same_type)
        {
          warning("Symbol " + name + " declared twice.");
          symb_id = mod_file->symbol_table.getID(name);
        }
      else
        error("Symbol " + name + " declared twice with different types!");
    }
  return symb_id;
}

int
ParsingDriver::declare_endogenous(const string& name, const string& tex_name,
                                  const vector<pair<string, string>>& partition_value)
{
  return declare_symbol(name, SymbolType::endogenous, tex_name, partition_value, {});
}

void
ParsingDriver::var(const vector<tuple<string, string, vector<pair<string, string>>>>& symbol_list,
                   const optional<string>& heterogeneity_dimension, bool log_option)
{
  for (auto& [name, tex_name, partition] : symbol_list)
    {
      int symb_id {[&] {
        if (heterogeneity_dimension)
          try
            {
              return declare_symbol(name, SymbolType::heterogeneousEndogenous, tex_name, partition,
                                    mod_file->heterogeneity_table.getID(*heterogeneity_dimension));
            }
          catch (HeterogeneityTable::UnknownDimensionNameException&)
            {
              error("Unknown heterogeneity dimension: " + *heterogeneity_dimension);
            }
        else
          return declare_endogenous(name, tex_name, partition);
      }()};

      if (log_option)
        mod_file->symbol_table.markWithLogTransform(symb_id);
    }
}

int
ParsingDriver::declare_exogenous(const string& name, const string& tex_name,
                                 const vector<pair<string, string>>& partition_value)
{
  return declare_symbol(name, SymbolType::exogenous, tex_name, partition_value, {});
}

void
ParsingDriver::varexo(
    const vector<tuple<string, string, vector<pair<string, string>>>>& symbol_list,
    const optional<string>& heterogeneity_dimension)
{
  for (auto& [name, tex_name, partition] : symbol_list)
    if (heterogeneity_dimension)
      try
        {
          declare_symbol(name, SymbolType::heterogeneousExogenous, tex_name, partition,
                         mod_file->heterogeneity_table.getID(*heterogeneity_dimension));
        }
      catch (HeterogeneityTable::UnknownDimensionNameException&)
        {
          error("Unknown heterogeneity dimension: " + *heterogeneity_dimension);
        }
    else
      declare_exogenous(name, tex_name, partition);
}

void
ParsingDriver::varexo_det(
    const vector<tuple<string, string, vector<pair<string, string>>>>& symbol_list)
{
  for (auto& [name, tex_name, partition] : symbol_list)
    declare_symbol(name, SymbolType::exogenousDet, tex_name, partition, {});
}

int
ParsingDriver::declare_parameter(const string& name, const string& tex_name,
                                 const vector<pair<string, string>>& partition_value)
{
  return declare_symbol(name, SymbolType::parameter, tex_name, partition_value, {});
}

void
ParsingDriver::parameters(
    const vector<tuple<string, string, vector<pair<string, string>>>>& symbol_list,
    const optional<string>& heterogeneity_dimension)
{
  for (auto& [name, tex_name, partition] : symbol_list)
    {
      if (name == "dsge_prior_weight")
        error("dsge_prior_weight cannot be declared as a parameter. Use the dsge_var option in "
              "the estimation statement instead.");
      if (heterogeneity_dimension)
        try
          {
            declare_symbol(name, SymbolType::heterogeneousParameter, tex_name, partition,
                           mod_file->heterogeneity_table.getID(*heterogeneity_dimension));
          }
        catch (HeterogeneityTable::UnknownDimensionNameException&)
          {
            error("Unknown heterogeneity dimension: " + *heterogeneity_dimension);
          }
      else
        declare_parameter(name, tex_name, partition);
    }
}

void
ParsingDriver::declare_statement_local_variable(const string& name)
{
  if (mod_file->symbol_table.exists(name))
    error("Symbol " + name + " cannot be assigned within a statement "
          + "while being assigned elsewhere in the modfile");
  declare_symbol(name, SymbolType::statementDeclaredVariable, "", {}, {});
}

void
ParsingDriver::set_planner_discount(expr_t value)
{
  planner_discount = value;
}

void
ParsingDriver::set_planner_discount_latex_name(string tex_name)
{
  planner_discount_latex_name = move(tex_name);
}

void
ParsingDriver::end_trend_var(bool log_trend, expr_t growth_factor,
                             const vector<pair<string, string>>& symbol_list)
{
  /* Run detrending engine if trend variables are present, even if unused in
     a var(deflator=…) statement (see #113). */
  mod_file->nonstationary_variables = true;

  vector<int> declared_trend_vars;
  for (auto& [name, tex_name] : symbol_list)
    {
      int symb_id = declare_symbol(name, log_trend ? SymbolType::logTrend : SymbolType::trend,
                                   tex_name, {}, {});
      declared_trend_vars.push_back(symb_id);
    }

  try
    {
      dynamic_model->addTrendVariables(declared_trend_vars, growth_factor);
    }
  catch (DataTree::TrendException& e)
    {
      error("Trend variable " + e.name + " was declared twice.");
    }
  declared_trend_vars.clear();
  reset_data_tree();
}

void
ParsingDriver::predetermined_variables(const vector<string>& symbol_list)
{
  for (auto& name : symbol_list)
    {
      check_symbol_is_endogenous(name);
      int symb_id = mod_file->symbol_table.getID(name);
      mod_file->symbol_table.markPredetermined(symb_id);
    }
}

expr_t
ParsingDriver::add_non_negative_constant(const string& constant)
{
  return data_tree->AddNonNegativeConstant(constant);
}

expr_t
ParsingDriver::add_nan_constant()
{
  return data_tree->NaN;
}

expr_t
ParsingDriver::add_inf_constant()
{
  return data_tree->Infinity;
}

expr_t
ParsingDriver::add_model_variable(const string& name)
{
  check_symbol_existence_in_model_block(name);
  int symb_id;
  try
    {
      symb_id = mod_file->symbol_table.getID(name);
      if (mod_file->symbol_table.getType(symb_id) == SymbolType::excludedVariable)
        error("Variable '" + name
              + "' can no longer be used since it has been excluded by a previous 'model_remove' "
                "or 'var_remove' statement");
    }
  catch (SymbolTable::UnknownSymbolNameException& e)
    {
      if (is_parsing_epilogue())
        error("Variable " + name + " used in the epilogue block but was not declared.");

      /* Declare variable as exogenous to continue parsing. Processing will end
         at end of model block (or planner_objective statement) if nostrict
         option was not passed. */
      symb_id = declare_exogenous(name);
      undeclared_model_vars.insert(name);
    }
  return add_model_variable(symb_id, 0);
}

expr_t
ParsingDriver::declare_or_change_type(SymbolType new_type, const string& name)
{
  if (is_parsing_epilogue())
    error("Declaring or changing type of variables with the vertical bar (|) syntax is not allowed "
          "in epilogue block");

  int symb_id;
  try
    {
      symb_id = mod_file->symbol_table.getID(name);
      mod_file->symbol_table.changeType(symb_id, new_type);

      // remove error messages
      undeclared_model_vars.erase(name);
      erase_if(undeclared_model_variable_errors, [&name](auto& v) { return v.first == name; });
    }
  catch (SymbolTable::UnknownSymbolNameException& e)
    {
      switch (new_type)
        {
        case SymbolType::endogenous:
          symb_id = declare_endogenous(name);
          break;
        case SymbolType::exogenous:
          symb_id = declare_exogenous(name);
          break;
        case SymbolType::parameter:
          symb_id = declare_parameter(name);
          break;
        default:
          error("Type not yet supported");
        }
    }
  return add_model_variable(symb_id, 0);
}

expr_t
ParsingDriver::add_model_variable(int symb_id, int lag)
{
  assert(symb_id >= 0);
  SymbolType type = mod_file->symbol_table.getType(symb_id);

  if (type == SymbolType::modFileLocalVariable)
    error("Variable " + mod_file->symbol_table.getName(symb_id)
          + " not allowed inside model declaration. Its scope is only outside model.");

  if (type == SymbolType::externalFunction)
    error("Symbol " + mod_file->symbol_table.getName(symb_id)
          + " is a function name external to Dynare. It cannot be used like a variable without "
            "input argument inside model.");

  // See dynare#1765
  if (type == SymbolType::exogenousDet && lag != 0)
    error("Exogenous deterministic variable " + mod_file->symbol_table.getName(symb_id)
          + " cannot be given a lead or a lag.");

  if (is_parsing_planner_objective())
    {
      if (lag != 0)
        error("Leads and lags on variables are forbidden in 'planner_objective'.");

      if (type == SymbolType::modelLocalVariable)
        error("Model local variable " + mod_file->symbol_table.getName(symb_id)
              + " cannot be used in 'planner_objective'.");

      if (type == SymbolType::heterogeneousEndogenous || type == SymbolType::heterogeneousExogenous
          || type == SymbolType::heterogeneousParameter)
        error("Symbol '" + mod_file->symbol_table.getName(symb_id)
              + "' cannot be used in 'planner_objective', because it is heterogeneous.");
    }

  if (is_parsing_occbin_constraints())
    {
      if (lag != 0)
        error("Leads and lags on variables are forbidden in 'occbin_constraints'. Note that you "
              "can achieve the same effect by introducing an auxiliary variable in the model.");

      if (type == SymbolType::modelLocalVariable)
        error("Model local variable " + mod_file->symbol_table.getName(symb_id)
              + " cannot be used in 'occbin_constraints'.");

      if (type == SymbolType::exogenous || type == SymbolType::exogenousDet)
        error("Exogenous variable " + mod_file->symbol_table.getName(symb_id)
              + " cannot be used in 'occbin_constraints'.");

      if (type == SymbolType::heterogeneousEndogenous || type == SymbolType::heterogeneousExogenous
          || type == SymbolType::heterogeneousParameter)
        error("Symbol '" + mod_file->symbol_table.getName(symb_id)
              + "' cannot be used in 'occbin_constraints', because it is heterogeneous.");
    }

  if (is_parsing_epilogue())
    {
      if (type == SymbolType::exogenous)
        error("Symbol '" + mod_file->symbol_table.getName(symb_id)
              + "' cannot be used inside the epilogue block, because it is an exogenous variable.");
      if (type == SymbolType::exogenousDet)
        error("Symbol '" + mod_file->symbol_table.getName(symb_id)
              + "' cannot be used inside the epilogue block, because it is an exogenous "
                "deterministic variable.");

      if (type == SymbolType::heterogeneousEndogenous || type == SymbolType::heterogeneousExogenous
          || type == SymbolType::heterogeneousParameter)
        error("Symbol '" + mod_file->symbol_table.getName(symb_id)
              + "' cannot be used in epilogue block, because it is heterogeneous.");
    }
  else
    {
      if (type == SymbolType::epilogue)
        error("Symbol '" + mod_file->symbol_table.getName(symb_id)
              + "' cannot be used outside the epilogue block.");
    }

  // It makes sense to allow a lead/lag on parameters: during steady state calibration, endogenous
  // and parameters can be swapped.
  // NB: we use data_tree here, to avoid a crash in the occbin_constraints or epilogue case
  return data_tree->AddVariable(symb_id, lag);
}

expr_t
ParsingDriver::add_expression_variable(const string& name)
{
  assert(name.find('.') == string::npos); // Ensure this is not namespace-qualified

  // If symbol doesn't exist declare it as a mod file local variable
  if (!is_parsing_shock_paths() && !mod_file->symbol_table.exists(name))
    mod_file->symbol_table.addSymbol(name, SymbolType::modFileLocalVariable);

  // Type checks must come after automatic mod-file local variable declaration!
  if (SymbolType type {mod_file->symbol_table.getType(name)}; is_parsing_shock_paths())
    {
      if (type != SymbolType::parameter)
        error("In the shock_paths block, parameters are the only symbols allowed without a "
              "namespace-qualifier");
    }
  else
    {
      if (type == SymbolType::modelLocalVariable)
        error("Variable " + name
              + " not allowed outside model declaration. Its scope is only inside model.");

      if (type == SymbolType::trend || type == SymbolType::logTrend)
        error("Variable " + name
              + " not allowed outside model declaration, because it is a trend variable.");

      if (type == SymbolType::externalFunction)
        error("Symbol '" + name
              + "' is the name of a MATLAB/Octave function, and cannot be used as a variable.");

      if (type == SymbolType::heterogeneousEndogenous || type == SymbolType::heterogeneousExogenous
          || type == SymbolType::heterogeneousParameter)
        error("Symbol '" + name
              + "' cannot be used outside model declaration, because it is heterogeneous.");

      if (type == SymbolType::epilogue)
        error("Symbol '" + name + "' cannot be used outside the epilogue block.");

      if (type == SymbolType::excludedVariable)
        error("Variable '" + name
              + "' can no longer be used since it has been excluded by a previous 'model_remove' "
                "or 'var_remove' statement");
    }

  int symb_id = mod_file->symbol_table.getID(name);
  return data_tree->AddVariable(symb_id);
}

expr_t
ParsingDriver::add_self_variable(const string& name, expr_t lag)
{
  check_symbol_is_exogenous(name, false);
  int symb_id {mod_file->symbol_table.getID(name)};

  if (is_parsing_shock_paths_controlled)
    error("The syntax self." + name
          + " is not accepted in an 'endogenize' stanza of a 'shock_paths' block");

  try
    {
      auto ilag = lag->matchIntegerConstant();
      if (ilag > 0)
        error("The syntax self." + name + " cannot be used with a lead");
      return data_tree->AddNamespaceQualifiedVariable(
          NamespaceQualifiedVariableNode::NamespaceType::self, symb_id, ilag);
    }
  catch (ExprNode::MatchFailureException&)
    {
      error("Symbol self." + name
            + " is being treated as if it were a function (i.e., passed an argument that is "
              "not an integer).");
    }
}

expr_t
ParsingDriver::add_initval_variable(const string& name)
{
  check_symbol_is_endogenous_or_exogenous(name, false);

  int symb_id {mod_file->symbol_table.getID(name)};

  return data_tree->AddNamespaceQualifiedVariable(
      NamespaceQualifiedVariableNode::NamespaceType::initval, symb_id);
}

expr_t
ParsingDriver::add_prev_variable(const string& name, expr_t lag)
{
  check_symbol_is_exogenous(name, false);

  int symb_id {mod_file->symbol_table.getID(name)};

  /* Check consistency of learnt_in information between block and variable; only possible at the
     preprocessor level when learnt_in at the block level is an integer */
  if (is_parsing_shock_paths() && holds_alternative<int>(shock_paths_learnt_in_period)
      && get<int>(shock_paths_learnt_in_period) == 1)
    error("The syntax prev." + name
          + " is not accepted in a 'shock_paths' block without the 'learnt_in' option or in a "
            "'shock_paths(learnt_in=1)' block");
  if (is_parsing_shock_paths_controlled)
    error("The syntax prev." + name
          + " is not accepted in an 'endogenize' stanza of a 'shock_paths' block");

  if (!lag)
    return data_tree->AddNamespaceQualifiedVariable(
        NamespaceQualifiedVariableNode::NamespaceType::prev, symb_id);
  else
    try
      {
        return data_tree->AddNamespaceQualifiedVariable(
            NamespaceQualifiedVariableNode::NamespaceType::prev, symb_id,
            lag->matchIntegerConstant());
      }
    catch (ExprNode::MatchFailureException&)
      {
        error("Symbol prev." + name
              + " is being treated as if it were a function (i.e., passed an argument that is "
                "not an integer).");
      }
}

expr_t
ParsingDriver::add_database_variable(const string& database_name, const string& symbol_name,
                                     expr_t lag)
{
  if (is_parsing_shock_paths_controlled)
    error("The syntax " + database_name + "." + symbol_name
          + " is not accepted in an 'endogenize' stanza of a 'shock_paths' block");

  int symb_id {[&] {
    try
      {
        return mod_file->symbol_table.getID(symbol_name);
      }
    catch (SymbolTable::UnknownSymbolNameException&)
      {
        return mod_file->symbol_table.addSymbol(symbol_name, SymbolType::databaseVariable);
      }
  }()};

  int database_id {[&] {
    try
      {
        return mod_file->database_table.getID(database_name);
      }
    catch (DatabaseTable::UnknownDatabaseNameException&)
      {
        error("Unknown database: " + database_name
              + ". You may want to declare it via the 'database' command.");
      }
  }()};

  if (!lag)
    return data_tree->AddNamespaceQualifiedVariable(
        NamespaceQualifiedVariableNode::NamespaceType::database, symb_id, 0, database_id);
  else
    try
      {
        return data_tree->AddNamespaceQualifiedVariable(
            NamespaceQualifiedVariableNode::NamespaceType::database, symb_id,
            lag->matchIntegerConstant(), database_id);
      }
    catch (ExprNode::MatchFailureException&)
      {
        error("Symbol " + database_name + "." + symbol_name
              + " is being treated as if it were a function (i.e., passed an argument that is "
                "not an integer).");
      }
}

expr_t
ParsingDriver::add_learnt_in_variable(const variant<int, string>& learnt_in_period,
                                      const string& name, expr_t lag)
{
  check_symbol_is_exogenous(name, false);
  int symb_id {mod_file->symbol_table.getID(name)};

  if (holds_alternative<int>(learnt_in_period) && get<int>(learnt_in_period) < 1)
    error("The syntax learnt_in(" + to_string(get<int>(learnt_in_period)) + ")." + name
          + " is not accepted");

  /* Check consistency of learnt_in information between block and variable; only possible at the
     preprocessor level when both are integers */
  if (is_parsing_shock_paths() && holds_alternative<int>(shock_paths_learnt_in_period)
      && holds_alternative<int>(learnt_in_period)
      && get<int>(shock_paths_learnt_in_period) <= get<int>(learnt_in_period))
    error("The syntax learnt_in(" + to_string(get<int>(learnt_in_period)) + ")." + name
          + " is not accepted in a 'shock_paths' block without the 'learnt_in' option or in a "
            "'shock_paths(learnt_in="
          + to_string(get<int>(shock_paths_learnt_in_period)) + ")' block");

  if (is_parsing_shock_paths_controlled)
    error("The syntax learnt_in("
          + (holds_alternative<int>(learnt_in_period) ? to_string(get<int>(learnt_in_period))
                                                      : get<string>(learnt_in_period))
          + ")." + name + " is not accepted in an 'endogenize' stanza of a 'shock_paths' block");

  if (!lag)
    return data_tree->AddNamespaceQualifiedVariable(
        NamespaceQualifiedVariableNode::NamespaceType::learnt_in, symb_id, 0, -1, learnt_in_period);
  else
    try
      {
        return data_tree->AddNamespaceQualifiedVariable(
            NamespaceQualifiedVariableNode::NamespaceType::learnt_in, symb_id,
            lag->matchIntegerConstant(), -1, learnt_in_period);
      }
    catch (ExprNode::MatchFailureException&)
      {
        error("Symbol learnt_in("
              + (holds_alternative<int>(learnt_in_period) ? to_string(get<int>(learnt_in_period))
                                                          : get<string>(learnt_in_period))
              + ")." + name
              + " is being treated as if it were a function (i.e., passed an argument that is "
                "not an integer).");
      }
}

void
ParsingDriver::end_nonstationary_var(
    bool log_deflator, expr_t deflator,
    const vector<tuple<string, string, vector<pair<string, string>>>>& symbol_list, bool log_option)
{
  mod_file->nonstationary_variables = true;

  vector<int> declared_nonstationary_vars;
  for (auto& [name, tex_name, partition] : symbol_list)
    {
      int symb_id = declare_endogenous(name, tex_name, partition);
      declared_nonstationary_vars.push_back(symb_id);
      if (log_option)
        mod_file->symbol_table.markWithLogTransform(symb_id);
    }

  try
    {
      dynamic_model->addNonstationaryVariables(declared_nonstationary_vars, log_deflator, deflator);
    }
  catch (DataTree::TrendException& e)
    {
      error("Variable " + e.name + " was listed more than once as following a trend.");
    }

  set<int> r;
  deflator->collectVariables(SymbolType::endogenous, r);
  for (int it : r)
    if (dynamic_model->isNonstationary(it))
      error("The deflator contains a non-stationary endogenous variable. This is not allowed. "
            "Please use only stationary endogenous and/or {log_}trend_vars.");

  declared_nonstationary_vars.clear();
  reset_data_tree();
}

void
ParsingDriver::dsample(const string& arg1)
{
  int arg1_val = stoi(arg1);
  mod_file->addStatement(make_unique<DsampleStatement>(arg1_val));
}

void
ParsingDriver::dsample(const string& arg1, const string& arg2)
{
  int arg1_val = stoi(arg1);
  int arg2_val = stoi(arg2);
  mod_file->addStatement(make_unique<DsampleStatement>(arg1_val, arg2_val));
}

void
ParsingDriver::init_param(const string& name, expr_t rhs)
{
  check_symbol_is_parameter(name);
  int symb_id = mod_file->symbol_table.getID(name);
  mod_file->addStatement(make_unique<InitParamStatement>(symb_id, rhs, mod_file->symbol_table));
}

void
ParsingDriver::init_val(const string& name, expr_t rhs)
{
  if (nostrict && !mod_file->symbol_table.exists(name))
    {
      warning("discarding '" + name + "' as it was not recognized in the initval statement");
      return;
    }

  check_symbol_is_endogenous_or_exogenous(name, true);
  int symb_id = mod_file->symbol_table.getID(name);
  init_values.emplace_back(symb_id, rhs);
}

void
ParsingDriver::initval_file()
{
  mod_file->addStatement(make_unique<InitvalFileStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::end_val(EndValLearntInStatement::LearntEndValType type, const string& name,
                       expr_t rhs)
{
  if (nostrict && !mod_file->symbol_table.exists(name))
    {
      warning("discarding '" + name + "' as it was not recognized in the endval statement");
      return;
    }

  check_symbol_is_endogenous_or_exogenous(name, false);
  int symb_id = mod_file->symbol_table.getID(name);
  end_values.emplace_back(type, symb_id, rhs);
}

void
ParsingDriver::hist_val(const string& name, const string& lag, expr_t rhs)
{
  if (nostrict && !mod_file->symbol_table.exists(name))
    {
      warning("discarding '" + name + "' as it was not recognized in the histval block");
      return;
    }

  check_symbol_is_endogenous_or_exogenous(name, true);
  int symb_id = mod_file->symbol_table.getID(name);

  int ilag = stoi(lag);
  if (ilag > 0)
    error("histval: the lag on " + name + " should be less than or equal to 0");

  pair key {symb_id, ilag};

  if (hist_values.contains(key))
    error("hist_val: (" + name + ", " + lag + ") declared twice");

  hist_values[move(key)] = rhs;
}

void
ParsingDriver::homotopy_val(const string& name, expr_t val1, expr_t val2)
{
  check_symbol_existence(name);
  int symb_id = mod_file->symbol_table.getID(name);
  SymbolType type = mod_file->symbol_table.getType(symb_id);

  if (type != SymbolType::parameter && type != SymbolType::exogenous
      && type != SymbolType::exogenousDet)
    error("homotopy_val: " + name + " should be a parameter or exogenous variable");

  homotopy_values.emplace_back(symb_id, val1, val2);
}

void
ParsingDriver::end_generate_irfs()
{
  mod_file->addStatement(make_unique<GenerateIRFsStatement>(
      move(options_list), move(generate_irf_names), move(generate_irf_elements)));

  generate_irf_elements.clear();
  generate_irf_names.clear();
  options_list.clear();
}

void
ParsingDriver::add_generate_irfs_element(string name)
{
  for (const auto& it : generate_irf_names)
    if (it == name)
      error("Names in the generate_irfs block must be unique but you entered '" + name
            + "' more than once.");

  generate_irf_names.push_back(move(name));
  generate_irf_elements.push_back(generate_irf_exos);

  generate_irf_exos.clear();
}

void
ParsingDriver::add_generate_irfs_exog_element(string exo, const string& value)
{
  check_symbol_is_exogenous(exo, false);
  if (generate_irf_exos.contains(exo))
    error("You have set the exogenous variable " + exo + " twice.");

  generate_irf_exos[move(exo)] = stod(value);
}

void
ParsingDriver::forecast(vector<string> symbol_list)
{
  mod_file->addStatement(make_unique<ForecastStatement>(move(symbol_list), move(options_list),
                                                        mod_file->symbol_table));
  options_list.clear();
}

void
ParsingDriver::use_dll()
{
  mod_file->use_dll = true;
}

void
ParsingDriver::block()
{
  mod_file->block = true;
}

void
ParsingDriver::no_static()
{
  mod_file->no_static = true;
}

void
ParsingDriver::bytecode()
{
  mod_file->bytecode = true;
}

void
ParsingDriver::differentiate_forward_vars_all()
{
  mod_file->differentiate_forward_vars = true;
}

void
ParsingDriver::differentiate_forward_vars_some(vector<string> symbol_list)
{
  mod_file->differentiate_forward_vars = true;
  mod_file->differentiate_forward_vars_subset = move(symbol_list);
  for (auto& it : mod_file->differentiate_forward_vars_subset)
    check_symbol_is_endogenous(it);
}

void
ParsingDriver::cutoff(const string& value)
{
  double val = stod(value);
  mod_file->dynamic_model.cutoff = val;
}

void
ParsingDriver::mfs(const string& value)
{
  int val = stoi(value);
  mod_file->dynamic_model.setMFS(val);
}

void
ParsingDriver::static_mfs(const string& value)
{
  int val = stoi(value);
  mod_file->dynamic_model.setStaticMFS(val);
}

void
ParsingDriver::compilation_setup_substitute_flags(const string& flags)
{
  mod_file->dynamic_model.user_set_subst_flags = flags;
}

void
ParsingDriver::compilation_setup_add_flags(const string& flags)
{
  mod_file->dynamic_model.user_set_add_flags = flags;
}

void
ParsingDriver::compilation_setup_substitute_libs(const string& libs)
{
  mod_file->dynamic_model.user_set_subst_libs = libs;
}

void
ParsingDriver::compilation_setup_add_libs(const string& libs)
{
  mod_file->dynamic_model.user_set_add_libs = libs;
}

void
ParsingDriver::compilation_setup_compiler(const string& compiler)
{
  mod_file->dynamic_model.user_set_compiler = compiler;
}

void
ParsingDriver::balanced_growth_test_tol(const string& value)
{
  mod_file->dynamic_model.balanced_growth_test_tol = stod(value);
}

void
ParsingDriver::end_initval(bool all_values_required)
{
  mod_file->addStatement(make_unique<InitValStatement>(move(init_values), mod_file->symbol_table,
                                                       all_values_required));
  init_values.clear();
}

void
ParsingDriver::end_endval(bool all_values_required)
{
  InitOrEndValStatement::init_values_t end_values_new;
  for (const auto& [type, symb_id, value] : end_values)
    switch (type)
      {
      case EndValLearntInStatement::LearntEndValType::level:
        end_values_new.emplace_back(symb_id, value);
        break;
      case EndValLearntInStatement::LearntEndValType::add:
        error("endval: '" + mod_file->symbol_table.getName(symb_id)
              + " += ...' line not allowed unless 'learnt_in' option with value >1 is passed");
      case EndValLearntInStatement::LearntEndValType::multiply:
        error("endval: '" + mod_file->symbol_table.getName(symb_id)
              + " *= ...' line not allowed unless 'learnt_in' option with value >1 is passed");
      }

  mod_file->addStatement(make_unique<EndValStatement>(move(end_values_new), mod_file->symbol_table,
                                                      all_values_required));
  end_values.clear();
}

void
ParsingDriver::end_endval_learnt_in(variant<int, string> learnt_in_period)
{
  if (holds_alternative<int>(learnt_in_period))
    {
      int learnt_in_period_int = get<int>(learnt_in_period);
      if (learnt_in_period_int < 1)
        error("endval: value '" + to_string(learnt_in_period_int)
              + "' is not allowed for 'learnt_in' option");
      if (learnt_in_period_int == 1)
        {
          end_endval(false);
          return;
        }
    }
  for (const auto& [type, symb_id, value] : end_values)
    if (mod_file->symbol_table.getType(symb_id) != SymbolType::exogenous)
      error("endval(learnt_in=...): " + mod_file->symbol_table.getName(symb_id)
            + " is not an exogenous variable");
  mod_file->addStatement(make_unique<EndValLearntInStatement>(
      move(learnt_in_period), move(end_values), mod_file->symbol_table));
  end_values.clear();
}

void
ParsingDriver::end_histval(bool all_values_required)
{
  mod_file->addStatement(make_unique<HistValStatement>(move(hist_values), mod_file->symbol_table,
                                                       all_values_required));
  hist_values.clear();
}

void
ParsingDriver::end_homotopy(bool from_initval_to_endval)
{
  mod_file->addStatement(make_unique<HomotopySetupStatement>(
      from_initval_to_endval, move(homotopy_values), mod_file->symbol_table));
  homotopy_values.clear();
}

void
ParsingDriver::begin_epilogue()
{
  set_current_data_tree(&mod_file->epilogue);
}

void
ParsingDriver::end_epilogue()
{
  reset_data_tree();
}

void
ParsingDriver::add_epilogue_variable(const string& name)
{
  declare_symbol(name, SymbolType::epilogue, "", {}, {});
}

void
ParsingDriver::add_epilogue_equal(const string& name, expr_t expr)
{
  mod_file->epilogue.addDefinition(mod_file->symbol_table.getID(name), expr);
}

void
ParsingDriver::begin_model()
{
  set_current_data_tree(&mod_file->dynamic_model);
}

void
ParsingDriver::begin_heterogeneous_model(const string& heterogeneity_dimension)
{
  int het_dim_id {[&] {
    try
      {
        return mod_file->heterogeneity_table.getID(heterogeneity_dimension);
      }
    catch (HeterogeneityTable::UnknownDimensionNameException&)
      {
        error("Unknown heterogeneity dimension: " + heterogeneity_dimension);
      }
  }()};
  set_current_data_tree(&mod_file->heterogeneous_models.at(het_dim_id));
}

void
ParsingDriver::end_model()
{
  bool exit_after_write = false;

  if (undeclared_model_variable_errors.size() > 0)
    for (auto& it : undeclared_model_variable_errors)
      {
        if (nostrict)
          warning(it.second);
        else
          {
            exit_after_write = true;
            cerr << it.second << '\n';
          }
      }
  undeclared_model_variable_errors.clear();

  if (exit_after_write)
    exit(EXIT_FAILURE);

  reset_data_tree();
}

void
ParsingDriver::end_shocks(bool overwrite)
{
  mod_file->addStatement(make_unique<ShocksStatement>(
      overwrite, move(det_shocks), move(var_shocks), move(std_shocks), move(covar_shocks),
      move(corr_shocks), move(skew_shocks), mod_file->symbol_table));
  det_shocks.clear();
  if (!learnt_shocks_add.empty())
    error("shocks: 'add' keyword not allowed unless 'learnt_in' option with value >1 is passed");
  if (!learnt_shocks_multiply.empty())
    error(
        "shocks: 'multiply' keyword not allowed unless 'learnt_in' option with value >1 is passed");
  var_shocks.clear();
  std_shocks.clear();
  covar_shocks.clear();
  corr_shocks.clear();
  skew_shocks.clear();
}

void
ParsingDriver::end_heterogeneous_shocks(const string& heterogeneity_dimension, bool overwrite)
{
  int het_dim_id {[&] {
    try
      {
        return mod_file->heterogeneity_table.getID(heterogeneity_dimension);
      }
    catch (HeterogeneityTable::UnknownDimensionNameException&)
      {
        error("Unknown heterogeneity dimension: " + heterogeneity_dimension);
      }
  }()};
  mod_file->addStatement(make_unique<HeterogeneousShocksStatement>(
      het_dim_id, overwrite, move(var_shocks), move(std_shocks), move(covar_shocks),
      move(corr_shocks), mod_file->symbol_table, mod_file->heterogeneity_table));
  var_shocks.clear();
  std_shocks.clear();
  covar_shocks.clear();
  corr_shocks.clear();
}

void
ParsingDriver::end_mshocks(bool overwrite, bool relative_to_initval)
{
  mod_file->addStatement(make_unique<MShocksStatement>(overwrite, relative_to_initval,
                                                       move(det_shocks), mod_file->symbol_table));
  det_shocks.clear();
  if (!learnt_shocks_add.empty())
    error("mshocks: 'add' keyword not allowed");
  if (!learnt_shocks_multiply.empty())
    error("mshocks: 'multiply' keyword not allowed");
}

void
ParsingDriver::end_shocks_surprise(bool overwrite)
{
  if (ranges::any_of(views::values(det_shocks), [](auto& v) {
        return ranges::any_of(views::keys(v),
                              [](auto& p) { return !holds_alternative<pair<int, int>>(p); });
      }))
    error("shocks(surprise): dates are not allowed in the 'periods' keyword");
  mod_file->addStatement(
      make_unique<ShocksSurpriseStatement>(overwrite, move(det_shocks), mod_file->symbol_table));
  det_shocks.clear();
  if (!learnt_shocks_add.empty())
    error("shocks(surprise): 'add' keyword not allowed");
  if (!learnt_shocks_multiply.empty())
    error("shocks(surprise): 'multiply' keyword not allowed");
}

void
ParsingDriver::end_shocks_learnt_in(variant<int, string> learnt_in_period, bool overwrite)
{
  if (holds_alternative<int>(learnt_in_period))
    {
      int learnt_in_period_int = get<int>(learnt_in_period);
      if (learnt_in_period_int < 1)
        error("shocks: value '" + to_string(learnt_in_period_int)
              + "' is not allowed for 'learnt_in' option");
      if (learnt_in_period_int == 1)
        {
          end_shocks(overwrite);
          return;
        }
      for (auto& storage : {det_shocks, learnt_shocks_add, learnt_shocks_multiply})
        for (auto& [symb_id, vals] : storage)
          for (const auto& [period_range, expr] : vals)
            if (holds_alternative<pair<int, int>>(period_range))
              if (int period1 = get<pair<int, int>>(period_range).first;
                  period1 < learnt_in_period_int)
                error("shocks: for variable " + mod_file->symbol_table.getName(symb_id)
                      + ", shock period (" + to_string(period1)
                      + ") is earlier than the period in which the shock is learnt ("
                      + to_string(learnt_in_period_int) + ")");
    }

  // Aggregate the three types of shocks
  ShocksLearntInStatement::learnt_shocks_t learnt_shocks;
  for (const auto& [id, v] : det_shocks)
    {
      vector<tuple<ShocksLearntInStatement::LearntShockType,
                   AbstractShocksStatement::period_range_t, expr_t>>
          v2;
      for (const auto& [period_range, value] : v)
        v2.emplace_back(ShocksLearntInStatement::LearntShockType::level, period_range, value);
      learnt_shocks[id] = v2;
    }
  for (const auto& [id, v] : learnt_shocks_add)
    {
      vector<tuple<ShocksLearntInStatement::LearntShockType,
                   AbstractShocksStatement::period_range_t, expr_t>>
          v2;
      for (const auto& [period_range, value] : v)
        v2.emplace_back(ShocksLearntInStatement::LearntShockType::add, period_range, value);
      learnt_shocks[id] = v2;
    }
  for (const auto& [id, v] : learnt_shocks_multiply)
    {
      vector<tuple<ShocksLearntInStatement::LearntShockType,
                   AbstractShocksStatement::period_range_t, expr_t>>
          v2;
      for (const auto& [period_range, value] : v)
        v2.emplace_back(ShocksLearntInStatement::LearntShockType::multiply, period_range, value);
      learnt_shocks[id] = v2;
    }

  mod_file->addStatement(make_unique<ShocksLearntInStatement>(
      move(learnt_in_period), overwrite, move(learnt_shocks), mod_file->symbol_table));
  det_shocks.clear();
  learnt_shocks_add.clear();
  learnt_shocks_multiply.clear();
}

void
ParsingDriver::end_mshocks_learnt_in(variant<int, string> learnt_in_period, bool overwrite,
                                     bool relative_to_initval)
{
  if (holds_alternative<int>(learnt_in_period))
    {
      int learnt_in_period_int = get<int>(learnt_in_period);
      if (learnt_in_period_int < 1)
        error("mshocks: value '" + to_string(learnt_in_period_int)
              + "' is not allowed for 'learnt_in' option");
      if (learnt_in_period_int == 1)
        {
          end_mshocks(overwrite, relative_to_initval);
          return;
        }
      for (auto& [symb_id, vals] : det_shocks)
        for (const auto& [period_range, expr] : vals)
          if (holds_alternative<pair<int, int>>(period_range))
            if (int period1 = get<pair<int, int>>(period_range).first;
                period1 < learnt_in_period_int)
              error("mshocks: for variable " + mod_file->symbol_table.getName(symb_id)
                    + ", shock period (" + to_string(period1)
                    + ") is earlier than the period in which the shock is learnt ("
                    + to_string(learnt_in_period_int) + ")");
    }

  ShocksLearntInStatement::learnt_shocks_t learnt_shocks;
  const auto type {relative_to_initval
                       ? ShocksLearntInStatement::LearntShockType::multiplyInitialSteadyState
                       : ShocksLearntInStatement::LearntShockType::multiplySteadyState};
  for (const auto& [id, v] : det_shocks)
    {
      vector<tuple<ShocksLearntInStatement::LearntShockType,
                   AbstractShocksStatement::period_range_t, expr_t>>
          v2;
      for (const auto& [period_range, value] : v)
        v2.emplace_back(type, period_range, value);
      learnt_shocks[id] = v2;
    }

  mod_file->addStatement(make_unique<ShocksLearntInStatement>(
      move(learnt_in_period), overwrite, move(learnt_shocks), mod_file->symbol_table));
  det_shocks.clear();
  if (!learnt_shocks_add.empty())
    error("mshocks: 'add' keyword not allowed");
  if (!learnt_shocks_multiply.empty())
    error("mshocks: 'multiply' keyword not allowed");
}

void
ParsingDriver::end_heteroskedastic_shocks(bool overwrite)
{
  mod_file->addStatement(make_unique<HeteroskedasticShocksStatement>(
      overwrite, move(heteroskedastic_shocks_exo_values), move(heteroskedastic_shocks_exo_scales),
      move(heteroskedastic_shocks_endo_values), move(heteroskedastic_shocks_endo_scales),
      mod_file->symbol_table));
  heteroskedastic_shocks_exo_values.clear();
  heteroskedastic_shocks_exo_scales.clear();
  heteroskedastic_shocks_endo_values.clear();
  heteroskedastic_shocks_endo_scales.clear();
}

void
ParsingDriver::add_det_shock(const string& var,
                             const vector<AbstractShocksStatement::period_range_t>& periods,
                             const vector<expr_t>& values, DetShockType type)
{
  switch (type)
    {
    case DetShockType::conditional_forecast:
      check_symbol_is_endogenous(var);
      break;
    case DetShockType::standard:
      // Allow exo_det, for stochastic context
      check_symbol_is_exogenous(var, true);
      break;
    case DetShockType::add:
    case DetShockType::multiply:
      check_symbol_is_exogenous(var, false);
      break;
    }

  int symb_id = mod_file->symbol_table.getID(var);

  if (det_shocks.contains(symb_id) || learnt_shocks_add.contains(symb_id)
      || learnt_shocks_multiply.contains(symb_id))
    error("shocks/conditional_forecast_paths: variable " + var + " declared twice");

  if (periods.size() != values.size())
    error("shocks/conditional_forecast_paths: variable " + var
          + ": number of periods is different from number of shock values");

  vector<pair<AbstractShocksStatement::period_range_t, expr_t>> v;

  v.reserve(periods.size());
  for (size_t i = 0; i < periods.size(); i++)
    v.emplace_back(periods[i], values[i]);

  switch (type)
    {
    case DetShockType::standard:
    case DetShockType::conditional_forecast:
      det_shocks[symb_id] = v;
      break;
    case DetShockType::add:
      learnt_shocks_add[symb_id] = v;
      break;
    case DetShockType::multiply:
      learnt_shocks_multiply[symb_id] = v;
      break;
    }
}

void
ParsingDriver::add_shock_paths_exo_elem(const string& var,
                                        const vector<ShockPathsStatement::period_range_t>& periods,
                                        const vector<expr_t>& values)
{
  check_symbol_is_exogenous(var, false);

  int symb_id = mod_file->symbol_table.getID(var);

  if (periods.size() != values.size())
    error("shock_paths: variable " + var
          + ": number of periods is different from number of shock values");

  vector<pair<ShockPathsStatement::period_range_t, expr_t>> v;

  for (size_t i {0}; i < periods.size(); i++)
    {
      /* Test whether periods and lags are consistent. This can only be done at the preprocessor
         level when the period is an integer (not date or “end”) and with lags (not leads) */
      if (auto p = periods[i].get_first();
          holds_alternative<int>(p) && get<int>(p) <= values[i]->maxLag())
        error("shock_paths: a lag of " + to_string(values[i]->maxLag())
              + " is not allowed at period " + to_string(get<int>(p)));

      v.emplace_back(periods[i], values[i]);
    }

  shock_paths_exo.emplace_back(symb_id, move(v));
}

void
ParsingDriver::add_heteroskedastic_shock(
    const string& var, const vector<AbstractShocksStatement::period_range_t>& periods,
    const vector<expr_t>& values, bool scales)
{
  check_symbol_is_endogenous_or_exogenous(var, false);
  int symb_id = mod_file->symbol_table.getID(var);

  if (periods.size() != values.size())
    error("heteroskedastic_shocks: variable " + var
          + ": number of periods is different from number of shock values");

  vector<pair<AbstractShocksStatement::period_range_t, expr_t>> v;
  v.reserve(periods.size());
  for (size_t i = 0; i < periods.size(); i++)
    v.emplace_back(periods[i], values[i]);

  if (mod_file->symbol_table.getType(var) == SymbolType::exogenous)
    {
      // For exogenous variables: Q_scale
      if ((!scales && heteroskedastic_shocks_exo_values.contains(symb_id))
          || (scales && heteroskedastic_shocks_exo_scales.contains(symb_id)))
        error("heteroskedastic_shocks: variable " + var + " declared twice");
      if (scales)
        heteroskedastic_shocks_exo_scales[symb_id] = v;
      else
        heteroskedastic_shocks_exo_values[symb_id] = v;
    }
  else
    {
      // For endogenous variables: H_scale
      if ((!scales && heteroskedastic_shocks_endo_values.contains(symb_id))
          || (scales && heteroskedastic_shocks_endo_scales.contains(symb_id)))
        error("heteroskedastic_shocks: variable " + var + " declared twice");
      if (scales)
        heteroskedastic_shocks_endo_scales[symb_id] = v;
      else
        heteroskedastic_shocks_endo_values[symb_id] = v;
    }
}

void
ParsingDriver::add_stderr_shock(const string& var, expr_t value)
{
  if (nostrict && !mod_file->symbol_table.exists(var))
    {
      warning("discarding shocks block declaration of the standard error of '" + var
              + "' as it was not declared");
      return;
    }

  check_symbol_existence(var);
  int symb_id = mod_file->symbol_table.getID(var);

  if (var_shocks.contains(symb_id) || std_shocks.contains(symb_id))
    error("shocks: variance or stderr of shock on " + var + " declared twice");

  std_shocks[symb_id] = value;
}

void
ParsingDriver::add_var_shock(const string& var, expr_t value)
{
  if (nostrict && !mod_file->symbol_table.exists(var))
    {
      warning("discarding shocks block declaration of the variance of '" + var
              + "' as it was not declared");
      return;
    }

  check_symbol_existence(var);
  int symb_id = mod_file->symbol_table.getID(var);

  if (var_shocks.contains(symb_id) || std_shocks.contains(symb_id))
    error("shocks: variance or stderr of shock on " + var + " declared twice");

  var_shocks[symb_id] = value;
}

void
ParsingDriver::add_covar_shock(const string& var1, const string& var2, expr_t value)
{
  if (nostrict && (!mod_file->symbol_table.exists(var1) || !mod_file->symbol_table.exists(var2)))
    {
      warning("discarding shocks block declaration of the covariance of '" + var1 + "' and '" + var2
              + "' as at least one was not declared");
      return;
    }

  check_symbol_existence(var1);
  check_symbol_existence(var2);
  int symb_id1 = mod_file->symbol_table.getID(var1);
  int symb_id2 = mod_file->symbol_table.getID(var2);

  pair key {symb_id1, symb_id2}, key_inv {symb_id2, symb_id1};

  if (covar_shocks.contains(key) || covar_shocks.contains(key_inv) || corr_shocks.contains(key)
      || corr_shocks.contains(key_inv))
    error("shocks: covariance or correlation shock on variable pair (" + var1 + ", " + var2
          + ") declared twice");

  covar_shocks[key] = value;
}

void
ParsingDriver::add_correl_shock(const string& var1, const string& var2, expr_t value)
{
  if (nostrict && (!mod_file->symbol_table.exists(var1) || !mod_file->symbol_table.exists(var2)))
    {
      warning("discarding shocks block declaration of the correlation of '" + var1 + "' and '"
              + var2 + "' as at least one was not declared");
      return;
    }

  check_symbol_existence(var1);
  check_symbol_existence(var2);
  int symb_id1 = mod_file->symbol_table.getID(var1);
  int symb_id2 = mod_file->symbol_table.getID(var2);

  pair key {symb_id1, symb_id2}, key_inv {symb_id2, symb_id1};

  if (covar_shocks.contains(key) || covar_shocks.contains(key_inv) || corr_shocks.contains(key)
      || corr_shocks.contains(key_inv))
    error("shocks: covariance or correlation shock on variable pair (" + var1 + ", " + var2
          + ") declared twice");

  corr_shocks[key] = value;
}

void
ParsingDriver::add_skew_single_shock(const string& var, expr_t value)
{
  // Single shock skewness: store as (i, i, i)
  if (nostrict && !mod_file->symbol_table.exists(var))
    {
      warning("discarding shocks block declaration of the skewness of '" + var
              + "' as it was not declared");
      return;
    }

  check_symbol_existence(var);
  int symb_id = mod_file->symbol_table.getID(var);

  tuple key {symb_id, symb_id, symb_id};

  if (skew_shocks.contains(key))
    error("shocks: skewness of " + var + " declared twice");

  skew_shocks[key] = value;
}

void
ParsingDriver::add_skew_triple_shock(const string& var1, const string& var2, const string& var3,
                                     expr_t value)
{
  // Three-shock co-skewness
  if (nostrict
      && (!mod_file->symbol_table.exists(var1) || !mod_file->symbol_table.exists(var2)
          || !mod_file->symbol_table.exists(var3)))
    {
      warning("discarding shocks block declaration of the co-skewness of ('" + var1 + "', '" + var2
              + "', '" + var3 + "') as at least one was not declared");
      return;
    }

  check_symbol_existence(var1);
  check_symbol_existence(var2);
  check_symbol_existence(var3);
  int symb_id1 = mod_file->symbol_table.getID(var1);
  int symb_id2 = mod_file->symbol_table.getID(var2);
  int symb_id3 = mod_file->symbol_table.getID(var3);

  // Check all permutations for duplicates
  if (skew_shocks.contains({symb_id1, symb_id2, symb_id3})
      || skew_shocks.contains({symb_id1, symb_id3, symb_id2})
      || skew_shocks.contains({symb_id2, symb_id1, symb_id3})
      || skew_shocks.contains({symb_id2, symb_id3, symb_id1})
      || skew_shocks.contains({symb_id3, symb_id1, symb_id2})
      || skew_shocks.contains({symb_id3, symb_id2, symb_id1}))
    error("shocks: co-skewness of (" + var1 + ", " + var2 + ", " + var3 + ") declared twice");

  skew_shocks[{symb_id1, symb_id2, symb_id3}] = value;
}

void
ParsingDriver::begin_svar_identification()
{
  svar_upper_cholesky = false;
  svar_lower_cholesky = false;
  svar_constants_exclusion = false;
}

void
ParsingDriver::end_svar_identification()
{
  mod_file->addStatement(make_unique<SvarIdentificationStatement>(
      move(svar_ident_restrictions), svar_upper_cholesky, svar_lower_cholesky,
      svar_constants_exclusion, mod_file->symbol_table));
  svar_equation_restrictions.clear();
  svar_ident_restrictions.clear();
  svar_Qi_restriction_nbr.clear();
  svar_Ri_restriction_nbr.clear();
}

void
ParsingDriver::combine_lag_and_restriction(const string& lag)
{
  int current_lag = stoi(lag);

  for (const auto& it : svar_ident_restrictions)
    if (it.lag == current_lag)
      error("lag " + lag + " used more than once.");

  for (const auto& it : svar_equation_restrictions)
    for (auto it1 : it.second)
      {
        SvarIdentificationStatement::svar_identification_restriction new_restriction;
        new_restriction.equation = it.first;
        if (current_lag > 0)
          new_restriction.restriction_nbr = ++svar_Ri_restriction_nbr[it.first];
        else
          new_restriction.restriction_nbr = ++svar_Qi_restriction_nbr[it.first];
        new_restriction.lag = current_lag;
        new_restriction.variable = it1;
        new_restriction.value = data_tree->One;
        svar_ident_restrictions.push_back(new_restriction);
      }

  svar_upper_cholesky = false;
  svar_lower_cholesky = false;
  svar_equation_restrictions.clear();
}

void
ParsingDriver::add_restriction_in_equation(const string& equation,
                                           const vector<string>& symbol_list)
{
  int eqn = stoi(equation);
  if (eqn < 1)
    error("equation numbers must be greater than or equal to 1.");

  if (svar_equation_restrictions.contains(eqn))
    error("equation number " + equation + " referenced more than once under a single lag.");

  vector<int> svar_restriction_symbols;
  for (auto& name : symbol_list)
    {
      check_symbol_existence(name);
      int symb_id = mod_file->symbol_table.getID(name);

      for (const auto& viit : svar_restriction_symbols)
        if (symb_id == viit)
          error(name + " restriction added twice.");

      svar_restriction_symbols.push_back(symb_id);
    }
  svar_equation_restrictions[eqn] = svar_restriction_symbols;
}

void
ParsingDriver::add_restriction_equation_nbr(const string& eq_nbr)
{
  svar_equation_nbr = stoi(eq_nbr);
  svar_left_handside = true;
  // reinitialize restriction type that must be set from the first restriction element
  svar_restriction_type = SvarRestrictionType::NOT_SET;
}

void
ParsingDriver::add_restriction_equal()
{
  if (svar_left_handside)
    svar_left_handside = false;
  else
    error("svar_identification: there are more than one EQUAL sign in a restriction equation");
}

void
ParsingDriver::add_positive_restriction_element(expr_t value, const string& variable,
                                                const string& lag)
{
  // if the expression is not on the left-hand side, change its sign
  if (!svar_left_handside)
    value = add_uminus(value);

  add_restriction_element(value, variable, lag);
}

void
ParsingDriver::add_positive_restriction_element(const string& variable, const string& lag)
{
  expr_t value(data_tree->One);

  // if the expression is not on the left-hand side, change its sign
  if (!svar_left_handside)
    value = add_uminus(value);

  add_restriction_element(value, variable, lag);
}

void
ParsingDriver::add_negative_restriction_element(expr_t value, const string& variable,
                                                const string& lag)
{
  // if the expression is on the left-hand side, change its sign
  if (svar_left_handside)
    value = add_uminus(value);

  add_restriction_element(value, variable, lag);
}

void
ParsingDriver::add_negative_restriction_element(const string& variable, const string& lag)
{
  expr_t value(data_tree->One);

  // if the expression is on the left-hand side, change its sign
  if (svar_left_handside)
    value = add_uminus(value);

  add_restriction_element(value, variable, lag);
}

void
ParsingDriver::add_restriction_element(expr_t value, const string& variable, const string& lag)
{
  check_symbol_existence(variable);
  int symb_id = mod_file->symbol_table.getID(variable);

  int current_lag = stoi(lag);
  if (svar_restriction_type == SvarRestrictionType::NOT_SET)
    {
      if (current_lag == 0)
        {
          svar_restriction_type = SvarRestrictionType::Qi_TYPE;
          ++svar_Qi_restriction_nbr[svar_equation_nbr];
        }
      else
        {
          svar_restriction_type = SvarRestrictionType::Ri_TYPE;
          ++svar_Ri_restriction_nbr[svar_equation_nbr];
        }
    }
  else
    {
      if ((svar_restriction_type == SvarRestrictionType::Qi_TYPE && current_lag > 0)
          || (svar_restriction_type == SvarRestrictionType::Ri_TYPE && current_lag == 0))
        error(
            "SVAR_IDENTIFICATION: a single restrictions must affect either Qi or Ri, but not both");
    }
  SvarIdentificationStatement::svar_identification_restriction new_restriction;
  new_restriction.equation = svar_equation_nbr;
  if (current_lag > 0)
    new_restriction.restriction_nbr = svar_Ri_restriction_nbr[svar_equation_nbr];
  else
    new_restriction.restriction_nbr = svar_Qi_restriction_nbr[svar_equation_nbr];
  new_restriction.lag = current_lag;
  new_restriction.variable = symb_id;
  new_restriction.value = value;

  svar_ident_restrictions.push_back(new_restriction);
}

void
ParsingDriver::check_restriction_expression_constant(expr_t value)
{
  if (value->eval({}) != 0)
    error("SVAR_INDENTIFICATION restrictions must be homogenous");
}

void
ParsingDriver::add_upper_cholesky()
{
  svar_upper_cholesky = true;
}

void
ParsingDriver::add_lower_cholesky()
{
  svar_lower_cholesky = true;
}

void
ParsingDriver::add_constants_exclusion()
{
  svar_constants_exclusion = true;
}

void
ParsingDriver::add_svar_global_identification_check()
{
  mod_file->addStatement(make_unique<SvarGlobalIdentificationCheckStatement>());
}

void
ParsingDriver::steady()
{
  mod_file->addStatement(make_unique<SteadyStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::option_num(string name_option, string opt1, string opt2)
{
  if (options_list.contains(name_option))
    error("option " + name_option + " declared twice");

  options_list.set(move(name_option), pair {move(opt1), move(opt2)});
}

void
ParsingDriver::option_num(string name_option, string opt)
{
  if (options_list.contains(name_option))
    error("option " + name_option + " declared twice");

  options_list.set(move(name_option), OptionsList::NumVal {move(opt)});
}

void
ParsingDriver::option_str(string name_option, string opt)
{
  if (options_list.contains(name_option))
    error("option " + name_option + " declared twice");

  options_list.set(move(name_option), OptionsList::StringVal {move(opt)});
}

void
ParsingDriver::option_date(string name_option, string opt)
{
  if (options_list.contains(name_option))
    error("option " + name_option + " declared twice");

  options_list.set(move(name_option), OptionsList::DateVal {move(opt)});
}

void
ParsingDriver::option_symbol_list(string name_option, vector<string> symbol_list)
{
  if (options_list.contains(name_option))
    error("option " + name_option + " declared twice");

  if (name_option == "irf_shocks")
    for (auto& shock : symbol_list)
      {
        if (!mod_file->symbol_table.exists(shock))
          error("Unknown symbol: " + shock);
        if (mod_file->symbol_table.getType(shock) != SymbolType::exogenous)
          error("Variables passed to irf_shocks must be exogenous. Caused by: " + shock);
      }

  if (name_option == "ms.parameters")
    for (auto& it : symbol_list)
      if (mod_file->symbol_table.getType(it) != SymbolType::parameter)
        error("Variables passed to the parameters option of the markov_switching statement must be "
              "parameters. Caused by: "
              + it);

  options_list.set(move(name_option), OptionsList::SymbolListVal {move(symbol_list)});
}

void
ParsingDriver::option_vec_int(string name_option, vector<int> opt)
{
  if (options_list.contains(name_option))
    error("option " + name_option + " declared twice");

  if (opt.empty())
    error("option " + name_option + " was passed an empty vector.");

  options_list.set(move(name_option), move(opt));
}

void
ParsingDriver::option_vec_str(string name_option, vector<string> opt)
{
  if (options_list.contains(name_option))
    error("option " + name_option + " declared twice");

  if (opt.empty())
    error("option " + name_option + " was passed an empty vector.");

  options_list.set(move(name_option), OptionsList::VecStrVal {move(opt)});
}

void
ParsingDriver::option_vec_cellstr(string name_option, vector<string> opt)
{
  if (options_list.contains(name_option))
    error("option " + name_option + " declared twice");

  if (opt.empty())
    error("option " + name_option + " was passed an empty vector.");

  options_list.set(move(name_option), OptionsList::VecCellStrVal {move(opt)});
}

void
ParsingDriver::option_str_or_int_list(string name_option, vector<string> opt)
{
  if (options_list.contains(name_option))
    error("option " + name_option + " declared twice");

  if (opt.empty())
    return;

  options_list.set(move(name_option), OptionsList::StrOrIntListVal {move(opt)});
}

void
ParsingDriver::option_vec_value(string name_option, vector<string> opt)
{
  if (options_list.contains(name_option))
    error("option " + name_option + " declared twice");

  if (opt.empty())
    error("option " + name_option + " was passed an empty vector.");

  options_list.set(move(name_option), OptionsList::VecValueVal {move(opt)});
}

void
ParsingDriver::option_vec_of_vec_value(string name_option, vector<vector<string>> opt)
{
  if (options_list.contains(name_option))
    error("option " + name_option + " declared twice");

  if (opt.empty())
    error("option " + name_option + " was passed an empty vector.");

  options_list.set(move(name_option), move(opt));
}

void
ParsingDriver::linear()
{
  mod_file->linear = true;
}

void
ParsingDriver::rplot(vector<string> symbol_list)
{
  mod_file->addStatement(make_unique<RplotStatement>(move(symbol_list), mod_file->symbol_table));
}

void
ParsingDriver::stoch_simul(SymbolList symbol_list)
{
  // make sure default order is known to preprocessor, see #49
  if (!options_list.contains("order"))
    options_list.set("order", OptionsList::NumVal {"2"});

  symbol_list.removeDuplicates("stoch_simul", warnings);

  mod_file->addStatement(make_unique<StochSimulStatement>(move(symbol_list), move(options_list),
                                                          mod_file->symbol_table));
  options_list.clear();
}

void
ParsingDriver::trend_component_model()
{
  try
    {
      mod_file->trend_component_model_table.addTrendComponentModel(
          options_list.get<OptionsList::StringVal>("trend_component.name"),
          options_list.get<OptionsList::VecStrVal>("trend_component.eqtags"),
          options_list.get<OptionsList::VecStrVal>("trend_component.targets"));
    }
  catch (OptionsList::UnknownOptionException& e)
    {
      string name {e.name.substr(16)};
      if (name == "name")
        name = "model_name";
      error("You must pass the '" + name + "' option to the 'trend_component_model' statement.");
    }

  options_list.clear();
}

void
ParsingDriver::var_model()
{
  try
    {
      mod_file->var_model_table.addVarModel(
          options_list.get<OptionsList::StringVal>("var.model_name"),
          options_list.get_if<OptionsList::NumVal>("var.structural")
                  .value_or(OptionsList::NumVal {"false"})
              == "true",
          options_list.get<OptionsList::VecStrVal>("var.eqtags"));
    }
  catch (OptionsList::UnknownOptionException& e)
    {
      error("You must pass the '" + e.name.substr(4) + "' option to the 'var_model' statement.");
    }
  options_list.clear();
}

void
ParsingDriver::simul()
{
  warning("The 'simul' statement is deprecated. Please use 'perfect_foresight_setup' and "
          "'perfect_foresight_solver' instead.");
  mod_file->addStatement(make_unique<SimulStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::model_info()
{
  mod_file->addStatement(make_unique<ModelInfoStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::check()
{
  mod_file->addStatement(make_unique<CheckStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::add_estimated_params_element()
{
  if (estim_params.name != "dsge_prior_weight")
    {
      check_symbol_existence(estim_params.name);
      SymbolType type = mod_file->symbol_table.getType(estim_params.name);
      switch (estim_params.type)
        {
        case 1:
          if (type != SymbolType::endogenous && type != SymbolType::exogenous)
            error(estim_params.name + " must be an endogenous or an exogenous variable");
          break;
        case 2:
          check_symbol_is_parameter(estim_params.name);
          break;
        case 3:
          check_symbol_existence(estim_params.name2);
          SymbolType type2 = mod_file->symbol_table.getType(estim_params.name2);
          if ((type != SymbolType::endogenous && type != SymbolType::exogenous) || type != type2)
            error(estim_params.name + " and " + estim_params.name2
                  + " must either be both endogenous variables or both exogenous");
          break;
        }
    }
  estim_params_list.push_back(estim_params);
  estim_params.init(*data_tree);
}

void
ParsingDriver::estimated_params(bool overwrite)
{
  mod_file->addStatement(make_unique<EstimatedParamsStatement>(move(estim_params_list),
                                                               mod_file->symbol_table, overwrite));
  estim_params_list.clear();
}

void
ParsingDriver::estimated_params_init(bool use_calibration)
{
  mod_file->addStatement(make_unique<EstimatedParamsInitStatement>(
      move(estim_params_list), mod_file->symbol_table, use_calibration));
  estim_params_list.clear();
}

void
ParsingDriver::estimated_params_bounds()
{
  mod_file->addStatement(
      make_unique<EstimatedParamsBoundsStatement>(move(estim_params_list), mod_file->symbol_table));
  estim_params_list.clear();
}

void
ParsingDriver::estimated_params_remove()
{
  mod_file->addStatement(
      make_unique<EstimatedParamsRemoveStatement>(move(estim_params_list), mod_file->symbol_table));
  estim_params_list.clear();
}

void
ParsingDriver::add_osr_params_element()
{
  check_symbol_existence(osr_params.name);
  SymbolType type = mod_file->symbol_table.getType(osr_params.name);
  if (type != SymbolType::parameter)
    error(osr_params.name + " must be a parameter to be used in the osr_bounds block");
  osr_params_list.push_back(osr_params);
  osr_params.init(*data_tree);
}

void
ParsingDriver::osr_params_bounds()
{
  mod_file->addStatement(make_unique<OsrParamsBoundsStatement>(move(osr_params_list)));
  osr_params_list.clear();
}

void
ParsingDriver::set_time(string period)
{
  mod_file->addStatement(make_unique<SetTimeStatement>(move(period)));
}

void
ParsingDriver::estimation_data()
{
  mod_file->addStatement(make_unique<EstimationDataStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::set_subsamples(string name1, string name2)
{
  check_symbol_existence(name1);
  if (!name2.empty())
    check_symbol_existence(name2);

  mod_file->addStatement(make_unique<SubsamplesStatement>(name1, name2, subsample_declaration_map,
                                                          mod_file->symbol_table));
  subsample_declarations[{move(name1), move(name2)}] = move(subsample_declaration_map);
  subsample_declaration_map.clear();
}

void
ParsingDriver::copy_subsamples(string to_name1, string to_name2, string from_name1,
                               string from_name2)
{
  check_symbol_existence(to_name1);
  check_symbol_existence(from_name1);
  if (!to_name2.empty())
    check_symbol_existence(to_name2);
  if (!from_name2.empty())
    check_symbol_existence(from_name2);

  if (!subsample_declarations.contains({from_name1, from_name2}))
    {
      string err {from_name1};
      if (!from_name2.empty())
        err.append(",").append(from_name2);
      error(err + " does not have an associated subsample statement.");
    }

  mod_file->addStatement(make_unique<SubsamplesEqualStatement>(to_name1, to_name2, from_name1,
                                                               from_name2, mod_file->symbol_table));

  subsample_declarations[{move(to_name1), move(to_name2)}]
      = subsample_declarations[{move(from_name1), move(from_name2)}];
}

void
ParsingDriver::check_symbol_is_statement_variable(const string& name)
{
  check_symbol_existence(name);
  int symb_id = mod_file->symbol_table.getID(name);
  if (mod_file->symbol_table.getType(symb_id) != SymbolType::statementDeclaredVariable)
    error(name + " is not a variable assigned in a statement");
}

void
ParsingDriver::set_subsample_name_equal_to_date_range(string name, string date1, string date2)
{
  if (subsample_declaration_map.contains(name))
    error("Symbol " + name + " may only be assigned once in a SUBSAMPLE statement");
  subsample_declaration_map[move(name)] = {move(date1), move(date2)};
}

void
ParsingDriver::check_subsample_declaration_exists(const string& name1, const string& subsample_name)
{
  if (subsample_name.empty())
    return;

  check_subsample_declaration_exists(name1, "", subsample_name);
}

void
ParsingDriver::check_subsample_declaration_exists(const string& name1, const string& name2,
                                                  const string& subsample_name)
{
  if (subsample_name.empty())
    return;

  check_symbol_existence(name1);
  if (!name2.empty())
    check_symbol_existence(name2);

  auto it = subsample_declarations.find({name1, name2});
  if (it == subsample_declarations.end())
    {
      it = subsample_declarations.find({name2, name1});
      if (it == subsample_declarations.end())
        {
          string err {name1};
          if (!name2.empty())
            err.append(",").append(name2);
          error("A subsample statement has not been issued for " + err);
        }
    }

  auto tmp_map = it->second;
  if (!tmp_map.contains(subsample_name))
    error("The subsample name " + subsample_name
          + " was not previously declared in a subsample statement.");
}

void
ParsingDriver::set_prior(string name, string subsample_name)
{
  check_symbol_is_parameter(name);
  check_subsample_declaration_exists(name, subsample_name);
  mod_file->addStatement(make_unique<PriorStatement>(move(name), move(subsample_name), prior_shape,
                                                     prior_variance, move(options_list)));
  options_list.clear();
  set_prior_variance();
  prior_shape = PriorDistributions::noShape;
}

void
ParsingDriver::set_joint_prior(const vector<string>& symbol_vec)
{
  for (auto& it : symbol_vec)
    add_joint_parameter(it);
  mod_file->addStatement(
      make_unique<JointPriorStatement>(move(joint_parameters), prior_shape, move(options_list)));
  joint_parameters.clear();
  options_list.clear();
  prior_shape = PriorDistributions::noShape;
}

void
ParsingDriver::add_joint_parameter(string name)
{
  check_symbol_is_parameter(name);
  joint_parameters.push_back(move(name));
}

void
ParsingDriver::set_prior_variance(expr_t variance)
{
  prior_variance = variance;
}

void
ParsingDriver::copy_prior(string to_declaration_type, string to_name1, string to_name2,
                          string to_subsample_name, string from_declaration_type, string from_name1,
                          string from_name2, string from_subsample_name)
{
  if (to_declaration_type == "par")
    check_symbol_is_parameter(to_name1);
  else
    {
      check_symbol_is_endogenous_or_exogenous(to_name1, false);
      if (!to_name2.empty())
        check_symbol_is_endogenous_or_exogenous(to_name2, false);
    }

  if (from_declaration_type == "par")
    check_symbol_is_parameter(from_name1);
  else
    {
      check_symbol_is_endogenous_or_exogenous(from_name1, false);
      if (!from_name2.empty())
        check_symbol_is_endogenous_or_exogenous(from_name2, false);
    }

  mod_file->addStatement(make_unique<PriorEqualStatement>(
      move(to_declaration_type), move(to_name1), move(to_name2), move(to_subsample_name),
      move(from_declaration_type), move(from_name1), move(from_name2), move(from_subsample_name),
      mod_file->symbol_table));
}

void
ParsingDriver::set_options(string name, string subsample_name)
{
  check_symbol_is_parameter(name);
  check_subsample_declaration_exists(name, subsample_name);
  mod_file->addStatement(
      make_unique<OptionsStatement>(move(name), move(subsample_name), move(options_list)));
  options_list.clear();
}

void
ParsingDriver::copy_options(string to_declaration_type, string to_name1, string to_name2,
                            string to_subsample_name, string from_declaration_type,
                            string from_name1, string from_name2, string from_subsample_name)
{
  if (to_declaration_type == "par")
    check_symbol_is_parameter(to_name1);
  else
    {
      check_symbol_is_endogenous_or_exogenous(to_name1, false);
      if (!to_name2.empty())
        check_symbol_is_endogenous_or_exogenous(to_name2, false);
    }

  if (from_declaration_type == "par")
    check_symbol_is_parameter(from_name1);
  else
    {
      check_symbol_is_endogenous_or_exogenous(from_name1, false);
      if (!from_name2.empty())
        check_symbol_is_endogenous_or_exogenous(from_name2, false);
    }

  mod_file->addStatement(make_unique<OptionsEqualStatement>(
      move(to_declaration_type), move(to_name1), move(to_name2), move(to_subsample_name),
      move(from_declaration_type), move(from_name1), move(from_name2), move(from_subsample_name),
      mod_file->symbol_table));
}

void
ParsingDriver::check_symbol_is_endogenous_or_exogenous(const string& name, bool allow_det)
{
  check_symbol_existence(name);
  switch (mod_file->symbol_table.getType(name))
    {
    case SymbolType::endogenous:
    case SymbolType::exogenous:
      break;
    case SymbolType::exogenousDet:
      if (!allow_det)
        error(name + " is an exogenous deterministic.");
      break;
    default:
      error(name + " is neither endogenous or exogenous.");
    }
}

void
ParsingDriver::check_symbol_is_endogenous(const string& name)
{
  check_symbol_existence(name);
  if (mod_file->symbol_table.getType(name) != SymbolType::endogenous)
    error(name + " is not endogenous.");
}

void
ParsingDriver::check_symbol_is_exogenous(const string& name, bool allow_exo_det)
{
  check_symbol_existence(name);
  switch (mod_file->symbol_table.getType(name))
    {
    case SymbolType::exogenous:
      break;
    case SymbolType::exogenousDet:
      if (!allow_exo_det)
        error(name + " is an exogenous deterministic.");
      break;
    default:
      error(name + " is not exogenous.");
    }
}

void
ParsingDriver::set_std_prior(string name, string subsample_name)
{
  check_symbol_is_endogenous_or_exogenous(name, false);
  check_subsample_declaration_exists(name, subsample_name);
  mod_file->addStatement(
      make_unique<StdPriorStatement>(move(name), move(subsample_name), prior_shape, prior_variance,
                                     move(options_list), mod_file->symbol_table));
  options_list.clear();
  set_prior_variance();
  prior_shape = PriorDistributions::noShape;
}

void
ParsingDriver::set_std_options(string name, string subsample_name)
{
  check_symbol_is_endogenous_or_exogenous(name, false);
  check_subsample_declaration_exists(name, subsample_name);
  mod_file->addStatement(make_unique<StdOptionsStatement>(
      move(name), move(subsample_name), move(options_list), mod_file->symbol_table));
  options_list.clear();
}

void
ParsingDriver::set_corr_prior(string name1, string name2, string subsample_name)
{
  check_symbol_is_endogenous_or_exogenous(name1, false);
  check_symbol_is_endogenous_or_exogenous(name2, false);
  check_subsample_declaration_exists(name1, name2, subsample_name);
  mod_file->addStatement(
      make_unique<CorrPriorStatement>(move(name1), move(name2), move(subsample_name), prior_shape,
                                      prior_variance, move(options_list), mod_file->symbol_table));
  options_list.clear();
  set_prior_variance();
  prior_shape = PriorDistributions::noShape;
}

void
ParsingDriver::set_corr_options(string name1, string name2, string subsample_name)
{
  check_symbol_is_endogenous_or_exogenous(name1, false);
  check_symbol_is_endogenous_or_exogenous(name2, false);
  check_subsample_declaration_exists(name1, name2, subsample_name);
  mod_file->addStatement(make_unique<CorrOptionsStatement>(
      move(name1), move(name2), move(subsample_name), move(options_list), mod_file->symbol_table));
  options_list.clear();
}

void
ParsingDriver::run_estimation(vector<string> symbol_list)
{
  mod_file->addStatement(make_unique<EstimationStatement>(mod_file->symbol_table, move(symbol_list),
                                                          move(options_list)));
  options_list.clear();
}

void
ParsingDriver::sensitivity()
{
  mod_file->addStatement(make_unique<SensitivityStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::check_varobs()
{
  if (exchange(varobs_seen, true))
    error("varobs: you cannot have several 'varobs' statements in the same MOD file");
}

void
ParsingDriver::add_varobs(const string& name)
{
  check_symbol_is_endogenous(name);
  int symb_id = mod_file->symbol_table.getID(name);
  try
    {
      mod_file->symbol_table.addObservedVariable(symb_id);
    }
  catch (SymbolTable::AlreadyDeclaredAsObservedException&)
    {
      error("varobs: '" + name + "' has already been declared as an observable");
    }
}

void
ParsingDriver::set_trends()
{
  mod_file->addStatement(
      make_unique<ObservationTrendsStatement>(move(trend_elements), mod_file->symbol_table));
  trend_elements.clear();
}

void
ParsingDriver::set_deterministic_trends()
{
  mod_file->addStatement(
      make_unique<DeterministicTrendsStatement>(move(trend_elements), mod_file->symbol_table));
  trend_elements.clear();
}

void
ParsingDriver::set_trend_element(const string& arg1, expr_t arg2)
{
  check_symbol_is_endogenous(arg1);
  int symb_id {mod_file->symbol_table.getID(arg1)};
  if (trend_elements.contains(symb_id))
    error("observation_trends/deterministic_trends: " + arg1 + " declared twice");
  trend_elements.emplace(symb_id, arg2);
}

void
ParsingDriver::set_filter_initial_state()
{
  mod_file->addStatement(make_unique<FilterInitialStateStatement>(
      move(filter_initial_state_elements), mod_file->symbol_table));
  filter_initial_state_elements.clear();
}

void
ParsingDriver::set_filter_initial_state_element(const string& name, const string& lag, expr_t rhs)
{
  check_symbol_existence(name);
  int symb_id = mod_file->symbol_table.getID(name);
  SymbolType type = mod_file->symbol_table.getType(symb_id);
  int ilag = stoi(lag);

  if (type != SymbolType::endogenous && type != SymbolType::exogenous
      && type != SymbolType::exogenousDet)
    error("filter_initial_state: " + name + " should be an endogenous or exogenous variable");

  if ((type == SymbolType::exogenous || type == SymbolType::exogenousDet) && ilag == 0)
    error("filter_initial_state: exogenous variable " + name + " must be provided with a lag");

  if (filter_initial_state_elements.contains({symb_id, ilag}))
    error("filter_initial_state: (" + name + ", " + lag + ") declared twice");

  if (mod_file->dynamic_model.minLagForSymbol(symb_id) > ilag - 1)
    error("filter_initial_state: variable " + name + " does not appear in the model with the lag "
          + to_string(ilag - 1)
          + " (see the reference manual for the timing convention in 'filter_initial_state')");

  filter_initial_state_elements[{symb_id, ilag}] = rhs;
}

void
ParsingDriver::set_optim_weights(string name, expr_t value)
{
  check_symbol_is_endogenous(name);
  if (var_weights.contains(name))
    error("optim_weights: " + name + " declared twice");
  var_weights[move(name)] = value;
}

void
ParsingDriver::set_optim_weights(const string& name1, const string& name2, expr_t value)
{
  check_symbol_is_endogenous(name1);
  check_symbol_is_endogenous(name2);

  pair covar_key {name1, name2};

  if (covar_weights.contains(covar_key))
    error("optim_weights: pair of variables (" + name1 + ", " + name2 + ") declared twice");

  covar_weights[covar_key] = value;
}

void
ParsingDriver::optim_weights()
{
  mod_file->addStatement(make_unique<OptimWeightsStatement>(move(var_weights), move(covar_weights),
                                                            mod_file->symbol_table));
  var_weights.clear();
  covar_weights.clear();
}

void
ParsingDriver::set_osr_params(vector<string> symbol_list)
{
  mod_file->addStatement(
      make_unique<OsrParamsStatement>(move(symbol_list), mod_file->symbol_table));
}

void
ParsingDriver::run_osr(vector<string> symbol_list)
{
  /* The logic here is different from “ramsey_policy” and “ramsey_model”,
     because we want to allow several instances of “osr” in
     the same .mod file. */
  if (!mod_file->symbol_table.exists("optimal_policy_discount_factor"))
    {
      int symb_id
          = declare_parameter("optimal_policy_discount_factor", planner_discount_latex_name);
      mod_file->symbol_table.markPreprocessorGeneratedParameter(symb_id);
    }

  if (!planner_discount)
    planner_discount = data_tree->One;
  init_param("optimal_policy_discount_factor", planner_discount);

  mod_file->addStatement(
      make_unique<OsrStatement>(move(symbol_list), move(options_list), mod_file->symbol_table));
  options_list.clear();
  planner_discount = nullptr;
  planner_discount_latex_name.clear();
}

void
ParsingDriver::run_dynatype(string filename, vector<string> symbol_list)
{
  mod_file->addStatement(
      make_unique<DynaTypeStatement>(move(symbol_list), move(filename), mod_file->symbol_table));
}

void
ParsingDriver::run_dynasave(string filename, vector<string> symbol_list)
{
  mod_file->addStatement(
      make_unique<DynaSaveStatement>(move(symbol_list), move(filename), mod_file->symbol_table));
}

void
ParsingDriver::run_load_params_and_steady_state(const string& filename)
{
  mod_file->addStatement(
      make_unique<LoadParamsAndSteadyStateStatement>(filename, mod_file->symbol_table, warnings));
}

void
ParsingDriver::run_save_params_and_steady_state(string filename)
{
  mod_file->addStatement(make_unique<SaveParamsAndSteadyStateStatement>(move(filename)));
}

void
ParsingDriver::run_identification()
{
  mod_file->addStatement(make_unique<IdentificationStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::heterogeneity_load_steady_state()
{
  mod_file->addStatement(make_unique<HeterogeneityLoadSteadyStateStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::heterogeneity_compute_steady_state()
{
  mod_file->addStatement(make_unique<HeterogeneityComputeSteadyStateStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::heterogeneity_solve()
{
  mod_file->addStatement(make_unique<HeterogeneitySolveStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::heterogeneity_simulate(vector<string> symbol_list)
{
  mod_file->addStatement(
      make_unique<HeterogeneitySimulateStatement>(move(symbol_list), move(options_list)));
  options_list.clear();
}

void
ParsingDriver::add_mc_filename(string filename, string prior)
{
  for (auto& it : filename_list)
    if (it.first == filename)
      error("model_comparison: filename " + filename + " declared twice");
  filename_list.emplace_back(move(filename), move(prior));
}

void
ParsingDriver::run_model_comparison()
{
  mod_file->addStatement(
      make_unique<ModelComparisonStatement>(move(filename_list), move(options_list)));
  filename_list.clear();
  options_list.clear();
}

void
ParsingDriver::begin_planner_objective()
{
  planner_objective = make_unique<PlannerObjective>(
      mod_file->symbol_table, mod_file->num_constants, mod_file->external_functions_table,
      mod_file->heterogeneity_table, mod_file->database_table);
  set_current_data_tree(planner_objective.get());
}

void
ParsingDriver::end_planner_objective(expr_t expr)
{
  // Add equation corresponding to expression
  expr_t eq = model_tree->AddEqual(expr, model_tree->Zero);
  model_tree->addEquation(eq, location.begin.line);

  mod_file->addStatement(make_unique<PlannerObjectiveStatement>(move(planner_objective)));

  // Handle undeclared variables (see #81)
  bool exit_after_write = false;
  if (undeclared_model_variable_errors.size() > 0)
    for (auto& it : undeclared_model_variable_errors)
      {
        if (nostrict)
          warning(it.second);
        else
          {
            exit_after_write = true;
            cerr << it.second << '\n';
          }
      }
  undeclared_model_variable_errors.clear();
  if (exit_after_write)
    exit(EXIT_FAILURE);

  reset_data_tree();
}

void
ParsingDriver::ramsey_model()
{
  // Some checks to ensure correct error messages (see #90)
  if (ramsey_policy_seen)
    error("A 'ramsey_model' statement cannot follow a 'ramsey_policy' statement.");
  if (exchange(ramsey_model_seen, true))
    error("Several 'ramsey_model' statements cannot appear in a given .mod file.");

  if (!mod_file->symbol_table.exists("optimal_policy_discount_factor"))
    {
      if (!planner_discount)
        planner_discount = data_tree->One;
      int symb_id
          = declare_parameter("optimal_policy_discount_factor", planner_discount_latex_name);
      mod_file->symbol_table.markPreprocessorGeneratedParameter(symb_id);
      init_param("optimal_policy_discount_factor", planner_discount);
    }
  else if (planner_discount)
    error("ramsey_model: the 'planner_discount' option cannot be used when the "
          "'optimal_policy_discount_factor' parameter is explicitly declared.");

  // Check that instruments are declared endogenous (#72)
  if (options_list.contains("instruments"))
    for (const auto& s : options_list.get<OptionsList::SymbolListVal>("instruments").getSymbols())
      check_symbol_is_endogenous(s);

  mod_file->addStatement(make_unique<RamseyModelStatement>(move(options_list)));
  options_list.clear();
  planner_discount = nullptr;
  planner_discount_latex_name.clear();
}

void
ParsingDriver::ramsey_policy(vector<string> symbol_list)
{
  warning("The 'ramsey_policy' statement is deprecated. Please use 'ramsey_model', 'stoch_simul', "
          "and 'evaluate_planner_objective' instead.");

  // Some checks to ensure correct error messages (see #90)
  if (ramsey_model_seen)
    error("A 'ramsey_policy' statement cannot follow a 'ramsey_model' statement.");
  if (exchange(ramsey_policy_seen, true))
    error("Several 'ramsey_policy' statements cannot appear in a given .mod file.");

  if (!mod_file->symbol_table.exists("optimal_policy_discount_factor"))
    {
      if (!planner_discount)
        planner_discount = data_tree->One;
      int symb_id = declare_parameter("optimal_policy_discount_factor");
      mod_file->symbol_table.markPreprocessorGeneratedParameter(symb_id);
      init_param("optimal_policy_discount_factor", planner_discount);
    }
  else if (planner_discount)
    error("ramsey_policy: the 'planner_discount' option cannot be used when the "
          "'optimal_policy_discount_factor' parameter is explicitly declared.");

  // Check that instruments are declared endogenous (#72)
  if (options_list.contains("instruments"))
    for (const auto& s : options_list.get<OptionsList::SymbolListVal>("instruments").getSymbols())
      check_symbol_is_endogenous(s);

  mod_file->addStatement(make_unique<RamseyPolicyStatement>(move(symbol_list), move(options_list),
                                                            mod_file->symbol_table));
  options_list.clear();
  planner_discount = nullptr;
}

void
ParsingDriver::evaluate_planner_objective()
{
  mod_file->addStatement(make_unique<EvaluatePlannerObjectiveStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::occbin_setup()
{
  mod_file->addStatement(make_unique<OccbinSetupStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::occbin_solver()
{
  mod_file->addStatement(make_unique<OccbinSolverStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::occbin_write_regimes()
{
  mod_file->addStatement(make_unique<OccbinWriteRegimesStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::occbin_graph(vector<string> symbol_list)
{
  mod_file->addStatement(make_unique<OccbinGraphStatement>(move(symbol_list), move(options_list)));
  options_list.clear();
}

void
ParsingDriver::discretionary_policy(vector<string> symbol_list)
{
  /* The logic here is different from “ramsey_policy” and “ramsey_model”,
     because we want to allow several instances of “discretionary_policy” in
     the same .mod file. */
  if (!mod_file->symbol_table.exists("optimal_policy_discount_factor"))
    {
      int symb_id
          = declare_parameter("optimal_policy_discount_factor", planner_discount_latex_name);
      mod_file->symbol_table.markPreprocessorGeneratedParameter(symb_id);
    }

  if (!planner_discount)
    planner_discount = data_tree->One;
  init_param("optimal_policy_discount_factor", planner_discount);

  // Check that instruments are declared endogenous (#72)
  if (options_list.contains("instruments"))
    for (const auto& s : options_list.get<OptionsList::SymbolListVal>("instruments").getSymbols())
      check_symbol_is_endogenous(s);

  mod_file->addStatement(make_unique<DiscretionaryPolicyStatement>(
      move(symbol_list), move(options_list), mod_file->symbol_table));
  options_list.clear();
  planner_discount = nullptr;
  planner_discount_latex_name.clear();
}

void
ParsingDriver::write_latex_dynamic_model(bool write_equation_tags)
{
  mod_file->addStatement(
      make_unique<WriteLatexDynamicModelStatement>(mod_file->dynamic_model, write_equation_tags));
}

void
ParsingDriver::write_latex_static_model(bool write_equation_tags)
{
  mod_file->addStatement(
      make_unique<WriteLatexStaticModelStatement>(mod_file->static_model, write_equation_tags));
}

void
ParsingDriver::write_latex_original_model(bool write_equation_tags)
{
  mod_file->addStatement(
      make_unique<WriteLatexOriginalModelStatement>(mod_file->original_model, write_equation_tags));
}

void
ParsingDriver::write_latex_steady_state_model()
{
  mod_file->addStatement(
      make_unique<WriteLatexSteadyStateModelStatement>(mod_file->steady_state_model));
}

void
ParsingDriver::bvar_density(const string& maxnlags)
{
  mod_file->addStatement(make_unique<BVARDensityStatement>(stoi(maxnlags), move(options_list)));
  options_list.clear();
}

void
ParsingDriver::bvar_forecast(const string& nlags)
{
  mod_file->addStatement(make_unique<BVARForecastStatement>(stoi(nlags), move(options_list)));
  options_list.clear();
}

void
ParsingDriver::bvar_irf(const string& nirf, string identificationname)
{
  mod_file->addStatement(make_unique<BVARIRFStatement>(stoi(nirf), move(identificationname)));
}

void
ParsingDriver::sbvar()
{
  mod_file->addStatement(make_unique<SBVARStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::ms_estimation()
{
  mod_file->addStatement(make_unique<MSSBVAREstimationStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::ms_simulation()
{
  mod_file->addStatement(make_unique<MSSBVARSimulationStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::ms_compute_mdd()
{
  mod_file->addStatement(make_unique<MSSBVARComputeMDDStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::ms_compute_probabilities()
{
  mod_file->addStatement(make_unique<MSSBVARComputeProbabilitiesStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::ms_irf(vector<string> symbol_list)
{
  mod_file->addStatement(make_unique<MSSBVARIrfStatement>(move(symbol_list), move(options_list),
                                                          mod_file->symbol_table));
  options_list.clear();
}

void
ParsingDriver::ms_forecast()
{
  mod_file->addStatement(make_unique<MSSBVARForecastStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::ms_variance_decomposition()
{
  mod_file->addStatement(make_unique<MSSBVARVarianceDecompositionStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::svar()
{
  bool has_coefficients = options_list.contains("ms.coefficients"),
       has_variances = options_list.contains("ms.variances"),
       has_constants = options_list.contains("ms.constants");
  if (!has_coefficients && !has_variances && !has_constants)
    error("You must pass one of 'coefficients', 'variances', or 'constants'.");

  if ((has_coefficients && has_variances) || (has_variances && has_constants)
      || (has_coefficients && has_constants))
    error("You may only pass one of 'coefficients', 'variances', or 'constants'.");

  try
    {
      if (stoi(options_list.get<OptionsList::NumVal>("ms.chain")) <= 0)
        error("The value passed to the 'chain' option must be greater than zero.");
    }
  catch (OptionsList::UnknownOptionException&)
    {
      error("A 'chain' option must be passed to the 'svar' statement.");
    }

  if (options_list.contains("ms.equations"))
    for (int viit : options_list.get<vector<int>>("ms.equations"))
      if (viit <= 0)
        error("The value(s) passed to the 'equations' option must be greater than zero.");

  mod_file->addStatement(make_unique<SvarStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::markov_switching()
{
  try
    {
      if (stoi(options_list.get<OptionsList::NumVal>("ms.chain")) <= 0)
        error("The value passed to the chain option must be greater than zero.");
      if (stoi(options_list.get<OptionsList::NumVal>("ms.number_of_regimes")) <= 0)
        error("The value passed to the number_of_regimes option must be greater than zero.");
      options_list.get<OptionsList::NumVal>("ms.duration"); // Just check its presence
    }
  catch (OptionsList::UnknownOptionException& e)
    {
      error("A '" + e.name.substr(3)
            + "' option must be passed to the 'markov_switching' statement.");
    }

  mod_file->addStatement(make_unique<MarkovSwitchingStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::shock_decomposition(vector<string> symbol_list)
{
  mod_file->addStatement(make_unique<ShockDecompositionStatement>(
      move(symbol_list), move(options_list), mod_file->symbol_table));
  options_list.clear();
}

void
ParsingDriver::realtime_shock_decomposition(vector<string> symbol_list)
{
  mod_file->addStatement(make_unique<RealtimeShockDecompositionStatement>(
      move(symbol_list), move(options_list), mod_file->symbol_table));
  options_list.clear();
}

void
ParsingDriver::plot_shock_decomposition(vector<string> symbol_list)
{
  mod_file->addStatement(make_unique<PlotShockDecompositionStatement>(
      move(symbol_list), move(options_list), mod_file->symbol_table));
  options_list.clear();
}

void
ParsingDriver::initial_condition_decomposition(vector<string> symbol_list)
{
  mod_file->addStatement(make_unique<InitialConditionDecompositionStatement>(
      move(symbol_list), move(options_list), mod_file->symbol_table));
  options_list.clear();
}

void
ParsingDriver::squeeze_shock_decomposition(vector<string> symbol_list)
{
  mod_file->addStatement(
      make_unique<SqueezeShockDecompositionStatement>(move(symbol_list), mod_file->symbol_table));
}

void
ParsingDriver::conditional_forecast()
{
  mod_file->addStatement(make_unique<ConditionalForecastStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::plot_conditional_forecast(const optional<string>& periods,
                                         vector<string> symbol_list)
{
  optional<int> iperiods;
  if (periods)
    iperiods = stoi(*periods);
  mod_file->addStatement(make_unique<PlotConditionalForecastStatement>(iperiods, move(symbol_list),
                                                                       mod_file->symbol_table));
}

void
ParsingDriver::conditional_forecast_paths()
{
  if (ranges::any_of(views::values(det_shocks), [](auto& v) {
        return ranges::any_of(views::keys(v),
                              [](auto& p) { return !holds_alternative<pair<int, int>>(p); });
      }))
    error("conditional_forecast_paths: dates are not allowed in the 'periods' keyword");
  mod_file->addStatement(
      make_unique<ConditionalForecastPathsStatement>(move(det_shocks), mod_file->symbol_table));
  det_shocks.clear();
  if (!learnt_shocks_add.empty())
    error("conditional_forecast_paths: 'add' keyword not allowed");
  if (!learnt_shocks_multiply.empty())
    error("conditional_forecast_paths: 'multiply' keyword not allowed");
}

void
ParsingDriver::calib_smoother(vector<string> symbol_list)
{
  mod_file->addStatement(make_unique<CalibSmootherStatement>(move(symbol_list), move(options_list),
                                                             mod_file->symbol_table));
  options_list.clear();
}

void
ParsingDriver::extended_path()
{
  mod_file->addStatement(make_unique<ExtendedPathStatement>(move(options_list)));
  options_list.clear();
}

expr_t
ParsingDriver::add_model_equal(expr_t arg1, expr_t arg2, map<string, string> eq_tags,
                               expr_t complementarity_condition)
{
  expr_t id = model_tree->AddEqual(arg1, arg2);

  for (const auto& [key, value] : eq_tags)
    if (key == "endogenous")
      declare_or_change_type(SymbolType::endogenous, value);

  optional<tuple<int, expr_t, expr_t>> matched_complementarity_condition;

  // Handle legacy “mcp” tags, for backward compatibility
  if (eq_tags.contains("mcp"))
    {
      if (complementarity_condition)
        error("Can't have both an 'mcp' tag and a complementarity condition after the "
              "perpendicular symbol");
      else
        warning("Specifying complementarity conditions with the 'mcp' tag is obsolete. Please "
                "consider switching to the new syntax using the perpendicular symbol.");

      auto [var_name, is_lower_bound, constant] {[&]() -> tuple<string, bool, string> {
        auto tagval = eq_tags.at("mcp");
        if (auto less_pos = tagval.find('<'); less_pos != string::npos)
          return {tagval.substr(0, less_pos), false, tagval.substr(less_pos + 1)};
        else if (auto greater_pos = tagval.find('>'); greater_pos != string::npos)
          return {tagval.substr(0, greater_pos), true, tagval.substr(greater_pos + 1)};
        else
          error("'mcp' tag does not contain an inequality");
      }()};

      // Trim whitespace
      var_name.erase(var_name.find_last_not_of(" \n\t") + 1);
      var_name.erase(0, var_name.find_first_not_of(" \n\t"));
      constant.erase(constant.find_last_not_of(" \n\t") + 1);
      constant.erase(0, constant.find_first_not_of(" \n\t"));

      int symb_id {[&] {
        try
          {
            return mod_file->symbol_table.getID(var_name);
          }
        catch (SymbolTable::UnknownSymbolNameException&)
          {
            error("Left-hand side of expression in 'mcp' tag is not a variable");
          }
      }()};

      if (heterogeneous_model)
        error("'mcp' tags are not allowed in heterogeneous model blocks");

      if (mod_file->symbol_table.getType(symb_id) != SymbolType::endogenous)
        error("Left-hand side of expression in 'mcp' tag is not an endogenous variable");

      expr_t matched_constant {[&] {
        char* str_end;
        double d = strtod(constant.c_str(), &str_end);
        if (str_end == constant.c_str())
          error("Right-hand side of expression in 'mcp' tag should be a constant");
        return data_tree->AddPossiblyNegativeConstant(d);
      }()};

      matched_complementarity_condition = {symb_id, is_lower_bound ? matched_constant : nullptr,
                                           is_lower_bound ? nullptr : matched_constant};
    }

  if (complementarity_condition)
    try
      {
        matched_complementarity_condition
            = heterogeneous_model ? complementarity_condition->matchComplementarityCondition(
                                        heterogeneous_model->heterogeneity_dimension)
                                  : complementarity_condition->matchComplementarityCondition();
      }
    catch (ExprNode::MatchFailureException& e)
      {
        error("Complementarity condition has an incorrect form"s
              + (e.message.empty() ? ""s : ": "s + e.message));
      }

  if (eq_tags.contains("static"))
    {
      // If the equation is tagged [static]
      if (!id->isInStaticForm())
        error("An equation tagged [static] cannot contain leads, lags, expectations, diff or "
              "STEADY_STATE operators");

      dynamic_model->addStaticOnlyEquation(id, location.begin.line,
                                           move(matched_complementarity_condition), eq_tags);
    }
  else if (eq_tags.contains("bind") || eq_tags.contains("relax"))
    {
      // If the equation has a “bind” or “relax” tag (occbin case)
      if (!eq_tags.contains("name"))
        error("An equation with a 'bind' or 'relax' tag must have a 'name' tag");
      auto constraints_bind = strsplit(eq_tags["bind"], ',');
      auto constraints_relax = strsplit(eq_tags["relax"], ',');
      auto constraints_all = constraints_bind;
      constraints_all.insert(constraints_all.end(), constraints_relax.begin(),
                             constraints_relax.end()); // Concatenate the two vectors
      for (const auto& constraint : constraints_all)
        {
          if (!isSymbolIdentifier(constraint))
            error("The string '" + constraint
                  + "' is not a valid Occbin constraint name (contains unauthorized characters)");
          string param_name = buildOccbinBindParamName(constraint);
          try
            {
              if (mod_file->symbol_table.getType(param_name) != SymbolType::parameter)
                error("The name '" + param_name
                      + "' is already used. Please use another name for Occbin constraint '"
                      + constraint + "'");
            }
          catch (SymbolTable::UnknownSymbolNameException& e)
            {
              // Declare and initialize the new parameter
              int symb_id = mod_file->symbol_table.addSymbol(param_name, SymbolType::parameter);
              mod_file->symbol_table.markPreprocessorGeneratedParameter(symb_id);
              mod_file->addStatement(make_unique<InitParamStatement>(symb_id, dynamic_model->Zero,
                                                                     mod_file->symbol_table));
            }
        }
      eq_tags.erase("bind");
      eq_tags.erase("relax");

      try
        {
          dynamic_model->addOccbinEquation(id, location.begin.line, move(eq_tags), constraints_bind,
                                           constraints_relax);
        }
      catch (DynamicModel::OccbinRegimeTracker::ConstraintInBothBindAndRelaxException& e)
        {
          error("The constraint '" + e.constraint + "' is both in the 'bind' and 'relax' tags");
        }
      catch (DynamicModel::OccbinRegimeTracker::RegimeAlreadyPresentException& e)
        {
          stringstream s;
          if (!e.constraints_bind.empty())
            {
              s << "bind='";
              for (bool first_printed {false}; const auto& c : e.constraints_bind)
                {
                  if (exchange(first_printed, true))
                    s << ",";
                  s << c;
                }
            }
          if (!e.constraints_bind.empty() && !e.constraints_relax.empty())
            s << "' and ";
          if (!e.constraints_relax.empty())
            {
              s << "relax='";
              for (bool first_printed {false}; const auto& c : e.constraints_relax)
                {
                  if (exchange(first_printed, true))
                    s << ",";
                  s << c;
                }
            }
          s << "'";
          error("The regime corresponding to " + s.str()
                + " has already been declared for this equation");
        }
    }
  else // General case
    model_tree->addEquation(id, location.begin.line, move(matched_complementarity_condition),
                            move(eq_tags));

  return id;
}

expr_t
ParsingDriver::add_model_equal_with_zero_rhs(expr_t arg, map<string, string> eq_tags,
                                             expr_t complementarity_condition)
{
  return add_model_equal(arg, model_tree->Zero, move(eq_tags), complementarity_condition);
}

void
ParsingDriver::model_local_variable(const vector<pair<string, string>>& symbol_list)
{
  for (auto& [name, tex_name] : symbol_list)
    declare_symbol(name, SymbolType::modelLocalVariable, tex_name, {}, {});
}

void
ParsingDriver::declare_and_init_model_local_variable(const string& name, expr_t rhs)
{
  int symb_id;
  try
    {
      symb_id = mod_file->symbol_table.addSymbol(name, SymbolType::modelLocalVariable);
    }
  catch (SymbolTable::AlreadyDeclaredException& e)
    {
      /* It can have already been declared in a steady_state_model block or
         model_local_variable statement, check that it is indeed a
         ModelLocalVariable */
      symb_id = mod_file->symbol_table.getID(name);
      if (mod_file->symbol_table.getType(symb_id) != SymbolType::modelLocalVariable)
        error(name
              + " has wrong type or was already used on the right-hand side. You cannot use it on "
                "the left-hand side of a pound ('#') expression");
    }

  try
    {
      model_tree->AddLocalVariable(symb_id, rhs);
    }
  catch (DataTree::LocalVariableException& e)
    {
      error("Local model variable " + name + " declared twice.");
    }
}

void
ParsingDriver::change_type(SymbolType new_type, const vector<string>& symbol_list)
{
  for (auto& it : symbol_list)
    {
      int id;
      try
        {
          id = mod_file->symbol_table.getID(it);
        }
      catch (SymbolTable::UnknownSymbolNameException& e)
        {
          error("Unknown variable " + it);
        }

      // Check if symbol already used in a VariableNode
      if (mod_file->expressions_tree.isSymbolUsed(id) || mod_file->dynamic_model.isSymbolUsed(id))
        error("You cannot modify the type of symbol " + it
              + " after having used it in an expression");

      mod_file->symbol_table.changeType(id, new_type);
    }
}

expr_t
ParsingDriver::add_plus(expr_t arg1, expr_t arg2)
{
  return data_tree->AddPlus(arg1, arg2);
}

expr_t
ParsingDriver::add_minus(expr_t arg1, expr_t arg2)
{
  return data_tree->AddMinus(arg1, arg2);
}

expr_t
ParsingDriver::add_uminus(expr_t arg1)
{
  return data_tree->AddUMinus(arg1);
}

expr_t
ParsingDriver::add_times(expr_t arg1, expr_t arg2)
{
  return data_tree->AddTimes(arg1, arg2);
}

expr_t
ParsingDriver::add_divide(expr_t arg1, expr_t arg2)
{
  try
    {
      return data_tree->AddDivide(arg1, arg2);
    }
  catch (const DataTree::DivisionByZeroException& e)
    {
      error(e.message.empty()
                ? "Division by zero error encountered when reading model from .mod file"s
                : e.message);
    }
}

expr_t
ParsingDriver::add_less(expr_t arg1, expr_t arg2)
{
  return data_tree->AddLess(arg1, arg2);
}

expr_t
ParsingDriver::add_greater(expr_t arg1, expr_t arg2)
{
  return data_tree->AddGreater(arg1, arg2);
}

expr_t
ParsingDriver::add_less_equal(expr_t arg1, expr_t arg2)
{
  return data_tree->AddLessEqual(arg1, arg2);
}

expr_t
ParsingDriver::add_greater_equal(expr_t arg1, expr_t arg2)
{
  return data_tree->AddGreaterEqual(arg1, arg2);
}

expr_t
ParsingDriver::add_equal_equal(expr_t arg1, expr_t arg2)
{
  return data_tree->AddEqualEqual(arg1, arg2);
}

expr_t
ParsingDriver::add_different(expr_t arg1, expr_t arg2)
{
  return data_tree->AddDifferent(arg1, arg2);
}

expr_t
ParsingDriver::add_power(expr_t arg1, expr_t arg2)
{
  return data_tree->AddPower(arg1, arg2);
}

expr_t
ParsingDriver::add_expectation(const string& arg1, expr_t arg2)
{
  if (is_parsing_epilogue())
    error("The 'expectation' operator is forbidden in 'epilogue'.");

  if (is_parsing_occbin_constraints())
    error("The 'expectation' operator is forbidden in 'occbin_constraints'.");

  return data_tree->AddExpectation(stoi(arg1), arg2);
}

expr_t
ParsingDriver::add_var_expectation(const string& model_name)
{
  if (is_parsing_epilogue())
    error("The 'var_expectation' operator is forbidden in 'epilogue'.");

  if (is_parsing_occbin_constraints())
    error("The 'var_expectation' operator is forbidden in 'occbin_constraints'.");

  return data_tree->AddVarExpectation(model_name);
}

expr_t
ParsingDriver::add_pac_expectation(const string& model_name)
{
  if (is_parsing_epilogue())
    error("The 'pac_expectation' operator is forbidden in 'epilogue'.");

  if (is_parsing_occbin_constraints())
    error("The 'pac_expectation' operator is forbidden in 'occbin_constraints'.");

  return data_tree->AddPacExpectation(model_name);
}

expr_t
ParsingDriver::add_pac_target_nonstationary(const string& model_name)
{
  if (is_parsing_epilogue())
    error("The 'pac_target_nonstationary' operator is forbidden in 'epilogue'.");

  if (is_parsing_occbin_constraints())
    error("The 'pac_target_nonstationary' operator is forbidden in 'occbin_constraints'.");

  return data_tree->AddPacTargetNonstationary(model_name);
}

void
ParsingDriver::begin_pac_model()
{
  pac_growth = nullptr;
  pac_auxname.clear();
  pac_kind = PacTargetKind::unspecified;
}

void
ParsingDriver::pac_model()
{
  try
    {
      auto discount {options_list.get<OptionsList::StringVal>("pac.discount")};
      check_symbol_is_parameter(discount);
      mod_file->pac_model_table.addPacModel(
          options_list.get<OptionsList::StringVal>("pac.model_name"),
          options_list.get_if<OptionsList::StringVal>("pac.aux_model_name")
              .value_or(OptionsList::StringVal {}),
          move(discount), pac_growth, pac_auxname, pac_kind);
    }
  catch (OptionsList::UnknownOptionException& e)
    {
      error("You must pass the '" + e.name.substr(4) + "' option to the 'pac_model' statement.");
    }

  options_list.clear();
}

void
ParsingDriver::set_pac_growth(expr_t pac_growth_arg)
{
  pac_growth = pac_growth_arg;
  reset_data_tree();
}

void
ParsingDriver::set_pac_auxname(string auxname)
{
  pac_auxname = move(auxname);
}

void
ParsingDriver::set_pac_kind(PacTargetKind kind)
{
  pac_kind = kind;
}

expr_t
ParsingDriver::add_exp(expr_t arg1)
{
  return data_tree->AddExp(arg1);
}

expr_t
ParsingDriver::add_diff(expr_t arg1)
{
  return data_tree->AddDiff(arg1);
}

expr_t
ParsingDriver::add_log(expr_t arg1)
{
  return data_tree->AddLog(arg1);
}

expr_t
ParsingDriver::add_log10(expr_t arg1)
{
  return data_tree->AddLog10(arg1);
}

expr_t
ParsingDriver::add_cos(expr_t arg1)
{
  return data_tree->AddCos(arg1);
}

expr_t
ParsingDriver::add_sin(expr_t arg1)
{
  return data_tree->AddSin(arg1);
}

expr_t
ParsingDriver::add_tan(expr_t arg1)
{
  return data_tree->AddTan(arg1);
}

expr_t
ParsingDriver::add_acos(expr_t arg1)
{
  return data_tree->AddAcos(arg1);
}

expr_t
ParsingDriver::add_asin(expr_t arg1)
{
  return data_tree->AddAsin(arg1);
}

expr_t
ParsingDriver::add_atan(expr_t arg1)
{
  return data_tree->AddAtan(arg1);
}

expr_t
ParsingDriver::add_cosh(expr_t arg1)
{
  return data_tree->AddCosh(arg1);
}

expr_t
ParsingDriver::add_sinh(expr_t arg1)
{
  return data_tree->AddSinh(arg1);
}

expr_t
ParsingDriver::add_tanh(expr_t arg1)
{
  return data_tree->AddTanh(arg1);
}

expr_t
ParsingDriver::add_acosh(expr_t arg1)
{
  return data_tree->AddAcosh(arg1);
}

expr_t
ParsingDriver::add_asinh(expr_t arg1)
{
  return data_tree->AddAsinh(arg1);
}

expr_t
ParsingDriver::add_atanh(expr_t arg1)
{
  return data_tree->AddAtanh(arg1);
}

expr_t
ParsingDriver::add_sqrt(expr_t arg1)
{
  return data_tree->AddSqrt(arg1);
}

expr_t
ParsingDriver::add_cbrt(expr_t arg1)
{
  return data_tree->AddCbrt(arg1);
}

expr_t
ParsingDriver::add_abs(expr_t arg1)
{
  return data_tree->AddAbs(arg1);
}

expr_t
ParsingDriver::add_sign(expr_t arg1)
{
  return data_tree->AddSign(arg1);
}

expr_t
ParsingDriver::add_max(expr_t arg1, expr_t arg2)
{
  return data_tree->AddMax(arg1, arg2);
}

expr_t
ParsingDriver::add_min(expr_t arg1, expr_t arg2)
{
  return data_tree->AddMin(arg1, arg2);
}

expr_t
ParsingDriver::add_normcdf(expr_t arg1, expr_t arg2, expr_t arg3)
{
  return data_tree->AddNormcdf(arg1, arg2, arg3);
}

expr_t
ParsingDriver::add_normcdf(expr_t arg)
{
  return add_normcdf(arg, data_tree->Zero, data_tree->One);
}

expr_t
ParsingDriver::add_normpdf(expr_t arg1, expr_t arg2, expr_t arg3)
{
  return data_tree->AddNormpdf(arg1, arg2, arg3);
}

expr_t
ParsingDriver::add_normpdf(expr_t arg)
{
  return add_normpdf(arg, data_tree->Zero, data_tree->One);
}

expr_t
ParsingDriver::add_erf(expr_t arg1)
{
  return data_tree->AddErf(arg1);
}

expr_t
ParsingDriver::add_erfc(expr_t arg1)
{
  return data_tree->AddErfc(arg1);
}

expr_t
ParsingDriver::add_steady_state(expr_t arg1)
{
  if (is_parsing_epilogue())
    error("The STEADY_STATE() operator is forbidden in epilogue block");

  // Forbid exogenous variables, see dynare#825
  if (arg1->hasExogenous())
    error("Exogenous variables are not allowed in the context of the STEADY_STATE() operator.");

  return data_tree->AddSteadyState(arg1);
}

expr_t
ParsingDriver::add_sum(expr_t arg)
{
  if (is_parsing_epilogue())
    error("The SUM() operator is forbidden in epilogue block");

  if (is_parsing_occbin_constraints())
    error("The SUM() operator is forbidden in occbin_constraints block");

  if (is_parsing_planner_objective())
    error("The SUM() operator is forbidden in planner_objective statement");

  if (heterogeneous_model)
    error("The SUM() operator cannot be used inside a model(heterogeneity=...) block");

  VariableNode* varg {dynamic_cast<VariableNode*>(arg)};
  if (!varg)
    error("The argument to the SUM() operator must be a single variable");
  if (varg->lag != 0)
    error("The argument to the SUM() operator must not have a lead or lag");
  if (mod_file->symbol_table.getType(varg->symb_id) != SymbolType::heterogeneousEndogenous)
    error("The argument to the SUM() operator must be a heterogeneous endogenous variable");

  return data_tree->AddSum(arg);
}

void
ParsingDriver::external_function(const map<string, string>& options)
{
  optional<int> external_function_id;
  ExternalFunctionsTable::external_function_options_t external_function_options;

  for (const auto& [key, value] : options)
    if (key == "name")
      {
        if (value.empty())
          error("An argument must be passed to the 'name' option of the external_function() "
                "statement.");
        declare_symbol(value, SymbolType::externalFunction, "", {}, {});
        external_function_id = mod_file->symbol_table.getID(value);
      }
    else if (key == "first_deriv_provided")
      {
        if (value.empty())
          external_function_options.firstDerivSymbID
              = ExternalFunctionsTable::IDSetButNoNameProvided;
        else
          {
            int symb_id = declare_symbol(value, SymbolType::externalFunction, "", {}, {});
            external_function_options.firstDerivSymbID = symb_id;
          }
      }
    else if (key == "second_deriv_provided")
      {
        if (value.empty())
          external_function_options.secondDerivSymbID
              = ExternalFunctionsTable::IDSetButNoNameProvided;
        else
          {
            int symb_id = declare_symbol(value, SymbolType::externalFunction, "", {}, {});
            external_function_options.secondDerivSymbID = symb_id;
          }
      }
    else if (key == "nargs")
      external_function_options.nargs = stoi(value);
    else
      {
        std::cerr << "ParsingDriver::external_function_option(): unexpected error" << '\n';
        exit(EXIT_FAILURE);
      }

  if (!external_function_id)
    error("The 'name' option must be passed to external_function().");

  if (external_function_options.secondDerivSymbID >= 0
      && external_function_options.firstDerivSymbID == ExternalFunctionsTable::IDNotSet)
    error("If the second derivative is provided to the external_function command, the first "
          "derivative must also be provided.");

  if (external_function_options.secondDerivSymbID == ExternalFunctionsTable::IDSetButNoNameProvided
      && external_function_options.firstDerivSymbID
             != ExternalFunctionsTable::IDSetButNoNameProvided)
    error("If the second derivative is provided in the top-level function, the first derivative "
          "must also be provided in that function.");

  mod_file->external_functions_table.addExternalFunction(*external_function_id,
                                                         external_function_options, true);
}

expr_t
ParsingDriver::add_lead_lag_var_or_external_function(const string& name, vector<expr_t> arguments,
                                                     bool in_model_expression)
{
  assert(!arguments.empty());

  if (mod_file->symbol_table.exists(name))
    if (mod_file->symbol_table.getType(name) != SymbolType::externalFunction)
      {
        if (!in_model_expression)
          error("Using variable " + name + " with a lead or a lag is not allowed in this context");

        // e.g. model_var(lag) => ADD MODEL VARIABLE WITH LEAD (NumConstNode)/LAG (UnaryOpNode)
        if (undeclared_model_vars.contains(name))
          undeclared_model_variable_error("Unknown symbol: " + name, name);

        if (arguments.size() > 1)
          error("Symbol " + name
                + " is being treated as if it were a function (it is given several arguments)");

        try
          {
            auto lead_lag = arguments.front()->matchIntegerConstant();
            return add_model_variable(mod_file->symbol_table.getID(name), lead_lag);
          }
        catch (ExprNode::MatchFailureException&)
          {
            error("Symbol " + name
                  + " is being treated as if it were a function (i.e., takes an argument that is "
                    "not an integer).");
          }
      }
    else
      { // e.g. this function has already been referenced (either ad hoc or through the
        // external_function() statement
        // => check that the information matches previously declared info
        int symb_id = mod_file->symbol_table.getID(name);
        if (!mod_file->external_functions_table.exists(symb_id))
          error("Using a derivative of an external function (" + name
                + ") in the model block is currently not allowed.");

        if (in_model_expression)
          {
            if (mod_file->external_functions_table.getNargs(symb_id)
                == ExternalFunctionsTable::IDNotSet)
              error("Before using " + name
                    + "() in the model block, you must first declare it via the "
                      "external_function() statement");
            else if (static_cast<int>(arguments.size())
                     != mod_file->external_functions_table.getNargs(symb_id))
              error("The number of arguments passed to " + name
                    + "() does not match those of a previous call or declaration of this "
                      "function.");
          }
      }
  else
    { /* First time encountering this external function or variable i.e., not previously declared
         or encountered */
      if (is_parsing_epilogue())
        error("Variable " + name + " used in the epilogue block but was not declared.");

      if (in_model_expression)
        {
          // Continue processing, noting that it was not declared
          // Processing will end at the end of the model block if nostrict was not passed
          undeclared_model_vars.insert(name);
          undeclared_model_variable_error("Unknown symbol: " + name, name);

          /* If it has a single integer argument and no namespace qualifier, assume that it’s a
             lead/lagged exogenous variable */
          if (arguments.size() == 1 && name.find('.') == string::npos)
            try
              {
                auto lead_lag = arguments.front()->matchIntegerConstant();
                int symb_id = declare_exogenous(name);
                return add_model_variable(symb_id, lead_lag);
              }
            catch (ExprNode::MatchFailureException&)
              {
              }

          error("To use an external function (" + name
                + ") within the model block, you must first declare it via the "
                  "external_function() statement.");
        }
      int symb_id = declare_symbol(name, SymbolType::externalFunction, "", {}, {});
      ExternalFunctionsTable::external_function_options_t external_function_options;
      external_function_options.nargs = arguments.size();
      mod_file->external_functions_table.addExternalFunction(symb_id, external_function_options,
                                                             in_model_expression);
    }

  /* By this point, we're sure that this function exists in the External Functions Table and is
     not a model var */
  int symb_id = mod_file->symbol_table.getID(name);
  return data_tree->AddExternalFunction(symb_id, move(arguments));
}

void
ParsingDriver::add_native(string s)
{
  mod_file->addStatement(make_unique<NativeStatement>(move(s)));
}

void
ParsingDriver::add_native_remove_charset(string_view str, string_view token)
{
  size_t found = str.find(token);
  assert(found != string_view::npos);
  add_native(string {str.substr(0, found)});
}

void
ParsingDriver::add_verbatim(string s)
{
  mod_file->addStatement(make_unique<VerbatimStatement>(move(s)));
}

void
ParsingDriver::add_verbatim_remove_charset(string_view str, string_view token)
{
  size_t found = str.find(token);
  assert(found != string_view::npos);
  add_verbatim(string {str.substr(0, found)});
}

void
ParsingDriver::begin_steady_state_model()
{
  set_current_data_tree(&mod_file->steady_state_model);
}

void
ParsingDriver::add_steady_state_model_equal(const string& varname, expr_t expr)
{
  int id;
  try
    {
      id = mod_file->symbol_table.getID(varname);
    }
  catch (SymbolTable::UnknownSymbolNameException& e)
    {
      // Unknown symbol, declare it as a ModFileLocalVariable
      id = mod_file->symbol_table.addSymbol(varname, SymbolType::modFileLocalVariable);
    }

  if (SymbolType type = mod_file->symbol_table.getType(id);
      type != SymbolType::endogenous && type != SymbolType::modFileLocalVariable
      && type != SymbolType::parameter)
    error(varname + " has incorrect type");

  mod_file->steady_state_model.addDefinition(id, expr, location.begin.line);
}

void
ParsingDriver::add_steady_state_model_equal_multiple(const vector<string>& symbol_list, expr_t expr)
{
  vector<int> ids;

  for (const auto& symb : symbol_list)
    {
      int id;
      try
        {
          id = mod_file->symbol_table.getID(symb);
        }
      catch (SymbolTable::UnknownSymbolNameException& e)
        {
          // Unknown symbol, declare it as a ModFileLocalVariable
          id = mod_file->symbol_table.addSymbol(symb, SymbolType::modFileLocalVariable);
        }
      if (SymbolType type = mod_file->symbol_table.getType(id);
          type != SymbolType::endogenous && type != SymbolType::modFileLocalVariable
          && type != SymbolType::parameter)
        error(symb + " has incorrect type");
      ids.push_back(id);
    }

  mod_file->steady_state_model.addMultipleDefinitions(move(ids), expr, location.begin.line);
}

void
ParsingDriver::add_steady_state_model_solve_from_equation(const string& symbol,
                                                          const string& eqname)
{
  int symb_id {[&] {
    try
      {
        return mod_file->symbol_table.getID(symbol);
      }
    catch (SymbolTable::UnknownSymbolNameException& e)
      {
        error("Unknown symbol: " + symbol);
      }
  }()};

  // NB: model-local variables cannot be defined in this way (we substitute them out below)
  if (SymbolType type = mod_file->symbol_table.getType(symb_id);
      type != SymbolType::endogenous && type != SymbolType::parameter)
    error(symbol + " has incorrect type");

  /* NB: we derive the implicit definition of the symbol from the dynamic model before any
     transformation, because otherwise it may include auxiliary variables that will complicate the
     symbolic manipulation. As a consequence, we must deal with some needed transformations here
     (model local variables substitution). */

  /* Retrieve the equation corresponding to the given name.
     static-only equations have priority over dynamic ones. */
  BinaryOpNode* eq_dynamic {[&] {
    try
      {
        // First try static-only equations
        auto eqns = mod_file->dynamic_model.getStaticOnlyEquationNumbersFromNames({eqname});
        if (eqns.size() > 1)
          error("There are several equations named '" + eqname + "' and marked [static]");
        return mod_file->dynamic_model.getStaticOnlyEquation(*eqns.begin());
      }
    catch (ModelTree::UnknownEquationNameException&)
      {
        // If no static-only equation found, search among dynamic one
        try
          {
            auto eqns = mod_file->dynamic_model.getEquationNumbersFromNames({eqname});
            if (eqns.size() > 1)
              error("There are several equations named '" + eqname + "'");
            return mod_file->dynamic_model.getEquation(*eqns.begin());
          }
        catch (ModelTree::UnknownEquationNameException&)
          {
            error("There is no equation named '" + eqname + "'");
          }
      }
  }()};

  // Substitute out model-local variables
  try
    {
      eq_dynamic = dynamic_cast<BinaryOpNode*>(eq_dynamic->substituteModelLocalVariables());
    }
  catch (DataTree::UnknownLocalVariableException& e)
    {
      error("Model-local variable '" + mod_file->symbol_table.getName(e.id)
            + "' is not defined in equation '" + eqname + "'");
    }

  // Convert the equation to a static form
  BinaryOpNode* eq_static {[&] {
    try
      {
        return dynamic_cast<BinaryOpNode*>(eq_dynamic->toStatic(mod_file->steady_state_model));
      }
    catch (ExprNode::StaticConversionException& e)
      {
        error("The equation named '" + eqname
              + "' cannot be converted to a static expression: " + e.message);
      }
  }()};

  // Solve for the symbol definition by normalizing the equation
  expr_t value {[&] {
    try
      {
        BinaryOpNode* normalized_eq = eq_static->normalizeEquation(symb_id, 0);
        return normalized_eq->arg2;
      }
    catch (ExprNode::NormalizationFailed&)
      {
        error("The equation named '" + eqname
              + "' could not be rearranged to implicitly define symbol + '" + symbol + "'");
      }
  }()};

  mod_file->steady_state_model.addDefinition(symb_id, value, location.begin.line);
}

void
ParsingDriver::add_graph_format(string name)
{
  graph_formats.emplace_back(move(name));
}

void
ParsingDriver::process_graph_format_option()
{
  options_list.set("graph_format", OptionsList::SymbolListVal {move(graph_formats)});
  graph_formats.clear();
}

void
ParsingDriver::initial_condition_decomp_process_graph_format_option()
{
  options_list.set("initial_condition_decomp.graph_format",
                   OptionsList::SymbolListVal {move(graph_formats)});
  graph_formats.clear();
}

void
ParsingDriver::plot_shock_decomp_process_graph_format_option()
{
  options_list.set("plot_shock_decomp.graph_format",
                   OptionsList::SymbolListVal {move(graph_formats)});
  graph_formats.clear();
}

void
ParsingDriver::model_diagnostics()
{
  mod_file->addStatement(make_unique<ModelDiagnosticsStatement>());
}

void
ParsingDriver::add_parallel_local_file(string filename)
{
  mod_file->parallel_local_files.push_back(move(filename));
}

void
ParsingDriver::add_moment_calibration_item(const string& endo1, const string& endo2, string lags,
                                           const pair<expr_t, expr_t>& range)
{
  MomentCalibration::Constraint c;

  check_symbol_is_endogenous(endo1);
  c.endo1 = mod_file->symbol_table.getID(endo1);

  check_symbol_is_endogenous(endo2);
  c.endo2 = mod_file->symbol_table.getID(endo2);

  c.lags = move(lags);

  c.lower_bound = range.first;
  c.upper_bound = range.second;

  moment_calibration_constraints.push_back(move(c));
}

void
ParsingDriver::end_moment_calibration()
{
  mod_file->addStatement(
      make_unique<MomentCalibration>(move(moment_calibration_constraints), mod_file->symbol_table));
  moment_calibration_constraints.clear();
}

void
ParsingDriver::add_irf_calibration_item(const string& endo, string periods, const string& exo,
                                        const pair<expr_t, expr_t>& range)
{
  IrfCalibration::Constraint c;

  check_symbol_is_endogenous(endo);
  c.endo = mod_file->symbol_table.getID(endo);

  c.periods = move(periods);

  check_symbol_existence(exo);
  c.exo = mod_file->symbol_table.getID(exo);
  if (mod_file->symbol_table.getType(exo) != SymbolType::exogenous)
    error("Variable " + endo + " is not an exogenous.");

  c.lower_bound = range.first;
  c.upper_bound = range.second;

  irf_calibration_constraints.push_back(move(c));
}

void
ParsingDriver::end_irf_calibration()
{
  mod_file->addStatement(make_unique<IrfCalibration>(move(irf_calibration_constraints),
                                                     mod_file->symbol_table, move(options_list)));
  irf_calibration_constraints.clear();
  options_list.clear();
}

void
ParsingDriver::smoother2histval()
{
  mod_file->addStatement(make_unique<Smoother2histvalStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::histval_file()
{
  mod_file->addStatement(make_unique<HistvalFileStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::perfect_foresight_setup()
{
  mod_file->addStatement(make_unique<PerfectForesightSetupStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::perfect_foresight_solver()
{
  mod_file->addStatement(make_unique<PerfectForesightSolverStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::perfect_foresight_with_expectation_errors_setup()
{
  mod_file->addStatement(
      make_unique<PerfectForesightWithExpectationErrorsSetupStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::perfect_foresight_with_expectation_errors_solver()
{
  mod_file->addStatement(
      make_unique<PerfectForesightWithExpectationErrorsSolverStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::perfect_foresight_controlled_paths(
    const vector<tuple<string, vector<AbstractShocksStatement::period_range_t>, vector<expr_t>,
                       string>>& paths,
    variant<int, string> learnt_in_period)
{
  if (holds_alternative<int>(learnt_in_period) && get<int>(learnt_in_period) < 1)
    error("Value '" + to_string(get<int>(learnt_in_period))
          + "' is not allowed for 'learnt_in' option");

  PerfectForesightControlledPathsStatement::paths_t paths_transformed;
  for (const auto& [exogenize, periods, values, endogenize] : paths)
    {
      int exogenize_id = mod_file->symbol_table.getID(exogenize);
      int endogenize_id = mod_file->symbol_table.getID(endogenize);

      vector<pair<AbstractShocksStatement::period_range_t, expr_t>> v;

      v.reserve(periods.size());
      for (size_t i = 0; i < periods.size(); i++)
        v.emplace_back(periods[i], values[i]);

      paths_transformed.emplace_back(exogenize_id, move(v), endogenize_id);
    }
  mod_file->addStatement(make_unique<PerfectForesightControlledPathsStatement>(
      move(paths_transformed), move(learnt_in_period), mod_file->symbol_table));
}

void
ParsingDriver::begin_shock_paths_controlled_elem()
{
  is_parsing_shock_paths_controlled = true;
}

void
ParsingDriver::end_shock_paths_controlled_elem(
    const string& exogenize, const vector<AbstractShocksStatement::period_range_t>& periods,
    const vector<expr_t>& values, const string& endogenize)
{
  check_symbol_is_endogenous(exogenize);
  check_symbol_is_exogenous(endogenize, false);
  if (periods.size() != values.size())
    error("The number of periods is different from the number of values");

  int exogenize_id {mod_file->symbol_table.getID(exogenize)};
  int endogenize_id {mod_file->symbol_table.getID(endogenize)};

  vector<pair<AbstractShocksStatement::period_range_t, expr_t>> v;

  v.reserve(periods.size());
  for (size_t i {0}; i < periods.size(); i++)
    v.emplace_back(periods[i], values[i]);

  shock_paths_controlled.emplace_back(exogenize_id, move(v), endogenize_id);

  is_parsing_shock_paths_controlled = false;
}

void
ParsingDriver::method_of_moments()
{
  mod_file->addStatement(make_unique<MethodOfMomentsStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::prior_posterior_function(bool prior_func)
{
  mod_file->addStatement(make_unique<PriorPosteriorFunctionStatement>(static_cast<bool>(prior_func),
                                                                      move(options_list)));
  options_list.clear();
}

void
ParsingDriver::begin_ramsey_constraints()
{
  set_current_data_tree(&mod_file->dynamic_model);
}

void
ParsingDriver::end_ramsey_constraints(const vector<expr_t>& constraints)
{
  for (expr_t c : constraints)
    try
      {
        auto [symb_id, lower_bound, upper_bound] = c->matchComplementarityCondition();

        auto [it, success]
            = mod_file->ramsey_constraints.try_emplace(symb_id, lower_bound, upper_bound);
        if (!success)
          error("The ramsey_constraints block contains two constraints for variable "
                + mod_file->symbol_table.getName(symb_id));
      }
    catch (ExprNode::MatchFailureException& e)
      {
        error("Ramsey constraint has an incorrect form: " + e.message);
      }

  reset_data_tree();
}

void
ParsingDriver::add_shock_group_element(string name)
{
  check_symbol_existence(name);
  int symb_id = mod_file->symbol_table.getID(name);

  if (mod_file->symbol_table.getType(symb_id) != SymbolType::exogenous)
    error("shock_groups: " + name + " should be an exogenous variable");

  shock_group.push_back(move(name));
}

void
ParsingDriver::add_shock_group(string name)
{
  ShockGroupsStatement::Group G;
  G.name = move(name);
  G.list = shock_group;
  shock_groups.push_back(move(G));

  shock_group.clear();
}

void
ParsingDriver::end_shock_groups(string name)
{
  mod_file->addStatement(make_unique<ShockGroupsStatement>(move(shock_groups), move(name)));
  shock_groups.clear();
}

void
ParsingDriver::add_init2shocks(const string& endo_name, const string& exo_name)
{
  check_symbol_existence(endo_name);
  check_symbol_existence(exo_name);
  int symb_id_endo = mod_file->symbol_table.getID(endo_name);
  if (mod_file->symbol_table.getType(symb_id_endo) != SymbolType::endogenous)
    error("init2shocks: " + endo_name + " should be an endogenous variable");

  int symb_id_exo = mod_file->symbol_table.getID(exo_name);
  if (mod_file->symbol_table.getType(symb_id_exo) != SymbolType::exogenous)
    error("init2shocks: " + exo_name + " should be an exogenous variable");

  init2shocks.emplace_back(symb_id_endo, symb_id_exo);
}

void
ParsingDriver::end_init2shocks(string name)
{
  mod_file->addStatement(
      make_unique<Init2shocksStatement>(move(init2shocks), move(name), mod_file->symbol_table));
  init2shocks.clear();
}

void
ParsingDriver::var_expectation_model()
{
  try
    {
      string v {options_list.get<OptionsList::StringVal>("variable")};
      if (var_expectation_model_expression)
        error("You can't pass both the 'variable' or the 'expression' options to the "
              "var_expectation_model statement.");
      var_expectation_model_expression = data_tree->AddVariable(mod_file->symbol_table.getID(v));
    }
  catch (OptionsList::UnknownOptionException&)
    {
      if (!var_expectation_model_expression)
        error("You must pass either the 'variable' or the 'expression' option to the "
              "var_expectation_model statement.");
    }

  if (var_expectation_model_discount)
    {
      VariableNode* var;
      if (!dynamic_cast<NumConstNode*>(var_expectation_model_discount)
          && !((var = dynamic_cast<VariableNode*>(var_expectation_model_discount))
               && var->get_type() == SymbolType::parameter))
        error("The discount factor must be a constant expression or a parameter");
    }
  else
    var_expectation_model_discount = data_tree->One;

  int time_shift {stoi(
      options_list.get_if<OptionsList::NumVal>("time_shift").value_or(OptionsList::NumVal {"0"}))};
  if (time_shift > 0)
    error("The 'time_shift' option must be a non-positive integer");

  try
    {
      mod_file->var_expectation_model_table.addVarExpectationModel(
          options_list.get<OptionsList::StringVal>("model_name"), var_expectation_model_expression,
          options_list.get<OptionsList::StringVal>("auxiliary_model_name"),
          options_list.get<OptionsList::NumVal>("horizon"), var_expectation_model_discount,
          time_shift);
    }
  catch (OptionsList::UnknownOptionException& e)
    {
      error("You must pass the '" + e.name + "' option to the 'var_expectation_model' statement.");
    }

  options_list.clear();
  var_expectation_model_discount = nullptr;
  var_expectation_model_expression = nullptr;
}

void
ParsingDriver::begin_matched_moments()
{
  set_current_data_tree(&mod_file->dynamic_model);
}

void
ParsingDriver::end_matched_moments(const vector<expr_t>& moments)
{
  vector<tuple<vector<int>, vector<int>, vector<int>>> parsed_moments;
  for (auto m : moments)
    try
      {
        vector<int> symb_ids, lags, powers;
        m->matchMatchedMoment(symb_ids, lags, powers);
        parsed_moments.emplace_back(move(symb_ids), move(lags), move(powers));
      }
    catch (ExprNode::MatchFailureException& e)
      {
        error("Matched moment expression has incorrect format: " + e.message);
      }
  mod_file->addStatement(
      make_unique<MatchedMomentsStatement>(mod_file->symbol_table, move(parsed_moments)));

  reset_data_tree();
}

void
ParsingDriver::begin_occbin_constraints()
{
  /* We use a separate data tree, because we will add inequality constraints,
     and those would trigger the non-linearity warning in a stochastic context
     if added to the main DynamicModel tree. It also simplifies the
     enforcement of various constraints at parsing time. */
  occbin_constraints_tree = make_unique<DataTree>(
      mod_file->symbol_table, mod_file->num_constants, mod_file->external_functions_table,
      mod_file->heterogeneity_table, mod_file->database_table, false);
  set_current_data_tree(occbin_constraints_tree.get());
}

void
ParsingDriver::end_occbin_constraints(
    vector<tuple<string, BinaryOpNode*, BinaryOpNode*, expr_t, expr_t>> constraints)
{
  // Perform a few checks
  for (const auto& [name, bind, relax, error_bind, error_relax] : constraints)
    {
      string param_name = buildOccbinBindParamName(name);
      if (!mod_file->symbol_table.exists(param_name))
        error("No equation has been declared for constraint '" + name + "'");
      if (!bind)
        error("The 'bind' expression is missing in constraint '" + name + "'");
    }

  mod_file->addStatement(
      make_unique<OccbinConstraintsStatement>(*occbin_constraints_tree, move(constraints)));

  reset_data_tree();
}

void
ParsingDriver::begin_pac_target_info(string name)
{
  pac_target_info_name = move(name);
  set_current_data_tree(&mod_file->dynamic_model);
}

void
ParsingDriver::end_pac_target_info()
{
  reset_data_tree();
}

void
ParsingDriver::set_pac_target_info_target(expr_t target)
{
  mod_file->pac_model_table.setTargetExpr(pac_target_info_name, target);
}

void
ParsingDriver::set_pac_target_info_auxname_target_nonstationary(string auxname)
{
  mod_file->pac_model_table.setTargetAuxnameNonstationary(pac_target_info_name, move(auxname));
}

void
ParsingDriver::add_pac_target_info_component(expr_t component_expr)
{
  get<0>(pac_target_info_component) = component_expr;
  mod_file->pac_model_table.addTargetComponent(pac_target_info_name,
                                               exchange(pac_target_info_component, {}));
}

void
ParsingDriver::set_pac_target_info_component_growth(expr_t growth)
{
  get<1>(pac_target_info_component) = growth;
}

void
ParsingDriver::set_pac_target_info_component_auxname(string auxname)
{
  get<2>(pac_target_info_component) = move(auxname);
}

void
ParsingDriver::set_pac_target_info_component_kind(PacTargetKind kind)
{
  get<3>(pac_target_info_component) = kind;
}

bool
ParsingDriver::isSymbolIdentifier(const string& str)
{
  if (str.empty())
    return false;
  auto myisalpha = [](char ch) {
    // We cannot use std::isalpha(), because it is locale-dependent
    return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');
  };
  if (!(myisalpha(str[0]) || str[0] == '_'))
    return false;
  for (size_t i = 1; i < str.size(); i++)
    if (!(myisalpha(str[i]) || isdigit(static_cast<unsigned char>(str[i])) || str[i] == '_'))
      return false;
  return true;
}

void
ParsingDriver::model_remove(const vector<map<string, string>>& listed_eqs_by_tags)
{
  mod_file->dynamic_model.removeEquations(listed_eqs_by_tags, true, true);
}

void
ParsingDriver::begin_model_replace(const vector<map<string, string>>& listed_eqs_by_tags)
{
  mod_file->dynamic_model.removeEquations(listed_eqs_by_tags, true, false);
  set_current_data_tree(&mod_file->dynamic_model);
}

void
ParsingDriver::var_remove(const vector<string>& symbol_list)
{
  for (const auto& name : symbol_list)
    {
      check_symbol_existence(name);
      int symb_id = mod_file->symbol_table.getID(name);
      mod_file->symbol_table.changeType(symb_id, SymbolType::excludedVariable);
    }
}

void
ParsingDriver::resid()
{
  mod_file->addStatement(make_unique<ResidStatement>(move(options_list)));
  options_list.clear();
}

void
ParsingDriver::matched_irfs(MatchedIrfsStatement::matched_irfs_t values_weights, bool overwrite)
{
  mod_file->addStatement(make_unique<MatchedIrfsStatement>(move(values_weights), overwrite));
}

void
ParsingDriver::matched_irfs_weights(MatchedIrfsWeightsStatement::matched_irfs_weights_t weights,
                                    bool overwrite)
{
  mod_file->addStatement(make_unique<MatchedIrfsWeightsStatement>(move(weights), overwrite));
}

void
ParsingDriver::heterogeneity_dimension(const vector<string>& dims)
{
  for (const auto& dim : dims)
    {
      int het_dim_id {[&] {
        try
          {
            return mod_file->heterogeneity_table.addDimension(dim);
          }
        catch (HeterogeneityTable::AlreadyDeclaredDimensionException&)
          {
            error("Heterogeneity dimension '" + dim + "' already declared");
          }
      }()};

      assert(static_cast<int>(mod_file->heterogeneous_models.size()) == het_dim_id);
      mod_file->heterogeneous_models.emplace_back(
          mod_file->symbol_table, mod_file->num_constants, mod_file->external_functions_table,
          mod_file->heterogeneity_table, mod_file->database_table, het_dim_id);
    }
}

void
ParsingDriver::database(const vector<string>& names)
{
  for (const auto& name : names)
    try
      {
        mod_file->database_table.addDatabase(name);
      }
    catch (DatabaseTable::AlreadyDeclaredDatabaseException&)
      {
        error("Database '" + name + "' already declared");
      }
}

bool
ParsingDriver::database_exists(const string& name) const
{
  return mod_file->database_table.exists(name);
}

void
ParsingDriver::begin_shock_paths(const variant<int, string>& learnt_in_period)
{
  set_current_data_tree(&mod_file->shock_paths_tree);

  if (holds_alternative<int>(learnt_in_period) && get<int>(learnt_in_period) < 1)
    error("Value '" + to_string(get<int>(learnt_in_period))
          + "' is not allowed for 'learnt_in' option");
  shock_paths_learnt_in_period = learnt_in_period;
}

void
ParsingDriver::end_shock_paths(bool overwrite)
{
  mod_file->addStatement(make_unique<ShockPathsStatement>(
      shock_paths_learnt_in_period, overwrite, move(shock_paths_exo), move(shock_paths_controlled),
      mod_file->symbol_table));
  reset_data_tree();
  shock_paths_exo.clear();
  shock_paths_controlled.clear();
}

void
ParsingDriver::add_filter_tunes_elem(const string& name,
                                     vector<AbstractShocksStatement::period_range_t>&& periods,
                                     const vector<expr_t>& values, const vector<expr_t>& stderrs)
{
  check_symbol_is_endogenous(name);

  if (periods.size() != values.size())
    error("filter_tunes: variable " + name
          + ": number of periods is different from number of shock values");

  if (stderrs.size() > 1 && stderrs.size() != periods.size())
    error("filter_tunes: variable " + name
          + ": heteroskedastic measurement errors but number of measurement errors is different "
            "from number of periods");

  auto& v = tunes[mod_file->symbol_table.getID(name)];

  v.reserve(v.size() + periods.size());
  for (size_t i {0}; i < periods.size(); i++)
    {
      expr_t measurement_error {data_tree->Zero};
      if (stderrs.size() == 1)
        measurement_error = stderrs.front();
      else if (stderrs.size() > 1)
        measurement_error = stderrs[i];

      v.emplace_back(move(periods[i]), values[i], measurement_error);
    }
}

void
ParsingDriver::end_filter_tunes()
{
  mod_file->addStatement(make_unique<FilterTunesStatement>(move(tunes), mod_file->symbol_table));
  tunes.clear();
}
