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