diff --git a/src/core/app/script.cpp b/src/core/app/script.cpp index 6b954f3..7ef490f 100644 --- a/src/core/app/script.cpp +++ b/src/core/app/script.cpp @@ -150,6 +150,7 @@ private: grp(r.jtag_anomalies, " JTAG chain anomaly(ies)."); grp(r.conflict_anomalies, " source-conflict(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. diff --git a/src/core/app/verify.cpp b/src/core/app/verify.cpp index dfc0044..cf7f16a 100644 --- a/src/core/app/verify.cpp +++ b/src/core/app/verify.cpp @@ -2,6 +2,7 @@ #include "core/domain/bsdl_check.hpp" #include "core/domain/connect.hpp" +#include "core/domain/diff_check.hpp" #include "core/domain/modules.hpp" #include "core/domain/nets.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}); } - // 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.jtag_anomalies = check_jtag_chain(sys, &nets); r.conflict_anomalies = check_source_conflicts(sys); r.completeness_anomalies = check_bsdl_completeness(sys); + r.diff_anomalies = check_diff_crossings(sys, &nets); return r; } diff --git a/src/core/app/verify.hpp b/src/core/app/verify.hpp index 38c905d..33d5b6f 100644 --- a/src/core/app/verify.hpp +++ b/src/core/app/verify.hpp @@ -52,11 +52,13 @@ struct VerifyReport { std::vector jtag_anomalies; ///< check_jtag_chain std::vector conflict_anomalies; ///< check_source_conflicts std::vector completeness_anomalies; ///< check_bsdl_completeness + std::vector diff_anomalies; ///< check_diff_crossings int orphan_total() const { return orphan_imported + orphan_dropped; } int model_total() const { return (int)(pin_anomalies.size() + jtag_anomalies.size() - + conflict_anomalies.size() + completeness_anomalies.size()); + + conflict_anomalies.size() + completeness_anomalies.size() + + diff_anomalies.size()); } }; diff --git a/src/core/domain/analysis.cpp b/src/core/domain/analysis.cpp index e526d54..4397020 100644 --- a/src/core/domain/analysis.cpp +++ b/src/core/domain/analysis.cpp @@ -31,15 +31,15 @@ const char *anomaly_kind_name(AnomalyKind k) { case AnomalyKind::JtagBusUnbridged: return "jtag-bus-unbridged"; case AnomalyKind::SourceConflict: return "source-conflict"; case AnomalyKind::BsdlPinMissing: return "bsdl-pin-missing"; + case AnomalyKind::DiffPolaritySwap: return "diff-polarity-swap"; + case AnomalyKind::DiffCrossIncomplete: return "diff-cross-incomplete"; } return "?"; } -namespace { - // Diff-pair suffix detection. Returns true and fills if // `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) { if (name.size() < 3) return false; char last = name.back(); @@ -52,6 +52,29 @@ bool diff_suffix(const std::string &name, std::string &stem, char &pol) { 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: // - bracketed: NAME[12] → stem "NAME", idx 12 // - 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; } -// 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) { // ---- Pass 1: diff pairs ---- std::unordered_map> dp; // stem -> {P, N} diff --git a/src/core/domain/analysis.hpp b/src/core/domain/analysis.hpp index e57c94d..ac6fd3b 100644 --- a/src/core/domain/analysis.hpp +++ b/src/core/domain/analysis.hpp @@ -39,6 +39,8 @@ enum class AnomalyKind { JtagBusUnbridged, ///< TMS or TCK is not common to all TAP devices. 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. + DiffPolaritySwap, ///< A diff pair crosses a connection with P and N swapped. + DiffCrossIncomplete, ///< A diff pair/bus only partially crosses a connection. }; 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); +// 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`: // - GndShield when the name unambiguously matches GND/SHIELD/CHASSIS/EARTH. // - Power when the name suggests Power AND there is structural evidence diff --git a/src/core/domain/diff_check.cpp b/src/core/domain/diff_check.cpp new file mode 100644 index 0000000..5daf2d0 --- /dev/null +++ b/src/core/domain/diff_check.cpp @@ -0,0 +1,194 @@ +#include "diff_check.hpp" + +#include "modules.hpp" +#include "signals.hpp" +#include "system.hpp" + +#include +#include +#include +#include +#include +#include + +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 check_diff_crossings(System *sys, + const std::vector *nets) +{ + std::vector out; + if (!sys || !nets) return out; + + // Signal → net id (compute_all_nets covers every signal, singletons too). + std::unordered_map 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 pairs; + std::unordered_map pair_of; // leg signal → index in `pairs` + for (auto &mkv : *sys->modules()) { + Module *mod = mkv.second; + std::map 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> 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 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 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::map> 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 touching_any; // lanes sharing ≥1 net with a peer + std::set complete_any; // lanes fully crossing somewhere + std::set 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 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 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; +} diff --git a/src/core/domain/diff_check.hpp b/src/core/domain/diff_check.hpp new file mode 100644 index 0000000..dc18349 --- /dev/null +++ b/src/core/domain/diff_check.hpp @@ -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 + +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 check_diff_crossings(System *sys, + const std::vector *nets); + +#endif // _DIFF_CHECK_HPP_ diff --git a/src/frontends/tui/commands.cpp b/src/frontends/tui/commands.cpp index 4eaa224..8e39659 100644 --- a/src/frontends/tui/commands.cpp +++ b/src/frontends/tui/commands.cpp @@ -259,6 +259,7 @@ void Tui::RegisterCommands() { render(r.jtag_anomalies, " JTAG chain anomaly(ies)."); render(r.conflict_anomalies, " source-conflict(s)."); render(r.completeness_anomalies, " BSDL completeness issue(s)."); + render(r.diff_anomalies, " diff-pair crossing anomaly(ies)."); }, true, "check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks" }; diff --git a/src/frontends/tui/screen_analyze.cpp b/src/frontends/tui/screen_analyze.cpp index d9a9eba..9a3e339 100644 --- a/src/frontends/tui/screen_analyze.cpp +++ b/src/frontends/tui/screen_analyze.cpp @@ -88,6 +88,7 @@ Component Tui::BuildAnalyzeScreen() { push_anoms(vr.jtag_anomalies); push_anoms(vr.conflict_anomalies); push_anoms(vr.completeness_anomalies); + push_anoms(vr.diff_anomalies); int n_model = vr.model_total(); if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)"); diff --git a/src/frontends/wx/wx_frame.cpp b/src/frontends/wx/wx_frame.cpp index 062d75a..8e3c92d 100644 --- a/src/frontends/wx/wx_frame.cpp +++ b/src/frontends/wx/wx_frame.cpp @@ -647,6 +647,7 @@ void EssimFrame::OnVerify(wxCommandEvent &) { log_anoms(r.jtag_anomalies, "JTAG chain anomaly(ies)"); log_anoms(r.conflict_anomalies, "source-conflict(s)"); log_anoms(r.completeness_anomalies, "BSDL completeness issue(s)"); + log_anoms(r.diff_anomalies, "diff-pair crossing anomaly(ies)"); RebuildModelView(); } diff --git a/tests/test_diff_check.cpp b/tests/test_diff_check.cpp new file mode 100644 index 0000000..a03d69e --- /dev/null +++ b/tests/test_diff_check.cpp @@ -0,0 +1,184 @@ +#include + +#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> 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()); +}