/*
 * Copyright © 2006-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 <cstdlib>
#include <fstream>
#include <iostream>
#include <typeinfo>

#include <filesystem>

#include "ComputingTasks.hh"

#include "ModFile.hh"
#include "Shocks.hh"
#include "Utils.hh"

ModFile::ModFile(WarningConsolidation& warnings_arg) :
    symbol_table {heterogeneity_table},
    var_model_table {symbol_table},
    trend_component_model_table {symbol_table},
    var_expectation_model_table {symbol_table},
    pac_model_table {symbol_table},
    expressions_tree {symbol_table, num_constants, external_functions_table, heterogeneity_table,
                      database_table},
    shock_paths_tree {symbol_table, num_constants, external_functions_table, heterogeneity_table,
                      database_table},
    original_model {symbol_table,        num_constants,  external_functions_table,
                    heterogeneity_table, database_table, trend_component_model_table,
                    var_model_table},
    dynamic_model {symbol_table,        num_constants,  external_functions_table,
                   heterogeneity_table, database_table, trend_component_model_table,
                   var_model_table},
    trend_dynamic_model {symbol_table,        num_constants,  external_functions_table,
                         heterogeneity_table, database_table, trend_component_model_table,
                         var_model_table},
    orig_ramsey_dynamic_model {symbol_table,        num_constants,  external_functions_table,
                               heterogeneity_table, database_table, trend_component_model_table,
                               var_model_table},
    epilogue {symbol_table, num_constants, external_functions_table, heterogeneity_table,
              database_table},
    static_model {symbol_table, num_constants, external_functions_table, heterogeneity_table,
                  database_table},
    steady_state_model {symbol_table,        num_constants,  external_functions_table,
                        heterogeneity_table, database_table,

                        static_model},
    warnings {warnings_arg}
{
  heterogeneity_table.setSymbolTable(&symbol_table);
}

void
ModFile::evalAllExpressions(bool warn_uninit)
{
  cout << "Evaluating expressions..." << '\n' << flush;

  // Loop over all statements, and fill global eval context if relevant
  for (auto& st : statements)
    {
      if (auto ips = dynamic_cast<InitParamStatement*>(st.get()); ips)
        ips->fillEvalContext(global_eval_context);

      if (auto ies = dynamic_cast<InitOrEndValStatement*>(st.get()); ies)
        ies->fillEvalContext(global_eval_context);

      if (auto lpass = dynamic_cast<LoadParamsAndSteadyStateStatement*>(st.get()); lpass)
        lpass->fillEvalContext(global_eval_context);
    }

  // Evaluate model local variables
  dynamic_model.fillEvalContext(global_eval_context);

  // Check if some symbols are not initialized, and give them a zero value then
  for (int id = 0; id <= symbol_table.maxID(); id++)
    if (auto type = symbol_table.getType(id);
        (type == SymbolType::endogenous || type == SymbolType::exogenous
         || type == SymbolType::exogenousDet || type == SymbolType::parameter
         || type == SymbolType::modelLocalVariable)
        && !global_eval_context.contains(id))
      {
        if (warn_uninit)
          warnings << "WARNING: Can't find a numeric initial value for " << symbol_table.getName(id)
                   << ", using zero" << '\n';
        global_eval_context[id] = 0;
      }
}

void
ModFile::addStatement(unique_ptr<Statement> st)
{
  statements.push_back(move(st));
}

void
ModFile::addStatementAtFront(unique_ptr<Statement> st)
{
  statements.insert(statements.begin(), move(st));
}

void
ModFile::checkPass(bool nostrict, bool stochastic)
{
  for (auto& statement : statements)
    statement->checkPass(mod_file_struct, warnings);

  // Check the steady state block
  steady_state_model.checkPass(mod_file_struct, warnings);

  // Check epilogue block
  epilogue.checkPass(mod_file_struct);

  pac_model_table.checkPass(mod_file_struct);

  // Check heterogeneous models
  for (auto& hm : heterogeneous_models)
    hm.checkPass();

  if (mod_file_struct.write_latex_steady_state_model_present
      && !mod_file_struct.steady_state_model_present)
    {
      cerr << "ERROR: You cannot have a write_latex_steady_state_model statement without a "
              "steady_state_model block."
           << '\n';
      exit(EXIT_FAILURE);
    }

  // If order option has not been set, default to 2
  if (!mod_file_struct.order_option)
    mod_file_struct.order_option = 2;

  auto params_used_with_lead_lag = dynamic_model.ParamUsedWithLeadLag();
  if (!params_used_with_lead_lag.empty())
    {
      warnings << "WARNING: The following parameter(s) are used with a lead or a lag in the model "
                  "block: ";
      for (bool printed_something {false}; auto& p : params_used_with_lead_lag)
        {
          if (exchange(printed_something, true))
            warnings << " ";
          warnings << p;
        }
      warnings << '\n';
    }

  bool stochastic_statement_present = mod_file_struct.isStochasticContext() || stochastic;

  // Allow empty model only when doing a standalone BVAR estimation
  if (dynamic_model.equation_number() == 0
      && (mod_file_struct.check_present || mod_file_struct.perfect_foresight_solver_present
          || mod_file_struct.perfect_foresight_with_expectation_errors_solver_present
          || stochastic_statement_present))
    {
      cerr << "ERROR: At least one model equation must be declared!" << '\n';
      exit(EXIT_FAILURE);
    }

  if (mod_file_struct.ramsey_model_present && mod_file_struct.discretionary_policy_present)
    {
      cerr << "ERROR: You cannot use the discretionary_policy command when you use either "
              "ramsey_model or ramsey_policy and vice versa"
           << '\n';
      exit(EXIT_FAILURE);
    }

  if (((mod_file_struct.ramsey_model_present || mod_file_struct.discretionary_policy_present)
       && !mod_file_struct.planner_objective_present)
      || (!(mod_file_struct.ramsey_model_present || mod_file_struct.discretionary_policy_present
            || mod_file_struct.osr_present)
          && mod_file_struct.planner_objective_present))
    {
      cerr << "ERROR: A planner_objective statement must be used with a ramsey_model, a "
              "ramsey_policy, osr, or a discretionary_policy statement and vice versa."
           << '\n';
      exit(EXIT_FAILURE);
    }

  if (!ramsey_constraints.empty() && !mod_file_struct.ramsey_model_present)
    {
      cerr << "ERROR: A ramsey_constraints block requires the presence of a ramsey_model or "
              "ramsey_policy statement"
           << '\n';
      exit(EXIT_FAILURE);
    }

  if (mod_file_struct.osr_present)
    {
      if (!mod_file_struct.osr_params_present)
        {
          cerr << "ERROR: The osr statement requires the osr_params statement." << '\n';
          exit(EXIT_FAILURE);
        }
      if (!mod_file_struct.optim_weights_present && !mod_file_struct.planner_objective_present)
        {
          cerr << "ERROR: The osr statement requires either an optim_weights block or a "
                  "planner_objective."
               << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.optim_weights_present && mod_file_struct.planner_objective_present)
        {
          cerr << "ERROR: The osr statement cannot have both optim_weights and a "
                  "planner_objective; they are mutually exclusive."
               << '\n';
          exit(EXIT_FAILURE);
        }
    }

  if ((mod_file_struct.perfect_foresight_solver_present
       || mod_file_struct.perfect_foresight_with_expectation_errors_solver_present)
      && stochastic_statement_present)
    {
      cerr << "ERROR: A .mod file cannot contain both one of {perfect_foresight_solver, simul, "
              "perfect_foresight_with_expectation_errors_solver} and one of {stoch_simul, "
              "estimation, osr, ramsey_policy, discretionary_policy}. This is not possible: one "
              "cannot mix perfect foresight context with stochastic context in the same file."
           << '\n';
      exit(EXIT_FAILURE);
    }

  if (use_dll && bytecode)
    {
      cerr << "ERROR: In 'model' block, 'use_dll' option is not compatible with 'bytecode'" << '\n';
      exit(EXIT_FAILURE);
    }

  if (bytecode) // Cf. dynare#1998
    warnings << "WARNING: the 'bytecode' option is deprecated and will be removed in a future "
                "release of Dynare."
             << '\n'
             << "If you really depend on that option, please get in touch with the Dynare Team "
                "before it is too late."
             << '\n';

  if ((stochastic_statement_present || mod_file_struct.check_present
       || mod_file_struct.steady_present)
      && no_static)
    {
      cerr << "ERROR: no_static option is incompatible with stoch_simul, estimation, osr, "
              "ramsey_policy, discretionary_policy, steady and check commands"
           << '\n';
      exit(EXIT_FAILURE);
    }

  if (mod_file_struct.dsge_var_estimated && !mod_file_struct.dsge_prior_weight_in_estimated_params)
    {
      cerr << "ERROR: When estimating a DSGE-VAR model and estimating the weight of the prior, "
              "dsge_prior_weight must "
           << "be referenced in the estimated_params block." << '\n';
      exit(EXIT_FAILURE);
    }

  if (mod_file_struct.dsge_prior_weight_in_estimated_params)
    {
      if (!mod_file_struct.dsge_var_estimated && !mod_file_struct.dsge_var_calibrated.empty())
        {
          cerr << "ERROR: If dsge_prior_weight is in the estimated_params block, the prior weight "
                  "cannot be calibrated "
               << "via the dsge_var option in the estimation statement." << '\n';
          exit(EXIT_FAILURE);
        }
      else if (!mod_file_struct.dsge_var_estimated)
        {
          cerr << "ERROR: If dsge_prior_weight is in the estimated_params block, the dsge_var "
                  "option must be passed to the estimation statement."
               << '\n';
          exit(EXIT_FAILURE);
        }
    }

  /* This check must come before checking that there are as many static-only as dynamic-only
     equations, see #103 */
  dynamic_model.checkOccbinRegimes();

  if (dynamic_model.staticOnlyEquationsNbr() != dynamic_model.dynamicOnlyEquationsNbr())
    {
      cerr << "ERROR: the number of equations marked [static] must be equal to the number of "
              "equations marked [dynamic]"
           << '\n';
      exit(EXIT_FAILURE);
    }

  if (dynamic_model.staticOnlyEquationsNbr() > 0
      && (mod_file_struct.ramsey_model_present || mod_file_struct.discretionary_policy_present))
    {
      cerr << "ERROR: marking equations as [static] or [dynamic] is not possible with "
              "ramsey_model, ramsey_policy or discretionary_policy"
           << '\n';
      exit(EXIT_FAILURE);
    }

  if (stochastic_statement_present
      && (dynamic_model.isUnaryOpUsed(UnaryOpcode::sign)
          || dynamic_model.isUnaryOpUsed(UnaryOpcode::abs)
          || dynamic_model.isBinaryOpUsed(BinaryOpcode::max)
          || dynamic_model.isBinaryOpUsed(BinaryOpcode::min)
          || dynamic_model.isBinaryOpUsed(BinaryOpcode::greater)
          || dynamic_model.isBinaryOpUsed(BinaryOpcode::less)
          || dynamic_model.isBinaryOpUsed(BinaryOpcode::greaterEqual)
          || dynamic_model.isBinaryOpUsed(BinaryOpcode::lessEqual)
          || dynamic_model.isBinaryOpUsed(BinaryOpcode::equalEqual)
          || dynamic_model.isBinaryOpUsed(BinaryOpcode::different)))
    warnings
        << R"(WARNING: you are using a function (max, min, abs, sign) or an operator (<, >, <=, >=, ==, !=) which is unsuitable for a stochastic context; see the reference manual, section about "Expressions", for more details.)"
        << '\n';

  if (linear
      && (dynamic_model.isUnaryOpUsedOnType(SymbolType::endogenous, UnaryOpcode::sign)
          || dynamic_model.isUnaryOpUsedOnType(SymbolType::endogenous, UnaryOpcode::abs)
          || dynamic_model.isBinaryOpUsedOnType(SymbolType::endogenous, BinaryOpcode::max)
          || dynamic_model.isBinaryOpUsedOnType(SymbolType::endogenous, BinaryOpcode::min)
          || dynamic_model.isBinaryOpUsedOnType(SymbolType::endogenous, BinaryOpcode::greater)
          || dynamic_model.isBinaryOpUsedOnType(SymbolType::endogenous, BinaryOpcode::less)
          || dynamic_model.isBinaryOpUsedOnType(SymbolType::endogenous, BinaryOpcode::greaterEqual)
          || dynamic_model.isBinaryOpUsedOnType(SymbolType::endogenous, BinaryOpcode::lessEqual)
          || dynamic_model.isBinaryOpUsedOnType(SymbolType::endogenous, BinaryOpcode::equalEqual)
          || dynamic_model.isBinaryOpUsedOnType(SymbolType::endogenous, BinaryOpcode::different)))
    {
      cerr << "ERROR: you have declared your model 'linear' but you are using a function "
           << "(max, min, abs, sign) or an operator (<, >, <=, >=, ==, !=) on an "
           << "endogenous variable." << '\n';
      exit(EXIT_FAILURE);
    }

  if (linear && !mod_file_struct.perfect_foresight_solver_present
      && !mod_file_struct.perfect_foresight_with_expectation_errors_solver_present
      && (dynamic_model.isUnaryOpUsedOnType(SymbolType::exogenous, UnaryOpcode::sign)
          || dynamic_model.isUnaryOpUsedOnType(SymbolType::exogenous, UnaryOpcode::abs)
          || dynamic_model.isBinaryOpUsedOnType(SymbolType::exogenous, BinaryOpcode::max)
          || dynamic_model.isBinaryOpUsedOnType(SymbolType::exogenous, BinaryOpcode::min)
          || dynamic_model.isBinaryOpUsedOnType(SymbolType::exogenous, BinaryOpcode::greater)
          || dynamic_model.isBinaryOpUsedOnType(SymbolType::exogenous, BinaryOpcode::less)
          || dynamic_model.isBinaryOpUsedOnType(SymbolType::exogenous, BinaryOpcode::greaterEqual)
          || dynamic_model.isBinaryOpUsedOnType(SymbolType::exogenous, BinaryOpcode::lessEqual)
          || dynamic_model.isBinaryOpUsedOnType(SymbolType::exogenous, BinaryOpcode::equalEqual)
          || dynamic_model.isBinaryOpUsedOnType(SymbolType::exogenous, BinaryOpcode::different)))
    {
      cerr << "ERROR: you have declared your model 'linear' but you are using a function "
           << "(max, min, abs, sign) or an operator (<, >, <=, >=, ==, !=) on an "
           << "exogenous variable in a non-perfect-foresight context." << '\n';
      exit(EXIT_FAILURE);
    }

  // Test if some estimated parameters are used within the values of shocks
  // statements (see issue #469)
  set<int> parameters_intersect;
  ranges::set_intersection(mod_file_struct.parameters_within_shocks_values,
                           mod_file_struct.estimated_parameters,
                           inserter(parameters_intersect, parameters_intersect.begin()));
  if (parameters_intersect.size() > 0)
    {
      cerr << "ERROR: some estimated parameters (";
      for (bool printed_something {false}; int symb_id : parameters_intersect)
        {
          if (exchange(printed_something, true))
            cerr << ", ";
          cerr << symbol_table.getName(symb_id);
        }
      cerr << ") also appear in the expressions defining the variance/covariance matrix of shocks; "
              "this is not allowed."
           << '\n';
      exit(EXIT_FAILURE);
    }

  // Check if some parameter is not used in the model block
  set<int> unusedParams {dynamic_model.findUnusedParameters()};
  set<int> unusedParamsAfterSSM;
  set<int> steadyStateParams {steady_state_model.getUsedParameters()};
  ranges::set_difference(unusedParams, steadyStateParams,
                         inserter(unusedParamsAfterSSM, unusedParamsAfterSSM.begin()));

  // Also exclude parameters used in heterogeneous model blocks
  set<int> heterogeneousParams;
  for (const auto& hm : heterogeneous_models)
    {
      const auto hm_params {hm.getUsedParameters()};
      heterogeneousParams.insert(hm_params.begin(), hm_params.end());
    }
  set<int> unusedParamsAfterHet;
  ranges::set_difference(unusedParamsAfterSSM, heterogeneousParams,
                         inserter(unusedParamsAfterHet, unusedParamsAfterHet.begin()));

  // Exclude preprocessor-generated parameters (e.g., optimal_policy_discount_factor, OccBin
  // parameters, heterogeneity parameters)
  set<int> unusedParamsExcludingPreprocessorGenerated;
  ranges::set_difference(unusedParamsAfterHet, symbol_table.getPreprocessorGeneratedParameters(),
                         inserter(unusedParamsExcludingPreprocessorGenerated,
                                  unusedParamsExcludingPreprocessorGenerated.begin()));

  if (!unusedParamsExcludingPreprocessorGenerated.empty())
    {
      string unused_params;
      for (int it : unusedParamsAfterHet)
        unused_params += symbol_table.getName(it) + " ";

      warnings << "WARNING: Parameter(s) " << unused_params << " not used in the model" << '\n';
    }

  // Check if some exogenous are not used in the model block, issue dynare#841
  set<int> unusedExo0;
  ranges::set_difference(dynamic_model.findUnusedExogenous(), mod_file_struct.pac_params,
                         inserter(unusedExo0, unusedExo0.begin()));
  // Do not consider aggregate variables used in heterogeneous blocks, see #145
  set<int> usedAggregateExo;
  for (const auto& hm : heterogeneous_models)
    usedAggregateExo.merge(hm.getUsedAggregateExogenous());
  set<int> unusedExo;
  ranges::set_difference(unusedExo0, usedAggregateExo, inserter(unusedExo, unusedExo.end()));
  if (unusedExo.size() > 0)
    {
      string unused_exos;
      for (int it : unusedExo)
        unused_exos += symbol_table.getName(it) + " ";

      if (nostrict)
        warnings << "WARNING: " << unused_exos
                 << "not used in model block, removed by nostrict command-line option" << '\n';
      else
        {
          cerr << "ERROR: " << unused_exos
               << "not used in model block. To bypass this error, use the `nostrict` option. This "
                  "may lead to crashes or unexpected behavior."
               << '\n';
          exit(EXIT_FAILURE);
        }
    }

  if (!heterogeneity_table.empty())
    {
      if (block)
        {
          cerr << "ERROR: the 'block' option of the 'model' block is not supported for "
                  "heterogeneous models"
               << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.check_present)
        {
          cerr << "ERROR: The 'check' command is not supported for heterogeneous models" << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.steady_present)
        {
          cerr << "ERROR: The 'steady' command is not supported for heterogeneous models" << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.perfect_foresight_solver_present)
        {
          cerr << "ERROR: The 'perfect_foresight_solver' command is not supported for "
                  "heterogeneous models"
               << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.perfect_foresight_with_expectation_errors_solver_present)
        {
          cerr << "ERROR: The 'perfect_foresight_with_expectation_errors_solver' command is not "
                  "supported for heterogeneous models"
               << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.stoch_simul_present)
        {
          cerr << "ERROR: The 'stoch_simul' command is not supported for heterogeneous models"
               << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.estimation_present)
        {
          cerr << "ERROR: The 'estimation' command is not supported for heterogeneous models"
               << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.osr_present)
        {
          cerr << "ERROR: The 'osr' command is not supported for heterogeneous models" << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.osr_params_present)
        {
          cerr << "ERROR: The 'osr_params' command is not supported for heterogeneous models"
               << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.optim_weights_present)
        {
          cerr << "ERROR: The 'optim_weights' block is not supported for heterogeneous models"
               << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.ramsey_model_present)
        {
          cerr << "ERROR: The 'ramsey_model' command is not supported for heterogeneous models"
               << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.discretionary_policy_present)
        {
          cerr << "ERROR: The 'discretionary_policy' command is not supported for heterogeneous "
                  "models"
               << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.planner_objective_present)
        {
          cerr << "ERROR: The 'planner_objective' command is not supported for heterogeneous models"
               << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.extended_path_present)
        {
          cerr << "ERROR: The 'extended_path' command is not supported for heterogeneous models"
               << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.identification_present)
        {
          cerr << "ERROR: The 'identification' command is not supported for heterogeneous models"
               << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.sensitivity_present)
        {
          cerr << "ERROR: The 'sensitivity' command is not supported for heterogeneous models"
               << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.mom_estimation_present)
        {
          cerr
              << "ERROR: The 'methods_of_moments' command is not supported for heterogeneous models"
              << '\n';
          exit(EXIT_FAILURE);
        }
      if (mod_file_struct.occbin_constraints_present)
        {
          cerr << "ERROR: The 'occbin_constraints' block is not supported for heterogeneous models"
               << '\n';
          exit(EXIT_FAILURE);
        }
    }
}

void
ModFile::transformPass(bool nostrict, bool stochastic, bool compute_xrefs, bool transform_unary_ops,
                       const string& exclude_eqs, const string& include_eqs)
{
  /* Save the original model (must be done before any model transformations by preprocessor)
     — except predetermined variables (which must be handled before the call to
       setLeadsLagsOrig(), see #47, and also before equation simplification,
       since the latter uses leads/lags, see #83)
     — except substituting out variables which we know are constant (they
       appear in an equation of the form: X = constant)
     — except adl operators which we always want expanded
     — except diff operators with a lead which have been expanded by
       DataTree:AddDiff()
  */
  dynamic_model.includeExcludeEquations(exclude_eqs, true);
  dynamic_model.includeExcludeEquations(include_eqs, false);
  if (symbol_table.predeterminedNbr() > 0)
    dynamic_model.transformPredeterminedVariables();
  dynamic_model.simplifyEquations();
  dynamic_model.setLeadsLagsOrig();
  original_model = dynamic_model;
  dynamic_model.expandEqTags();

  // Replace all model-local variables by their expression
  dynamic_model.substituteModelLocalVariables();

  // Check that all declared endogenous are used in equations
  // Do not consider aggregate endogenous used in heterogeneous model blocks
  set<int> usedAggregateEndogs;
  for (const auto& hm : heterogeneous_models)
    usedAggregateEndogs.merge(hm.getUsedAggregateEndogenous());
  set<int> unusedEndogs;
  ranges::set_difference(dynamic_model.findUnusedEndogenous(), usedAggregateEndogs,
                         inserter(unusedEndogs, unusedEndogs.end()));
  bool unusedEndogsIsErr = !nostrict && !mod_file_struct.bvar_present && !unusedEndogs.empty();
  for (int unusedEndog : unusedEndogs)
    if (nostrict)
      {
        symbol_table.changeType(unusedEndog, SymbolType::unusedEndogenous);
        warnings << "WARNING: '" << symbol_table.getName(unusedEndog)
                 << "' not used in model block, removed by nostrict command-line option" << '\n';
      }
    else if (unusedEndogsIsErr)
      cerr << "Error: " << symbol_table.getName(unusedEndog) << " not used in the model block"
           << '\n';

  if (unusedEndogsIsErr)
    exit(EXIT_FAILURE);

  /* Get the list of equations in which to scan for and substitute unary ops:
     – equations which are part of VARs and Trend Component Models
     – PAC equations (those with a pac_expectation operator) */
  set<string> var_tcm_eqtags;
  for (const auto& [name, tags] : trend_component_model_table.getEqTags())
    for (auto& tag : tags)
      var_tcm_eqtags.insert(tag);
  for (const auto& [name, tags] : var_model_table.getEqTags())
    for (auto& tag : tags)
      var_tcm_eqtags.insert(tag);

  set<int> unary_ops_eqs {[&] {
    try
      {
        return dynamic_model.getEquationNumbersFromNames(var_tcm_eqtags);
      }
    catch (ModelTree::UnknownEquationNameException& e)
      {
        cerr << "ERROR: var_model/trend_component_model: there is no equation named " << e.name
             << "." << '\n';
        exit(EXIT_FAILURE);
      }
  }()};
  unary_ops_eqs.merge(dynamic_model.findPacExpectationEquationNumbers());

  // Check that no variable in VAR/TCM/PAC equations was declared with “var(log)”
  dynamic_model.checkNoWithLogTransform(unary_ops_eqs);

  // Create auxiliary variables and equations for unary ops
  lag_equivalence_table_t unary_ops_nodes;
  ExprNode::subst_table_t unary_ops_subst_table;
  if (transform_unary_ops)
    tie(unary_ops_nodes, unary_ops_subst_table)
        = dynamic_model.substituteUnaryOps(var_expectation_model_table, pac_model_table);
  else
    // substitute only those unary ops that appear in VAR, TCM and PAC model equations
    tie(unary_ops_nodes, unary_ops_subst_table) = dynamic_model.substituteUnaryOps(
        unary_ops_eqs, var_expectation_model_table, pac_model_table);

  // Create auxiliary variable and equations for Diff operators
  auto [diff_nodes, diff_subst_table]
      = dynamic_model.substituteDiff(var_expectation_model_table, pac_model_table);

  // Fill trend component and VAR model tables
  dynamic_model.fillTrendComponentModelTable();
  original_model.fillTrendComponentModelTableFromOrigModel();
  dynamic_model.fillTrendComponentModelTableAREC(diff_subst_table);
  dynamic_model.fillVarModelTable();
  original_model.fillVarModelTableFromOrigModel();

  // VAR expectation models
  var_expectation_model_table.transformPass(diff_subst_table, dynamic_model, var_model_table,
                                            trend_component_model_table);

  // PAC model
  pac_model_table.transformPass(unary_ops_nodes, unary_ops_subst_table, diff_nodes,
                                diff_subst_table, dynamic_model, var_model_table,
                                trend_component_model_table);

  // Create auxiliary vars for Expectation operator
  dynamic_model.substituteExpectation(mod_file_struct.partial_information);

  if (nonstationary_variables)
    {
      dynamic_model.detrendEquations();
      trend_dynamic_model = dynamic_model;
      dynamic_model.removeTrendVariableFromEquations();
      const auto& trend_symbols = dynamic_model.getTrendSymbolsMap();
      const auto& nonstationary_symbols = dynamic_model.getNonstationarySymbolsMap();
      epilogue.detrend(trend_symbols, nonstationary_symbols);
    }

  epilogue.toStatic();

  if (mod_file_struct.ramsey_model_present)
    {
      mod_file_struct.ramsey_orig_eq_nbr = dynamic_model.equation_number();
      PlannerObjectiveStatement* pos = nullptr;
      for (auto& statement : statements)
        if (auto pos2 = dynamic_cast<PlannerObjectiveStatement*>(statement.get()); pos2)
          {
            if (pos)
              {
                cerr << "ERROR: there can only be one planner_objective statement" << '\n';
                exit(EXIT_FAILURE);
              }
            else
              pos = pos2;
          }
      assert(pos);
      const PlannerObjective& planner_objective = pos->getPlannerObjective();

      /*
        clone the model then clone the new equations back to the original because
        we have to call computeDerivIDs (in computeRamseyPolicyFOCs and computingPass)
      */
      if (linear)
        orig_ramsey_dynamic_model = dynamic_model;
      DynamicModel ramsey_FOC_equations_dynamic_model {
          symbol_table,        num_constants,  external_functions_table,
          heterogeneity_table, database_table, trend_component_model_table,
          var_model_table};
      ramsey_FOC_equations_dynamic_model = dynamic_model;
      auto clone_if_not_null
          = [&](expr_t e) { return e ? e->clone(ramsey_FOC_equations_dynamic_model) : nullptr; };
      map<int, pair<expr_t, expr_t>> cloned_ramsey_constraints;
      for (const auto& [symb_id, bounds] : ramsey_constraints)
        cloned_ramsey_constraints.try_emplace(symb_id, clone_if_not_null(bounds.first),
                                              clone_if_not_null(bounds.second));
      mod_file_struct.ramsey_orig_endo_nbr
          = ramsey_FOC_equations_dynamic_model.computeRamseyPolicyFOCs(planner_objective,
                                                                       cloned_ramsey_constraints);
      ramsey_FOC_equations_dynamic_model.replaceMyEquations(dynamic_model);

      // Ensure that all Lagrange mutipliers are present in the FOC (see #102)
      set<int> unusedLagrangeMultipliers;
      ranges::set_intersection(
          dynamic_model.findUnusedEndogenous(), symbol_table.getLagrangeMultipliers(),
          inserter(unusedLagrangeMultipliers, unusedLagrangeMultipliers.begin()));
      if (!unusedLagrangeMultipliers.empty())
        {
          cerr << "ERROR: the following Lagrange multiplier(s) do(es) not appear in first-order "
                  "conditions: ";
          for (bool printed_something {false}; int symb_id : unusedLagrangeMultipliers)
            {
              if (exchange(printed_something, true))
                cerr << " ";
              cerr << symbol_table.getName(symb_id);
            }
          cerr << "; most likely some constraint has been simplified out because it was trivial"
               << '\n';
          exit(EXIT_FAILURE);
        }
    }

  dynamic_model.createVariableMapping();

  // Must come after detrending of variables and Ramsey policy transformation
  dynamic_model.substituteLogTransform();

  if (!heterogeneity_table.empty())
    dynamic_model.substituteAggregationOperators();

  /* Create auxiliary vars for leads and lags greater than 2, on both endos and
     exos. The transformation is not exactly the same on stochastic and
     deterministic models, because there is no need to take into account the
     Jensen inequality on the latter. */
  bool deterministic_model = !(mod_file_struct.isStochasticContext() || stochastic);
  dynamic_model.substituteEndoLeadGreaterThanTwo(deterministic_model);
  dynamic_model.substituteExoLead(deterministic_model);
  dynamic_model.substituteEndoLagGreaterThanTwo(deterministic_model);
  dynamic_model.substituteExoLag(deterministic_model);

  dynamic_model.updateVarAndTrendModel();

  if (differentiate_forward_vars)
    dynamic_model.differentiateForwardVars(differentiate_forward_vars_subset);

  if (mod_file_struct.dsge_var_estimated || !mod_file_struct.dsge_var_calibrated.empty())
    try
      {
        int sid = symbol_table.addSymbol("dsge_prior_weight", SymbolType::parameter);
        symbol_table.markPreprocessorGeneratedParameter(sid);
        if (!mod_file_struct.dsge_var_calibrated.empty())
          addStatementAtFront(make_unique<InitParamStatement>(
              sid, expressions_tree.AddNonNegativeConstant(mod_file_struct.dsge_var_calibrated),
              symbol_table));
      }
    catch (SymbolTable::AlreadyDeclaredException& e)
      {
        cerr << "ERROR: dsge_prior_weight should not be declared as a model variable / parameter "
             << "when the dsge_var option is passed to the estimation statement." << '\n';
        exit(EXIT_FAILURE);
      }

  dynamic_model.reorderAuxiliaryEquations();

  symbol_table.resizeHetAuxVars();

  for (auto& hm : heterogeneous_models)
    {
      hm.substituteEndoLead(dynamic_model);
      hm.substituteExoLead(dynamic_model);
      hm.substituteEndoLagGreaterThanTwo(dynamic_model);
      hm.substituteExoLag(dynamic_model);
    }

  for (auto& hm : heterogeneous_models)
    hm.transformPass();

  // Freeze the symbol table
  symbol_table.freeze();

  if (compute_xrefs)
    dynamic_model.computeXrefs();

  /*
    Enforce the same number of equations and endogenous, except in three cases:
    - ramsey_model, ramsey_policy or discretionary_policy is used
    - a BVAR command is used and there is no equation (standalone BVAR estimation)
    - nostrict option is passed and there are more endogs than equations (dealt with before freeze)
  */
  if (!(mod_file_struct.ramsey_model_present || mod_file_struct.discretionary_policy_present)
      && !(mod_file_struct.bvar_present && dynamic_model.equation_number() == 0)
      && (dynamic_model.equation_number() != symbol_table.endo_nbr()))
    {
      cerr << "ERROR: There are " << dynamic_model.equation_number() << " equations but "
           << symbol_table.endo_nbr() << " endogenous variables!" << '\n';
      exit(EXIT_FAILURE);
    }

  if (symbol_table.exo_det_nbr() > 0
      && (mod_file_struct.perfect_foresight_solver_present
          || mod_file_struct.perfect_foresight_with_expectation_errors_solver_present))
    {
      cerr << "ERROR: A .mod file cannot contain both one of {perfect_foresight_solver, simul, "
              "perfect_foresight_with_expectation_errors_solver} and varexo_det declaration (all "
              "exogenous variables are deterministic in this case)"
           << '\n';
      exit(EXIT_FAILURE);
    }

  if (mod_file_struct.ramsey_model_present && symbol_table.exo_det_nbr() > 0)
    {
      cerr << "ERROR: ramsey_model and ramsey_policy are incompatible with deterministic exogenous "
              "variables"
           << '\n';
      exit(EXIT_FAILURE);
    }

  if (mod_file_struct.identification_present && symbol_table.exo_det_nbr() > 0)
    {
      cerr << "ERROR: identification is incompatible with deterministic exogenous variables"
           << '\n';
      exit(EXIT_FAILURE);
    }

  if (mod_file_struct.occbin_constraints_present
      && (mod_file_struct.osr_present || mod_file_struct.mom_estimation_present
          || mod_file_struct.ramsey_model_present || mod_file_struct.discretionary_policy_present
          || mod_file_struct.extended_path_present || mod_file_struct.identification_present
          || mod_file_struct.sensitivity_present))
    {
      cerr << "ERROR: the 'occbin_constraints' block is not compatible with commands other than "
              "'estimation', 'stoch_simul', and 'calib_smoother'."
           << '\n';
      exit(EXIT_FAILURE);
    }

  if (mod_file_struct.shocks_surprise_present && !mod_file_struct.occbin_constraints_present)
    {
      cerr << "ERROR: the 'shocks(surprise)' block can only be used in conjunction with the "
              "'occbin_constraints' block."
           << '\n';
      exit(EXIT_FAILURE);
    }

  bool should_not_have_learnt_in {
      mod_file_struct.perfect_foresight_setup_present
      || mod_file_struct.perfect_foresight_solver_present
      || !mod_file_struct.perfect_foresight_with_expectation_errors_setup_present
      || !mod_file_struct.perfect_foresight_with_expectation_errors_solver_present};

  if (mod_file_struct.shocks_learnt_in_present && should_not_have_learnt_in)
    {
      cerr << "ERROR: the 'shocks(learnt_in=…)' block can only be used in conjunction with the "
              "'perfect_foresight_with_expectation_errors_setup' and "
              "'perfect_foresight_with_expectation_errors_solver' commands."
           << '\n';
      exit(EXIT_FAILURE);
    }

  if (mod_file_struct.endval_learnt_in_present && should_not_have_learnt_in)
    {
      cerr << "ERROR: the 'endval(learnt_in=…)' block can only be used in conjunction with the "
              "'perfect_foresight_with_expectation_errors_setup' and "
              "'perfect_foresight_with_expectation_errors_solver' commands."
           << '\n';
      exit(EXIT_FAILURE);
    }

  if (mod_file_struct.perfect_foresight_controlled_paths_learnt_in_present
      && should_not_have_learnt_in)
    {
      cerr << "ERROR: the 'perfect_foresight_controlled_paths(learnt_in=…)' block can only be used "
              "in conjunction with the 'perfect_foresight_with_expectation_errors_setup' and "
              "'perfect_foresight_with_expectation_errors_solver' commands."
           << '\n';
      exit(EXIT_FAILURE);
    }

  if (mod_file_struct.shock_paths_learnt_in_present && should_not_have_learnt_in)
    {
      cerr << "ERROR: the 'shock_paths(learnt_in=…)' block can only be used in conjunction with "
              "the 'perfect_foresight_with_expectation_errors_setup' and "
              "'perfect_foresight_with_expectation_errors_solver' commands."
           << '\n';
      exit(EXIT_FAILURE);
    }

  if (mod_file_struct.shock_paths_number > 0
      && (mod_file_struct.shocks_present || mod_file_struct.mshocks_present
          || mod_file_struct.endval_present || mod_file_struct.shocks_learnt_in_present
          || mod_file_struct.endval_learnt_in_present
          || mod_file_struct.perfect_foresight_controlled_paths_present))
    {
      cerr << "ERROR: the 'shock_paths' block cannot be used in conjunction with either 'shocks', "
              "'mshocks', 'endval' or 'perfect_foresight_controlled_paths' blocks."
           << '\n';
      exit(EXIT_FAILURE);
    }

  if (!mod_file_struct.ramsey_model_present)
    cout << "Found " << dynamic_model.equation_number() << " equation(s)." << '\n';
  else
    {
      cout << "Found " << mod_file_struct.ramsey_orig_eq_nbr << " equation(s)." << '\n';
      cout << "Found " << dynamic_model.equation_number() << " FOC equation(s) for Ramsey Problem."
           << '\n';
    }

  if (symbol_table.exists("dsge_prior_weight"))
    {
      if (mod_file_struct.bayesian_irf_present)
        {
          if (symbol_table.exo_nbr() != symbol_table.observedVariablesNbr())
            {
              cerr << "ERROR: When estimating a DSGE-Var and the bayesian_irf option is passed to "
                      "the estimation "
                   << "statement, the number of shocks must equal the number of observed variables."
                   << '\n';
              exit(EXIT_FAILURE);
            }
        }
      else if (symbol_table.exo_nbr() < symbol_table.observedVariablesNbr())
        {
          cerr << "ERROR: When estimating a DSGE-Var, the number of shocks must be "
               << "greater than or equal to the number of observed variables." << '\n';
          exit(EXIT_FAILURE);
        }
    }

  for (int dim {0}; dim < heterogeneity_table.size(); dim++)
    if (heterogeneous_models.at(dim).equation_number() != symbol_table.het_endo_nbr(dim))
      {
        cerr << "ERROR: There are " << heterogeneous_models.at(dim).equation_number()
             << " equations but " << symbol_table.het_endo_nbr(dim)
             << " endogenous variables in the model for heterogeneity dimension '"
             << heterogeneity_table.getName(dim) << "'!" << '\n';
        exit(EXIT_FAILURE);
      }

  // Create static model
  static_model = static_cast<StaticModel>(dynamic_model);
}

void
ModFile::computingPass(bool no_tmp_terms, OutputType output, int params_derivs_order)
{
  // Mod file may have no equation (for example in a standalone BVAR estimation)
  if (dynamic_model.equation_number() > 0)
    {
      if (nonstationary_variables)
        trend_dynamic_model.runTrendTest(global_eval_context);

      // Compute static model derivatives
      if (!no_static)
        {
          if (mod_file_struct.stoch_simul_present || mod_file_struct.estimation_present
              || mod_file_struct.osr_present || mod_file_struct.ramsey_model_present
              || mod_file_struct.identification_present || mod_file_struct.calib_smoother_present
              || mod_file_struct.mom_estimation_present)
            static_model.set_cutoff_to_zero();

          int derivsOrder = 1;
          int paramsDerivsOrder = 0;
          if (mod_file_struct.identification_present
              || mod_file_struct.estimation_analytic_derivation)
            derivsOrder = 2;

          if (mod_file_struct.identification_present
              || mod_file_struct.estimation_analytic_derivation
              || mod_file_struct.osr_analytic_derivation
              || (mod_file_struct.GMM_present
                  && (mod_file_struct.analytic_standard_errors_present
                      || mod_file_struct.analytic_jacobian_present)))
            paramsDerivsOrder = params_derivs_order;

          static_model.computingPass(derivsOrder, paramsDerivsOrder, global_eval_context,
                                     no_tmp_terms, block, use_dll);
          if (mod_file_struct.ramsey_model_present)
            static_model.computeRamseyMultipliersDerivatives(mod_file_struct.ramsey_orig_endo_nbr,
                                                             !use_dll, no_tmp_terms);
        }
      // Set things to compute for dynamic model
      if (mod_file_struct.perfect_foresight_solver_present
          || mod_file_struct.perfect_foresight_with_expectation_errors_solver_present
          || mod_file_struct.check_present || mod_file_struct.stoch_simul_present
          || mod_file_struct.estimation_present || mod_file_struct.osr_present
          || mod_file_struct.ramsey_model_present || mod_file_struct.identification_present
          || mod_file_struct.calib_smoother_present || mod_file_struct.mom_estimation_present
          || mod_file_struct.extended_path_present)
        {
          if (mod_file_struct.perfect_foresight_solver_present
              || mod_file_struct.perfect_foresight_with_expectation_errors_solver_present)
            {
              int derivsOrder = 1;
              if (output == OutputType::second)
                derivsOrder = 2;
              else if (output == OutputType::third)
                derivsOrder = 3;
              dynamic_model.computingPass(derivsOrder, 0, global_eval_context, no_tmp_terms, block,
                                          use_dll);
            }
          else if (mod_file_struct.extended_path_present)
            {
              int derivsOrder = max(1, mod_file_struct.extended_path_hybrid_order);
              dynamic_model.computingPass(derivsOrder, 0, global_eval_context, no_tmp_terms, block,
                                          use_dll);
              if (mod_file_struct.extended_path_order > 0 && dynamic_model.getMaxLead() == 0)
                {
                  cerr
                      << "ERROR: The 'order' option of 'extended_path' cannot have a positive "
                         "value with a backward model. Since agents do not form expectations about "
                         "the future, future uncertainty cannot influence their current behavior; "
                         "a positive order would just add computational cost without changing the "
                         "results."
                      << '\n';
                  exit(EXIT_FAILURE);
                }
              if (dynamic_model.getMaxLag() == 0)
                {
                  cerr << "ERROR: The 'extended_path' command cannot be used with a purely "
                          "forward model."
                       << '\n';
                  exit(EXIT_FAILURE);
                }
            }
          else
            {
              if (mod_file_struct.stoch_simul_present || mod_file_struct.estimation_present
                  || mod_file_struct.osr_present || mod_file_struct.ramsey_model_present
                  || mod_file_struct.identification_present
                  || mod_file_struct.calib_smoother_present
                  || mod_file_struct.mom_estimation_present)
                dynamic_model.set_cutoff_to_zero();
              if (mod_file_struct.order_option < 1)
                {
                  cerr << "ERROR: Incorrect order option..." << '\n';
                  exit(EXIT_FAILURE);
                }
              int derivsOrder
                  = max(mod_file_struct.order_option,
                        mod_file_struct.identification_order + 1); // See preprocessor#40
              if (mod_file_struct.GMM_present
                  && (mod_file_struct.analytic_standard_errors_present
                      || mod_file_struct.analytic_jacobian_present)) // analytic_standard_errors or
                                                                     // analytic_jacobian require
                                                                     // one order more
                derivsOrder
                    = max(mod_file_struct.order_option,
                          max(mod_file_struct.identification_order, mod_file_struct.mom_order)
                              + 1); // See preprocessor#40

              if (mod_file_struct.sensitivity_present || linear || output == OutputType::second)
                derivsOrder = max(derivsOrder, 2);
              if (mod_file_struct.estimation_analytic_derivation || output == OutputType::third)
                derivsOrder = max(derivsOrder, 3);
              int paramsDerivsOrder = 0;
              if (mod_file_struct.identification_present
                  || mod_file_struct.estimation_analytic_derivation
                  || mod_file_struct.osr_analytic_derivation
                  || (mod_file_struct.GMM_present
                      && (mod_file_struct.analytic_standard_errors_present
                          || mod_file_struct.analytic_jacobian_present)))
                paramsDerivsOrder = params_derivs_order;
              dynamic_model.computingPass(derivsOrder, paramsDerivsOrder, global_eval_context,
                                          no_tmp_terms, block, use_dll);
              if (linear && mod_file_struct.ramsey_model_present)
                orig_ramsey_dynamic_model.computingPass(2, paramsDerivsOrder, global_eval_context,
                                                        no_tmp_terms, block, use_dll);
            }
        }
      else // No computing task requested, compute derivatives up to 2nd order by default unless
           // output=first (preprocessor#100) or third (preprocessor#121) is requested
        {
          switch (output)
            {
            case OutputType::first:
              dynamic_model.computingPass(1, 0, global_eval_context, no_tmp_terms, block, use_dll);
              break;
            case OutputType::third:
              dynamic_model.computingPass(3, 0, global_eval_context, no_tmp_terms, block, use_dll);
              break;
            default:
              dynamic_model.computingPass(2, 0, global_eval_context, no_tmp_terms, block, use_dll);
              break;
            }
        }

      if (linear)
        {
          if (mod_file_struct.ramsey_model_present)
            orig_ramsey_dynamic_model.checkIsLinear();
          else
            dynamic_model.checkIsLinear();
        }
    }

  // Those matrices can only be filled here, because we use derivatives
  dynamic_model.fillVarModelTableMatrices();

  for (auto& hm : heterogeneous_models)
    hm.computingPass(mod_file_struct.order_option, no_tmp_terms, use_dll);

  for (auto& statement : statements)
    statement->computingPass(mod_file_struct);
}

void
ModFile::writeMOutput(const string& basename, bool clear_all, bool clear_global, bool no_warn,
                      bool console, bool nograph, bool nointeractive, const Configuration& config,
                      bool check_model_changes, bool minimal_workspace, bool compute_xrefs,
                      const string& mexext, const filesystem::path& matlabroot, bool onlymodel,
                      bool gui, bool notime) const
{
  if (basename.empty())
    {
      cerr << "ERROR: Missing file name" << '\n';
      exit(EXIT_FAILURE);
    }

  auto plusfolder {packageDir(basename)};

  if (check_model_changes && !heterogeneity_table.empty())
    {
      cerr << "ERROR: the 'fast' option is not supported for heterogeneous models" << '\n';
      exit(EXIT_FAILURE);
    }
  bool hasModelChanged = !dynamic_model.isChecksumMatching(basename) || !check_model_changes;
  if (hasModelChanged)
    {
      // Erase possible remnants of previous runs

      /* Under MATLAB+Windows (but not under Octave nor under GNU/Linux or
         macOS), if we directly remove the "+" subdirectory, then the
         preprocessor is not able to recreate it afterwards (presumably because
         MATLAB maintains some sort of lock on it). So we use a hack. */
      remove_directory_with_matlab_lock(plusfolder);

      filesystem::remove_all(basename + "/model/src");
      filesystem::remove_all(basename + "/model/bytecode");
      // Do not remove basename/model/julia/, otherwise it would break calls to
      // writeToFileIfModified()
    }

  create_directory(plusfolder);
  filesystem::path fname {plusfolder / "driver.m"};
  ofstream mOutputFile {fname, ios::out | ios::binary};
  if (!mOutputFile.is_open())
    {
      cerr << "ERROR: Can't open file " << fname.string() << " for writing" << '\n';
      exit(EXIT_FAILURE);
    }

  mOutputFile << "%" << '\n'
              << "% Status : main Dynare file" << '\n'
              << "%" << '\n'
              << "% Warning : this file is generated automatically by Dynare" << '\n'
              << "%           from model file (.mod)" << '\n'
              << '\n';

  if (no_warn)
    mOutputFile << "warning off" << '\n'; // This will be executed *after* function warning_config()

  if (clear_all)
    mOutputFile << "clearvars -global" << '\n'
                << "clear_persistent_variables(fileparts(which('dynare')), false)" << '\n';
  else if (clear_global)
    mOutputFile << "clearvars -global M_ options_ oo_ estim_params_ dataset_ "
                   "dataset_info estimation_info;"
                << '\n';

  if (!notime)
    mOutputFile << "tic0 = tic;" << '\n';

  mOutputFile << "% Define global variables." << '\n'
              << "global M_ options_ oo_ estim_params_ dataset_ dataset_info estimation_info"
              << '\n'
              << "options_ = [];" << '\n'
              << "M_.fname = '" << basename << "';" << '\n'
              << "M_.dynare_version = '" << PACKAGE_VERSION << "';" << '\n'
              << "oo_.dynare_version = '" << PACKAGE_VERSION << "';" << '\n'
              << "options_.dynare_version = '" << PACKAGE_VERSION << "';" << '\n'
              << "%" << '\n'
              << "% Some global variables initialization" << '\n'
              << "%" << '\n';
  if (!onlymodel)
    config.writeHooks(mOutputFile);
  mOutputFile << "global_initialization;" << '\n';

  if (console)
    mOutputFile << "options_.console_mode = true;" << '\n' << "options_.nodisplay = true;" << '\n';
  if (nograph)
    mOutputFile << "options_.nograph = true;" << '\n';

  if (nointeractive)
    mOutputFile << "options_.nointeractive = true;" << '\n';

  if (param_used_with_lead_lag)
    mOutputFile << "M_.parameter_used_with_lead_lag = true;" << '\n';

  symbol_table.writeOutput(mOutputFile);
  heterogeneity_table.writeOutput(mOutputFile);

  var_model_table.writeOutput(basename, mOutputFile);
  trend_component_model_table.writeOutput(basename, mOutputFile);
  var_expectation_model_table.writeOutput(mOutputFile);
  pac_model_table.writeOutput(mOutputFile);

  database_table.writeOutput(mOutputFile);

  // Initialize M_.Sigma_e, M_.Correlation_matrix, M_.Skew_e, M_.H, and M_.Correlation_matrix_ME
  mOutputFile << "M_.Sigma_e = zeros(" << symbol_table.exo_nbr() << ", " << symbol_table.exo_nbr()
              << ");" << '\n'
              << "M_.Correlation_matrix = eye(" << symbol_table.exo_nbr() << ", "
              << symbol_table.exo_nbr() << ");" << '\n'
              << "M_.Skew_e = zeros(0, 4);" << '\n';
  for (int hd {0}; hd < heterogeneity_table.size(); hd++)
    mOutputFile << "M_.heterogeneity(" << hd + 1 << ").Sigma_e = zeros("
                << symbol_table.het_exo_nbr(hd) << ", " << symbol_table.het_exo_nbr(hd) << ");"
                << '\n';

  if (mod_file_struct.calibrated_measurement_errors)
    mOutputFile << "M_.H = zeros(" << symbol_table.observedVariablesNbr() << ", "
                << symbol_table.observedVariablesNbr() << ");" << '\n'
                << "M_.Correlation_matrix_ME = eye(" << symbol_table.observedVariablesNbr() << ", "
                << symbol_table.observedVariablesNbr() << ");" << '\n';
  else
    mOutputFile << "M_.H = 0;" << '\n' << "M_.Correlation_matrix_ME = 1;" << '\n';

  // May be later modified by a shocks block
  mOutputFile << "M_.sigma_e_is_diagonal = true;" << '\n';

  /* Initialize the structures created for several blocks, as part of the implementation of the
     “overwrite” option.
     Note that struct() creates a 1×1 struct array, so we have to use struct([]). */
  mOutputFile << "M_.det_shocks = struct([]);" << '\n'
              << "M_.surprise_shocks = struct([]);" << '\n'
              << "M_.learnt_shocks = struct([]);" << '\n'
              << "M_.learnt_endval = struct([]);" << '\n'
              << "M_.shock_paths = struct([]);" << '\n'
              << "M_.heteroskedastic_shocks.Qvalue_orig = struct([]);" << '\n'
              << "M_.heteroskedastic_shocks.Qscale_orig = struct([]);" << '\n'
              << "M_.heteroskedastic_shocks.Hvalue_orig = struct([]);" << '\n'
              << "M_.heteroskedastic_shocks.Hscale_orig = struct([]);" << '\n'
              << "M_.matched_irfs = {};" << '\n'
              << "M_.matched_irfs_weights = {};" << '\n'
              << "M_.perfect_foresight_controlled_paths = struct([]);" << '\n'
              << "M_.filter_tunes = struct([]);" << '\n';

  // NB: options_.{ramsey,discretionary}_policy should rather be fields of M_
  mOutputFile << boolalpha << "options_.linear = " << linear << ";" << '\n'
              << "options_.block = " << block << ";" << '\n'
              << "options_.bytecode = " << bytecode << ";" << '\n'
              << "options_.use_dll = " << use_dll << ";" << '\n'
              << "options_.ramsey_policy = " << mod_file_struct.ramsey_model_present << ";" << '\n'
              << "options_.discretionary_policy = " << mod_file_struct.discretionary_policy_present
              << ";" << '\n';

  if (mod_file_struct.discretionary_policy_present)
    mOutputFile << "M_.discretionary_orig_eq_nbr = " << original_model.equation_number() << ";"
                << '\n';

  if (parallel_local_files.size() > 0)
    {
      mOutputFile << "options_.parallel_info.local_files = {" << '\n';
      for (const auto& parallel_local_file : parallel_local_files)
        {
          size_t j = parallel_local_file.find_last_of(R"(/\)");
          if (j == string::npos)
            mOutputFile << "'', '" << parallel_local_file << "';" << '\n';
          else
            mOutputFile << "'" << parallel_local_file.substr(0, j + 1) << "', '"
                        << parallel_local_file.substr(j + 1) << "';" << '\n';
        }
      mOutputFile << "};" << '\n';
    }

  if (dynamic_model.isHessianComputed())
    {
      mOutputFile << "M_.nonzero_hessian_eqs = ";
      dynamic_model.printNonZeroHessianEquations(mOutputFile);
      mOutputFile << ";" << '\n' << "M_.hessian_eq_zero = isempty(M_.nonzero_hessian_eqs);" << '\n';
    }

  if (!onlymodel)
    config.writeCluster(mOutputFile);

  if (bytecode)
    mOutputFile << "if exist('bytecode') ~= 3" << '\n'
                << "  error('DYNARE: Can''t find bytecode DLL. Please compile it or remove the "
                   "''bytecode'' option.')"
                << '\n'
                << "end" << '\n';

  mOutputFile << "M_.eq_nbr = " << dynamic_model.equation_number() << ";" << '\n'
              << "M_.ramsey_orig_eq_nbr = " << mod_file_struct.ramsey_orig_eq_nbr << ";" << '\n'
              << "M_.ramsey_orig_endo_nbr = " << mod_file_struct.ramsey_orig_endo_nbr << ";" << '\n'
              << "M_.set_auxiliary_variables = exist(['./+' M_.fname "
                 "'/set_auxiliary_variables.m'], 'file') == 2;"
              << '\n';

  epilogue.writeOutput(mOutputFile);

  if (dynamic_model.equation_number() > 0)
    {
      dynamic_model.writeDriverOutput(mOutputFile, compute_xrefs);
      if (!no_static)
        {
          static_model.writeDriverOutput(mOutputFile);
          if (mod_file_struct.ramsey_model_present)
            static_model.writeDriverRamseyMultipliersDerivativesSparseIndices(mOutputFile);
        }
    }

  for (const auto& hm : heterogeneous_models)
    hm.writeDriverOutput(mOutputFile);

  if (onlymodel || gui)
    for (const auto& statement : statements)
      {
        // Special treatment for initval block: insert initial values for the auxiliary variables
        if (auto ivs = dynamic_cast<InitValStatement*>(statement.get()); ivs)
          {
            ivs->writeOutput(mOutputFile, basename, minimal_workspace);
            static_model.writeAuxVarInitval(mOutputFile, ExprNodeOutputType::matlabOutsideModel);
          }

        // Special treatment for endval block: insert initial values for the auxiliary variables
        if (auto evs = dynamic_cast<EndValStatement*>(statement.get()); evs)
          {
            evs->writeOutput(mOutputFile, basename, minimal_workspace);
            static_model.writeAuxVarInitval(mOutputFile, ExprNodeOutputType::matlabOutsideModel);
          }

        if (auto ips = dynamic_cast<InitParamStatement*>(statement.get()); ips)
          ips->writeOutput(mOutputFile, basename, minimal_workspace);

        if (auto ss = dynamic_cast<ShocksStatement*>(statement.get()); ss)
          ss->writeOutput(mOutputFile, basename, minimal_workspace);

        if (auto eps = dynamic_cast<EstimatedParamsStatement*>(statement.get()); eps)
          eps->writeOutput(mOutputFile, basename, minimal_workspace);

        if (auto sgs = dynamic_cast<ShockGroupsStatement*>(statement.get()); sgs)
          sgs->writeOutput(mOutputFile, basename, minimal_workspace);

        if (gui)
          if (auto it = dynamic_cast<NativeStatement*>(statement.get()); it)
            it->writeOutput(mOutputFile, basename, minimal_workspace);
      }
  else
    {
      for (const auto& statement : statements)
        {
          statement->writeOutput(mOutputFile, basename, minimal_workspace);

          // Special treatment for initval block: insert initial values for the auxiliary variables
          if (auto ivs = dynamic_cast<InitValStatement*>(statement.get()); ivs)
            static_model.writeAuxVarInitval(mOutputFile, ExprNodeOutputType::matlabOutsideModel);

          // Special treatment for endval block: insert initial values for the auxiliary variables
          if (auto evs = dynamic_cast<EndValStatement*>(statement.get()); evs)
            static_model.writeAuxVarInitval(mOutputFile, ExprNodeOutputType::matlabOutsideModel);

          // Special treatment for load params and steady state statement: insert initial values for
          // the auxiliary variables
          if (auto lpass = dynamic_cast<LoadParamsAndSteadyStateStatement*>(statement.get());
              lpass && !no_static)
            static_model.writeAuxVarInitval(mOutputFile, ExprNodeOutputType::matlabOutsideModel);
        }

      if (!notime)
        mOutputFile << '\n'
                    << '\n'
                    << "oo_.time = toc(tic0);" << '\n'
                    << "disp(['Total computing time : ' dynsec2hms(oo_.time) ]);" << '\n';

      mOutputFile << "if ~exist([M_.dname filesep 'Output'],'dir')" << '\n'
                  << "    mkdir(M_.dname,'Output');" << '\n'
                  << "end" << '\n'
                  << "save([M_.dname filesep 'Output' filesep '" << basename
                  << "_results.mat'], 'oo_', 'M_', 'options_');" << '\n'
                  << "if exist('estim_params_', 'var') == 1" << '\n'
                  << "  save([M_.dname filesep 'Output' filesep '" << basename
                  << "_results.mat'], 'estim_params_', '-append');" << '\n'
                  << "end" << '\n'
                  << "if exist('dataset_', 'var') == 1" << '\n'
                  << "  save([M_.dname filesep 'Output' filesep '" << basename
                  << "_results.mat'], 'dataset_', '-append');" << '\n'
                  << "end" << '\n'
                  << "if exist('estimation_info', 'var') == 1" << '\n'
                  << "  save([M_.dname filesep 'Output' filesep '" << basename
                  << "_results.mat'], 'estimation_info', '-append');" << '\n'
                  << "end" << '\n'
                  << "if exist('dataset_info', 'var') == 1" << '\n'
                  << "  save([M_.dname filesep 'Output' filesep '" << basename
                  << "_results.mat'], 'dataset_info', '-append');" << '\n'
                  << "end" << '\n'
                  << "if exist('oo_recursive_', 'var') == 1" << '\n'
                  << "  save([M_.dname filesep 'Output' filesep '" << basename
                  << "_results.mat'], 'oo_recursive_', '-append');" << '\n'
                  << "end" << '\n'
                  << "if exist('options_mom_', 'var') == 1" << '\n'
                  << "  save([M_.dname filesep 'Output' filesep '" << basename
                  << "_results.mat'], 'options_mom_', '-append');" << '\n'
                  << "end" << '\n';

      config.writeEndParallel(mOutputFile);

      if (!no_warn)
        {
          if (int num_warnings {warnings.numWarnings()}; num_warnings > 0)
            mOutputFile << "disp('Note: " << num_warnings
                        << " warning(s) encountered in the preprocessor')" << '\n';

          mOutputFile << "if ~isempty(lastwarn)" << '\n'
                      << "  disp('Note: warning(s) encountered in MATLAB/Octave code')" << '\n'
                      << "end" << '\n';
        }
    }

  mOutputFile.close();

  if (hasModelChanged)
    {
      // Create static and dynamic files
      if (dynamic_model.equation_number() > 0)
        {
          if (!no_static)
            {
              static_model.writeStaticFile(basename, use_dll, mexext, matlabroot, false);
              static_model.writeParamsDerivativesFile<false>(basename);
              if (mod_file_struct.ramsey_model_present)
                {
                  if (use_dll)
                    static_model.writeRamseyMultipliersDerivativesCFile(
                        basename, mexext, matlabroot, mod_file_struct.ramsey_orig_endo_nbr);
                  else
                    static_model.writeRamseyMultipliersDerivativesMFile(
                        basename, mod_file_struct.ramsey_orig_endo_nbr);
                }
            }

          dynamic_model.writeDynamicFile(basename, use_dll, mexext, matlabroot, false);

          dynamic_model.writeParamsDerivativesFile<false>(basename);
        }

      // Create steady state file
      steady_state_model.writeSteadyStateFile(basename, false);

      // Create epilogue file
      epilogue.writeEpilogueFile(basename);

      pac_model_table.writeTargetCoefficientsFile(basename);

      for (const auto& hm : heterogeneous_models)
        hm.writeModelFiles(basename, false);
    }
}

void
ModFile::writeJuliaOutput(const string& basename) const
{
  if (dynamic_model.equation_number() > 0)
    {
      if (!no_static)
        {
          static_model.writeStaticFile(basename, false, "", {}, true);
          static_model.writeParamsDerivativesFile<true>(basename);
        }
      dynamic_model.writeDynamicFile(basename, use_dll, "", {}, true);
      dynamic_model.writeParamsDerivativesFile<true>(basename);
    }
  steady_state_model.writeSteadyStateFile(basename, true);
}

void
ModFile::writePythonOutput(const string& basename, bool use_jax, bool use_numba) const
{
  filesystem::path python_dir = filesystem::path(basename) / "python";
  if (!filesystem::exists(python_dir))
    filesystem::create_directories(python_dir);

  writePythonDriverFile(basename, use_jax, use_numba);

  writePythonInitFile(python_dir);
  writePythonModelInterfaceFile(python_dir, use_jax, use_numba);
  writePythonTypeStubs(basename);

  if (dynamic_model.equation_number() > 0)
    {
      if (!no_static)
        {
          static_model.writePythonStaticFile(basename, use_jax, use_numba);
          static_model.writePythonStaticParamsDerivatives(basename, use_jax, use_numba);
        }
      dynamic_model.writePythonDynamicFile(basename, use_jax, use_numba);
      dynamic_model.writePythonDynamicParamsDerivatives(basename, use_jax, use_numba);
    }

  steady_state_model.writePythonSteadyStateFile(basename, use_jax, use_numba);

  cout << "Python output files generated successfully." << '\n';
}

void
ModFile::writeJsonOutput(const string& basename, JsonOutputPointType json,
                         JsonFileOutputType json_output_mode, bool onlyjson, bool jsonderivsimple)
{
  if (json == JsonOutputPointType::nojson)
    return;

  if (json == JsonOutputPointType::parsing || json == JsonOutputPointType::checkpass)
    symbol_table.freeze();

  if (json_output_mode == JsonFileOutputType::standardout)
    cout << "//-- BEGIN JSON --// " << '\n' << "{" << '\n';

  writeJsonOutputParsingCheck(basename, json_output_mode,
                              json == JsonOutputPointType::transformpass,
                              json == JsonOutputPointType::computingpass);

  if (json == JsonOutputPointType::parsing || json == JsonOutputPointType::checkpass)
    symbol_table.unfreeze();

  if (json == JsonOutputPointType::computingpass)
    writeJsonComputingPassOutput(basename, json_output_mode, jsonderivsimple);

  if (json_output_mode == JsonFileOutputType::standardout)
    cout << "}" << '\n' << "//-- END JSON --// " << '\n';

  switch (json)
    {
    case JsonOutputPointType::parsing:
      cout << "JSON written after Parsing step." << '\n';
      break;
    case JsonOutputPointType::checkpass:
      cout << "JSON written after Check step." << '\n';
      break;
    case JsonOutputPointType::transformpass:
      cout << "JSON written after Transform step." << '\n';
      break;
    case JsonOutputPointType::computingpass:
      cout << "JSON written after Computing step." << '\n';
      break;
    case JsonOutputPointType::nojson:
      cerr << "ModFile::writeJsonOutput: should not arrive here." << '\n';
      exit(EXIT_FAILURE);
    }

  if (onlyjson)
    exit(EXIT_SUCCESS);
}

void
ModFile::writeJsonOutputParsingCheck(const string& basename, JsonFileOutputType json_output_mode,
                                     bool transformpass, bool computingpass) const
{
  ostringstream output;
  output << "{" << '\n';

  symbol_table.writeJsonOutput(output);
  output << ", ";
  if (!heterogeneity_table.empty())
    {
      heterogeneity_table.writeJsonOutput(output);
      output << ", ";
    }
  if (!database_table.empty())
    {
      database_table.writeJsonOutput(output);
      output << ", ";
    }
  dynamic_model.writeJsonOutput(output);
  output << ", ";
  static_model.writeJsonOutput(output);

  if (!statements.empty() || !var_model_table.empty() || !trend_component_model_table.empty())
    {
      output << R"(, "statements": [)";
      if (!var_model_table.empty())
        {
          var_model_table.writeJsonOutput(output);
          output << ", ";
        }

      if (!trend_component_model_table.empty())
        {
          trend_component_model_table.writeJsonOutput(output);
          output << ", ";
        }

      if (!var_expectation_model_table.empty())
        {
          var_expectation_model_table.writeJsonOutput(output);
          output << ", ";
        }

      if (!pac_model_table.empty())
        {
          pac_model_table.writeJsonOutput(output);
          output << ", ";
        }

      for (bool printed_something {false}; auto& it : statements)
        {
          if (exchange(printed_something, true))
            output << ", " << '\n';
          it->writeJsonOutput(output);
        }
      output << "]" << '\n';
    }

  if (computingpass)
    {
      output << ",";
      dynamic_model.writeJsonDynamicModelInfo(output);
    }

  output << R"(, "steady_state_model": )" << mod_file_struct.steady_state_model_present << '\n';

  output << "}" << '\n';

  ostringstream original_model_output;
  original_model_output << "";
  if (transformpass || computingpass)
    {
      original_model_output << "{";
      original_model.writeJsonOriginalModelOutput(original_model_output);
      if (!statements.empty() || !var_model_table.empty() || !trend_component_model_table.empty())
        {
          original_model_output << '\n' << R"(, "statements": [)";
          if (!var_model_table.empty())
            {
              var_model_table.writeJsonOutput(original_model_output);
              original_model_output << ", ";
            }
          if (!trend_component_model_table.empty())
            {
              trend_component_model_table.writeJsonOutput(original_model_output);
              original_model_output << ", ";
            }
          if (!pac_model_table.empty())
            {
              pac_model_table.writeJsonOutput(original_model_output);
              original_model_output << ", ";
            }
          for (bool printed_something {false}; const auto& it : statements)
            {
              original_model_output << (exchange(printed_something, true) ? "," : "") << '\n';
              it->writeJsonOutput(original_model_output);
            }
          original_model_output << "]" << '\n';
        }
      original_model_output << "}" << '\n';
    }

  ostringstream steady_state_model_output;
  steady_state_model_output << "";
  if (dynamic_model.equation_number() > 0)
    steady_state_model.writeJsonSteadyStateFile(steady_state_model_output,
                                                transformpass || computingpass);

  if (json_output_mode == JsonFileOutputType::standardout)
    {
      if (transformpass || computingpass)
        cout << R"("transformed_modfile": )";
      else
        cout << R"("modfile": )";
      cout << output.str();
      if (!original_model_output.str().empty())
        cout << R"(, "original_model": )" << original_model_output.str();
      if (!steady_state_model_output.str().empty())
        cout << R"(, "steady_state_model": )" << steady_state_model_output.str();
    }
  else
    {
      if (!basename.size())
        {
          cerr << "ERROR: Missing file name" << '\n';
          exit(EXIT_FAILURE);
        }

      filesystem::create_directories(basename + "/model/json");
      const filesystem::path fname {basename + "/model/json/modfile.json"};
      ofstream jsonOutputFile {fname, ios::out | ios::binary};
      if (!jsonOutputFile.is_open())
        {
          cerr << "ERROR: Can't open file " << fname.string() << " for writing" << '\n';
          exit(EXIT_FAILURE);
        }

      jsonOutputFile << output.str();
      jsonOutputFile.close();

      if (!original_model_output.str().empty())
        {
          if (basename.size())
            {
              const filesystem::path fname {basename + "/model/json/modfile-original.json"};
              jsonOutputFile.open(fname, ios::out | ios::binary);
              if (!jsonOutputFile.is_open())
                {
                  cerr << "ERROR: Can't open file " << fname.string() << " for writing" << '\n';
                  exit(EXIT_FAILURE);
                }
            }
          else
            {
              cerr << "ERROR: Missing file name" << '\n';
              exit(EXIT_FAILURE);
            }

          jsonOutputFile << original_model_output.str();
          jsonOutputFile.close();
        }
      if (!steady_state_model_output.str().empty())
        {
          if (basename.size())
            {
              const filesystem::path fname {basename + "/model/json/steady_state_model.json"};
              jsonOutputFile.open(fname, ios::out | ios::binary);
              if (!jsonOutputFile.is_open())
                {
                  cerr << "ERROR: Can't open file " << fname.string() << " for writing" << '\n';
                  exit(EXIT_FAILURE);
                }
            }
          else
            {
              cerr << "ERROR: Missing file name" << '\n';
              exit(EXIT_FAILURE);
            }

          jsonOutputFile << steady_state_model_output.str();
          jsonOutputFile.close();
        }
    }
}

void
ModFile::writeJsonComputingPassOutput(const string& basename, JsonFileOutputType json_output_mode,
                                      bool jsonderivsimple) const
{
  if (basename.empty() && json_output_mode != JsonFileOutputType::standardout)
    {
      cerr << "ERROR: Missing file name" << '\n';
      exit(EXIT_FAILURE);
    }

  ostringstream tmp_out, static_output, dynamic_output, static_paramsd_output,
      dynamic_paramsd_output;

  static_output << "{";
  static_model.writeJsonComputingPassOutput(static_output, !jsonderivsimple);
  static_output << "}";

  dynamic_output << "{";
  dynamic_model.writeJsonComputingPassOutput(dynamic_output, !jsonderivsimple);
  dynamic_output << "}";

  static_model.writeJsonParamsDerivatives(tmp_out, !jsonderivsimple);
  if (!tmp_out.str().empty())
    static_paramsd_output << "{" << tmp_out.str() << "}" << '\n';

  tmp_out.str("");
  dynamic_model.writeJsonParamsDerivatives(tmp_out, !jsonderivsimple);
  if (!tmp_out.str().empty())
    dynamic_paramsd_output << "{" << tmp_out.str() << "}" << '\n';

  if (json_output_mode == JsonFileOutputType::standardout)
    {
      cout << R"(, "static_model": )" << static_output.str() << '\n'
           << R"(, "dynamic_model": )" << dynamic_output.str() << '\n';

      if (!static_paramsd_output.str().empty())
        cout << R"(, "static_params_deriv": )" << static_paramsd_output.str() << '\n';

      if (!dynamic_paramsd_output.str().empty())
        cout << R"(, "dynamic_params_deriv": )" << dynamic_paramsd_output.str() << '\n';
    }
  else
    {
      filesystem::create_directories(basename + "/model/json");

      writeJsonFileHelper(basename + "/model/json/static.json", static_output);
      writeJsonFileHelper(basename + "/model/json/dynamic.json", dynamic_output);

      if (!static_paramsd_output.str().empty())
        writeJsonFileHelper(basename + "/model/json/static_params_derivs.json",
                            static_paramsd_output);

      if (!dynamic_paramsd_output.str().empty())
        writeJsonFileHelper(basename + "/model/json/params_derivs.json", dynamic_paramsd_output);
    }
}

void
ModFile::writeJsonFileHelper(const filesystem::path& fname, ostringstream& output) const
{
  ofstream jsonOutput {fname, ios::out | ios::binary};
  if (!jsonOutput.is_open())
    {
      cerr << "ERROR: Can't open file " << fname.string() << " for writing" << '\n';
      exit(EXIT_FAILURE);
    }
  jsonOutput << output.str();
  jsonOutput.close();
}

void
ModFile::writePythonDriverFile(const string& basename, bool use_jax, bool use_numba) const
{
  filesystem::path python_dir = filesystem::path(basename) / "python";
  ostringstream output;

  output << R"(""")" << '\n'
         << "Dynare Model: " << basename << '\n'
         << "Generated automatically by Dynare preprocessor" << '\n'
         << R"(""")" << '\n'
         << '\n';

  output << DataTree::writePythonHelpersImports(use_jax, use_numba) << '\n';

  ofstream file(python_dir / "driver.py", ios::out | ios::binary);
  if (!file.is_open())
    {
      cerr << "ERROR: Can't open file " << (python_dir / "driver.py").string() << " for writing"
           << '\n';
      exit(EXIT_FAILURE);
    }
  file << output.str();
  file.close();
}

void
ModFile::writePythonInitFile(const filesystem::path& python_dir) const
{
  ostringstream output;
  output << R"(""")" << '\n'
         << "Dynare Model Package" << '\n'
         << "Generated by Dynare Preprocessor" << '\n'
         << R"(""")" << '\n'
         << '\n'
         << "# Helper functions for derivatives" << '\n'
         << DataTree::writePythonHelpersDefinition() << '\n'
         << "# Import generated model modules" << '\n'
         << "from . import static" << '\n'
         << "from . import dynamic" << '\n'
         << '\n'
         << "# Import steady state module if it exists" << '\n'
         << "try:" << '\n'
         << "    from . import steadystate" << '\n'
         << "    __all__ = ['static', 'dynamic', 'steadystate']" << '\n'
         << "except ImportError:" << '\n'
         << "    __all__ = ['static', 'dynamic']" << '\n'
         << '\n'
         << "__version__ = '0.1.0'" << '\n';

  if (!filesystem::exists(python_dir))
    filesystem::create_directories(python_dir);

  ofstream file(python_dir / "__init__.py", ios::out | ios::binary);
  if (!file.is_open())
    {
      cerr << "ERROR: Can't open file " << (python_dir / "__init__.py").string() << " for writing"
           << '\n';
      exit(EXIT_FAILURE);
    }
  file << output.str();
  file.close();
}

void
ModFile::writePythonModelInterfaceFile(const filesystem::path& python_dir, bool use_jax,
                                       bool use_numba) const
{
  ostringstream output;

  output << R"(""")" << '\n'
         << "Model interface functions for numerical computation" << '\n'
         << R"(""")" << '\n'
         << '\n';

  output << DataTree::writePythonHelpersImports(use_jax, use_numba) << '\n';

  // Import the generated static and dynamic model files
  output << "from . import static as static_model" << '\n';
  output << "from . import dynamic as dynamic_model" << '\n' << '\n';

  output << DataTree::writePythonHelpersDecorator(use_jax, use_numba)
         << "def static_resid(y: np.ndarray, x: np.ndarray, params: np.ndarray) -> np.ndarray:"
         << '\n'
         << "    " << R"(""")" << '\n'
         << "    Compute static model residuals" << '\n'
         << "    " << '\n'
         << "    Parameters" << '\n'
         << "    ----------" << '\n'
         << "    y : np.ndarray" << '\n'
         << "        Endogenous variables" << '\n'
         << "    x : np.ndarray" << '\n'
         << "        Exogenous variables" << '\n'
         << "    params : np.ndarray" << '\n'
         << "        Parameters" << '\n'
         << "    " << '\n'
         << "    Returns" << '\n'
         << "    -------" << '\n'
         << "    np.ndarray" << '\n'
         << "        Residuals" << '\n'
         << "    " << R"(""")" << '\n'
         << "    return static_model.static_resid(y, x, params)" << '\n'
         << '\n';

  output << DataTree::writePythonHelpersDecorator(use_jax, use_numba)
         << "def dynamic_resid(y: np.ndarray, x: np.ndarray, params: np.ndarray, " << '\n'
         << "                  steady_state: np.ndarray, it_: int) -> np.ndarray:" << '\n'
         << "    " << R"(""")" << '\n'
         << "    Compute dynamic model residuals" << '\n'
         << "    " << '\n'
         << "    Parameters" << '\n'
         << "    ----------" << '\n'
         << "    y : np.ndarray" << '\n'
         << "        Endogenous variables (including leads and lags)" << '\n'
         << "    x : np.ndarray" << '\n'
         << "        Exogenous variables" << '\n'
         << "    params : np.ndarray" << '\n'
         << "        Parameters" << '\n'
         << "    steady_state : np.ndarray" << '\n'
         << "        Steady state values" << '\n'
         << "    it_ : int" << '\n'
         << "        Time period" << '\n'
         << "    " << '\n'
         << "    Returns" << '\n'
         << "    -------" << '\n'
         << "    np.ndarray" << '\n'
         << "        Residuals" << '\n'
         << "    " << R"(""")" << '\n'
         << "    return dynamic_model.dynamic_resid(y, x, params, steady_state, it_)" << '\n';

  ofstream file(python_dir / "model.py", ios::out | ios::binary);
  if (!file.is_open())
    {
      cerr << "ERROR: Can't open file " << (python_dir / "model.py").string() << " for writing"
           << '\n';
      exit(EXIT_FAILURE);
    }
  file << output.str();
  file.close();
}

void
ModFile::writePythonTypeStubs(const string& basename) const
{
  filesystem::path python_dir = filesystem::path(basename) / "python";
  ostringstream output;

  output << "# Type stubs for Dynare model" << '\n'
         << "import numpy as np" << '\n'
         << "from typing import List" << '\n'
         << '\n'
         << "class DynareModel:" << '\n'
         << "    model_name: str" << '\n'
         << "    endo_nbr: int" << '\n'
         << "    exo_nbr: int" << '\n'
         << "    param_nbr: int" << '\n'
         << "    param_names: List[str]" << '\n'
         << "    endo_names: List[str]" << '\n'
         << "    exo_names: List[str]" << '\n'
         << "    def __init__(self) -> None: ..." << '\n';

  ofstream file(python_dir / "driver.pyi", ios::out | ios::binary);
  if (!file.is_open())
    {
      cerr << "ERROR: Can't open file " << (python_dir / "driver.pyi").string() << " for writing"
           << '\n';
      exit(EXIT_FAILURE);
    }
  file << output.str();
  file.close();
}
