Skip to main content

dune_netlist/
meas.rs

1//! `.measure` / `.meas` cards: the ngspice-47 grammar
2//! (`frontend/com_measure2.c`, `get_measure2` and the
3//! `measure_parse_*` helpers; the 3f5 has no measurements).
4//!
5//! Words are cut like ngspice's `gettok` (blanks, and commas outside
6//! parentheses, so `v(a,b)` stays one word) and an `=` with blanks
7//! around it is glued to its neighbours (`measure_parse_line` glues
8//! `val= 0.5`; we also accept `val =0.5` and `val = 0.5`). Then:
9//!
10//! ```text
11//! .meas ANALYSIS NAME TRIG|DELAY out VAL= [TD= FROM= TO=] RISE|FALL|CROSS=n|LAST
12//!                    | TRIG AT=x
13//!                     TARG … (same shapes)
14//! .meas ANALYSIS NAME WHEN out=val|out2 [TD= FROM= TO=] [RISE|FALL|CROSS=n|LAST]
15//! .meas ANALYSIS NAME FIND|DERIV out AT=x
16//! .meas ANALYSIS NAME FIND|DERIV out WHEN out2=val|out3 [… as WHEN]
17//! .meas ANALYSIS NAME AVG|MIN|MAX|MIN_AT|MAX_AT|PP|RMS|INTEG out [FROM= TO=]
18//! ```
19
20use crate::diag::Diagnostic;
21use crate::ir::{
22    MeasAnalysis, MeasCount, MeasCross, MeasEdge, MeasFunc, MeasLevel, MeasPoint, MeasStat,
23    MeasTrig, MeasWindow, Measure, OutVar,
24};
25use crate::span::Span;
26
27/// A malformed measurement: the message and the byte range of the
28/// offending word in the text that was parsed.
29#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
30#[error("{message}")]
31pub struct MeasSyntaxError {
32    pub message: String,
33    pub start: usize,
34    pub end: usize,
35}
36
37/// A remark on a well-formed measurement (a parameter ngspice
38/// accepts but ignores), with its byte range.
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct MeasWarning {
41    pub message: String,
42    pub start: usize,
43    pub end: usize,
44}
45
46#[derive(Clone, Debug)]
47struct Word {
48    /// Lowercased text, with glued `=` forms.
49    text: String,
50    start: usize,
51    end: usize,
52}
53
54type PResult<T> = Result<T, MeasSyntaxError>;
55
56fn err<T>(w: &Word, message: impl Into<String>) -> PResult<T> {
57    Err(MeasSyntaxError {
58        message: message.into(),
59        start: w.start,
60        end: w.end,
61    })
62}
63
64/// `gettok` words with their byte ranges, `=` glued.
65fn words(text: &str) -> Vec<Word> {
66    let b = text.as_bytes();
67    let mut raw: Vec<(usize, usize)> = Vec::new();
68    let mut i = 0;
69    while i < b.len() {
70        while i < b.len() && (b[i].is_ascii_whitespace() || b[i] == b',') {
71            i += 1;
72        }
73        if i >= b.len() {
74            break;
75        }
76        let start = i;
77        let mut paren = 0i32;
78        while i < b.len() && !b[i].is_ascii_whitespace() {
79            match b[i] {
80                b'(' => paren += 1,
81                b')' => paren -= 1,
82                b',' if paren < 1 => break,
83                _ => {}
84            }
85            i += 1;
86        }
87        if i > start {
88            raw.push((start, i));
89        }
90    }
91    let mut out: Vec<Word> = Vec::new();
92    let mut k = 0;
93    while k < raw.len() {
94        let (start, mut end) = raw[k];
95        let mut s = text[start..end].to_ascii_lowercase();
96        k += 1;
97        // Glue `a =b`, `a= b`, `a = b` into `a=b`.
98        loop {
99            let next_eq = k < raw.len() && text[raw[k].0..raw[k].1].starts_with('=');
100            if s.ends_with('=') && k < raw.len() || next_eq && !s.contains('=') {
101                s.push_str(&text[raw[k].0..raw[k].1].to_ascii_lowercase());
102                end = raw[k].1;
103                k += 1;
104                if s.ends_with('=') && k < raw.len() {
105                    continue;
106                }
107            }
108            break;
109        }
110        out.push(Word {
111            text: s,
112            start,
113            end,
114        });
115    }
116    out
117}
118
119/// Parse a measurement body — the function keyword and what follows
120/// it, as in `.meas tran NAME <body>` — e.g. `"trig v(1) val=0.5
121/// rise=1 targ v(2) val=0.5 rise=1"` or `"max v(out) from=1n"`.
122/// This is what the language bindings' direct measurement API takes.
123///
124/// On success also returns the remarks (parameters ngspice accepts
125/// and ignores, such as `td=` on `avg`).
126pub fn parse_measure_body(body: &str) -> Result<(MeasFunc, Vec<MeasWarning>), MeasSyntaxError> {
127    let ws = words(body);
128    let Some(first) = ws.first() else {
129        return Err(MeasSyntaxError {
130            message: "empty measurement".into(),
131            start: 0,
132            end: body.len(),
133        });
134    };
135    let mut warnings = Vec::new();
136    let func = parse_func(first, &ws[1..], &mut warnings)?;
137    Ok((func, warnings))
138}
139
140/// Parse a whole `.measure` card (`.meas ANALYSIS NAME FUNCTION …`)
141/// from its text; `span_of` maps byte ranges of `text` to source
142/// spans.
143pub(crate) fn parse_measure_card(
144    text: &str,
145    card_span: Span,
146    span_of: &dyn Fn(usize, usize) -> Span,
147    diags: &mut Vec<Diagnostic>,
148) -> Option<Measure> {
149    let ws = words(text);
150    let fail = |diags: &mut Vec<Diagnostic>, e: MeasSyntaxError| {
151        diags.push(Diagnostic::error(
152            format!(".measure: {}", e.message),
153            span_of(e.start, e.end),
154        ));
155    };
156    // ws[0] is `.meas`/`.measure`.
157    let Some(an) = ws.get(1) else {
158        diags.push(Diagnostic::error(
159            ".measure needs an analysis (tran, ac, dc or sp), a name and a function",
160            card_span,
161        ));
162        return None;
163    };
164    let analysis = match an.text.as_str() {
165        "tran" => MeasAnalysis::Tran,
166        "ac" => MeasAnalysis::Ac,
167        "dc" => MeasAnalysis::Dc,
168        "sp" => MeasAnalysis::Sp,
169        other => {
170            fail(
171                diags,
172                MeasSyntaxError {
173                    message: format!("unrecognized analysis type `{other}` (tran, ac, dc or sp)"),
174                    start: an.start,
175                    end: an.end,
176                },
177            );
178            return None;
179        }
180    };
181    let (Some(name), Some(func_word)) = (ws.get(2), ws.get(3)) else {
182        diags.push(Diagnostic::error(
183            ".measure needs a result name and a function",
184            card_span,
185        ));
186        return None;
187    };
188    if name.text.contains('=') {
189        fail(
190            diags,
191            MeasSyntaxError {
192                message: format!("`{}` is not a result name", name.text),
193                start: name.start,
194                end: name.end,
195            },
196        );
197        return None;
198    }
199    let mut warnings = Vec::new();
200    match parse_func(func_word, &ws[4..], &mut warnings) {
201        Ok(func) => {
202            for w in warnings {
203                diags.push(Diagnostic::warning(
204                    format!(".measure {}: {}", name.text, w.message),
205                    span_of(w.start, w.end),
206                ));
207            }
208            Some(Measure {
209                analysis,
210                name: name.text.clone(),
211                func,
212                keyword: func_word.text.clone(),
213                text: text.to_string(),
214                span: card_span,
215            })
216        }
217        Err(e) => {
218            fail(diags, e);
219            None
220        }
221    }
222}
223
224/// `measure_function_type`, then the per-function parse.
225fn parse_func(fw: &Word, rest: &[Word], warnings: &mut Vec<MeasWarning>) -> PResult<MeasFunc> {
226    let st = match fw.text.as_str() {
227        "trig" | "delay" | "targ" => return parse_delay(fw, rest),
228        "when" => {
229            let Some(cond) = rest.first() else {
230                return err(fw, "when needs `out=value` or `out=out2`");
231            };
232            return Ok(MeasFunc::When(parse_when(cond, &rest[1..])?));
233        }
234        "find" | "deriv" | "derivative" => {
235            let Some(var_w) = rest.first() else {
236                return err(fw, format!("{} needs an output variable", fw.text));
237            };
238            let var = outvar(var_w)?;
239            let at = parse_point(fw, var_w, &rest[1..])?;
240            return Ok(if fw.text == "find" {
241                MeasFunc::Find { var, at }
242            } else {
243                MeasFunc::Deriv { var, at }
244            });
245        }
246        "avg" => Some(MeasStat::Avg),
247        "min" => Some(MeasStat::Min),
248        "max" => Some(MeasStat::Max),
249        "min_at" => Some(MeasStat::MinAt),
250        "max_at" => Some(MeasStat::MaxAt),
251        "pp" => Some(MeasStat::Pp),
252        "rms" => Some(MeasStat::Rms),
253        "integ" | "integral" => Some(MeasStat::Integ),
254        t if t.starts_with("param") || t.starts_with("expr") => {
255            return err(
256                fw,
257                "`param`/`expr` measurements (expressions) are not supported yet",
258            );
259        }
260        "err" | "err1" | "err2" | "err3" => {
261            return err(
262                fw,
263                format!(
264                    "function `{}` is not supported (ngspice-47 does not implement it either)",
265                    fw.text
266                ),
267            );
268        }
269        _ => None,
270    };
271    let Some(stat) = st else {
272        return err(fw, format!("no such function as `{}`", fw.text));
273    };
274    let Some(var_w) = rest.first() else {
275        return err(fw, format!("{} needs an output variable", fw.text));
276    };
277    let var = outvar(var_w)?;
278    let p = std_params(&rest[1..])?;
279    for (key, w) in &p.given {
280        if !matches!(key.as_str(), "from" | "to") {
281            warnings.push(MeasWarning {
282                message: format!("`{key}` has no effect on {} (as in ngspice)", fw.text),
283                start: w.start,
284                end: w.end,
285            });
286        }
287    }
288    Ok(MeasFunc::Stat {
289        stat,
290        var,
291        window: p.window,
292    })
293}
294
295/// `TRIG … TARG …`.
296fn parse_delay(fw: &Word, rest: &[Word]) -> PResult<MeasFunc> {
297    let Some(targ_at) = rest.iter().position(|w| w.text == "targ") else {
298        return err(fw, "trig needs a matching targ clause");
299    };
300    let trig = parse_trigtarg(fw, &rest[..targ_at], "trig")?;
301    let targ = parse_trigtarg(&rest[targ_at], &rest[targ_at + 1..], "targ")?;
302    Ok(MeasFunc::Delay { trig, targ })
303}
304
305/// `measure_parse_trigtarg`: `out VAL= … RISE=n` or `AT=x`.
306fn parse_trigtarg(kw: &Word, ws: &[Word], what: &str) -> PResult<MeasTrig> {
307    let Some(first) = ws.first() else {
308        return err(kw, format!("bad syntax of `{what}`: nothing follows it"));
309    };
310    let (var, params) = if first.text.starts_with("at") && first.text.contains('=') {
311        (None, ws)
312    } else {
313        (Some(outvar(first)?), &ws[1..])
314    };
315    let p = std_params(params)?;
316    if let Some(at) = p.at {
317        return Ok(MeasTrig::At(at));
318    }
319    let Some(var) = var else {
320        return err(first, format!("{what}: missing output variable"));
321    };
322    if p.edge == MeasEdge::Any {
323        let w = ws.last().unwrap_or(first);
324        return err(w, format!("{what}: at, rise, fall or cross must be given"));
325    }
326    Ok(MeasTrig::Cross(MeasCross {
327        var,
328        // ngspice's TMALLOC'd (zeroed) structure: VAL defaults to 0.
329        level: MeasLevel::Value(p.val.unwrap_or(0.0)),
330        edge: p.edge,
331        window: p.window,
332    }))
333}
334
335/// `measure_parse_when`: `out=val` or `out=out2`, then the standard
336/// parameters.
337fn parse_when(cond: &Word, rest: &[Word]) -> PResult<MeasCross> {
338    let Some((lhs, rhs)) = cond.text.split_once('=') else {
339        return err(
340            cond,
341            "bad syntax of when: expected `out=value` or `out=out2`",
342        );
343    };
344    if lhs.is_empty() || rhs.is_empty() {
345        return err(
346            cond,
347            "bad syntax of when: expected `out=value` or `out=out2`",
348        );
349    }
350    let var = outvar_text(lhs, cond)?;
351    let level = match crate::scan::evaluate(rhs) {
352        Some(v) if !has_arith(rhs) => MeasLevel::Value(v),
353        _ if has_arith(rhs) => {
354            return err(cond, format!("expressions like `{rhs}` are not supported"));
355        }
356        _ => MeasLevel::Var(outvar_text(rhs, cond)?),
357    };
358    let p = std_params(rest)?;
359    Ok(MeasCross {
360        var,
361        level,
362        edge: p.edge,
363        window: p.window,
364    })
365}
366
367/// After `FIND out` / `DERIV out`: `AT=x` or `WHEN …`.
368fn parse_point(fw: &Word, var_w: &Word, rest: &[Word]) -> PResult<MeasPoint> {
369    let Some(next) = rest.first() else {
370        return err(
371            var_w,
372            format!("{} needs `at=` or `when` after the variable", fw.text),
373        );
374    };
375    if next.text == "when" {
376        let Some(cond) = rest.get(1) else {
377            return err(next, "when needs `out=value` or `out=out2`");
378        };
379        return Ok(MeasPoint::When(parse_when(cond, &rest[2..])?));
380    }
381    if next.text.starts_with("at=") {
382        let p = std_params(rest)?;
383        if let Some(at) = p.at {
384            return Ok(MeasPoint::At(at));
385        }
386    }
387    err(
388        next,
389        format!("{} needs `at=` or `when` after the variable", fw.text),
390    )
391}
392
393/// `str_has_arith_char2`.
394fn has_arith(s: &str) -> bool {
395    let s = s.strip_prefix(['+', '-']).unwrap_or(s);
396    s.chars().any(|c| "*/<>?:|&^!%\\".contains(c))
397}
398
399fn outvar(w: &Word) -> PResult<OutVar> {
400    if w.text.contains('=') {
401        return err(w, format!("expected an output variable, got `{}`", w.text));
402    }
403    outvar_text(&w.text, w)
404}
405
406fn outvar_text(text: &str, w: &Word) -> PResult<OutVar> {
407    let text = text.trim_matches(['"', '\'']);
408    if text.is_empty() {
409        return err(w, "missing output variable");
410    }
411    let mut d = Vec::new();
412    let v = crate::dot::parse_outvar(text, Span::new(crate::span::FileId(0), 0, 0), &mut d);
413    if let Some(first) = d.first() {
414        return err(w, first.message.clone());
415    }
416    Ok(v)
417}
418
419/// What `measure_parse_stdParams` collects.
420struct StdParams {
421    edge: MeasEdge,
422    val: Option<f64>,
423    at: Option<f64>,
424    window: MeasWindow,
425    /// Every key given, with its word (for "no effect" remarks).
426    given: Vec<(String, Word)>,
427}
428
429/// `measure_parse_stdParams`: `NAME=VALUE` words (`RISE FALL CROSS
430/// VAL TD FROM TO AT`) and a bare `LAST` (= `CROSS=LAST`). The last
431/// of RISE/FALL/CROSS given wins. Counts round like the C
432/// (`floor(v + 0.5)`) and must be at least 1.
433fn std_params(ws: &[Word]) -> PResult<StdParams> {
434    let mut p = StdParams {
435        edge: MeasEdge::Any,
436        val: None,
437        at: None,
438        window: MeasWindow::default(),
439        given: Vec::new(),
440    };
441    for w in ws {
442        let Some((name, value)) = w.text.split_once('=') else {
443            if w.text == "last" {
444                p.edge = MeasEdge::Cross(MeasCount::Last);
445                p.given.push(("cross".into(), w.clone()));
446                continue;
447            }
448            return err(
449                w,
450                format!("bad syntax at `{}`: equal sign missing?", w.text),
451            );
452        };
453        if value.is_empty() {
454            return err(w, format!("missing value for `{name}`"));
455        }
456        let count = |w: &Word| -> PResult<MeasCount> {
457            if value == "last" {
458                return Ok(MeasCount::Last);
459            }
460            let Some(v) = crate::scan::evaluate(value) else {
461                return err(w, format!("cannot evaluate `{value}` for `{name}`"));
462            };
463            let n = (v + 0.5).floor();
464            if !(1.0..=f64::from(u32::MAX)).contains(&n) {
465                return err(
466                    w,
467                    format!("`{name}` must be a count of at least 1 or `last`"),
468                );
469            }
470            Ok(MeasCount::Nth(n as u32))
471        };
472        let num = |w: &Word| -> PResult<f64> {
473            match crate::scan::evaluate(value) {
474                Some(v) if v.is_finite() => Ok(v),
475                _ => err(w, format!("cannot evaluate `{value}` for `{name}`")),
476            }
477        };
478        match name {
479            "rise" => p.edge = MeasEdge::Rise(count(w)?),
480            "fall" => p.edge = MeasEdge::Fall(count(w)?),
481            "cross" => p.edge = MeasEdge::Cross(count(w)?),
482            "val" => p.val = Some(num(w)?),
483            "td" => p.window.td = Some(num(w)?),
484            "from" => p.window.from = Some(num(w)?),
485            "to" => p.window.to = Some(num(w)?),
486            "at" => p.at = Some(num(w)?),
487            _ => return err(w, format!("no such parameter as `{name}`")),
488        }
489        p.given.push((name.to_string(), w.clone()));
490    }
491    Ok(p)
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497    use crate::ir::OutVar;
498
499    fn v(node: &str) -> OutVar {
500        OutVar::Voltage {
501            node: node.into(),
502            reference: None,
503            suffix: None,
504        }
505    }
506
507    fn body(s: &str) -> MeasFunc {
508        parse_measure_body(s)
509            .unwrap_or_else(|e| panic!("{s}: {e}"))
510            .0
511    }
512
513    #[test]
514    fn words_glue_equals_and_keep_parens() {
515        let w: Vec<String> = words("WHEN v(a,b) = 0.5 val= 1 td =2n, rise=LAST")
516            .into_iter()
517            .map(|w| w.text)
518            .collect();
519        assert_eq!(w, ["when", "v(a,b)=0.5", "val=1", "td=2n", "rise=last"]);
520    }
521
522    #[test]
523    fn delay_card() {
524        let f = body("trig v(1) val=0.1 rise=1 targ v(2) val=0.9 fall=2 td=1n");
525        let MeasFunc::Delay { trig, targ } = f else {
526            panic!()
527        };
528        assert_eq!(
529            trig,
530            MeasTrig::Cross(MeasCross {
531                var: v("1"),
532                level: MeasLevel::Value(0.1),
533                edge: MeasEdge::Rise(MeasCount::Nth(1)),
534                window: MeasWindow::default(),
535            })
536        );
537        let MeasTrig::Cross(t) = targ else { panic!() };
538        assert_eq!(t.edge, MeasEdge::Fall(MeasCount::Nth(2)));
539        assert!((t.window.td.unwrap() - 1e-9).abs() < 1e-24);
540        let f = body("trig at=2n targ v(2) val=0.5 cross=last");
541        let MeasFunc::Delay { trig, targ } = f else {
542            panic!()
543        };
544        assert_eq!(trig, MeasTrig::At(2e-9));
545        let MeasTrig::Cross(t) = targ else { panic!() };
546        assert_eq!(t.edge, MeasEdge::Cross(MeasCount::Last));
547    }
548
549    #[test]
550    fn when_find_deriv_and_stats() {
551        let MeasFunc::When(c) = body("when v(out)=v(ref) cross=2 from=1u") else {
552            panic!()
553        };
554        assert_eq!(c.level, MeasLevel::Var(v("ref")));
555        assert_eq!(c.edge, MeasEdge::Cross(MeasCount::Nth(2)));
556        let MeasFunc::Find { at, .. } = body("find i(vin) at=5u") else {
557            panic!()
558        };
559        let MeasPoint::At(x) = at else { panic!() };
560        assert!((x - 5e-6).abs() < 1e-20);
561        let MeasFunc::Find { var, at } = body("find vdb(out) when vp(out)=-0.785 last") else {
562            panic!()
563        };
564        assert!(matches!(
565            var,
566            OutVar::Voltage {
567                suffix: Some(_),
568                ..
569            }
570        ));
571        let MeasPoint::When(c) = at else { panic!() };
572        assert_eq!(c.edge, MeasEdge::Cross(MeasCount::Last));
573        assert!(matches!(
574            body("deriv v(2) at=1m"),
575            MeasFunc::Deriv {
576                at: MeasPoint::At(_),
577                ..
578            }
579        ));
580        let MeasFunc::Stat { stat, window, .. } = body("INTEGRAL v(2) from=1u to=2u") else {
581            panic!()
582        };
583        assert_eq!(stat, MeasStat::Integ);
584        assert_eq!(window.to, Some(2e-6));
585        let (_, warns) = parse_measure_body("avg v(2) td=1u").unwrap();
586        assert_eq!(warns.len(), 1, "{warns:?}");
587    }
588
589    #[test]
590    fn malformed_bodies_point_at_the_word() {
591        let cases = [
592            ("trig v(1) val=0.5 rise=1", "targ"),
593            (
594                "trig v(1) val=0.5 targ v(2) val=1 rise=1",
595                "rise, fall or cross",
596            ),
597            ("when v(1)", "out=value"),
598            ("when v(1)=2*v(3)", "expressions"),
599            ("find v(1)", "at="),
600            ("max", "output variable"),
601            ("max v(1) bogus=3", "no such parameter"),
602            ("max v(1) from", "equal sign"),
603            ("when v(1)=0.5 rise=0", "at least 1"),
604            ("when v(1)=0.5 rise=x", "cannot evaluate"),
605            ("frobnicate v(1)", "no such function"),
606            ("param='a+b'", "not supported"),
607            ("deriv v(1) at=", "missing value"),
608            ("", "empty"),
609        ];
610        for (text, want) in cases {
611            let e = parse_measure_body(text).expect_err(text);
612            assert!(e.message.contains(want), "{text}: {}", e.message);
613            assert!(e.end <= text.len() && e.start <= e.end, "{text}: {e:?}");
614        }
615        let e = parse_measure_body("max v(1) bogus=3").unwrap_err();
616        assert_eq!((e.start, e.end), (9, 16));
617    }
618}