#include "core/app/script.hpp" #include "core/app/connect.hpp" #include "core/app/edit.hpp" #include "core/app/export.hpp" #include "core/app/load.hpp" #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/persist.hpp" #include "core/domain/signal_type.hpp" #include "core/domain/signals.hpp" #include "core/domain/system.hpp" #include #include #include #include #include #include namespace app { namespace { // Whitespace split with "double quotes" grouping — same rules as the TUI shell. std::vector tokenize(const std::string &s) { std::vector out; std::string cur; bool in_q = false; bool has = false; for (char c : s) { if (c == '"') { in_q = !in_q; has = true; continue; } if (!in_q && std::isspace((unsigned char)c)) { if (has) { out.push_back(std::move(cur)); cur.clear(); has = false; } } else { cur.push_back(c); has = true; } } if (has) out.push_back(std::move(cur)); return out; } // One script execution: holds the variable table, the System reference (so new/ // restore can replace it) and the output stream. class Runner { public: Runner(std::unique_ptr &sys, std::ostream &out) : sys_(sys), out_(out) {} // Run a file; `opened` reports whether it could be opened. Returns the count // of effective lines; accumulates command errors into `r.errors`. int run_file(const std::string &path, int depth, ScriptResult &r, bool &opened) { opened = false; if (depth > 32) { emit("source: nesting too deep, skipping " + path); return 0; } std::ifstream f(path); if (!f) return 0; opened = true; int count = 0; std::string line; while (std::getline(f, line)) { std::size_t s = line.find_first_not_of(" \t"); if (s == std::string::npos) continue; if (line[s] == '#') continue; std::string t = line.substr(s); while (!t.empty() && std::isspace((unsigned char)t.back())) t.pop_back(); if (t.empty()) continue; ++count; if (!exec(t, depth, r)) ++r.errors; } return count; } private: void emit(const std::string &line) { out_ << line << '\n'; } // $name / ${name} → variable value; unknown names kept literally. std::string expand(const std::string &s) const { std::string out; std::size_t i = 0; while (i < s.size()) { if (s[i] != '$') { out.push_back(s[i++]); continue; } std::size_t j = i + 1; bool braces = (j < s.size() && s[j] == '{'); if (braces) ++j; std::size_t start = j; while (j < s.size() && (std::isalnum((unsigned char)s[j]) || s[j] == '_')) ++j; std::string name = s.substr(start, j - start); if (braces) { if (j >= s.size() || s[j] != '}') { out.push_back('$'); ++i; continue; } ++j; } if (name.empty()) { out.push_back('$'); ++i; continue; } auto it = vars_.find(name); if (it != vars_.end()) out += it->second; else out += s.substr(i, j - i); i = j; } return out; } Module *resolve_module(const std::string &name) { try { return sys_->modules()->get(name); } catch (const std::exception &) { emit("unknown module: " + name); return nullptr; } } Part *resolve_part(Module *m, const std::string &name) { try { return m->get(name); } catch (const std::exception &) { emit("part in " + m->name + " not found: " + name); return nullptr; } } void render_verify() { VerifyReport r = verify(sys_.get()); for (const auto &m : r.role_mismatches) emit(" " + m.module + "/" + m.part + "/" + m.pin + ": expected " + signal_type_name(m.expected) + ", got " + signal_type_name(m.actual) + " (signal: " + m.signal + ")"); emit("verify: " + std::to_string(r.role_mismatches.size()) + " local mismatch(es) over " + std::to_string(r.typed_pins) + " typed pin(s)."); for (const auto &ni : r.net_inconsistencies) { std::string line = " net mixes Power and GndShield:"; for (const auto &mem : ni.members) line += " " + mem.module + "/" + mem.signal + "(" + signal_type_name(mem.type) + ")"; emit(line); } emit("verify: " + std::to_string(r.net_inconsistencies.size()) + " inconsistent net(s) over " + std::to_string(r.bridged_nets) + " bridged net(s) (" + std::to_string(r.total_nets) + " total)."); emit("verify: " + std::to_string(r.orphan_total()) + " orphan pin(s) at import (" + std::to_string(r.orphan_imported) + " imported NC, " + std::to_string(r.orphan_dropped) + " dropped singleton)."); auto grp = [&](const std::vector &v, const char *tail) { for (const auto &a : v) emit(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); emit("verify: " + std::to_string(v.size()) + tail); }; grp(r.pin_anomalies, " model-driven pin anomaly(ies)."); grp(r.jtag_anomalies, " JTAG chain anomaly(ies)."); grp(r.conflict_anomalies, " source-conflict(s)."); grp(r.completeness_anomalies, " BSDL completeness issue(s)."); } // Execute one already-trimmed line. Returns false on a hard error. bool exec(const std::string &raw, int depth, ScriptResult &top) { std::vector tok = tokenize(raw); if (tok.empty()) return true; const std::string cmd = tok[0]; std::vector a; for (std::size_t i = 1; i < tok.size(); ++i) a.push_back(expand(tok[i])); auto need = [&](std::size_t n) { if (a.size() == n) return true; emit(cmd + ": expected " + std::to_string(n) + " argument(s)"); return false; }; if (cmd == "set") { if (a.size() != 2) { emit("set: usage: set "); return false; } vars_[a[0]] = a[1]; return true; } if (cmd == "new") { sys_ = std::make_unique(); emit("system created."); return true; } if (cmd == "load") { if (!need(3)) return false; ImportType t; if (!import_type_from_name(a[2], t)) { emit("unknown import type: " + a[2]); return false; } LoadResult r = load_module(sys_.get(), a[0], a[1], t); if (!r.ok) { emit("load failed: " + r.error); return false; } emit("loaded '" + a[0] + "' from " + a[1]); emit(" parts: " + std::to_string(r.parts)); emit(" signals: " + std::to_string(r.signals) + (r.dropped ? " (dropped " + std::to_string(r.dropped) + " 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), " + std::to_string(r.mgmt) + " power-management (control/measure — kept as Other)"); return true; } if (cmd == "connect" || cmd == "plug") { if (!need(4)) return false; Module *m1 = resolve_module(a[0]); if (!m1) return false; Part *p1 = resolve_part(m1, a[1]); if (!p1) return false; Module *m2 = resolve_module(a[2]); if (!m2) return false; Part *p2 = resolve_part(m2, a[3]); if (!p2) return false; ConnectResult r = connect_parts(sys_.get(), m1, p1, m2, p2); if (r.refused) { emit("connect refused: " + r.error); return false; } if (!r.identity_info.empty()) { emit("connect: " + r.identity_info); if (r.nc_added > 0) emit("connect: added " + std::to_string(r.nc_added) + " NC pin(s) so both sides match"); } if (!r.ok) { emit("connect failed: " + r.error); return false; } emit("connected: " + r.connection_name + " via " + r.transform_name + " (" + std::to_string(r.wires) + " wires)"); return true; } if (cmd == "set-connector-type") { if (!need(3)) return false; Module *m = resolve_module(a[0]); if (!m) return false; Part *p = resolve_part(m, a[1]); if (!p) return false; SetConnectorTypeResult r = set_connector_type(p, a[2]); if (!r.ok) { emit("set-connector-type refused: " + r.error); return false; } emit(m->name + "/" + p->name + ": connector_type = " + (a[2].empty() ? "(none)" : a[2])); if (r.materialised > 0) emit("set-connector-type: added " + std::to_string(r.materialised) + " NC pin(s) from the connector layout"); return true; } if (cmd == "set-signal-type") { if (!need(3)) return false; Module *m = resolve_module(a[0]); if (!m) return false; Signal *sig; try { sig = m->signals->get(a[1]); } catch (const std::exception &) { emit("unknown signal: " + m->name + "/" + a[1]); return false; } SetSignalTypeResult r = set_signal_type(sig, a[2]); if (!r.ok) { emit(r.error); return false; } emit(m->name + "/" + sig->name + ": signal type = " + signal_type_name(r.type)); return true; } if (cmd == "attach-bsdl") { if (!need(3)) return false; Module *m = resolve_module(a[0]); if (!m) return false; Part *p = resolve_part(m, a[1]); if (!p) return false; AttachBsdlResult r = attach_bsdl(p, a[2]); if (!r.ok) { emit("attach-bsdl: " + r.error); return false; } emit(m->name + "/" + p->name + ": attached BSDL '" + r.entity + "' — " + std::to_string(r.bound) + "/" + std::to_string(r.ports_total) + " ports bound" + (r.unbound ? (", " + std::to_string(r.unbound) + " unbound") : "")); return true; } if (cmd == "duplicate") { if (!need(2)) return false; DuplicateResult r = duplicate_module(sys_.get(), a[0], a[1]); if (!r.ok) { emit(r.error); return false; } emit("duplicate: '" + a[0] + "' → '" + a[1] + "' (" + std::to_string(r.parts) + " part(s), " + std::to_string(r.signals) + " signal(s))"); return true; } if (cmd == "verify") { render_verify(); return true; } if (cmd == "export") { if (!need(1)) return false; ExportFormat fmt; if (!export_format_from_path(a[0], fmt)) { emit("export: unknown extension (use .csv or .ods): " + a[0]); return false; } ExportResult r = export_connections(sys_.get(), a[0], fmt); if (!r.ok) { emit("export failed: " + r.error); return false; } emit("exported " + std::to_string(r.rows) + " row(s) to " + a[0]); return true; } if (cmd == "save") { if (!need(1)) return false; std::string err; if (!save_system(sys_.get(), a[0], err)) { emit("save failed: " + err); return false; } emit("saved to " + a[0]); return true; } if (cmd == "restore") { if (!need(1)) return false; std::string err; System *fresh = restore_system(a[0], err); if (!fresh) { emit("restore failed: " + err); return false; } sys_.reset(fresh); emit("restored from " + a[0] + " (" + std::to_string(sys_->modules()->size()) + " module(s), " + std::to_string(sys_->connections()->size()) + " connection(s))"); return true; } if (cmd == "source") { if (!need(1)) return false; bool opened; int n = run_file(a[0], depth + 1, top, opened); if (!opened) { emit("source: cannot open " + a[0]); return false; } emit("source: " + a[0] + " (" + std::to_string(n) + " line(s))"); return true; } emit("script: unsupported command '" + cmd + "'"); return false; } std::unique_ptr &sys_; std::ostream &out_; std::map vars_; }; } // namespace ScriptResult run_script(std::unique_ptr &sys, const std::string &path, std::ostream &out) { ScriptResult r; Runner runner(sys, out); bool opened = false; int n = runner.run_file(path, 0, r, opened); if (!opened) { r.error = "cannot open " + path; return r; } r.ok = true; r.lines = n; return r; } } // namespace app