#![forbid(unsafe_code)]
mod connect;
mod spans;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt;
use std::ops::Range;
use rpic_core::ir::{Drawing, MacroCall, PlaceInfo, Shape};
use rpic_core::{CompileOptions, IncludePolicy};
use crate::connect::{Pt, UnionFind, join_geometry};
pub const PRELUDE: &str = include_str!("spice.pic");
pub const DEFAULT_SNAP: f64 = 1e-3;
const GROUND_MARKER: &str = "Gnd";
const ANCHOR_MARKER: &str = "At";
const COUPLING_MARKER: &str = "Mutual";
const ELEMENT_MARKERS: &[&str] = &[
"P", "N", "Cp", "Cn", "C", "B", "E", "D", "G", "S", "Inp", "Inm", "Out", "P1", "N1", "P2",
"N2", "P3", "P4", "P5", "P6", "P7", "At", "Mutual",
];
const BJT4_PINS: &[&str] = &["C", "B", "E", "S"];
const SUBCKT_PINS: &[&str] = &["P1", "P2", "P3", "P4", "P5", "P6", "P7"];
const DISPLAY_MARKER: &str = "Disp";
const DISPLAY_MACROS: &[&str] = &[
"resistor",
"capacitor",
"inductor",
"diode",
"vsource",
"acsource",
"battery",
"isource",
"cccs",
"ccvs",
"vcvs",
"vccs",
"npn",
"pnp",
"npn4",
"pnp4",
"nmos",
"pmos",
"nmos4",
"pmos4",
"njfet",
"pjfet",
"nmes",
"pmes",
"opamp",
"subckt2",
"subckt3",
"subckt4",
"subckt5",
"subckt6",
"subckt7",
];
const LETTERS: &str = "R C L V I D Q M J Z S W X T O E F G H K";
#[derive(Debug, Clone, Copy)]
pub struct ExtractOptions<'a> {
pub snap: f64,
pub prelude: Option<&'a str>,
}
impl Default for ExtractOptions<'_> {
fn default() -> Self {
Self {
snap: DEFAULT_SNAP,
prelude: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PicSpan {
pub file: Option<String>,
pub line: u32,
pub col: u32,
pub end_col: u32,
}
impl fmt::Display for PicSpan {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(file) = &self.file {
write!(f, "{file}:")?;
}
write!(f, "{}:{}", self.line, self.col)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PicSeverity {
Error,
Warning,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PicDiagKind {
DanglingPin,
FloatingNet,
MissingValue,
DuplicateRefdes,
UnknownElement,
MissingPin,
UnlabeledElement,
NodeNameConflict,
Prelude,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PicDiagnostic {
pub severity: PicSeverity,
pub kind: PicDiagKind,
pub message: String,
pub span: Option<PicSpan>,
}
impl fmt::Display for PicDiagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let sev = match self.severity {
PicSeverity::Error => "error",
PicSeverity::Warning => "warning",
};
match &self.span {
Some(s) => write!(f, "{s}: {sev}: {}", self.message),
None => write!(f, "{sev}: {}", self.message),
}
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum PicError {
#[error("pic: {message}")]
Compile {
message: String,
span: Option<PicSpan>,
},
#[error("figure not marked `spice = 1`; dune-pic only extracts figures that opt in")]
NotMarked,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ElementOverlay {
pub refdes: String,
pub shapes: Range<usize>,
pub pins: Vec<PinOverlay>,
pub value: Option<ValueSpanOverlay>,
pub params: Vec<ParamSpanOverlay>,
pub principal: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PicRange {
pub line: u32,
pub col: u32,
pub end_line: u32,
pub end_col: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValueSpanOverlay {
pub span: PicRange,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParamSpanOverlay {
pub name: String,
pub span: PicRange,
pub text: String,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CanvasOverlay {
pub w: f64,
pub h: f64,
pub origin: [f64; 2],
pub rescale: f64,
}
impl Default for CanvasOverlay {
fn default() -> Self {
Self {
w: 0.0,
h: 0.0,
origin: [0.0, 0.0],
rescale: 1.0,
}
}
}
impl CanvasOverlay {
pub fn to_svg(&self, x: f64, y: f64) -> (f64, f64) {
(
(x + self.origin[0]) * SVG_PPI,
(self.h - (y + self.origin[1])) * SVG_PPI,
)
}
}
const SVG_PPI: f64 = 96.0;
#[derive(Debug, Clone, PartialEq)]
pub struct PinOverlay {
pub name: String,
pub net: String,
pub x: f64,
pub y: f64,
}
impl ElementOverlay {
pub fn ids(&self) -> String {
format!(
"s{}..s{}",
self.shapes.start,
self.shapes.end.saturating_sub(1)
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct NetOverlay {
pub name: String,
pub shapes: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct OverlayMap {
pub elements: Vec<ElementOverlay>,
pub nets: Vec<NetOverlay>,
pub canvas: CanvasOverlay,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Extraction {
pub netlist: String,
pub map: OverlayMap,
pub diagnostics: Vec<PicDiagnostic>,
}
impl Extraction {
pub fn is_clean(&self) -> bool {
self.diagnostics
.iter()
.all(|d| d.severity != PicSeverity::Error)
}
}
pub fn render_svg(src: &str, opts: &ExtractOptions) -> Result<String, PicError> {
let c = compile(src, opts)?;
Ok(rpic_core::to_svg(&c.drawing))
}
pub fn extract(src: &str, opts: &ExtractOptions) -> Result<Extraction, PicError> {
let mut c = compile(src, opts)?;
let marked = c
.drawing
.vars
.iter()
.any(|(k, v)| k == "spice" && *v == 1.0);
if !marked {
return Err(PicError::NotMarked);
}
let snap = if opts.snap.is_finite() && opts.snap > 0.0 {
opts.snap
} else {
DEFAULT_SNAP
};
let canvas = canvas_of(&mut c.drawing);
let mut ex = Extractor::new(src, &c, snap).run();
ex.map.canvas = canvas;
Ok(ex)
}
fn canvas_of(d: &mut Drawing) -> CanvasOverlay {
let saved = std::mem::take(&mut d.places);
d.places.push(PlaceInfo {
label: String::new(),
at: rpic_core::geom::Point::new(0.0, 0.0),
block: None,
span: None,
});
let o = rpic_core::svg::place_points(d)[0];
d.places = saved;
let (raw_w, raw_h) = match d.canvas {
Some(b) if !b.is_empty() => (b.width(), b.height()),
Some(_) => (0.0, 0.0),
None => {
let mut lo = (f64::INFINITY, f64::INFINITY);
let mut hi = (f64::NEG_INFINITY, f64::NEG_INFINITY);
for g in rpic_core::svg::object_geometries(d) {
if let Some((x, y, w, h)) = g.bbox {
lo = (lo.0.min(x), lo.1.min(y));
hi = (hi.0.max(x + w), hi.1.max(y + h));
}
}
if lo.0.is_finite() {
((hi.0 - lo.0) / SVG_PPI, (hi.1 - lo.1) / SVG_PPI)
} else {
(0.0, 0.0)
}
}
};
let pad = d.prelude_thick.max(0.0) / 144.0;
let m = d.canvas_margin;
let extent = |v: f64| if v.is_finite() && v > 0.0 { v } else { 0.0 };
let w = extent(raw_w + 6.0 * pad + m.horizontal());
let h = extent(raw_h + 6.0 * pad + m.vertical());
CanvasOverlay {
w,
h,
origin: [o.x / SVG_PPI, h - o.y / SVG_PPI],
rescale: d.rescale,
}
}
struct Compiled {
drawing: Drawing,
directives: Vec<String>,
prelude_lines: u32,
element_macros: HashSet<String>,
}
fn compile(src: &str, opts: &ExtractOptions) -> Result<Compiled, PicError> {
let prelude = opts.prelude.unwrap_or(PRELUDE);
let mut full = String::with_capacity(prelude.len() + src.len() + 1);
full.push_str(prelude);
if !prelude.ends_with('\n') {
full.push('\n');
}
let prelude_lines = full.matches('\n').count() as u32;
full.push_str(src);
let copts = CompileOptions {
circuits: true,
includes: IncludePolicy::Deny,
..Default::default()
};
let drawing = rpic_core::compile_with_diagnostics(&full, &copts).map_err(|e| {
let info = &e.info;
let span = info.line.map(|line| {
map_span(
prelude_lines,
info.file.as_deref(),
line,
info.col.unwrap_or(1),
info.end_col.unwrap_or(info.col.unwrap_or(1)),
)
});
PicError::Compile {
message: info.message.clone(),
span,
}
})?;
let directives = drawing
.passthrough
.iter()
.filter(|l| l.span.file.is_none() && l.span.line > prelude_lines)
.map(|l| directive_text(&l.text))
.collect();
let element_macros = prelude
.lines()
.filter_map(|l| l.trim_start().strip_prefix("define"))
.filter(|r| r.starts_with(char::is_whitespace))
.filter_map(|r| {
r.split(|c: char| c.is_whitespace() || c == '{')
.find(|w| !w.is_empty())
})
.filter(|n| !n.starts_with("__"))
.map(str::to_string)
.collect();
Ok(Compiled {
drawing,
directives,
prelude_lines,
element_macros,
})
}
fn directive_text(text: &str) -> String {
let mut lines = text.split('\n');
let mut out = lines.next().unwrap_or("").trim_end().to_string();
for cont in lines {
out.push('\n');
out.push_str(cont.trim());
}
out
}
fn map_span(prelude_lines: u32, file: Option<&str>, line: u32, col: u32, end_col: u32) -> PicSpan {
match file {
Some(f) => PicSpan {
file: Some(f.to_string()),
line,
col,
end_col,
},
None if line > prelude_lines => PicSpan {
file: None,
line: line - prelude_lines,
col,
end_col,
},
None => PicSpan {
file: Some("spice.pic".into()),
line,
col,
end_col,
},
}
}
fn pins_for(letter: char) -> Option<&'static [&'static str]> {
Some(match letter {
'R' | 'C' | 'L' | 'V' | 'I' | 'D' | 'W' | 'F' | 'H' => &["P", "N"],
'S' | 'E' | 'G' => &["P", "N", "Cp", "Cn"],
'K' => &[],
'Q' => &["C", "B", "E"],
'M' => &["D", "G", "S", "B"],
'J' | 'Z' => &["D", "G", "S"],
'X' => &["Inp", "Inm", "Out"],
'T' | 'O' => &["P1", "N1", "P2", "N2"],
_ => return None,
})
}
#[derive(Clone, Copy, PartialEq)]
enum BlockKind {
Plain,
Element,
Ground,
}
#[derive(Clone, Copy)]
enum Term {
Pin {
elem: usize,
pin: usize,
},
Ground,
Place(usize),
Wire,
Dot,
}
struct Terminal {
at: Pt,
shape: usize,
term: Term,
}
struct Element {
refdes: String,
pins: &'static [&'static str],
terms: Vec<usize>,
value: String,
value_arg: Option<usize>,
block: usize,
span: Option<PicSpan>,
}
const NO_SHAPE: usize = usize::MAX;
struct Place {
label: String,
span: Option<PicSpan>,
}
struct Extractor<'a> {
src: &'a str,
d: &'a Drawing,
directives: &'a [String],
prelude_lines: u32,
element_macros: &'a HashSet<String>,
snap: f64,
diags: Vec<PicDiagnostic>,
places: Vec<Place>,
}
impl<'a> Extractor<'a> {
fn new(src: &'a str, c: &'a Compiled, snap: f64) -> Self {
Self {
src,
d: &c.drawing,
directives: &c.directives,
prelude_lines: c.prelude_lines,
element_macros: &c.element_macros,
snap,
diags: Vec::new(),
places: Vec::new(),
}
}
fn diag(
&mut self,
severity: PicSeverity,
kind: PicDiagKind,
message: String,
span: Option<PicSpan>,
) {
self.diags.push(PicDiagnostic {
severity,
kind,
message,
span,
});
}
fn span(&self, s: &rpic_core::Span) -> PicSpan {
map_span(
self.prelude_lines,
s.file.as_deref(),
s.line,
s.col,
s.end_col,
)
}
fn shape_span(&self, i: usize) -> Option<PicSpan> {
let s = self.d.shape_spans.get(i)?.as_ref()?;
Some(self.span(s))
}
fn marker(&self, i: usize) -> Option<(&'a str, Pt)> {
let d = self.d;
let label = d.shape_labels.get(i)?.as_deref()?;
match &d.shapes[i] {
Shape::Circle {
c, r, dot, style, ..
} if *r == 0.0 && !*dot && style.invis => Some((label, (c.x, c.y))),
_ => None,
}
}
fn markers_of(&self, b: usize) -> Vec<(usize, &'a str, Pt)> {
let (lo, hi) = self.d.blocks[b].shapes;
(lo..hi.min(self.d.shapes.len()))
.filter(|&i| self.d.shape_blocks.get(i).copied().flatten() == Some(b))
.filter_map(|i| self.marker(i).map(|(l, p)| (i, l, p)))
.collect()
}
fn run(mut self) -> Extraction {
let d = self.d;
if self.src.lines().any(|l| {
let t = l.trim();
t.starts_with("copy") && t.contains("\"circuits\"")
}) {
self.diag(
PicSeverity::Warning,
PicDiagKind::Prelude,
"`copy \"circuits\"` reloads rpic's std library over the dune prelude, \
turning elements back into plain drawings; dune-pic loads the library itself"
.into(),
None,
);
}
let kinds: Vec<BlockKind> = (0..d.blocks.len())
.map(|b| {
let ms = self.markers_of(b);
if ms.iter().any(|m| m.1 == GROUND_MARKER) {
BlockKind::Ground
} else if ms.iter().any(|m| ELEMENT_MARKERS.contains(&m.1)) {
BlockKind::Element
} else {
BlockKind::Plain
}
})
.collect();
let in_special = |mut b: Option<usize>| {
while let Some(bi) = b {
if kinds[bi] != BlockKind::Plain {
return true;
}
b = d.blocks[bi].parent;
}
false
};
let internal = |i: usize| in_special(d.shape_blocks.get(i).copied().flatten());
let mut terms: Vec<Terminal> = Vec::new();
let mut elements: Vec<Element> = Vec::new();
let mut order: Vec<usize> = (0..d.blocks.len()).collect();
order.sort_by_key(|&b| d.blocks[b].shapes.0);
let mut seen: HashMap<String, String> = HashMap::new();
for &b in &order {
let ms = self.markers_of(b);
match kinds[b] {
BlockKind::Plain => {}
BlockKind::Ground => {
for &(i, l, p) in &ms {
if l == GROUND_MARKER {
terms.push(Terminal {
at: p,
shape: i,
term: Term::Ground,
});
}
}
}
BlockKind::Element => {
if let Some(e) = self.element(b, &ms, &mut terms, &mut seen, elements.len()) {
elements.push(e);
}
}
}
}
self.check_references(&elements);
for p in &d.places {
let span = p.span.as_ref().map(|s| self.span(s));
if span.as_ref().is_some_and(|s| s.file.is_some()) || in_special(p.block) {
continue;
}
if let Some(refdes) = seen.get(&p.label.to_ascii_lowercase()) {
self.diag(
PicSeverity::Warning,
PicDiagKind::NodeNameConflict,
format!(
"place `{}` has the name of element `{refdes}`; it names no net",
p.label
),
span,
);
continue;
}
terms.push(Terminal {
at: (p.at.x, p.at.y),
shape: NO_SHAPE,
term: Term::Place(self.places.len()),
});
self.places.push(Place {
label: p.label.clone(),
span,
});
}
let mut segments: Vec<(Pt, Pt, usize)> = Vec::new();
for (i, s) in d.shapes.iter().enumerate() {
if internal(i) {
continue;
}
match s {
Shape::Path { pts, style, .. } if !style.invis && !pts.is_empty() => {
let first = terms.len();
for p in pts {
terms.push(Terminal {
at: (p.x, p.y),
shape: i,
term: Term::Wire,
});
}
for w in pts.windows(2) {
segments.push(((w[0].x, w[0].y), (w[1].x, w[1].y), first));
}
}
Shape::Circle { c, dot: true, .. } => terms.push(Terminal {
at: (c.x, c.y),
shape: i,
term: Term::Dot,
}),
_ => {}
}
}
let mut uf = UnionFind::new(terms.len());
for w in terms.windows(2).enumerate() {
let (k, pair) = w;
if let (Term::Wire, Term::Wire) = (pair[0].term, pair[1].term)
&& pair[0].shape == pair[1].shape
{
uf.union(k, k + 1);
}
}
let points: Vec<Pt> = terms.iter().map(|t| t.at).collect();
join_geometry(&mut uf, &points, &segments, self.snap);
let mut by_name: HashMap<String, usize> = HashMap::new();
for (k, t) in terms.iter().enumerate() {
if let Term::Place(n) = t.term {
let key = self.places[n].label.to_ascii_lowercase();
match by_name.get(&key) {
Some(&other) => uf.union(k, other),
None => {
by_name.insert(key, k);
}
}
}
}
let roots: Vec<usize> = (0..terms.len()).map(|k| uf.find(k)).collect();
let mut nets: BTreeMap<usize, Vec<usize>> = BTreeMap::new(); for (k, &r) in roots.iter().enumerate() {
nets.entry(r).or_default().push(k);
}
let user_names: Vec<String> = self
.places
.iter()
.map(|p| p.label.to_ascii_lowercase())
.collect();
let mut name_of: HashMap<usize, String> = HashMap::new();
let mut net_order: Vec<(usize, usize)> = nets
.iter()
.map(|(&r, ks)| (ks.iter().map(|&k| terms[k].shape).min().unwrap_or(0), r))
.collect();
net_order.sort();
for &(_, r) in &net_order {
let ks = &nets[&r];
let grounded = ks.iter().any(|&k| matches!(terms[k].term, Term::Ground));
let mut names: Vec<usize> = ks
.iter()
.filter_map(|&k| match terms[k].term {
Term::Place(n) => Some(n),
_ => None,
})
.collect();
names.sort_unstable();
let mut distinct: Vec<String> = Vec::new();
for n in names {
let l = &self.places[n].label;
if !distinct.iter().any(|m| m.eq_ignore_ascii_case(l)) {
distinct.push(l.clone());
}
}
if grounded {
if let Some(n) = distinct.first() {
let span = self.label_span(n);
self.diag(
PicSeverity::Warning,
PicDiagKind::NodeNameConflict,
format!("place `{n}` is on the ground net; it is named `0`"),
span,
);
}
name_of.insert(r, "0".into());
} else if let Some(first) = distinct.first() {
if distinct.len() > 1 {
let span = self.label_span(&distinct[1]);
self.diag(
PicSeverity::Warning,
PicDiagKind::NodeNameConflict,
format!(
"one net named {}; using `{first}`",
distinct
.iter()
.map(|n| format!("`{n}`"))
.collect::<Vec<_>>()
.join(", ")
),
span,
);
}
name_of.insert(r, first.clone());
}
}
let mut auto = 0usize;
for &(_, r) in &net_order {
if name_of.contains_key(&r) {
continue;
}
let has_pin = nets[&r]
.iter()
.any(|&k| matches!(terms[k].term, Term::Pin { .. }));
if !has_pin {
continue;
}
let name = loop {
auto += 1;
let n = format!("n{auto}");
if !user_names.contains(&n) {
break n;
}
};
name_of.insert(r, name);
}
for &(_, r) in &net_order {
let ks = &nets[&r];
let pins: Vec<usize> = ks
.iter()
.copied()
.filter(|&k| matches!(terms[k].term, Term::Pin { .. }))
.collect();
let grounded = ks.iter().any(|&k| matches!(terms[k].term, Term::Ground));
if pins.is_empty() {
let drawn = ks
.iter()
.copied()
.find(|&k| matches!(terms[k].term, Term::Wire | Term::Dot));
if let Some(k) = drawn {
let what = match name_of.get(&r) {
Some(n) => format!("net `{n}`"),
None => "wire".into(),
};
let span = self.shape_span(terms[k].shape).filter(|s| s.file.is_none());
self.diag(
PicSeverity::Warning,
PicDiagKind::FloatingNet,
format!("{what} connects to no element pin"),
span,
);
} else if let Some(n) = ks.iter().find_map(|&k| match terms[k].term {
Term::Place(n) => Some(n),
_ => None,
}) {
let p = &self.places[n];
let msg = format!("place `{}` names no net: no wire or pin is there", p.label);
let span = p.span.clone();
self.diag(PicSeverity::Warning, PicDiagKind::FloatingNet, msg, span);
}
continue;
}
if pins.len() == 1 && !grounded {
let Term::Pin { elem, pin } = terms[pins[0]].term else {
unreachable!()
};
let e = &elements[elem];
let span = e.span.clone();
let net = name_of.get(&r).cloned().unwrap_or_default();
let msg = format!(
"pin {}.{} is dangling: net `{net}` reaches no other element pin",
e.refdes, e.pins[pin]
);
self.diag(PicSeverity::Warning, PicDiagKind::DanglingPin, msg, span);
}
}
for e in &elements {
if e.pins.len() == 4 && e.refdes.to_ascii_uppercase().starts_with('Q') {
let sub = &name_of[&roots[e.terms[3]]];
let model = e.value.split_whitespace().next().unwrap_or("");
if sub.eq_ignore_ascii_case(model) {
let msg = format!(
"`{}`: substrate net `{sub}` has the name of the model; \
SPICE would read it as the model and the next word as the model name",
e.refdes
);
let span = e.span.clone();
self.diag(
PicSeverity::Warning,
PicDiagKind::NodeNameConflict,
msg,
span,
);
}
}
}
let mut netlist = String::from("* netlist extracted by dune-pic\n");
let mut map = OverlayMap::default();
for e in &elements {
netlist.push_str(&e.refdes);
for &k in &e.terms {
netlist.push(' ');
netlist.push_str(&name_of[&roots[k]]);
}
if !e.value.is_empty() {
netlist.push(' ');
netlist.push_str(&e.value);
}
netlist.push('\n');
let (lo, hi) = d.blocks[e.block].shapes;
let pins = e
.pins
.iter()
.zip(&e.terms)
.map(|(&name, &k)| PinOverlay {
name: name.to_string(),
net: name_of[&roots[k]].clone(),
x: terms[k].at.0,
y: terms[k].at.1,
})
.collect();
let letter = e.refdes.chars().next().unwrap_or(' ').to_ascii_uppercase();
let (value, params) = match e
.value_arg
.and_then(|k| self.value_span(e.block, k, letter))
{
Some((v, p)) => (Some(v), p),
None => (None, Vec::new()),
};
map.elements.push(ElementOverlay {
refdes: e.refdes.clone(),
shapes: lo..hi,
pins,
value,
params,
principal: spans::principal(letter).map(str::to_string),
});
}
for dir in self.directives {
if dir.eq_ignore_ascii_case(".end") {
continue;
}
netlist.push_str(dir);
netlist.push('\n');
}
netlist.push_str(".end\n");
let mut net_shapes: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
for (k, t) in terms.iter().enumerate() {
match t.term {
Term::Wire | Term::Dot => net_shapes.entry(roots[k]).or_default().push(t.shape),
Term::Ground => {
let b = d.shape_blocks[t.shape].expect("marker inside its block");
let (lo, hi) = d.blocks[b].shapes;
net_shapes.entry(roots[k]).or_default().extend(lo..hi);
}
Term::Pin { .. } | Term::Place(_) => {}
}
}
let mut emitted: Vec<String> = Vec::new();
let mut ordered: Vec<(usize, usize)> = net_order.clone();
ordered.sort_by_key(|&(first, r)| (name_of.get(&r).map(|n| n != "0"), first));
for (_, r) in ordered {
let Some(name) = name_of.get(&r) else {
continue;
};
let mut shapes = net_shapes.remove(&r).unwrap_or_default();
shapes.sort_unstable();
shapes.dedup();
if let Some(pos) = emitted.iter().position(|n| n == name) {
map.nets[pos].shapes.extend(shapes);
map.nets[pos].shapes.sort_unstable();
map.nets[pos].shapes.dedup();
continue;
}
emitted.push(name.clone());
map.nets.push(NetOverlay {
name: name.clone(),
shapes,
});
}
Extraction {
netlist,
map,
diagnostics: self.diags,
}
}
fn element(
&mut self,
b: usize,
ms: &[(usize, &'a str, Pt)],
terms: &mut Vec<Terminal>,
seen: &mut HashMap<String, String>,
index: usize,
) -> Option<Element> {
let d = self.d;
let Some(refdes) = d.blocks[b].label.clone() else {
let value = self.value(b, ms, ' ').map(|v| v.0).unwrap_or_default();
let at = ms.first().map(|m| m.2).unwrap_or((0.0, 0.0));
let msg = format!(
"element without a label (refdes) at ({:.3}, {:.3}){}; skipped — write `R1: resistor(…)`",
at.0,
at.1,
if value.is_empty() {
String::new()
} else {
format!(", value `{value}`")
}
);
let span = self.block_span(b);
self.diag(PicSeverity::Error, PicDiagKind::UnlabeledElement, msg, span);
return None;
};
let span = self.block_span(b);
let letter = refdes.chars().next().unwrap_or(' ').to_ascii_uppercase();
let Some(mut pins) = pins_for(letter) else {
self.diag(
PicSeverity::Error,
PicDiagKind::UnknownElement,
format!(
"`{refdes}`: element letter `{letter}` is not supported ({LETTERS}); skipped"
),
span,
);
return None;
};
if letter == 'Q' && ms.iter().any(|m| m.1 == "S") {
pins = BJT4_PINS;
}
if letter == 'X' {
let n = subckt_arity(ms);
if n > 0 {
pins = &SUBCKT_PINS[..n];
}
}
if letter == 'K' && !ms.iter().any(|m| m.1 == COUPLING_MARKER) {
self.diag(
PicSeverity::Error,
PicDiagKind::MissingPin,
format!(
"`{refdes}`: a `K` element is drawn with `coupling(L1, L2, k)`, \
not this macro; skipped"
),
span,
);
return None;
}
let value = self.value(b, ms, letter);
let key = refdes.to_ascii_lowercase();
if let Some(prev) = seen.get(&key) {
self.diag(
PicSeverity::Error,
PicDiagKind::DuplicateRefdes,
format!("refdes `{refdes}` already used by `{prev}`; skipped"),
span,
);
return None;
}
let mut pin_terms = Vec::with_capacity(pins.len());
let mut missing = Vec::new();
for (pi, &pin) in pins.iter().enumerate() {
match ms.iter().find(|m| m.1 == pin) {
Some(&(i, _, p)) => {
pin_terms.push(terms.len());
terms.push(Terminal {
at: p,
shape: i,
term: Term::Pin {
elem: index,
pin: pi,
},
});
}
None => missing.push(pin),
}
}
if !missing.is_empty() {
terms.truncate(terms.len() - pin_terms.len());
self.diag(
PicSeverity::Error,
PicDiagKind::MissingPin,
format!(
"`{refdes}`: a `{letter}` element needs pins {}, but the drawn element \
lacks {} (wrong refdes letter for this macro?); skipped",
pins.join(", "),
missing.join(", ")
),
span,
);
return None;
}
seen.insert(key, refdes.clone());
let Some((value, value_arg)) = value else {
self.diag(
PicSeverity::Error,
PicDiagKind::MissingValue,
format!(
"`{refdes}` draws a display text instead of its value, but its call \
is not the element's own macro (a wrapper?), so the value cannot \
be read; the card is incomplete"
),
span.clone(),
);
return Some(Element {
refdes,
pins,
terms: pin_terms,
value: String::new(),
value_arg: None,
block: b,
span,
});
};
if value.is_empty() {
let (sev, what) = if matches!(letter, 'V' | 'I') {
(PicSeverity::Warning, "it is a 0 V/0 A source")
} else {
(PicSeverity::Error, "the card is incomplete")
};
self.diag(
sev,
PicDiagKind::MissingValue,
format!("`{refdes}` has no value/model text; {what}"),
span.clone(),
);
}
Some(Element {
refdes,
pins,
terms: pin_terms,
value,
value_arg,
block: b,
span,
})
}
fn check_references(&mut self, elements: &[Element]) {
let letter_of = |name: &str| {
elements
.iter()
.find(|e| e.refdes.eq_ignore_ascii_case(name))
.and_then(|e| e.refdes.chars().next())
.map(|c| c.to_ascii_uppercase())
};
for e in elements {
let letter = e.refdes.chars().next().unwrap_or(' ').to_ascii_uppercase();
let toks: Vec<&str> = e.value.split_whitespace().collect();
if toks.is_empty() {
continue;
}
let (want, refs, what): (char, &[&str], &str) = match letter {
'F' | 'H' => (
'V',
&toks[..1],
"a controlling voltage source (`Vname gain`)",
),
'K' => (
'L',
&toks[..toks.len().min(2)],
"two inductors (`L1, L2, k`)",
),
_ => continue,
};
for r in refs {
let problem = match letter_of(r) {
Some(l) if l == want => continue,
Some(_) => format!("`{r}` is not a `{want}` element"),
None if r
.chars()
.next()
.is_some_and(|c| c.eq_ignore_ascii_case(&want)) =>
{
format!("no element `{r}` in the figure")
}
None => format!("`{r}` is not a `{want}` refdes"),
};
self.diag(
PicSeverity::Error,
PicDiagKind::MissingValue,
format!(
"`{}` needs {what}: {problem}; the card is invalid",
e.refdes
),
e.span.clone(),
);
}
if letter == 'K' && toks.len() < 3 {
self.diag(
PicSeverity::Error,
PicDiagKind::MissingValue,
format!("`{}` has no coupling coefficient", e.refdes),
e.span.clone(),
);
}
}
}
fn block_span(&self, b: usize) -> Option<PicSpan> {
self.d.blocks[b].span.as_ref().map(|s| self.span(s))
}
fn value(
&self,
b: usize,
ms: &[(usize, &'a str, Pt)],
letter: char,
) -> Option<(String, Option<usize>)> {
let coupling = letter == 'K';
let drawn = self.value_text(b);
let shows_display = ms.iter().any(|m| m.1 == DISPLAY_MARKER);
let fallback = if shows_display {
None
} else if coupling {
Some((String::new(), None))
} else {
Some((drawn.clone(), None))
};
let arity = if letter == 'X' { subckt_arity(ms) } else { 0 };
let k = match letter {
'E' | 'G' => 4,
_ if ms.iter().any(|m| m.1 == ANCHOR_MARKER) => 1,
'X' if arity > 0 => arity,
_ => 2,
};
let Some(call) = &self.d.blocks[b].call else {
return fallback;
};
let Some(arg) = call.args.get(k) else {
return fallback;
};
if coupling && call.args.len() != 3 {
return fallback;
}
let words = |s: &str| s.split_whitespace().collect::<Vec<_>>().join(" ");
let spliced = unquote(arg);
let display = (DISPLAY_MACROS.contains(&call.name.as_str()) && call.args.len() == k + 2)
.then(|| words(unquote(&call.args[k + 1])))
.filter(|d| !d.is_empty());
if shows_display && display.is_none() {
return None;
}
let expect = if coupling {
format!("k={}", words(spliced))
} else if let Some(d) = display {
d
} else {
words(spliced)
};
if !drawn.is_empty() && expect != words(&drawn) {
return fallback;
}
let pick = |i: usize| unquote(&self.raw_arg(call, i)).trim().to_string();
Some(if coupling {
((0..3).map(pick).collect::<Vec<_>>().join(" "), None)
} else {
(pick(k), Some(k))
})
}
fn raw_arg(&self, call: &MacroCall, i: usize) -> String {
match call.raw_args.get(i) {
Some(raw)
if call.raw_args.len() == call.args.len()
&& raw.span.file.is_none()
&& raw.span.line > self.prelude_lines
&& raw.end_line == raw.span.line =>
{
raw.text.clone()
}
_ => call.args[i].clone(),
}
}
fn value_span(
&self,
b: usize,
k: usize,
letter: char,
) -> Option<(ValueSpanOverlay, Vec<ParamSpanOverlay>)> {
let call = self.d.blocks[b].call.as_ref()?;
if !self.element_macros.contains(&call.name) || call.raw_args.len() != call.args.len() {
return None;
}
let raw = call.raw_args.get(k)?;
if raw.span.file.is_some() || raw.span.line <= self.prelude_lines {
return None;
}
let chars: Vec<char> = raw.text.chars().collect();
let pos = spans::positions(&chars, raw.span.line - self.prelude_lines, raw.span.col);
if pos[chars.len()] != (raw.end_line - self.prelude_lines, raw.span.end_col) {
return None;
}
let (a, z) = spans::value_range(&chars);
let value = ValueSpanOverlay {
span: spans::range(&pos, a, z),
text: chars[a..z].iter().collect(),
};
Some((value, spans::param_overlays(letter, &chars, a, z, &pos)))
}
fn value_text(&self, b: usize) -> String {
let (lo, hi) = self.d.blocks[b].shapes;
for i in lo..hi.min(self.d.shapes.len()) {
if let Shape::Text { text, .. } = &self.d.shapes[i] {
let joined: Vec<&str> = text.iter().map(|t| t.s.trim()).collect();
return joined.join(" ").trim().to_string();
}
}
String::new()
}
fn label_span(&self, label: &str) -> Option<PicSpan> {
self.places
.iter()
.find(|p| p.label.eq_ignore_ascii_case(label))
.and_then(|p| p.span.clone())
}
}
fn subckt_arity(ms: &[(usize, &str, Pt)]) -> usize {
SUBCKT_PINS
.iter()
.take_while(|p| ms.iter().any(|m| m.1 == **p))
.count()
}
fn unquote(arg: &str) -> &str {
let t = arg.trim();
match t.strip_prefix('"').and_then(|r| r.strip_suffix('"')) {
Some(inner) if !inner.contains('"') => inner,
_ => arg,
}
}