#include "util.h" #include "eval.h" #include "eval_python.h" #include "eval_cpp.h" #include "log.h" #include "show.h" #include "katom.h" #include "file.h" #include #include #include std::string shell(State state, std::string command, Locator loc) { command = state.subst(command); FILE* pipe = popen(command.c_str(), "r"); if (!pipe) { throw Parsing_error( "Could not run command:\n" + command, loc, false); } char buffer[128]; std::string result = ""; while (fgets(buffer, sizeof(buffer), pipe) != nullptr) { result += buffer; } pclose(pipe); // std::cout << "Command output:\n" << result << "\n"; return result; } bool is_haskell_file(const std::string& text) { std::string trimmed = trim(text); if (trimmed.size() < 4) return false; if (trimmed.find(' ') != std::string::npos) return false; if (trimmed.find('\n') != std::string::npos) return false; return trimmed.substr(trimmed.size() - 3) == ".hs"; } std::string run_haskell(const std::string& hsfile, Locator loc) { std::string command = "runghc " + hsfile + " 2>&1"; FILE* pipe = popen(command.c_str(), "r"); if (!pipe) { throw Parsing_error( "Could not run runghc.", loc, false); } char buffer[128]; std::string result = ""; while (fgets(buffer, sizeof(buffer), pipe) != nullptr) { result += buffer; } int status = pclose(pipe); if (status != 0) { throw Parsing_error( "Haskell evaluation failed:\n" + result, loc, false); } return result; } std::string haskell(State state, std::string code, Locator loc) { if (system("which runghc > /dev/null 2>&1") != 0) { throw Parsing_error( "@eval with the :haskell argument requires runghc, which was not found in PATH.\n" "Install it using GHCup; see https://www.haskell.org/ghcup/install/.", loc, false); } code = state.subst(code); if (is_haskell_file(code)) { return run_haskell(trim(code), loc); } const char* tmpdir = std::getenv("TMPDIR"); if (!tmpdir) tmpdir = "/tmp"; std::string tmpl = std::string(tmpdir) + "/klammertext_haskell_XXXXXX"; std::vector tmppath(tmpl.begin(), tmpl.end()); tmppath.push_back('\0'); int fd = mkstemp(tmppath.data()); if (fd < 0) { throw Parsing_error( "Could not create temporary file for Haskell evaluation.", loc, false); } std::string hsfile = std::string(tmppath.data()) + ".hs"; close(fd); rename(tmppath.data(), hsfile.c_str()); FILE* f = fopen(hsfile.c_str(), "w"); if (!f) { unlink(hsfile.c_str()); throw Parsing_error( "Could not write temporary Haskell file.", loc, false); } fprintf(f, "%s\n", code.c_str()); fclose(f); std::string result = run_haskell(hsfile, loc); unlink(hsfile.c_str()); return result; } void check_cpp_arguments(katom_list args, Locator loc) { if (args.size() != 4 && args.size() != 5) { std::stringstream ss {}; ss << "Incorrect @eval format for a C++ function. Either:\n" << " @eval :cpp @\n" << "or\n" << " @eval :cpp @\n" << "In the first case, the library basename is used for the function name."; throw Argument_error(ss.str(), loc, false); } } // Save the process working directory, change to DIR, and restore on // destruction (exception-safe), so an @eval's :cwd cannot leak into the // rest of the run. NOTE: the cwd is process-global state; if input files // are ever processed in parallel, this needs rethinking. class Cwd_guard { public: explicit Cwd_guard(const std::string& dir) : m_saved(fs::current_path()) { fs::current_path(dir); } ~Cwd_guard() { std::error_code ec; fs::current_path(m_saved, ec); // never throw from a destructor } Cwd_guard(const Cwd_guard&) = delete; Cwd_guard& operator=(const Cwd_guard&) = delete; private: fs::path m_saved; }; std::string Eval::eval_command(katom_iter begin, katom_iter end) { (void)K::log(3, *begin, *(end - 1)); //msg() << "in Eval::eval:\n" << ktype << kall << kindex << std::pair(begin, end) << "\n"; katom_iter first = after_whitespace(begin + 1); std::string eval_result = "[unevaluated]"; // :cwd DIR — run the eval (any mode) with DIR as the working directory, // restoring the process cwd afterwards. The default is the directory // ktext was started in (unchanged behavior). DIR may hold state // substitutions (:cwd *K_input_dir* is the document's directory) and, // per the filenames-with-spaces convention, whitespace-separated tokens // are joined until they name an existing directory. std::optional cwd_guard {}; if (first->m_text == ":cwd") { katom_iter tok = after_whitespace(first + 1); std::string dir {}; katom_iter cursor = tok; katom_iter resume = tok; while (cursor < end - 1 && !cursor->m_text.starts_with(":")) { dir = m_machine.m_state.subst(as_string(tok, cursor + 1, true)); resume = after_whitespace(cursor + 1); if (fs::is_directory(dir)) break; cursor = resume; } if (dir.empty() || !fs::is_directory(dir)) { throw Argument_error( "The :cwd directory does not exist: \"" + dir + "\"", begin->m_loc, false); } cwd_guard.emplace(dir); first = resume; } std::string first_word = first->m_text; int offset = first_word[0] == ':' ? 1 : 0; std::string command = as_string(first + offset, end - 1, true); if (offset == 0 || first_word == ":python") { // Default is Python Eval_python E_python(m_machine, begin->m_loc); eval_result = E_python.eval(command); } else if (first_word == ":shell") { eval_result = shell(m_machine.m_state, command, begin->m_loc); } else if (first_word == ":haskell") { eval_result = haskell(m_machine.m_state, command, begin->m_loc); } else if (first_word == ":cpp") { //msg() << ":cpp: " << first_word << *(begin + 4) << "\n"; for (auto ki = begin; ki < end; ki++) { //msg() << " " << kall << kindex << *ki << "\n"; } katom_list args = text_katoms(begin, end); check_cpp_arguments(args, begin->m_loc); //msg() << "args: |" << args << "|\n"; std::string lib_text = args[2].m_text; std::string khome = m_machine.m_state.value("KLAMMERTEXT_HOME", false); if (!khome.empty()) { lib_text = string_replace(lib_text, "*KLAMMERTEXT_HOME*", khome); } fs::path libpath(lib_text + ".so"); // A relative library name not found from the cwd is searched in the // directories of the files the Machine has read (same rule as the // Python module path: the library lives next to the file using it). if (libpath.is_relative() && !fs::exists(fs::absolute(libpath))) { for (const auto& dir : m_machine.m_state.m_search_dirs) { fs::path candidate = fs::path(dir) / libpath; if (fs::exists(candidate)) { libpath = candidate; break; } } } libpath = fs::absolute(libpath); std::string funcname = args.size() == 4 ? libpath.stem().string() : args[3].m_text; Eval_cpp E_cpp(m_machine, begin->m_loc); eval_result = E_cpp.eval(libpath, funcname); } return eval_result; } katom_list Eval::eval(katom_iter begin, katom_iter end) { std::string eval_result = eval_command(begin, end); katom_list result {}; Machine M = m_machine; M.m_warn_unparsed = false; // Result text is machine-generated (see machine.h) size_t before = M.m_katoms.size(); M.read(eval_result); // If the @eval produced more Klammertext -- the read-back result still // holds klammers to reduce (a "generator", e.g. a Python klammer returning // a @table call with data) -- then its text is writer content: escape it // for the target before applying, exactly as typed input is escaped, so a // "%" in the data becomes "\%". If it produced final target markup (no // klammers left, a "renderer" such as @table itself emitting \begin{tabular}) // leave it untouched. ^'...'^ literal spans are skipped by the escape pass, // so a generator can still carry raw target markup. bool has_klammer = std::any_of( M.m_katoms.begin() + before, M.m_katoms.end(), begin_apply); M.apply(M.m_state.value("K_target"), false, has_klammer); result = trim(M.m_katoms); return result; }