Skip to main content

virtuoso_cli/commands/
sim.rs

1use crate::client::bridge::VirtuosoClient;
2use crate::error::{Result, VirtuosoError};
3use crate::ocean;
4use crate::ocean::corner::CornerConfig;
5use crate::spectre::jobs::Job;
6use crate::spectre::runner::SpectreSimulator;
7use serde_json::{json, Value};
8use std::collections::HashMap;
9
10pub fn setup(lib: &str, cell: &str, view: &str, simulator: &str) -> Result<Value> {
11    let client = VirtuosoClient::from_env()?;
12    let skill = ocean::setup_skill(lib, cell, view, simulator);
13    let result = client.execute_skill(&skill, None)?;
14
15    if !result.ok() {
16        return Err(VirtuosoError::Execution(result.errors.join("; ")));
17    }
18
19    Ok(json!({
20        "status": "success",
21        "simulator": simulator,
22        "design": { "lib": lib, "cell": cell, "view": view },
23        "results_dir": result.output.trim().trim_matches('"'),
24    }))
25}
26
27pub fn run(analysis: &str, params: &HashMap<String, String>, timeout: u64) -> Result<Value> {
28    let client = VirtuosoClient::from_env()?;
29
30    // Check if resultsDir is set — do NOT override if it is, as changing
31    // resultsDir while an ADE session is active causes run() to silently
32    // return nil (ADE binds the session to a specific results path).
33    let rdir = client.execute_skill("resultsDir()", None)?;
34    let rdir_val = rdir.output.trim().trim_matches('"');
35    if rdir_val == "nil" || rdir_val.is_empty() {
36        return Err(VirtuosoError::Execution(
37            "resultsDir is not set. Run `virtuoso sim setup` first, or open \
38             ADE L for your testbench and run at least one simulation to \
39             establish the session path."
40                .into(),
41        ));
42    }
43
44    // Send analysis setup
45    let analysis_skill = ocean::analysis_skill_simple(analysis, params);
46    let analysis_result = client.execute_skill(&analysis_skill, None)?;
47    if !analysis_result.ok() {
48        return Err(VirtuosoError::Execution(analysis_result.errors.join("; ")));
49    }
50
51    // Send save
52    let _ = client.execute_skill("save('all)", None);
53
54    // Execute run
55    let result = client.execute_skill("run()", Some(timeout))?;
56    if !result.ok() {
57        return Err(VirtuosoError::Execution(result.errors.join("; ")));
58    }
59
60    // Get actual results dir
61    let rdir = client.execute_skill("resultsDir()", None)?;
62    let results_dir = rdir.output.trim().trim_matches('"').to_string();
63
64    // Validate: run() returning nil usually means simulation didn't execute
65    let run_output = result.output.trim().trim_matches('"');
66    if run_output == "nil" {
67        let check =
68            client.execute_skill(&format!(r#"isFile("{results_dir}/psf/spectre.out")"#), None)?;
69        let has_spectre_out = check.output.trim().trim_matches('"');
70        if has_spectre_out == "nil" || has_spectre_out == "0" {
71            return Err(VirtuosoError::Execution(
72                "Simulation failed: run() returned nil and no spectre.out found. \
73                 The netlist may be missing or stale — regenerate via ADE \
74                 (Simulation → Netlist and Run) or `virtuoso sim netlist`."
75                    .into(),
76            ));
77        }
78    }
79
80    Ok(json!({
81        "status": "success",
82        "analysis": analysis,
83        "params": params,
84        "results_dir": results_dir,
85        "execution_time": result.execution_time,
86    }))
87}
88
89/// Reject SKILL expressions that could cause side effects outside of waveform access.
90/// `measure` is intended for read-only PSF queries; block known destructive/execution APIs.
91fn validate_measure_expr(expr: &str) -> Result<()> {
92    // Case-insensitive prefix patterns that indicate non-measurement operations
93    let blocked: &[&str] = &[
94        "system(",
95        "sh(",
96        "ipcbeginprocess(",
97        "ipcwriteprocess(",
98        "ipckillprocess(",
99        "deletefile(",
100        "deletedir(",
101        "copyfile(",
102        "movefile(",
103        "writefile(",
104        "createdir(",
105        "load(",
106        "evalstring(",
107        "hiloaddmenu(",
108    ];
109    let lower = expr.to_lowercase();
110    for pat in blocked {
111        if lower.contains(pat) {
112            return Err(VirtuosoError::Execution(format!(
113                "measure expression contains blocked function '{pat}': \
114                 only waveform access functions are allowed"
115            )));
116        }
117    }
118    Ok(())
119}
120
121pub fn measure(analysis: &str, exprs: &[String]) -> Result<Value> {
122    for expr in exprs {
123        validate_measure_expr(expr)?;
124    }
125
126    let client = VirtuosoClient::from_env()?;
127
128    // Open results from resultsDir PSF and select result type
129    let rdir = client.execute_skill("resultsDir()", None)?;
130    let rdir_val = rdir.output.trim().trim_matches('"');
131    if rdir_val != "nil" && !rdir_val.is_empty() {
132        let open_skill = format!("openResults(\"{rdir_val}/psf\")");
133        let _ = client.execute_skill(&open_skill, None);
134    }
135    let select_skill = format!("selectResult('{analysis})");
136    let _ = client.execute_skill(&select_skill, None);
137
138    // Execute each measure expression individually for reliability
139    let mut measures = Vec::new();
140    for expr in exprs {
141        let result = client.execute_skill(expr, None)?;
142        let value = if result.ok() {
143            result.output.trim().trim_matches('"').to_string()
144        } else {
145            format!("ERROR: {}", result.errors.join("; "))
146        };
147        measures.push(json!({
148            "expr": expr,
149            "value": value,
150        }));
151    }
152
153    // Detect all-nil results and provide diagnostics
154    let all_nil = !measures.is_empty()
155        && measures.iter().all(|m| {
156            m.get("value")
157                .and_then(|v| v.as_str())
158                .map(|s| s == "nil")
159                .unwrap_or(false)
160        });
161
162    let mut warnings: Vec<String> = Vec::new();
163    if all_nil {
164        let rdir_for_check = rdir_val.to_string();
165        let spectre_exists = client
166            .execute_skill(
167                &format!(r#"isFile("{rdir_for_check}/psf/spectre.out")"#),
168                None,
169            )
170            .map(|r| {
171                let v = r.output.trim().trim_matches('"');
172                v != "nil" && v != "0"
173            })
174            .unwrap_or(false);
175
176        if !spectre_exists {
177            warnings.push(
178                "All measurements returned nil. No spectre.out found — simulation \
179                 may not have run. Check netlist with `virtuoso sim netlist`."
180                    .into(),
181            );
182        } else {
183            warnings.push(
184                "All measurements returned nil. Spectre ran but produced no matching \
185                 data — verify signal names match your schematic and that the correct \
186                 analysis type is selected."
187                    .into(),
188            );
189        }
190    }
191
192    Ok(json!({
193        "status": "success",
194        "measures": measures,
195        "warnings": warnings,
196    }))
197}
198
199pub fn sweep(
200    var: &str,
201    from: f64,
202    to: f64,
203    step: f64,
204    analysis: &str,
205    measure_exprs: &[String],
206    timeout: u64,
207) -> Result<Value> {
208    let client = VirtuosoClient::from_env()?;
209
210    // Generate value list
211    let mut values = Vec::new();
212    let mut v = from;
213    while v <= to + step * 0.01 {
214        values.push(v);
215        v += step;
216    }
217
218    let skill = ocean::sweep_skill(var, &values, analysis, measure_exprs);
219    let result = client.execute_skill(&skill, Some(timeout))?;
220
221    if !result.ok() {
222        return Err(VirtuosoError::Execution(result.errors.join("; ")));
223    }
224
225    let parsed = ocean::parse_skill_list(result.output.trim());
226
227    let mut headers = vec![var.to_string()];
228    headers.extend(measure_exprs.iter().cloned());
229
230    let rows: Vec<Value> = parsed
231        .iter()
232        .map(|row| {
233            let mut obj = serde_json::Map::new();
234            for (i, h) in headers.iter().enumerate() {
235                if let Some(val) = row.get(i) {
236                    obj.insert(h.clone(), json!(val));
237                }
238            }
239            Value::Object(obj)
240        })
241        .collect();
242
243    Ok(json!({
244        "status": "success",
245        "variable": var,
246        "points": values.len(),
247        "headers": headers,
248        "data": rows,
249        "execution_time": result.execution_time,
250    }))
251}
252
253pub fn corner(file: &str, timeout: u64) -> Result<Value> {
254    let content = std::fs::read_to_string(file)
255        .map_err(|e| VirtuosoError::NotFound(format!("corner config not found: {file}: {e}")))?;
256
257    let config: CornerConfig = serde_json::from_str(&content)
258        .map_err(|e| VirtuosoError::Config(format!("invalid corner config: {e}")))?;
259
260    let client = VirtuosoClient::from_env()?;
261    let skill = ocean::corner_skill(&config);
262    let result = client.execute_skill(&skill, Some(timeout))?;
263
264    if !result.ok() {
265        return Err(VirtuosoError::Execution(result.errors.join("; ")));
266    }
267
268    let parsed = ocean::parse_skill_list(result.output.trim());
269
270    let mut headers = vec!["corner".to_string(), "temp".to_string()];
271    headers.extend(config.measures.iter().map(|m| m.name.clone()));
272
273    let rows: Vec<Value> = parsed
274        .iter()
275        .map(|row| {
276            let mut obj = serde_json::Map::new();
277            for (i, h) in headers.iter().enumerate() {
278                if let Some(val) = row.get(i) {
279                    obj.insert(h.clone(), json!(val));
280                }
281            }
282            Value::Object(obj)
283        })
284        .collect();
285
286    Ok(json!({
287        "status": "success",
288        "corners": config.corners.len(),
289        "measures": config.measures.len(),
290        "headers": headers,
291        "data": rows,
292        "execution_time": result.execution_time,
293    }))
294}
295
296pub fn results() -> Result<Value> {
297    let client = VirtuosoClient::from_env()?;
298    let result = client.execute_skill("resultsDir()", None)?;
299
300    if !result.ok() {
301        return Err(VirtuosoError::Execution(result.errors.join("; ")));
302    }
303
304    let dir = result.output.trim().trim_matches('"').to_string();
305
306    // Query available result types
307    let types_result = client.execute_skill(
308        &format!(r#"let((dir files) dir="{dir}" when(isDir(dir) files=getDirFiles(dir)) files)"#),
309        None,
310    )?;
311
312    Ok(json!({
313        "status": "success",
314        "results_dir": dir,
315        "contents": types_result.output.trim(),
316    }))
317}
318
319pub fn netlist(recreate: bool) -> Result<Value> {
320    let client = VirtuosoClient::from_env()?;
321
322    // Method 1: Ocean createNetlist
323    let r1 = client.execute_skill(
324        if recreate {
325            "createNetlist(?recreateAll t ?display nil)"
326        } else {
327            "createNetlist(?display nil)"
328        },
329        Some(60),
330    )?;
331    let r1_out = r1.output.trim().trim_matches('"');
332    if r1.ok() && r1_out != "nil" {
333        return Ok(json!({
334            "status": "success",
335            "method": "createNetlist",
336            "output": r1_out,
337        }));
338    }
339
340    // Method 2: ASI session-based netlisting
341    let r2 = client.execute_skill(
342        "asiCreateNetlist(asiGetSession(hiGetCurrentWindow()))",
343        Some(60),
344    )?;
345    let r2_out = r2.output.trim().trim_matches('"');
346    if r2.ok() && r2_out != "nil" {
347        return Ok(json!({
348            "status": "success",
349            "method": "asiCreateNetlist",
350            "output": r2_out,
351        }));
352    }
353
354    Err(VirtuosoError::Execution(
355        "Cannot create netlist programmatically. \
356         Open ADE L for this cell and run Simulation → Netlist and Run."
357            .into(),
358    ))
359}
360
361// ── Async job commands ──────────────────────────────────────────────
362
363pub fn run_async(netlist_path: &str) -> Result<Value> {
364    let content = std::fs::read_to_string(netlist_path)
365        .map_err(|e| VirtuosoError::Config(format!("Cannot read netlist '{netlist_path}': {e}")))?;
366    let sim = SpectreSimulator::from_env()?;
367    let job = sim.run_async(&content)?;
368    Ok(json!({
369        "status": "launched",
370        "job_id": job.id,
371        "pid": job.pid,
372        "netlist": netlist_path,
373    }))
374}
375
376#[cfg(test)]
377mod tests {
378    use super::validate_measure_expr;
379
380    #[test]
381    fn safe_waveform_exprs_are_allowed() {
382        for expr in &[
383            "VT(\"vout\" \"VGS\")",
384            "bandwidth(getData(\"vout\") 3)",
385            "value(getData(\"vout\") 1e-9)",
386            "getData(\"/vout\")",
387            "ymax(getData(\"id\"))",
388            "delay(getData(\"vout\") 0.5)",
389        ] {
390            assert!(
391                validate_measure_expr(expr).is_ok(),
392                "should be allowed: {expr}"
393            );
394        }
395    }
396
397    #[test]
398    fn dangerous_exprs_are_blocked() {
399        let cases = [
400            ("system(\"id\")", "system("),
401            ("sh(\"ls\")", "sh("),
402            ("ipcBeginProcess(\"cmd\")", "ipcbeginprocess("),
403            ("deleteFile(\"/etc/hosts\")", "deletefile("),
404            ("load(\"/tmp/evil.il\")", "load("),
405            ("evalstring(\"getData(1)\")", "evalstring("),
406            // case-insensitive
407            ("SYSTEM(\"id\")", "system("),
408            ("DeleteFile(\"/tmp/x\")", "deletefile("),
409        ];
410        for (expr, pat) in &cases {
411            let err = validate_measure_expr(expr).unwrap_err();
412            assert!(
413                err.to_string().contains(pat),
414                "error should mention '{pat}': {err}"
415            );
416        }
417    }
418}
419
420pub fn job_status(id: &str) -> Result<Value> {
421    let mut job = Job::load(id)?;
422    job.refresh()?;
423    serde_json::to_value(&job).map_err(|e| VirtuosoError::Execution(e.to_string()))
424}
425
426pub fn job_list() -> Result<Value> {
427    let mut jobs = Job::list_all()?;
428    for job in &mut jobs {
429        let _ = job.refresh();
430    }
431    let jobs_value = serde_json::to_value(&jobs)
432        .map_err(|e| VirtuosoError::Execution(format!("Failed to serialize jobs: {e}")))?;
433    Ok(json!({
434        "count": jobs.len(),
435        "jobs": jobs_value,
436    }))
437}
438
439pub fn job_cancel(id: &str) -> Result<Value> {
440    let mut job = Job::load(id)?;
441    job.cancel()?;
442    Ok(json!({
443        "status": "cancelled",
444        "job_id": id,
445    }))
446}