A snapshot of the development tree. The substantial changes since the last one:
COMMAND OUTPUT POLICY. The three commands display text in exactly three cases,
and each owns a stream: LOGGING under "-v" greater than 0 and an ERROR before
termination go to STDERR; OUTPUT THE USER ASKED FOR goes to STDOUT. For ktext
that output is a document, so "ktext doc.kt -d | ..." is now safe -- logging
used to share the stream and land inside the document. A bare command prints
its usage and succeeds rather than failing. Colour is emitted only to a
terminal, per stream, and NO_COLOR is honoured.
"-v 1" reports every decision whose outcome you could not have read off your own
input: the klammerset that was loaded and from which file, a font's directory, a
":files" name's file, how "-o" was expanded. Higher levels are the trace.
The commands no longer warn and continue: an anomaly is an error, described with
its location. Two exceptions remain, each for a stated reason -- a condition
that is expected and temporary by design, and a judgment that is a heuristic
rather than exact.
@cond IS NOW A TRUE SPECIAL FORM, resolved at APPLICATION time rather than when
the file is read. Two consequences for a writer:
* a state variable reaches the predicate. "@@@state Flag :value true @@@
@cond *Flag* | T | F @" renders "T"; it used to see the literal "*Flag*" and
silently take the false branch. The document now behaves like a klammer
body, whose arguments are bound before its conditionals are decided.
* nothing in a discarded branch happens -- it is not read, not evaluated, not
expanded. An @eval in the branch not taken used to run anyway.
Its predicate relation is total and strict: true, True, 1; false, False, 0, and
empty; anything else is an error at the @cond rather than silently false.
@eval REACHING OUTSIDE. ":shell" and ":haskell" now keep the command's standard
error out of the document (it appears under "-v 1") and treat a nonzero exit as
an error naming what the command reported. A command that exits nonzero on
purpose -- "grep" finding no match -- says so with "|| true".
KLAMMER SETS. Several combine: "--klammersets a b c" loads all three in the
order given, sharing one namespace, with the definition modes deciding
collisions. "none" means none and may not be combined with other symbols. A
klammerset with symbol X is declared in a file X/X.k, which is what lets two
sets require the same third set without loading it twice.
TESTS. Four new suites: the kdiag command's interface, the @eval primitive's
contract with the outside world, and verbosity at both tiers. Three suites
that could not run on macOS at all now do.
Assembled from dev commit 6c8ee6c22fca.
299 lines
11 KiB
C++
299 lines
11 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).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<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: "
|
|
+ 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<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;
|
|
}
|
|
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<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;
|
|
}
|