Compare commits

...

5 Commits

Author SHA1 Message Date
dceb61237d tui: fix nested source abandoning the calling script
Tui::Source kept the script position in single member fields
(loading_lines/idx/...), so a sourced line that was itself `source inner`
overwrote them: when the inner file finished, the outer script's remaining
lines were silently dropped.

The state is now a stack of SourceFrames — the stack is the call chain. A
nested source just pushes a frame (the running driver, ticker thread or
headless drain, picks it up next line) and the caller's frame resumes when
it pops. Each frame still prints its own "source: <file> (N line(s))"
summary; an interactive-line abort clears the whole chain; depth capped at
32 like the core script engine. The Computing modal shows the top frame.

Regression-tested headless via BootDispatch (tests/tui/test_source.cpp):
nested-then-continue, and self-recursion hitting the depth guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 20:32:36 +02:00
0b10e1c1b7 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>
2026-06-04 20:05:43 +02:00
9cf43696a2 Rename the power-adjacent category to "power management"
"Adjacent" read as jargon; "power management" is the standard EE umbrella
for enable/power-good/sense/fault/seq signals (cf. PMIC). Renamed across
the board: NameVerdict::PowerMgmt, stats/LoadResult field `mgmt`, analyze
tag [Power mgmt] + header "Pwr-mgmt" + glossary, load lines now say
"power-management (control/measure — kept as Other)" (TUI / script / wx
kept in sync).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 18:21:25 +02:00
e914c84c18 Power-control lexicon: adjust/trim/set, feedback variants, cmd, led, ref
ADJ/ADJUST/VADJ/TRIM, MARG/MARGIN, SET/VSET/ISET (regulator set-point),
FBK/FDB/VFB (feedback variants), CMD, LED (indicator drive), REF/VREF.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 16:46:29 +02:00
1943f1f88a Power inference: classify rail+control names as power-adjacent, not suspect
Names holding both a rail token (VCC/VDD/PWR/...) and a control token
(SENSE, EN, PG, FB, OK, FAULT, ...) are signals ABOUT a rail - feedback,
enable, power-good - so their non-Power classification is confident.
They used to land in the Suspect bucket, drowning the genuine ambiguities.

- classify_signal_name(): 3-state name verdict (Rail / PowerAdjacent /
  GndShield / Other) with whole-token matching (trailing digits stripped,
  long lexemes also match as suffix: VSENSE, PWRGOOD, NFAULT).
  infer_signal_type() becomes a thin wrapper, so the dashboard suspect
  count and the export suspect column shrink automatically.
- infer_signal_types(): PowerAdjacent -> Other + new `adjacent` stat,
  before the structural gate (a big-fanout sense net stays Other).
- LoadResult.adjacent rendered by all three consumers (TUI command,
  script engine, wx log) - outputs kept in sync.
- analyze Types tab: new [Pwr-adjacent] rows with the deciding token,
  deliberate sort order (Power, Suspect, Adjacent, Gnd), glossary entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 16:41:19 +02:00
21 changed files with 812 additions and 87 deletions

View File

@@ -36,6 +36,7 @@ LoadResult load_module(System *sys, const std::string &module_name,
r.power = inf.power;
r.gnd = inf.gnd;
r.kept_other = inf.kept_other;
r.mgmt = inf.mgmt;
r.ok = true;
} catch (const std::exception &e) {
r.error = e.what();

View File

@@ -23,6 +23,7 @@ struct LoadResult {
int power = 0; ///< signals inferred Power (name + structure)
int gnd = 0; ///< signals inferred GndShield (name)
int kept_other = 0; ///< name said Power but evidence too weak → kept Other
int mgmt = 0; ///< power-management signal (rail + SENSE/EN/PG/… token) → Other, not suspect
};
// Import a module from a netlist/pinout file into `sys`, drop singleton signals,

View File

@@ -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.
@@ -189,7 +190,9 @@ private:
+ " singleton/NC signal(s))" : ""));
emit(" types: " + std::to_string(r.power) + " power, "
+ std::to_string(r.gnd) + " gnd, " + std::to_string(r.kept_other)
+ " suspect Power (name only — kept as Other)");
+ " suspect Power (name only — kept as Other), "
+ std::to_string(r.mgmt)
+ " power-management (control/measure — kept as Other)");
return true;
}
if (cmd == "connect" || cmd == "plug") {

View File

@@ -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;
}

View File

@@ -52,11 +52,13 @@ struct VerifyReport {
std::vector<Anomaly> jtag_anomalies; ///< check_jtag_chain
std::vector<Anomaly> conflict_anomalies; ///< check_source_conflicts
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 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());
}
};

View File

@@ -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 <stem, polarity> 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<std::string, std::pair<Signal *, Signal *>> dp; // stem -> {P, N}
@@ -276,13 +278,21 @@ SignalTypeInferenceStats infer_signal_types(System *sys) {
Module *mod = mkv.second;
for (auto &skv : *mod->signals) {
Signal *s = skv.second;
SignalType named = infer_signal_type(s->name);
if (named == SignalType::GndShield) {
NameClassification ncl = classify_signal_name(s->name);
if (ncl.verdict == NameVerdict::GndShield) {
s->type = SignalType::GndShield;
++st.gnd;
continue;
}
if (named == SignalType::Power) {
if (ncl.verdict == NameVerdict::PowerMgmt) {
// A rail token next to a control token (SENSE, EN, PG, …):
// a signal about a rail, confidently NOT the rail — never
// suspect, whatever the fan-out.
s->type = SignalType::Other;
++st.mgmt;
continue;
}
if (ncl.verdict == NameVerdict::Rail) {
int fanout = (int)s->size();
// Hard rule: a "power" net that touches fewer than three
// pins cannot physically be a rail (a real rail goes to

View File

@@ -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 {
@@ -62,6 +64,8 @@ struct SignalTypeInferenceStats {
int power = 0; ///< Signals promoted to Power (name + structural).
int gnd = 0; ///< Signals promoted to GndShield (name only).
int kept_other = 0; ///< Name said Power but structural evidence too weak.
int mgmt = 0; ///< Power-management signal (rail + SENSE/EN/PG/…
///< token) → confidently Other, never suspect.
};
// Thresholds used by `infer_signal_types` (re-exposed so the analyze screen
@@ -71,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

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

@@ -5,8 +5,25 @@
enum class SignalType { Power, GndShield, Other };
// Name-level verdict, richer than SignalType. `PowerMgmt` is the key
// addition: a name holding BOTH a rail token (VCC/VDD/PWR/…) and a control
// token (SENSE/EN/PG/FB/…) is a power-management signal — measurement,
// enable, power-good — not the rail itself. Its non-Power classification is
// therefore confident, where a bare rail name without structural evidence
// stays suspect.
enum class NameVerdict { Rail, GndShield, PowerMgmt, Other };
struct NameClassification {
NameVerdict verdict = NameVerdict::Other;
std::string token; ///< PowerMgmt only: the control token that decided it.
};
NameClassification classify_signal_name(const std::string &signal_name);
const char *signal_type_name(SignalType t);
bool signal_type_from_name(const std::string &s, SignalType &out);
// Thin wrapper over classify_signal_name: Rail → Power, GndShield → GndShield,
// PowerMgmt/Other → Other.
SignalType infer_signal_type(const std::string &signal_name);
SignalType next_signal_type(SignalType t); // Power → GndShield → Other → Power

View File

@@ -4,6 +4,7 @@
#include <algorithm>
#include <cctype>
#include <cstring>
#include <vector>
const char *signal_type_name(SignalType t) {
@@ -36,11 +37,74 @@ bool signal_type_from_name(const std::string &s, SignalType &out) {
return false;
}
// Heuristic. Names like GND, GNDA, SHIELD, CHASSIS → GndShield.
// Names containing PWR/POWER/VCC/VDD/VEE/VSS, or matching V±N or +N.NV
// patterns, or starting with VS_/VS3_ → Power. Else Other.
SignalType infer_signal_type(const std::string &name) {
if (name.empty()) return SignalType::Other;
namespace {
// Control/monitoring vocabulary: a name holding both a rail token and one of
// these is a signal ABOUT a rail (feedback, enable, power-good, fault, …) —
// not the rail itself. Matched against whole separator-delimited tokens
// (uppercase, trailing digits stripped so EN1/PG0 still hit). Entries of
// length ≥ 4 also match as a token *suffix*, catching fused (VSENSE, PWRGOOD)
// and active-low (NFAULT) forms; short entries match exactly, so GREEN or
// SENSOR never trip on EN / SENSE.
const char *const kPowerControlTokens[] = {
"SENSE", "SNS", "KELVIN", // remote / Kelvin sense
"FB", "FBK", "FDB", "FDBK", "VFB", // regulator feedback
"FEEDBACK",
"EN", "ENA", "ENABLE", "INH", "INHIBIT", // enable / inhibit
"PG", "PGOOD", "PWRGD", "PWROK", // power-good
"GOOD", "OK", "FAIL", "FAULT", "FLT", // status / fault
"ALERT", "ALRT", "WARN",
"MON", "IMON", "VMON", "PMON", // monitoring
"DET", "DETECT", "PRSNT", "PRESENT", // presence detection
"OC", "OCP", "OV", "OVP", "UV", "UVP", // protection trips
"TRIP", "SHDN", "SHUTDOWN",
"ADJ", "ADJUST", "VADJ", "TRIM", // regulator adjust / trim
"MARG", "MARGIN", // voltage margining
"SET", "VSET", "ISET", // set-point pins
"SEQ", "CTRL", "CTL", "CMD", // sequencing / control / command
"STAT", "STATUS",
"ON", "OFF", "BTN", // on/off request
"REF", "VREF", // voltage reference
"LED", // indicator drive
"CS", "IRQ",
};
bool is_power_control_token(std::string tok) {
while (!tok.empty() && std::isdigit((unsigned char)tok.back()))
tok.pop_back(); // EN1, PG0, FB2 …
if (tok.empty()) return false;
for (const char *lex : kPowerControlTokens) {
size_t n = std::strlen(lex);
if (tok == lex) return true;
if (n >= 4 && tok.size() > n
&& tok.compare(tok.size() - n, n, lex) == 0)
return true; // VSENSE, PWRGOOD, NFAULT …
}
return false;
}
// Split on every non-alphanumeric character. `u` is already uppercase.
std::vector<std::string> alnum_tokens(const std::string &u) {
std::vector<std::string> out;
std::string cur;
for (char c : u) {
if (std::isalnum((unsigned char)c)) { cur += c; continue; }
if (!cur.empty()) { out.push_back(std::move(cur)); cur.clear(); }
}
if (!cur.empty()) out.push_back(std::move(cur));
return out;
}
} // namespace
// Heuristic. Names like GND, GNDA, SHIELD, CHASSIS → GndShield (name alone is
// reliable there — left out of the control-token logic on purpose). Names
// containing PWR/POWER/VCC/VDD/VEE/VSS, or starting with VS_/VBAT/+/ → rail
// candidates; a rail candidate whose tokens include a control word (SENSE,
// EN, PG, …) is downgraded to PowerMgmt. Else Other.
NameClassification classify_signal_name(const std::string &name) {
NameClassification out;
if (name.empty()) return out;
std::string u = name;
std::transform(u.begin(), u.end(), u.begin(),
[](unsigned char c) { return std::toupper(c); });
@@ -52,14 +116,14 @@ SignalType infer_signal_type(const std::string &name) {
return u.rfind(needle, 0) == 0;
};
if (u == "GND" || u == "GROUND") return SignalType::GndShield;
if (starts_with("GND_")
if (u == "GND" || u == "GROUND"
|| starts_with("GND_")
|| (starts_with("GND") && u.size() >= 4
&& std::isalpha((unsigned char)u[3]))) {
return SignalType::GndShield;
&& std::isalpha((unsigned char)u[3]))
|| contains("SHIELD") || contains("CHASSIS") || contains("EARTH")) {
out.verdict = NameVerdict::GndShield;
return out;
}
if (contains("SHIELD") || contains("CHASSIS") || contains("EARTH"))
return SignalType::GndShield;
if (contains("PWR") || contains("POWER")
|| contains("VCC") || contains("VDD") || contains("VEE") || contains("VSS")
@@ -67,7 +131,25 @@ SignalType infer_signal_type(const std::string &name) {
|| starts_with("VS3_") || starts_with("VS4_")
|| starts_with("VBAT") || starts_with("VBUS")
|| starts_with("+") || starts_with("-")) {
return SignalType::Power;
for (const std::string &tok : alnum_tokens(u)) {
if (is_power_control_token(tok)) {
out.verdict = NameVerdict::PowerMgmt;
out.token = tok;
return out;
}
}
out.verdict = NameVerdict::Rail;
return out;
}
return out;
}
SignalType infer_signal_type(const std::string &name) {
switch (classify_signal_name(name).verdict) {
case NameVerdict::Rail: return SignalType::Power;
case NameVerdict::GndShield: return SignalType::GndShield;
case NameVerdict::PowerMgmt:
case NameVerdict::Other: break;
}
return SignalType::Other;
}

View File

@@ -152,7 +152,9 @@ void Tui::RegisterCommands() {
Print(" types: " + std::to_string(r.power) + " power, "
+ std::to_string(r.gnd) + " gnd, "
+ std::to_string(r.kept_other)
+ " suspect Power (name only — kept as Other)");
+ " suspect Power (name only — kept as Other), "
+ std::to_string(r.mgmt)
+ " power-management (control/measure — kept as Other)");
},
/*prompt_for_missing=*/ true,
"load a module from a netlist / pinout file (mentor, altium, ods)",
@@ -257,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" };

View File

@@ -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)");
@@ -131,29 +132,39 @@ Component Tui::BuildAnalyzeScreen() {
// ============================================================= Types
// Power decisions (confirmed / refuted) and NC orphan breakdown.
analyze_types.clear();
int conf_pwr = 0, ref_pwr = 0, gnd = 0;
struct Row { char kind; std::string mod, sig; int fanout; bool voltage; };
int conf_pwr = 0, ref_pwr = 0, mgmt = 0, gnd = 0;
struct Row { char kind; std::string mod, sig; int fanout; bool voltage;
std::string token; };
std::vector<Row> rows;
for (auto &mkv : *sys->modules()) {
Module *mod = mkv.second;
for (auto &skv : *mod->signals) {
Signal *s = skv.second;
SignalType named = infer_signal_type(s->name);
NameClassification ncl = classify_signal_name(s->name);
char kind = 0;
if (named == SignalType::GndShield && s->type == SignalType::GndShield) {
std::string token;
if (ncl.verdict == NameVerdict::GndShield && s->type == SignalType::GndShield) {
kind = 'G'; ++gnd;
} else if (named == SignalType::Power && s->type == SignalType::Power) {
} else if (ncl.verdict == NameVerdict::Rail && s->type == SignalType::Power) {
kind = 'P'; ++conf_pwr;
} else if (named == SignalType::Power && s->type == SignalType::Other) {
} else if (ncl.verdict == NameVerdict::Rail && s->type == SignalType::Other) {
kind = 'R'; ++ref_pwr;
} else if (ncl.verdict == NameVerdict::PowerMgmt) {
kind = 'M'; ++mgmt; token = ncl.token;
} else continue;
rows.push_back({kind, mod->name, s->name,
(int)s->size(), has_voltage_pattern(s->name)});
(int)s->size(), has_voltage_pattern(s->name),
token});
}
}
// Deliberate display order: confirmed rails, then the suspects (the
// actionable residue), then the power-management signals, gnd last.
auto rank = [](char k) {
return k == 'P' ? 0 : k == 'R' ? 1 : k == 'M' ? 2 : 3;
};
std::sort(rows.begin(), rows.end(),
[](const Row &a, const Row &b) {
if (a.kind != b.kind) return a.kind < b.kind;
[&](const Row &a, const Row &b) {
if (a.kind != b.kind) return rank(a.kind) < rank(b.kind);
if (a.mod != b.mod) return a.mod < b.mod;
return a.sig < b.sig;
});
@@ -181,6 +192,10 @@ Component Tui::BuildAnalyzeScreen() {
else reason = "name only — fan-out "
+ std::to_string(r.fanout)
+ ", no voltage";
} else if (r.kind == 'M') {
tag = "[Power mgmt] ";
reason = "control token '" + r.token
+ "' in name — kept as Other";
} else {
tag = "[Gnd] ";
reason = "name match (fan-out " + std::to_string(r.fanout) + ")";
@@ -200,7 +215,8 @@ Component Tui::BuildAnalyzeScreen() {
std::string types_header = "Types: " + std::to_string(conf_pwr)
+ " Power, " + std::to_string(ref_pwr)
+ " Suspect, " + std::to_string(gnd)
+ " Suspect, " + std::to_string(mgmt)
+ " Pwr-mgmt, " + std::to_string(gnd)
+ " Gnd";
// Tab bar — horizontal headers, active one inverted.
@@ -240,8 +256,14 @@ Component Tui::BuildAnalyzeScreen() {
"Name suggests Power AND structure agrees: fan-out ≥ 4 pins, "
"or a voltage pattern in the name (e.g. 3V3, 5V, 12V)."),
term("Suspect Power",
"Name suggests Power but the structural check failed — "
"fan-out too low and no voltage in the name."),
"Name suggests Power, no control token explains it, but the "
"structural check failed — fan-out too low and no voltage "
"in the name."),
term("Power mgmt",
"Name holds a rail token AND a control token (SENSE, EN, PG, "
"FB, …): a power-management signal — measurement or control "
"of a rail — not the rail itself. Confidently Other, never "
"suspect."),
term("Hard floor",
"Fan-out below 3 pins forces Other regardless of the name. "
"A real rail physically cannot live on 1-2 pads."),

View File

@@ -281,20 +281,29 @@ void Tui::Source(const std::string &filename) {
if (const char *home = std::getenv("HOME"))
expanded = std::string(home) + expanded.substr(1);
}
if (source_stack.size() >= 32) { // same depth guard as the core engine
Print("source: nesting too deep, skipping " + filename);
return;
}
std::ifstream f(expanded);
if (!f) { Print("source failed: cannot open " + filename); return; }
// Slurp the whole file so we can drive line-by-line processing from the
// event loop (one line per posted task). This lets the screen redraw
// between lines and surface the "Computing…" modal.
loading_lines.clear();
SourceFrame fr;
fr.filename = filename;
std::string line;
while (std::getline(f, line)) loading_lines.push_back(line);
while (std::getline(f, line)) fr.lines.push_back(std::move(line));
// Nested source (a sourced line is itself `source …`): just stack the
// frame — the driver already running (ticker thread or headless drain)
// picks it up on the next ProcessNextSourceLine, and the caller's frame
// resumes when it finishes.
bool nested = !source_stack.empty();
source_stack.push_back(std::move(fr));
if (nested) return;
loading_filename = filename;
loading_idx = 0;
loading_executed = 0;
loading_lineno = 0;
loading_prev_in_source = in_source;
source_origin_screen = screen_idx; // a sourced line that leaves this screen aborts
in_source = true;
@@ -332,9 +341,18 @@ void Tui::Source(const std::string &filename) {
void Tui::ProcessNextSourceLine() {
if (!loading.load()) return;
while (loading_idx < loading_lines.size()) {
const std::string &raw = loading_lines[loading_idx++];
++loading_lineno;
while (!source_stack.empty()) {
if (source_stack.back().idx >= source_stack.back().lines.size()) {
// Frame done: summarise it and resume the caller's frame.
const SourceFrame &done = source_stack.back();
Print("source: " + done.filename
+ " (" + std::to_string(done.executed) + " line(s))");
source_stack.pop_back();
continue;
}
SourceFrame &fr = source_stack.back();
const std::string raw = fr.lines[fr.idx++];
++fr.lineno;
size_t start = raw.find_first_not_of(" \t");
if (start == std::string::npos) continue;
if (raw[start] == '#') continue;
@@ -343,28 +361,26 @@ void Tui::ProcessNextSourceLine() {
trimmed.pop_back();
if (trimmed.empty()) continue;
++fr.executed;
int lineno = fr.lineno; // copies: Submit can push a nested frame,
// which may reallocate and invalidate `fr`.
input = trimmed;
cursor_pos = (int)input.size();
Submit();
++loading_executed;
if (screen_idx != source_origin_screen) {
Print("source: line " + std::to_string(loading_lineno)
Print("source: line " + std::to_string(lineno)
+ " is interactive (would open a screen) — aborting.");
screen_idx = source_origin_screen;
loading.store(false);
computing_open = false;
tick_in_flight.store(false);
in_source = loading_prev_in_source;
return;
source_stack.clear(); // an abort cancels the whole chain
break;
}
// One effective line per tick — ack so the ticker can pace the next.
tick_in_flight.store(false);
return;
}
Print("source: " + loading_filename
+ " (" + std::to_string(loading_executed) + " line(s))");
// Stack drained (or aborted): close up.
loading.store(false);
computing_open = false;
tick_in_flight.store(false);

View File

@@ -12,7 +12,6 @@ using namespace ftxui;
Tui::Tui()
: cursor_pos(0), history_idx(-1), scroll_offset(0), quit(false), in_source(false),
loading(false), tick_in_flight(false),
loading_idx(0), loading_executed(0), loading_lineno(0),
loading_prev_in_source(false), screen_ptr(nullptr),
screen_idx(4), // boot to the dashboard; console (screen 0) is now a sub-screen
connect_m1_idx(0), connect_m2_idx(0),
@@ -64,12 +63,17 @@ void Tui::Run() {
// script is opened from the dashboard. The Renderer re-reads the live
// progress every frame.
auto computing_modal = Renderer([this] {
std::string progress = std::to_string(loading_executed) + " / "
+ std::to_string((int)loading_lines.size()) + " lines";
std::string fname, progress;
if (!source_stack.empty()) { // top frame = the file currently running
const SourceFrame &fr = source_stack.back();
fname = fr.filename;
progress = std::to_string(fr.executed) + " / "
+ std::to_string((int)fr.lines.size()) + " lines";
}
return vbox({
text(" Computing… ") | bold | center,
separator(),
text(loading_filename) | center,
text(fname) | center,
text(progress) | center,
}) | borderDouble | size(WIDTH, GREATER_THAN, 40);
});

View File

@@ -99,11 +99,16 @@ class Tui : public Frontend {
// ---- Source-file loading (event-driven, one line per tick) ----
std::atomic<bool> loading; ///< true while a script is being processed; read by tick thread.
std::atomic<bool> tick_in_flight; ///< main thread acks each tick by clearing this; ticker waits.
std::string loading_filename;
std::vector<std::string> loading_lines;
size_t loading_idx;
int loading_executed;
int loading_lineno;
// One script being processed. Nested `source` pushes a frame so the
// caller resumes where it left off — the stack IS the call chain.
struct SourceFrame {
std::string filename;
std::vector<std::string> lines;
size_t idx = 0; ///< next line to process
int executed = 0; ///< effective (non-blank, non-comment) lines run
int lineno = 0; ///< current 1-based line, for messages
};
std::vector<SourceFrame> source_stack;
bool loading_prev_in_source;
int source_origin_screen = 0; ///< screen a `source` started from; a sourced line that navigates away (opens an interactive screen) aborts it.
bool computing_open = false; ///< drives the global "Computing…" progress modal while a script loads.

View File

@@ -352,9 +352,9 @@ void EssimFrame::OnLoad(wxCommandEvent &) {
}
Log(wxString::Format(
"loaded '%s' from %s — %d part(s), %d signal(s)"
" (dropped %d; types: %d power / %d gnd / %d suspect)",
" (dropped %d; types: %d power / %d gnd / %d suspect / %d pwr-mgmt)",
modname, path, r.parts, r.signals, r.dropped, r.power, r.gnd,
r.kept_other));
r.kept_other, r.mgmt));
RebuildModelView();
}
@@ -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();
}

View File

@@ -235,7 +235,7 @@ TEST_CASE("infer_signal_types: Power requires name+structural agreement") {
Signal *p_3v3 = m->signals->merge("PWR_3V3"); fan_out(p_3v3, 3); // voltage + ≥ floor → Power
Signal *vcc = m->signals->merge("VCC"); fan_out(vcc, 5); // fan-out ≥ 4 → Power
Signal *pwr_ok = m->signals->merge("PWR_OK"); fan_out(pwr_ok, 1); // < 3 → hard floor → Other
Signal *pwr_ok = m->signals->merge("PWR_OK"); fan_out(pwr_ok, 1); // control token → pwr-mgmt
Signal *pwr_2 = m->signals->merge("PWR_2"); fan_out(pwr_2, 2); // < 3 → hard floor → Other
Signal *gnd = m->signals->merge("GND"); fan_out(gnd, 1); // gnd: name alone
Signal *clk = m->signals->merge("CLK_50MHZ"); fan_out(clk, 3); // not power-ish → Other
@@ -243,7 +243,8 @@ TEST_CASE("infer_signal_types: Power requires name+structural agreement") {
auto st = infer_signal_types(sys.get());
CHECK(st.power == 2); // PWR_3V3, VCC
CHECK(st.gnd == 1); // GND (name alone)
CHECK(st.kept_other == 2); // PWR_OK, PWR_2 below the hard floor
CHECK(st.kept_other == 1); // PWR_2 below the hard floor
CHECK(st.mgmt == 1); // PWR_OK: power-good control, not suspect
CHECK(p_3v3->type == SignalType::Power);
CHECK(vcc->type == SignalType::Power);
@@ -253,6 +254,27 @@ TEST_CASE("infer_signal_types: Power requires name+structural agreement") {
CHECK(clk->type == SignalType::Other);
}
TEST_CASE("infer_signal_types: power-management beats fan-out — a big sense net is still Other") {
auto sys = std::make_unique<System>();
Module *m = sys->modules()->merge("M");
Part *p = new Part("U1"); m->add(p);
// VDD_CORE_SENSE with fan-out 5: structure alone would confirm Power,
// but the control token settles it as a measurement net → Other,
// counted mgmt (not suspect, not power).
Signal *s = m->signals->merge("VDD_CORE_SENSE");
for (int i = 0; i < 5; ++i) {
Pin *pin = new Pin("p" + std::to_string(i));
p->add(pin); s->add(pin); pin->connect(s);
}
auto st = infer_signal_types(sys.get());
CHECK(st.power == 0);
CHECK(st.kept_other == 0);
CHECK(st.mgmt == 1);
CHECK(s->type == SignalType::Other);
}
TEST_CASE("infer_signal_types: fan-out hard floor overrides voltage in name") {
auto sys = std::make_unique<System>();
Module *m = sys->modules()->merge("M");

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());
}

View File

@@ -51,7 +51,52 @@ TEST_CASE("infer_signal_type: power family") {
CHECK(infer_signal_type("VS3_5V0") == SignalType::Power);
CHECK(infer_signal_type("+5V") == SignalType::Power);
CHECK(infer_signal_type("-12V") == SignalType::Power);
CHECK(infer_signal_type("VBAT_SENSE") == SignalType::Power);
// Rail token + control token → power-management, mapped to Other by the
// wrapper (it is a sense line ABOUT VBAT, not the rail).
CHECK(infer_signal_type("VBAT_SENSE") == SignalType::Other);
}
TEST_CASE("classify_signal_name: rail vs power-management signals") {
CHECK(classify_signal_name("VCC").verdict == NameVerdict::Rail);
CHECK(classify_signal_name("VDD_3V3").verdict == NameVerdict::Rail);
CHECK(classify_signal_name("+5V").verdict == NameVerdict::Rail);
NameClassification c = classify_signal_name("VDD_CORE_SENSE");
CHECK(c.verdict == NameVerdict::PowerMgmt);
CHECK(c.token == "SENSE");
CHECK(classify_signal_name("VBAT_SENSE").verdict == NameVerdict::PowerMgmt);
CHECK(classify_signal_name("VCC_EN").verdict == NameVerdict::PowerMgmt);
CHECK(classify_signal_name("VCC_EN1").verdict == NameVerdict::PowerMgmt); // trailing digit
CHECK(classify_signal_name("VDD_FB").verdict == NameVerdict::PowerMgmt);
CHECK(classify_signal_name("PWR_GOOD").verdict == NameVerdict::PowerMgmt);
CHECK(classify_signal_name("PWR_OK").verdict == NameVerdict::PowerMgmt);
CHECK(classify_signal_name("VBUS_DET").verdict == NameVerdict::PowerMgmt);
CHECK(classify_signal_name("POWER_FAIL").verdict == NameVerdict::PowerMgmt);
CHECK(classify_signal_name("VDD_VSENSE").verdict == NameVerdict::PowerMgmt); // fused suffix
CHECK(classify_signal_name("PWR_NFAULT").verdict == NameVerdict::PowerMgmt); // active-low
CHECK(classify_signal_name("VDD_ADJ").verdict == NameVerdict::PowerMgmt); // regulator adjust
CHECK(classify_signal_name("VCC_TRIM").verdict == NameVerdict::PowerMgmt);
CHECK(classify_signal_name("VDD_VTRIM").verdict == NameVerdict::PowerMgmt); // fused suffix
CHECK(classify_signal_name("VCC_VSET").verdict == NameVerdict::PowerMgmt);
CHECK(classify_signal_name("VCC_FBK").verdict == NameVerdict::PowerMgmt); // feedback variants
CHECK(classify_signal_name("VDD_FDB").verdict == NameVerdict::PowerMgmt);
CHECK(classify_signal_name("PWR_CMD").verdict == NameVerdict::PowerMgmt); // command
CHECK(classify_signal_name("PWR_LED").verdict == NameVerdict::PowerMgmt); // indicator
CHECK(classify_signal_name("VDD_REF").verdict == NameVerdict::PowerMgmt); // reference
CHECK(classify_signal_name("VCC_VREF").verdict == NameVerdict::PowerMgmt);
// Whole-token matching: SENSOR is not SENSE, GREEN is not EN —
// these stay genuine rails.
CHECK(classify_signal_name("VDD_SENSOR").verdict == NameVerdict::Rail);
CHECK(classify_signal_name("VCC_GREEN").verdict == NameVerdict::Rail);
// No rail token at all → Other, even with a control word.
CHECK(classify_signal_name("SPI_CS").verdict == NameVerdict::Other);
CHECK(classify_signal_name("FAN_SENSE").verdict == NameVerdict::Other);
// GND family is deliberately left out of the control-token logic.
CHECK(classify_signal_name("GND_RET").verdict == NameVerdict::GndShield);
}
TEST_CASE("infer_signal_type: other (data signals)") {

71
tests/tui/test_source.cpp Normal file
View File

@@ -0,0 +1,71 @@
#include <doctest/doctest.h>
#include "frontends/tui/tui.hpp"
#include <cstdio>
#include <fstream>
#include <sstream>
#include <string>
// Tui::Source nesting — regression for the bug where a nested `source`
// overwrote the single loading state, so the CALLING script's remaining
// lines never ran. Headless path (no screen): BootDispatch drains
// synchronously, exactly like `essim --batch --source`.
namespace {
std::string run_boot(const std::string &cmd) {
Tui t;
t.BootDispatch(cmd);
std::ostringstream oss;
t.DumpOutput(oss);
return oss.str();
}
} // namespace
TEST_CASE("source: lines after a nested source still run") {
const char *inner = "test_src_inner.essim";
const char *outer = "test_src_outer.essim";
{
std::ofstream f(inner);
f << "new\n";
}
{
std::ofstream f(outer);
f << "source " << inner << "\n"
"verify\n";
}
std::string out = run_boot(std::string("source ") + outer);
// The inner script ran and was summarised…
CHECK(out.find("system created.") != std::string::npos);
CHECK(out.find(std::string("source: ") + inner) != std::string::npos);
// …and the OUTER script kept going after it: verify executed…
CHECK(out.find("verify: 0 local mismatch(es)") != std::string::npos);
// …and the outer summary counts its 2 effective lines.
CHECK(out.find(std::string("source: ") + outer + " (2 line(s))")
!= std::string::npos);
std::remove(inner);
std::remove(outer);
}
TEST_CASE("source: self-recursion stops at the depth guard") {
const char *loop = "test_src_loop.essim";
{
std::ofstream f(loop);
f << "source " << loop << "\n";
}
std::string out = run_boot(std::string("source ") + loop);
CHECK(out.find("source: nesting too deep, skipping")
!= std::string::npos);
// Every frame still closes with its own summary.
CHECK(out.find(std::string("source: ") + loop + " (1 line(s))")
!= std::string::npos);
std::remove(loop);
}