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)
This commit is contained in:
2026-08-12 17:20:23 +02:00
parent 61987c8b1f
commit 59c1599bc9
49 changed files with 1960 additions and 431 deletions

View File

@@ -10,34 +10,82 @@
using namespace std::literals;
std::regex Klammer::name_re = std::regex(R"((\w+)(?:\.(\w+))?)");
std::regex Klammer::name_re = std::regex(R"((\w+)(?:\.((?:\w+|\*)(?:,(?:\w+|\*))*))?)");
std::tuple<std::string, std::string>
std::tuple<std::string, strings_t, bool>
parse_name(const Target_registry& targets, const Katom& name_katom)
{
std::string name_with_target = trim_char(name_katom.m_text, '@');
std::smatch match {};
if (!std::regex_match(name_with_target, match, Klammer::name_re)) {
throw Parsing_error(
"The klammer name \"" + name_with_target + "\" is not correctly defined. "
"The form is \"<klammer-name>\" for general klammers or \"<klammer-name>.<target-name>\" "
"for a specialized target. The klammer defined as \"<klammer-name>.k\" specifies the "
"for a specialized target. Several targets that share one body are written as a "
"comma-separated list: \"<klammer-name>.<target-name>,<target-name>\". The klammer "
"defined as \"<klammer-name>.k\" specifies the "
"arguments and contains a description of the klammer in the definition body.",
name_katom.m_loc);
}
std::string klammer_name = match[1];
std::string target_name = match[2];
if (target_name.empty()) {
target_name = Target_registry::general_name;
std::string target_part = match[2];
if (target_part.empty()) {
// No suffix at all: the general target, but not a STATEMENT about
// coverage. This is the writer's macro form -- someone defining a
// repeated phrase is not building a klammer set -- so the third
// result is false and the coverage analysis treats the body on its
// merits rather than as an assertion.
return { klammer_name, { Target_registry::general_name }, false };
}
if (!targets.has(target_name)) {
strings_t target_names = regex_split(target_part, std::regex(","));
strings_t seen {};
for (const auto& target_name : target_names) {
if (!targets.has(target_name)) {
throw Target_error(
"The target \"" + target_name + "\" in klammer definition \"" + name_with_target + "\" "
"is not defined. Enter \"kdesc -t\" to see the targets defined by the Standard Klammer Set.",
name_katom.m_loc);
}
if (is_in(target_name, seen)) {
throw Target_error(
"The target \"" + target_name + "\" is named more than once in klammer definition \"" +
name_with_target + "\".",
name_katom.m_loc);
}
seen.push_back(target_name);
}
// A ".k" declaration states ONE interface for every target, and a ".o"
// declares an option set; neither produces output, so neither has any
// meaning as a member of a list of output targets.
if (target_names.size() > 1) {
for (const auto& reserved :
{ Target_registry::declare_name, Target_registry::optionset_name }) {
if (is_in(reserved, target_names)) {
throw Target_error(
"The klammer definition \"" + name_with_target + "\" names \"" + reserved +
"\" in a list of targets. A \"." + reserved + "\" definition declares an "
"interface for all targets rather than producing output for one, so it must "
"be written on its own.",
name_katom.m_loc);
}
}
}
// "*" written out is an ASSERTION: this klammer works for every target,
// including targets that do not exist yet. A list of "all the targets
// defined today" cannot say that, and the difference matters the moment a
// new target is added. It may not appear IN a list -- "all targets and
// also html" is either redundant or a misunderstanding.
bool general_declared = is_in(Target_registry::general_name, target_names);
if (general_declared && target_names.size() > 1) {
throw Target_error(
"The target \"" + target_name + "\" in klammer definition \"" + name_with_target + "\" "
"is not defined. Enter \"kdesc -t\" to see the targets defined by the Standard Klammer Set.",
"The klammer definition \"" + name_with_target + "\" names \"" +
Target_registry::general_name + "\" in a list of targets. \"" +
Target_registry::general_name + "\" already means every target, so it "
"must be written on its own.",
name_katom.m_loc);
}
return { klammer_name, target_name };
return { klammer_name, target_names, general_declared };
}
std::tuple<Katom, Parameter_set, katom_list, Locator>
@@ -146,12 +194,50 @@ void Klammer::remove_target_definition(const std::string& target_name)
// Rationalize multiple definitions
// ONE definition in the source can register more than once. A target that
// "provides" another registers both (the SKS's pdf includes tex, so
// "@@fraktur.tex : ..." becomes a tex definition and a pdf one), and so does
// every member of a comma-separated target list. Counting or listing those
// registrations reports work the author did not do: "2 definitions" for a
// single line, followed by that same line printed twice -- which sends the
// reader hunting for a second definition that does not exist.
//
// These two report what was WRITTEN. Registrations are grouped by source
// location, and a location that produced several targets names them, so the
// count and the listing agree with the file.
using location_group = std::pair<std::string, strings_t>;
std::vector<location_group> group_by_location(const auto& components)
{
std::vector<location_group> groups {};
for (const auto& c : components) {
std::string loc = c.loc.desc();
auto it = std::find_if(groups.begin(), groups.end(),
[&loc](const location_group& g) { return g.first == loc; });
if (it == groups.end()) {
groups.push_back({loc, {c.target}});
} else {
it->second.push_back(c.target);
}
}
return groups;
}
int written_count(const auto& components)
{
return static_cast<int>(group_by_location(components).size());
}
std::string error_list(const std::string& label, const auto& components, const std::string& after="")
{
std::stringstream ss {};
ss << label << ":\n";
for (const auto& c : components) {
ss << " " << c.loc.desc() << "\n";
for (const auto& [loc, targets] : group_by_location(components)) {
ss << " " << loc;
if (targets.size() > 1) {
ss << " (targets " << join(targets, ", ") << ")";
}
ss << "\n";
}
ss << after;
return ss.str();
@@ -182,7 +268,7 @@ void Klammer::disallow_instances() //Klammer::components declaration)
{
auto instances = instance_defs();
if (!instances.empty()) {
int icount = instances.size();
int icount = written_count(instances);
std::stringstream ss {};
ss << "There " << to_be(icount) << " " << icount << " "
<< plural("instance", icount) << " (defined by \"::\"), but "
@@ -201,9 +287,9 @@ bool Klammer::copy_to_instances(const Target_registry& targets)
[] (const auto& def) {
return def.deftype != katom_t::klammer_instance
&& def.deftype != katom_t::klammer_override; });
int dcount = definitions.size();
int dcount = written_count(definitions);
if (dcount != 1) {
int icount = instances.size();
int icount = written_count(instances);
std::stringstream ss {};
ss << "There " << to_be(icount) << " " << icount << " "
<< plural("instance", icount) << " (defined by \"::\"), but "
@@ -268,11 +354,13 @@ void Klammer::check_for_declaration_and_definitions()
}
}
if (!definitions.empty()) {
auto defsize = definitions.size();
std::string desc = defsize == 1 ? "a definition" :
std::to_string(defsize) + " definitions";
int defsize = written_count(definitions);
std::string desc = defsize == 1 ? "a definition that declares its own parameters"
: std::to_string(defsize) + " definitions that declare their own parameters";
throw Definition_error(
error_list("A klammer has both a declaration (.k) as well as " + desc + "\n(instances are defined by \"::\")",
error_list("A klammer has both a \".k\" declaration and " + desc +
".\nWrite \"::\" instead of \":\" so the definition takes its parameters "
"from the declaration",
definitions),
declares[0].loc, false);
}