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(recreate: bool) -> Result<Value> {
320 let client = VirtuosoClient::from_env()?;
321
322 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 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
361pub 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 ("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}