Skip to main content

lift_config/
parser.rs

1use crate::types::*;
2use std::collections::HashMap;
3use thiserror::Error;
4
5#[derive(Debug, Error)]
6pub enum ConfigError {
7    #[error("Missing required field: {0}")]
8    MissingField(String),
9    #[error("Invalid value for {field}: {value}")]
10    InvalidValue { field: String, value: String },
11    #[error("Parse error at line {line}: {message}")]
12    ParseError { line: usize, message: String },
13}
14
15#[derive(Debug)]
16pub struct ConfigParser;
17
18impl ConfigParser {
19    pub fn new() -> Self {
20        Self
21    }
22
23    pub fn parse(&self, source: &str) -> Result<LithConfig, ConfigError> {
24        let mut config = LithConfig::default();
25        let mut current_section = String::new();
26        let mut kv_map: HashMap<String, HashMap<String, String>> = HashMap::new();
27
28        for (line_num, line) in source.lines().enumerate() {
29            let trimmed = line.trim();
30
31            if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
32                continue;
33            }
34
35            // Section header: [section]
36            if trimmed.starts_with('[') && trimmed.ends_with(']') {
37                current_section = trimmed[1..trimmed.len() - 1].to_string();
38                kv_map.entry(current_section.clone()).or_default();
39                continue;
40            }
41
42            // Key = value
43            if let Some((key, value)) = trimmed.split_once('=') {
44                let key = key.trim().to_string();
45                let value = value.trim().trim_matches('"').to_string();
46                kv_map
47                    .entry(current_section.clone())
48                    .or_default()
49                    .insert(key, value);
50            } else {
51                return Err(ConfigError::ParseError {
52                    line: line_num + 1,
53                    message: format!("Expected key = value, got: {}", trimmed),
54                });
55            }
56        }
57
58        // Apply parsed values to config
59        if let Some(target) = kv_map.get("target") {
60            if let Some(backend) = target.get("backend") {
61                config.target.backend = backend.clone();
62            }
63            if let Some(device) = target.get("device") {
64                config.target.device = Some(device.clone());
65            }
66            if let Some(precision) = target.get("precision") {
67                config.target.precision = Some(precision.clone());
68            }
69        }
70
71        if let Some(budget) = kv_map.get("budget") {
72            if let Some(v) = budget.get("max_flops") {
73                config.budget.max_flops = v.parse().ok();
74            }
75            if let Some(v) = budget.get("max_memory_bytes") {
76                config.budget.max_memory_bytes = v.parse().ok();
77            }
78            if let Some(v) = budget.get("max_time_ms") {
79                config.budget.max_time_ms = v.parse().ok();
80            }
81            if let Some(v) = budget.get("min_fidelity") {
82                config.budget.min_fidelity = v.parse().ok();
83            }
84            if let Some(v) = budget.get("max_circuit_depth") {
85                config.budget.max_circuit_depth = v.parse().ok();
86            }
87        }
88
89        if let Some(opt) = kv_map.get("optimisation") {
90            if let Some(level) = opt.get("level") {
91                config.optimisation.level = match level.as_str() {
92                    "O0" | "0" => OptLevel::O0,
93                    "O1" | "1" => OptLevel::O1,
94                    "O2" | "2" => OptLevel::O2,
95                    "O3" | "3" => OptLevel::O3,
96                    _ => {
97                        return Err(ConfigError::InvalidValue {
98                            field: "optimisation.level".into(),
99                            value: level.clone(),
100                        })
101                    }
102                };
103            }
104            if let Some(max_iter) = opt.get("max_iterations") {
105                config.optimisation.max_iterations = max_iter.parse().unwrap_or(10);
106            }
107            if let Some(passes) = opt.get("passes") {
108                config.optimisation.passes = passes
109                    .split(',')
110                    .map(|s| s.trim().to_string())
111                    .filter(|s| !s.is_empty())
112                    .collect();
113            }
114            if let Some(disabled) = opt.get("disabled_passes") {
115                config.optimisation.disabled_passes = disabled
116                    .split(',')
117                    .map(|s| s.trim().to_string())
118                    .filter(|s| !s.is_empty())
119                    .collect();
120            }
121        }
122
123        if let Some(sim) = kv_map.get("simulation") {
124            if let Some(v) = sim.get("shape_propagation") {
125                config.simulation.enable_shape_propagation = v == "true";
126            }
127            if let Some(v) = sim.get("flop_counting") {
128                config.simulation.enable_flop_counting = v == "true";
129            }
130            if let Some(v) = sim.get("memory_analysis") {
131                config.simulation.enable_memory_analysis = v == "true";
132            }
133            if let Some(v) = sim.get("noise_simulation") {
134                config.simulation.enable_noise_simulation = v == "true";
135            }
136        }
137
138        if let Some(quantum) = kv_map.get("quantum") {
139            let qc = QuantumConfig {
140                topology: quantum
141                    .get("topology")
142                    .cloned()
143                    .unwrap_or_else(|| "linear".into()),
144                num_qubits: quantum
145                    .get("num_qubits")
146                    .and_then(|v| v.parse().ok())
147                    .unwrap_or(5),
148                error_mitigation: quantum.get("error_mitigation").cloned(),
149                shots: quantum.get("shots").and_then(|v| v.parse().ok()),
150                provider: quantum
151                    .get("provider")
152                    .and_then(|v| QuantumProvider::from_str_opt(v)),
153            };
154            config.quantum = Some(qc);
155        }
156
157        Ok(config)
158    }
159
160    pub fn parse_json(&self, json: &str) -> Result<LithConfig, ConfigError> {
161        serde_json::from_str(json).map_err(|e| ConfigError::ParseError {
162            line: 0,
163            message: format!("JSON parse error: {}", e),
164        })
165    }
166}
167
168impl Default for ConfigParser {
169    fn default() -> Self {
170        Self::new()
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_parse_lith_config() {
180        let src = r#"
181[target]
182backend = "cuda"
183device = "A100"
184precision = "fp16"
185
186[budget]
187max_flops = 1000000000
188max_memory_bytes = 80000000000
189
190[optimisation]
191level = O3
192max_iterations = 20
193
194[simulation]
195shape_propagation = true
196flop_counting = true
197"#;
198        let parser = ConfigParser::new();
199        let config = parser.parse(src).unwrap();
200        assert_eq!(config.target.backend, "cuda");
201        assert_eq!(config.optimisation.level, OptLevel::O3);
202        assert_eq!(config.budget.max_flops, Some(1000000000));
203    }
204
205    #[test]
206    fn test_parse_quantum_config() {
207        let src = r#"
208[target]
209backend = "qasm"
210
211[quantum]
212topology = "grid"
213num_qubits = 27
214shots = 4096
215
216[budget]
217min_fidelity = 0.95
218"#;
219        let parser = ConfigParser::new();
220        let config = parser.parse(src).unwrap();
221        assert!(config.quantum.is_some());
222        assert_eq!(config.quantum.as_ref().unwrap().num_qubits, 27);
223        assert_eq!(config.budget.min_fidelity, Some(0.95));
224    }
225}