Skip to main content

virtuoso_cli/commands/
schematic.rs

1use std::collections::HashMap;
2use std::fs;
3
4use crate::client::bridge::VirtuosoClient;
5use crate::client::editor::SchematicEditor;
6use crate::error::{Result, VirtuosoError};
7use serde::Deserialize;
8use serde_json::{json, Value};
9
10/// Cadence symbol orientation. Exactly the 8 values SKILL accepts.
11#[derive(Debug, Clone, Copy, Deserialize, clap::ValueEnum)]
12#[clap(rename_all = "verbatim")]
13pub enum Orient {
14    R0,
15    R90,
16    R180,
17    R270,
18    MX,
19    MY,
20    MXR90,
21    MYR90,
22}
23
24impl Orient {
25    pub fn as_str(&self) -> &'static str {
26        match self {
27            Self::R0 => "R0",
28            Self::R90 => "R90",
29            Self::R180 => "R180",
30            Self::R270 => "R270",
31            Self::MX => "MX",
32            Self::MY => "MY",
33            Self::MXR90 => "MXR90",
34            Self::MYR90 => "MYR90",
35        }
36    }
37}
38
39// ── Atomic commands ─────────────────────────────────────────────────
40
41pub fn open(lib: &str, cell: &str, view: &str) -> Result<Value> {
42    let client = VirtuosoClient::from_env()?;
43    let skill = client.schematic.open_cellview(lib, cell, view);
44    let r = client.execute_skill(&skill, None)?;
45    Ok(json!({
46        "status": if r.skill_ok() { "success" } else { "error" },
47        "lib": lib, "cell": cell, "view": view,
48        "output": r.output,
49    }))
50}
51
52pub fn place(
53    master: &str,
54    name: &str,
55    x: i64,
56    y: i64,
57    orient: Orient,
58    params: &[(String, String)],
59) -> Result<Value> {
60    let (lib, cell) = master
61        .split_once('/')
62        .ok_or_else(|| VirtuosoError::Config("--master must be lib/cell format".into()))?;
63    let client = VirtuosoClient::from_env()?;
64    let mut ed = SchematicEditor::new(&client);
65    ed.add_instance(lib, cell, "symbol", name, (x, y), orient.as_str());
66    for (k, v) in params {
67        ed.set_param(name, k, v);
68    }
69    let r = ed.execute()?;
70    Ok(json!({
71        "status": if r.skill_ok() { "success" } else { "error" },
72        "instance": name, "master": master,
73        "output": r.output,
74    }))
75}
76
77pub fn wire_from_strings(net: &str, points: &[String]) -> Result<Value> {
78    let pts: Vec<(i64, i64)> = points
79        .iter()
80        .map(|s| {
81            let (x, y) = s
82                .split_once(',')
83                .ok_or_else(|| VirtuosoError::Config(format!("Point '{s}' must be x,y")))?;
84            Ok((
85                x.parse()
86                    .map_err(|_| VirtuosoError::Config(format!("Bad x: {x}")))?,
87                y.parse()
88                    .map_err(|_| VirtuosoError::Config(format!("Bad y: {y}")))?,
89            ))
90        })
91        .collect::<Result<Vec<_>>>()?;
92    wire(net, &pts)
93}
94
95pub fn wire(net: &str, points: &[(i64, i64)]) -> Result<Value> {
96    let client = VirtuosoClient::from_env()?;
97    let skill = client.schematic.create_wire(points, "wire", net);
98    let r = client.execute_skill(&skill, None)?;
99    Ok(json!({
100        "status": if r.skill_ok() { "success" } else { "error" },
101        "net": net, "output": r.output,
102    }))
103}
104
105pub fn conn(net: &str, from: &str, to: &str) -> Result<Value> {
106    let (inst1, term1) = from
107        .split_once(':')
108        .ok_or_else(|| VirtuosoError::Config("--from must be inst:term format".into()))?;
109    let (inst2, term2) = to
110        .split_once(':')
111        .ok_or_else(|| VirtuosoError::Config("--to must be inst:term format".into()))?;
112    let client = VirtuosoClient::from_env()?;
113    let mut ed = SchematicEditor::new(&client);
114    ed.assign_net(inst1, term1, net);
115    ed.assign_net(inst2, term2, net);
116    let r = ed.execute()?;
117    Ok(json!({
118        "status": if r.skill_ok() { "success" } else { "error" },
119        "net": net, "from": from, "to": to,
120        "output": r.output,
121    }))
122}
123
124pub fn label(net: &str, x: i64, y: i64) -> Result<Value> {
125    let client = VirtuosoClient::from_env()?;
126    let skill = client.schematic.create_wire_label(net, (x, y));
127    let r = client.execute_skill(&skill, None)?;
128    Ok(json!({
129        "status": if r.skill_ok() { "success" } else { "error" },
130        "net": net, "output": r.output,
131    }))
132}
133
134pub fn pin(net: &str, pin_type: &str, x: i64, y: i64) -> Result<Value> {
135    let client = VirtuosoClient::from_env()?;
136    let skill = client.schematic.create_pin(net, pin_type, (x, y));
137    let r = client.execute_skill(&skill, None)?;
138    Ok(json!({
139        "status": if r.skill_ok() { "success" } else { "error" },
140        "net": net, "type": pin_type, "output": r.output,
141    }))
142}
143
144pub fn check() -> Result<Value> {
145    let client = VirtuosoClient::from_env()?;
146    let skill = client.schematic.check();
147    let r = client.execute_skill(&skill, None)?;
148    Ok(json!({
149        "status": if r.skill_ok() { "success" } else { "error" },
150        "output": r.output,
151    }))
152}
153
154pub fn save() -> Result<Value> {
155    let client = VirtuosoClient::from_env()?;
156    let skill = client.schematic.save();
157    let r = client.execute_skill(&skill, None)?;
158    Ok(json!({
159        "status": if r.skill_ok() { "success" } else { "error" },
160        "output": r.output,
161    }))
162}
163
164// ── Build (batch from JSON spec) ────────────────────────────────────
165
166#[derive(Deserialize)]
167pub struct SchematicSpec {
168    pub target: SpecTarget,
169    #[serde(default)]
170    pub instances: Vec<SpecInstance>,
171    #[serde(default)]
172    pub connections: Vec<SpecConnection>,
173    #[serde(default)]
174    pub globals: Vec<SpecGlobal>,
175    #[serde(default)]
176    pub pins: Vec<SpecPin>,
177}
178
179#[derive(Deserialize)]
180pub struct SpecTarget {
181    pub lib: String,
182    pub cell: String,
183    #[serde(default = "default_view")]
184    pub view: String,
185}
186
187fn default_view() -> String {
188    "schematic".into()
189}
190
191#[derive(Deserialize)]
192pub struct SpecInstance {
193    pub name: String,
194    pub master: String, // "lib/cell"
195    #[serde(default)]
196    pub x: i64,
197    #[serde(default)]
198    pub y: i64,
199    #[serde(default = "default_orient")]
200    pub orient: Orient,
201    #[serde(default)]
202    pub params: HashMap<String, String>,
203}
204
205fn default_orient() -> Orient {
206    Orient::R0
207}
208
209#[derive(Deserialize)]
210pub struct SpecConnection {
211    pub net: String,
212    pub from: String, // "inst:term"
213    pub to: String,
214}
215
216#[derive(Deserialize)]
217pub struct SpecGlobal {
218    pub net: String,
219    pub insts: Vec<String>, // ["M5:S", "M5:B"]
220}
221
222#[derive(Deserialize)]
223pub struct SpecPin {
224    pub net: String,
225    #[serde(rename = "type")]
226    pub pin_type: String,
227    #[serde(default)]
228    pub connect: Option<String>, // "M2:G"
229    #[serde(default)]
230    pub x: i64,
231    #[serde(default)]
232    pub y: i64,
233}
234
235pub fn build(spec_path: &str) -> Result<Value> {
236    let spec_str = fs::read_to_string(spec_path)
237        .map_err(|e| VirtuosoError::Config(format!("Cannot read spec file {spec_path}: {e}")))?;
238    let spec: SchematicSpec = serde_json::from_str(&spec_str)
239        .map_err(|e| VirtuosoError::Config(format!("Invalid spec JSON: {e}")))?;
240
241    let client = VirtuosoClient::from_env()?;
242
243    // 1. Open/create cellview
244    let open_skill =
245        client
246            .schematic
247            .open_cellview(&spec.target.lib, &spec.target.cell, &spec.target.view);
248    let r = client.execute_skill(&open_skill, None)?;
249    if !r.skill_ok() {
250        return Err(VirtuosoError::Execution(format!(
251            "Failed to open cellview: {}",
252            r.output
253        )));
254    }
255
256    // 2. Place instances + set params
257    let mut ed = SchematicEditor::new(&client);
258    for inst in &spec.instances {
259        let (lib, cell) = inst.master.split_once('/').ok_or_else(|| {
260            VirtuosoError::Config(format!(
261                "Instance {} master '{}' must be lib/cell",
262                inst.name, inst.master
263            ))
264        })?;
265        ed.add_instance(
266            lib,
267            cell,
268            "symbol",
269            &inst.name,
270            (inst.x, inst.y),
271            inst.orient.as_str(),
272        );
273        for (k, v) in &inst.params {
274            ed.set_param(&inst.name, k, v);
275        }
276    }
277    let r = ed.execute()?;
278    if !r.skill_ok() {
279        return Err(VirtuosoError::Execution(format!(
280            "Failed to place instances: {}",
281            r.output
282        )));
283    }
284
285    // 3. Connections — generate .il script with RB_connectTerminal calls
286    //    Bridge has limitations with complex SKILL, so we write to file and load().
287    {
288        let mut assignments: Vec<(String, String, String)> = Vec::new();
289        for c in &spec.connections {
290            let (i1, t1) = c.from.split_once(':').ok_or_else(|| {
291                VirtuosoError::Config(format!("Bad from '{}' in connection", c.from))
292            })?;
293            let (i2, t2) = c
294                .to
295                .split_once(':')
296                .ok_or_else(|| VirtuosoError::Config(format!("Bad to '{}' in connection", c.to)))?;
297            assignments.push((i1.into(), t1.into(), c.net.clone()));
298            assignments.push((i2.into(), t2.into(), c.net.clone()));
299        }
300        for g in &spec.globals {
301            for inst_term in &g.insts {
302                let (inst, term) = inst_term.split_once(':').ok_or_else(|| {
303                    VirtuosoError::Config(format!("Bad global '{}' in {}", inst_term, g.net))
304                })?;
305                assignments.push((inst.into(), term.into(), g.net.clone()));
306            }
307        }
308
309        // Load the RB_connectTerminal helper procedure
310        let helper_path = "/tmp/rb_schematic_helper.il";
311        fs::write(
312            helper_path,
313            include_str!("../../resources/rb_connect_terminal.il"),
314        )
315        .map_err(|e| VirtuosoError::Config(format!("Cannot write helper: {e}")))?;
316        let r = client.execute_skill(&format!(r#"load("{helper_path}")"#), None)?;
317        if !r.skill_ok() {
318            return Err(VirtuosoError::Execution(format!(
319                "Failed to load connection helper: {}",
320                r.output
321            )));
322        }
323
324        // Generate connection script
325        let mut lines = vec!["let((cv)".to_string(), "cv = RB_SCH_CV".to_string()];
326        for (inst, term, net) in &assignments {
327            lines.push(format!(
328                r#"RB_connectTerminal(cv "{inst}" "{term}" "{net}")"#
329            ));
330        }
331        lines.push("t)".to_string());
332
333        let script_path = "/tmp/rb_schematic_conn.il";
334        fs::write(script_path, lines.join("\n"))
335            .map_err(|e| VirtuosoError::Config(format!("Cannot write script: {e}")))?;
336        let r = client.execute_skill(&format!(r#"load("{script_path}")"#), None)?;
337        if !r.skill_ok() {
338            return Err(VirtuosoError::Execution(format!(
339                "Failed to create connections: {}",
340                r.output
341            )));
342        }
343    }
344
345    // 4. Pins
346    if !spec.pins.is_empty() {
347        let mut ed = SchematicEditor::new(&client);
348        for p in &spec.pins {
349            ed.add_pin(&p.net, &p.pin_type, (p.x, p.y));
350        }
351        let r = ed.execute()?;
352        if !r.skill_ok() {
353            return Err(VirtuosoError::Execution(format!(
354                "Failed to create pins: {}",
355                r.output
356            )));
357        }
358    }
359
360    // 5. Save + check
361    let save_skill = client.schematic.save();
362    client.execute_skill(&save_skill, None)?;
363    let check_skill = client.schematic.check();
364    let r = client.execute_skill(&check_skill, None)?;
365
366    Ok(json!({
367        "status": "success",
368        "target": format!("{}/{}/{}", spec.target.lib, spec.target.cell, spec.target.view),
369        "instances": spec.instances.len(),
370        "connections": spec.connections.len() + spec.globals.len(),
371        "pins": spec.pins.len(),
372        "check": r.output,
373    }))
374}
375
376// ── Read commands ───────────────────────────────────────────────────
377
378/// Parse SKILL JSON output: bridge returns `"\"[...]\""`  — strip outer quotes, unescape inner.
379/// Returns `Err` if the output cannot be parsed as JSON after unescaping.
380pub fn parse_skill_json(output: &str) -> Result<Value> {
381    // output is like: "\"[{\\\"name\\\":\\\"M1\\\"}]\""
382    // Step 1: strip outer quotes from SKILL string
383    let s = output.trim_matches('"');
384    // Step 2: try parsing directly (works if no extra escaping)
385    if let Ok(v) = serde_json::from_str(s) {
386        return Ok(v);
387    }
388    // Step 3: unescape \" → " and \\\\ → \ then retry
389    let unescaped = s.replace("\\\"", "\"").replace("\\\\", "\\");
390    serde_json::from_str(&unescaped).map_err(|e| {
391        VirtuosoError::Execution(format!(
392            "Failed to parse SKILL JSON output: {e}. Raw: {output}"
393        ))
394    })
395}
396
397pub fn list_instances() -> Result<Value> {
398    let client = VirtuosoClient::from_env()?;
399    let skill = client.schematic.list_instances();
400    let r = client.execute_skill(&skill, None)?;
401    if !r.skill_ok() {
402        return Err(VirtuosoError::Execution(format!(
403            "Failed to list instances: {}",
404            r.output
405        )));
406    }
407    parse_skill_json(&r.output)
408}
409
410pub fn list_nets() -> Result<Value> {
411    let client = VirtuosoClient::from_env()?;
412    let skill = client.schematic.list_nets();
413    let r = client.execute_skill(&skill, None)?;
414    if !r.skill_ok() {
415        return Err(VirtuosoError::Execution(format!(
416            "Failed to list nets: {}",
417            r.output
418        )));
419    }
420    parse_skill_json(&r.output)
421}
422
423pub fn list_pins() -> Result<Value> {
424    let client = VirtuosoClient::from_env()?;
425    let skill = client.schematic.list_pins();
426    let r = client.execute_skill(&skill, None)?;
427    if !r.skill_ok() {
428        return Err(VirtuosoError::Execution(format!(
429            "Failed to list pins: {}",
430            r.output
431        )));
432    }
433    parse_skill_json(&r.output)
434}
435
436pub fn get_params(inst: &str) -> Result<Value> {
437    let client = VirtuosoClient::from_env()?;
438    let skill = client.schematic.get_instance_params(inst);
439    let r = client.execute_skill(&skill, None)?;
440    if !r.skill_ok() {
441        return Err(VirtuosoError::Execution(format!(
442            "Failed to get params for '{}': {}",
443            inst, r.output
444        )));
445    }
446    Ok(json!({"instance": inst, "params": parse_skill_json(&r.output)?}))
447}