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}