Verify: check diff pairs/buses crossing connections (swap, incomplete)

New pass 8 (core/domain/diff_check.{hpp,cpp}): every complete local diff
pair (X_P/X_N, name-based) resolves its legs to two bridged nets; peer
pairs on those nets must match leg for leg.
- DiffPolaritySwap: P legs meet N legs across a connection (sometimes
  intentional - reported for review), or both legs joined onto one net.
- DiffCrossIncomplete: pairs sharing only one leg, and diff-bus lanes
  crossing NOWHERE while sibling lanes cross (distributed/fan-out buses
  stay silent - validated against the real 7-card VPX system: 21 noisy
  findings down to 3 genuine dangling-lane reports, 0 false swaps).

diff_suffix/split_trailing_index/is_internal_name promoted out of
analysis.cpp's anonymous namespace for reuse. VerifyReport.diff_anomalies
wired into model_total() and all four renderers (TUI verify, script
engine, wx log, analyze Issues). 8 new test cases (466 assertions).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 20:05:43 +02:00
parent 9cf43696a2
commit 0b10e1c1b7
11 changed files with 452 additions and 26 deletions

View File

@@ -150,6 +150,7 @@ private:
grp(r.jtag_anomalies, " JTAG chain anomaly(ies)."); grp(r.jtag_anomalies, " JTAG chain anomaly(ies).");
grp(r.conflict_anomalies, " source-conflict(s)."); grp(r.conflict_anomalies, " source-conflict(s).");
grp(r.completeness_anomalies, " BSDL completeness issue(s)."); grp(r.completeness_anomalies, " BSDL completeness issue(s).");
grp(r.diff_anomalies, " diff-pair crossing anomaly(ies).");
} }
// Execute one already-trimmed line. Returns false on a hard error. // Execute one already-trimmed line. Returns false on a hard error.

View File

@@ -2,6 +2,7 @@
#include "core/domain/bsdl_check.hpp" #include "core/domain/bsdl_check.hpp"
#include "core/domain/connect.hpp" #include "core/domain/connect.hpp"
#include "core/domain/diff_check.hpp"
#include "core/domain/modules.hpp" #include "core/domain/modules.hpp"
#include "core/domain/nets.hpp" #include "core/domain/nets.hpp"
#include "core/domain/parts.hpp" #include "core/domain/parts.hpp"
@@ -93,11 +94,12 @@ VerifyReport verify(System *sys)
r.orphans.push_back({mkv.first, pkv.first, nkv.first, dropped}); r.orphans.push_back({mkv.first, pkv.first, nkv.first, dropped});
} }
// Passes 4-7 — model-driven checks (reuse the nets from pass 2). // Passes 4-8 — model-driven checks (reuse the nets from pass 2).
r.pin_anomalies = check_pin_specs(sys, &nets); r.pin_anomalies = check_pin_specs(sys, &nets);
r.jtag_anomalies = check_jtag_chain(sys, &nets); r.jtag_anomalies = check_jtag_chain(sys, &nets);
r.conflict_anomalies = check_source_conflicts(sys); r.conflict_anomalies = check_source_conflicts(sys);
r.completeness_anomalies = check_bsdl_completeness(sys); r.completeness_anomalies = check_bsdl_completeness(sys);
r.diff_anomalies = check_diff_crossings(sys, &nets);
return r; return r;
} }

View File

@@ -52,11 +52,13 @@ struct VerifyReport {
std::vector<Anomaly> jtag_anomalies; ///< check_jtag_chain std::vector<Anomaly> jtag_anomalies; ///< check_jtag_chain
std::vector<Anomaly> conflict_anomalies; ///< check_source_conflicts std::vector<Anomaly> conflict_anomalies; ///< check_source_conflicts
std::vector<Anomaly> completeness_anomalies; ///< check_bsdl_completeness std::vector<Anomaly> completeness_anomalies; ///< check_bsdl_completeness
std::vector<Anomaly> diff_anomalies; ///< check_diff_crossings
int orphan_total() const { return orphan_imported + orphan_dropped; } int orphan_total() const { return orphan_imported + orphan_dropped; }
int model_total() const { int model_total() const {
return (int)(pin_anomalies.size() + jtag_anomalies.size() return (int)(pin_anomalies.size() + jtag_anomalies.size()
+ conflict_anomalies.size() + completeness_anomalies.size()); + conflict_anomalies.size() + completeness_anomalies.size()
+ diff_anomalies.size());
} }
}; };

View File

@@ -31,15 +31,15 @@ const char *anomaly_kind_name(AnomalyKind k) {
case AnomalyKind::JtagBusUnbridged: return "jtag-bus-unbridged"; case AnomalyKind::JtagBusUnbridged: return "jtag-bus-unbridged";
case AnomalyKind::SourceConflict: return "source-conflict"; case AnomalyKind::SourceConflict: return "source-conflict";
case AnomalyKind::BsdlPinMissing: return "bsdl-pin-missing"; case AnomalyKind::BsdlPinMissing: return "bsdl-pin-missing";
case AnomalyKind::DiffPolaritySwap: return "diff-polarity-swap";
case AnomalyKind::DiffCrossIncomplete: return "diff-cross-incomplete";
} }
return "?"; return "?";
} }
namespace {
// Diff-pair suffix detection. Returns true and fills <stem, polarity> if // Diff-pair suffix detection. Returns true and fills <stem, polarity> if
// `name` ends with one of {_P, _N, _p, _n} preceded by a non-suffix char. // `name` ends with one of {_P, _N, _p, _n} preceded by a non-suffix char.
// 'P' / 'N' result is normalised to uppercase. // 'P' / 'N' result is normalised to uppercase. Shared with diff_check.cpp.
bool diff_suffix(const std::string &name, std::string &stem, char &pol) { bool diff_suffix(const std::string &name, std::string &stem, char &pol) {
if (name.size() < 3) return false; if (name.size() < 3) return false;
char last = name.back(); char last = name.back();
@@ -52,6 +52,29 @@ bool diff_suffix(const std::string &name, std::string &stem, char &pol) {
return true; return true;
} }
// Tool-internal net names we never want to surface to the user (Mentor's
// `$Nxxxx` convention, ODS placeholders, etc.). Cheap prefix check.
bool is_internal_name(const std::string &n) {
return !n.empty() && n[0] == '$';
}
// Trailing-integer split: "MDI0" → ("MDI", 0); "PCIE_TX_3" → ("PCIE_TX_", 3);
// "USB" → false (no trailing digits). Used for diff-bus aggregation only —
// the strict `_` rule from `numeric_suffix` does NOT apply here because the
// caller has already stripped a `_P` / `_N` polarity suffix, so we know the
// remaining digits are an index rather than part of a longer name.
bool split_trailing_index(const std::string &s, std::string &outer, int &idx) {
if (s.empty()) return false;
size_t i = s.size();
while (i > 0 && std::isdigit((unsigned char)s[i - 1])) --i;
if (i == s.size() || i == 0) return false;
idx = std::atoi(s.c_str() + i);
outer = s.substr(0, i);
return true;
}
namespace {
// Bus suffix detection. Two accepted forms: // Bus suffix detection. Two accepted forms:
// - bracketed: NAME[12] → stem "NAME", idx 12 // - bracketed: NAME[12] → stem "NAME", idx 12
// - underscore: NAME_12 → stem "NAME_", idx 12 (underscore is REQUIRED // - underscore: NAME_12 → stem "NAME_", idx 12 (underscore is REQUIRED
@@ -81,27 +104,6 @@ bool numeric_suffix(const std::string &name, std::string &stem, int &idx,
return true; return true;
} }
// Tool-internal net names we never want to surface to the user (Mentor's
// `$Nxxxx` convention, ODS placeholders, etc.). Cheap prefix check.
bool is_internal_name(const std::string &n) {
return !n.empty() && n[0] == '$';
}
// Trailing-integer split: "MDI0" → ("MDI", 0); "PCIE_TX_3" → ("PCIE_TX_", 3);
// "USB" → false (no trailing digits). Used for diff-bus aggregation only —
// the strict `_` rule from `numeric_suffix` does NOT apply here because the
// caller has already stripped a `_P` / `_N` polarity suffix, so we know the
// remaining digits are an index rather than part of a longer name.
bool split_trailing_index(const std::string &s, std::string &outer, int &idx) {
if (s.empty()) return false;
size_t i = s.size();
while (i > 0 && std::isdigit((unsigned char)s[i - 1])) --i;
if (i == s.size() || i == 0) return false;
idx = std::atoi(s.c_str() + i);
outer = s.substr(0, i);
return true;
}
void analyse_module(Module *mod, AnalysisReport &out) { void analyse_module(Module *mod, AnalysisReport &out) {
// ---- Pass 1: diff pairs ---- // ---- Pass 1: diff pairs ----
std::unordered_map<std::string, std::pair<Signal *, Signal *>> dp; // stem -> {P, N} std::unordered_map<std::string, std::pair<Signal *, Signal *>> dp; // stem -> {P, N}

View File

@@ -39,6 +39,8 @@ enum class AnomalyKind {
JtagBusUnbridged, ///< TMS or TCK is not common to all TAP devices. JtagBusUnbridged, ///< TMS or TCK is not common to all TAP devices.
SourceConflict, ///< A model contradicts the netlist (e.g. BSDL power pin left NC). SourceConflict, ///< A model contradicts the netlist (e.g. BSDL power pin left NC).
BsdlPinMissing, ///< A BSDL power/ground port has no pin on the netlist part. BsdlPinMissing, ///< A BSDL power/ground port has no pin on the netlist part.
DiffPolaritySwap, ///< A diff pair crosses a connection with P and N swapped.
DiffCrossIncomplete, ///< A diff pair/bus only partially crosses a connection.
}; };
struct Anomaly { struct Anomaly {
@@ -73,6 +75,15 @@ inline constexpr int POWER_FANOUT_CONFIRM_MIN = 4; ///< ≥ this confirms Powe
bool has_voltage_pattern(const std::string &name); bool has_voltage_pattern(const std::string &name);
// Name-parsing helpers shared with the diff-crossing check (diff_check.cpp).
// diff_suffix: true if `name` ends with _P/_N (case-insensitive); fills the
// stem and the polarity normalised to uppercase 'P'/'N'.
// split_trailing_index: "MDI0" → ("MDI", 0); false without trailing digits.
// is_internal_name: tool-internal net names never surfaced ($Nxxxx …).
bool diff_suffix(const std::string &name, std::string &stem, char &pol);
bool split_trailing_index(const std::string &s, std::string &outer, int &idx);
bool is_internal_name(const std::string &n);
// Best-effort signal-type inference. Sets `Signal::type`: // Best-effort signal-type inference. Sets `Signal::type`:
// - GndShield when the name unambiguously matches GND/SHIELD/CHASSIS/EARTH. // - GndShield when the name unambiguously matches GND/SHIELD/CHASSIS/EARTH.
// - Power when the name suggests Power AND there is structural evidence // - Power when the name suggests Power AND there is structural evidence

View File

@@ -0,0 +1,194 @@
#include "diff_check.hpp"
#include "modules.hpp"
#include "signals.hpp"
#include "system.hpp"
#include <algorithm>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <utility>
namespace {
// One complete local diff pair with the net ids of its two legs.
struct LocalPair {
Module *mod = nullptr;
std::string stem;
Signal *p = nullptr, *n = nullptr;
int np = -1, nn = -1;
};
std::string pair_label(const LocalPair &lp) {
return lp.mod->name + "/" + lp.stem + "_P/N";
}
} // namespace
std::vector<Anomaly> check_diff_crossings(System *sys,
const std::vector<Net> *nets)
{
std::vector<Anomaly> out;
if (!sys || !nets) return out;
// Signal → net id (compute_all_nets covers every signal, singletons too).
std::unordered_map<Signal *, int> net_of;
for (size_t i = 0; i < nets->size(); ++i)
for (const auto &mp : (*nets)[i].members)
net_of[mp.second] = (int)i;
// Complete local pairs, module by module. Orphan halves (X_P without
// X_N) are analysis's DiffPairOrphan business — skipped here.
std::vector<LocalPair> pairs;
std::unordered_map<Signal *, int> pair_of; // leg signal → index in `pairs`
for (auto &mkv : *sys->modules()) {
Module *mod = mkv.second;
std::map<std::string, LocalPair> by_stem;
for (auto &skv : *mod->signals) {
if (is_internal_name(skv.first)) continue;
std::string stem; char pol;
if (!diff_suffix(skv.first, stem, pol)) continue;
LocalPair &lp = by_stem[stem];
lp.mod = mod; lp.stem = stem;
if (pol == 'P') lp.p = skv.second;
else lp.n = skv.second;
}
for (auto &kv : by_stem) {
LocalPair lp = kv.second;
if (!lp.p || !lp.n) continue;
auto ip = net_of.find(lp.p), in = net_of.find(lp.n);
if (ip == net_of.end() || in == net_of.end()) continue;
lp.np = ip->second;
lp.nn = in->second;
int idx = (int)pairs.size();
pairs.push_back(lp);
pair_of[lp.p] = idx;
pair_of[lp.n] = idx;
}
}
// Pass 1 — pair against pair. Each unordered couple of pairs is judged
// once (dedup set), so A↔B is never also reported as B↔A.
std::set<std::pair<int, int>> seen;
for (int i = 0; i < (int)pairs.size(); ++i) {
const LocalPair &a = pairs[i];
if (a.np == a.nn) {
// Degenerate: both legs land on one net — only the connections
// can do that (two module-local signals are distinct by nature).
Anomaly an;
an.kind = AnomalyKind::DiffPolaritySwap;
an.module = a.mod;
an.message = a.mod->name + ": " + a.stem + "_P and " + a.stem
+ "_N join the same net (through the connections)";
an.involved = {a.p, a.n};
out.push_back(std::move(an));
continue;
}
// Candidate peers: pairs of OTHER modules with a leg on net np or nn.
std::set<int> cands;
for (int net : {a.np, a.nn})
for (const auto &mp : (*nets)[net].members) {
auto it = pair_of.find(mp.second);
if (it == pair_of.end()) continue;
const LocalPair &b = pairs[it->second];
if (b.mod == a.mod) continue; // intra-module: nothing to say
if (b.np == b.nn) continue; // degenerate: own anomaly above
cands.insert(it->second);
}
for (int j : cands) {
std::pair<int, int> key = std::minmax(i, j);
if (!seen.insert(key).second) continue;
const LocalPair &b = pairs[j];
if (a.np == b.np && a.nn == b.nn) continue; // straight: all good
Anomaly an;
an.module = a.mod;
an.involved = {a.p, a.n, b.p, b.n};
if (a.np == b.nn && a.nn == b.np) {
an.kind = AnomalyKind::DiffPolaritySwap;
an.message = pair_label(a) + " <-> " + pair_label(b)
+ ": polarity swapped (P legs meet N legs)";
} else {
an.kind = AnomalyKind::DiffCrossIncomplete;
std::string how;
if (a.np == b.np) how = "only the P legs are bridged";
else if (a.nn == b.nn) how = "only the N legs are bridged";
else if (a.np == b.nn) how = "P leg bridged to N leg; "
"the other legs are not";
else how = "N leg bridged to P leg; "
"the other legs are not";
an.message = pair_label(a) + " <-> " + pair_label(b)
+ ": " + how;
}
out.push_back(std::move(an));
}
}
// Pass 2 — diff buses at a crossing. Lanes grouped by outer stem. A lane
// is "dangling" when it crosses to NO module at all while sibling lanes
// do cross — distributed buses (lanes fanned out to different peers, a
// backplane classic) are legitimate and stay silent. Lanes crossing
// partially are already reported above, so they don't count as dangling.
// One aggregated anomaly per bus, per side (each side names its lanes).
std::map<std::pair<Module *, std::string>, std::map<int, int>> groups;
for (int i = 0; i < (int)pairs.size(); ++i) {
std::string outer; int idx;
if (!split_trailing_index(pairs[i].stem, outer, idx)) continue;
groups[{pairs[i].mod, outer}][idx] = i;
}
for (auto &gkv : groups) {
auto &lanes = gkv.second; // lane index → pair index
if (lanes.size() < 2) continue;
std::set<int> touching_any; // lanes sharing ≥1 net with a peer
std::set<int> complete_any; // lanes fully crossing somewhere
std::set<Module *> reached;
for (auto &lkv : lanes) {
const LocalPair &a = pairs[lkv.second];
if (a.np == a.nn) continue;
for (int net : {a.np, a.nn})
for (const auto &mp : (*nets)[net].members) {
auto it = pair_of.find(mp.second);
if (it == pair_of.end()) continue;
const LocalPair &b = pairs[it->second];
if (b.mod == a.mod) continue;
touching_any.insert(lkv.first);
bool straight = (a.np == b.np && a.nn == b.nn);
bool swapped = (a.np == b.nn && a.nn == b.np);
if (straight || swapped) {
complete_any.insert(lkv.first);
reached.insert(b.mod);
}
}
}
if (complete_any.empty()) continue; // fully local bus: fine
std::vector<int> dangling;
for (auto &lkv : lanes)
if (!touching_any.count(lkv.first))
dangling.push_back(lkv.first);
if (dangling.empty()) continue;
int lo = lanes.begin()->first;
int hi = lanes.rbegin()->first;
Anomaly an;
an.kind = AnomalyKind::DiffCrossIncomplete;
an.module = gkv.first.first;
std::string m = gkv.first.first->name + ": " + gkv.first.second
+ "[" + std::to_string(lo) + ".."
+ std::to_string(hi) + "]_P/N: lane(s)";
for (int ix : dangling) m += " " + std::to_string(ix);
m += " do not cross (others reach";
std::vector<std::string> names;
for (Module *mod : reached) names.push_back(mod->name);
std::sort(names.begin(), names.end());
for (const std::string &nm : names) m += " " + nm;
m += ")";
an.message = std::move(m);
for (auto &lkv : lanes) {
an.involved.push_back(pairs[lkv.second].p);
an.involved.push_back(pairs[lkv.second].n);
}
out.push_back(std::move(an));
}
return out;
}

View File

@@ -0,0 +1,27 @@
#ifndef _DIFF_CHECK_HPP_
#define _DIFF_CHECK_HPP_
#include "analysis.hpp" // Anomaly, diff_suffix, split_trailing_index
#include "nets.hpp" // Net
#include <vector>
class System;
// Differential-pair crossing checks. Every complete local diff pair
// (X_P / X_N, name-based) resolves its two legs to two bridged nets; any
// other module whose own pair sits on those nets must match them leg for
// leg. Findings:
// - DiffPolaritySwap: the peer pair is wired P→N / N→P, or a pair's two
// legs end up joined onto one single net through the connections.
// - DiffCrossIncomplete: the two pairs share only one leg (the other does
// not cross), or some lanes of a diff bus do not reach a module the
// other lanes reach.
// Name-based on BOTH sides: a peer whose signals carry no _P/_N suffix is
// not judged (silent). Polarity swaps are sometimes intentional (routing
// compensation, SerDes with configurable polarity) — these are findings to
// review, not hard errors. `nets` must come from compute_all_nets(sys).
std::vector<Anomaly> check_diff_crossings(System *sys,
const std::vector<Net> *nets);
#endif // _DIFF_CHECK_HPP_

View File

@@ -259,6 +259,7 @@ void Tui::RegisterCommands() {
render(r.jtag_anomalies, " JTAG chain anomaly(ies)."); render(r.jtag_anomalies, " JTAG chain anomaly(ies).");
render(r.conflict_anomalies, " source-conflict(s)."); render(r.conflict_anomalies, " source-conflict(s).");
render(r.completeness_anomalies, " BSDL completeness issue(s)."); render(r.completeness_anomalies, " BSDL completeness issue(s).");
render(r.diff_anomalies, " diff-pair crossing anomaly(ies).");
}, true, }, true,
"check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks" }; "check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks" };

View File

@@ -88,6 +88,7 @@ Component Tui::BuildAnalyzeScreen() {
push_anoms(vr.jtag_anomalies); push_anoms(vr.jtag_anomalies);
push_anoms(vr.conflict_anomalies); push_anoms(vr.conflict_anomalies);
push_anoms(vr.completeness_anomalies); push_anoms(vr.completeness_anomalies);
push_anoms(vr.diff_anomalies);
int n_model = vr.model_total(); int n_model = vr.model_total();
if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)"); if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)");

View File

@@ -647,6 +647,7 @@ void EssimFrame::OnVerify(wxCommandEvent &) {
log_anoms(r.jtag_anomalies, "JTAG chain anomaly(ies)"); log_anoms(r.jtag_anomalies, "JTAG chain anomaly(ies)");
log_anoms(r.conflict_anomalies, "source-conflict(s)"); log_anoms(r.conflict_anomalies, "source-conflict(s)");
log_anoms(r.completeness_anomalies, "BSDL completeness issue(s)"); log_anoms(r.completeness_anomalies, "BSDL completeness issue(s)");
log_anoms(r.diff_anomalies, "diff-pair crossing anomaly(ies)");
RebuildModelView(); RebuildModelView();
} }

184
tests/test_diff_check.cpp Normal file
View File

@@ -0,0 +1,184 @@
#include <doctest/doctest.h>
#include "core/app/verify.hpp"
#include "core/domain/analysis.hpp"
#include "core/domain/connect.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/pins.hpp"
#include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
// check_diff_crossings: a complete local diff pair (X_P / X_N) must cross a
// connection leg for leg. Both sides are judged by name only.
namespace {
// New pin on `part`, wired to (or creating) module signal `sig_name`.
Pin *wire(Module *m, Part *p, const std::string &pin_name,
const std::string &sig_name)
{
Pin *pin = new Pin(pin_name);
p->add(pin);
Signal *s = m->signals->merge(sig_name);
s->add(pin);
pin->connect(s);
return pin;
}
struct Rig {
System sys;
Module *a, *b;
Part *ja, *jb;
Rig() {
a = sys.modules()->merge("A");
b = sys.modules()->merge("B");
ja = new Part("J1"); a->add(ja);
jb = new Part("P1"); b->add(jb);
}
void bridge(std::initializer_list<std::pair<Pin *, Pin *>> wires) {
Connection *c = new Connection("A/J1 <-> B/P1", a, ja, b, jb);
c->transform_name = "identity";
for (const auto &w : wires) c->pin_map.push_back(w);
sys.connections()->add(c);
}
};
} // namespace
TEST_CASE("diff crossing: straight P↔P / N↔N is silent") {
Rig r;
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
Pin *an = wire(r.a, r.ja, "2", "TX_N");
Pin *bp = wire(r.b, r.jb, "1", "RX_P");
Pin *bn = wire(r.b, r.jb, "2", "RX_N");
r.bridge({{ap, bp}, {an, bn}});
app::VerifyReport vr = app::verify(&r.sys);
CHECK(vr.diff_anomalies.empty());
}
TEST_CASE("diff crossing: swapped legs report ONE polarity-swap anomaly") {
Rig r;
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
Pin *an = wire(r.a, r.ja, "2", "TX_N");
Pin *bp = wire(r.b, r.jb, "1", "RX_P");
Pin *bn = wire(r.b, r.jb, "2", "RX_N");
r.bridge({{ap, bn}, {an, bp}}); // crossed on purpose
app::VerifyReport vr = app::verify(&r.sys);
REQUIRE(vr.diff_anomalies.size() == 1); // deduped: not once per side
CHECK(vr.diff_anomalies[0].kind == AnomalyKind::DiffPolaritySwap);
CHECK(vr.diff_anomalies[0].message.find("polarity swapped")
!= std::string::npos);
}
TEST_CASE("diff crossing: a single bridged leg reports incomplete") {
Rig r;
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
wire(r.a, r.ja, "2", "TX_N"); // N leg stays local
Pin *bp = wire(r.b, r.jb, "1", "RX_P");
wire(r.b, r.jb, "2", "RX_N"); // peer N leg exists, unbridged
r.bridge({{ap, bp}});
app::VerifyReport vr = app::verify(&r.sys);
REQUIRE(vr.diff_anomalies.size() == 1);
CHECK(vr.diff_anomalies[0].kind == AnomalyKind::DiffCrossIncomplete);
CHECK(vr.diff_anomalies[0].message.find("only the P legs")
!= std::string::npos);
}
TEST_CASE("diff crossing: an unsuffixed peer is not judged") {
Rig r;
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
Pin *an = wire(r.a, r.ja, "2", "TX_N");
Pin *b1 = wire(r.b, r.jb, "1", "RXP"); // no _P/_N suffix
Pin *b2 = wire(r.b, r.jb, "2", "RXN");
r.bridge({{ap, b2}, {an, b1}}); // even crossed: silent
app::VerifyReport vr = app::verify(&r.sys);
CHECK(vr.diff_anomalies.empty());
}
TEST_CASE("diff crossing: P and N joined onto one net is flagged") {
Rig r;
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
Pin *an = wire(r.a, r.ja, "2", "TX_N");
Pin *b1 = wire(r.b, r.jb, "1", "X");
Pin *b2 = wire(r.b, r.jb, "2", "X"); // same peer signal
r.bridge({{ap, b1}, {an, b2}});
app::VerifyReport vr = app::verify(&r.sys);
REQUIRE(vr.diff_anomalies.size() == 1);
CHECK(vr.diff_anomalies[0].kind == AnomalyKind::DiffPolaritySwap);
CHECK(vr.diff_anomalies[0].message.find("join the same net")
!= std::string::npos);
}
TEST_CASE("diff bus crossing: a dangling lane is listed, once per side") {
Rig r;
// Two lanes on each side; only lane 0 is bridged — lane 1 crosses nowhere.
Pin *a0p = wire(r.a, r.ja, "1", "TX0_P");
Pin *a0n = wire(r.a, r.ja, "2", "TX0_N");
wire(r.a, r.ja, "3", "TX1_P");
wire(r.a, r.ja, "4", "TX1_N");
Pin *b0p = wire(r.b, r.jb, "1", "RX0_P");
Pin *b0n = wire(r.b, r.jb, "2", "RX0_N");
wire(r.b, r.jb, "3", "RX1_P");
wire(r.b, r.jb, "4", "RX1_N");
r.bridge({{a0p, b0p}, {a0n, b0n}});
app::VerifyReport vr = app::verify(&r.sys);
// One aggregated anomaly per side (each names its own lane signals).
REQUIRE(vr.diff_anomalies.size() == 2);
for (const auto &an : vr.diff_anomalies) {
CHECK(an.kind == AnomalyKind::DiffCrossIncomplete);
CHECK(an.message.find("lane(s) 1 do not cross") != std::string::npos);
}
}
TEST_CASE("diff bus crossing: a distributed bus (lanes fanned out) is silent") {
// A's two lanes go to two DIFFERENT modules — legitimate backplane fan-out.
System sys;
Module *a = sys.modules()->merge("A");
Module *b = sys.modules()->merge("B");
Module *c = sys.modules()->merge("C");
Part *ja = new Part("J1"); a->add(ja);
Part *jb = new Part("P1"); b->add(jb);
Part *jc = new Part("P1"); c->add(jc);
Pin *a0p = wire(a, ja, "1", "TX0_P");
Pin *a0n = wire(a, ja, "2", "TX0_N");
Pin *a1p = wire(a, ja, "3", "TX1_P");
Pin *a1n = wire(a, ja, "4", "TX1_N");
Pin *b0p = wire(b, jb, "1", "RX0_P");
Pin *b0n = wire(b, jb, "2", "RX0_N");
Pin *c0p = wire(c, jc, "1", "RX0_P");
Pin *c0n = wire(c, jc, "2", "RX0_N");
Connection *cb = new Connection("A/J1 <-> B/P1", a, ja, b, jb);
cb->transform_name = "identity";
cb->pin_map.emplace_back(a0p, b0p);
cb->pin_map.emplace_back(a0n, b0n);
sys.connections()->add(cb);
Connection *cc = new Connection("A/J1 <-> C/P1", a, ja, c, jc);
cc->transform_name = "identity";
cc->pin_map.emplace_back(a1p, c0p);
cc->pin_map.emplace_back(a1n, c0n);
sys.connections()->add(cc);
app::VerifyReport vr = app::verify(&sys);
CHECK(vr.diff_anomalies.empty()); // every lane crosses somewhere
}
TEST_CASE("diff crossing: empty / unconnected systems are silent") {
System sys;
app::VerifyReport vr = app::verify(&sys);
CHECK(vr.diff_anomalies.empty());
// A pair that never crosses anything: silent (local pairs are fine).
Rig r;
wire(r.a, r.ja, "1", "TX_P");
wire(r.a, r.ja, "2", "TX_N");
vr = app::verify(&r.sys);
CHECK(vr.diff_anomalies.empty());
}