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:
2026-05-14 13:42:58 +02:00
parent 280526304d
commit 5e89b33088
6 changed files with 598 additions and 3 deletions

171
src/system/analysis.cpp Normal file
View 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
View 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_

View File

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