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 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 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 let _ = client.execute_skill("save('all)", None);
53
54 let result = client.execute_skill("run()", Some(timeout))?;
56 if !result.ok() {
57 return Err(VirtuosoError::Execution(result.errors.join("; ")));
58 }
59
60 let rdir = client.execute_skill("resultsDir()", None)?;
62 let results_dir = rdir.output.trim().trim_matches('"').to_string();
63
64 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
89fn validate_measure_expr(expr: &str) -> Result<()> {
92 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 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 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 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 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 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(lib: &str, cell: &str, view: &str, recreate: bool) -> Result<Value> {
320 let client = VirtuosoClient::from_env()?;
321
322 let setup = ocean::setup_skill(lib, cell, view, "spectre");
327 let sr = client.execute_skill(&setup, None)?;
328 if !sr.ok() {
329 return Err(VirtuosoError::Execution(format!(
330 "sim setup failed before netlisting: {}",
331 sr.errors.join("; ")
332 )));
333 }
334
335 let create_cmd = if recreate {
337 "createNetlist(?recreateAll t ?display nil)"
338 } else {
339 "createNetlist(?display nil)"
340 };
341 let nr = client.execute_skill(create_cmd, Some(60))?;
342 let nr_out = nr.output.trim().trim_matches('"').to_string();
343
344 if !nr.ok() || nr_out == "nil" {
345 return Err(VirtuosoError::Execution(format!(
346 "createNetlist returned nil. Errors: {}. \
347 Ensure the schematic is saved and the PDK models are loaded.",
348 if nr.errors.is_empty() {
349 "none".to_string()
350 } else {
351 nr.errors.join("; ")
352 }
353 )));
354 }
355
356 let candidate = if nr_out.ends_with(".scs") {
362 nr_out.clone()
363 } else if nr_out != "t" && !nr_out.is_empty() {
364 format!("{nr_out}/netlist/input.scs")
365 } else {
366 let rdir = client.execute_skill("resultsDir()", None)?;
368 let rdir_val = rdir.output.trim().trim_matches('"').to_string();
369 if rdir_val == "nil" || rdir_val.is_empty() {
370 return Err(VirtuosoError::Execution(
371 "createNetlist returned 't' but resultsDir() is nil. \
372 Run `vcli sim setup` first or open ADE L for this cell."
373 .into(),
374 ));
375 }
376 format!("{rdir_val}/netlist/input.scs")
377 };
378
379 let check = client.execute_skill(
381 &format!(r#"isFile("{candidate}")"#),
382 None,
383 )?;
384 let file_exists = {
385 let v = check.output.trim().trim_matches('"');
386 v != "nil" && v != "0"
387 };
388
389 if !file_exists {
390 return Err(VirtuosoError::Execution(format!(
391 "createNetlist ran but file not found at '{candidate}'. \
392 createNetlist output was: '{nr_out}'. \
393 Check resultsDir() and ensure write permissions."
394 )));
395 }
396
397 Ok(json!({
398 "status": "success",
399 "netlist_path": candidate,
400 }))
401}
402
403pub fn run_async(netlist_path: &str) -> Result<Value> {
406 let content = std::fs::read_to_string(netlist_path)
407 .map_err(|e| VirtuosoError::Config(format!("Cannot read netlist '{netlist_path}': {e}")))?;
408 let sim = SpectreSimulator::from_env()?;
409 let job = sim.run_async(&content)?;
410 Ok(json!({
411 "status": "launched",
412 "job_id": job.id,
413 "pid": job.pid,
414 "netlist": netlist_path,
415 }))
416}
417
418#[cfg(test)]
419mod tests {
420 use super::validate_measure_expr;
421
422 #[test]
423 fn safe_waveform_exprs_are_allowed() {
424 for expr in &[
425 "VT(\"vout\" \"VGS\")",
426 "bandwidth(getData(\"vout\") 3)",
427 "value(getData(\"vout\") 1e-9)",
428 "getData(\"/vout\")",
429 "ymax(getData(\"id\"))",
430 "delay(getData(\"vout\") 0.5)",
431 ] {
432 assert!(
433 validate_measure_expr(expr).is_ok(),
434 "should be allowed: {expr}"
435 );
436 }
437 }
438
439 #[test]
440 fn dangerous_exprs_are_blocked() {
441 let cases = [
442 ("system(\"id\")", "system("),
443 ("sh(\"ls\")", "sh("),
444 ("ipcBeginProcess(\"cmd\")", "ipcbeginprocess("),
445 ("deleteFile(\"/etc/hosts\")", "deletefile("),
446 ("load(\"/tmp/evil.il\")", "load("),
447 ("evalstring(\"getData(1)\")", "evalstring("),
448 ("SYSTEM(\"id\")", "system("),
450 ("DeleteFile(\"/tmp/x\")", "deletefile("),
451 ];
452 for (expr, pat) in &cases {
453 let err = validate_measure_expr(expr).unwrap_err();
454 assert!(
455 err.to_string().contains(pat),
456 "error should mention '{pat}': {err}"
457 );
458 }
459 }
460}
461
462pub fn job_status(id: &str) -> Result<Value> {
463 let mut job = Job::load(id)?;
464 job.refresh()?;
465 serde_json::to_value(&job).map_err(|e| VirtuosoError::Execution(e.to_string()))
466}
467
468pub fn job_list() -> Result<Value> {
469 let mut jobs = Job::list_all()?;
470 for job in &mut jobs {
471 let _ = job.refresh();
472 }
473 let jobs_value = serde_json::to_value(&jobs)
474 .map_err(|e| VirtuosoError::Execution(format!("Failed to serialize jobs: {e}")))?;
475 Ok(json!({
476 "count": jobs.len(),
477 "jobs": jobs_value,
478 }))
479}
480
481pub fn job_cancel(id: &str) -> Result<Value> {
482 let mut job = Job::load(id)?;
483 job.cancel()?;
484 Ok(json!({
485 "status": "cancelled",
486 "job_id": id,
487 }))
488}