Signal analysis pass (analyze), NC tests, DESIGN.md catch-up.
- New `src/system/analysis.{hpp,cpp}` — stateless post-processing pass
`analyze_system(System*) → AnalysisReport`. Per-module detection of
signal groups and anomalies; pure read, re-runnable.
- Groups: diff pairs (`*_P` / `*_N`, case-insensitive), buses
(`NAME[N]` or strict `NAME_N` — the `_` before digits is required
so names like `GETH_01_VDD12` are not misread as a bus).
- Anomalies: `DiffPairOrphan` (asymmetric: only `_P` without `_N` is
reported — `_N` alone is overloaded with active-low semantics and
floods the output with false positives), `BusGap` (missing index
inside a detected `[lo..hi]`).
- Noise filters: signals starting with `$` (Mentor internals) are
skipped wholesale.
- New `analyze` shell command — prints groups sorted by module +
label, then anomalies. Sized for the upcoming dashboard.
- `tests/test_analysis.cpp` — 8 cases covering both detectors, false-
positive guards (no-underscore digits, `$`-prefixed internals), and
per-module scoping.
- `tests/test_nc_origin.cpp` — completes the prior NC-tagging commit
with round-trip + drop_singleton_signals coverage.
- DESIGN.md updated: layout entry for `analysis.{hpp,cpp}` and new
section explaining the pass; NC-origin paragraph aligned with the
actual tag semantics and the verify three-pass summary.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
171
src/system/analysis.cpp
Normal file
171
src/system/analysis.cpp
Normal file
@@ -0,0 +1,171 @@
|
||||
#include "analysis.hpp"
|
||||
|
||||
#include "modules.hpp"
|
||||
#include "signals.hpp"
|
||||
#include "system.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
|
||||
const char *group_kind_name(GroupKind k) {
|
||||
switch (k) {
|
||||
case GroupKind::DiffPair: return "diff-pair";
|
||||
case GroupKind::Bus: return "bus";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
const char *anomaly_kind_name(AnomalyKind k) {
|
||||
switch (k) {
|
||||
case AnomalyKind::DiffPairOrphan: return "diff-pair-orphan";
|
||||
case AnomalyKind::BusGap: return "bus-gap";
|
||||
}
|
||||
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.
|
||||
bool diff_suffix(const std::string &name, std::string &stem, char &pol) {
|
||||
if (name.size() < 3) return false;
|
||||
char last = name.back();
|
||||
char up = (char)std::toupper((unsigned char)last);
|
||||
if (up != 'P' && up != 'N') return false;
|
||||
char sep = name[name.size() - 2];
|
||||
if (sep != '_') return false;
|
||||
stem = name.substr(0, name.size() - 2);
|
||||
pol = up;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bus suffix detection. Two accepted forms:
|
||||
// - bracketed: NAME[12] → stem "NAME", idx 12
|
||||
// - underscore: NAME_12 → stem "NAME_", idx 12 (underscore is REQUIRED
|
||||
// before the digits so we don't misread
|
||||
// names like "GETH_01_VDD12" as a bus)
|
||||
// Returns false otherwise.
|
||||
bool numeric_suffix(const std::string &name, std::string &stem, int &idx,
|
||||
bool &bracketed) {
|
||||
if (name.empty()) return false;
|
||||
bracketed = false;
|
||||
if (name.back() == ']') {
|
||||
size_t open = name.rfind('[');
|
||||
if (open == std::string::npos || open == 0) return false;
|
||||
for (size_t i = open + 1; i < name.size() - 1; ++i)
|
||||
if (!std::isdigit((unsigned char)name[i])) return false;
|
||||
idx = std::atoi(name.c_str() + open + 1);
|
||||
stem = name.substr(0, open);
|
||||
bracketed = true;
|
||||
return !stem.empty();
|
||||
}
|
||||
size_t i = name.size();
|
||||
while (i > 0 && std::isdigit((unsigned char)name[i - 1])) --i;
|
||||
if (i == name.size() || i < 2) return false; // no digits, or no room for stem+_
|
||||
if (name[i - 1] != '_') return false; // strict `_` before digits
|
||||
idx = std::atoi(name.c_str() + i);
|
||||
stem = name.substr(0, i); // includes the trailing '_'
|
||||
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] == '$';
|
||||
}
|
||||
|
||||
void analyse_module(Module *mod, AnalysisReport &out) {
|
||||
// ---- Pass 1: diff pairs ----
|
||||
std::unordered_map<std::string, std::pair<Signal *, Signal *>> dp; // stem -> {P, N}
|
||||
for (auto &kv : *mod->signals) {
|
||||
if (is_internal_name(kv.first)) continue;
|
||||
std::string stem; char pol;
|
||||
if (!diff_suffix(kv.first, stem, pol)) continue;
|
||||
auto &slot = dp[stem];
|
||||
if (pol == 'P') slot.first = kv.second;
|
||||
else slot.second = kv.second;
|
||||
}
|
||||
for (const auto &kv : dp) {
|
||||
const auto &slot = kv.second;
|
||||
if (slot.first && slot.second) {
|
||||
SignalGroup g;
|
||||
g.kind = GroupKind::DiffPair;
|
||||
g.label = kv.first + "_P/N";
|
||||
g.module = mod;
|
||||
g.members = {slot.first, slot.second};
|
||||
out.groups.push_back(std::move(g));
|
||||
continue;
|
||||
}
|
||||
// Orphan reporting is asymmetric on purpose: a `_P` without a `_N`
|
||||
// is almost always a broken diff pair, whereas `_N` is also widely
|
||||
// used as the active-low marker (RESET_N, BOOTMODE_N, …) so a `_N`
|
||||
// without `_P` is too noisy to surface.
|
||||
if (!slot.first) continue;
|
||||
Signal *present = slot.first;
|
||||
Anomaly a;
|
||||
a.kind = AnomalyKind::DiffPairOrphan;
|
||||
a.module = mod;
|
||||
a.message = mod->name + ": " + present->name
|
||||
+ " has no matching " + kv.first + "_N";
|
||||
a.involved.push_back(present);
|
||||
out.anomalies.push_back(std::move(a));
|
||||
}
|
||||
|
||||
// ---- Pass 2: buses ----
|
||||
// Group by stem; only consider stems with at least 2 entries.
|
||||
// Mixed bracketed / non-bracketed in the same stem are treated as one
|
||||
// family — the suffix value is what matters.
|
||||
std::map<std::string, std::map<int, Signal *>> buses;
|
||||
for (auto &kv : *mod->signals) {
|
||||
if (is_internal_name(kv.first)) continue;
|
||||
std::string stem; int idx; bool bracketed;
|
||||
if (!numeric_suffix(kv.first, stem, idx, bracketed)) continue;
|
||||
buses[stem][idx] = kv.second;
|
||||
}
|
||||
for (auto &bkv : buses) {
|
||||
auto &members = bkv.second;
|
||||
if (members.size() < 2) continue;
|
||||
int lo = members.begin()->first;
|
||||
int hi = members.rbegin()->first;
|
||||
SignalGroup g;
|
||||
g.kind = GroupKind::Bus;
|
||||
g.module = mod;
|
||||
g.lo = lo; g.hi = hi;
|
||||
g.label = bkv.first + "[" + std::to_string(lo) + ".."
|
||||
+ std::to_string(hi) + "]";
|
||||
for (auto &mkv : members) g.members.push_back(mkv.second);
|
||||
out.groups.push_back(std::move(g));
|
||||
|
||||
// Gap detection: missing indices inside [lo..hi].
|
||||
std::vector<int> missing;
|
||||
for (int i = lo + 1; i < hi; ++i)
|
||||
if (!members.count(i)) missing.push_back(i);
|
||||
if (!missing.empty()) {
|
||||
Anomaly a;
|
||||
a.kind = AnomalyKind::BusGap;
|
||||
a.module = mod;
|
||||
std::string m = mod->name + ": " + bkv.first + "["
|
||||
+ std::to_string(lo) + ".." + std::to_string(hi)
|
||||
+ "] missing index(es)";
|
||||
for (int idx : missing) m += " " + std::to_string(idx);
|
||||
a.message = std::move(m);
|
||||
for (auto &mkv : members) a.involved.push_back(mkv.second);
|
||||
out.anomalies.push_back(std::move(a));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AnalysisReport analyze_system(const System *sys) {
|
||||
AnalysisReport out;
|
||||
if (!sys) return out;
|
||||
// const_cast: SystemElementContainer iteration is non-const; analysis is
|
||||
// logically read-only. Mirrors the rest of the codebase.
|
||||
auto *mods = const_cast<System *>(sys)->modules();
|
||||
for (auto &kv : *mods) analyse_module(kv.second, out);
|
||||
return out;
|
||||
}
|
||||
50
src/system/analysis.hpp
Normal file
50
src/system/analysis.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#ifndef _ANALYSIS_HPP_
|
||||
#define _ANALYSIS_HPP_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class Module;
|
||||
class Signal;
|
||||
class System;
|
||||
|
||||
// Stateless post-processing over a System. Detects structural groups of
|
||||
// signals (diff pairs, buses, …) and structural anomalies (orphan diff
|
||||
// pair, gap in a bus, …). Pure read; no mutation. Re-runnable.
|
||||
|
||||
enum class GroupKind {
|
||||
DiffPair, ///< Two signals of the form X_P / X_N (or X_p / X_n).
|
||||
Bus, ///< NAME[0..N] or NAME_0..NAME_N (consecutive integer suffix).
|
||||
};
|
||||
|
||||
struct SignalGroup {
|
||||
GroupKind kind;
|
||||
std::string label; ///< Human-readable, e.g. "JTAG_TDI_P/N" or "DATA[0..7]".
|
||||
Module *module = nullptr; ///< Owning module (signals are module-scoped).
|
||||
std::vector<Signal *> members; ///< Pointers into the module's Signals.
|
||||
int lo = 0, hi = 0; ///< Bus only: extremities found.
|
||||
};
|
||||
|
||||
enum class AnomalyKind {
|
||||
DiffPairOrphan, ///< X_P present without X_N (or vice versa).
|
||||
BusGap, ///< NAME[0..N] has a missing index inside the range.
|
||||
};
|
||||
|
||||
struct Anomaly {
|
||||
AnomalyKind kind;
|
||||
std::string message;
|
||||
Module *module = nullptr;
|
||||
std::vector<Signal *> involved;
|
||||
};
|
||||
|
||||
struct AnalysisReport {
|
||||
std::vector<SignalGroup> groups;
|
||||
std::vector<Anomaly> anomalies;
|
||||
};
|
||||
|
||||
AnalysisReport analyze_system(const System *sys);
|
||||
|
||||
const char *group_kind_name(GroupKind k);
|
||||
const char *anomaly_kind_name(AnomalyKind k);
|
||||
|
||||
#endif // _ANALYSIS_HPP_
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/analysis.hpp"
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/nets.hpp"
|
||||
@@ -280,6 +281,56 @@ void Tui::RegisterCommands() {
|
||||
}, true,
|
||||
"check pin roles locally and signal-type consistency across bridged nets" };
|
||||
|
||||
commands["analyze"] = { {}, [this](auto &) {
|
||||
if (!sys) { Print("no system: run 'new' first."); return; }
|
||||
AnalysisReport rep = analyze_system(sys.get());
|
||||
|
||||
int n_diff = 0, n_bus = 0;
|
||||
for (const auto &g : rep.groups) {
|
||||
if (g.kind == GroupKind::DiffPair) ++n_diff;
|
||||
else if (g.kind == GroupKind::Bus) ++n_bus;
|
||||
}
|
||||
int n_dp_orph = 0, n_bus_gap = 0;
|
||||
for (const auto &a : rep.anomalies) {
|
||||
if (a.kind == AnomalyKind::DiffPairOrphan) ++n_dp_orph;
|
||||
else if (a.kind == AnomalyKind::BusGap) ++n_bus_gap;
|
||||
}
|
||||
|
||||
Print("analyze: " + std::to_string(n_diff) + " diff pair(s), "
|
||||
+ std::to_string(n_bus) + " bus(es).");
|
||||
|
||||
// Sort groups by module then label so output is stable.
|
||||
auto by_label = [](const SignalGroup &a, const SignalGroup &b) {
|
||||
std::string ma = a.module ? a.module->name : std::string{};
|
||||
std::string mb = b.module ? b.module->name : std::string{};
|
||||
if (ma != mb) return ma < mb;
|
||||
return a.label < b.label;
|
||||
};
|
||||
auto groups = rep.groups; // copy: report stays untouched
|
||||
std::sort(groups.begin(), groups.end(), by_label);
|
||||
for (const auto &g : groups) {
|
||||
std::string mname = g.module ? g.module->name : std::string("?");
|
||||
std::string line = " " + mname + "/" + g.label
|
||||
+ " [" + group_kind_name(g.kind) + "]"
|
||||
+ " — " + std::to_string(g.members.size())
|
||||
+ " signal(s)";
|
||||
Print(line);
|
||||
}
|
||||
|
||||
if (rep.anomalies.empty()) {
|
||||
Print("analyze: no anomaly.");
|
||||
} else {
|
||||
Print("analyze: " + std::to_string(rep.anomalies.size())
|
||||
+ " anomaly(ies) ("
|
||||
+ std::to_string(n_dp_orph) + " diff-pair orphan, "
|
||||
+ std::to_string(n_bus_gap) + " bus gap):");
|
||||
for (const auto &a : rep.anomalies)
|
||||
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] "
|
||||
+ a.message);
|
||||
}
|
||||
}, true,
|
||||
"detect signal groups (diff pairs, buses) and structural anomalies" };
|
||||
|
||||
commands["net"] = {
|
||||
{{"module", Completion::None},
|
||||
{"signal name", Completion::None}},
|
||||
|
||||
Reference in New Issue
Block a user