De-dup verify passes: drive analyze screen + dashboard from app::verify.

The analyze Issues pane and the dashboard Health rows each recomputed the
same verify passes inline (pin-role mismatches, Power/Gnd net-mix, NC orphan
rollup, model-driven checks) — the third and second copies of what the verify
command also did. Route both screens through app::verify(System*) instead, so
the passes live in exactly one place.

Enrich VerifyReport with a per-pin OrphanPin detail list (module/part/pin +
dropped flag) so the dashboard can still nest its dropped-singleton breakdown
under the NC health line without re-walking modules/parts/pins. Output is
unchanged in both screens (same label formats, same numbers).

Prune the now-dead includes (nets/bsdl_check/connect/parts/pins as applicable,
<unordered_set>) from both screens. Extend tests/test_verify.cpp to cover the
new orphans detail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 20:04:31 +02:00
parent e3350b8d95
commit 25939998ab
5 changed files with 82 additions and 134 deletions

View File

@@ -80,10 +80,17 @@ VerifyReport verify(System *sys)
Pin *pin = nkv.second;
if (pin->signal() || bridged_pins.count(pin))
continue;
if (pin->nc_origin == NcOrigin::ImportedUnconnected)
bool dropped;
if (pin->nc_origin == NcOrigin::ImportedUnconnected) {
++r.orphan_imported;
else if (pin->nc_origin == NcOrigin::DroppedSingleton)
dropped = false;
} else if (pin->nc_origin == NcOrigin::DroppedSingleton) {
++r.orphan_dropped;
dropped = true;
} else {
continue;
}
r.orphans.push_back({mkv.first, pkv.first, nkv.first, dropped});
}
// Passes 4-7 — model-driven checks (reuse the nets from pass 2).

View File

@@ -26,6 +26,13 @@ struct NetInconsistency {
std::vector<Member> members;
};
// One orphan pin: no signal and not bridged via a connection. `dropped` is true
// for a dropped singleton (essim detached it), false for an import-time NC.
struct OrphanPin {
std::string module, part, pin;
bool dropped = false;
};
// The full result of `verify`: structured data only — no strings beyond the
// names, no formatting. Frontends (the verify command, the analyze screen, the
// dashboard) render it however they like.
@@ -39,6 +46,7 @@ struct VerifyReport {
int orphan_imported = 0;
int orphan_dropped = 0;
std::vector<OrphanPin> orphans; ///< per-pin detail (both origins)
std::vector<Anomaly> pin_anomalies; ///< check_pin_specs
std::vector<Anomaly> jtag_anomalies; ///< check_jtag_chain

View File

@@ -1,13 +1,9 @@
#include "frontends/tui/tui.hpp"
#include "frontends/tui/tui_helpers.hpp"
#include "core/app/verify.hpp"
#include "core/domain/analysis.hpp"
#include "core/domain/bsdl_check.hpp"
#include "core/domain/connect.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/nets.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/pins.hpp"
#include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
@@ -17,7 +13,6 @@
#include <algorithm>
#include <array>
#include <unordered_set>
using namespace ftxui;
@@ -57,41 +52,23 @@ Component Tui::BuildAnalyzeScreen() {
// connection), then structural anomalies from the analysis pass.
analyze_issues.clear();
int n_role_mismatches = 0, n_typed_pins = 0;
for (auto &mkv : *sys->modules())
for (auto &pkv : *mkv.second) {
Part *prt = pkv.second;
if (prt->connector_type.empty()) continue;
for (auto &nkv : *prt) {
Pin *pin = nkv.second;
++n_typed_pins;
SignalType expected = pin->expected_signal_type();
if (expected == SignalType::Other) continue;
Signal *s = pin->signal();
SignalType actual = s ? s->type : SignalType::Other;
if (actual == expected) continue;
++n_role_mismatches;
std::string sig_label = s ? s->name : std::string("(NC)");
analyze_issues.push_back(
"[pin-role] " + mkv.first + "/" + prt->name + "/"
+ pin->name + ": expected " + signal_type_name(expected)
+ ", got " + signal_type_name(actual)
+ " (signal: " + sig_label + ")");
}
}
// verify + structural anomalies. The verify passes (pin-role, net-mix,
// orphans, model checks) come from the shared core op; the structural
// anomalies (diff-pair/bus) come from analyze_system above.
app::VerifyReport vr = app::verify(sys.get());
auto nets = compute_all_nets(sys.get());
int n_bridged = 0, n_inconsistent = 0;
for (const auto &n : nets) {
if (n.members.size() < 2) continue;
++n_bridged;
SignalType dom;
if (net_type_consistent(n, dom)) continue;
++n_inconsistent;
for (const auto &m : vr.role_mismatches)
analyze_issues.push_back(
"[pin-role] " + m.module + "/" + m.part + "/" + m.pin
+ ": expected " + signal_type_name(m.expected)
+ ", got " + signal_type_name(m.actual)
+ " (signal: " + m.signal + ")");
for (const auto &ni : vr.net_inconsistencies) {
std::string line = "[net-mix] mixes Power and Gnd:";
for (const auto &mp : n.members)
line += " " + mp.first->name + "/" + mp.second->name
+ "(" + signal_type_name(mp.second->type) + ")";
for (const auto &mem : ni.members)
line += " " + mem.module + "/" + mem.signal
+ "(" + signal_type_name(mem.type) + ")";
analyze_issues.push_back(std::move(line));
}
@@ -100,28 +77,25 @@ Component Tui::BuildAnalyzeScreen() {
+ anomaly_kind_name(a.kind) + "] "
+ a.message);
// Model-driven checks (same as `verify`), reusing the nets above.
std::vector<Anomaly> model_anoms;
{
auto a1 = check_pin_specs(sys.get(), &nets);
auto a2 = check_jtag_chain(sys.get(), &nets);
auto a3 = check_source_conflicts(sys.get());
auto a4 = check_bsdl_completeness(sys.get());
model_anoms.insert(model_anoms.end(), a1.begin(), a1.end());
model_anoms.insert(model_anoms.end(), a2.begin(), a2.end());
model_anoms.insert(model_anoms.end(), a3.begin(), a3.end());
model_anoms.insert(model_anoms.end(), a4.begin(), a4.end());
}
for (const auto &a : model_anoms)
analyze_issues.push_back(std::string("[")
+ anomaly_kind_name(a.kind) + "] "
+ a.message);
int n_model = (int)model_anoms.size();
// Model-driven checks (pin / JTAG / source-conflict / completeness).
auto push_anoms = [this](const std::vector<Anomaly> &v) {
for (const auto &a : v)
analyze_issues.push_back(std::string("[")
+ anomaly_kind_name(a.kind) + "] "
+ a.message);
};
push_anoms(vr.pin_anomalies);
push_anoms(vr.jtag_anomalies);
push_anoms(vr.conflict_anomalies);
push_anoms(vr.completeness_anomalies);
int n_model = vr.model_total();
if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)");
if (analyze_issue_idx >= (int)analyze_issues.size())
analyze_issue_idx = (int)analyze_issues.size() - 1;
int n_role_mismatches = (int)vr.role_mismatches.size();
int n_inconsistent = (int)vr.net_inconsistencies.size();
std::string issues_header = "Issues ("
+ std::to_string(n_role_mismatches + n_inconsistent
+ (int)rep.anomalies.size() + n_model)
@@ -215,26 +189,11 @@ Component Tui::BuildAnalyzeScreen() {
std::string(tag) + r.mod + "/" + r.sig + "" + reason);
}
// NC orphan rollup — same filter as the verify pass.
std::unordered_set<Pin *> bridged_pins;
for (auto &ckv : *sys->connections())
for (auto &wp : ckv.second->pin_map) {
if (wp.first) bridged_pins.insert(wp.first);
if (wp.second) bridged_pins.insert(wp.second);
}
int orph_imported = 0, orph_dropped = 0;
for (auto &mkv : *sys->modules())
for (auto &pkv : *mkv.second)
for (auto &nkv : *pkv.second) {
Pin *pin = nkv.second;
if (pin->signal() || bridged_pins.count(pin)) continue;
if (pin->nc_origin == NcOrigin::ImportedUnconnected) ++orph_imported;
else if (pin->nc_origin == NcOrigin::DroppedSingleton) ++orph_dropped;
}
// NC orphan rollup — from the shared verify report.
analyze_types.push_back(
"[NC] orphan pin(s): " + std::to_string(orph_imported + orph_dropped)
+ " (" + std::to_string(orph_imported) + " imported, "
+ std::to_string(orph_dropped) + " dropped)");
"[NC] orphan pin(s): " + std::to_string(vr.orphan_total())
+ " (" + std::to_string(vr.orphan_imported) + " imported, "
+ std::to_string(vr.orphan_dropped) + " dropped)");
if (analyze_type_idx >= (int)analyze_types.size())
analyze_type_idx = (int)analyze_types.size() - 1;

View File

@@ -1,13 +1,11 @@
#include "frontends/tui/tui.hpp"
#include "frontends/tui/tui_helpers.hpp"
#include "core/app/verify.hpp"
#include "core/domain/analysis.hpp"
#include "core/domain/bsdl_check.hpp"
#include "core/domain/connect.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/nets.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/pins.hpp"
#include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
@@ -16,7 +14,6 @@
#include <algorithm>
#include <map>
#include <unordered_set>
#include <vector>
using namespace ftxui;
@@ -77,58 +74,23 @@ Component Tui::BuildDashboardScreen() {
}
int n_conn = (int)sys->connections()->size();
// ---- verify-style health (recomputed; cheap on realistic sizes) ----
int n_role_mismatches = 0, n_typed_pins = 0;
for (auto &mkv : *sys->modules())
for (auto &pkv : *mkv.second) {
Part *prt = pkv.second;
if (prt->connector_type.empty()) continue;
for (auto &nkv : *prt) {
Pin *pin = nkv.second;
++n_typed_pins;
SignalType expected = pin->expected_signal_type();
if (expected == SignalType::Other) continue;
Signal *s = pin->signal();
SignalType actual = s ? s->type : SignalType::Other;
if (actual != expected) ++n_role_mismatches;
}
}
// ---- verify-style health (shared core op; cheap on realistic sizes) ----
app::VerifyReport vr = app::verify(sys.get());
int n_role_mismatches = (int)vr.role_mismatches.size();
int n_typed_pins = vr.typed_pins;
int n_inconsistent = (int)vr.net_inconsistencies.size();
int n_bridged = vr.bridged_nets;
int orph_imported = vr.orphan_imported;
int orph_dropped = vr.orphan_dropped;
auto nets = compute_all_nets(sys.get());
int n_bridged = 0, n_inconsistent = 0;
for (const auto &n : nets) {
if (n.members.size() < 2) continue;
++n_bridged;
SignalType dom;
if (!net_type_consistent(n, dom)) ++n_inconsistent;
}
// ---- NC orphan summary (matches verify pass 3) ----
std::unordered_set<Pin *> bridged_pins;
for (auto &ckv : *sys->connections())
for (auto &wp : ckv.second->pin_map) {
if (wp.first) bridged_pins.insert(wp.first);
if (wp.second) bridged_pins.insert(wp.second);
}
int orph_imported = 0, orph_dropped = 0;
// Per-module list of dropped-singleton pins, for the detail rows below
// the NC health line. The signal name is gone (the Signal object was
// deleted by `drop_singleton_signals`), but the pin's full path is
// enough to locate it in `explore`.
std::map<std::string, std::vector<std::string>> dropped_by_module;
for (auto &mkv : *sys->modules())
for (auto &pkv : *mkv.second)
for (auto &nkv : *pkv.second) {
Pin *pin = nkv.second;
if (pin->signal() || bridged_pins.count(pin)) continue;
if (pin->nc_origin == NcOrigin::ImportedUnconnected) {
++orph_imported;
} else if (pin->nc_origin == NcOrigin::DroppedSingleton) {
++orph_dropped;
dropped_by_module[mkv.first].push_back(
pkv.first + "/" + nkv.first);
}
}
for (const auto &o : vr.orphans)
if (o.dropped)
dropped_by_module[o.module].push_back(o.part + "/" + o.pin);
auto health_line = [](bool ok, const std::string &s) {
return hbox({
@@ -144,7 +106,7 @@ Component Tui::BuildDashboardScreen() {
+ " typed pin(s)"));
health_rows.push_back(health_line(n_inconsistent == 0,
"nets: " + std::to_string(n_inconsistent) + " inconsistent over "
+ std::to_string(n_bridged) + " bridged (" + std::to_string(nets.size())
+ std::to_string(n_bridged) + " bridged (" + std::to_string(vr.total_nets)
+ " total)"));
int orph_total = orph_imported + orph_dropped;
health_rows.push_back(health_line(orph_total == 0,
@@ -172,12 +134,9 @@ Component Tui::BuildDashboardScreen() {
}
}
// Model-driven checks (BSDL pin specs, JTAG chain, source conflicts),
// reusing the nets computed above.
int n_model = (int)(check_pin_specs(sys.get(), &nets).size()
+ check_jtag_chain(sys.get(), &nets).size()
+ check_source_conflicts(sys.get()).size()
+ check_bsdl_completeness(sys.get()).size());
// Model-driven checks (BSDL pin specs, JTAG chain, source conflicts,
// completeness) — from the shared verify report.
int n_model = vr.model_total();
health_rows.push_back(health_line(n_model == 0,
"model: " + std::to_string(n_model) + " BSDL/JTAG anomaly(ies)"));

View File

@@ -77,4 +77,19 @@ TEST_CASE("verify counts orphan pins by their import origin") {
CHECK(r.orphan_imported == 1);
CHECK(r.orphan_dropped == 1);
CHECK(r.orphan_total() == 2);
// Per-pin detail carries the path and origin (the dashboard lists the
// dropped ones under the NC health row).
REQUIRE(r.orphans.size() == 2);
int n_dropped = 0;
bool dropped_path_ok = false;
for (const auto &o : r.orphans) {
if (o.dropped) {
++n_dropped;
if (o.module == "M" && o.part == "J1" && o.pin == "2")
dropped_path_ok = true;
}
}
CHECK(n_dropped == 1);
CHECK(dropped_path_ok);
}