diff --git a/README.md b/README.md index 471bdea..1224e29 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ are regenerated on each release — patches cannot be merged directly. Report problems (or send patches) to the author; accepted changes are applied to the development tree and appear in a following snapshot. -This snapshot was assembled from development commit `c59774b8252d`. +This snapshot was assembled from development commit `931ea5490a5d`. ## License diff --git a/com/kdesc.cpp b/com/kdesc.cpp index bba00f6..639cb24 100644 --- a/com/kdesc.cpp +++ b/com/kdesc.cpp @@ -43,6 +43,12 @@ static void coverage_usage() " --coverage all Also name the file each klammer is written\n" " in, and show every \"needs attention\"\n" " category, including the empty ones\n" + " --coverage \n" + " One target's view: the klammers that cover it,\n" + " those that do so with an EMPTY definition (a\n" + " decision: no meaning there), and the undecided\n" + " ones (no definition: nothing decided). \"all\"\n" + " may be added for the explanations.\n" " --coverage help This description\n" "\n" "Coverage is a fact about a klammer: the targets it can render to. It is\n" @@ -336,21 +342,26 @@ int main(int argc, char* argv[]) // --coverage is validated here, before a klammer set is read, so a // mistyped word or "help" is answered at once. The report itself // needs the loaded registry, so it runs at the end. + // The words are "help", "all", and at most one target name, which + // can only be checked once the klammersets are loaded (below). bool coverage_all = false; + std::string coverage_target {}; if (args.given("coverage")) { strings_t words = option_words(args, "coverage"); - std::string verb = words.empty() ? "" : words[0]; - if (verb == "help") { - coverage_usage(); - return 0; - } - if (verb == "all") { - coverage_all = true; - } else if (!verb.empty()) { - std::cout << "Unrecognized coverage command: --coverage " - << join(words, " ") << "\n\n"; - coverage_usage(); - return 1; + for (const std::string& word : words) { + if (word == "help") { + coverage_usage(); + return 0; + } else if (word == "all") { + coverage_all = true; + } else if (coverage_target.empty()) { + coverage_target = word; + } else { + std::cout << "Unrecognized coverage command: --coverage " + << join(words, " ") << "\n\n"; + coverage_usage(); + return 1; + } } } @@ -400,7 +411,16 @@ int main(int argc, char* argv[]) // Analysis only: klammer_coverage() reads the registry and modifies // nothing, so what a document renders to is unaffected by asking. if (args.given("coverage")) { - report_coverage(M, klammer_coverage(M, own_klammers_only), coverage_all, std::cout); + if (!coverage_target.empty()) { + if (!is_in(coverage_target, M.m_targets.user_defined())) { + std::cout << "No target \"" << coverage_target << "\" is defined. Enter \"kdesc -t\" to see the\n" + << "targets the loaded klammersets define, or \"--coverage help\".\n"; + return 1; + } + report_target_coverage(klammer_coverage(M, own_klammers_only), coverage_target, coverage_all, std::cout); + } else { + report_coverage(M, klammer_coverage(M, own_klammers_only), coverage_all, std::cout); + } } } catch (Error& e) { diff --git a/mac/coverage.cpp b/mac/coverage.cpp index 3b29ae1..f282a19 100644 --- a/mac/coverage.cpp +++ b/mac/coverage.cpp @@ -233,6 +233,23 @@ std::vector klammer_coverage(const Machine& machine, if (!is_in(file, kc.m_files)) { kc.m_files.push_back(file); } + // An empty body (nothing, or whitespace only) is a definition + // that yields no text: the designer's statement that the + // klammer has no meaning in that target. Recorded so a + // target's report can tell it from a gap. + if (def.target != Target_registry::declare_name && + def.target != Target_registry::general_name && + def.target != Target_registry::optionset_name) { + auto body = klammer.m_body.find(def.target); + bool empty = body == klammer.m_body.end(); + if (!empty) { + empty = std::all_of(body->second.begin(), body->second.end(), + [](const Katom& k) { return k.is_whitespace(); }); + } + if (empty && !is_in(def.target, kc.m_empty)) { + kc.m_empty.push_back(def.target); + } + } } for (const auto& [target, loc] : klammer.m_defloc) { if (target != Target_registry::declare_name && @@ -509,3 +526,61 @@ void report_coverage(const Machine& machine, os << "\n"; } } + +// --------------------------------------------------------------------------- +// One target's view (kdesc --coverage ), 2026-09-26. Three lists, and +// the middle one is the point: an EMPTY definition is a decision, not a gap. +// One name per line, alphabetical, as the general report's rows are (Andy: +// consistency, and no better order exists). + +namespace { + +void names_listed(std::ostream& os, std::vector names) +{ + std::sort(names.begin(), names.end()); + for (const auto& n : names) { + os << " @" << n << "\n"; + } +} + +} // namespace + +void report_target_coverage(const std::vector& coverage, + const std::string& target, bool full, std::ostream& os) +{ + std::vector covered {}, empty {}, undecided {}; + for (const auto& kc : coverage) { + if (is_in(target, kc.m_empty)) { + empty.push_back(kc.m_name); + } else if (is_in(target, kc.m_targets)) { + covered.push_back(kc.m_name); + } else { + undecided.push_back(kc.m_name); + } + } + os << "Klammer coverage of target \"" << target << "\"\n" + << std::string(29 + target.size(), '=') << "\n\n" + << coverage.size() << " " << plural("klammer", static_cast(coverage.size())) + << ": " << covered.size() + empty.size() << " covered"; + if (!empty.empty()) { + os << " (" << empty.size() << " by an empty definition)"; + } + os << ", " << undecided.size() << " undecided\n"; + + section(os, "Covered", covered.size(), + full ? " A definition for this target, or a general one that reaches it.\n" : ""); + names_listed(os, covered); + if (!empty.empty() || full) { + section(os, "Covered by an empty definition", empty.size(), + full ? " Defined for this target with a body of nothing, so applying the\n" + " klammer yields no text: the designer's decision that it has no\n" + " meaning here (a presentation hint with no analogue, say). A\n" + " decision, not a gap.\n" : ""); + names_listed(os, empty); + } + section(os, "Undecided", undecided.size(), + full ? " No definition for this target. Nothing has been decided either\n" + " way: this is what a designer of the target has not written yet,\n" + " not what the target excludes.\n" : ""); + names_listed(os, undecided); +} diff --git a/mac/coverage.h b/mac/coverage.h index d4decf2..ca13222 100644 --- a/mac/coverage.h +++ b/mac/coverage.h @@ -89,6 +89,7 @@ struct Klammer_coverage // definitions are spread over several files lists them all. Shown by // "--coverage -v", and only on a row that names ONE klammer. std::vector m_files {}; + std::vector m_empty {}; // targets whose written definition has an empty body }; // Compute the coverage of every klammer the machine has loaded. Analysis @@ -116,3 +117,8 @@ std::vector klammer_coverage(const Machine& machine, void report_coverage(const Machine& machine, const std::vector& coverage, bool full, std::ostream& os); +// One target's view: which klammers cover it, which do so with an empty +// definition (a decision that the klammer has no meaning there), and which +// are undecided (no definition -- nothing decided either way). +void report_target_coverage(const std::vector& coverage, + const std::string& target, bool full, std::ostream& os); diff --git a/tst/coverage/target_view.k b/tst/coverage/target_view.k new file mode 100644 index 0000000..5606829 --- /dev/null +++ b/tst/coverage/target_view.k @@ -0,0 +1,17 @@ +# Fixture for kdesc_test.sh: one target's view (--coverage ). +# Engine tier: it declares its own targets, so the suite runs with no +# klammer set. +@@@target ta | Target A @@@ +@@@target tb | Target B @@@ + +# Covers ta with a body. +@@full.k s : defined for ta @@ +@@full.ta :: A*s* @@ + +# Covers ta with an EMPTY body: a decision that it has no meaning in ta. +@@nothing.k s : defined for ta by an empty body @@ +@@nothing.ta :: @@ + +# Defined for tb only: undecided in ta. +@@elsewhere.k s : defined for tb @@ +@@elsewhere.tb :: B*s* @@ diff --git a/tst/kdesc_test.sh b/tst/kdesc_test.sh index 0662c6a..c36ea88 100755 --- a/tst/kdesc_test.sh +++ b/tst/kdesc_test.sh @@ -163,7 +163,26 @@ echo echo "-- the subcommand words --" shows "24. --coverage help explains" 'Coverage commands' "$KDESC" --coverage help shows "25. --katoms help explains" 'Katom commands' "$KDESC" --katoms help -shows "26. an unknown coverage word" 'Unrecognized coverage command' "$KDESC" --coverage nonsense +# Since 2026-09-26 a word after --coverage is a TARGET name, checked once the +# klammersets are loaded, so an unknown word is answered as a missing target. +shows "26. an unknown coverage word" 'No target "nonsense" is defined' "$KDESC" --coverage nonsense + +# --coverage (2026-09-26): one target's view -- covered, covered by +# an EMPTY definition (a decision), undecided (no definition, nothing decided). +shows "26a. --coverage is that target's view" \ + 'Klammer coverage of target "ta"' "$KDESC" --klammersets none -i "$DIR/target_view.k" --coverage ta +shows "26b. ... counting the empty definition apart" \ + '3 klammers: 2 covered \(1 by an empty definition\), 1 undecided' "$KDESC" --klammersets none -i "$DIR/target_view.k" --coverage ta +shows "26c. ... the empty one under its own heading" \ + '^ @nothing$' "$KDESC" --klammersets none -i "$DIR/target_view.k" --coverage ta +shows "26d. ... the undefined one as undecided" \ + '^ @elsewhere$' "$KDESC" --klammersets none -i "$DIR/target_view.k" --coverage ta +shows "26e. ... \"all\" adds the explanation" \ + 'decision, not a gap' "$KDESC" --klammersets none -i "$DIR/target_view.k" --coverage ta all +shows "26f. a target that is not defined is named" \ + 'No target "tz" is defined' "$KDESC" --klammersets none -i "$DIR/target_view.k" --coverage tz +shows "26g. two target words are one too many" \ + 'Unrecognized coverage command' "$KDESC" --klammersets none -i "$DIR/target_view.k" --coverage ta tb shows "27. an unknown katom word" 'Unrecognized katom command' "$KDESC" --katoms nonsense # The help text and the error must name the flag by its CURRENT spelling: # both still said --font for three weeks after the 2026-08-25 rename, and # retired-ok