- 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>
755 lines
32 KiB
C++
755 lines
32 KiB
C++
#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"
|
|
#include "system/parts.hpp"
|
|
#include "system/persist.hpp"
|
|
#include "system/pin_role.hpp"
|
|
#include "system/pins.hpp"
|
|
#include "system/signals.hpp"
|
|
#include "system/system.hpp"
|
|
#include "system/transform.hpp"
|
|
#include "system/transform_vpx.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cstdlib>
|
|
#include <exception>
|
|
#include <fstream>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
|
|
void Tui::RegisterCommands() {
|
|
commands["help"] = {
|
|
{{"command name (optional)", Completion::Command}},
|
|
[this](const std::vector<std::string> &args) {
|
|
if (args.empty()) {
|
|
size_t maxw = 0;
|
|
for (const auto &kv : commands) maxw = std::max(maxw, kv.first.size());
|
|
auto print_group = [&](const std::string &title, bool want_interactive) {
|
|
bool printed_any = false;
|
|
for (const auto &kv : commands) {
|
|
if (kv.second.interactive != want_interactive) continue;
|
|
if (!printed_any) { Print(title); printed_any = true; }
|
|
Print(" " + kv.first
|
|
+ std::string(maxw - kv.first.size() + 2, ' ')
|
|
+ kv.second.description);
|
|
}
|
|
};
|
|
Print("Commands — type `help <name>` for details.");
|
|
print_group("Interactive (open a full-screen mode):", true);
|
|
print_group("Other:", false);
|
|
Print("Keys: Esc cancels a multi-step prompt or leaves an interactive screen;");
|
|
Print(" Tab completes commands/paths or cycles focus in interactive screens;");
|
|
Print(" PageUp/PageDown scroll output (10 lines), Home/End jump to top/bottom.");
|
|
return;
|
|
}
|
|
const std::string &name = args[0];
|
|
auto it = commands.find(name);
|
|
if (it == commands.end()) { Print("unknown command: " + name); return; }
|
|
const auto &spec = it->second;
|
|
std::string tag = spec.interactive ? " [interactive]" : "";
|
|
Print(name + tag + " — " + spec.description);
|
|
if (spec.params.empty()) {
|
|
Print(" no arguments.");
|
|
} else {
|
|
for (size_t i = 0; i < spec.params.size(); ++i) {
|
|
Print(" arg " + std::to_string(i + 1) + ": " + spec.params[i].name);
|
|
}
|
|
}
|
|
if (spec.interactive) {
|
|
Print(" run with no args to open the interactive screen,");
|
|
Print(" or with all args for inline (scriptable) execution.");
|
|
} else if (!spec.prompt_for_missing) {
|
|
Print(" no per-arg prompt — provide all args inline (or use the bare form).");
|
|
} else if (!spec.params.empty()) {
|
|
Print(" missing args trigger a prompt for each one.");
|
|
}
|
|
},
|
|
/*prompt_for_missing=*/ false,
|
|
"show command help (optionally for a specific command)",
|
|
};
|
|
commands["clear"] = { {}, [this](auto &) { output.clear(); }, true,
|
|
"clear the visualization area" };
|
|
commands["quit"] = { {}, [this](auto &) { quit = true; }, true,
|
|
"leave essim" };
|
|
commands["exit"] = { {}, [this](auto &) { quit = true; }, true,
|
|
"leave essim (alias of quit)" };
|
|
|
|
commands["new"] = { {}, [this](auto &) {
|
|
sys = std::make_unique<System>();
|
|
recorded.clear();
|
|
vars.clear();
|
|
Print("system created.");
|
|
}, true, "create a new (empty) system; resets the script-save buffer and $vars" };
|
|
|
|
commands["set"] = {
|
|
{{"name", Completion::None},
|
|
{"value", Completion::None}},
|
|
[this](const std::vector<std::string> &args) {
|
|
if (args.empty()) {
|
|
if (vars.empty()) { Print("(no variables defined)"); return; }
|
|
for (const auto &kv : vars)
|
|
Print(" $" + kv.first + " = " + kv.second);
|
|
return;
|
|
}
|
|
if (args.size() != 2) {
|
|
Print("usage: set <name> <value> (or no args to list)");
|
|
return;
|
|
}
|
|
const std::string &name = args[0];
|
|
if (name.empty()) { Print("set: empty name"); return; }
|
|
for (size_t i = 0; i < name.size(); ++i) {
|
|
char c = name[i];
|
|
bool ok = std::isalnum((unsigned char)c) || c == '_';
|
|
bool first_ok = i == 0 ? !std::isdigit((unsigned char)c) : true;
|
|
if (!ok || !first_ok) {
|
|
Print("set: invalid name '" + name
|
|
+ "' (must match [A-Za-z_][A-Za-z0-9_]*)");
|
|
return;
|
|
}
|
|
}
|
|
vars[name] = args[1];
|
|
},
|
|
/*prompt_for_missing=*/ false,
|
|
"define a $variable for substitution in subsequent commands "
|
|
"(no args = list defined vars)",
|
|
};
|
|
|
|
commands["load"] = {
|
|
{{"module name", Completion::None},
|
|
{"filename", Completion::Path},
|
|
{"import type [mentor|altium|ods]", Completion::None}},
|
|
[this](const std::vector<std::string> &args) {
|
|
if (!sys) { Print("no system: run 'new' first."); return; }
|
|
std::string ls = ToLower(args[2]);
|
|
ImportType t;
|
|
if (ls == "mentor") t = ImportType::IMPORT_MENTOR;
|
|
else if (ls == "altium") t = ImportType::IMPORT_ALTIUM;
|
|
else if (ls == "ods") t = ImportType::IMPORT_ODS;
|
|
else { Print("unknown import type: " + args[2]); return; }
|
|
try {
|
|
sys->Load(args[0], args[1], t);
|
|
Module *mod = sys->modules()->get(args[0]);
|
|
int dropped = drop_singleton_signals(mod->signals);
|
|
Print("loaded '" + args[0] + "' from " + args[1]);
|
|
Print(" parts: " + std::to_string(mod->size()));
|
|
Print(" signals: " + std::to_string(mod->signals->size())
|
|
+ (dropped ? " (dropped " + std::to_string(dropped)
|
|
+ " singleton/NC signal(s))" : ""));
|
|
} catch (const std::exception &e) {
|
|
Print(std::string("load failed: ") + e.what());
|
|
}
|
|
},
|
|
/*prompt_for_missing=*/ true,
|
|
"load a module from a netlist / pinout file (mentor, altium, ods)",
|
|
};
|
|
|
|
commands["source"] = {
|
|
{{"filename", Completion::Path}},
|
|
[this](const std::vector<std::string> &args) {
|
|
Source(args[0]);
|
|
},
|
|
/*prompt_for_missing=*/ true,
|
|
"execute a file of commands line by line (interactive cmds rejected)",
|
|
};
|
|
|
|
commands["script-save"] = {
|
|
{{"filename", Completion::Path}},
|
|
[this](const std::vector<std::string> &args) {
|
|
std::string expanded = args[0];
|
|
if (!expanded.empty() && expanded[0] == '~') {
|
|
if (const char *home = std::getenv("HOME"))
|
|
expanded = std::string(home) + expanded.substr(1);
|
|
}
|
|
std::ofstream f(expanded);
|
|
if (!f) { Print("script-save: cannot open " + args[0]); return; }
|
|
for (const auto &cmd : recorded) f << cmd << '\n';
|
|
Print("script-save: " + std::to_string(recorded.size())
|
|
+ " line(s) → " + args[0]);
|
|
},
|
|
/*prompt_for_missing=*/ true,
|
|
"write commands run since last 'new' as a replay-ready script",
|
|
};
|
|
|
|
commands["save"] = {
|
|
{{"filename", Completion::Path}},
|
|
[this](const std::vector<std::string> &args) {
|
|
if (!sys) { Print("no system: run 'new' first."); return; }
|
|
std::string err;
|
|
if (save_system(sys.get(), args[0], err)) {
|
|
Print("saved to " + args[0]);
|
|
} else {
|
|
Print("save failed: " + err);
|
|
}
|
|
},
|
|
/*prompt_for_missing=*/ true,
|
|
"write the current system snapshot to a file",
|
|
};
|
|
|
|
commands["restore"] = {
|
|
{{"filename", Completion::Path}},
|
|
[this](const std::vector<std::string> &args) {
|
|
std::string err;
|
|
System *fresh = restore_system(args[0], err);
|
|
if (!fresh) { Print("restore failed: " + err); return; }
|
|
sys.reset(fresh);
|
|
int mods = (int)sys->modules()->size();
|
|
int conns = (int)sys->connections()->size();
|
|
Print("restored from " + args[0]
|
|
+ " (" + std::to_string(mods) + " module(s), "
|
|
+ std::to_string(conns) + " connection(s))");
|
|
},
|
|
/*prompt_for_missing=*/ true,
|
|
"replace the current system with a saved snapshot",
|
|
};
|
|
|
|
commands["verify"] = { {}, [this](auto &) {
|
|
if (!sys) { Print("no system: run 'new' first."); return; }
|
|
int checked = 0;
|
|
int mismatches = 0;
|
|
for (auto &mkv : *sys->modules()) {
|
|
Module *mod = mkv.second;
|
|
for (auto &pkv : *mod) {
|
|
Part *prt = pkv.second;
|
|
if (prt->connector_type.empty()) continue;
|
|
for (auto &nkv : *prt) {
|
|
Pin *pin = nkv.second;
|
|
++checked;
|
|
SignalType expected = pin->expected_signal_type;
|
|
if (expected == SignalType::Other) continue;
|
|
Signal *s = pin->signal();
|
|
SignalType actual = s ? s->type : SignalType::Other;
|
|
if (actual == expected) continue;
|
|
++mismatches;
|
|
std::string sig_label = s ? s->name : std::string("(NC)");
|
|
Print(" " + mod->name + "/" + prt->name + "/" + pin->name
|
|
+ ": expected " + signal_type_name(expected)
|
|
+ ", got " + signal_type_name(actual)
|
|
+ " (signal: " + sig_label + ")");
|
|
}
|
|
}
|
|
}
|
|
Print("verify: " + std::to_string(mismatches) + " local mismatch(es) over "
|
|
+ std::to_string(checked) + " typed pin(s).");
|
|
|
|
auto nets = compute_all_nets(sys.get());
|
|
int bridged = 0, inconsistent = 0;
|
|
for (const auto &n : nets) {
|
|
if (n.members.size() < 2) continue;
|
|
++bridged;
|
|
SignalType dom;
|
|
if (net_type_consistent(n, dom)) continue;
|
|
++inconsistent;
|
|
std::string line = " net mixes Power and GndShield:";
|
|
for (const auto &mp : n.members) {
|
|
line += " " + mp.first->name + "/" + mp.second->name
|
|
+ "(" + signal_type_name(mp.second->type) + ")";
|
|
}
|
|
Print(line);
|
|
}
|
|
Print("verify: " + std::to_string(inconsistent) + " inconsistent net(s) over "
|
|
+ std::to_string(bridged) + " bridged net(s) ("
|
|
+ std::to_string(nets.size()) + " total).");
|
|
|
|
// Orphan pin report. A pin is "orphan" if it came out of import (or
|
|
// post-import drop) with no signal, and is still not bridged to a
|
|
// real signal via any Connection::pin_map. Use `nc-export` for the
|
|
// per-pin list.
|
|
std::unordered_set<Pin *> bridged_pins;
|
|
for (auto &ckv : *sys->connections())
|
|
for (auto &wp : ckv.second->pin_map) {
|
|
if (wp.first) bridged_pins.insert(wp.first);
|
|
if (wp.second) bridged_pins.insert(wp.second);
|
|
}
|
|
int orph_imported = 0, orph_dropped = 0;
|
|
for (auto &mkv : *sys->modules())
|
|
for (auto &pkv : *mkv.second)
|
|
for (auto &nkv : *pkv.second) {
|
|
Pin *pin = nkv.second;
|
|
if (pin->signal() || bridged_pins.count(pin)) continue;
|
|
if (pin->nc_origin == NcOrigin::ImportedUnconnected) ++orph_imported;
|
|
else if (pin->nc_origin == NcOrigin::DroppedSingleton) ++orph_dropped;
|
|
}
|
|
Print("verify: " + std::to_string(orph_imported + orph_dropped)
|
|
+ " orphan pin(s) at import ("
|
|
+ std::to_string(orph_imported) + " imported NC, "
|
|
+ std::to_string(orph_dropped) + " dropped singleton).");
|
|
}, 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}},
|
|
[this](const std::vector<std::string> &args) {
|
|
if (!sys) { Print("no system: run 'new' first."); return; }
|
|
|
|
if (args.empty()) {
|
|
net_modules.clear();
|
|
for (auto &m : *sys->modules()) net_modules.push_back(m.first);
|
|
std::sort(net_modules.begin(), net_modules.end(), NaturalLess);
|
|
if (net_modules.empty()) { Print("no modules loaded."); return; }
|
|
net_module_idx = 0;
|
|
net_sig_filter.clear();
|
|
net_sig_idx = 0;
|
|
net_focus_idx = 0;
|
|
screen_idx = 5;
|
|
return;
|
|
}
|
|
|
|
if (args.size() != 2) {
|
|
Print("usage: net <module> <signal> (or no args for interactive)");
|
|
return;
|
|
}
|
|
|
|
Module *mod;
|
|
try { mod = sys->modules()->get(args[0]); }
|
|
catch (const std::exception &) {
|
|
Print("unknown module: " + args[0]); return;
|
|
}
|
|
Signal *sig;
|
|
try { sig = mod->signals->get(args[1]); }
|
|
catch (const std::exception &) {
|
|
Print("unknown signal: " + mod->name + "/" + args[1]); return;
|
|
}
|
|
Net n = find_net(sys.get(), mod, sig);
|
|
SignalType dom;
|
|
bool ok = net_type_consistent(n, dom);
|
|
Print("net containing " + mod->name + "/" + sig->name
|
|
+ " — " + std::to_string(n.members.size()) + " signal(s)"
|
|
+ (ok ? "" : " [INCONSISTENT]")
|
|
+ ", dominant: " + signal_type_name(dom));
|
|
for (const auto &mp : n.members) {
|
|
Print(" " + mp.first->name + "/" + mp.second->name
|
|
+ " (" + signal_type_name(mp.second->type) + ")");
|
|
}
|
|
},
|
|
/*prompt_for_missing=*/ false,
|
|
"show all signals reachable from <module>/<signal> through connections "
|
|
"(interactive screen if no args)",
|
|
/*scriptable=*/ true,
|
|
/*interactive=*/ true,
|
|
};
|
|
|
|
commands["set-signal-type"] = {
|
|
{{"module", Completion::None},
|
|
{"signal name", Completion::None},
|
|
{"type [power|gnd|other]", Completion::None}},
|
|
[this](const std::vector<std::string> &args) {
|
|
if (!sys) { Print("no system: run 'new' first."); return; }
|
|
Module *mod;
|
|
try { mod = sys->modules()->get(args[0]); }
|
|
catch (const std::exception &) {
|
|
Print("unknown module: " + args[0]); return;
|
|
}
|
|
Signal *sig;
|
|
try { sig = mod->signals->get(args[1]); }
|
|
catch (const std::exception &) {
|
|
Print("unknown signal: " + mod->name + "/" + args[1]); return;
|
|
}
|
|
SignalType t;
|
|
if (!signal_type_from_name(args[2], t)) {
|
|
Print("type must be one of: power, gnd, other (got: " + args[2] + ")");
|
|
return;
|
|
}
|
|
sig->type = t;
|
|
Print(mod->name + "/" + sig->name + ": signal type = "
|
|
+ signal_type_name(t));
|
|
},
|
|
/*prompt_for_missing=*/ true,
|
|
"override the auto-detected signal type (power | gnd | other)",
|
|
};
|
|
|
|
commands["set-type"] = {
|
|
{{"module", Completion::None},
|
|
{"part (name or pattern)", Completion::None},
|
|
{"connector type (free string, e.g. vpx-bp, vpx-payload)", Completion::None}},
|
|
[this](const std::vector<std::string> &args) {
|
|
if (!sys) { Print("no system: run 'new' first."); return; }
|
|
|
|
if (args.empty()) {
|
|
settype_modules.clear();
|
|
for (auto &m : *sys->modules()) settype_modules.push_back(m.first);
|
|
std::sort(settype_modules.begin(), settype_modules.end(), NaturalLess);
|
|
if (settype_modules.empty()) { Print("no modules loaded."); return; }
|
|
settype_m_idx = 0;
|
|
settype_p_filter.clear();
|
|
settype_p_idx = 0;
|
|
settype_type.clear();
|
|
settype_status.clear();
|
|
settype_focus_idx = 0;
|
|
screen_idx = 3;
|
|
return;
|
|
}
|
|
|
|
if (args.size() != 3) {
|
|
Print("usage: set-type <module> <part> <kind> (or no args for interactive)");
|
|
return;
|
|
}
|
|
|
|
Module *mod;
|
|
try { mod = sys->modules()->get(args[0]); }
|
|
catch (const std::exception &) {
|
|
Print("unknown module: " + args[0]); return;
|
|
}
|
|
Part *prt = nullptr;
|
|
try { prt = mod->get(args[1]); }
|
|
catch (const std::exception &) {
|
|
std::string needle = ToLower(args[1]);
|
|
std::vector<Part *> matches;
|
|
for (auto &p : *mod)
|
|
if (ToLower(p.first).find(needle) != std::string::npos)
|
|
matches.push_back(p.second);
|
|
if (matches.size() == 1) prt = matches[0];
|
|
else {
|
|
Print(std::to_string(matches.size())
|
|
+ " match(es) for part '" + args[1] + "' in " + mod->name);
|
|
return;
|
|
}
|
|
}
|
|
std::string err = ValidatePartForKind(prt, args[2]);
|
|
if (!err.empty()) {
|
|
Print("set-type refused: " + err);
|
|
return;
|
|
}
|
|
prt->connector_type = args[2];
|
|
int filled = FillPartFromLayout(prt, args[2]);
|
|
for (auto &kv : *prt)
|
|
kv.second->expected_signal_type = pin_role(args[2], kv.first);
|
|
Print(mod->name + "/" + prt->name + ": connector_type = "
|
|
+ (args[2].empty() ? "(none)" : args[2]));
|
|
if (filled > 0)
|
|
Print("set-type: materialised " + std::to_string(filled)
|
|
+ " NC pin(s) from connector layout");
|
|
},
|
|
/*prompt_for_missing=*/ false,
|
|
"tag a part's connector type for transform lookup",
|
|
/*scriptable=*/ true,
|
|
/*interactive=*/ true,
|
|
};
|
|
|
|
commands["connect"] = {
|
|
{{"module1", Completion::None},
|
|
{"part1 (name or pattern)", Completion::None},
|
|
{"module2", Completion::None},
|
|
{"part2 (name or pattern)", Completion::None}},
|
|
[this](const std::vector<std::string> &args) {
|
|
if (!sys) { Print("no system: run 'new' first."); return; }
|
|
|
|
if (args.empty()) {
|
|
connect_modules.clear();
|
|
for (auto &m : *sys->modules()) connect_modules.push_back(m.first);
|
|
std::sort(connect_modules.begin(), connect_modules.end(), NaturalLess);
|
|
if (connect_modules.empty()) { Print("no modules loaded."); return; }
|
|
connect_m1_idx = 0;
|
|
connect_m2_idx = std::min(1, (int)connect_modules.size() - 1);
|
|
connect_p1_filter.clear();
|
|
connect_p2_filter.clear();
|
|
connect_p1_idx = 0;
|
|
connect_p2_idx = 0;
|
|
connect_focus_idx = 0;
|
|
screen_idx = 2;
|
|
return;
|
|
}
|
|
|
|
if (args.size() != 4) {
|
|
Print("usage: connect <m1> <p1> <m2> <p2> (or no args for interactive)");
|
|
return;
|
|
}
|
|
|
|
auto resolve_module = [this](const std::string &name)
|
|
-> std::pair<Module*, std::vector<std::string>> {
|
|
try { return {sys->modules()->get(name), {}}; }
|
|
catch (const std::exception &) {}
|
|
std::string needle = ToLower(name);
|
|
std::vector<Module*> matches;
|
|
std::vector<std::string> names;
|
|
for (auto &m : *sys->modules()) {
|
|
if (ToLower(m.first).find(needle) != std::string::npos) {
|
|
matches.push_back(m.second);
|
|
names.push_back(m.first);
|
|
}
|
|
}
|
|
if (matches.size() == 1) return {matches[0], {}};
|
|
return {nullptr, names};
|
|
};
|
|
|
|
auto resolve_part = [](Module *mod, const std::string &name)
|
|
-> std::pair<Part*, std::vector<std::string>> {
|
|
try { return {mod->get(name), {}}; }
|
|
catch (const std::exception &) {}
|
|
std::string needle = ToLower(name);
|
|
std::vector<Part*> matches;
|
|
std::vector<std::string> names;
|
|
for (auto &p : *mod) {
|
|
if (ToLower(p.first).find(needle) != std::string::npos) {
|
|
matches.push_back(p.second);
|
|
names.push_back(p.first);
|
|
}
|
|
}
|
|
if (matches.size() == 1) return {matches[0], {}};
|
|
return {nullptr, names};
|
|
};
|
|
|
|
auto report_ambiguous = [this](const std::string &what,
|
|
const std::string &needle,
|
|
const std::vector<std::string> &names) {
|
|
if (names.empty()) {
|
|
Print(what + " not found: " + needle);
|
|
} else {
|
|
Print(what + " ambiguous for '" + needle + "': "
|
|
+ std::to_string(names.size()) + " match(es)");
|
|
int shown = 0;
|
|
for (const auto &n : names) {
|
|
if (shown++ >= 8) { Print(" …"); break; }
|
|
Print(" " + n);
|
|
}
|
|
}
|
|
};
|
|
|
|
auto [m1, m1_alts] = resolve_module(args[0]);
|
|
if (!m1) { report_ambiguous("module", args[0], m1_alts); return; }
|
|
auto [p1, p1_alts] = resolve_part(m1, args[1]);
|
|
if (!p1) { report_ambiguous("part in " + m1->name, args[1], p1_alts); return; }
|
|
auto [m2, m2_alts] = resolve_module(args[2]);
|
|
if (!m2) { report_ambiguous("module", args[2], m2_alts); return; }
|
|
auto [p2, p2_alts] = resolve_part(m2, args[3]);
|
|
if (!p2) { report_ambiguous("part in " + m2->name, args[3], p2_alts); return; }
|
|
|
|
auto ® = TransformRegistry::get();
|
|
Transform *t = reg.lookup(p1->connector_type, p2->connector_type);
|
|
bool both_empty = p1->connector_type.empty() && p2->connector_type.empty();
|
|
if (t == reg.identity()) {
|
|
if (!both_empty) {
|
|
Print("connect refused: no transform for types '"
|
|
+ (p1->connector_type.empty() ? "(none)" : p1->connector_type)
|
|
+ "' ↔ '"
|
|
+ (p2->connector_type.empty() ? "(none)" : p2->connector_type)
|
|
+ "'. Set matching types via 'set-type' first.");
|
|
return;
|
|
}
|
|
std::string info;
|
|
std::string err = CheckIdentityCompatible(p1, p2, &info);
|
|
if (!err.empty()) {
|
|
Print("connect refused: " + err);
|
|
return;
|
|
}
|
|
if (!info.empty()) {
|
|
int added = FillIdentityNCs(p1, p2);
|
|
Print("connect: " + info);
|
|
if (added > 0)
|
|
Print("connect: materialised " + std::to_string(added)
|
|
+ " NC pin(s) so both sides match");
|
|
}
|
|
}
|
|
auto pin_map = t->apply(p1, p2);
|
|
|
|
std::string conn_name = m1->name + "/" + p1->name
|
|
+ " <-> " + m2->name + "/" + p2->name;
|
|
try {
|
|
Connection *c = new Connection(conn_name, m1, p1, m2, p2);
|
|
c->transform_name = t->name;
|
|
c->pin_map = std::move(pin_map);
|
|
sys->connections()->add(c);
|
|
Print("connected: " + conn_name
|
|
+ " via " + t->name
|
|
+ " (" + std::to_string(c->pin_map.size()) + " wires)");
|
|
} catch (const std::exception &e) {
|
|
Print(std::string("connect failed: ") + e.what());
|
|
}
|
|
},
|
|
/*prompt_for_missing=*/ false,
|
|
"connect a part across two modules (interactive screen if no args)",
|
|
/*scriptable=*/ true,
|
|
/*interactive=*/ true,
|
|
};
|
|
|
|
commands["explore"] = { {}, [this](auto &) {
|
|
if (!sys) { Print("no system: run 'new' first."); return; }
|
|
explore_modules.clear();
|
|
for (auto &m : *sys->modules()) explore_modules.push_back(m.first);
|
|
std::sort(explore_modules.begin(), explore_modules.end(), NaturalLess);
|
|
if (explore_modules.empty()) { Print("no modules loaded."); return; }
|
|
explore_module_idx = 0;
|
|
explore_type_idx = 0;
|
|
explore_child_idx = 0;
|
|
explore_detail_idx = 0;
|
|
explore_child_filter.clear();
|
|
explore_detail_filter.clear();
|
|
explore_focus_idx = 0;
|
|
screen_idx = 4;
|
|
}, true, "browse modules → parts/signals/connections → details (interactive)",
|
|
/*scriptable=*/ false,
|
|
/*interactive=*/ true };
|
|
|
|
commands["search"] = {
|
|
{{"module", Completion::None},
|
|
{"kind [parts|signals]", Completion::None},
|
|
{"pattern", Completion::None}},
|
|
[this](const std::vector<std::string> &args) {
|
|
if (!sys) { Print("no system: run 'new' first."); return; }
|
|
|
|
if (args.empty()) {
|
|
search_modules.clear();
|
|
for (auto &m : *sys->modules()) search_modules.push_back(m.first);
|
|
std::sort(search_modules.begin(), search_modules.end(), NaturalLess);
|
|
if (search_modules.empty()) { Print("no modules loaded."); return; }
|
|
search_module_idx = 0;
|
|
search_type_idx = 0;
|
|
search_query.clear();
|
|
search_focus_idx = 0;
|
|
screen_idx = 1;
|
|
return;
|
|
}
|
|
|
|
if (args.size() != 3) {
|
|
Print("usage: search <module> <parts|signals> <pattern>");
|
|
Print(" search (interactive)");
|
|
return;
|
|
}
|
|
|
|
Module *mod;
|
|
try { mod = sys->modules()->get(args[0]); }
|
|
catch (const std::exception &) {
|
|
Print("unknown module: " + args[0]); return;
|
|
}
|
|
|
|
std::string kind = ToLower(args[1]);
|
|
std::string needle = ToLower(args[2]);
|
|
|
|
std::vector<std::pair<std::string, size_t>> hits;
|
|
if (kind == "parts" || kind == "part") {
|
|
for (auto &pkv : *mod)
|
|
if (ToLower(pkv.first).find(needle) != std::string::npos)
|
|
hits.emplace_back(pkv.first, pkv.second->size());
|
|
} else if (kind == "signals" || kind == "signal") {
|
|
for (auto &skv : *mod->signals)
|
|
if (ToLower(skv.first).find(needle) != std::string::npos)
|
|
hits.emplace_back(skv.first, skv.second->size());
|
|
} else {
|
|
Print("kind must be 'parts' or 'signals' (got: " + args[1] + ")");
|
|
return;
|
|
}
|
|
std::sort(hits.begin(), hits.end(),
|
|
[](const auto &a, const auto &b) { return NaturalLess(a.first, b.first); });
|
|
for (const auto &h : hits) {
|
|
Print(" " + args[0] + "/" + h.first
|
|
+ " (" + std::to_string(h.second) + " pins)");
|
|
}
|
|
Print(std::to_string(hits.size()) + " match(es).");
|
|
},
|
|
/*prompt_for_missing=*/ false,
|
|
"list parts/signals matching a pattern (interactive screen if no args)",
|
|
/*scriptable=*/ true,
|
|
/*interactive=*/ true,
|
|
};
|
|
|
|
commands["duplicate"] = {
|
|
{{"source module", Completion::None},
|
|
{"new module name", Completion::None}},
|
|
[this](const std::vector<std::string> &args) {
|
|
if (!sys) { Print("no system: run 'new' first."); return; }
|
|
|
|
Module *src;
|
|
try { src = sys->modules()->get(args[0]); }
|
|
catch (const std::exception &) {
|
|
Print("unknown module: " + args[0]); return;
|
|
}
|
|
if (sys->modules()->exists(args[1])) {
|
|
Print("duplicate refused: module '" + args[1] + "' already exists.");
|
|
return;
|
|
}
|
|
|
|
Module *dst = new Module(args[1]);
|
|
|
|
// 1. Copy signals (preserve type overrides).
|
|
for (auto &skv : *src->signals) {
|
|
Signal *ss = skv.second;
|
|
Signal *ds = new Signal(ss->name);
|
|
ds->type = ss->type;
|
|
dst->signals->add(ds);
|
|
}
|
|
|
|
// 2. Copy parts, pins, and re-wire pin→signal.
|
|
for (auto &pkv : *src) {
|
|
Part *sp = pkv.second;
|
|
Part *dp = new Part(sp->name);
|
|
dp->connector_type = sp->connector_type;
|
|
for (auto &nkv : *sp) {
|
|
Pin *sn = nkv.second;
|
|
Pin *dn = new Pin(sn->name);
|
|
dn->expected_signal_type = sn->expected_signal_type;
|
|
dn->nc_origin = sn->nc_origin;
|
|
dp->add(dn);
|
|
if (sn->signal()) {
|
|
Signal *ds = dst->signals->get(sn->signal()->name);
|
|
ds->add(dn);
|
|
dn->connect(ds);
|
|
}
|
|
}
|
|
dst->add(dp);
|
|
}
|
|
|
|
sys->modules()->add(dst);
|
|
Print("duplicate: '" + args[0] + "' → '" + args[1] + "'"
|
|
+ " (" + std::to_string(dst->size()) + " part(s), "
|
|
+ std::to_string(dst->signals->size()) + " signal(s))");
|
|
},
|
|
/*prompt_for_missing=*/ true,
|
|
"clone a module under a new name (parts, pins, signals; no connections)",
|
|
};
|
|
}
|