Files
klammertext/mac/klammerset_registry.cpp
T
ack 830b19394d Klammertext runs natively on Windows; a Windows source-install guide; the bundled sh for @eval :shell
Assembled from klammertext-dev f344a37d772c. The commands, the library,
the Standard Klammer Set, the reflection klammerset and the Python
binding build and run natively on Windows (MSYS2 UCRT64 gcc, python.org
Python), and both test tiers pass there. doc/install/windows_source_install.md
is the fifth install guide: a source install parallel to the Linux and
macOS ones, verified by following it on a pristine Windows 11.

Behaviour on every system: paths Klammertext prints use forward slashes;
output is LF; the command line is UTF-8 on Windows; @eval :shell is a
POSIX shell command everywhere -- on Windows it runs the sh Klammertext
ships, BusyBox for Windows (win/busybox/, GPLv2, with its licence and the
source pointer). KLAMMERTEXT_FONTS and KLAMMERTEXT_KLAMMERSETS are lists in
the system's PATH convention (';' on Windows). On Linux and macOS nothing
observable changes.
2026-09-25 00:33:26 +02:00

347 lines
14 KiB
C++

#include <cctype>
#include <cstdlib>
#include <iomanip>
#include <sstream>
#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> Klammerset_registry::add(
std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& 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<unsigned char>(symbol[0]));
for (char c : symbol) {
if (!std::isalnum(static_cast<unsigned char>(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<unsigned char>(name[0]))) {
return false;
}
for (char c : name) {
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_') {
return false;
}
}
return true;
}
std::vector<std::string> klammerset_search_dirs(const std::string& local_dir)
{
std::vector<std::string> 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).generic_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 strings_t installed =
directory_list("KLAMMERTEXT_KLAMMERSETS", ".klammertext/klammersets");
for (const std::string& dir : installed) {
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<std::string> 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:\n "
+ join(dirs, "\n ")
+ "\nEnter \"kdesc --klammersets\" to list the available klammersets.",
loc);
}
// The description out of a klammerset's declaration, WITHOUT loading it.
//
// The listing below enumerates the sets a user could load, and loading is
// not an option for finding out what they are: a "@@@klammerset"
// declaration is OPERATIVE -- processing one reads its ":requires" and
// ":files" -- so describing five available sets by loading them would load
// all five. So this reads the declaring file as text and takes the second
// positional, following the declaration's own parameter spec
// ("symbol | desc :name :author :date :requires :files"): the description
// runs from the bar after the symbol to whichever comes first of an option
// (" :" and a letter -- the same boundary the real parser sees, which is
// why a description may not contain a bare ":name"), another bar, or the
// closing "@@@".
//
// It is a DISPLAY convenience and says so by failing quietly: anything it
// cannot read as a description yields an empty string and the listing
// simply shows the symbol and its file, as it did before.
static std::string klammerset_description(const fs::path& declaration)
{
std::string text {};
try {
text = string_from_file(declaration.string());
} catch (...) {
return "";
}
static const std::string tag = "@@@klammerset";
size_t at = text.find(tag);
if (at == std::string::npos) return "";
size_t bar = text.find('|', at + tag.size());
if (bar == std::string::npos) return "";
size_t pos = bar + 1;
size_t end = text.size();
for (size_t i = pos; i + 1 < text.size(); i++) {
if (text[i] == '|' || (text.compare(i, 3, "@@@") == 0)) {
end = i;
break;
}
// An option: whitespace, ":", a letter. "Document production:
// formatting" is NOT one -- its colon ends a word and is followed
// by a space -- which is the distinction the parser makes too.
if (text[i] == ':' && i > pos
&& std::isspace(static_cast<unsigned char>(text[i - 1]))
&& std::isalpha(static_cast<unsigned char>(text[i + 1]))) {
end = i;
break;
}
}
return collapse_whitespace(text.substr(pos, end - pos));
}
std::string describe_klammerset_search(const std::string& local_dir, int margin)
{
std::string tab(margin, ' ');
std::stringstream ss {};
std::map<std::string, std::string> 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;
}
std::string line = tab + symbol + sp_arrow + declaration.generic_string();
if (first_hit.count(symbol)) {
line += " (shadowed by " + first_hit[symbol] + ")";
} else {
first_hit[symbol] = declaration.generic_string();
}
// What a set IS, under where it lives: the symbol and the path
// say which file would load, and nothing at all about whether
// this is a set to load or (say) the commands' own reference
// documentation. Read from the declaration without loading it
// -- see klammerset_description() above.
std::string desc = klammerset_description(declaration);
if (!desc.empty()) {
line += "\n" + tab + std::string(symbol.size(), ' ')
+ " " + desc;
}
ss << line << "\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<std::string> 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;
}