#include #include #include #include #include "klammerset_registry.h" #include "error.h" #include "file.h" #include "log.h" #include "util.h" #include "show.h" #include "katom.h" Klammerset_registry::Klammerset_registry() : m_parameters(Parameter_set("symbol | desc :name :author :date :requires :files")) { } void Klammerset_registry::add(Klammerset klammerset) { (void)K::log(3, klammerset.m_symbol); m_klammersets[klammerset.m_symbol] = klammerset; m_symbols.push_back(klammerset.m_symbol); } std::optional Klammerset_registry::add( std::vector::iterator begin, std::vector::iterator end, std::vector& katoms) { (void)K::log(3, *begin, *(end - 1)); auto [positional, optional, rest] = argument_split(begin + 1, end - 1, m_parameters.m_positional.size()); auto values = m_parameters.value_map(positional, optional, rest, begin->m_loc); modify_type(katom_t::replaced, begin, end); auto next_iter = end; ignore_whitespace(next_iter, katoms); std::string symbol = values["symbol"]; check_symbol(symbol, begin->m_loc); check_declaring_file(symbol, begin->m_loc); if (has(symbol)) { (void)K::log(2, "Klammerset \"" + symbol + "\" is already loaded; declaration skipped"); return std::nullopt; } // The filename lists follow the standard conventions (spaces allowed, // standalone "/" separator). The existence rescue tests names against // the declaring file's directory, where relative names are later // resolved by Machine::load_klammerset_files. fs::path declaring(begin->m_loc.m_filename); std::string base_dir = fs::exists(declaring) ? declaring.parent_path().string() : ""; Klammerset klammerset(symbol, values["desc"], begin->m_loc); klammerset.m_name = values["name"]; klammerset.m_author = values["author"]; klammerset.m_date = values["date"]; klammerset.m_requires = resolve_filename_list(values["requires"], base_dir); klammerset.m_files = resolve_filename_list(values["files"], base_dir); add(klammerset); return klammerset; } void Klammerset_registry::check_symbol(const std::string& symbol, const Locator& loc) const { bool valid = !symbol.empty() && std::isalpha(static_cast(symbol[0])); for (char c : symbol) { if (!std::isalnum(static_cast(c)) && c != '_') { valid = false; } } if (!valid) { throw Klammerset_error( "The klammerset symbol \"" + symbol + "\" is not valid. A symbol begins " "with a letter and contains only letters, digits, and underscores.", loc); } } // A klammerset that can be NAMED must live where its name says: symbol X is // declared in X/X.k (Andy, 2026-08-15). The convention already governed // symbol RESOLUTION; making it a requirement of the declaration too is what // turns the symbol into a function of the path -- and that is what lets the // already-loaded guard run BEFORE a file is read rather than after, which is // the whole of the diamond-":requires" fix. Without it the symbol is known // only once the file has been parsed, and by then a second read has already // re-executed the declaring file's own definitions. // // It also continues the reasoning behind symbols-only on the command line // (2026-08-14): if every klammerset is reachable by symbol the set of all of // them is ENUMERABLE; if every declaration is in X/X.k each one is also // IDENTIFIABLE from where it sits. // // EXEMPT: a declaration that is not in a ".k" file at all. A document may // declare a klammerset -- that is how a designer writes one, and kdesc's // provenance filter depends on it -- and such a set is local to the document: // nothing can ":requires" it, so it has no identity to protect. void Klammerset_registry::check_declaring_file( const std::string& symbol, const Locator& loc) const { fs::path declaring(loc.m_filename); if (declaring.extension() != ".k") { return; } if (declaring.stem() == symbol && declaring.parent_path().filename() == symbol) { return; } throw Klammerset_error( "The klammerset \"" + symbol + "\" must be declared in a file named \"" + symbol + "/" + symbol + ".k\", but this declaration is in \"" + declaring.filename().string() + "\" (in a directory named \"" + declaring.parent_path().filename().string() + "\"). A klammerset that " "can be named is identified by where it is, so that a symbol names one " "file and one file declares one symbol. A klammerset local to a document " "may be declared in the document itself.", loc); } bool Klammerset_registry::has(const std::string& symbol) const { return m_klammersets.count(symbol) > 0; } Klammerset Klammerset_registry::get(const std::string& symbol, const Locator& loc) const { if (has(symbol)) { return m_klammersets.at(symbol); } else { throw Klammerset_error("Klammerset " + symbol + " does not exist", loc); } } // --- The klammerset search path --- bool is_klammerset_symbol(const std::string& name) { if (name.empty() || !std::isalpha(static_cast(name[0]))) { return false; } for (char c : name) { if (!std::isalnum(static_cast(c)) && c != '_') { return false; } } return true; } std::vector klammerset_search_dirs(const std::string& local_dir) { std::vector result {}; // A repeated directory (e.g. the local stage already IS // $KLAMMERTEXT_HOME) adds nothing and clutters error messages. auto push_unique = [&result](const std::string& dir) { std::string canonical = fs::weakly_canonical(dir).string(); if (!is_in(canonical, result)) { result.push_back(canonical); } }; if (!local_dir.empty()) { push_unique(local_dir); } // Read ONCE per process, not once per resolution. A search path decides // WHICH FILE a symbol means -- an identity question, not a value one -- so // it must not move while a document is being processed: if it did, "which // klammerset is X" would depend on evaluation order and "kdesc // --klammersets" could not be a complete answer, which is the reason the // command line takes symbols only. // // It could move. The embedded Python shares the process, so an @eval // doing os.environ[...] = ... changed what a later getenv here returned; // a document could extend its own search path between one declaration and // the next. Nothing intended that -- it fell out of a fresh getenv and an // in-process interpreter. static const std::string paths = [] { const char* env = std::getenv("KLAMMERTEXT_KLAMMERSETS"); if (env && *env) { return std::string(env); } if (const char* home = std::getenv("HOME"); home && *home) { return std::string(home) + "/.klammertext/klammersets"; } return std::string(); }(); std::stringstream ss(paths); std::string dir; while (std::getline(ss, dir, ':')) { if (!dir.empty()) { push_unique(dir); } } if (const char* kthome = std::getenv(klammertext_home_var.c_str()); kthome && *kthome) { push_unique(kthome); } return result; } fs::path resolve_klammerset_symbol( const std::string& symbol, const std::string& local_dir, const Locator& loc) { std::vector dirs = klammerset_search_dirs(local_dir); for (const std::string& dir : dirs) { fs::path candidate = fs::path(dir) / symbol / (symbol + ".k"); if (fs::exists(candidate) && fs::is_regular_file(candidate)) { return fs::weakly_canonical(candidate); } } throw Klammerset_error( "The klammerset \"" + symbol + "\" was not found. A symbol x names the " "declaration file x/x.k in one of the search directories: " + join(dirs, ", ") + ". Enter \"kdesc --klammersets\" to list the available klammersets.", loc); } std::string describe_klammerset_search(const std::string& local_dir, int margin) { std::string tab(margin, ' '); std::stringstream ss {}; std::map first_hit {}; for (const std::string& dir : klammerset_search_dirs(local_dir)) { if (!fs::is_directory(dir)) { continue; } for (const auto& entry : fs::directory_iterator(dir)) { if (!entry.is_directory()) { continue; } std::string symbol = entry.path().filename().string(); fs::path declaration = entry.path() / (symbol + ".k"); if (!is_klammerset_symbol(symbol) || !fs::is_regular_file(declaration)) { continue; } ss << tab << symbol << sp_arrow << declaration.string(); if (first_hit.count(symbol)) { ss << " (shadowed by " << first_hit[symbol] << ")"; } else { first_hit[symbol] = declaration.string(); } ss << "\n"; } } if (first_hit.empty()) { ss << tab << "(no klammersets found)\n"; } return ss.str(); } std::string Klammerset_registry::describe(int margin, bool long_format) const { std::string tab(margin, ' '); std::stringstream ss {}; std::vector descs {}; for (const std::string& symbol : m_symbols) { descs.push_back(m_klammersets.at(symbol).m_desc); } auto symbol_width = max_length(m_symbols); auto desc_width = max_length(descs); for (const std::string& symbol : m_symbols) { const Klammerset& ks = m_klammersets.at(symbol); if (long_format) { ss << tab << std::setfill(' ') << std::setw(symbol_width) << std::left << symbol << " " << std::setw(desc_width) << std::left << ks.m_desc << " " << ks.m_loc.str() << "\n"; if (!ks.m_name.empty()) ss << tab << std::string(symbol_width, ' ') << " name: " << ks.m_name << "\n"; if (!ks.m_author.empty()) ss << tab << std::string(symbol_width, ' ') << " author: " << ks.m_author << "\n"; if (!ks.m_date.empty()) ss << tab << std::string(symbol_width, ' ') << " date: " << ks.m_date << "\n"; if (!ks.m_requires.empty()) ss << tab << std::string(symbol_width, ' ') << " requires: " << join(ks.m_requires, " / ") << "\n"; if (!ks.m_files.empty()) ss << tab << std::string(symbol_width, ' ') << " files: " << join(ks.m_files, " / ") << "\n"; } else { ss << tab << std::setfill(' ') << std::setw(symbol_width) << std::left << symbol << sp_arrow << ks << "\n"; } } return ss.str(); } void Klammerset_registry::set_loaded_files(const std::string& symbol, const strings_t& files) { auto it = m_klammersets.find(symbol); if (it != m_klammersets.end()) { it->second.m_loaded_files = files; } } strings_t Klammerset_registry::loaded_files() const { strings_t result {}; for (const auto& [symbol, klammerset] : m_klammersets) { result.insert(result.end(), klammerset.m_loaded_files.begin(), klammerset.m_loaded_files.end()); } return result; }