Skip to main content

virtuoso_cli/client/
schematic_ops.rs

1use crate::client::bridge::escape_skill_string;
2
3/// SKILL global that holds the currently active schematic cellview.
4///
5/// # Concurrency note
6/// `RB_SCH_CV` is a process-wide SKILL global. Concurrent vcli processes
7/// that call `open_cellview` on *different* cellviews will overwrite each
8/// other's global. For serial scripting this is safe; for parallel use,
9/// callers must serialize schematic commands or use separate Virtuoso sessions.
10const SCH_CV_VAR: &str = "RB_SCH_CV";
11
12/// SKILL guard: checks that the cellview global is bound and still valid,
13/// errors with a helpful message otherwise.
14fn cv_guard() -> String {
15    format!(
16        r#"unless(boundp('{SCH_CV_VAR}) && {SCH_CV_VAR} && dbIsValidObject({SCH_CV_VAR}) error("RB_SCH_CV is not set or invalid — run 'vcli schematic open lib/cell/view' first"))"#
17    )
18}
19
20#[derive(Default)]
21pub struct SchematicOps;
22
23impl SchematicOps {
24    pub fn new() -> Self {
25        Self
26    }
27
28    pub fn create_instance(
29        &self,
30        lib: &str,
31        cell: &str,
32        view: &str,
33        name: &str,
34        origin: (i64, i64),
35        orient: &str,
36    ) -> String {
37        let lib = escape_skill_string(lib);
38        let cell = escape_skill_string(cell);
39        let view = escape_skill_string(view);
40        let name = escape_skill_string(name);
41        let orient = escape_skill_string(orient);
42        let (x, y) = origin;
43        let guard = cv_guard();
44        format!(
45            r#"let((cv master inst) {guard} cv = RB_SCH_CV master = dbOpenCellViewByType("{lib}" "{cell}" "{view}" nil "r") inst = dbCreateInst(cv master "{name}" list({x} {y}) "{orient}" 1) inst)"#
46        )
47    }
48
49    pub fn create_wire(&self, points: &[(i64, i64)], layer: &str, net_name: &str) -> String {
50        let layer = escape_skill_string(layer);
51        let net_name = escape_skill_string(net_name);
52        let pts: String = points
53            .iter()
54            .map(|(x, y)| format!("list({x} {y})"))
55            .collect::<Vec<_>>()
56            .join(" ");
57        let guard = cv_guard();
58        format!(
59            r#"let((cv) {guard} cv = RB_SCH_CV dbCreateWire(cv dbMakeNet(cv "{net_name}") dbFindLayerByName(cv "{layer}") list({pts})))"#
60        )
61    }
62
63    #[allow(dead_code)]
64    pub fn create_wire_between_terms(
65        &self,
66        inst1: &str,
67        _term1: &str,
68        inst2: &str,
69        _term2: &str,
70        net_name: &str,
71    ) -> String {
72        let inst1 = escape_skill_string(inst1);
73        let inst2 = escape_skill_string(inst2);
74        let net_name = escape_skill_string(net_name);
75        let guard = cv_guard();
76        format!(
77            r#"let((cv net) {guard} cv = RB_SCH_CV net = dbMakeNet(cv "{net_name}") dbCreateWire(net dbFindTermByName(cv "{inst1}") dbFindTermByName(cv "{inst2}")))"#
78        )
79    }
80
81    pub fn create_wire_label(&self, net_name: &str, origin: (i64, i64)) -> String {
82        let net_name = escape_skill_string(net_name);
83        let (x, y) = origin;
84        let guard = cv_guard();
85        format!(
86            r#"let((cv net) {guard} cv = RB_SCH_CV net = dbFindNetByName(cv "{net_name}") when(net dbCreateLabel(cv net "{net_name}" list({x} {y}) "centerCenter" "R0" "stick" 0.0625)))"#
87        )
88    }
89
90    pub fn create_pin(&self, net_name: &str, _pin_type: &str, origin: (i64, i64)) -> String {
91        let net_name = escape_skill_string(net_name);
92        let (x, y) = origin;
93        let guard = cv_guard();
94        format!(
95            r#"let((cv net pinInst) {guard} cv = RB_SCH_CV net = dbMakeNet(cv "{net_name}") pinInst = dbCreateInst(cv dbOpenCellViewByType("basic" "ipin" "symbol" nil "r") "PIN_{net_name}" list({x} {y}) "R0" 1) dbCreatePin(net pinInst))"#
96        )
97    }
98
99    pub fn check(&self) -> String {
100        let guard = cv_guard();
101        format!(r#"let((cv) {guard} cv = RB_SCH_CV schCheck(cv))"#)
102    }
103
104    pub fn open_cellview(&self, lib: &str, cell: &str, view: &str) -> String {
105        let lib = escape_skill_string(lib);
106        let cell = escape_skill_string(cell);
107        let view = escape_skill_string(view);
108        // dbOpenCellViewByType with viewType="schematic" mode="a":
109        //   creates cellview if absent, opens for editing (non-interactive)
110        // Store in RB_SCH_CV global for use by subsequent commands
111        format!(r#"RB_SCH_CV = dbOpenCellViewByType("{lib}" "{cell}" "{view}" "schematic" "a")"#)
112    }
113
114    pub fn save(&self) -> String {
115        let guard = cv_guard();
116        format!(r#"let((cv) {guard} cv = RB_SCH_CV dbSave(cv))"#)
117    }
118
119    pub fn set_instance_param(&self, inst_name: &str, param: &str, value: &str) -> String {
120        let inst_name = escape_skill_string(inst_name);
121        let param = escape_skill_string(param);
122        let value = escape_skill_string(value);
123        let guard = cv_guard();
124        format!(
125            r#"let((cv inst) {guard} cv = RB_SCH_CV inst = car(setof(i cv~>instances i~>name == "{inst_name}")) when(inst dbReplaceProp(inst "{param}" "string" "{value}")))"#
126        )
127    }
128
129    // ── Read operations ──────────────────────────────────────────────
130
131    /// List all instances in the open cellview. Returns JSON array via sprintf.
132    pub fn list_instances(&self) -> String {
133        let guard = cv_guard();
134        format!(
135            r#"let((cv out sep lib cell) {guard} cv = RB_SCH_CV out = "[" sep = "" foreach(inst cv~>instances lib = if(inst~>master inst~>master~>libName "?") cell = if(inst~>master inst~>master~>cellName "?") out = strcat(out sep sprintf(nil "{{\"name\":\"%s\",\"master\":\"%s/%s\",\"x\":%g,\"y\":%g}}" inst~>name lib cell car(inst~>xy) cadr(inst~>xy))) sep = ",") strcat(out "]"))"#
136        )
137    }
138
139    /// List all nets in the open cellview. Returns JSON array.
140    pub fn list_nets(&self) -> String {
141        let guard = cv_guard();
142        format!(
143            r#"let((cv out sep) {guard} cv = RB_SCH_CV out = "[" sep = "" foreach(net cv~>nets out = strcat(out sep sprintf(nil "\"%s\"" net~>name)) sep = ",") strcat(out "]"))"#
144        )
145    }
146
147    /// List all pins (terminals) in the open cellview. Returns JSON array.
148    pub fn list_pins(&self) -> String {
149        let guard = cv_guard();
150        format!(
151            r#"let((cv out sep) {guard} cv = RB_SCH_CV out = "[" sep = "" foreach(term cv~>terminals out = strcat(out sep sprintf(nil "{{\"name\":\"%s\",\"direction\":\"%s\"}}" term~>name term~>direction)) sep = ",") strcat(out "]"))"#
152        )
153    }
154
155    /// Get parameters of a specific instance. Returns JSON object.
156    pub fn get_instance_params(&self, inst_name: &str) -> String {
157        let inst_name = escape_skill_string(inst_name);
158        let guard = cv_guard();
159        format!(
160            r#"let((cv inst out sep v) {guard} cv = RB_SCH_CV inst = car(setof(i cv~>instances strcmp(i~>name "{inst_name}")==0)) if(inst then out = "{{" sep = "" foreach(prop inst~>prop when(prop~>name != nil v = prop~>value when(v out = strcat(out sep sprintf(nil "\"%s\":\"%s\"" prop~>name if(stringp(v) v sprintf(nil "%L" v)))) sep = ","))) strcat(out "}}") else "null"))"#
161        )
162    }
163
164    /// Assign net name to instance terminal.
165    /// Finds the instTerm by name and connects it to a named net via dbConnectToNet.
166    /// No wire drawing coordinates needed — purely a logical connection.
167    pub fn assign_net(&self, inst_name: &str, term_name: &str, net_name: &str) -> String {
168        let inst_name = escape_skill_string(inst_name);
169        let term_name = escape_skill_string(term_name);
170        let net_name = escape_skill_string(net_name);
171        format!(
172            r#"let((cv inst iterm net) cv = RB_SCH_CV inst = car(setof(i cv~>instances strcmp(i~>name "{inst_name}")==0)) iterm = car(setof(x inst~>instTerms strcmp(x~>name "{term_name}")==0)) net = dbMakeNet(cv "{net_name}") when(iterm dbConnectToNet(iterm net)))"#
173        )
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    fn ops() -> SchematicOps {
182        SchematicOps::new()
183    }
184
185    #[test]
186    fn create_instance_uses_orient() {
187        let s = ops().create_instance("analogLib", "nmos4", "symbol", "M1", (100, 200), "MY");
188        assert!(s.contains("\"MY\""), "orient must be in SKILL: {s}");
189        assert!(
190            s.contains("100") && s.contains("200"),
191            "origin must be in SKILL: {s}"
192        );
193        assert!(s.contains("\"M1\""), "instance name must be quoted: {s}");
194    }
195
196    #[test]
197    fn create_instance_default_orient() {
198        let s = ops().create_instance("lib", "cell", "symbol", "X0", (0, 0), "R0");
199        assert!(s.contains("\"R0\""), "{s}");
200    }
201
202    #[test]
203    fn assign_net_uses_dbconnect() {
204        let s = ops().assign_net("M1", "G", "VIN");
205        assert!(s.contains("dbConnectToNet"), "must use dbConnectToNet: {s}");
206        assert!(
207            !s.contains("schCreateWire"),
208            "must not use schCreateWire: {s}"
209        );
210        assert!(
211            !s.contains("0 0"),
212            "hardcoded coordinates must be gone: {s}"
213        );
214    }
215
216    #[test]
217    fn assign_net_escapes_names() {
218        let s = ops().assign_net(r#"M"1"#, "D", "VDD");
219        assert!(s.contains(r#"M\"1"#), "inst name must be escaped: {s}");
220    }
221
222    #[test]
223    fn open_cellview_sets_global() {
224        let s = ops().open_cellview("myLib", "myCell", "schematic");
225        assert!(s.starts_with("RB_SCH_CV ="), "{s}");
226        assert!(s.contains("\"myLib\"") && s.contains("\"myCell\""), "{s}");
227    }
228
229    #[test]
230    fn cv_guard_is_injected_in_write_ops() {
231        let s = ops().create_wire(&[(0, 0), (10, 10)], "wire", "VDD");
232        assert!(s.contains("dbIsValidObject"), "guard must be present: {s}");
233        assert!(s.contains("dbCreateWire"), "{s}");
234    }
235
236    #[test]
237    fn create_wire_label_contains_guard() {
238        let s = ops().create_wire_label("GND", (50, 50));
239        assert!(s.contains("dbIsValidObject"), "{s}");
240    }
241
242    #[test]
243    fn save_contains_guard() {
244        let s = ops().save();
245        assert!(s.contains("dbIsValidObject"), "{s}");
246        assert!(s.contains("dbSave"), "{s}");
247    }
248}