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