Skip to main content

virtuoso_cli/client/
maestro_ops.rs

1use crate::client::bridge::escape_skill_string;
2use crate::version::VirtuosoVersion;
3
4pub struct MaestroOps;
5
6impl MaestroOps {
7    /// Returns session handle like `"fnxSession4"`.
8    pub fn open_session(&self, lib: &str, cell: &str, view: &str) -> String {
9        let lib = escape_skill_string(lib);
10        let cell = escape_skill_string(cell);
11        let view = escape_skill_string(view);
12        format!(r#"maeOpenSetup("{lib}" "{cell}" "{view}")"#)
13    }
14
15    /// Force-closes the session, cancels any in-flight simulation.
16    pub fn close_session(&self, session: &str) -> String {
17        let session = escape_skill_string(session);
18        format!(r#"maeCloseSession("{session}" ?forceClose t)"#)
19    }
20
21    pub fn list_sessions(&self) -> String {
22        skill_strings_to_json("maeGetSessions()")
23    }
24
25    /// Set a design variable value.
26    /// maeSetVar(name value) — no session arg (IC23/IC25 compatible).
27    pub fn set_var(&self, name: &str, value: &str) -> String {
28        let name = escape_skill_string(name);
29        let value = escape_skill_string(value);
30        format!(r#"maeSetVar("{name}" "{value}")"#)
31    }
32
33    pub fn get_var(&self, name: &str) -> String {
34        let name = escape_skill_string(name);
35        format!(r#"maeGetVar("{name}")"#)
36    }
37
38    /// List all design variables. Returns JSON via sprintf.
39    pub fn list_vars(&self) -> String {
40        r#"let((vars out sep) vars = asiGetDesignVarList(asiGetCurrentSession()) out = "[" sep = "" foreach(v vars out = strcat(out sep sprintf(nil "{\"name\":\"%s\",\"value\":\"%s\"}" car(v) cadr(v))) sep = ",") strcat(out "]"))"#.into()
41    }
42
43    /// Get enabled analyses — version-aware.
44    ///
45    /// IC23: `maeGetEnabledAnalysis(setupName)` — needs car(maeGetSetup(...)) first.
46    /// IC25: `maeGetEnabledAnalysis(?session sessionName)` — direct keyword.
47    pub fn get_analyses(&self, session: &str, version: VirtuosoVersion) -> String {
48        let session = escape_skill_string(session);
49        if version.is_ic25() {
50            format!(r#"maeGetEnabledAnalysis(?session "{session}")"#)
51        } else {
52            format!(
53                r#"let((setup) setup = car(maeGetSetup(?session "{session}")) maeGetEnabledAnalysis(setup))"#
54            )
55        }
56    }
57
58    /// Enable an analysis type — version-aware.
59    ///
60    /// IC23: `maeSetAnalysis(setupName analysisType)`.
61    /// IC25: `maeSetAnalysis(analysisType ?session s ?enable t ?options \`(...))`.
62    ///
63    /// `options_skill_alist` is validated and converted at the command layer before this is called.
64    pub fn set_analysis(
65        &self,
66        session: &str,
67        analysis_type: &str,
68        options_skill_alist: Option<&str>,
69        version: VirtuosoVersion,
70    ) -> String {
71        let session = escape_skill_string(session);
72        let analysis_type = escape_skill_string(analysis_type);
73        if version.is_ic25() {
74            let options_part = match options_skill_alist {
75                Some(alist) => format!(" ?options `{alist}"),
76                None => String::new(),
77            };
78            format!(
79                r#"maeSetAnalysis("{analysis_type}" ?session "{session}" ?enable t{options_part})"#
80            )
81        } else {
82            // IC23: positional — setup name first; options not supported in this path
83            format!(
84                r#"let((setup) setup = car(maeGetSetup(?session "{session}")) maeSetAnalysis(setup "{analysis_type}"))"#
85            )
86        }
87    }
88
89    /// Run simulation asynchronously. Returns immediately.
90    pub fn run_simulation(&self, session: &str) -> String {
91        let session = escape_skill_string(session);
92        format!(r#"maeRunSimulation(?session "{session}")"#)
93    }
94
95    /// Get test outputs — version-aware.
96    ///
97    /// IC23/IC25: maeGetTestOutputs(testName) — both use positional.
98    /// IC25 additionally supports ?session keyword.
99    #[allow(dead_code)]
100    pub fn get_outputs(&self, test_name: &str) -> String {
101        let test_name = escape_skill_string(test_name);
102        format!(
103            r#"let((outs out sep) outs = maeGetTestOutputs("{test_name}") out = "[" sep = "" foreach(o outs out = strcat(out sep sprintf(nil "{{\"name\":\"%s\",\"type\":\"%s\",\"signalName\":\"%s\",\"expr\":\"%s\"}}" o~>name o~>outputType o~>signalName o~>expr)) sep = ",") strcat(out "]"))"#
104        )
105    }
106
107    pub fn add_output(&self, output_name: &str, test_name: &str, expr: &str) -> String {
108        let output_name = escape_skill_string(output_name);
109        let test_name = escape_skill_string(test_name);
110        let expr = escape_skill_string(expr);
111        format!(r#"maeAddOutput("{output_name}" "{test_name}" ?expr "{expr}")"#)
112    }
113
114    #[allow(dead_code)]
115    pub fn set_design(&self, session: &str, lib: &str, cell: &str, view: &str) -> String {
116        let session = escape_skill_string(session);
117        let lib = escape_skill_string(lib);
118        let cell = escape_skill_string(cell);
119        let view = escape_skill_string(view);
120        format!(
121            r#"maeSetDesign(?session "{session}" ?libName "{lib}" ?cellName "{cell}" ?viewName "{view}")"#
122        )
123    }
124
125    pub fn save_setup(&self, session: &str) -> String {
126        let session = escape_skill_string(session);
127        format!(r#"maeSaveSetup(?session "{session}")"#)
128    }
129
130    pub fn get_sim_messages(&self, session: &str) -> String {
131        let session = escape_skill_string(session);
132        format!(r#"maeGetSimulationMessages(?session "{session}")"#)
133    }
134
135    /// Get focused ADE window name, davSession, all window names, sessions, and run_dir in one RTT.
136    ///
137    /// Returns a 5-element SKILL list:
138    ///   (title davSession (all_titles...) (sessions...) run_dir_or_nil)
139    ///
140    /// `davSession` is `cw->davSession` — the Maestro session name bound to the ADE window.
141    /// `run_dir_or_nil` is bundled so callers need only 1 RTT when the focused window has a session.
142    pub fn focused_window_skill(&self) -> String {
143        r#"let((cw sess) cw=hiGetCurrentWindow() sess=if(cw cw->davSession nil) list(if(cw hiGetWindowName(cw) nil) sess mapcar(lambda((w) hiGetWindowName(w)) hiGetWindowList()) maeGetSessions() if(sess let((s) s=asiGetSession(sess) if(s asiGetAnalogRunDir(s) nil)) nil)))"#.into()
144    }
145
146    /// Get simulation run directory for a maestro session via asiGetAnalogRunDir.
147    /// Used when the caller provides a different session than the focused window's davSession.
148    pub fn run_dir_skill(&self, session: &str) -> String {
149        let session = escape_skill_string(session);
150        format!(
151            r#"let((sess) sess=asiGetSession("{session}") if(sess asiGetAnalogRunDir(sess) nil))"#
152        )
153    }
154
155    /// Export results to CSV via maeExportOutputView.
156    pub fn export_results(
157        &self,
158        session: &str,
159        file_path: &str,
160        test_name: Option<&str>,
161        history: Option<&str>,
162    ) -> String {
163        let session = escape_skill_string(session);
164        let file_path = escape_skill_string(file_path);
165        let test_name_part = match test_name {
166            Some(t) => format!(r#" ?testName "{}""#, escape_skill_string(t)),
167            None => String::new(),
168        };
169        let history_part = match history {
170            Some(h) => format!(r#" ?historyName "{}""#, escape_skill_string(h)),
171            None => String::new(),
172        };
173        format!(
174            r#"maeExportOutputView(?session "{session}"{test_name_part}{history_part} ?view "Detail" ?fileName "{file_path}")"#
175        )
176    }
177
178    // =========================================================================
179    // Result Reading Functions (IC23/IC25 compatible)
180    // =========================================================================
181
182    /// Open a history run for programmatic result access.
183    pub fn open_results(&self, history: &str) -> String {
184        let history = escape_skill_string(history);
185        format!(r#"maeOpenResults(?history "{history}")"#)
186    }
187
188    /// Close the currently open results.
189    pub fn close_results(&self) -> String {
190        r#"maeCloseResults()"#.into()
191    }
192
193    /// List all test names that have results in the current history.
194    pub fn get_result_tests(&self) -> String {
195        r#"let((tests out sep) tests = maeGetResultTests() out = "[" sep = "" foreach(t tests out = strcat(out sep sprintf(nil "\"%s\"" t)) sep = ",") strcat(out "]"))"#.into()
196    }
197
198    /// List all output names available for a given test in the current history.
199    pub fn get_result_outputs(&self, test_name: &str) -> String {
200        let test_name = escape_skill_string(test_name);
201        format!(
202            r#"let((outs out sep) outs = maeGetResultOutputs(?testName "{test_name}") out = "[" sep = "" foreach(o outs out = strcat(out sep sprintf(nil "\"%s\"" o)) sep = ",") strcat(out "]"))"#
203        )
204    }
205
206    /// Get the value of a specific output for a specific test and corner.
207    pub fn get_output_value(&self, name: &str, test_name: &str, corner: Option<&str>) -> String {
208        let name = escape_skill_string(name);
209        let test_name = escape_skill_string(test_name);
210        match corner {
211            Some(c) => {
212                let c = escape_skill_string(c);
213                format!(r#"maeGetOutputValue("{name}" "{test_name}" ?cornerName "{c}")"#)
214            }
215            None => format!(r#"maeGetOutputValue("{name}" "{test_name}")"#),
216        }
217    }
218
219    /// Get the spec pass/fail status for an output.
220    pub fn get_spec_status(&self, name: &str, test_name: &str) -> String {
221        let name = escape_skill_string(name);
222        let test_name = escape_skill_string(test_name);
223        format!(r#"maeGetSpecStatus("{name}" "{test_name}")"#)
224    }
225
226    /// List available history runs for the current Maestro session.
227    /// Returns JSON array of history names.
228    pub fn get_history_list(&self) -> String {
229        r#"let((base histories out sep) base = getDirFiles(strcat(asiGetResultsDir(asiGetCurrentSession()) "/..")) histories = remove("maestro" remove("exprOutputs.log" base)) out = "[" sep = "" foreach(h histories when(h && !index(h ".") out = strcat(out sep sprintf(nil "\"%s\"" h)) sep = ",")) strcat(out "]"))"#.into()
230    }
231
232    /// Get the Maestro session ID for the current session.
233    #[allow(dead_code)]
234    pub fn get_current_session(&self) -> String {
235        r#"let((sess out) sess = asiGetCurrentSession() out = if(sess then sess~>name else "nil"))"#
236            .into()
237    }
238}
239
240/// Wrap a SKILL expression that returns a list-of-strings into a JSON array string.
241///
242/// If `list_expr` returns nil (empty), the output is `"[]"`.
243/// This ensures list-returning ops never produce SKILL nil — callers use r.ok() not r.skill_ok().
244fn skill_strings_to_json(list_expr: &str) -> String {
245    format!(
246        r#"let((xs out sep) xs = {list_expr} out = "[" sep = "" foreach(x xs out = strcat(out sep sprintf(nil "\"%s\"" x)) sep = ",") strcat(out "]"))"#
247    )
248}
249
250/// Convert a JSON object string to a SKILL association list.
251///
252/// Input: `{"start":"1","stop":"10G","dec":"20"}`
253/// Output: `(("start" "1") ("stop" "10G") ("dec" "20"))`
254///
255/// Returns `Err` if the input is not valid JSON or not a JSON object.
256pub(crate) fn json_to_skill_alist(json_str: &str) -> Result<String, String> {
257    let parsed: serde_json::Value =
258        serde_json::from_str(json_str).map_err(|e| format!("invalid JSON: {e}"))?;
259    let obj = parsed
260        .as_object()
261        .ok_or_else(|| "expected a JSON object".to_string())?;
262    let pairs: Vec<String> = obj
263        .iter()
264        .map(|(k, v)| {
265            let binding = v.to_string();
266            let val = v.as_str().unwrap_or(&binding);
267            format!("(\"{k}\" \"{val}\")")
268        })
269        .collect();
270    Ok(format!("({})", pairs.join(" ")))
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    fn ops() -> MaestroOps {
278        MaestroOps
279    }
280
281    #[test]
282    fn open_session_quoting() {
283        let s = ops().open_session("myLib", "myCell", "adexl");
284        assert_eq!(s, r#"maeOpenSetup("myLib" "myCell" "adexl")"#);
285    }
286
287    #[test]
288    fn open_session_escapes_quotes() {
289        let s = ops().open_session(r#"lib"x"#, "cell", "adexl");
290        assert!(s.contains(r#"lib\"x"#), "{s}");
291    }
292
293    #[test]
294    fn set_var_format() {
295        let s = ops().set_var("Vdd", "1.8");
296        assert_eq!(s, r#"maeSetVar("Vdd" "1.8")"#);
297    }
298
299    #[test]
300    fn run_simulation_includes_session() {
301        let s = ops().run_simulation("sess1");
302        assert!(s.contains("maeRunSimulation"), "{s}");
303        assert!(s.contains("\"sess1\""), "{s}");
304    }
305
306    #[test]
307    fn get_analyses_ic23_resolves_setup() {
308        let s = ops().get_analyses("sess1", VirtuosoVersion::IC23);
309        assert!(s.contains("maeGetSetup"), "IC23 must resolve setup: {s}");
310        assert!(s.contains("maeGetEnabledAnalysis"), "{s}");
311    }
312
313    #[test]
314    fn get_analyses_ic25_uses_ic23_path() {
315        // IC25.1 ISR4 实测:maeGetSetup 仍返回 list,car() 有效
316        // is_ic25() 返回 false,所以 IC25 版本走 IC23 路径
317        let s = ops().get_analyses("sess1", VirtuosoVersion::IC25);
318        assert!(
319            s.contains("maeGetSetup"),
320            "IC25 currently uses IC23 path: {s}"
321        );
322        assert!(s.contains("maeGetEnabledAnalysis"), "{s}");
323    }
324
325    #[test]
326    fn list_sessions_uses_helper() {
327        let s = ops().list_sessions();
328        assert!(s.contains("maeGetSessions()"), "{s}");
329        assert!(s.contains("foreach"), "{s}");
330        assert!(s.contains(r#"strcat(out "]")"#), "{s}");
331    }
332
333    #[test]
334    fn get_result_tests_uses_helper() {
335        let s = ops().get_result_tests();
336        assert!(s.contains("maeGetResultTests()"), "{s}");
337        assert!(s.contains("foreach"), "{s}");
338    }
339
340    #[test]
341    fn get_history_list_uses_helper() {
342        let s = ops().get_history_list();
343        assert!(s.contains("asiGetResultsDir"), "{s}");
344        assert!(s.contains("foreach"), "{s}");
345    }
346
347    #[test]
348    fn export_results_minimal() {
349        let s = ops().export_results("sess1", "/tmp/out.csv", None, None);
350        assert!(s.contains("maeExportOutputView"), "{s}");
351        assert!(s.contains(r#"?session "sess1""#), "{s}");
352        assert!(s.contains(r#"?fileName "/tmp/out.csv""#), "{s}");
353        assert!(s.contains(r#"?view "Detail""#), "{s}");
354        assert!(!s.contains("?testName"), "should be absent when None: {s}");
355        assert!(
356            !s.contains("?historyName"),
357            "should be absent when None: {s}"
358        );
359    }
360
361    #[test]
362    fn export_results_with_all_params() {
363        let s = ops().export_results("sess1", "/tmp/out.csv", Some("AC"), Some("ExplorerRun.0"));
364        assert!(s.contains(r#"?testName "AC""#), "{s}");
365        assert!(s.contains(r#"?historyName "ExplorerRun.0""#), "{s}");
366    }
367
368    #[test]
369    fn set_analysis_ic23_positional() {
370        let s = ops().set_analysis("sess1", "ac", None, VirtuosoVersion::IC23);
371        assert!(s.contains("maeGetSetup"), "IC23 must resolve setup: {s}");
372        assert!(s.contains("maeSetAnalysis"), "{s}");
373        assert!(s.contains("\"ac\""), "{s}");
374    }
375
376    #[test]
377    fn set_analysis_ic23_no_options() {
378        let s = ops().set_analysis("sess1", "ac", None, VirtuosoVersion::IC23);
379        assert!(
380            !s.contains("?options"),
381            "IC23 path must not inject options: {s}"
382        );
383    }
384
385    #[test]
386    fn set_analysis_ic25_uses_ic23_path() {
387        // IC25.1 ISR4 实测:maeSetAnalysis 仍为 positional (setupName type)
388        let s = ops().set_analysis("sess1", "ac", None, VirtuosoVersion::IC25);
389        assert!(
390            s.contains("maeGetSetup"),
391            "IC25 currently uses IC23 path: {s}"
392        );
393        assert!(s.contains("maeSetAnalysis"), "{s}");
394    }
395
396    #[test]
397    fn add_output_includes_expr() {
398        let s = ops().add_output("gain", "AC", "getData(\"vout\")");
399        assert!(s.contains("maeAddOutput"), "{s}");
400        assert!(s.contains("\"gain\""), "{s}");
401        assert!(s.contains("\"AC\""), "{s}");
402    }
403
404    #[test]
405    fn json_to_skill_alist_valid_input() {
406        let input = r#"{"start":"1","stop":"10G"}"#;
407        let out = json_to_skill_alist(input).unwrap();
408        assert!(out.contains("(\"start\" \"1\")"), "{out}");
409        assert!(out.contains("(\"stop\" \"10G\")"), "{out}");
410    }
411
412    #[test]
413    fn json_to_skill_alist_invalid_json_returns_err() {
414        assert!(json_to_skill_alist("not json").is_err());
415    }
416
417    #[test]
418    fn json_to_skill_alist_non_object_returns_err() {
419        assert!(json_to_skill_alist("[1,2,3]").is_err());
420    }
421}