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
358 .output
359 .trim()
360 .trim_start_matches('(')
361 .trim_end_matches(')');
362 let err_count: i64 = raw
363 .split_whitespace()
364 .next()
365 .and_then(|s| s.parse().ok())
366 .unwrap_or(-1);
367
368 if err_count == -1 {
369 let lib_probe = format!(r#"when(car(setof(l ddGetLibList() l~>name=="{lib_e}")) "found")"#);
380 let probe_r = client.execute_skill(&lib_probe, None)?;
381 let lib_found = probe_r.output.trim().trim_matches('"');
382 if lib_found != "found" {
383 let cwd_r = client.execute_skill("getWorkingDir()", None)?;
384 let cwd = cwd_r.output.trim().trim_matches('"');
385 let cwd_note = if !cwd.is_empty() && cwd != "nil" {
386 format!(" Virtuoso was started from '{cwd}'.")
387 } else {
388 String::new()
389 };
390 return Err(VirtuosoError::Execution(format!(
391 "Library '{lib}' is not registered in the current Virtuoso session.{cwd_note} \
392 Start Virtuoso from the project directory whose cds.lib includes '{lib}', \
393 or run hiLoadCDSLibDefs() in the CIW to register it at runtime."
394 )));
395 }
396 return Ok("t".into());
397 }
398 if err_count != 0 {
399 return Err(VirtuosoError::Execution(format!(
400 "createNetlist failed; schematic has {err_count} check error(s) (OSSHNL-109). \
401 Fix schematic connectivity errors before netlisting."
402 )));
403 }
404
405 let retry = client.execute_skill(cmd, Some(60))?;
407 let retry_out = retry.output.trim().trim_matches('"').to_string();
408 if !retry.skill_ok() {
409 let errs = if retry.errors.is_empty() {
410 "none".into()
411 } else {
412 retry.errors.join("; ")
413 };
414 return Err(VirtuosoError::Execution(format!(
415 "createNetlist returned nil after Check and Save. Errors: {errs}. \
416 Ensure the schematic is saved and PDK models are loaded."
417 )));
418 }
419 Ok(retry_out)
420}
421
422fn analysis_block(kind: &str) -> Option<&'static str> {
425 match kind {
426 "dc" => Some(
427 "dcOp dc write=\"spectre.dc\" maxiters=150 maxsteps=10000 annotate=status\n\
428 dcOpInfo info what=oppoint where=rawfile\n",
429 ),
430 "ac" => Some("acSweep ac start=1 stop=10G dec=20 annotate=status\n"),
431 "tran" => Some("tran tran stop=10u annotate=status\n"),
432 _ => None,
433 }
434}
435
436pub fn netlist(
437 lib: &str,
438 cell: &str,
439 view: &str,
440 recreate: bool,
441 analyses: &[String],
442) -> Result<Value> {
443 let client = VirtuosoClient::from_env()?;
444
445 let setup = ocean::setup_skill(lib, cell, view, "spectre");
450 let sr = client.execute_skill(&setup, None)?;
451 if !sr.ok() {
452 return Err(VirtuosoError::Execution(format!(
453 "sim setup failed before netlisting: {}",
454 sr.errors.join("; ")
455 )));
456 }
457
458 let nr_out = create_netlist_inner(&client, lib, cell, view, recreate)?;
460
461 let candidate = if nr_out.ends_with(".scs") {
467 nr_out.clone()
468 } else if nr_out != "t" && !nr_out.is_empty() {
469 format!("{nr_out}/netlist/input.scs")
470 } else {
471 let rdir_val = {
475 let from_setup = sr.output.trim().trim_matches('"');
476 let raw =
477 if from_setup != "nil" && !from_setup.is_empty() && from_setup.starts_with('/') {
478 from_setup.to_string()
479 } else {
480 let rdir = client.execute_skill("resultsDir()", None)?;
481 rdir.output.trim().trim_matches('"').to_string()
482 };
483 if !raw.is_empty() && raw != "nil" && !raw.starts_with('/') {
485 let cwd_r = client.execute_skill("getWorkingDir()", None)?;
486 let cwd = cwd_r.output.trim().trim_matches('"');
487 if cwd != "nil" && !cwd.is_empty() {
488 format!("{cwd}/{raw}")
489 } else {
490 raw
491 }
492 } else {
493 raw
494 }
495 };
496 if rdir_val == "nil" || rdir_val.is_empty() {
497 return Err(VirtuosoError::Execution(
498 "createNetlist returned 't' but resultsDir() is nil. \
499 Run `vcli sim setup` first or open ADE L for this cell."
500 .into(),
501 ));
502 }
503 format!("{rdir_val}/netlist/input.scs")
504 };
505
506 let check = client.execute_skill(&format!(r#"isFile("{candidate}")"#), None)?;
508 let v = check.output.trim().trim_matches('"');
509 let file_exists = v != "nil" && v != "0";
510
511 if !file_exists {
512 return Err(VirtuosoError::Execution(format!(
513 "createNetlist ran but file not found at '{candidate}'. \
514 createNetlist output was: '{nr_out}'. \
515 Check resultsDir() and ensure write permissions."
516 )));
517 }
518
519 let mut patched = false;
521 let mut unknown_analyses: Vec<&str> = Vec::new();
522
523 if !analyses.is_empty() {
524 let mut content = std::fs::read_to_string(&candidate).map_err(|e| {
525 VirtuosoError::Execution(format!("cannot read netlist '{candidate}': {e}"))
526 })?;
527
528 if content.contains("/oa/smic13mmrf_1233//../") {
531 content = content.replace("/oa/smic13mmrf_1233//../", "/");
532 patched = true;
533 }
534
535 for kind in analyses {
537 match analysis_block(kind) {
538 Some(block) => {
539 let marker = match kind.as_str() {
540 "dc" => "dcOp ",
541 "ac" => "acSweep ",
542 "tran" => "tran tran",
543 _ => unreachable!(),
544 };
545 if !content.contains(marker) {
546 content.push('\n');
547 content.push_str(block);
548 patched = true;
549 }
550 }
551 None => unknown_analyses.push(kind),
552 }
553 }
554
555 if patched {
556 std::fs::write(&candidate, &content).map_err(|e| {
557 VirtuosoError::Execution(format!("cannot write patched netlist '{candidate}': {e}"))
558 })?;
559 }
560 }
561
562 let mut out = json!({
563 "status": "success",
564 "netlist_path": candidate,
565 });
566 if patched {
567 out["patched"] = json!(true);
568 }
569 if !unknown_analyses.is_empty() {
570 out["unknown_analyses"] = json!(unknown_analyses);
571 }
572 Ok(out)
573}
574
575pub fn run_async(netlist_path: &str) -> Result<Value> {
578 let content = std::fs::read_to_string(netlist_path)
579 .map_err(|e| VirtuosoError::Config(format!("Cannot read netlist '{netlist_path}': {e}")))?;
580 let sim = SpectreSimulator::from_env()?;
581 let job = sim.run_async(&content)?;
582 Ok(json!({
583 "status": "launched",
584 "job_id": job.id,
585 "pid": job.pid,
586 "netlist": netlist_path,
587 }))
588}
589
590#[cfg(test)]
591mod tests {
592 use super::validate_measure_expr;
593
594 #[test]
595 fn safe_waveform_exprs_are_allowed() {
596 for expr in &[
597 "VT(\"vout\" \"VGS\")",
598 "bandwidth(getData(\"vout\") 3)",
599 "value(getData(\"vout\") 1e-9)",
600 "getData(\"/vout\")",
601 "ymax(getData(\"id\"))",
602 "delay(getData(\"vout\") 0.5)",
603 ] {
604 assert!(
605 validate_measure_expr(expr).is_ok(),
606 "should be allowed: {expr}"
607 );
608 }
609 }
610
611 #[test]
612 fn dangerous_exprs_are_blocked() {
613 let cases = [
614 ("system(\"id\")", "system("),
615 ("sh(\"ls\")", "sh("),
616 ("ipcBeginProcess(\"cmd\")", "ipcbeginprocess("),
617 ("deleteFile(\"/etc/hosts\")", "deletefile("),
618 ("load(\"/tmp/evil.il\")", "load("),
619 ("evalstring(\"getData(1)\")", "evalstring("),
620 ("SYSTEM(\"id\")", "system("),
622 ("DeleteFile(\"/tmp/x\")", "deletefile("),
623 ];
624 for (expr, pat) in &cases {
625 let err = validate_measure_expr(expr).unwrap_err();
626 assert!(
627 err.to_string().contains(pat),
628 "error should mention '{pat}': {err}"
629 );
630 }
631 }
632}
633
634pub fn job_status(id: &str) -> Result<Value> {
635 let mut job = Job::load(id)?;
636 job.refresh()?;
637 serde_json::to_value(&job).map_err(|e| VirtuosoError::Execution(e.to_string()))
638}
639
640pub fn job_list() -> Result<Value> {
641 let mut jobs = Job::list_all()?;
642 for job in &mut jobs {
643 let _ = job.refresh();
644 }
645 let jobs_value = serde_json::to_value(&jobs)
646 .map_err(|e| VirtuosoError::Execution(format!("Failed to serialize jobs: {e}")))?;
647 Ok(json!({
648 "count": jobs.len(),
649 "jobs": jobs_value,
650 }))
651}
652
653pub fn job_cancel(id: &str) -> Result<Value> {
654 let mut job = Job::load(id)?;
655 job.cancel()?;
656 Ok(json!({
657 "status": "cancelled",
658 "job_id": id,
659 }))
660}