#include #include #include #include #include #include "coverage.h" #include "machine.h" #include "katom.h" #include "util.h" namespace { using target_set = std::set; // 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 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 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 std::vector klammer_coverage(const Machine& machine, const strings_t& defined_outside) { 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 written {}; std::map 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 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; } } } // 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. std::vector result {}; for (const auto& [name, klammer] : machine.m_klammers.m_klammers) { if (!defined_outside.empty() && !klammer.defined_outside(defined_outside)) { continue; } 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& 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& coverage, bool full, std::ostream& os) { strings_t targets = machine.m_targets.user_defined(); os << "Klammer coverage\n" << "================\n\n" << coverage.size() << " " << plural("klammer", static_cast(coverage.size())) << ", " << targets.size() << " " << plural("target", static_cast(targets.size())) << ": " << join(targets, " ") << "\n"; auto of_kind = [&coverage](coverage_t kind) { std::vector 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; // text, file auto emit = [&os, full](const std::vector& 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; // 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& group, const std::string& explanation, const Row_of& row) { section(os, title, group.size(), full ? explanation : ""); std::vector 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& 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& 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 derived {}; std::vector 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 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(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 \" 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"; } }