1#![forbid(unsafe_code)]
63
64mod connect;
65mod spans;
66
67use std::collections::{BTreeMap, HashMap, HashSet};
68use std::fmt;
69use std::ops::Range;
70
71use rpic_core::ir::{Drawing, MacroCall, PlaceInfo, Shape};
72use rpic_core::{CompileOptions, IncludePolicy};
73
74use crate::connect::{Pt, UnionFind, join_geometry};
75
76pub const PRELUDE: &str = include_str!("spice.pic");
80
81pub const DEFAULT_SNAP: f64 = 1e-3;
83
84const GROUND_MARKER: &str = "Gnd";
86const ANCHOR_MARKER: &str = "At";
89const COUPLING_MARKER: &str = "Mutual";
92const ELEMENT_MARKERS: &[&str] = &[
95 "P", "N", "Cp", "Cn", "C", "B", "E", "D", "G", "S", "Inp", "Inm", "Out", "P1", "N1", "P2",
96 "N2", "P3", "P4", "P5", "P6", "P7", "At", "Mutual",
97];
98const BJT4_PINS: &[&str] = &["C", "B", "E", "S"];
100const SUBCKT_PINS: &[&str] = &["P1", "P2", "P3", "P4", "P5", "P6", "P7"];
103const DISPLAY_MARKER: &str = "Disp";
105const DISPLAY_MACROS: &[&str] = &[
109 "resistor",
110 "capacitor",
111 "inductor",
112 "diode",
113 "vsource",
114 "acsource",
115 "battery",
116 "isource",
117 "cccs",
118 "ccvs",
119 "vcvs",
120 "vccs",
121 "npn",
122 "pnp",
123 "npn4",
124 "pnp4",
125 "nmos",
126 "pmos",
127 "nmos4",
128 "pmos4",
129 "njfet",
130 "pjfet",
131 "nmes",
132 "pmes",
133 "opamp",
134 "subckt2",
135 "subckt3",
136 "subckt4",
137 "subckt5",
138 "subckt6",
139 "subckt7",
140];
141const LETTERS: &str = "R C L V I D Q M J Z S W X T O E F G H K";
143
144#[derive(Debug, Clone, Copy)]
146pub struct ExtractOptions<'a> {
147 pub snap: f64,
150 pub prelude: Option<&'a str>,
153}
154
155impl Default for ExtractOptions<'_> {
156 fn default() -> Self {
157 Self {
158 snap: DEFAULT_SNAP,
159 prelude: None,
160 }
161 }
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct PicSpan {
168 pub file: Option<String>,
171 pub line: u32,
172 pub col: u32,
173 pub end_col: u32,
174}
175
176impl fmt::Display for PicSpan {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 if let Some(file) = &self.file {
179 write!(f, "{file}:")?;
180 }
181 write!(f, "{}:{}", self.line, self.col)
182 }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub enum PicSeverity {
188 Error,
191 Warning,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum PicDiagKind {
198 DanglingPin,
200 FloatingNet,
202 MissingValue,
204 DuplicateRefdes,
207 UnknownElement,
209 MissingPin,
211 UnlabeledElement,
213 NodeNameConflict,
215 Prelude,
217}
218
219#[derive(Debug, Clone, PartialEq)]
221pub struct PicDiagnostic {
222 pub severity: PicSeverity,
223 pub kind: PicDiagKind,
224 pub message: String,
225 pub span: Option<PicSpan>,
227}
228
229impl fmt::Display for PicDiagnostic {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 let sev = match self.severity {
232 PicSeverity::Error => "error",
233 PicSeverity::Warning => "warning",
234 };
235 match &self.span {
236 Some(s) => write!(f, "{s}: {sev}: {}", self.message),
237 None => write!(f, "{sev}: {}", self.message),
238 }
239 }
240}
241
242#[derive(Debug, Clone, PartialEq, thiserror::Error)]
244pub enum PicError {
245 #[error("pic: {message}")]
247 Compile {
248 message: String,
249 span: Option<PicSpan>,
250 },
251 #[error("figure not marked `spice = 1`; dune-pic only extracts figures that opt in")]
253 NotMarked,
254}
255
256#[derive(Debug, Clone, PartialEq)]
258pub struct ElementOverlay {
259 pub refdes: String,
260 pub shapes: Range<usize>,
262 pub pins: Vec<PinOverlay>,
265 pub value: Option<ValueSpanOverlay>,
280 pub params: Vec<ParamSpanOverlay>,
297 pub principal: Option<String>,
304}
305
306#[derive(Debug, Clone, PartialEq, Eq)]
314pub struct PicRange {
315 pub line: u32,
316 pub col: u32,
317 pub end_line: u32,
318 pub end_col: u32,
319}
320
321#[derive(Debug, Clone, PartialEq, Eq)]
323pub struct ValueSpanOverlay {
324 pub span: PicRange,
327 pub text: String,
331}
332
333#[derive(Debug, Clone, PartialEq, Eq)]
335pub struct ParamSpanOverlay {
336 pub name: String,
338 pub span: PicRange,
340 pub text: String,
342}
343
344#[derive(Debug, Clone, Copy, PartialEq)]
363pub struct CanvasOverlay {
364 pub w: f64,
366 pub h: f64,
367 pub origin: [f64; 2],
369 pub rescale: f64,
373}
374
375impl Default for CanvasOverlay {
376 fn default() -> Self {
377 Self {
378 w: 0.0,
379 h: 0.0,
380 origin: [0.0, 0.0],
381 rescale: 1.0,
382 }
383 }
384}
385
386impl CanvasOverlay {
387 pub fn to_svg(&self, x: f64, y: f64) -> (f64, f64) {
389 (
390 (x + self.origin[0]) * SVG_PPI,
391 (self.h - (y + self.origin[1])) * SVG_PPI,
392 )
393 }
394}
395
396const SVG_PPI: f64 = 96.0;
398
399#[derive(Debug, Clone, PartialEq)]
405pub struct PinOverlay {
406 pub name: String,
409 pub net: String,
411 pub x: f64,
416 pub y: f64,
417}
418
419impl ElementOverlay {
420 pub fn ids(&self) -> String {
422 format!(
423 "s{}..s{}",
424 self.shapes.start,
425 self.shapes.end.saturating_sub(1)
426 )
427 }
428}
429
430#[derive(Debug, Clone, PartialEq)]
433pub struct NetOverlay {
434 pub name: String,
435 pub shapes: Vec<usize>,
436}
437
438#[derive(Debug, Clone, PartialEq, Default)]
440pub struct OverlayMap {
441 pub elements: Vec<ElementOverlay>,
443 pub nets: Vec<NetOverlay>,
445 pub canvas: CanvasOverlay,
447}
448
449#[derive(Debug, Clone, PartialEq)]
451pub struct Extraction {
452 pub netlist: String,
454 pub map: OverlayMap,
455 pub diagnostics: Vec<PicDiagnostic>,
456}
457
458impl Extraction {
459 pub fn is_clean(&self) -> bool {
461 self.diagnostics
462 .iter()
463 .all(|d| d.severity != PicSeverity::Error)
464 }
465}
466
467pub fn render_svg(src: &str, opts: &ExtractOptions) -> Result<String, PicError> {
471 let c = compile(src, opts)?;
472 Ok(rpic_core::to_svg(&c.drawing))
473}
474
475pub fn extract(src: &str, opts: &ExtractOptions) -> Result<Extraction, PicError> {
477 let mut c = compile(src, opts)?;
478 let marked = c
479 .drawing
480 .vars
481 .iter()
482 .any(|(k, v)| k == "spice" && *v == 1.0);
483 if !marked {
484 return Err(PicError::NotMarked);
485 }
486 let snap = if opts.snap.is_finite() && opts.snap > 0.0 {
487 opts.snap
488 } else {
489 DEFAULT_SNAP
490 };
491 let canvas = canvas_of(&mut c.drawing);
492 let mut ex = Extractor::new(src, &c, snap).run();
493 ex.map.canvas = canvas;
494 Ok(ex)
495}
496
497fn canvas_of(d: &mut Drawing) -> CanvasOverlay {
505 let saved = std::mem::take(&mut d.places);
506 d.places.push(PlaceInfo {
507 label: String::new(),
508 at: rpic_core::geom::Point::new(0.0, 0.0),
509 block: None,
510 span: None,
511 });
512 let o = rpic_core::svg::place_points(d)[0];
513 d.places = saved;
514 let (raw_w, raw_h) = match d.canvas {
515 Some(b) if !b.is_empty() => (b.width(), b.height()),
516 Some(_) => (0.0, 0.0),
517 None => {
518 let mut lo = (f64::INFINITY, f64::INFINITY);
520 let mut hi = (f64::NEG_INFINITY, f64::NEG_INFINITY);
521 for g in rpic_core::svg::object_geometries(d) {
522 if let Some((x, y, w, h)) = g.bbox {
523 lo = (lo.0.min(x), lo.1.min(y));
524 hi = (hi.0.max(x + w), hi.1.max(y + h));
525 }
526 }
527 if lo.0.is_finite() {
528 ((hi.0 - lo.0) / SVG_PPI, (hi.1 - lo.1) / SVG_PPI)
529 } else {
530 (0.0, 0.0)
531 }
532 }
533 };
534 let pad = d.prelude_thick.max(0.0) / 144.0;
535 let m = d.canvas_margin;
536 let extent = |v: f64| if v.is_finite() && v > 0.0 { v } else { 0.0 };
537 let w = extent(raw_w + 6.0 * pad + m.horizontal());
538 let h = extent(raw_h + 6.0 * pad + m.vertical());
539 CanvasOverlay {
541 w,
542 h,
543 origin: [o.x / SVG_PPI, h - o.y / SVG_PPI],
544 rescale: d.rescale,
545 }
546}
547
548struct Compiled {
550 drawing: Drawing,
551 directives: Vec<String>,
554 prelude_lines: u32,
556 element_macros: HashSet<String>,
559}
560
561fn compile(src: &str, opts: &ExtractOptions) -> Result<Compiled, PicError> {
562 let prelude = opts.prelude.unwrap_or(PRELUDE);
563 let mut full = String::with_capacity(prelude.len() + src.len() + 1);
564 full.push_str(prelude);
565 if !prelude.ends_with('\n') {
566 full.push('\n');
567 }
568 let prelude_lines = full.matches('\n').count() as u32;
569 full.push_str(src);
570 let copts = CompileOptions {
571 circuits: true,
572 includes: IncludePolicy::Deny,
573 ..Default::default()
574 };
575 let drawing = rpic_core::compile_with_diagnostics(&full, &copts).map_err(|e| {
576 let info = &e.info;
577 let span = info.line.map(|line| {
578 map_span(
579 prelude_lines,
580 info.file.as_deref(),
581 line,
582 info.col.unwrap_or(1),
583 info.end_col.unwrap_or(info.col.unwrap_or(1)),
584 )
585 });
586 PicError::Compile {
587 message: info.message.clone(),
588 span,
589 }
590 })?;
591 let directives = drawing
595 .passthrough
596 .iter()
597 .filter(|l| l.span.file.is_none() && l.span.line > prelude_lines)
598 .map(|l| directive_text(&l.text))
599 .collect();
600 let element_macros = prelude
601 .lines()
602 .filter_map(|l| l.trim_start().strip_prefix("define"))
603 .filter(|r| r.starts_with(char::is_whitespace))
604 .filter_map(|r| {
605 r.split(|c: char| c.is_whitespace() || c == '{')
606 .find(|w| !w.is_empty())
607 })
608 .filter(|n| !n.starts_with("__"))
609 .map(str::to_string)
610 .collect();
611 Ok(Compiled {
612 drawing,
613 directives,
614 prelude_lines,
615 element_macros,
616 })
617}
618
619fn directive_text(text: &str) -> String {
622 let mut lines = text.split('\n');
623 let mut out = lines.next().unwrap_or("").trim_end().to_string();
624 for cont in lines {
625 out.push('\n');
626 out.push_str(cont.trim());
627 }
628 out
629}
630
631fn map_span(prelude_lines: u32, file: Option<&str>, line: u32, col: u32, end_col: u32) -> PicSpan {
633 match file {
634 Some(f) => PicSpan {
635 file: Some(f.to_string()),
636 line,
637 col,
638 end_col,
639 },
640 None if line > prelude_lines => PicSpan {
641 file: None,
642 line: line - prelude_lines,
643 col,
644 end_col,
645 },
646 None => PicSpan {
647 file: Some("spice.pic".into()),
648 line,
649 col,
650 end_col,
651 },
652 }
653}
654
655fn pins_for(letter: char) -> Option<&'static [&'static str]> {
659 Some(match letter {
660 'R' | 'C' | 'L' | 'V' | 'I' | 'D' | 'W' | 'F' | 'H' => &["P", "N"],
661 'S' | 'E' | 'G' => &["P", "N", "Cp", "Cn"],
662 'K' => &[],
663 'Q' => &["C", "B", "E"],
664 'M' => &["D", "G", "S", "B"],
665 'J' | 'Z' => &["D", "G", "S"],
666 'X' => &["Inp", "Inm", "Out"],
667 'T' | 'O' => &["P1", "N1", "P2", "N2"],
668 _ => return None,
669 })
670}
671
672#[derive(Clone, Copy, PartialEq)]
673enum BlockKind {
674 Plain,
675 Element,
676 Ground,
677}
678
679#[derive(Clone, Copy)]
681enum Term {
682 Pin {
684 elem: usize,
685 pin: usize,
686 },
687 Ground,
688 Place(usize),
690 Wire,
692 Dot,
693}
694
695struct Terminal {
696 at: Pt,
697 shape: usize,
700 term: Term,
701}
702
703struct Element {
704 refdes: String,
705 pins: &'static [&'static str],
706 terms: Vec<usize>,
708 value: String,
709 value_arg: Option<usize>,
711 block: usize,
712 span: Option<PicSpan>,
714}
715
716const NO_SHAPE: usize = usize::MAX;
718
719struct Place {
721 label: String,
722 span: Option<PicSpan>,
723}
724
725struct Extractor<'a> {
726 src: &'a str,
727 d: &'a Drawing,
728 directives: &'a [String],
729 prelude_lines: u32,
730 element_macros: &'a HashSet<String>,
731 snap: f64,
732 diags: Vec<PicDiagnostic>,
733 places: Vec<Place>,
734}
735
736impl<'a> Extractor<'a> {
737 fn new(src: &'a str, c: &'a Compiled, snap: f64) -> Self {
738 Self {
739 src,
740 d: &c.drawing,
741 directives: &c.directives,
742 prelude_lines: c.prelude_lines,
743 element_macros: &c.element_macros,
744 snap,
745 diags: Vec::new(),
746 places: Vec::new(),
747 }
748 }
749
750 fn diag(
751 &mut self,
752 severity: PicSeverity,
753 kind: PicDiagKind,
754 message: String,
755 span: Option<PicSpan>,
756 ) {
757 self.diags.push(PicDiagnostic {
758 severity,
759 kind,
760 message,
761 span,
762 });
763 }
764
765 fn span(&self, s: &rpic_core::Span) -> PicSpan {
766 map_span(
767 self.prelude_lines,
768 s.file.as_deref(),
769 s.line,
770 s.col,
771 s.end_col,
772 )
773 }
774
775 fn shape_span(&self, i: usize) -> Option<PicSpan> {
776 let s = self.d.shape_spans.get(i)?.as_ref()?;
777 Some(self.span(s))
778 }
779
780 fn marker(&self, i: usize) -> Option<(&'a str, Pt)> {
783 let d = self.d;
784 let label = d.shape_labels.get(i)?.as_deref()?;
785 match &d.shapes[i] {
786 Shape::Circle {
787 c, r, dot, style, ..
788 } if *r == 0.0 && !*dot && style.invis => Some((label, (c.x, c.y))),
789 _ => None,
790 }
791 }
792
793 fn markers_of(&self, b: usize) -> Vec<(usize, &'a str, Pt)> {
795 let (lo, hi) = self.d.blocks[b].shapes;
796 (lo..hi.min(self.d.shapes.len()))
797 .filter(|&i| self.d.shape_blocks.get(i).copied().flatten() == Some(b))
798 .filter_map(|i| self.marker(i).map(|(l, p)| (i, l, p)))
799 .collect()
800 }
801
802 fn run(mut self) -> Extraction {
803 let d = self.d;
804 if self.src.lines().any(|l| {
805 let t = l.trim();
806 t.starts_with("copy") && t.contains("\"circuits\"")
807 }) {
808 self.diag(
809 PicSeverity::Warning,
810 PicDiagKind::Prelude,
811 "`copy \"circuits\"` reloads rpic's std library over the dune prelude, \
812 turning elements back into plain drawings; dune-pic loads the library itself"
813 .into(),
814 None,
815 );
816 }
817
818 let kinds: Vec<BlockKind> = (0..d.blocks.len())
820 .map(|b| {
821 let ms = self.markers_of(b);
822 if ms.iter().any(|m| m.1 == GROUND_MARKER) {
823 BlockKind::Ground
824 } else if ms.iter().any(|m| ELEMENT_MARKERS.contains(&m.1)) {
825 BlockKind::Element
826 } else {
827 BlockKind::Plain
828 }
829 })
830 .collect();
831 let in_special = |mut b: Option<usize>| {
833 while let Some(bi) = b {
834 if kinds[bi] != BlockKind::Plain {
835 return true;
836 }
837 b = d.blocks[bi].parent;
838 }
839 false
840 };
841 let internal = |i: usize| in_special(d.shape_blocks.get(i).copied().flatten());
843
844 let mut terms: Vec<Terminal> = Vec::new();
845 let mut elements: Vec<Element> = Vec::new();
846
847 let mut order: Vec<usize> = (0..d.blocks.len()).collect();
849 order.sort_by_key(|&b| d.blocks[b].shapes.0);
850 let mut seen: HashMap<String, String> = HashMap::new();
851 for &b in &order {
852 let ms = self.markers_of(b);
853 match kinds[b] {
854 BlockKind::Plain => {}
855 BlockKind::Ground => {
856 for &(i, l, p) in &ms {
857 if l == GROUND_MARKER {
858 terms.push(Terminal {
859 at: p,
860 shape: i,
861 term: Term::Ground,
862 });
863 }
864 }
865 }
866 BlockKind::Element => {
867 if let Some(e) = self.element(b, &ms, &mut terms, &mut seen, elements.len()) {
868 elements.push(e);
869 }
870 }
871 }
872 }
873
874 self.check_references(&elements);
875
876 for p in &d.places {
880 let span = p.span.as_ref().map(|s| self.span(s));
881 if span.as_ref().is_some_and(|s| s.file.is_some()) || in_special(p.block) {
882 continue;
883 }
884 if let Some(refdes) = seen.get(&p.label.to_ascii_lowercase()) {
885 self.diag(
886 PicSeverity::Warning,
887 PicDiagKind::NodeNameConflict,
888 format!(
889 "place `{}` has the name of element `{refdes}`; it names no net",
890 p.label
891 ),
892 span,
893 );
894 continue;
895 }
896 terms.push(Terminal {
897 at: (p.at.x, p.at.y),
898 shape: NO_SHAPE,
899 term: Term::Place(self.places.len()),
900 });
901 self.places.push(Place {
902 label: p.label.clone(),
903 span,
904 });
905 }
906
907 let mut segments: Vec<(Pt, Pt, usize)> = Vec::new();
909 for (i, s) in d.shapes.iter().enumerate() {
910 if internal(i) {
911 continue;
912 }
913 match s {
914 Shape::Path { pts, style, .. } if !style.invis && !pts.is_empty() => {
915 let first = terms.len();
916 for p in pts {
917 terms.push(Terminal {
918 at: (p.x, p.y),
919 shape: i,
920 term: Term::Wire,
921 });
922 }
923 for w in pts.windows(2) {
924 segments.push(((w[0].x, w[0].y), (w[1].x, w[1].y), first));
925 }
926 }
927 Shape::Circle { c, dot: true, .. } => terms.push(Terminal {
928 at: (c.x, c.y),
929 shape: i,
930 term: Term::Dot,
931 }),
932 _ => {}
933 }
934 }
935
936 let mut uf = UnionFind::new(terms.len());
938 for w in terms.windows(2).enumerate() {
939 let (k, pair) = w;
940 if let (Term::Wire, Term::Wire) = (pair[0].term, pair[1].term)
941 && pair[0].shape == pair[1].shape
942 {
943 uf.union(k, k + 1);
944 }
945 }
946 let points: Vec<Pt> = terms.iter().map(|t| t.at).collect();
947 join_geometry(&mut uf, &points, &segments, self.snap);
948 let mut by_name: HashMap<String, usize> = HashMap::new();
951 for (k, t) in terms.iter().enumerate() {
952 if let Term::Place(n) = t.term {
953 let key = self.places[n].label.to_ascii_lowercase();
954 match by_name.get(&key) {
955 Some(&other) => uf.union(k, other),
956 None => {
957 by_name.insert(key, k);
958 }
959 }
960 }
961 }
962
963 let roots: Vec<usize> = (0..terms.len()).map(|k| uf.find(k)).collect();
965 let mut nets: BTreeMap<usize, Vec<usize>> = BTreeMap::new(); for (k, &r) in roots.iter().enumerate() {
967 nets.entry(r).or_default().push(k);
968 }
969
970 let user_names: Vec<String> = self
972 .places
973 .iter()
974 .map(|p| p.label.to_ascii_lowercase())
975 .collect();
976 let mut name_of: HashMap<usize, String> = HashMap::new();
977 let mut net_order: Vec<(usize, usize)> = nets
979 .iter()
980 .map(|(&r, ks)| (ks.iter().map(|&k| terms[k].shape).min().unwrap_or(0), r))
981 .collect();
982 net_order.sort();
983 for &(_, r) in &net_order {
984 let ks = &nets[&r];
985 let grounded = ks.iter().any(|&k| matches!(terms[k].term, Term::Ground));
986 let mut names: Vec<usize> = ks
988 .iter()
989 .filter_map(|&k| match terms[k].term {
990 Term::Place(n) => Some(n),
991 _ => None,
992 })
993 .collect();
994 names.sort_unstable();
995 let mut distinct: Vec<String> = Vec::new();
996 for n in names {
997 let l = &self.places[n].label;
998 if !distinct.iter().any(|m| m.eq_ignore_ascii_case(l)) {
999 distinct.push(l.clone());
1000 }
1001 }
1002 if grounded {
1003 if let Some(n) = distinct.first() {
1004 let span = self.label_span(n);
1005 self.diag(
1006 PicSeverity::Warning,
1007 PicDiagKind::NodeNameConflict,
1008 format!("place `{n}` is on the ground net; it is named `0`"),
1009 span,
1010 );
1011 }
1012 name_of.insert(r, "0".into());
1013 } else if let Some(first) = distinct.first() {
1014 if distinct.len() > 1 {
1015 let span = self.label_span(&distinct[1]);
1016 self.diag(
1017 PicSeverity::Warning,
1018 PicDiagKind::NodeNameConflict,
1019 format!(
1020 "one net named {}; using `{first}`",
1021 distinct
1022 .iter()
1023 .map(|n| format!("`{n}`"))
1024 .collect::<Vec<_>>()
1025 .join(", ")
1026 ),
1027 span,
1028 );
1029 }
1030 name_of.insert(r, first.clone());
1031 }
1032 }
1033 let mut auto = 0usize;
1034 for &(_, r) in &net_order {
1035 if name_of.contains_key(&r) {
1036 continue;
1037 }
1038 let has_pin = nets[&r]
1039 .iter()
1040 .any(|&k| matches!(terms[k].term, Term::Pin { .. }));
1041 if !has_pin {
1042 continue;
1043 }
1044 let name = loop {
1045 auto += 1;
1046 let n = format!("n{auto}");
1047 if !user_names.contains(&n) {
1048 break n;
1049 }
1050 };
1051 name_of.insert(r, name);
1052 }
1053
1054 for &(_, r) in &net_order {
1056 let ks = &nets[&r];
1057 let pins: Vec<usize> = ks
1058 .iter()
1059 .copied()
1060 .filter(|&k| matches!(terms[k].term, Term::Pin { .. }))
1061 .collect();
1062 let grounded = ks.iter().any(|&k| matches!(terms[k].term, Term::Ground));
1063 if pins.is_empty() {
1064 let drawn = ks
1065 .iter()
1066 .copied()
1067 .find(|&k| matches!(terms[k].term, Term::Wire | Term::Dot));
1068 if let Some(k) = drawn {
1069 let what = match name_of.get(&r) {
1070 Some(n) => format!("net `{n}`"),
1071 None => "wire".into(),
1072 };
1073 let span = self.shape_span(terms[k].shape).filter(|s| s.file.is_none());
1074 self.diag(
1075 PicSeverity::Warning,
1076 PicDiagKind::FloatingNet,
1077 format!("{what} connects to no element pin"),
1078 span,
1079 );
1080 } else if let Some(n) = ks.iter().find_map(|&k| match terms[k].term {
1081 Term::Place(n) => Some(n),
1082 _ => None,
1083 }) {
1084 let p = &self.places[n];
1086 let msg = format!("place `{}` names no net: no wire or pin is there", p.label);
1087 let span = p.span.clone();
1088 self.diag(PicSeverity::Warning, PicDiagKind::FloatingNet, msg, span);
1089 }
1090 continue;
1091 }
1092 if pins.len() == 1 && !grounded {
1093 let Term::Pin { elem, pin } = terms[pins[0]].term else {
1094 unreachable!()
1095 };
1096 let e = &elements[elem];
1097 let span = e.span.clone();
1098 let net = name_of.get(&r).cloned().unwrap_or_default();
1099 let msg = format!(
1100 "pin {}.{} is dangling: net `{net}` reaches no other element pin",
1101 e.refdes, e.pins[pin]
1102 );
1103 self.diag(PicSeverity::Warning, PicDiagKind::DanglingPin, msg, span);
1104 }
1105 }
1106
1107 for e in &elements {
1110 if e.pins.len() == 4 && e.refdes.to_ascii_uppercase().starts_with('Q') {
1111 let sub = &name_of[&roots[e.terms[3]]];
1112 let model = e.value.split_whitespace().next().unwrap_or("");
1113 if sub.eq_ignore_ascii_case(model) {
1114 let msg = format!(
1115 "`{}`: substrate net `{sub}` has the name of the model; \
1116 SPICE would read it as the model and the next word as the model name",
1117 e.refdes
1118 );
1119 let span = e.span.clone();
1120 self.diag(
1121 PicSeverity::Warning,
1122 PicDiagKind::NodeNameConflict,
1123 msg,
1124 span,
1125 );
1126 }
1127 }
1128 }
1129
1130 let mut netlist = String::from("* netlist extracted by dune-pic\n");
1132 let mut map = OverlayMap::default();
1133 for e in &elements {
1134 netlist.push_str(&e.refdes);
1135 for &k in &e.terms {
1136 netlist.push(' ');
1137 netlist.push_str(&name_of[&roots[k]]);
1138 }
1139 if !e.value.is_empty() {
1140 netlist.push(' ');
1141 netlist.push_str(&e.value);
1142 }
1143 netlist.push('\n');
1144 let (lo, hi) = d.blocks[e.block].shapes;
1145 let pins = e
1146 .pins
1147 .iter()
1148 .zip(&e.terms)
1149 .map(|(&name, &k)| PinOverlay {
1150 name: name.to_string(),
1151 net: name_of[&roots[k]].clone(),
1152 x: terms[k].at.0,
1153 y: terms[k].at.1,
1154 })
1155 .collect();
1156 let letter = e.refdes.chars().next().unwrap_or(' ').to_ascii_uppercase();
1157 let (value, params) = match e
1158 .value_arg
1159 .and_then(|k| self.value_span(e.block, k, letter))
1160 {
1161 Some((v, p)) => (Some(v), p),
1162 None => (None, Vec::new()),
1163 };
1164 map.elements.push(ElementOverlay {
1165 refdes: e.refdes.clone(),
1166 shapes: lo..hi,
1167 pins,
1168 value,
1169 params,
1170 principal: spans::principal(letter).map(str::to_string),
1171 });
1172 }
1173 for dir in self.directives {
1174 if dir.eq_ignore_ascii_case(".end") {
1175 continue;
1176 }
1177 netlist.push_str(dir);
1178 netlist.push('\n');
1179 }
1180 netlist.push_str(".end\n");
1181
1182 let mut net_shapes: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
1184 for (k, t) in terms.iter().enumerate() {
1185 match t.term {
1186 Term::Wire | Term::Dot => net_shapes.entry(roots[k]).or_default().push(t.shape),
1187 Term::Ground => {
1188 let b = d.shape_blocks[t.shape].expect("marker inside its block");
1189 let (lo, hi) = d.blocks[b].shapes;
1190 net_shapes.entry(roots[k]).or_default().extend(lo..hi);
1191 }
1192 Term::Pin { .. } | Term::Place(_) => {}
1193 }
1194 }
1195 let mut emitted: Vec<String> = Vec::new();
1196 let mut ordered: Vec<(usize, usize)> = net_order.clone();
1197 ordered.sort_by_key(|&(first, r)| (name_of.get(&r).map(|n| n != "0"), first));
1198 for (_, r) in ordered {
1199 let Some(name) = name_of.get(&r) else {
1200 continue;
1201 };
1202 let mut shapes = net_shapes.remove(&r).unwrap_or_default();
1203 shapes.sort_unstable();
1204 shapes.dedup();
1205 if let Some(pos) = emitted.iter().position(|n| n == name) {
1207 map.nets[pos].shapes.extend(shapes);
1208 map.nets[pos].shapes.sort_unstable();
1209 map.nets[pos].shapes.dedup();
1210 continue;
1211 }
1212 emitted.push(name.clone());
1213 map.nets.push(NetOverlay {
1214 name: name.clone(),
1215 shapes,
1216 });
1217 }
1218
1219 Extraction {
1220 netlist,
1221 map,
1222 diagnostics: self.diags,
1223 }
1224 }
1225
1226 fn element(
1228 &mut self,
1229 b: usize,
1230 ms: &[(usize, &'a str, Pt)],
1231 terms: &mut Vec<Terminal>,
1232 seen: &mut HashMap<String, String>,
1233 index: usize,
1234 ) -> Option<Element> {
1235 let d = self.d;
1236 let Some(refdes) = d.blocks[b].label.clone() else {
1237 let value = self.value(b, ms, ' ').map(|v| v.0).unwrap_or_default();
1238 let at = ms.first().map(|m| m.2).unwrap_or((0.0, 0.0));
1239 let msg = format!(
1240 "element without a label (refdes) at ({:.3}, {:.3}){}; skipped — write `R1: resistor(…)`",
1241 at.0,
1242 at.1,
1243 if value.is_empty() {
1244 String::new()
1245 } else {
1246 format!(", value `{value}`")
1247 }
1248 );
1249 let span = self.block_span(b);
1250 self.diag(PicSeverity::Error, PicDiagKind::UnlabeledElement, msg, span);
1251 return None;
1252 };
1253 let span = self.block_span(b);
1254 let letter = refdes.chars().next().unwrap_or(' ').to_ascii_uppercase();
1255 let Some(mut pins) = pins_for(letter) else {
1256 self.diag(
1257 PicSeverity::Error,
1258 PicDiagKind::UnknownElement,
1259 format!(
1260 "`{refdes}`: element letter `{letter}` is not supported ({LETTERS}); skipped"
1261 ),
1262 span,
1263 );
1264 return None;
1265 };
1266 if letter == 'Q' && ms.iter().any(|m| m.1 == "S") {
1267 pins = BJT4_PINS;
1268 }
1269 if letter == 'X' {
1270 let n = subckt_arity(ms);
1271 if n > 0 {
1272 pins = &SUBCKT_PINS[..n];
1273 }
1274 }
1275 if letter == 'K' && !ms.iter().any(|m| m.1 == COUPLING_MARKER) {
1276 self.diag(
1277 PicSeverity::Error,
1278 PicDiagKind::MissingPin,
1279 format!(
1280 "`{refdes}`: a `K` element is drawn with `coupling(L1, L2, k)`, \
1281 not this macro; skipped"
1282 ),
1283 span,
1284 );
1285 return None;
1286 }
1287 let value = self.value(b, ms, letter);
1288 let key = refdes.to_ascii_lowercase();
1289 if let Some(prev) = seen.get(&key) {
1290 self.diag(
1291 PicSeverity::Error,
1292 PicDiagKind::DuplicateRefdes,
1293 format!("refdes `{refdes}` already used by `{prev}`; skipped"),
1294 span,
1295 );
1296 return None;
1297 }
1298 let mut pin_terms = Vec::with_capacity(pins.len());
1299 let mut missing = Vec::new();
1300 for (pi, &pin) in pins.iter().enumerate() {
1301 match ms.iter().find(|m| m.1 == pin) {
1302 Some(&(i, _, p)) => {
1303 pin_terms.push(terms.len());
1304 terms.push(Terminal {
1305 at: p,
1306 shape: i,
1307 term: Term::Pin {
1308 elem: index,
1309 pin: pi,
1310 },
1311 });
1312 }
1313 None => missing.push(pin),
1314 }
1315 }
1316 if !missing.is_empty() {
1317 terms.truncate(terms.len() - pin_terms.len());
1319 self.diag(
1320 PicSeverity::Error,
1321 PicDiagKind::MissingPin,
1322 format!(
1323 "`{refdes}`: a `{letter}` element needs pins {}, but the drawn element \
1324 lacks {} (wrong refdes letter for this macro?); skipped",
1325 pins.join(", "),
1326 missing.join(", ")
1327 ),
1328 span,
1329 );
1330 return None;
1331 }
1332 seen.insert(key, refdes.clone());
1333 let Some((value, value_arg)) = value else {
1334 self.diag(
1335 PicSeverity::Error,
1336 PicDiagKind::MissingValue,
1337 format!(
1338 "`{refdes}` draws a display text instead of its value, but its call \
1339 is not the element's own macro (a wrapper?), so the value cannot \
1340 be read; the card is incomplete"
1341 ),
1342 span.clone(),
1343 );
1344 return Some(Element {
1345 refdes,
1346 pins,
1347 terms: pin_terms,
1348 value: String::new(),
1349 value_arg: None,
1350 block: b,
1351 span,
1352 });
1353 };
1354 if value.is_empty() {
1355 let (sev, what) = if matches!(letter, 'V' | 'I') {
1358 (PicSeverity::Warning, "it is a 0 V/0 A source")
1359 } else {
1360 (PicSeverity::Error, "the card is incomplete")
1361 };
1362 self.diag(
1363 sev,
1364 PicDiagKind::MissingValue,
1365 format!("`{refdes}` has no value/model text; {what}"),
1366 span.clone(),
1367 );
1368 }
1369 Some(Element {
1370 refdes,
1371 pins,
1372 terms: pin_terms,
1373 value,
1374 value_arg,
1375 block: b,
1376 span,
1377 })
1378 }
1379
1380 fn check_references(&mut self, elements: &[Element]) {
1387 let letter_of = |name: &str| {
1388 elements
1389 .iter()
1390 .find(|e| e.refdes.eq_ignore_ascii_case(name))
1391 .and_then(|e| e.refdes.chars().next())
1392 .map(|c| c.to_ascii_uppercase())
1393 };
1394 for e in elements {
1395 let letter = e.refdes.chars().next().unwrap_or(' ').to_ascii_uppercase();
1396 let toks: Vec<&str> = e.value.split_whitespace().collect();
1397 if toks.is_empty() {
1398 continue;
1399 }
1400 let (want, refs, what): (char, &[&str], &str) = match letter {
1401 'F' | 'H' => (
1402 'V',
1403 &toks[..1],
1404 "a controlling voltage source (`Vname gain`)",
1405 ),
1406 'K' => (
1407 'L',
1408 &toks[..toks.len().min(2)],
1409 "two inductors (`L1, L2, k`)",
1410 ),
1411 _ => continue,
1412 };
1413 for r in refs {
1414 let problem = match letter_of(r) {
1415 Some(l) if l == want => continue,
1416 Some(_) => format!("`{r}` is not a `{want}` element"),
1417 None if r
1418 .chars()
1419 .next()
1420 .is_some_and(|c| c.eq_ignore_ascii_case(&want)) =>
1421 {
1422 format!("no element `{r}` in the figure")
1423 }
1424 None => format!("`{r}` is not a `{want}` refdes"),
1425 };
1426 self.diag(
1427 PicSeverity::Error,
1428 PicDiagKind::MissingValue,
1429 format!(
1430 "`{}` needs {what}: {problem}; the card is invalid",
1431 e.refdes
1432 ),
1433 e.span.clone(),
1434 );
1435 }
1436 if letter == 'K' && toks.len() < 3 {
1437 self.diag(
1438 PicSeverity::Error,
1439 PicDiagKind::MissingValue,
1440 format!("`{}` has no coupling coefficient", e.refdes),
1441 e.span.clone(),
1442 );
1443 }
1444 }
1445 }
1446
1447 fn block_span(&self, b: usize) -> Option<PicSpan> {
1451 self.d.blocks[b].span.as_ref().map(|s| self.span(s))
1452 }
1453
1454 fn value(
1485 &self,
1486 b: usize,
1487 ms: &[(usize, &'a str, Pt)],
1488 letter: char,
1489 ) -> Option<(String, Option<usize>)> {
1490 let coupling = letter == 'K';
1491 let drawn = self.value_text(b);
1492 let shows_display = ms.iter().any(|m| m.1 == DISPLAY_MARKER);
1494 let fallback = if shows_display {
1495 None
1496 } else if coupling {
1497 Some((String::new(), None))
1498 } else {
1499 Some((drawn.clone(), None))
1500 };
1501 let arity = if letter == 'X' { subckt_arity(ms) } else { 0 };
1502 let k = match letter {
1503 'E' | 'G' => 4,
1504 _ if ms.iter().any(|m| m.1 == ANCHOR_MARKER) => 1,
1505 'X' if arity > 0 => arity,
1507 _ => 2,
1508 };
1509 let Some(call) = &self.d.blocks[b].call else {
1510 return fallback;
1511 };
1512 let Some(arg) = call.args.get(k) else {
1513 return fallback;
1514 };
1515 if coupling && call.args.len() != 3 {
1516 return fallback;
1517 }
1518 let words = |s: &str| s.split_whitespace().collect::<Vec<_>>().join(" ");
1519 let spliced = unquote(arg);
1520 let display = (DISPLAY_MACROS.contains(&call.name.as_str()) && call.args.len() == k + 2)
1525 .then(|| words(unquote(&call.args[k + 1])))
1526 .filter(|d| !d.is_empty());
1527 if shows_display && display.is_none() {
1528 return None;
1531 }
1532 let expect = if coupling {
1533 format!("k={}", words(spliced))
1534 } else if let Some(d) = display {
1535 d
1536 } else {
1537 words(spliced)
1538 };
1539 if !drawn.is_empty() && expect != words(&drawn) {
1540 return fallback;
1541 }
1542 let pick = |i: usize| unquote(&self.raw_arg(call, i)).trim().to_string();
1543 Some(if coupling {
1544 ((0..3).map(pick).collect::<Vec<_>>().join(" "), None)
1545 } else {
1546 (pick(k), Some(k))
1547 })
1548 }
1549
1550 fn raw_arg(&self, call: &MacroCall, i: usize) -> String {
1555 match call.raw_args.get(i) {
1556 Some(raw)
1557 if call.raw_args.len() == call.args.len()
1558 && raw.span.file.is_none()
1559 && raw.span.line > self.prelude_lines
1560 && raw.end_line == raw.span.line =>
1561 {
1562 raw.text.clone()
1563 }
1564 _ => call.args[i].clone(),
1565 }
1566 }
1567
1568 fn value_span(
1573 &self,
1574 b: usize,
1575 k: usize,
1576 letter: char,
1577 ) -> Option<(ValueSpanOverlay, Vec<ParamSpanOverlay>)> {
1578 let call = self.d.blocks[b].call.as_ref()?;
1579 if !self.element_macros.contains(&call.name) || call.raw_args.len() != call.args.len() {
1580 return None;
1581 }
1582 let raw = call.raw_args.get(k)?;
1583 if raw.span.file.is_some() || raw.span.line <= self.prelude_lines {
1584 return None;
1585 }
1586 let chars: Vec<char> = raw.text.chars().collect();
1587 let pos = spans::positions(&chars, raw.span.line - self.prelude_lines, raw.span.col);
1588 if pos[chars.len()] != (raw.end_line - self.prelude_lines, raw.span.end_col) {
1592 return None;
1593 }
1594 let (a, z) = spans::value_range(&chars);
1595 let value = ValueSpanOverlay {
1596 span: spans::range(&pos, a, z),
1597 text: chars[a..z].iter().collect(),
1598 };
1599 Some((value, spans::param_overlays(letter, &chars, a, z, &pos)))
1600 }
1601
1602 fn value_text(&self, b: usize) -> String {
1605 let (lo, hi) = self.d.blocks[b].shapes;
1606 for shape in self.d.shapes.iter().take(hi).skip(lo) {
1607 if let Shape::Text { text, .. } = shape {
1608 let joined: Vec<&str> = text.iter().map(|t| t.s.trim()).collect();
1609 return joined.join(" ").trim().to_string();
1610 }
1611 }
1612 String::new()
1613 }
1614
1615 fn label_span(&self, label: &str) -> Option<PicSpan> {
1617 self.places
1618 .iter()
1619 .find(|p| p.label.eq_ignore_ascii_case(label))
1620 .and_then(|p| p.span.clone())
1621 }
1622}
1623
1624fn subckt_arity(ms: &[(usize, &str, Pt)]) -> usize {
1629 SUBCKT_PINS
1630 .iter()
1631 .take_while(|p| ms.iter().any(|m| m.1 == **p))
1632 .count()
1633}
1634
1635fn unquote(arg: &str) -> &str {
1638 let t = arg.trim();
1639 match t.strip_prefix('"').and_then(|r| r.strip_suffix('"')) {
1640 Some(inner) if !inner.contains('"') => inner,
1641 _ => arg,
1642 }
1643}