Target coverage: a klammer states the targets it serves
kdesc gains --coverage, which reports for every klammer the set of targets it
can render to, and — the point of it — which klammers' coverage cannot be
derived and must therefore be declared. Three rules: coverage is DERIVED
where the definitions determine it (a general body of klammer calls covers
the intersection of what those klammers cover, by a greatest fixpoint after
loading), DECLARED where the engine cannot interpret what decides it (an
@eval body, whose targets are undecidable), and UNKNOWN where nothing is
written — which never means "deliberately unavailable".
Two new spellings in a definition's name. A comma-separated target list,
"@@table.html,tex :: ...", gives one body several targets; it is surface
syntax, expanded at registration, and each member goes through the
redefinition rules on its own. And "@@date.* :: ..." writes the general
target out, asserting that the klammer works for EVERY target including ones
not yet defined — a stronger claim than a list of the targets defined today,
and the one target declaration that could be mechanically falsified.
The Standard Klammer Set was swept accordingly: it now has no general
definitions at all, every klammer names the targets it serves, six use ".*",
and tex and pdf are at zero undecided.
kdesc's flags are reorganised on two rules: a flag reached for often gets a
single letter (-k klammers, -t targets, -c characters, -i input), a more
specialised topic a multi-letter name (--argtypes, --katoms, --rewrite,
--optionsets, --coverage, --klammerset, --font); and -v says how much to show
about PROCESSING, never what the RESULT contains — so the katom regex column
is "--katoms full" and the coverage detail "--coverage all". NOTE: "-k" now
lists klammers (optionally filtered by a name/description search); the katom
table moved to "--katoms".
Fixes carried along: an option written with no value crashed the command with
SIGSEGV instead of reporting the mistake; two required positional arguments
never parsed; kdesc and kdiag printed an error and exited 0; and definition
diagnostics counted registrations rather than what was written, so one line
could be reported as two definitions and then printed twice.
Four new test suites: target_list, coverage, command_option, kdesc.
(from dev 46f54080bd9a)
2026-08-12 17:20:23 +02:00
|
|
|
#include <algorithm>
|
|
|
|
|
#include <functional>
|
|
|
|
|
#include <iomanip>
|
|
|
|
|
#include <ostream>
|
|
|
|
|
#include <set>
|
|
|
|
|
|
|
|
|
|
#include "coverage.h"
|
|
|
|
|
#include "machine.h"
|
|
|
|
|
#include "katom.h"
|
|
|
|
|
#include "util.h"
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
using target_set = std::set<std::string>;
|
|
|
|
|
|
|
|
|
|
// The written definitions of one klammer, split by what they tell us. A
|
|
|
|
|
// definition filed under the general name is the only one whose coverage has
|
|
|
|
|
// to be worked out; a definition written for a target IS its own statement.
|
|
|
|
|
struct Written
|
|
|
|
|
{
|
|
|
|
|
const Klammer::components* m_general { nullptr };
|
|
|
|
|
target_set m_targets {}; // targets with a definition of their own
|
|
|
|
|
bool m_declared { false }; // has a ".k" declaration
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Written written_definitions(const Klammer& klammer)
|
|
|
|
|
{
|
|
|
|
|
Written w {};
|
|
|
|
|
for (const auto& def : klammer.m_defs) {
|
|
|
|
|
if (def.target == Target_registry::general_name) {
|
|
|
|
|
w.m_general = &def;
|
|
|
|
|
} else if (def.target == Target_registry::declare_name) {
|
|
|
|
|
w.m_declared = true;
|
|
|
|
|
} else if (def.target != Target_registry::optionset_name) {
|
|
|
|
|
w.m_targets.insert(def.target);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return w;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// What a general body is made of. The three findings are ordered by how
|
|
|
|
|
// firmly they settle the question: anything the engine cannot interpret ends
|
|
|
|
|
// the analysis, and only a body of plain klammer calls is derivable.
|
|
|
|
|
struct Body_scan
|
|
|
|
|
{
|
|
|
|
|
bool m_undecidable { false };
|
|
|
|
|
std::string m_reason {};
|
|
|
|
|
std::vector<std::string> m_calls {};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Body_scan scan_body(const katom_list& body)
|
|
|
|
|
{
|
|
|
|
|
Body_scan scan {};
|
|
|
|
|
target_set seen {};
|
|
|
|
|
for (const auto& k : body) {
|
|
|
|
|
switch (k.m_type) {
|
|
|
|
|
case katom_t::eval_begin:
|
|
|
|
|
// Which targets a Python function answers for is undecidable, so
|
|
|
|
|
// the analysis stops here and the targets must be declared.
|
|
|
|
|
scan.m_undecidable = true;
|
|
|
|
|
scan.m_reason = "@eval body";
|
|
|
|
|
return scan;
|
|
|
|
|
case katom_t::read_begin:
|
|
|
|
|
scan.m_undecidable = true;
|
|
|
|
|
scan.m_reason = "@read body";
|
|
|
|
|
return scan;
|
|
|
|
|
case katom_t::literal_begin:
|
|
|
|
|
// A ^'...'^ span exists to carry raw target markup past the
|
|
|
|
|
// escaping pass. A general body holding one is target-specific
|
|
|
|
|
// with nothing for the intersection rule to see -- the blind spot
|
|
|
|
|
// a body of plain text would otherwise hide.
|
|
|
|
|
scan.m_undecidable = true;
|
|
|
|
|
scan.m_reason = "^'...'^ literal span";
|
|
|
|
|
return scan;
|
|
|
|
|
case katom_t::apply_begin: {
|
|
|
|
|
// The body read here is the STORED body, which is the body as
|
|
|
|
|
// written with one exception: a general klammer that takes no
|
|
|
|
|
// parameters is a constant, and a constant's body is spliced into
|
|
|
|
|
// later definitions at definition time. So a call to a constant
|
|
|
|
|
// does not appear here -- what appears is whatever the constant
|
|
|
|
|
// expanded to. That is the right thing for coverage (the calls
|
|
|
|
|
// that remain are the ones that will be applied), but it makes
|
|
|
|
|
// the "from" list a statement about the stored body, not about
|
|
|
|
|
// the source text.
|
|
|
|
|
std::string name = trim_char(k.m_text, '@');
|
|
|
|
|
if (seen.insert(name).second) {
|
|
|
|
|
scan.m_calls.push_back("@" + name);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return scan;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
target_set intersect(const target_set& a, const target_set& b)
|
|
|
|
|
{
|
|
|
|
|
target_set result {};
|
|
|
|
|
std::set_intersection(a.begin(), a.end(), b.begin(), b.end(),
|
|
|
|
|
std::inserter(result, result.begin()));
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> as_vector(const target_set& s)
|
|
|
|
|
{
|
|
|
|
|
return { s.begin(), s.end() };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Shorten a definition's pathname for the report. What identifies a
|
|
|
|
|
// definition to a reader is its tail -- "sks/block/block.k" -- not the
|
|
|
|
|
// absolute path the file happened to be read from, which is the same long
|
|
|
|
|
// prefix on every row. A klammer set outside $KLAMMERTEXT_HOME keeps its
|
|
|
|
|
// path in full rather than being shortened to something ambiguous.
|
|
|
|
|
std::string short_path(const std::string& path)
|
|
|
|
|
{
|
|
|
|
|
auto pos = path.rfind("/sks/");
|
|
|
|
|
if (pos != std::string::npos) {
|
|
|
|
|
return path.substr(pos + 1);
|
|
|
|
|
}
|
|
|
|
|
const char* home = std::getenv("KLAMMERTEXT_HOME");
|
|
|
|
|
if (home != nullptr) {
|
|
|
|
|
std::string prefix = std::string(home) + "/";
|
|
|
|
|
if (path.size() > prefix.size() && path.compare(0, prefix.size(), prefix) == 0) {
|
|
|
|
|
return path.substr(prefix.size());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
An output policy for the three commands, and @cond as a true special form
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.
2026-08-16 01:37:59 +02:00
|
|
|
std::vector<Klammer_coverage> klammer_coverage(const Machine& machine,
|
|
|
|
|
const strings_t& defined_outside)
|
Target coverage: a klammer states the targets it serves
kdesc gains --coverage, which reports for every klammer the set of targets it
can render to, and — the point of it — which klammers' coverage cannot be
derived and must therefore be declared. Three rules: coverage is DERIVED
where the definitions determine it (a general body of klammer calls covers
the intersection of what those klammers cover, by a greatest fixpoint after
loading), DECLARED where the engine cannot interpret what decides it (an
@eval body, whose targets are undecidable), and UNKNOWN where nothing is
written — which never means "deliberately unavailable".
Two new spellings in a definition's name. A comma-separated target list,
"@@table.html,tex :: ...", gives one body several targets; it is surface
syntax, expanded at registration, and each member goes through the
redefinition rules on its own. And "@@date.* :: ..." writes the general
target out, asserting that the klammer works for EVERY target including ones
not yet defined — a stronger claim than a list of the targets defined today,
and the one target declaration that could be mechanically falsified.
The Standard Klammer Set was swept accordingly: it now has no general
definitions at all, every klammer names the targets it serves, six use ".*",
and tex and pdf are at zero undecided.
kdesc's flags are reorganised on two rules: a flag reached for often gets a
single letter (-k klammers, -t targets, -c characters, -i input), a more
specialised topic a multi-letter name (--argtypes, --katoms, --rewrite,
--optionsets, --coverage, --klammerset, --font); and -v says how much to show
about PROCESSING, never what the RESULT contains — so the katom regex column
is "--katoms full" and the coverage detail "--coverage all". NOTE: "-k" now
lists klammers (optionally filtered by a name/description search); the katom
table moved to "--katoms".
Fixes carried along: an option written with no value crashed the command with
SIGSEGV instead of reporting the mistake; two required positional arguments
never parsed; kdesc and kdiag printed an error and exited 0; and definition
diagnostics counted registrations rather than what was written, so one line
could be reported as two definitions and then printed twice.
Four new test suites: target_list, coverage, command_option, kdesc.
(from dev 46f54080bd9a)
2026-08-12 17:20:23 +02:00
|
|
|
{
|
|
|
|
|
target_set all_targets {};
|
|
|
|
|
for (const auto& t : machine.m_targets.user_defined()) {
|
|
|
|
|
all_targets.insert(t);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pass 1: what each klammer's definitions say, without resolving anything.
|
|
|
|
|
std::map<std::string, Written> written {};
|
|
|
|
|
std::map<std::string, Body_scan> scans {};
|
|
|
|
|
for (const auto& [name, klammer] : machine.m_klammers.m_klammers) {
|
|
|
|
|
written[name] = written_definitions(klammer);
|
|
|
|
|
if (written[name].m_general != nullptr) {
|
|
|
|
|
scans[name] = scan_body(written[name].m_general->body);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pass 2: the greatest fixpoint. Every general klammer starts optimistic
|
|
|
|
|
// -- all targets -- and the intersection rule is applied until nothing
|
|
|
|
|
// shrinks. Starting optimistic is what makes a cycle terminate: two
|
|
|
|
|
// klammers calling each other simply keep each other's sets, and a set
|
|
|
|
|
// that only loses members cannot iterate forever.
|
|
|
|
|
std::map<std::string, target_set> general {};
|
|
|
|
|
for (const auto& [name, scan] : scans) {
|
|
|
|
|
general[name] = all_targets;
|
|
|
|
|
}
|
|
|
|
|
// A klammer's coverage, for use as an operand of the intersection: what
|
|
|
|
|
// its own definitions cover, plus whatever its general body covers.
|
|
|
|
|
auto coverage_of = [&](const std::string& name) -> target_set {
|
|
|
|
|
auto w = written.find(name);
|
|
|
|
|
if (w == written.end()) return {}; // not defined; contributes nothing
|
|
|
|
|
target_set result = w->second.m_targets;
|
|
|
|
|
auto g = general.find(name);
|
|
|
|
|
if (g != general.end()) {
|
|
|
|
|
result.insert(g->second.begin(), g->second.end());
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
};
|
|
|
|
|
bool changed = true;
|
|
|
|
|
while (changed) {
|
|
|
|
|
changed = false;
|
|
|
|
|
for (auto& [name, targets] : general) {
|
|
|
|
|
const Body_scan& scan = scans[name];
|
|
|
|
|
if (scan.m_undecidable || scan.m_calls.empty()) continue;
|
|
|
|
|
target_set next = all_targets;
|
|
|
|
|
for (const auto& call : scan.m_calls) {
|
|
|
|
|
std::string called = trim_char(call, '@');
|
|
|
|
|
if (called == name) continue; // self-reference constrains nothing
|
|
|
|
|
next = intersect(next, coverage_of(called));
|
|
|
|
|
}
|
|
|
|
|
if (next != targets) {
|
|
|
|
|
targets = next;
|
|
|
|
|
changed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
An output policy for the three commands, and @cond as a true special form
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.
2026-08-16 01:37:59 +02:00
|
|
|
// Pass 3: classify and collect. The filter applies HERE and not earlier:
|
|
|
|
|
// passes 1 and 2 must see every klammer, because a derived coverage is the
|
|
|
|
|
// intersection of what the called klammers cover and those are mostly the
|
|
|
|
|
// klammerset's.
|
Target coverage: a klammer states the targets it serves
kdesc gains --coverage, which reports for every klammer the set of targets it
can render to, and — the point of it — which klammers' coverage cannot be
derived and must therefore be declared. Three rules: coverage is DERIVED
where the definitions determine it (a general body of klammer calls covers
the intersection of what those klammers cover, by a greatest fixpoint after
loading), DECLARED where the engine cannot interpret what decides it (an
@eval body, whose targets are undecidable), and UNKNOWN where nothing is
written — which never means "deliberately unavailable".
Two new spellings in a definition's name. A comma-separated target list,
"@@table.html,tex :: ...", gives one body several targets; it is surface
syntax, expanded at registration, and each member goes through the
redefinition rules on its own. And "@@date.* :: ..." writes the general
target out, asserting that the klammer works for EVERY target including ones
not yet defined — a stronger claim than a list of the targets defined today,
and the one target declaration that could be mechanically falsified.
The Standard Klammer Set was swept accordingly: it now has no general
definitions at all, every klammer names the targets it serves, six use ".*",
and tex and pdf are at zero undecided.
kdesc's flags are reorganised on two rules: a flag reached for often gets a
single letter (-k klammers, -t targets, -c characters, -i input), a more
specialised topic a multi-letter name (--argtypes, --katoms, --rewrite,
--optionsets, --coverage, --klammerset, --font); and -v says how much to show
about PROCESSING, never what the RESULT contains — so the katom regex column
is "--katoms full" and the coverage detail "--coverage all". NOTE: "-k" now
lists klammers (optionally filtered by a name/description search); the katom
table moved to "--katoms".
Fixes carried along: an option written with no value crashed the command with
SIGSEGV instead of reporting the mistake; two required positional arguments
never parsed; kdesc and kdiag printed an error and exited 0; and definition
diagnostics counted registrations rather than what was written, so one line
could be reported as two definitions and then printed twice.
Four new test suites: target_list, coverage, command_option, kdesc.
(from dev 46f54080bd9a)
2026-08-12 17:20:23 +02:00
|
|
|
std::vector<Klammer_coverage> result {};
|
|
|
|
|
for (const auto& [name, klammer] : machine.m_klammers.m_klammers) {
|
An output policy for the three commands, and @cond as a true special form
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.
2026-08-16 01:37:59 +02:00
|
|
|
if (!defined_outside.empty() && !klammer.defined_outside(defined_outside)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
Target coverage: a klammer states the targets it serves
kdesc gains --coverage, which reports for every klammer the set of targets it
can render to, and — the point of it — which klammers' coverage cannot be
derived and must therefore be declared. Three rules: coverage is DERIVED
where the definitions determine it (a general body of klammer calls covers
the intersection of what those klammers cover, by a greatest fixpoint after
loading), DECLARED where the engine cannot interpret what decides it (an
@eval body, whose targets are undecidable), and UNKNOWN where nothing is
written — which never means "deliberately unavailable".
Two new spellings in a definition's name. A comma-separated target list,
"@@table.html,tex :: ...", gives one body several targets; it is surface
syntax, expanded at registration, and each member goes through the
redefinition rules on its own. And "@@date.* :: ..." writes the general
target out, asserting that the klammer works for EVERY target including ones
not yet defined — a stronger claim than a list of the targets defined today,
and the one target declaration that could be mechanically falsified.
The Standard Klammer Set was swept accordingly: it now has no general
definitions at all, every klammer names the targets it serves, six use ".*",
and tex and pdf are at zero undecided.
kdesc's flags are reorganised on two rules: a flag reached for often gets a
single letter (-k klammers, -t targets, -c characters, -i input), a more
specialised topic a multi-letter name (--argtypes, --katoms, --rewrite,
--optionsets, --coverage, --klammerset, --font); and -v says how much to show
about PROCESSING, never what the RESULT contains — so the katom regex column
is "--katoms full" and the coverage detail "--coverage all". NOTE: "-k" now
lists klammers (optionally filtered by a name/description search); the katom
table moved to "--katoms".
Fixes carried along: an option written with no value crashed the command with
SIGSEGV instead of reporting the mistake; two required positional arguments
never parsed; kdesc and kdiag printed an error and exited 0; and definition
diagnostics counted registrations rather than what was written, so one line
could be reported as two definitions and then printed twice.
Four new test suites: target_list, coverage, command_option, kdesc.
(from dev 46f54080bd9a)
2026-08-12 17:20:23 +02:00
|
|
|
const Written& w = written.at(name);
|
|
|
|
|
Klammer_coverage kc {};
|
|
|
|
|
kc.m_name = name;
|
|
|
|
|
kc.m_declared = w.m_declared;
|
|
|
|
|
kc.m_written = as_vector(w.m_targets);
|
|
|
|
|
// Written definitions only (m_defs), so this is where the klammer is
|
|
|
|
|
// WRITTEN. m_defloc would also carry the targets a general body was
|
|
|
|
|
// copied to, which name the same file again.
|
|
|
|
|
for (const auto& def : klammer.m_defs) {
|
|
|
|
|
std::string file = short_path(def.loc.m_filename);
|
|
|
|
|
if (!is_in(file, kc.m_files)) {
|
|
|
|
|
kc.m_files.push_back(file);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (const auto& [target, loc] : klammer.m_defloc) {
|
|
|
|
|
if (target != Target_registry::declare_name &&
|
|
|
|
|
target != Target_registry::general_name &&
|
|
|
|
|
target != Target_registry::optionset_name) {
|
|
|
|
|
kc.m_effective.push_back(target);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (klammer.m_general_declared) {
|
|
|
|
|
// The author has said "every target". That is a statement, not
|
|
|
|
|
// something to be re-derived from the body: the whole point of
|
|
|
|
|
// writing ".*" is to assert what an @eval body cannot be read to
|
|
|
|
|
// mean.
|
|
|
|
|
kc.m_kind = coverage_t::all_declared;
|
|
|
|
|
target_set covered = w.m_targets;
|
|
|
|
|
covered.insert(all_targets.begin(), all_targets.end());
|
|
|
|
|
kc.m_targets = as_vector(covered);
|
|
|
|
|
} else if (w.m_general == nullptr) {
|
|
|
|
|
kc.m_kind = w.m_targets.empty() ? coverage_t::none : coverage_t::declared;
|
|
|
|
|
kc.m_targets = kc.m_written;
|
|
|
|
|
} else {
|
|
|
|
|
const Body_scan& scan = scans.at(name);
|
|
|
|
|
target_set covered = w.m_targets;
|
|
|
|
|
if (scan.m_undecidable) {
|
|
|
|
|
kc.m_kind = coverage_t::undecidable;
|
|
|
|
|
kc.m_reason = scan.m_reason;
|
|
|
|
|
// What it covers is not knowable here; report what is written.
|
|
|
|
|
kc.m_targets = kc.m_written;
|
|
|
|
|
} else if (scan.m_calls.empty()) {
|
|
|
|
|
kc.m_kind = coverage_t::all;
|
|
|
|
|
covered.insert(all_targets.begin(), all_targets.end());
|
|
|
|
|
kc.m_targets = as_vector(covered);
|
|
|
|
|
} else {
|
|
|
|
|
kc.m_kind = coverage_t::derived;
|
|
|
|
|
kc.m_from = scan.m_calls;
|
|
|
|
|
const target_set& g = general.at(name);
|
|
|
|
|
covered.insert(g.begin(), g.end());
|
|
|
|
|
kc.m_targets = as_vector(covered);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
result.push_back(kc);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
std::string list_of(const std::vector<std::string>& v)
|
|
|
|
|
{
|
|
|
|
|
return v.empty() ? "--" : join(v, " ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void section(std::ostream& os, const std::string& title, size_t count,
|
|
|
|
|
const std::string& explanation)
|
|
|
|
|
{
|
|
|
|
|
os << "\n" << title << " (" << count << ")\n";
|
|
|
|
|
if (!explanation.empty()) {
|
|
|
|
|
os << explanation;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
void report_coverage(const Machine& machine,
|
|
|
|
|
const std::vector<Klammer_coverage>& coverage,
|
|
|
|
|
bool full, std::ostream& os)
|
|
|
|
|
{
|
|
|
|
|
strings_t targets = machine.m_targets.user_defined();
|
|
|
|
|
os << "Klammer coverage\n"
|
|
|
|
|
<< "================\n\n"
|
An output policy for the three commands, and @cond as a true special form
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.
2026-08-16 01:37:59 +02:00
|
|
|
<< coverage.size() << " " << plural("klammer", static_cast<int>(coverage.size()))
|
|
|
|
|
<< ", " << targets.size() << " " << plural("target", static_cast<int>(targets.size()))
|
|
|
|
|
<< ": " << join(targets, " ") << "\n";
|
Target coverage: a klammer states the targets it serves
kdesc gains --coverage, which reports for every klammer the set of targets it
can render to, and — the point of it — which klammers' coverage cannot be
derived and must therefore be declared. Three rules: coverage is DERIVED
where the definitions determine it (a general body of klammer calls covers
the intersection of what those klammers cover, by a greatest fixpoint after
loading), DECLARED where the engine cannot interpret what decides it (an
@eval body, whose targets are undecidable), and UNKNOWN where nothing is
written — which never means "deliberately unavailable".
Two new spellings in a definition's name. A comma-separated target list,
"@@table.html,tex :: ...", gives one body several targets; it is surface
syntax, expanded at registration, and each member goes through the
redefinition rules on its own. And "@@date.* :: ..." writes the general
target out, asserting that the klammer works for EVERY target including ones
not yet defined — a stronger claim than a list of the targets defined today,
and the one target declaration that could be mechanically falsified.
The Standard Klammer Set was swept accordingly: it now has no general
definitions at all, every klammer names the targets it serves, six use ".*",
and tex and pdf are at zero undecided.
kdesc's flags are reorganised on two rules: a flag reached for often gets a
single letter (-k klammers, -t targets, -c characters, -i input), a more
specialised topic a multi-letter name (--argtypes, --katoms, --rewrite,
--optionsets, --coverage, --klammerset, --font); and -v says how much to show
about PROCESSING, never what the RESULT contains — so the katom regex column
is "--katoms full" and the coverage detail "--coverage all". NOTE: "-k" now
lists klammers (optionally filtered by a name/description search); the katom
table moved to "--katoms".
Fixes carried along: an option written with no value crashed the command with
SIGSEGV instead of reporting the mistake; two required positional arguments
never parsed; kdesc and kdiag printed an error and exited 0; and definition
diagnostics counted registrations rather than what was written, so one line
could be reported as two definitions and then printed twice.
Four new test suites: target_list, coverage, command_option, kdesc.
(from dev 46f54080bd9a)
2026-08-12 17:20:23 +02:00
|
|
|
|
|
|
|
|
auto of_kind = [&coverage](coverage_t kind) {
|
|
|
|
|
std::vector<const Klammer_coverage*> result {};
|
|
|
|
|
for (const auto& kc : coverage) {
|
|
|
|
|
if (kc.m_kind == kind) result.push_back(&kc);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
};
|
|
|
|
|
size_t width = 0;
|
|
|
|
|
for (const auto& kc : coverage) {
|
|
|
|
|
width = std::max(width, kc.m_name.size());
|
|
|
|
|
}
|
|
|
|
|
// The indent of a continuation line, under the name column.
|
|
|
|
|
std::string continuation(2 + 1 + width, ' ');
|
|
|
|
|
|
|
|
|
|
auto name_of = [&](const Klammer_coverage& kc) {
|
|
|
|
|
std::stringstream ss {};
|
|
|
|
|
ss << " @" << std::left << std::setw(width) << kc.m_name;
|
|
|
|
|
return ss.str();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// A section's rows, held until the whole section is built so the
|
|
|
|
|
// source-file column can be aligned. The file goes LAST because it is
|
|
|
|
|
// reference information: what the row says comes first, and the reader
|
|
|
|
|
// looks right only when they want to go and edit it. Only a row naming
|
|
|
|
|
// ONE klammer carries a file -- the "All targets" section lists many
|
|
|
|
|
// names on a line and has nothing to attach one to.
|
|
|
|
|
using Row = std::pair<std::string, std::string>; // text, file
|
|
|
|
|
auto emit = [&os, full](const std::vector<Row>& rows) {
|
|
|
|
|
size_t text_width = 0;
|
|
|
|
|
if (full) {
|
|
|
|
|
for (const auto& [text, file] : rows) {
|
|
|
|
|
if (!file.empty()) text_width = std::max(text_width, text.size());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (const auto& [text, file] : rows) {
|
|
|
|
|
if (full && !file.empty()) {
|
|
|
|
|
os << std::left << std::setw(text_width) << text << " " << file << "\n";
|
|
|
|
|
} else {
|
|
|
|
|
os << trim_right(text) << "\n";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ---- what works, first --------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// The reporting categories come before the problems because a terminal is
|
|
|
|
|
// read from the BOTTOM: an eighty-klammer listing scrolls a three-line
|
|
|
|
|
// warning off the screen entirely, so the actionable part has to be last,
|
|
|
|
|
// where "| tail" finds it.
|
|
|
|
|
//
|
|
|
|
|
// The two halves of the report hide an empty category for different
|
|
|
|
|
// reasons, so they are two functions rather than one with a flag.
|
|
|
|
|
using Row_of = std::function<std::string(const Klammer_coverage&)>;
|
|
|
|
|
// The explanation under a heading is for a reader learning the categories,
|
|
|
|
|
// so it appears only under "all" -- the same argument that shows the empty
|
|
|
|
|
// ones. A default report is headings and rows.
|
|
|
|
|
auto write = [&](const std::string& title,
|
|
|
|
|
const std::vector<const Klammer_coverage*>& group,
|
|
|
|
|
const std::string& explanation, const Row_of& row) {
|
|
|
|
|
section(os, title, group.size(), full ? explanation : "");
|
|
|
|
|
std::vector<Row> rows {};
|
|
|
|
|
for (const auto* kc : group) {
|
|
|
|
|
rows.push_back({row(*kc), join(kc->m_files, ", ")});
|
|
|
|
|
}
|
|
|
|
|
emit(rows);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// A REPORTING category describes the shape of the klammer set, so an
|
|
|
|
|
// empty one still says something ("nothing here uses .*") and "all" shows
|
|
|
|
|
// it as a designer's checklist.
|
|
|
|
|
auto reporting = [&](const std::string& title,
|
|
|
|
|
const std::vector<const Klammer_coverage*>& group,
|
|
|
|
|
const std::string& explanation, const Row_of& row) {
|
|
|
|
|
if (group.empty() && !full) return;
|
|
|
|
|
write(title, group, explanation, row);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// A PROBLEM category is different: it sits under a banner that says
|
|
|
|
|
// "Needs attention", and an empty one does not. Printing "Covers no
|
|
|
|
|
// target (0)" there states the opposite of the heading above it, so it is
|
|
|
|
|
// hidden whether or not "all" was given -- "all" is for information that
|
|
|
|
|
// is missing, and a category with nothing in it is not missing anything.
|
|
|
|
|
auto problem = [&](const std::string& title,
|
|
|
|
|
const std::vector<const Klammer_coverage*>& group,
|
|
|
|
|
const std::string& explanation, const Row_of& row) {
|
|
|
|
|
if (group.empty()) return;
|
|
|
|
|
write(title, group, explanation, row);
|
|
|
|
|
};
|
|
|
|
|
auto with_targets = [&](const Klammer_coverage& kc) {
|
|
|
|
|
return name_of(kc) + " " + list_of(kc.m_targets);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
reporting("Defined per target", of_kind(coverage_t::declared), "", with_targets);
|
|
|
|
|
|
|
|
|
|
reporting("All targets, declared", of_kind(coverage_t::all_declared),
|
|
|
|
|
" Written \".*\": the author states that these work for every target,\n"
|
|
|
|
|
" including targets that do not exist yet.\n", with_targets);
|
|
|
|
|
|
|
|
|
|
// Many names on one line, so no file column: there is nothing for a file
|
|
|
|
|
// to attach to.
|
|
|
|
|
auto all = of_kind(coverage_t::all);
|
|
|
|
|
if (!all.empty() || full) {
|
|
|
|
|
section(os, "All targets, derived", all.size(),
|
|
|
|
|
full ? " No target suffix and a body of plain text, so nothing in them is\n"
|
|
|
|
|
" target-specific. This is the writer's macro form -- a repeated\n"
|
|
|
|
|
" phrase, not a klammer set -- and needs no declaration.\n" : "");
|
|
|
|
|
strings_t names {};
|
|
|
|
|
for (const auto* kc : all) {
|
|
|
|
|
names.push_back("@" + kc->m_name);
|
|
|
|
|
}
|
|
|
|
|
if (!names.empty()) {
|
|
|
|
|
os << " " << join(names, " ") << "\n";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A derived klammer covering NOTHING is unusable, so it is reported with
|
|
|
|
|
// the problems rather than here.
|
|
|
|
|
std::vector<const Klammer_coverage*> derived {};
|
|
|
|
|
std::vector<const Klammer_coverage*> uncoverable {};
|
|
|
|
|
for (const auto* kc : of_kind(coverage_t::derived)) {
|
|
|
|
|
(kc->m_targets.empty() ? uncoverable : derived).push_back(kc);
|
|
|
|
|
}
|
|
|
|
|
reporting("Derived from the klammers the body calls", derived,
|
|
|
|
|
" No target suffix and a body of klammer calls, so the coverage is the\n"
|
|
|
|
|
" intersection of what those klammers cover.\n",
|
|
|
|
|
[&](const Klammer_coverage& kc) {
|
|
|
|
|
std::stringstream ss {};
|
|
|
|
|
ss << name_of(kc) << " " << std::left << std::setw(22) << list_of(kc.m_targets)
|
|
|
|
|
<< " from " << join(kc.m_from, " ");
|
|
|
|
|
return ss.str();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---- then what needs doing ----------------------------------------
|
|
|
|
|
//
|
|
|
|
|
// Four categories, ordered by severity: the first two mean the klammer
|
|
|
|
|
// cannot be used at all, the third that its coverage is a guess, the
|
|
|
|
|
// fourth that it is undocumented. Counted by DISTINCT klammer -- "no .k"
|
|
|
|
|
// is orthogonal to the others, so a klammer can be in two categories and
|
|
|
|
|
// summing the counts would overstate the work.
|
|
|
|
|
auto undecidable = of_kind(coverage_t::undecidable);
|
|
|
|
|
auto none = of_kind(coverage_t::none);
|
|
|
|
|
std::vector<const Klammer_coverage*> undescribed {};
|
|
|
|
|
for (const auto& kc : coverage) {
|
|
|
|
|
if (!kc.m_declared) undescribed.push_back(&kc);
|
|
|
|
|
}
|
|
|
|
|
target_set needing {};
|
|
|
|
|
for (const auto* group : { &none, &uncoverable, &undecidable, &undescribed }) {
|
|
|
|
|
for (const auto* kc : *group) needing.insert(kc->m_name);
|
|
|
|
|
}
|
|
|
|
|
// Nothing to attend to, nothing said -- the same rule as the categories
|
|
|
|
|
// below it. "Needs attention: 0" under "all" was the banner contradicting
|
|
|
|
|
// itself, exactly as an empty category under it would.
|
|
|
|
|
if (!needing.empty()) {
|
|
|
|
|
os << "\nNeeds attention: " << needing.size() << " "
|
|
|
|
|
<< plural("klammer", static_cast<int>(needing.size())) << "\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
problem("Declared but never defined", none,
|
|
|
|
|
" A \".k\" declaration with no definition for any target, so the klammer\n"
|
|
|
|
|
" can never be applied.\n",
|
|
|
|
|
[&](const Klammer_coverage& kc) { return name_of(kc); });
|
|
|
|
|
problem("Covers no target", uncoverable,
|
|
|
|
|
" The klammers this one calls have no target in common, so the\n"
|
|
|
|
|
" intersection is empty and it can never be applied. The klammers\n"
|
|
|
|
|
" named are the ones to look at.\n",
|
|
|
|
|
[&](const Klammer_coverage& kc) {
|
|
|
|
|
return name_of(kc) + " from " + join(kc.m_from, " ");
|
|
|
|
|
});
|
|
|
|
|
problem("Must be declared", undecidable,
|
|
|
|
|
" A general definition whose body the engine cannot interpret, so it is\n"
|
|
|
|
|
" offered to EVERY target whether or not its code answers for that\n"
|
|
|
|
|
" target. Name the targets it does answer for -- @@name.html,tex :: --\n"
|
|
|
|
|
" or, if it works for any target at all, @@name.* ::\n",
|
|
|
|
|
[&](const Klammer_coverage& kc) { return name_of(kc) + " " + kc.m_reason; });
|
|
|
|
|
problem("No \".k\" declaration", undescribed,
|
|
|
|
|
" These render, but nothing describes them: a klammer without a \".k\"\n"
|
|
|
|
|
" has no description, so kdesc can say nothing about what it does and\n"
|
|
|
|
|
" \"kdesc -k <text>\" can only find it by name.\n", with_targets);
|
|
|
|
|
|
|
|
|
|
// The progress number. "Undecided" is the accurate label, and the one
|
|
|
|
|
// that asserts no more than was measured: a klammer that does not cover a
|
|
|
|
|
// target has not been declared unavailable there -- no notation for that
|
|
|
|
|
// exists yet -- it simply has no definition. "Unsupported" or "excluded"
|
|
|
|
|
// would each claim a decision nobody made.
|
|
|
|
|
os << "\nBy target\n";
|
|
|
|
|
for (const auto& target : targets) {
|
|
|
|
|
size_t covered = 0;
|
|
|
|
|
size_t offered = 0;
|
|
|
|
|
for (const auto& kc : coverage) {
|
|
|
|
|
if (is_in(target, kc.m_targets)) covered++;
|
|
|
|
|
if (is_in(target, kc.m_effective)) offered++;
|
|
|
|
|
}
|
|
|
|
|
os << " " << std::left << std::setw(10) << target
|
|
|
|
|
<< std::right << std::setw(4) << covered << " covered"
|
|
|
|
|
<< std::setw(6) << (coverage.size() - covered) << " undecided";
|
|
|
|
|
if (offered > covered) {
|
|
|
|
|
os << " (" << offered - covered
|
|
|
|
|
<< " more currently offered by an underivable general body)";
|
|
|
|
|
}
|
|
|
|
|
os << "\n";
|
|
|
|
|
}
|
|
|
|
|
}
|