reda_sp/parse/
mod.rs

1mod base;
2mod components;
3mod source;
4mod simulate;
5mod measures;
6mod error;
7mod subckt;
8use std::path::Path;
9
10use nom::branch::alt;
11
12pub use base::*;
13pub use components::*;
14pub use source::*;
15pub use simulate::*;
16pub use measures::*;
17pub use error::*;
18pub use subckt::*;
19
20use crate::model::{Component, Instance, MeasureCommand, Model, SimCommand, Source, Spice, Subckt};
21use nom::{error::convert_error, Err};
22use nom::combinator::map;
23
24pub fn load_spice<P: AsRef<Path>>(path: P) -> Result<Spice, ParseError> {
25    let input = std::fs::read_to_string(path.as_ref())?;
26    read_spice(&input)
27}
28
29pub fn read_spice(full_input: &str) -> Result<Spice, ParseError> {
30    let mut spice = Spice::default();
31    let mut input = full_input;
32
33    while !input.trim_start().is_empty() {
34        input = skip_blank_or_comment_lines(input);
35        if input.is_empty() {
36            break;
37        }
38
39        match statement(input) {
40            Ok((rest, stmt)) => {
41                match stmt {
42                    ParsedStatement::Component(c) => spice.components.push(c),
43                    ParsedStatement::Source(s) => spice.sources.push(s),
44                    ParsedStatement::SimCommand(s) => spice.simulation.push(s),
45                    ParsedStatement::Measure(m) => spice.measures.push(m),
46                    ParsedStatement::Instance(i) => spice.instances.push(i),
47                    ParsedStatement::Subckt(s) => spice.subckts.push(s),
48                    ParsedStatement::Model(m) => spice.model.push(m),
49                }
50                input = rest;
51            }
52            Err(Err::Failure(e)) => {
53                let first_error_input = e.errors.get(0).map(|(slice, _)| *slice).unwrap_or(input);
54                let err_text = convert_error(full_input, e);
55                let line_num = get_error_line(full_input, first_error_input);
56                return Err(ParseError::Parse(format!("Error at line {}:\n{}", line_num, err_text)));
57            }
58            Err(Err::Error(_)) => {
59                let line_num = get_error_line(full_input, input);
60                return Err(ParseError::Parse(format!(
61                    "At line {}: Unknown statement: {}",
62                    line_num,
63                    preview_line(input)
64                )));
65            }
66            Err(Err::Incomplete(e)) => {
67                return Err(ParseError::Parse(format!("Incomplete: {:?}", e)));
68            }
69        }
70    }
71
72    Ok(spice)
73}
74
75fn get_error_line(full_input: &str, error_input: &str) -> usize {
76    let err_pos = error_input.as_ptr() as usize - full_input.as_ptr() as usize;
77    let line_num = full_input[..err_pos].chars().filter(|&c| c == '\n').count() + 1;
78    line_num
79}
80pub enum ParsedStatement {
81    Component(Component),
82    Source(Source),
83    SimCommand(SimCommand),
84    Measure(MeasureCommand),
85    Instance(Instance),
86    Subckt(Subckt),
87    Model(Model),
88}
89
90fn statement(input: &str) -> NomResult<ParsedStatement> {
91    alt((
92        map(component, ParsedStatement::Component),
93        map(source, ParsedStatement::Source),
94        map(sim_command, ParsedStatement::SimCommand),
95        map(measure_command, ParsedStatement::Measure),
96        map(instance, ParsedStatement::Instance),
97        map(subckt, ParsedStatement::Subckt),
98        map(model, ParsedStatement::Model),
99    ))(input)
100}
101
102pub fn skip_blank_or_comment_lines(mut input: &str) -> &str {
103    loop {
104        input = input.trim_start();
105        if input.is_empty() {
106            return "";
107        }
108
109        if let Ok((rest, _)) = comment(input) {
110            input = rest;
111            continue;
112        }
113
114        if input.starts_with('\n') {
115            input = &input[1..];
116            continue;
117        }
118
119        return input;
120    }
121}
122
123fn preview_line(input: &str) -> &str {
124    input.lines().next().unwrap_or(input).trim()
125}
126
127#[cfg(test)]
128mod tests {
129    use crate::model::{Component, MeasureCommand, SimCommand, SourceValue};
130    use reda_unit::u;
131
132    use super::*;
133
134    #[test]
135    fn test_read_spice_with_subckt_and_instance() {
136        let input = r#"
137    * Test circuit
138    .SUBCKT inv in out vdd gnd
139    M1 out in vdd vdd pmos L=1u W=2u
140    M2 out in gnd gnd nmos L=1u W=1u
141    .ENDS
142    
143    X1 a b vdd gnd inv
144    Vdd vdd 0 DC 5
145    .TRAN 1n 10n
146    "#;
147        
148        let spice = read_spice(input).map_err(|e| {
149            println!("{}", e);
150        }).unwrap();
151        
152        assert_eq!(spice.subckts.len(), 1);
153        assert_eq!(spice.instances.len(), 1);
154        assert_eq!(spice.sources.len(), 1);
155        assert_eq!(spice.simulation.len(), 1);
156    }
157
158    #[test]
159    fn test_read_spice_basic() {
160        let input = r#"
161            * Simple resistor circuit
162            R1 in out 10k
163            C1 out 0 1u
164            V1 in 0 DC 5
165            .TRAN 1n 10n
166            .MEAS TRAN rise_time TRIG V(out) VAL=0.2 RISE=1 TARG V(out) VAL=0.8 RISE=1
167        "#;
168
169        let spice = read_spice(input).map_err(|e| {
170            println!("{}", e);
171        }).unwrap();
172
173        // Check number of parsed objects
174        assert_eq!(spice.components.len(), 2);  // R1, C1
175        assert_eq!(spice.sources.len(), 1);     // V1
176        assert_eq!(spice.simulation.len(), 1);  // .TRAN
177        assert_eq!(spice.measures.len(), 1);    // .MEAS
178
179        // Validate parsed resistor
180        if let Component::R(r) = &spice.components[0] {
181            assert_eq!(r.name, "1");
182            assert_eq!(r.node_pos, "in");
183            assert_eq!(r.node_neg, "out");
184            assert_eq!(r.resistance, u!(10. kΩ));
185        } else {
186            panic!("Expected resistor");
187        }
188
189        // Validate parsed source
190        assert_eq!(&spice.sources[0].name, "1");
191        assert_eq!(&spice.sources[0].node_pos, "in");
192        assert_eq!(&spice.sources[0].node_neg, "0");
193        if let SourceValue::DcVoltage(dc) = &spice.sources[0].value {
194            assert_eq!(*dc, u!(5. V));
195        } else {
196            panic!("Expected DC voltage source");
197        }
198
199        // Validate .TRAN
200        if let SimCommand::Tran(tran) = &spice.simulation[0] {
201            assert_eq!(tran.t_step, u!(1. ns));
202            assert_eq!(tran.t_stop, u!(10. ns));
203        } else {
204            panic!("Expected .TRAN command");
205        }
206
207        // Validate .MEAS
208        if let MeasureCommand::Rise(rise) = &spice.measures[0] {
209            assert_eq!(rise.name, "rise_time");
210        } else {
211            panic!("Expected .MEAS rise command");
212        }
213    }
214
215    #[test]
216    fn test_unknown_statement_error() {
217        let input = "
218            R1 in out 1k
219            .DC V1 0 10 1
220            ??? this line is invalid
221            .TRAN 1n 10n
222        ";
223        let result = read_spice(input);
224        assert!(matches!(result, Err(ParseError::Parse(_))));
225        println!("{:?}", result.unwrap_err());
226    }
227    
228    #[test]
229    fn test_subckt_parse_failure() {
230        let input = "
231            .SUBCKT
232            R1 a b 1k
233            .ENDS
234        ";
235        let result = read_spice(input);
236        assert!(matches!(result, Err(ParseError::Parse(_))));
237    }
238
239    #[test]
240    fn test_read_spice_basic_components() {
241        let input = r#"
242            R1 1 0 1k
243            C1 1 0 1u
244            L1 1 0 10u
245            D1 1 0 Dmodel
246            Q1 3 2 0 NPN
247            M1 3 2 1 0 nmos L=0.18u W=1u
248        "#;
249
250        let spice = read_spice(input).unwrap();
251        assert_eq!(spice.components.len(), 6);
252    }
253
254    #[test]
255    fn test_read_spice_sources_dc() {
256        let input = r#"
257            V1 1 0 DC 5
258            I1 2 0 0.001
259        "#;
260
261        let spice = read_spice(input).unwrap();
262        assert_eq!(spice.sources.len(), 2);
263    }
264
265    #[test]
266    fn test_read_spice_sources_sin_pwl() {
267        let input = r#"
268            Vsin 1 0 SIN(0 5 1k)
269            Vpwl 2 0 PWL(0 0 1u 5 2u 0)
270        "#;
271
272        let spice = read_spice(input).unwrap();
273        assert_eq!(spice.sources.len(), 2);
274    }
275
276    #[test]
277    fn test_read_spice_sim_commands() {
278        let input = r#"
279            .DC Vin 0 5 0.1
280            .AC LIN 10 1 1k
281            .TRAN 1n 10n 0n 1n
282        "#;
283
284        let spice = read_spice(input).unwrap();
285        assert_eq!(spice.simulation.len(), 3);
286    }
287
288    #[test]
289    fn test_read_spice_measures() {
290        let input = r#"
291            .model NMOS1 NMOS (LEVEL=1 VTO=0.7 KP=20u LAMBDA=0.02)
292            .model PMOS1 PMOS (LEVEL=1 VTO=-0.7 KP=10u LAMBDA=0.02)
293            .MEAS TRAN t_rise TRIG V(1) VAL=0.2 RISE=1 TARG V(1) VAL=0.8 RISE=1
294            .MEAS TRAN avgval AVG V(1) FROM=1n TO=10n
295            .MEAS TRAN when FIND I(V1) WHEN V(2)=1.0
296        "#;
297
298        let spice = read_spice(input).unwrap();
299        assert_eq!(spice.measures.len(), 3);
300    }
301
302    #[test]
303    fn test_read_spice_subckt_and_instance() {
304        let input = r#"
305.SUBCKT inverter in out vdd gnd
306M1 out in vdd vdd pmos 
307+ L=1u W=2u
308M2 out in gnd gnd nmos L=1u W=1u
309.ENDS
310
311Xinv a b vdd gnd inverter
312        "#;
313
314        let spice = read_spice(input).unwrap();
315        assert_eq!(spice.subckts.len(), 1);
316        assert_eq!(spice.instances.len(), 1);
317        assert_eq!(spice.subckts[0].ports, ["in", "out", "vdd", "gnd"]);
318        assert_eq!(spice.subckts[0].components.len(), 2);
319    }
320
321    #[test]
322    fn test_read_spice_line_continuation() {
323        let input = r#"
324V1 1 0 
325+ DC 5
326        "#;
327
328        let spice = read_spice(input).unwrap();
329        assert_eq!(spice.sources.len(), 1);
330    }
331
332    #[test]
333    fn test_read_spice_comment_and_blank_lines() {
334        let input = r#"
335* This is a comment
336R1 1 0 1k
337
338* Another comment
339C1 1 0 1u
340        "#;
341
342        let spice = read_spice(input).unwrap();
343        assert_eq!(spice.components.len(), 2);
344    }
345
346    #[test]
347    fn test_read_spice_failure_invalid_line() {
348        let input = r#"
349R1 1 0 1k
350THIS_IS_INVALID
351        "#;
352
353        let res = read_spice(input);
354        assert!(res.is_err());
355        if let Err(ParseError::Parse(msg)) = res {
356            assert!(msg.contains("Unknown statement"));
357        } else {
358            panic!("Expected ParseError::Parse");
359        }
360    }
361}
362