Skip to main content

virtuoso_cli/ocean/
mod.rs

1pub mod corner;
2
3use crate::client::bridge::escape_skill_string;
4use corner::{AnalysisConfig, CornerConfig};
5use std::collections::HashMap;
6
7pub fn setup_skill(lib: &str, cell: &str, view: &str, simulator: &str) -> String {
8    let lib = escape_skill_string(lib);
9    let cell = escape_skill_string(cell);
10    let view = escape_skill_string(view);
11    // progn() wraps three expressions into one — evalstring() only evaluates the
12    // first top-level expression in a string, so newline-separated calls are silently
13    // ignored.  Unless simulator is already set to avoid resetting modelFile etc.
14    format!(
15        r#"progn(unless(simulator()=='{simulator} simulator('{simulator})) design("{lib}" "{cell}" "{view}") resultsDir())"#
16    )
17}
18
19pub fn analysis_skill(config: &AnalysisConfig) -> String {
20    let typ = &config.analysis_type;
21    let mut skill = format!("analysis('{typ}");
22    for (k, v) in &config.params {
23        let val = match v {
24            serde_json::Value::String(s) => format!(" ?{k} \"{s}\""),
25            serde_json::Value::Number(n) => format!(" ?{k} {n}"),
26            other => format!(" ?{k} \"{other}\""),
27        };
28        skill.push_str(&val);
29    }
30    skill.push(')');
31    skill
32}
33
34pub fn analysis_skill_simple(typ: &str, params: &HashMap<String, String>) -> String {
35    let mut skill = format!("analysis('{typ}");
36    for (k, v) in params {
37        // Don't quote booleans (t/nil) or numbers
38        if v == "t" || v == "nil" || v.parse::<f64>().is_ok() {
39            skill.push_str(&format!(" ?{k} {v}"));
40        } else {
41            skill.push_str(&format!(" ?{k} \"{v}\""));
42        }
43    }
44    skill.push(')');
45    skill
46}
47
48pub fn run_skill() -> String {
49    "run()".into()
50}
51
52pub fn measure_skill(analysis_type: &str, exprs: &[String]) -> String {
53    if exprs.len() == 1 {
54        format!("selectResult('{analysis_type})\n{}", exprs[0])
55    } else {
56        let body = exprs
57            .iter()
58            .map(|e| format!("  {e}"))
59            .collect::<Vec<_>>()
60            .join("\n");
61        format!("selectResult('{analysis_type})\nlist(\n{body}\n)")
62    }
63}
64
65pub fn sweep_skill(
66    var: &str,
67    values: &[f64],
68    analysis_type: &str,
69    measure_exprs: &[String],
70) -> String {
71    let var = escape_skill_string(var);
72    let values_str = values
73        .iter()
74        .map(|v| format!("{v:e}"))
75        .collect::<Vec<_>>()
76        .join(" ");
77
78    let measures = measure_exprs
79        .iter()
80        .map(|e| format!("      {e}"))
81        .collect::<Vec<_>>()
82        .join("\n");
83
84    format!(
85        r#"let((results)
86  results = nil
87  foreach(val '({values_str})
88    desVar("{var}" val)
89    run()
90    selectResult('{analysis_type})
91    results = cons(list(val
92{measures}
93    ) results)
94  )
95  reverse(results)
96)"#
97    )
98}
99
100pub fn corner_skill(config: &CornerConfig) -> String {
101    let model_file = escape_skill_string(&config.model_file);
102    let analysis = analysis_skill(&config.analysis);
103
104    // Build corner data list
105    let _corner_entries: Vec<String> = config
106        .corners
107        .iter()
108        .map(|c| {
109            let _name = escape_skill_string(&c.name);
110            let section = escape_skill_string(&c.section);
111            // Collect extra vars
112            let vars: Vec<String> = c
113                .vars
114                .iter()
115                .map(|(k, v)| {
116                    let val = match v {
117                        serde_json::Value::Number(n) => n.to_string(),
118                        serde_json::Value::String(s) => format!("\"{s}\""),
119                        other => other.to_string(),
120                    };
121                    format!("    desVar(\"{k}\" {val})")
122                })
123                .collect();
124            let vars_code = vars.join("\n");
125            format!(
126                r#"    ;; Corner: {name}
127    modelFile('("{model_file}" "") "{section}")
128    temp({temp})
129{vars_code}"#,
130                name = c.name,
131                temp = c.temp,
132            )
133        })
134        .collect();
135
136    let measures = config
137        .measures
138        .iter()
139        .map(|m| format!("      {}", m.expr))
140        .collect::<Vec<_>>()
141        .join("\n");
142
143    // Build corner names for identification
144    let _corner_names: Vec<String> = config
145        .corners
146        .iter()
147        .map(|c| format!("\"{}\"", escape_skill_string(&c.name)))
148        .collect();
149
150    let mut skill = format!(
151        "simulator('{sim})\ndesign(\"{lib}\" \"{cell}\" \"{view}\")\n{analysis}\n",
152        sim = config.simulator.as_deref().unwrap_or("spectre"),
153        lib = escape_skill_string(&config.design.lib),
154        cell = escape_skill_string(&config.design.cell),
155        view = escape_skill_string(&config.design.view),
156    );
157
158    skill.push_str("let((results)\n  results = nil\n");
159
160    for corner in config.corners.iter() {
161        let name = escape_skill_string(&corner.name);
162        let section = escape_skill_string(&corner.section);
163        let vars_code: String = corner
164            .vars
165            .iter()
166            .map(|(k, v)| {
167                let val = match v {
168                    serde_json::Value::Number(n) => n.to_string(),
169                    serde_json::Value::String(s) => format!("\"{s}\""),
170                    other => other.to_string(),
171                };
172                format!("  desVar(\"{k}\" {val})\n")
173            })
174            .collect();
175
176        skill.push_str(&format!(
177            r#"  ;; {name}
178  modelFile('("{model_file}" "") "{section}")
179  temp({temp})
180{vars_code}  run()
181  selectResult('{analysis_type})
182  results = cons(list("{name}" {temp}
183{measures}
184  ) results)
185"#,
186            temp = corner.temp,
187            analysis_type = config.analysis.analysis_type,
188        ));
189    }
190
191    skill.push_str("  reverse(results)\n)");
192    skill
193}
194
195/// Parse a SKILL list result like `((1.0 2.0) (3.0 4.0))` into Vec<Vec<String>>
196pub fn parse_skill_list(output: &str) -> Vec<Vec<String>> {
197    let output = output.trim();
198    if output.is_empty() || output == "nil" {
199        return Vec::new();
200    }
201
202    let mut results = Vec::new();
203    let mut depth = 0i32;
204    let mut current_row = Vec::new();
205    let mut current_token = String::new();
206
207    for ch in output.chars() {
208        match ch {
209            '(' => {
210                depth += 1;
211                if depth == 1 {
212                    // outer list start
213                    continue;
214                }
215                if depth == 2 {
216                    // inner list start
217                    current_row.clear();
218                    continue;
219                }
220                current_token.push(ch);
221            }
222            ')' => {
223                depth -= 1;
224                if depth == 1 {
225                    // inner list end
226                    if !current_token.is_empty() {
227                        current_row.push(current_token.trim().trim_matches('"').to_string());
228                        current_token.clear();
229                    }
230                    if !current_row.is_empty() {
231                        results.push(current_row.clone());
232                    }
233                    continue;
234                }
235                if depth == 0 {
236                    // outer list end — handle flat list case
237                    if !current_token.is_empty() {
238                        current_row.push(current_token.trim().trim_matches('"').to_string());
239                        current_token.clear();
240                    }
241                    if !current_row.is_empty() && results.is_empty() {
242                        results.push(current_row.clone());
243                    }
244                    continue;
245                }
246                current_token.push(ch);
247            }
248            ' ' | '\t' | '\n' => {
249                if !current_token.is_empty() {
250                    current_row.push(current_token.trim().trim_matches('"').to_string());
251                    current_token.clear();
252                }
253            }
254            _ => {
255                current_token.push(ch);
256            }
257        }
258    }
259
260    // Handle single value case
261    if results.is_empty() && !output.starts_with('(') {
262        results.push(vec![output.trim_matches('"').to_string()]);
263    }
264
265    results
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use std::collections::HashMap;
272
273    #[test]
274    fn setup_skill_format() {
275        let s = setup_skill("myLib", "myCell", "schematic", "spectre");
276        // Must be a single top-level expression so evalstring() evaluates all parts.
277        assert!(s.starts_with("progn("), "{s}");
278        assert!(
279            s.contains("design(\"myLib\" \"myCell\" \"schematic\")"),
280            "{s}"
281        );
282        assert!(s.contains("spectre"), "{s}");
283        assert!(s.contains("resultsDir()"), "{s}");
284    }
285
286    #[test]
287    fn setup_skill_escapes_lib() {
288        let s = setup_skill(r#"my"Lib"#, "cell", "schematic", "spectre");
289        assert!(s.contains(r#"my\"Lib"#), "{s}");
290    }
291
292    #[test]
293    fn analysis_skill_simple_boolean_unquoted() {
294        let mut params = HashMap::new();
295        params.insert("start".into(), "1e-9".into());
296        params.insert("stop".into(), "1e-6".into());
297        params.insert("conservative".into(), "t".into());
298        let s = analysis_skill_simple("tran", &params);
299        assert!(s.starts_with("analysis('tran"), "{s}");
300        // boolean 't' must not be quoted
301        assert!(s.contains("?conservative t"), "{s}");
302    }
303
304    #[test]
305    fn analysis_skill_simple_string_quoted() {
306        let mut params = HashMap::new();
307        params.insert("errpreset".into(), "moderate".into());
308        let s = analysis_skill_simple("tran", &params);
309        assert!(s.contains("?errpreset \"moderate\""), "{s}");
310    }
311
312    #[test]
313    fn sweep_skill_uses_desvar() {
314        let values = vec![1.0, 1.2, 1.8];
315        let exprs = vec!["VT(\"M1\" \"VGS\")".to_string()];
316        let s = sweep_skill("Vdd", &values, "dc", &exprs);
317        assert!(s.contains("desVar(\"Vdd\" val)"), "{s}");
318        assert!(s.contains("run()"), "{s}");
319        assert!(s.contains("reverse(results)"), "{s}");
320    }
321
322    #[test]
323    fn parse_skill_list_nested() {
324        let rows = parse_skill_list("((1.0 2.0) (3.0 4.0))");
325        assert_eq!(rows.len(), 2);
326        assert_eq!(rows[0], vec!["1.0", "2.0"]);
327        assert_eq!(rows[1], vec!["3.0", "4.0"]);
328    }
329
330    #[test]
331    fn parse_skill_list_single_value() {
332        let rows = parse_skill_list("42");
333        assert_eq!(rows, vec![vec!["42"]]);
334    }
335}