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 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 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 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}