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    #[allow(dead_code)]
229    pub connect: Option<String>, // "M2:G"
230    #[serde(default)]
231    pub x: i64,
232    #[serde(default)]
233    pub y: i64,
234}
235
236pub fn build(spec_path: &str) -> Result<Value> {
237    let spec_str = fs::read_to_string(spec_path)
238        .map_err(|e| VirtuosoError::Config(format!("Cannot read spec file {spec_path}: {e}")))?;
239    let spec: SchematicSpec = serde_json::from_str(&spec_str)
240        .map_err(|e| VirtuosoError::Config(format!("Invalid spec JSON: {e}")))?;
241
242    let client = VirtuosoClient::from_env()?;
243
244    // 1. Open/create cellview
245    let open_skill =
246        client
247            .schematic
248            .open_cellview(&spec.target.lib, &spec.target.cell, &spec.target.view);
249    let r = client.execute_skill(&open_skill, None)?;
250    if !r.skill_ok() {
251        return Err(VirtuosoError::Execution(format!(
252            "Failed to open cellview: {}",
253            r.output
254        )));
255    }
256
257    // 2. Place instances + set params
258    let mut ed = SchematicEditor::new(&client);
259    for inst in &spec.instances {
260        let (lib, cell) = inst.master.split_once('/').ok_or_else(|| {
261            VirtuosoError::Config(format!(
262                "Instance {} master '{}' must be lib/cell",
263                inst.name, inst.master
264            ))
265        })?;
266        ed.add_instance(
267            lib,
268            cell,
269            "symbol",
270            &inst.name,
271            (inst.x, inst.y),
272            inst.orient.as_str(),
273        );
274        for (k, v) in &inst.params {
275            ed.set_param(&inst.name, k, v);
276        }
277    }
278    let r = ed.execute()?;
279    if !r.skill_ok() {
280        return Err(VirtuosoError::Execution(format!(
281            "Failed to place instances: {}",
282            r.output
283        )));
284    }
285
286    // 3. Connections — generate .il script with RB_connectTerminal calls
287    //    Bridge has limitations with complex SKILL, so we write to file and load().
288    {
289        let mut assignments: Vec<(String, String, String)> = Vec::new();
290        for c in &spec.connections {
291            let (i1, t1) = c.from.split_once(':').ok_or_else(|| {
292                VirtuosoError::Config(format!("Bad from '{}' in connection", c.from))
293            })?;
294            let (i2, t2) = c
295                .to
296                .split_once(':')
297                .ok_or_else(|| VirtuosoError::Config(format!("Bad to '{}' in connection", c.to)))?;
298            assignments.push((i1.into(), t1.into(), c.net.clone()));
299            assignments.push((i2.into(), t2.into(), c.net.clone()));
300        }
301        for g in &spec.globals {
302            for inst_term in &g.insts {
303                let (inst, term) = inst_term.split_once(':').ok_or_else(|| {
304                    VirtuosoError::Config(format!("Bad global '{}' in {}", inst_term, g.net))
305                })?;
306                assignments.push((inst.into(), term.into(), g.net.clone()));
307            }
308        }
309
310        // Load the RB_connectTerminal helper procedure
311        let helper_path = "/tmp/rb_schematic_helper.il";
312        fs::write(
313            helper_path,
314            include_str!("../../resources/rb_connect_terminal.il"),
315        )
316        .map_err(|e| VirtuosoError::Config(format!("Cannot write helper: {e}")))?;
317        let r = client.execute_skill(&format!(r#"load("{helper_path}")"#), None)?;
318        if !r.skill_ok() {
319            return Err(VirtuosoError::Execution(format!(
320                "Failed to load connection helper: {}",
321                r.output
322            )));
323        }
324
325        // Generate connection script
326        let mut lines = vec!["let((cv)".to_string(), "cv = RB_SCH_CV".to_string()];
327        for (inst, term, net) in &assignments {
328            lines.push(format!(
329                r#"RB_connectTerminal(cv "{inst}" "{term}" "{net}")"#
330            ));
331        }
332        lines.push("t)".to_string());
333
334        let script_path = "/tmp/rb_schematic_conn.il";
335        fs::write(script_path, lines.join("\n"))
336            .map_err(|e| VirtuosoError::Config(format!("Cannot write script: {e}")))?;
337        let r = client.execute_skill(&format!(r#"load("{script_path}")"#), None)?;
338        if !r.skill_ok() {
339            return Err(VirtuosoError::Execution(format!(
340                "Failed to create connections: {}",
341                r.output
342            )));
343        }
344    }
345
346    // 4. Pins
347    if !spec.pins.is_empty() {
348        let mut ed = SchematicEditor::new(&client);
349        for p in &spec.pins {
350            ed.add_pin(&p.net, &p.pin_type, (p.x, p.y));
351        }
352        let r = ed.execute()?;
353        if !r.skill_ok() {
354            return Err(VirtuosoError::Execution(format!(
355                "Failed to create pins: {}",
356                r.output
357            )));
358        }
359    }
360
361    // 5. Save + check
362    let save_skill = client.schematic.save();
363    client.execute_skill(&save_skill, None)?;
364    let check_skill = client.schematic.check();
365    let r = client.execute_skill(&check_skill, None)?;
366
367    Ok(json!({
368        "status": "success",
369        "target": format!("{}/{}/{}", spec.target.lib, spec.target.cell, spec.target.view),
370        "instances": spec.instances.len(),
371        "connections": spec.connections.len() + spec.globals.len(),
372        "pins": spec.pins.len(),
373        "check": r.output,
374    }))
375}
376
377// ── Read commands ───────────────────────────────────────────────────
378
379/// Parse SKILL JSON output: bridge returns `"\"[...]\""`  — strip outer quotes, unescape inner.
380/// Returns `Err` if the output cannot be parsed as JSON after unescaping.
381pub fn parse_skill_json(output: &str) -> Result<Value> {
382    // output is like: "\"[{\\\"name\\\":\\\"M1\\\"}]\""
383    // Step 1: strip outer quotes from SKILL string
384    let s = output.trim_matches('"');
385    // Step 2: try parsing directly (works if no extra escaping)
386    if let Ok(v) = serde_json::from_str(s) {
387        return Ok(v);
388    }
389    // Step 3: unescape \" → " and \\\\ → \ then retry
390    let unescaped = s.replace("\\\"", "\"").replace("\\\\", "\\");
391    serde_json::from_str(&unescaped).map_err(|e| {
392        VirtuosoError::Execution(format!(
393            "Failed to parse SKILL JSON output: {e}. Raw: {output}"
394        ))
395    })
396}
397
398pub fn list_instances() -> Result<Value> {
399    let client = VirtuosoClient::from_env()?;
400    let skill = client.schematic.list_instances();
401    let r = client.execute_skill(&skill, None)?;
402    if !r.skill_ok() {
403        return Err(VirtuosoError::Execution(format!(
404            "Failed to list instances: {}",
405            r.output
406        )));
407    }
408    parse_skill_json(&r.output)
409}
410
411pub fn list_nets() -> Result<Value> {
412    let client = VirtuosoClient::from_env()?;
413    let skill = client.schematic.list_nets();
414    let r = client.execute_skill(&skill, None)?;
415    if !r.skill_ok() {
416        return Err(VirtuosoError::Execution(format!(
417            "Failed to list nets: {}",
418            r.output
419        )));
420    }
421    parse_skill_json(&r.output)
422}
423
424pub fn list_pins() -> Result<Value> {
425    let client = VirtuosoClient::from_env()?;
426    let skill = client.schematic.list_pins();
427    let r = client.execute_skill(&skill, None)?;
428    if !r.skill_ok() {
429        return Err(VirtuosoError::Execution(format!(
430            "Failed to list pins: {}",
431            r.output
432        )));
433    }
434    parse_skill_json(&r.output)
435}
436
437pub fn get_params(inst: &str) -> Result<Value> {
438    let client = VirtuosoClient::from_env()?;
439    let skill = client.schematic.get_instance_params(inst);
440    let r = client.execute_skill(&skill, None)?;
441    if !r.skill_ok() {
442        return Err(VirtuosoError::Execution(format!(
443            "Failed to get params for '{}': {}",
444            inst, r.output
445        )));
446    }
447    Ok(json!({"instance": inst, "params": parse_skill_json(&r.output)?}))
448}