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