1use crate::client::bridge::{escape_skill_string, 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
319fn create_netlist_inner(
326 client: &VirtuosoClient,
327 lib: &str,
328 cell: &str,
329 view: &str,
330 recreate: bool,
331) -> Result<String> {
332 let cmd = if recreate {
333 "createNetlist(?recreateAll t ?display nil)"
334 } else {
335 "createNetlist(?display nil)"
336 };
337
338 let nr = client.execute_skill(cmd, Some(60))?;
340 let nr_out = nr.output.trim().trim_matches('"').to_string();
341 if nr.skill_ok() {
342 return Ok(nr_out);
343 }
344
345 let lib_e = escape_skill_string(lib);
349 let cell_e = escape_skill_string(cell);
350 let view_e = escape_skill_string(view);
351 let fix = format!(
352 r#"let((cv chk) cv=dbOpenCellViewByType("{lib_e}" "{cell_e}" "{view_e}") unless(cv cv=car(setof(ocv dbGetOpenCellViews() and(ocv~>libName=="{lib_e}" ocv~>cellName=="{cell_e}" ocv~>viewName=="{view_e}" ocv~>mode=="a")))) if(cv progn(chk=schCheck(cv) when(car(chk)==0 dbSave(cv)) list(car(chk))) list(-1)))"#
353 );
354 let fix_r = client.execute_skill(&fix, None)?;
355
356 let raw = fix_r.output.trim().trim_start_matches('(').trim_end_matches(')');
358 let err_count: i64 =
359 raw.split_whitespace().next().and_then(|s| s.parse().ok()).unwrap_or(-1);
360
361 if err_count != 0 {
362 return Err(VirtuosoError::Execution(format!(
363 "createNetlist failed; schematic has {err_count} check error(s) (OSSHNL-109). \
364 Fix schematic connectivity errors before netlisting."
365 )));
366 }
367
368 let retry = client.execute_skill(cmd, Some(60))?;
370 let retry_out = retry.output.trim().trim_matches('"').to_string();
371 if !retry.skill_ok() {
372 let errs = if retry.errors.is_empty() { "none".into() } else { retry.errors.join("; ") };
373 return Err(VirtuosoError::Execution(format!(
374 "createNetlist returned nil after Check and Save. Errors: {errs}. \
375 Ensure the schematic is saved and PDK models are loaded."
376 )));
377 }
378 Ok(retry_out)
379}
380
381pub fn netlist(lib: &str, cell: &str, view: &str, recreate: bool) -> Result<Value> {
382 let client = VirtuosoClient::from_env()?;
383
384 let setup = ocean::setup_skill(lib, cell, view, "spectre");
389 let sr = client.execute_skill(&setup, None)?;
390 if !sr.ok() {
391 return Err(VirtuosoError::Execution(format!(
392 "sim setup failed before netlisting: {}",
393 sr.errors.join("; ")
394 )));
395 }
396
397 let nr_out = create_netlist_inner(&client, lib, cell, view, recreate)?;
399
400 let candidate = if nr_out.ends_with(".scs") {
406 nr_out.clone()
407 } else if nr_out != "t" && !nr_out.is_empty() {
408 format!("{nr_out}/netlist/input.scs")
409 } else {
410 let rdir_val = {
413 let from_setup = sr.output.trim().trim_matches('"');
414 if from_setup != "nil" && !from_setup.is_empty() {
415 from_setup.to_string()
416 } else {
417 let rdir = client.execute_skill("resultsDir()", None)?;
418 rdir.output.trim().trim_matches('"').to_string()
419 }
420 };
421 if rdir_val == "nil" || rdir_val.is_empty() {
422 return Err(VirtuosoError::Execution(
423 "createNetlist returned 't' but resultsDir() is nil. \
424 Run `vcli sim setup` first or open ADE L for this cell."
425 .into(),
426 ));
427 }
428 format!("{rdir_val}/netlist/input.scs")
429 };
430
431 let check = client.execute_skill(&format!(r#"isFile("{candidate}")"#), None)?;
433 let v = check.output.trim().trim_matches('"');
434 let file_exists = v != "nil" && v != "0";
435
436 if !file_exists {
437 return Err(VirtuosoError::Execution(format!(
438 "createNetlist ran but file not found at '{candidate}'. \
439 createNetlist output was: '{nr_out}'. \
440 Check resultsDir() and ensure write permissions."
441 )));
442 }
443
444 Ok(json!({
445 "status": "success",
446 "netlist_path": candidate,
447 }))
448}
449
450pub fn run_async(netlist_path: &str) -> Result<Value> {
453 let content = std::fs::read_to_string(netlist_path)
454 .map_err(|e| VirtuosoError::Config(format!("Cannot read netlist '{netlist_path}': {e}")))?;
455 let sim = SpectreSimulator::from_env()?;
456 let job = sim.run_async(&content)?;
457 Ok(json!({
458 "status": "launched",
459 "job_id": job.id,
460 "pid": job.pid,
461 "netlist": netlist_path,
462 }))
463}
464
465#[cfg(test)]
466mod tests {
467 use super::validate_measure_expr;
468
469 #[test]
470 fn safe_waveform_exprs_are_allowed() {
471 for expr in &[
472 "VT(\"vout\" \"VGS\")",
473 "bandwidth(getData(\"vout\") 3)",
474 "value(getData(\"vout\") 1e-9)",
475 "getData(\"/vout\")",
476 "ymax(getData(\"id\"))",
477 "delay(getData(\"vout\") 0.5)",
478 ] {
479 assert!(
480 validate_measure_expr(expr).is_ok(),
481 "should be allowed: {expr}"
482 );
483 }
484 }
485
486 #[test]
487 fn dangerous_exprs_are_blocked() {
488 let cases = [
489 ("system(\"id\")", "system("),
490 ("sh(\"ls\")", "sh("),
491 ("ipcBeginProcess(\"cmd\")", "ipcbeginprocess("),
492 ("deleteFile(\"/etc/hosts\")", "deletefile("),
493 ("load(\"/tmp/evil.il\")", "load("),
494 ("evalstring(\"getData(1)\")", "evalstring("),
495 ("SYSTEM(\"id\")", "system("),
497 ("DeleteFile(\"/tmp/x\")", "deletefile("),
498 ];
499 for (expr, pat) in &cases {
500 let err = validate_measure_expr(expr).unwrap_err();
501 assert!(
502 err.to_string().contains(pat),
503 "error should mention '{pat}': {err}"
504 );
505 }
506 }
507}
508
509pub fn job_status(id: &str) -> Result<Value> {
510 let mut job = Job::load(id)?;
511 job.refresh()?;
512 serde_json::to_value(&job).map_err(|e| VirtuosoError::Execution(e.to_string()))
513}
514
515pub fn job_list() -> Result<Value> {
516 let mut jobs = Job::list_all()?;
517 for job in &mut jobs {
518 let _ = job.refresh();
519 }
520 let jobs_value = serde_json::to_value(&jobs)
521 .map_err(|e| VirtuosoError::Execution(format!("Failed to serialize jobs: {e}")))?;
522 Ok(json!({
523 "count": jobs.len(),
524 "jobs": jobs_value,
525 }))
526}
527
528pub fn job_cancel(id: &str) -> Result<Value> {
529 let mut job = Job::load(id)?;
530 job.cancel()?;
531 Ok(json!({
532 "status": "cancelled",
533 "job_id": id,
534 }))
535}