1use 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#[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#[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 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
64fn 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 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
119pub 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
140pub(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 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
224fn 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
295fn 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
305fn 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 level: MeasLevel::Value(p.val.unwrap_or(0.0)),
330 edge: p.edge,
331 window: p.window,
332 }))
333}
334
335fn 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
367fn 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
393fn 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
419struct StdParams {
421 edge: MeasEdge,
422 val: Option<f64>,
423 at: Option<f64>,
424 window: MeasWindow,
425 given: Vec<(String, Word)>,
427}
428
429fn 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}