Skip to main content

pedalkernel/
dsl.rs

1//! DSL parser for `.pedal` circuit definition files.
2//!
3//! Parses component declarations, net connections, and control mappings
4//! into an AST that can drive WDF tree construction or KiCad export.
5
6use nom::{
7    branch::alt,
8    bytes::complete::{tag, take_till, take_while, take_while1},
9    character::complete::{alpha1, char, multispace1, not_line_ending},
10    combinator::{map, opt, peek, recognize, value},
11    multi::{many0, separated_list1},
12    number::complete::double,
13    sequence::{delimited, pair, preceded, tuple},
14    IResult,
15};
16
17use crate::compiler::components::*;
18
19/// Type alias for boxed component trait objects returned by parser functions.
20type BoxComp = Box<dyn crate::compiler::Component>;
21
22// ---------------------------------------------------------------------------
23// AST
24// ---------------------------------------------------------------------------
25
26/// Rectifier type — determines supply output impedance character.
27#[derive(Debug, Clone, Copy, PartialEq, Default)]
28pub enum RectifierType {
29    /// Tube rectifier (GZ34, 5U4, 5Y3) — high impedance, significant sag.
30    /// Adds ~40–80V drop under load depending on tube type.
31    Tube,
32    /// Solid-state rectifier (silicon diodes) — very low impedance, stiff supply.
33    /// Adds ~1–2V drop under load.
34    #[default]
35    SolidState,
36}
37
38/// Power supply configuration — models B+ rail behavior under load.
39///
40/// In real amps, the power supply has finite output impedance from the
41/// rectifier forward drop, transformer winding resistance, and filter cap ESR.
42/// When the output stage draws current, the B+ voltage sags proportionally.
43///
44/// Simple form: `supply 9V` — just sets nominal voltage, no sag.
45/// Block form: `supply 480V { impedance: 150, filter_cap: 40u, rectifier: tube }`
46#[derive(Debug, Clone, PartialEq)]
47pub struct SupplyConfig {
48    /// Nominal (no-load) supply voltage in volts.
49    pub voltage: f64,
50    /// Supply output impedance in ohms (rectifier + transformer + ESR).
51    /// Higher = more sag. Tube rectifier: 50–200Ω, solid-state: 1–10Ω.
52    pub impedance: Option<f64>,
53    /// Main filter capacitance in farads — sets sag recovery time.
54    /// Larger cap = faster recovery. Typical: 40µF (vintage) to 220µF (modern).
55    pub filter_cap: Option<f64>,
56    /// Rectifier type — tube (saggy) or solid-state (stiff).
57    pub rectifier: RectifierType,
58}
59
60impl SupplyConfig {
61    /// Create a simple supply config with just a voltage (no sag modeling).
62    pub fn voltage_only(voltage: f64) -> Self {
63        Self {
64            voltage,
65            impedance: None,
66            filter_cap: None,
67            rectifier: RectifierType::default(),
68        }
69    }
70
71    /// Returns true if this supply has sag parameters configured.
72    pub fn has_sag(&self) -> bool {
73        self.impedance.is_some()
74    }
75}
76
77/// A named power supply rail.
78///
79/// Allows circuits to have multiple supply voltages with different names
80/// (e.g., `V+`, `V-`, `B+` for bipolar supplies or tube circuits).
81#[derive(Debug, Clone, PartialEq)]
82pub struct NamedSupply {
83    /// Rail name (e.g., "vcc", "V+", "V-", "B+")
84    pub name: String,
85    /// Supply configuration (voltage and optional sag parameters)
86    pub config: SupplyConfig,
87}
88
89impl NamedSupply {
90    /// Create a named supply with just a voltage (no sag modeling).
91    pub fn new(name: impl Into<String>, voltage: f64) -> Self {
92        Self {
93            name: name.into(),
94            config: SupplyConfig::voltage_only(voltage),
95        }
96    }
97
98    /// Create a named supply with full configuration.
99    pub fn with_config(name: impl Into<String>, config: SupplyConfig) -> Self {
100        Self {
101            name: name.into(),
102            config,
103        }
104    }
105}
106
107/// Sidechain target — what the sidechain CV modulates.
108#[derive(Debug, Clone, PartialEq)]
109pub enum SidechainTarget {
110    /// Modulate the push-pull grid bias (variable-mu compression).
111    /// CV is subtracted from the grid bias: Vgrid = Vbias - Vcv.
112    PushPullGridBias,
113}
114
115/// Sidechain definition for feedback compression loops.
116///
117/// Describes a sidechain path that taps the audio output, extracts an
118/// envelope via amplification + rectification + RC time constant, and
119/// feeds back a control voltage to modulate the audio path gain.
120///
121/// ```text
122/// sidechain {
123///     tap: A_node_ch_out          # node to tap audio from
124///     cv: A_node_sc_cv            # node where CV feeds back
125///     target: push_pull_grid      # modulation target
126/// }
127/// ```
128#[derive(Debug, Clone, PartialEq)]
129pub struct SidechainInfo {
130    /// Node name where audio is tapped (e.g., "A_node_ch_out").
131    pub tap_node: String,
132    /// Node name where the CV feeds back (e.g., "A_node_sc_cv").
133    pub cv_node: String,
134    /// What the sidechain CV modulates.
135    pub target: SidechainTarget,
136}
137
138// ─────────────────────────────────────────────────────────────────────────────
139// Init block types
140// ─────────────────────────────────────────────────────────────────────────────
141
142/// Named or explicit initial state for one NL device in an `init { ... }` block.
143///
144/// Named states expand to physics-based voltages at compile time:
145/// - BJT NPN: `saturated` → [Vbe=0.75, Vce=0.1], `cutoff` → [Vbe=0.0, Vce=supply],
146///   `active` → [Vbe=0.65, Vce=supply/2]
147/// - Diode: `forward` → Vd=0.7, `reverse` → Vd=-supply/2
148/// - PNP signs are flipped automatically.
149#[derive(Debug, Clone, PartialEq)]
150pub enum InitState {
151    /// A human-readable alias: "saturated", "cutoff", "active", "forward", "reverse".
152    Named(String),
153    /// Explicit per-terminal operating-point seed from the optional `op { }` block.
154    ///
155    /// For a BJT these are the two nonlinear-port voltages the grouped-NR solves
156    /// over: base-emitter (`vbe`) and collector-emitter (`vce`). This is a pure
157    /// warm-start seed (`v_prev`); it never folds device Jacobians or changes the
158    /// stage's DC source term. Used to test whether an externally-derived
159    /// operating point (e.g. ngspice's `.op`) is a stable WDF fixed point.
160    Explicit { vbe: f64, vce: f64 },
161    /// Explicit **node voltage** seed from the optional `op { nodes { ... } }`
162    /// sub-block. `device_label` carries the circuit node NAME (a pin name like
163    /// `Q3.base`, or a reserved node like `in`/`out`/`vcc`), and `v` is its DC
164    /// voltage. Used to seed reactive (capacitor) ports at a full operating
165    /// point: each cap's stored DC voltage is `V(node_a) − V(node_b)`. Like
166    /// `Explicit`, it is a pure warm-start seed — it never changes the stage's
167    /// DC source term.
168    NodeVoltage { v: f64 },
169}
170
171/// One device hint from the `init { ... }` block.
172///
173/// ```text
174/// init {
175///     Q1: saturated
176///     Q2: cutoff
177/// }
178/// ```
179#[derive(Debug, Clone, PartialEq)]
180pub struct InitHint {
181    /// Component label in the pedal (e.g., "Q1").
182    pub device_label: String,
183    /// Desired initial state.
184    pub state: InitState,
185}
186
187/// Top-level pedal definition.
188#[derive(Debug, Clone, PartialEq)]
189pub struct PedalDef {
190    pub name: String,
191    /// Optional subtitle for UI display (e.g., "Klon Centaur — transparent overdrive")
192    pub subtitle: Option<String>,
193    /// Power supply rails.
194    /// Multiple named supplies are supported (e.g., V+, V-, B+).
195    /// If empty, defaults to a single 9V "vcc" rail in the compiler.
196    pub supplies: Vec<NamedSupply>,
197    pub components: Vec<ComponentDef>,
198    pub nets: Vec<NetDef>,
199    pub controls: Vec<ControlDef>,
200    /// Internal trim pots (not user-facing, factory adjustments)
201    pub trims: Vec<ControlDef>,
202    /// Monitor definitions for real-time metering visualization
203    pub monitors: Vec<MonitorDef>,
204    /// Sidechain definitions for feedback compression loops.
205    /// Each entry defines a tap point, CV return, and modulation target.
206    pub sidechains: Vec<SidechainInfo>,
207    /// Mirrored pot mappings: mirrored_pot_id → source_pot_id.
208    /// A mirrored pot's position is always `1.0 - source.position`.
209    /// Used for dual-gang pots where one gang tracks inversely.
210    pub mirrors: hashbrown::HashMap<String, String>,
211    /// When true, auto-calibrate output level at compile time.
212    pub calibrate: bool,
213    /// Subcircuit definitions. When non-empty, the top-level `components` and
214    /// `nets` become the routing layer connecting subcircuits.
215    pub subcircuits: Vec<SubcircuitDef>,
216    /// Named voltage port declarations (audio I/O, CV, gates, envelope taps).
217    /// Ports are voltage nodes in the circuit, drivable at audio rate.
218    /// When empty, implicit `in`/`out` ports are created by the compiler.
219    pub ports: Vec<PortDef>,
220    /// Optional initial NL device states from the `init { ... }` block.
221    /// Empty when no `init` block is present (the common case — all existing pedals).
222    /// Used by the compiler to seed the homotopy pre-convergence asymmetrically,
223    /// which is required for free-running oscillators (BJT astable multivibrators)
224    /// that would otherwise converge to a symmetric saddle point.
225    pub init_hints: Vec<InitHint>,
226    /// File-based subcircuit instances declared via `id: use("path.pedal")`
227    /// inside the `components { ... }` block. These are FLATTENED into the
228    /// parent's component/net/control lists by [`crate::dsl_expand::expand_uses`]
229    /// before compilation; the compiler never sees them. Empty for the common
230    /// case (no `use(...)` instances).
231    pub uses: Vec<UseInstance>,
232}
233
234/// A file-based subcircuit instance: `micamp: use("../sub/ba284.pedal")`.
235///
236/// Declared inside the `components { ... }` block. The instance `id` namespaces
237/// every component/node of the instantiated sub when flattened into the parent.
238///
239/// An optional `with { }` block overrides specific component values before
240/// flattening:
241///
242/// ```text
243/// g1: use("sub/sub_gain.pedal") with { R1: 348k, C1: 8.2n }
244/// ```
245#[derive(Debug, Clone, PartialEq)]
246pub struct UseInstance {
247    /// Instance identifier (e.g. `micamp`). Used as the namespace prefix.
248    pub id: String,
249    /// Path to the sub `.pedal` file, relative to the parent file's directory.
250    pub path: String,
251    /// Optional component-value overrides: `(component_id, new_value)`.
252    /// Applied to the sub's matching `ComponentDef` before namespacing/merge.
253    /// An error is returned if any id has no matching component in the sub.
254    pub overrides: Vec<(String, f64)>,
255}
256
257/// Named port declaration from the .pedal DSL.
258///
259/// ```text
260/// ports {
261///     audio_in: input
262///     cv_cutoff: input
263///     audio_out: output
264/// }
265/// ```
266#[derive(Debug, Clone, PartialEq)]
267pub struct PortDef {
268    pub name: String,
269    pub direction: pedalkernel_rt::PortDirection,
270    /// Optional source/load impedance in Ohms.
271    /// When set, the WDF VoltageSource gets this Rp instead of the default 1Ω.
272    /// Syntax: `audio_in: input(10k)` → impedance = Some(10_000.0)
273    pub impedance: Option<f64>,
274}
275
276/// A subcircuit block within a pedal/equipment definition.
277/// Subcircuits partition a circuit into named sub-networks that can have
278/// independent rate domains (e.g., audio path vs. sidechain).
279#[derive(Debug, Clone, PartialEq)]
280pub struct SubcircuitDef {
281    /// Name of the subcircuit (e.g., "sidechain", "audio").
282    pub name: String,
283    /// Optional decimation factor (power of 2). None = full audio rate.
284    pub rate: Option<u32>,
285    /// Components local to this subcircuit.
286    pub components: Vec<ComponentDef>,
287    /// Internal nets. Node names not resolving to component pins or reserved
288    /// nodes (gnd, vcc, supply rails) become implicit ports.
289    pub nets: Vec<NetDef>,
290    /// Controls scoped to this subcircuit.
291    pub controls: Vec<ControlDef>,
292    /// Trims scoped to this subcircuit.
293    pub trims: Vec<ControlDef>,
294    /// Monitor definitions scoped to this subcircuit.
295    pub monitors: Vec<MonitorDef>,
296    /// Mirrored pot mappings within this subcircuit.
297    pub mirrors: hashbrown::HashMap<String, String>,
298}
299
300impl PedalDef {
301    /// Returns true if this pedal has an FX send/return loop defined.
302    pub fn has_fx_loop(&self) -> bool {
303        let has_send = self.nets.iter().any(|n| {
304            n.from == Pin::Reserved("fx_send".into())
305                || n.to.contains(&Pin::Reserved("fx_send".into()))
306        });
307        let has_return = self.nets.iter().any(|n| {
308            n.from == Pin::Reserved("fx_return".into())
309                || n.to.contains(&Pin::Reserved("fx_return".into()))
310        });
311        has_send && has_return
312    }
313
314    /// Get the primary supply voltage (first supply, or default 9V).
315    /// For backwards compatibility with single-supply code.
316    pub fn primary_supply_voltage(&self) -> f64 {
317        self.supplies
318            .first()
319            .map(|s| s.config.voltage)
320            .unwrap_or(9.0)
321    }
322
323    /// Get a supply by name.
324    pub fn get_supply(&self, name: &str) -> Option<&NamedSupply> {
325        self.supplies.iter().find(|s| s.name == name)
326    }
327
328    /// Get all supply rail names.
329    pub fn supply_names(&self) -> Vec<&str> {
330        self.supplies.iter().map(|s| s.name.as_str()).collect()
331    }
332
333    /// Check if a name is a supply rail.
334    pub fn is_supply_rail(&self, name: &str) -> bool {
335        self.supplies.iter().any(|s| s.name == name)
336    }
337}
338
339/// A single component declaration, e.g. `R1: resistor(4.7k)`
340#[derive(Debug, Clone)]
341pub struct ComponentDef {
342    pub id: String,
343    pub kind: Box<dyn crate::compiler::Component>,
344}
345
346impl PartialEq for ComponentDef {
347    fn eq(&self, other: &Self) -> bool {
348        self.id == other.id && self.kind.dyn_eq(other.kind.as_ref())
349    }
350}
351
352pub use crate::pot_taper::PotTaper;
353
354/// Op-amp types with different characteristics.
355/// Each type has distinct slew rate, gain-bandwidth product, and input impedance
356/// that affect the tone and response of the circuit.
357#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
358pub enum OpAmpType {
359    /// Generic op-amp (defaults to TL072 characteristics)
360    #[default]
361    Generic,
362    /// TL071 - Single JFET-input op-amp, electrically identical to TL072
363    /// (slew: 13V/µs, GBW: 3MHz). Single package drops into an LM308 socket
364    /// (the "FET RAT" mod); the TL072 is a dual with a different pinout.
365    Tl071,
366    /// TL072 - JFET input, fast, clean (slew: 13V/µs, GBW: 3MHz)
367    /// Common in: Tube Screamer mods, Klon, modern pedals
368    Tl072,
369    /// JRC4558D - BJT input, warmer, compressed (slew: 1.7V/µs, GBW: 3MHz)
370    /// The classic Tube Screamer op-amp
371    Jrc4558,
372    /// LM308 - Slow slew rate gives distinctive compression (slew: 0.3V/µs, GBW: 1MHz)
373    /// The heart of the Pro-Co RAT
374    Lm308,
375    /// LM741 - Classic slow op-amp (slew: 0.5V/µs, GBW: 1MHz)
376    /// Vintage circuits
377    Lm741,
378    /// NE5532 - Low noise, high drive (slew: 9V/µs, GBW: 10MHz)
379    /// Studio-grade circuits
380    Ne5532,
381    /// CA3080 - Operational Transconductance Amplifier (OTA)
382    /// Current-controlled gain, used in compressors like Dyna Comp
383    Ca3080,
384    /// RC4558 - Texas Instruments version of 4558 (slew: 1.7V/µs)
385    Rc4558,
386    /// TL082 - Similar to TL072 but different bias (slew: 13V/µs)
387    Tl082,
388    /// OP07 - Precision low-offset op-amp (slew: 0.3V/µs, GBW: 0.6MHz)
389    Op07,
390}
391
392impl OpAmpType {
393    /// Slew rate in V/µs - affects high frequency response and distortion character
394    pub fn slew_rate(&self) -> f64 {
395        match self {
396            OpAmpType::Generic | OpAmpType::Tl071 | OpAmpType::Tl072 | OpAmpType::Tl082 => 13.0,
397            OpAmpType::Jrc4558 | OpAmpType::Rc4558 => 1.7,
398            OpAmpType::Lm308 => 0.3,
399            OpAmpType::Lm741 => 0.5,
400            OpAmpType::Ne5532 => 9.0,
401            OpAmpType::Ca3080 => 50.0, // OTA - very fast
402            OpAmpType::Op07 => 0.3,
403        }
404    }
405
406    /// Gain-bandwidth product in Hz - affects frequency response
407    pub fn gain_bandwidth(&self) -> f64 {
408        match self {
409            OpAmpType::Generic | OpAmpType::Tl071 | OpAmpType::Tl072 | OpAmpType::Tl082 => 3e6,
410            OpAmpType::Jrc4558 | OpAmpType::Rc4558 => 3e6,
411            OpAmpType::Lm308 => 1e6,
412            OpAmpType::Lm741 => 1e6,
413            OpAmpType::Ne5532 => 10e6,
414            OpAmpType::Ca3080 => 2e6, // Transconductance-dependent
415            OpAmpType::Op07 => 0.6e6,
416        }
417    }
418
419    /// Maximum supply voltage (total V+ to V-)
420    pub fn supply_max(&self) -> f64 {
421        match self {
422            OpAmpType::Generic | OpAmpType::Tl071 | OpAmpType::Tl072 | OpAmpType::Tl082 => 36.0,
423            OpAmpType::Jrc4558 | OpAmpType::Rc4558 => 36.0,
424            OpAmpType::Lm308 => 36.0,
425            OpAmpType::Lm741 => 36.0,
426            OpAmpType::Ne5532 => 44.0,
427            OpAmpType::Ca3080 => 36.0,
428            OpAmpType::Op07 => 44.0,
429        }
430    }
431
432    /// Whether this is an OTA (Operational Transconductance Amplifier)
433    /// OTAs have current output and behave differently
434    pub fn is_ota(&self) -> bool {
435        matches!(self, OpAmpType::Ca3080)
436    }
437}
438
439/// Default SPICE model name for a generic NPN BJT.
440pub const DEFAULT_NPN_MODEL: &str = "GENERIC_NPN";
441/// Default SPICE model name for a generic PNP BJT.
442pub const DEFAULT_PNP_MODEL: &str = "GENERIC_PNP";
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq)]
445pub enum DiodeType {
446    Silicon,
447    Germanium,
448    Led,
449    Schottky,
450}
451
452/// Capacitor dielectric types with different parasitic characteristics.
453///
454/// Different dielectric materials have different leakage and dielectric
455/// absorption properties. Film caps are nearly ideal, while electrolytics
456/// have significant leakage and aging effects.
457#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
458pub enum CapType {
459    /// Film capacitor (polyester, polypropylene, etc.)
460    /// Negligible leakage, no dielectric absorption.
461    /// Used in: coupling caps, tone stacks, most pedal applications.
462    #[default]
463    Film,
464    /// Electrolytic capacitor (aluminum, tantalum).
465    /// Significant leakage (typ. 100kΩ-10MΩ parallel resistance).
466    /// Aging increases leakage. Can exhibit dielectric absorption.
467    /// Used in: power supply filtering, bias decoupling, large coupling caps.
468    Electrolytic,
469    /// Ceramic capacitor (X7R, C0G/NP0, Y5V).
470    /// Low leakage but can have voltage coefficient (capacitance changes with voltage).
471    /// C0G/NP0 types are stable; X7R/Y5V lose capacitance under DC bias.
472    /// Used in: HF bypass, decoupling, RF applications.
473    Ceramic,
474    /// Tantalum capacitor (solid electrolytic).
475    /// Better leakage than aluminum electrolytic, smaller size.
476    /// Used in: compact designs, stable bypass applications.
477    Tantalum,
478}
479
480/// Extended capacitor configuration with parasitic modeling.
481///
482/// Supports leakage resistance (parallel R) and dielectric absorption.
483#[derive(Debug, Clone, Copy, PartialEq)]
484pub struct CapConfig {
485    /// Capacitance value in Farads.
486    pub value: f64,
487    /// Dielectric type (affects default parasitics).
488    pub cap_type: CapType,
489    /// Parallel leakage resistance in ohms. None = ideal (infinite).
490    /// Typical values: 100kΩ (worn electrolytic) to 10MΩ (new electrolytic).
491    pub leakage: Option<f64>,
492    /// Dielectric absorption coefficient (0.0 to 1.0). None = no DA.
493    /// Represents fraction of charge that "soaks in" and slowly returns.
494    /// Typical: 0.01-0.05 for electrolytics, <0.001 for film.
495    pub da: Option<f64>,
496}
497
498impl CapConfig {
499    pub fn new(value: f64) -> Self {
500        Self {
501            value,
502            cap_type: CapType::Film,
503            leakage: None,
504            da: None,
505        }
506    }
507
508    pub fn with_type(mut self, cap_type: CapType) -> Self {
509        self.cap_type = cap_type;
510        self
511    }
512
513    pub fn with_leakage(mut self, leakage: f64) -> Self {
514        self.leakage = Some(leakage);
515        self
516    }
517
518    pub fn with_da(mut self, da: f64) -> Self {
519        self.da = Some(da);
520        self
521    }
522}
523
524#[derive(Debug, Clone, Copy, PartialEq, Eq)]
525pub enum PhotocouplerType {
526    Vtl5c3,
527    Vtl5c1,
528    Nsl32,
529    /// T4B - Electroluminescent panel + CdS LDR used in LA-2A
530    /// EL panel driven by sidechain, LDR in audio path as variable attenuator
531    T4b,
532}
533
534/// LFO waveform shapes for DSL.
535#[derive(Debug, Clone, Copy, PartialEq, Eq)]
536pub enum LfoWaveformDsl {
537    Sine,
538    Triangle,
539    Square,
540    SawUp,
541    SawDown,
542    SampleAndHold,
543}
544
545/// VCO waveform selection for DSL.
546#[derive(Debug, Clone, Copy, PartialEq, Eq)]
547pub enum VcoWaveformDsl {
548    Saw,
549    Triangle,
550    Pulse,
551}
552
553// Triode and Pentode types use String model names, looked up from the
554// embedded model files (triodes.model, pentodes.model) at compile time.
555
556/// MOSFET types for enhancement-mode devices used in guitar pedals.
557#[derive(Debug, Clone, Copy, PartialEq, Eq)]
558pub enum MosfetType {
559    /// 2N7000 N-channel MOSFET - common in Fulltone OCD clipping, modern drives
560    N2n7000,
561    /// IRF520 N-channel power MOSFET - higher current capability
562    Irf520,
563    /// BS250 P-channel MOSFET - used in some boost circuits
564    Bs250,
565    /// IRF9520 P-channel power MOSFET
566    Irf9520,
567}
568
569/// BBD (Bucket-Brigade Device) types for analog delay lines.
570#[derive(Debug, Clone, Copy, PartialEq, Eq)]
571pub enum BbdType {
572    /// MN3207 — 1024-stage, Boss CE-2 chorus
573    Mn3207,
574    /// MN3007 — 1024-stage low-noise, Boss DM-2 delay
575    Mn3007,
576    /// MN3005 — 4096-stage long delay, Memory Man
577    Mn3005,
578}
579
580/// Spring-reverb tank models (Accutronics / RE-201 grounded).
581#[derive(Debug, Clone, Copy, PartialEq, Eq)]
582pub enum SpringTankType {
583    /// Accutronics Type 4 — 17" tank, Fender standard, long decay.
584    Type4,
585    /// Accutronics Type 8 — 9.25" short tank, 3 springs.
586    Type8,
587    /// Accutronics Type 9 — 17", 6-spring (3 coupled pairs), densest.
588    Type9,
589    /// Roland RE-201 small dark 2-spring tank.
590    Re201Tank,
591}
592
593/// Neon bulb types for relaxation oscillators and optocouplers.
594/// Neon bulbs exhibit negative resistance behavior:
595/// - Off until striking voltage is reached (~90V for NE-2)
596/// - Conduct and emit light until voltage drops below maintaining voltage (~60V)
597/// - Used in vintage tremolo/vibrato circuits (Fender, Wurlitzer)
598#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
599pub enum NeonType {
600    /// NE-2 — Standard miniature neon indicator
601    /// Striking: ~90V, Maintaining: ~60V, Current: 0.3-2mA
602    #[default]
603    Ne2,
604    /// NE-51 — Higher current neon lamp
605    /// Striking: ~95V, Maintaining: ~65V, Current: 0.5-3mA
606    Ne51,
607    /// NE-83 — Lower striking voltage variant
608    /// Striking: ~65V, Maintaining: ~50V
609    Ne83,
610}
611
612impl NeonType {
613    /// Striking voltage - voltage at which the bulb ionizes and begins conducting
614    pub fn striking_voltage(&self) -> f64 {
615        match self {
616            NeonType::Ne2 => 90.0,
617            NeonType::Ne51 => 95.0,
618            NeonType::Ne83 => 65.0,
619        }
620    }
621
622    /// Maintaining voltage - voltage below which the bulb extinguishes
623    pub fn maintaining_voltage(&self) -> f64 {
624        match self {
625            NeonType::Ne2 => 60.0,
626            NeonType::Ne51 => 65.0,
627            NeonType::Ne83 => 50.0,
628        }
629    }
630
631    /// Typical operating current in Amps
632    pub fn typical_current(&self) -> f64 {
633        match self {
634            NeonType::Ne2 => 0.5e-3,  // 0.5mA
635            NeonType::Ne51 => 1.0e-3, // 1mA
636            NeonType::Ne83 => 0.3e-3, // 0.3mA
637        }
638    }
639
640    /// Whether this neon bulb emits enough light for optocoupler use
641    pub fn suitable_for_opto(&self) -> bool {
642        // All common neon types work with LDRs
643        true
644    }
645}
646
647// ── Synth-specific component type enums ─────────────────────────────────
648
649/// VCO (Voltage-Controlled Oscillator) IC types.
650#[derive(Debug, Clone, Copy, PartialEq, Eq)]
651pub enum VcoType {
652    /// CEM3340 — Curtis original. Prophet 5, MemoryMoog, Jupiter 6.
653    Cem3340,
654    /// AS3340 — Alfa RPAR clone (currently in production, pin-compatible).
655    As3340,
656    /// V3340 — CoolAudio clone (currently in production, pin-compatible).
657    V3340,
658}
659
660/// VCF (Voltage-Controlled Filter) IC types.
661#[derive(Debug, Clone, Copy, PartialEq, Eq)]
662pub enum VcfType {
663    /// CEM3320 — Curtis 4-pole lowpass. Prophet 5, Oberheim OB-Xa.
664    Cem3320,
665    /// AS3320 — Alfa RPAR clone (currently in production).
666    As3320,
667}
668
669/// VCA (Voltage-Controlled Amplifier) IC types.
670#[derive(Debug, Clone, Copy, PartialEq, Eq)]
671pub enum VcaType {
672    /// SSM2164 — Quad exponential VCA. Widely used in Eurorack.
673    Ssm2164,
674    /// V2164 — CoolAudio clone (currently in production, pin-compatible).
675    V2164,
676}
677
678/// Comparator IC types.
679#[derive(Debug, Clone, Copy, PartialEq, Eq)]
680pub enum ComparatorType {
681    /// LM311 — Single comparator, open-collector output. VCO reset, Schmitt triggers.
682    Lm311,
683    /// LM393 — Dual comparator. Precision applications, window comparators.
684    Lm393,
685}
686
687/// Analog switch IC types.
688#[derive(Debug, Clone, Copy, PartialEq, Eq)]
689pub enum AnalogSwitchType {
690    /// CD4066 — Quad bilateral switch (~100Ω on-resistance). S&H, muting, routing.
691    Cd4066,
692    /// DG411 — Quad SPST analog switch (~25Ω on-resistance). Higher precision.
693    Dg411,
694}
695
696/// Matched transistor pair/array types for exponential converters.
697#[derive(Debug, Clone, Copy, PartialEq, Eq)]
698pub enum MatchedTransistorType {
699    /// SSM2210 — Matched dual NPN. Tight Vbe matching for V/Oct tracking.
700    Ssm2210,
701    /// CA3046 — 5-NPN transistor array (common substrate). Expo converters.
702    Ca3046,
703    /// LM394 — Supermatch pair. Ultralow Vbe offset.
704    Lm394,
705    /// THAT340 — Modern matched quad NPN (THAT Corporation).
706    That340,
707}
708
709// ═══════════════════════════════════════════════════════════════════════════
710// Studio Equipment Types
711// ═══════════════════════════════════════════════════════════════════════════
712
713/// Transformer winding configuration.
714#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
715pub enum WindingType {
716    /// Standard two-terminal winding
717    #[default]
718    Standard,
719    /// Center-tapped winding (adds .ct terminal)
720    CenterTap,
721    /// Push-pull primary (two halves with center tap for B+)
722    PushPull,
723}
724
725/// Audio transformer configuration.
726///
727/// Transformers are modeled with:
728/// - Turns ratio (affects impedance transformation by n²)
729/// - Primary inductance (determines low-frequency response)
730/// - Optional center taps for push-pull configurations
731/// - Parasitic elements (DCR, capacitance) for accurate HF response
732#[derive(Debug, Clone, PartialEq)]
733pub struct TransformerConfig {
734    /// Optional model-library name. When present, library values seed the
735    /// electrical/core fields and explicit DSL fields override them.
736    pub model: Option<String>,
737    /// Turns ratio (primary:secondary). 1.0 = 1:1, 10.0 = 10:1 step-down
738    pub turns_ratio: f64,
739    /// Primary winding inductance in Henries
740    pub primary_inductance: f64,
741    /// Primary winding configuration
742    pub primary_type: WindingType,
743    /// Secondary winding configuration
744    pub secondary_type: WindingType,
745    /// DC resistance of primary winding (Ω), default 0
746    pub primary_dcr: f64,
747    /// DC resistance of secondary winding (Ω), default 0
748    pub secondary_dcr: f64,
749    /// Parasitic capacitance (F), default 0
750    pub capacitance: f64,
751    /// Coupling coefficient (0-1), default 0.99 for audio transformers
752    pub coupling: f64,
753    /// Optional explicit primary leakage inductance in Henries.
754    pub primary_leakage: Option<f64>,
755    /// Optional explicit secondary leakage inductance in Henries.
756    pub secondary_leakage: Option<f64>,
757    /// Optional explicit magnetizing inductance in Henries.
758    pub magnetizing_inductance: Option<f64>,
759    /// Optional core-loss resistance in Ohms.
760    pub core_loss_resistance: Option<f64>,
761    /// Optional primary turns for a nonlinear magnetizing core model.
762    pub core_primary_turns: Option<f64>,
763    /// Optional core cross-sectional area in square meters.
764    pub core_area: Option<f64>,
765    /// Optional magnetic path length in meters.
766    pub core_path_length: Option<f64>,
767    /// Optional air gap length in meters.
768    pub core_gap: Option<f64>,
769    /// Optional standing primary DC bias current in Amps.
770    pub dc_bias_current: Option<f64>,
771    /// Optional Jiles-Atherton saturation magnetization in A/m.
772    pub ja_ms: Option<f64>,
773    /// Optional Jiles-Atherton anhysteretic shape parameter in A/m.
774    pub ja_a: Option<f64>,
775    /// Optional Jiles-Atherton inter-domain coupling.
776    pub ja_alpha: Option<f64>,
777    /// Optional Jiles-Atherton domain-wall pinning parameter in A/m.
778    pub ja_k: Option<f64>,
779    /// Optional Jiles-Atherton reversible magnetization fraction.
780    pub ja_c: Option<f64>,
781    /// Optional tertiary winding turns ratio (primary:tertiary).
782    /// When present, the transformer is modeled as a 3-winding R-type adaptor.
783    /// Used for transformers with NFB windings (e.g., Fairchild 670 sidechain output).
784    pub tertiary_turns_ratio: Option<f64>,
785}
786
787impl Default for TransformerConfig {
788    fn default() -> Self {
789        Self {
790            turns_ratio: 1.0,
791            primary_inductance: 1.0,
792            model: None,
793            primary_type: WindingType::Standard,
794            secondary_type: WindingType::Standard,
795            primary_dcr: 0.0,
796            secondary_dcr: 0.0,
797            capacitance: 0.0,
798            coupling: 0.99,
799            primary_leakage: None,
800            secondary_leakage: None,
801            magnetizing_inductance: None,
802            core_loss_resistance: None,
803            core_primary_turns: None,
804            core_area: None,
805            core_path_length: None,
806            core_gap: None,
807            dc_bias_current: None,
808            ja_ms: None,
809            ja_a: None,
810            ja_alpha: None,
811            ja_k: None,
812            ja_c: None,
813            tertiary_turns_ratio: None,
814        }
815    }
816}
817
818impl TransformerConfig {
819    /// Create a simple transformer with turns ratio and inductance.
820    pub fn new(turns_ratio: f64, primary_inductance: f64) -> Self {
821        Self {
822            turns_ratio,
823            primary_inductance,
824            ..Default::default()
825        }
826    }
827
828    /// Create a transformer backed by an embedded model-library entry.
829    pub fn with_model(turns_ratio: f64, model: String) -> Self {
830        Self {
831            turns_ratio,
832            primary_inductance: 0.0,
833            coupling: 0.0,
834            model: Some(model),
835            ..Default::default()
836        }
837    }
838
839    /// Create a center-tapped secondary transformer.
840    pub fn with_center_tap(mut self) -> Self {
841        self.secondary_type = WindingType::CenterTap;
842        self
843    }
844
845    /// Create a push-pull primary transformer.
846    pub fn with_push_pull_primary(mut self) -> Self {
847        self.primary_type = WindingType::PushPull;
848        self
849    }
850
851    /// Add DC resistance to windings.
852    pub fn with_dcr(mut self, primary: f64, secondary: f64) -> Self {
853        self.primary_dcr = primary;
854        self.secondary_dcr = secondary;
855        self
856    }
857
858    /// Add parasitic capacitance.
859    pub fn with_capacitance(mut self, cap: f64) -> Self {
860        self.capacitance = cap;
861        self
862    }
863
864    /// Add a tertiary winding with the given primary:tertiary turns ratio.
865    pub fn with_tertiary(mut self, tertiary_turns_ratio: f64) -> Self {
866        self.tertiary_turns_ratio = Some(tertiary_turns_ratio);
867        self
868    }
869
870    /// Returns true if this transformer has a tertiary (third) winding.
871    pub fn has_tertiary(&self) -> bool {
872        self.tertiary_turns_ratio.is_some()
873    }
874}
875
876/// Voltage-driven Jiles-Atherton tape head configuration.
877///
878/// Unlike a transformer core (driven by Ampere-law magnetizing CURRENT, whose
879/// knee needs hundreds of mA at studio turns counts), a record/playback head is
880/// a tiny gap/coil whose magnetic field is driven by the head GAP VOLTAGE:
881/// `H = kv * V`. With `kv` chosen so the J-A knee lands at line level (~1 V),
882/// tape saturation tracks signal Drive directly.
883///
884/// The J-A hysteresis parameters share physics with the transformer core
885/// (Jiles & Atherton 1986; Chowdhury DAFx-19 wave-digital tape realization)
886/// but use a SMALL-GEOMETRY head whose saturation occurs at ~1 V:
887///   - `ja_a` is reduced vs an output-transformer core so the Langevin knee is
888///     reached at the small fields a head produces,
889///   - the geometry turns/area/path are nominal (the voltage law uses `kv`, not
890///     Ampere's law, so they only need to be sane positives).
891#[derive(Debug, Clone, PartialEq)]
892pub struct TapeHeadConfig {
893    /// Optional named head model (for provenance / future library lookup).
894    pub model: Option<String>,
895    /// Jiles-Atherton saturation magnetization (A/m).
896    pub ja_ms: f64,
897    /// Jiles-Atherton anhysteretic shape parameter (A/m).
898    pub ja_a: f64,
899    /// Jiles-Atherton inter-domain coupling.
900    pub ja_alpha: f64,
901    /// Jiles-Atherton domain-wall pinning parameter (A/m).
902    pub ja_k: f64,
903    /// Jiles-Atherton reversible magnetization fraction (0..1).
904    pub ja_c: f64,
905    /// Field-per-volt coupling (A/m per V) — places the knee at line level.
906    pub kv: f64,
907    /// Saturating magnetic current scale (A) — harmonic drive strength.
908    pub isat: f64,
909    /// Linear leakage conductance (S) keeping the port well-posed.
910    pub gp: f64,
911    /// Fixed record-bias field offset (A/m): asymmetric transfer -> even
912    /// harmonics (the soft, even-rich tape/transformer colour).
913    pub h_bias: f64,
914    /// WDF port resistance (Ω) — sets the scattering port impedance.
915    pub rp: f64,
916}
917
918impl Default for TapeHeadConfig {
919    fn default() -> Self {
920        Self::studio_head()
921    }
922}
923
924impl TapeHeadConfig {
925    /// A studio record/playback head whose J-A knee lands at ~1 V line level.
926    ///
927    /// Values verified in `pedalkernel-rt/tests/tape_head_voltage.rs`: THD
928    /// climbs monotonically from ~0.7% at 0.05 V to ~7% at 2 V, Newton
929    /// converges in well under the iteration budget, output is finite/stable.
930    pub fn studio_head() -> Self {
931        Self {
932            model: Some("STUDIO-HEAD".to_string()),
933            ja_ms: 3.5e5,
934            ja_a: 1500.0,
935            ja_alpha: 1.6e-3,
936            ja_k: 30.0,
937            ja_c: 0.65,
938            kv: 8000.0,
939            isat: 2.0e-4,
940            gp: 8.0e-5,
941            h_bias: 400.0,
942            rp: 1_000.0,
943        }
944    }
945
946    pub fn with_model(model: String) -> Self {
947        Self {
948            model: Some(model),
949            ..Self::studio_head()
950        }
951    }
952}
953
954/// A net connection: `in -> C1.a` or `C1.b -> R1.a, D1.a`
955#[derive(Debug, Clone, PartialEq)]
956pub struct NetDef {
957    pub from: Pin,
958    pub to: Vec<Pin>,
959}
960
961/// A pin reference – either a reserved node (`in`, `out`, `gnd`, `vcc`),
962/// a component pin (`C1.a`, `R1.b`), or a fork for dynamic routing.
963#[derive(Debug, Clone, PartialEq, Eq, Hash)]
964pub enum Pin {
965    Reserved(String),
966    ComponentPin {
967        component: String,
968        pin: String,
969    },
970    /// Dynamic routing controlled by a switch component.
971    /// `fork(SW_time, [B8.a, B9.a])` routes signal to one of the destinations
972    /// based on the switch position.
973    Fork {
974        /// The switch component that controls routing
975        switch: String,
976        /// Destination pins for each switch position
977        destinations: Vec<Pin>,
978    },
979    /// Reference to a subcircuit port from the parent scope.
980    /// `audio.ctrl` → SubcircuitPort { subcircuit: "audio", port: "ctrl" }
981    SubcircuitPort {
982        subcircuit: String,
983        port: String,
984    },
985}
986
987/// Control mapping: `Gain.position -> "Drive" [0.0, 1.0] = 0.5`
988#[derive(Debug, Clone, PartialEq)]
989pub struct ControlDef {
990    pub component: String,
991    pub property: String,
992    pub label: String,
993    pub range: (f64, f64),
994    pub default: f64,
995}
996
997// MIDI binding: maps a component trigger to a specific MIDI note number.
998
999/// Monitor definition for real-time metering visualization.
1000///
1001/// Specifies which circuit nodes/components to tap for VU meters and visualizations.
1002///
1003/// # Syntax
1004/// ```text
1005/// monitors {
1006///   V1.plate_current -> "Tube 1" [vu]
1007///   output -> "Output Level" [ppm]
1008///   GR.reduction -> "Gain Reduction" [gr]
1009/// }
1010/// ```
1011#[derive(Debug, Clone, PartialEq)]
1012pub struct MonitorDef {
1013    /// Component to monitor (e.g., "V1", "input", "output", "GR")
1014    pub component: String,
1015    /// Property to monitor (e.g., "plate_current", "level", "reduction")
1016    pub property: String,
1017    /// Display label for the meter
1018    pub label: String,
1019    /// Meter type (VU, PPM, peak, gain reduction)
1020    pub meter_type: MeterType,
1021}
1022
1023/// Meter type for monitor visualization.
1024#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1025pub enum MeterType {
1026    /// VU meter (300ms rise/fall, RMS-responding)
1027    #[default]
1028    Vu,
1029    /// Peak Programme Meter (10ms rise, 1.5s fall)
1030    Ppm,
1031    /// True peak with hold
1032    Peak,
1033    /// Gain reduction meter (for compressors)
1034    GainReduction,
1035    /// Tube glow visualization
1036    TubeGlow,
1037    /// Supply sag indicator
1038    SupplySag,
1039}
1040
1041// ---------------------------------------------------------------------------
1042// Helpers
1043// ---------------------------------------------------------------------------
1044
1045/// Consume whitespace **and** `# …` comments.
1046fn ws_comments(input: &str) -> IResult<&str, ()> {
1047    let (input, _) = many0(alt((
1048        value((), multispace1),
1049        value((), pair(char('#'), not_line_ending)),
1050    )))(input)?;
1051    Ok((input, ()))
1052}
1053
1054/// Identifier: starts with alpha/underscore, continues with alphanumeric/underscore.
1055fn identifier(input: &str) -> IResult<&str, &str> {
1056    recognize(pair(
1057        take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'),
1058        take_while(|c: char| c.is_ascii_alphanumeric() || c == '_'),
1059    ))(input)
1060}
1061
1062/// Engineering-notation multiplier suffix.
1063fn eng_suffix(input: &str) -> IResult<&str, f64> {
1064    alt((
1065        value(1e-12, tag("p")),
1066        value(1e-9, tag("n")),
1067        value(1e-6, tag("us")), // microseconds (before 'u' to consume unit)
1068        value(1e-6, tag("u")),
1069        value(1e-6, tag("µ")), // micro (Unicode µ)
1070        value(1e3, tag("k")),
1071        value(1e3, tag("K")), // kilo (uppercase variant)
1072        value(1e6, tag("M")),
1073        value(1e-3, tag("ms")), // milliseconds (before 'm' to consume unit)
1074        value(1e-3, tag("m")),  // milli – after 'M' to disambiguate
1075        value(1.0, tag("H")),   // Henries (unit marker, no scaling)
1076        value(1.0, tag("F")),   // Farads (unit marker, no scaling)
1077        value(1.0, tag("s")),   // Seconds (unit marker, no scaling)
1078        value(1.0, tag("Ω")),   // Ohms (unit marker, no scaling)
1079        value(1.0, tag("R")),   // Ohms alternate notation
1080    ))(input)
1081}
1082
1083/// Parse a number with optional engineering suffix, e.g. `4.7k`, `220n`, `100m`.
1084/// Supports IEC 60062 embedded-decimal notation where the multiplier letter replaces
1085/// the decimal point: `4k7` = 4.7k = 4700, `2u5` = 2.5µF, `0R47` = 0.47Ω.
1086/// Also accepts `inf` for infinite impedance (open circuit).
1087fn eng_value(input: &str) -> IResult<&str, f64> {
1088    // Try `inf` keyword first (infinite impedance / open circuit)
1089    if let Ok((rest, _)) = tag::<&str, &str, nom::error::Error<&str>>("inf")(input) {
1090        return Ok((rest, f64::INFINITY));
1091    }
1092    // Otherwise parse numeric value with optional suffix
1093    let (input, num) = double(input)?;
1094    let (input, mult) = opt(eng_suffix)(input)?;
1095    match mult {
1096        Some(m) => {
1097            // Check for embedded-decimal notation (IEC 60062): digits after the
1098            // multiplier letter form the fractional part.
1099            // e.g. "4k7" → 4.7 × 1e3, "0R47" → 0.47 × 1
1100            let (input, frac_digits) = opt(take_while1(|c: char| c.is_ascii_digit()))(input)?;
1101            if let Some(frac_str) = frac_digits {
1102                let frac: f64 = frac_str.parse().unwrap_or(0.0);
1103                let shift = 10f64.powi(frac_str.len() as i32);
1104                Ok((input, (num + frac / shift) * m))
1105            } else {
1106                Ok((input, num * m))
1107            }
1108        }
1109        None => Ok((input, num)),
1110    }
1111}
1112
1113/// Quoted string: `"Foo Bar"`
1114fn quoted_string(input: &str) -> IResult<&str, &str> {
1115    delimited(char('"'), take_while(|c: char| c != '"'), char('"'))(input)
1116}
1117
1118// ---------------------------------------------------------------------------
1119// Component parsers
1120// ---------------------------------------------------------------------------
1121
1122fn parse_resistor(input: &str) -> IResult<&str, BoxComp> {
1123    let (input, _) = tag("resistor")(input)?;
1124    let (input, _) = char('(')(input)?;
1125    let (input, _) = ws_comments(input)?;
1126    let (input, val) = eng_value(input)?;
1127    let (input, _) = ws_comments(input)?;
1128
1129    // Optional wattage rating (ignored for WDF, but allows parsing)
1130    let (input, _wattage) = opt(tuple((
1131        char(','),
1132        ws_comments,
1133        eng_value, // e.g., 2W or 0.5W
1134        opt(char('W')),
1135    )))(input)?;
1136
1137    let (input, _) = ws_comments(input)?;
1138    let (input, _) = char(')')(input)?;
1139    Ok((input, Box::new(Resistor { value: val })))
1140}
1141
1142/// Parse capacitor type keyword.
1143fn cap_type(input: &str) -> IResult<&str, CapType> {
1144    alt((
1145        value(CapType::Film, tag("film")),
1146        value(CapType::Electrolytic, tag("electrolytic")),
1147        value(CapType::Ceramic, tag("ceramic")),
1148        value(CapType::Tantalum, tag("tantalum")),
1149    ))(input)
1150}
1151
1152/// Parse `leakage: 100k` parameter (resistance in ohms).
1153fn cap_field_leakage(input: &str) -> IResult<&str, f64> {
1154    let (input, _) = tag("leakage")(input)?;
1155    let (input, _) = ws_comments(input)?;
1156    let (input, _) = char(':')(input)?;
1157    let (input, _) = ws_comments(input)?;
1158    let (input, val) = eng_value(input)?;
1159    Ok((input, val))
1160}
1161
1162/// Parse `da: 0.05` parameter (dielectric absorption coefficient 0-1).
1163fn cap_field_da(input: &str) -> IResult<&str, f64> {
1164    let (input, _) = tag("da")(input)?;
1165    let (input, _) = ws_comments(input)?;
1166    let (input, _) = char(':')(input)?;
1167    let (input, _) = ws_comments(input)?;
1168    let (input, val) = double(input)?;
1169    Ok((input, val))
1170}
1171
1172/// Parse capacitor with optional type and parasitics.
1173///
1174/// Syntax:
1175/// - `cap(22u)` - film cap (default), no parasitics
1176/// - `cap(22u, electrolytic)` - electrolytic, no explicit parasitics
1177/// - `cap(22u, electrolytic, leakage: 100k)` - with leakage resistance
1178/// - `cap(22u, electrolytic, leakage: 10k, da: 0.05)` - with leakage and DA
1179fn parse_cap(input: &str) -> IResult<&str, BoxComp> {
1180    let (input, _) = tag("cap")(input)?;
1181    let (input, _) = char('(')(input)?;
1182    let (input, _) = ws_comments(input)?;
1183    let (input, val) = eng_value(input)?;
1184    let (input, _) = ws_comments(input)?;
1185
1186    // Optional cap type (electrolytic, film, ceramic, tantalum)
1187    let (input, ctype) = opt(preceded(tuple((char(','), ws_comments)), cap_type))(input)?;
1188
1189    let (input, _) = ws_comments(input)?;
1190
1191    // Parse optional named parameters (leakage, da) in any order
1192    let mut leakage = None;
1193    let mut da = None;
1194    let mut input = input;
1195
1196    loop {
1197        let (rest, _) = ws_comments(input)?;
1198
1199        // Check for comma-separated parameter
1200        if let Ok((rest2, _)) = char::<_, nom::error::Error<&str>>(',')(rest) {
1201            let (rest3, _) = ws_comments(rest2)?;
1202
1203            // Try parsing each parameter type
1204            if let Ok((rest4, val)) = cap_field_leakage(rest3) {
1205                leakage = Some(val);
1206                input = rest4;
1207                continue;
1208            }
1209            if let Ok((rest4, val)) = cap_field_da(rest3) {
1210                da = Some(val);
1211                input = rest4;
1212                continue;
1213            }
1214        }
1215        break;
1216    }
1217
1218    let (input, _) = ws_comments(input)?;
1219    let (input, _) = char(')')(input)?;
1220
1221    Ok((
1222        input,
1223        Box::new(Capacitor {
1224            config: CapConfig {
1225                value: val,
1226                cap_type: ctype.unwrap_or(CapType::Film),
1227                leakage,
1228                da,
1229            },
1230        }),
1231    ))
1232}
1233
1234fn parse_inductor(input: &str) -> IResult<&str, BoxComp> {
1235    let (input, _) = tag("inductor")(input)?;
1236    let (input, _) = char('(')(input)?;
1237    let (input, val) = eng_value(input)?;
1238    let (input, _) = char(')')(input)?;
1239    Ok((input, Box::new(Inductor { value: val })))
1240}
1241
1242fn diode_type(input: &str) -> IResult<&str, DiodeType> {
1243    alt((
1244        value(DiodeType::Silicon, tag("silicon")),
1245        value(DiodeType::Germanium, tag("germanium")),
1246        value(DiodeType::Led, tag("led")),
1247        value(DiodeType::Schottky, tag("schottky")),
1248    ))(input)
1249}
1250
1251fn parse_diode_pair(input: &str) -> IResult<&str, BoxComp> {
1252    let (input, _) = tag("diode_pair")(input)?;
1253    let (input, _) = char('(')(input)?;
1254    let (input, dt) = diode_type(input)?;
1255    let (input, _) = char(')')(input)?;
1256    Ok((input, Box::new(DiodePair { diode_type: dt })))
1257}
1258
1259fn parse_diode(input: &str) -> IResult<&str, BoxComp> {
1260    let (input, _) = tag("diode")(input)?;
1261    let (input, _) = char('(')(input)?;
1262    let (input, dt) = diode_type(input)?;
1263    let (input, _) = char(')')(input)?;
1264    Ok((input, Box::new(Diode { diode_type: dt })))
1265}
1266
1267/// Parse zener diode: `zener(5.1)` or `zener(5.1v)`
1268///
1269/// The voltage parameter specifies the breakdown voltage in volts.
1270/// Common values: 3.3, 4.7, 5.1, 5.6, 6.2, 9.1, 12
1271fn parse_zener(input: &str) -> IResult<&str, BoxComp> {
1272    let (input, _) = tag("zener")(input)?;
1273    let (input, _) = char('(')(input)?;
1274    let (input, _) = ws_comments(input)?;
1275    let (input, voltage) = double(input)?;
1276    // Optional 'v' suffix for clarity
1277    let (input, _) = opt(tag("v"))(input)?;
1278    let (input, _) = ws_comments(input)?;
1279    let (input, _) = char(')')(input)?;
1280    Ok((
1281        input,
1282        Box::new(Zener {
1283            breakdown_voltage: voltage,
1284        }),
1285    ))
1286}
1287
1288fn parse_pot(input: &str) -> IResult<&str, BoxComp> {
1289    let (input, _) = tag("pot")(input)?;
1290    let (input, _) = char('(')(input)?;
1291    let (input, _) = ws_comments(input)?;
1292    let (input, val) = eng_value(input)?;
1293    let (input, _) = ws_comments(input)?;
1294
1295    // Optional taper type: a (audio/log), b (linear), c (reverse log)
1296    // Also support legacy names: log, linear, audio
1297    let (input, taper) = opt(tuple((
1298        char(','),
1299        ws_comments,
1300        alt((
1301            // Legacy names (must be before single-letter to avoid partial match)
1302            value(PotTaper::A, tag("audio")),
1303            value(PotTaper::B, tag("linear")),
1304            value(PotTaper::A, tag("log")),
1305            // Single-letter tapers (preferred)
1306            value(PotTaper::A, tag("a")),
1307            value(PotTaper::B, tag("b")),
1308            value(PotTaper::C, tag("c")),
1309        )),
1310    )))(input)?;
1311
1312    let taper = taper.map(|(_, _, t)| t).unwrap_or(PotTaper::B);
1313
1314    let (input, _) = ws_comments(input)?;
1315    let (input, _) = char(')')(input)?;
1316    Ok((input, Box::new(Potentiometer { max_r: val, taper })))
1317}
1318
1319/// Parse a SPICE model name: alphanumeric, hyphens, underscores → uppercase.
1320fn model_name_str(input: &str) -> IResult<&str, String> {
1321    let (input, name) = take_while1(|c: char| c.is_alphanumeric() || c == '-' || c == '_')(input)?;
1322    Ok((input, name.to_uppercase()))
1323}
1324
1325fn parse_npn(input: &str) -> IResult<&str, BoxComp> {
1326    let (input, _) = tag("npn")(input)?;
1327    let (input, _) = char('(')(input)?;
1328    let (input, _) = ws_comments(input)?;
1329    let (input, name) = opt(model_name_str)(input)?;
1330    let (input, _) = ws_comments(input)?;
1331    let (input, _) = char(')')(input)?;
1332    Ok((
1333        input,
1334        Box::new(Npn {
1335            model: name.unwrap_or_else(|| DEFAULT_NPN_MODEL.to_string()),
1336        }),
1337    ))
1338}
1339
1340fn parse_pnp(input: &str) -> IResult<&str, BoxComp> {
1341    let (input, _) = tag("pnp")(input)?;
1342    let (input, _) = char('(')(input)?;
1343    let (input, _) = ws_comments(input)?;
1344    let (input, name) = opt(model_name_str)(input)?;
1345    let (input, _) = ws_comments(input)?;
1346    let (input, _) = char(')')(input)?;
1347    Ok((
1348        input,
1349        Box::new(Pnp {
1350            model: name.unwrap_or_else(|| DEFAULT_PNP_MODEL.to_string()),
1351        }),
1352    ))
1353}
1354
1355fn opamp_type(input: &str) -> IResult<&str, OpAmpType> {
1356    alt((
1357        // NOTE: "tl071"/"tl072"/"tl082" are all full 5-char tags, so none can
1358        // prefix-shadow another in this `alt` chain.
1359        value(OpAmpType::Tl071, tag("tl071")),
1360        value(OpAmpType::Tl072, tag("tl072")),
1361        value(OpAmpType::Tl082, tag("tl082")),
1362        value(OpAmpType::Jrc4558, alt((tag("jrc4558"), tag("4558")))),
1363        value(OpAmpType::Rc4558, tag("rc4558")),
1364        value(OpAmpType::Lm308, tag("lm308")),
1365        value(OpAmpType::Lm741, tag("lm741")),
1366        value(
1367            OpAmpType::Ne5532,
1368            alt((tag("njm5532"), tag("ne5532"), tag("5532"))),
1369        ),
1370        value(OpAmpType::Ca3080, tag("ca3080")),
1371        value(OpAmpType::Op07, tag("op07")),
1372    ))(input)
1373}
1374
1375fn parse_opamp(input: &str) -> IResult<&str, BoxComp> {
1376    let (input, _) = tag("opamp")(input)?;
1377    let (input, _) = char('(')(input)?;
1378    let (input, _) = ws_comments(input)?;
1379    let (input, ot) = opt(opamp_type)(input)?;
1380    let (input, _) = ws_comments(input)?;
1381    let (input, _) = char(')')(input)?;
1382    Ok((
1383        input,
1384        Box::new(OpAmp {
1385            op_type: ot.unwrap_or(OpAmpType::Generic),
1386        }),
1387    ))
1388}
1389
1390fn parse_njfet(input: &str) -> IResult<&str, BoxComp> {
1391    let (input, _) = tag("njfet")(input)?;
1392    let (input, _) = char('(')(input)?;
1393    let (input, _) = ws_comments(input)?;
1394    let (input, name) = model_name_str(input)?;
1395    let (input, _) = ws_comments(input)?;
1396    let (input, _) = char(')')(input)?;
1397    Ok((input, Box::new(NJfet { model: name })))
1398}
1399
1400fn parse_pjfet(input: &str) -> IResult<&str, BoxComp> {
1401    let (input, _) = tag("pjfet")(input)?;
1402    let (input, _) = char('(')(input)?;
1403    let (input, _) = ws_comments(input)?;
1404    let (input, name) = model_name_str(input)?;
1405    let (input, _) = ws_comments(input)?;
1406    let (input, _) = char(')')(input)?;
1407    Ok((input, Box::new(PJfet { model: name })))
1408}
1409
1410fn photocoupler_type(input: &str) -> IResult<&str, PhotocouplerType> {
1411    alt((
1412        value(PhotocouplerType::Vtl5c3, tag("vtl5c3")),
1413        value(PhotocouplerType::Vtl5c1, tag("vtl5c1")),
1414        value(PhotocouplerType::Nsl32, tag("nsl32")),
1415        value(PhotocouplerType::T4b, tag("t4b")),
1416    ))(input)
1417}
1418
1419fn parse_photocoupler(input: &str) -> IResult<&str, BoxComp> {
1420    let (input, _) = tag("photocoupler")(input)?;
1421    let (input, _) = char('(')(input)?;
1422    let (input, pt) = photocoupler_type(input)?;
1423    let (input, _) = char(')')(input)?;
1424    Ok((input, Box::new(PhotocouplerComp { coupler_type: pt })))
1425}
1426
1427fn parse_triode(input: &str) -> IResult<&str, BoxComp> {
1428    let (input, _) = tag("triode")(input)?;
1429    let (input, _) = char('(')(input)?;
1430    let (input, _) = ws_comments(input)?;
1431    let (input, name) = model_name_str(input)?;
1432    let (input, _) = ws_comments(input)?;
1433    let (input, _) = char(')')(input)?;
1434    Ok((input, Box::new(Triode { model: name })))
1435}
1436
1437fn parse_pentode(input: &str) -> IResult<&str, BoxComp> {
1438    let (input, _) = tag("pentode")(input)?;
1439    let (input, _) = char('(')(input)?;
1440    let (input, _) = ws_comments(input)?;
1441    let (input, name) = model_name_str(input)?;
1442    let (input, _) = ws_comments(input)?;
1443    let (input, _) = char(')')(input)?;
1444    Ok((input, Box::new(Pentode { model: name })))
1445}
1446
1447fn parse_vari_mu(input: &str) -> IResult<&str, BoxComp> {
1448    let (input, _) = tag("vari_mu")(input)?;
1449    let (input, _) = char('(')(input)?;
1450    let (input, _) = ws_comments(input)?;
1451    let (input, name) = model_name_str(input)?;
1452    let (input, _) = ws_comments(input)?;
1453    let (input, _) = char(')')(input)?;
1454    Ok((input, Box::new(VariMu { model: name })))
1455}
1456
1457fn mosfet_type(input: &str) -> IResult<&str, MosfetType> {
1458    alt((
1459        value(MosfetType::N2n7000, tag("2n7000")),
1460        value(MosfetType::Irf520, tag("irf520")),
1461        value(MosfetType::Bs250, tag("bs250")),
1462        value(MosfetType::Irf9520, tag("irf9520")),
1463    ))(input)
1464}
1465
1466fn parse_nmos(input: &str) -> IResult<&str, BoxComp> {
1467    let (input, _) = tag("nmos")(input)?;
1468    let (input, _) = char('(')(input)?;
1469    let (input, mt) = mosfet_type(input)?;
1470    let (input, _) = char(')')(input)?;
1471    Ok((input, Box::new(Nmos { mosfet_type: mt })))
1472}
1473
1474fn parse_pmos(input: &str) -> IResult<&str, BoxComp> {
1475    let (input, _) = tag("pmos")(input)?;
1476    let (input, _) = char('(')(input)?;
1477    let (input, mt) = mosfet_type(input)?;
1478    let (input, _) = char(')')(input)?;
1479    Ok((input, Box::new(Pmos { mosfet_type: mt })))
1480}
1481
1482fn bbd_type(input: &str) -> IResult<&str, BbdType> {
1483    alt((
1484        value(BbdType::Mn3207, tag("mn3207")),
1485        value(BbdType::Mn3007, tag("mn3007")),
1486        value(BbdType::Mn3005, tag("mn3005")),
1487    ))(input)
1488}
1489
1490/// `bbd(mn3207)` — Bucket-brigade device delay line.
1491fn parse_bbd(input: &str) -> IResult<&str, BoxComp> {
1492    let (input, _) = tag("bbd")(input)?;
1493    let (input, _) = char('(')(input)?;
1494    let (input, bt) = bbd_type(input)?;
1495    let (input, _) = char(')')(input)?;
1496    Ok((input, Box::new(Bbd { bbd_type: bt })))
1497}
1498
1499/// Spring-tank model keyword. `type9` must precede `type8`/`type4` only by
1500/// distinctness; `tag` matches a prefix so order the longer-unique first is
1501/// not needed here (all four are full tokens followed by `)`).
1502fn spring_type(input: &str) -> IResult<&str, SpringTankType> {
1503    alt((
1504        value(SpringTankType::Type4, tag("type4")),
1505        value(SpringTankType::Type8, tag("type8")),
1506        value(SpringTankType::Type9, tag("type9")),
1507        value(SpringTankType::Re201Tank, tag("re201_tank")),
1508    ))(input)
1509}
1510
1511/// `spring(type4)` — dispersive spring-reverb tank (behavioral island).
1512fn parse_spring(input: &str) -> IResult<&str, BoxComp> {
1513    let (input, _) = tag("spring")(input)?;
1514    let (input, _) = char('(')(input)?;
1515    let (input, st) = spring_type(input)?;
1516    let (input, _) = char(')')(input)?;
1517    Ok((input, Box::new(Spring { tank_type: st })))
1518}
1519
1520/// Parse interpolation mode keyword.
1521fn interpolation_mode(input: &str) -> IResult<&str, crate::elements::Interpolation> {
1522    alt((
1523        value(crate::elements::Interpolation::Linear, tag("linear")),
1524        value(crate::elements::Interpolation::Allpass, tag("allpass")),
1525        value(crate::elements::Interpolation::Cubic, tag("cubic")),
1526    ))(input)
1527}
1528
1529/// Parse medium type keyword.
1530fn medium_type(input: &str) -> IResult<&str, crate::elements::Medium> {
1531    alt((
1532        value(crate::elements::Medium::None, tag("none")),
1533        value(crate::elements::Medium::TapeOxide, tag("tape_oxide")),
1534        value(crate::elements::Medium::BbdLeakage, tag("bbd_leakage")),
1535        value(
1536            crate::elements::Medium::DigitalQuantize,
1537            tag("digital_quantize"),
1538        ),
1539    ))(input)
1540}
1541
1542/// `delay_line(1ms, 1200ms)` — Generic delay line.
1543///
1544/// Supports optional interpolation mode and medium type:
1545/// - `delay_line(1ms, 1200ms)` — defaults: allpass interpolation, no medium
1546/// - `delay_line(1ms, 1200ms, allpass)` — explicit interpolation
1547/// - `delay_line(1ms, 1200ms, medium: tape_oxide)` — with medium
1548/// - `delay_line(1ms, 1200ms, allpass, medium: tape_oxide)` — both
1549fn parse_delay_line(input: &str) -> IResult<&str, BoxComp> {
1550    let (input, _) = tag("delay_line")(input)?;
1551    let (input, _) = char('(')(input)?;
1552    let (input, _) = ws_comments(input)?;
1553    let (input, min_delay) = eng_value(input)?;
1554    let (input, _) = ws_comments(input)?;
1555    let (input, _) = char(',')(input)?;
1556    let (input, _) = ws_comments(input)?;
1557    let (input, max_delay) = eng_value(input)?;
1558    let (input, _) = ws_comments(input)?;
1559    // Optional interpolation mode
1560    let (input, interp) = opt(preceded(pair(char(','), ws_comments), interpolation_mode))(input)?;
1561    let (input, _) = ws_comments(input)?;
1562    // Optional medium: keyword
1563    let (input, medium) = opt(preceded(
1564        pair(char(','), ws_comments),
1565        preceded(pair(tag("medium:"), ws_comments), medium_type),
1566    ))(input)?;
1567    let (input, _) = ws_comments(input)?;
1568    let (input, _) = char(')')(input)?;
1569    let interp = interp.unwrap_or(crate::elements::Interpolation::Allpass);
1570    let medium = medium.unwrap_or(crate::elements::Medium::None);
1571    Ok((
1572        input,
1573        Box::new(DelayLineComp {
1574            min_delay,
1575            max_delay,
1576            interpolation: interp,
1577            medium,
1578        }),
1579    ))
1580}
1581
1582/// `tap(DL1, 2.0)` — Read-only tap into a named delay line.
1583///
1584/// Parameters: parent delay line component ID, ratio relative to base delay.
1585fn parse_tap(input: &str) -> IResult<&str, BoxComp> {
1586    let (input, _) = tag("tap")(input)?;
1587    let (input, _) = char('(')(input)?;
1588    let (input, _) = ws_comments(input)?;
1589    let (input, parent_id) = take_while1(|c: char| c.is_alphanumeric() || c == '_')(input)?;
1590    let (input, _) = ws_comments(input)?;
1591    let (input, _) = char(',')(input)?;
1592    let (input, _) = ws_comments(input)?;
1593    let (input, ratio) = double(input)?;
1594    let (input, _) = ws_comments(input)?;
1595    let (input, _) = char(')')(input)?;
1596    Ok((
1597        input,
1598        Box::new(Tap {
1599            parent_id: parent_id.to_string(),
1600            ratio,
1601        }),
1602    ))
1603}
1604
1605fn neon_type(input: &str) -> IResult<&str, NeonType> {
1606    alt((
1607        value(
1608            NeonType::Ne2,
1609            alt((tag("ne2"), tag("ne-2"), tag("NE2"), tag("NE-2"))),
1610        ),
1611        value(
1612            NeonType::Ne51,
1613            alt((tag("ne51"), tag("ne-51"), tag("NE51"), tag("NE-51"))),
1614        ),
1615        value(
1616            NeonType::Ne83,
1617            alt((tag("ne83"), tag("ne-83"), tag("NE83"), tag("NE-83"))),
1618        ),
1619    ))(input)
1620}
1621
1622/// `neon()` or `neon(ne2)` — Neon bulb for relaxation oscillators.
1623/// Used in vintage tremolo circuits (Fender Vibrato, Wurlitzer) paired with LDRs.
1624fn parse_neon(input: &str) -> IResult<&str, BoxComp> {
1625    let (input, _) = tag("neon")(input)?;
1626    let (input, _) = char('(')(input)?;
1627    let (input, _) = ws_comments(input)?;
1628    // Optional type - defaults to NE-2 if empty
1629    let (input, nt) = opt(neon_type)(input)?;
1630    let (input, _) = ws_comments(input)?;
1631    let (input, _) = char(')')(input)?;
1632    Ok((
1633        input,
1634        Box::new(Neon {
1635            neon_type: nt.unwrap_or_default(),
1636        }),
1637    ))
1638}
1639
1640// ── Synth component parsers ─────────────────────────────────────────────
1641
1642fn vco_type(input: &str) -> IResult<&str, VcoType> {
1643    alt((
1644        value(VcoType::Cem3340, tag("cem3340")),
1645        value(VcoType::As3340, tag("as3340")),
1646        value(VcoType::V3340, tag("v3340")),
1647    ))(input)
1648}
1649
1650fn vco_waveform(input: &str) -> IResult<&str, VcoWaveformDsl> {
1651    alt((
1652        value(VcoWaveformDsl::Saw, tag("saw")),
1653        value(VcoWaveformDsl::Triangle, alt((tag("triangle"), tag("tri")))),
1654        value(VcoWaveformDsl::Pulse, alt((tag("pulse"), tag("square")))),
1655    ))(input)
1656}
1657
1658fn parse_vco(input: &str) -> IResult<&str, BoxComp> {
1659    let (input, _) = tag("vco")(input)?;
1660    let (input, _) = char('(')(input)?;
1661    let (input, _) = ws_comments(input)?;
1662    let (input, vt) = vco_type(input)?;
1663    let (input, _) = ws_comments(input)?;
1664    // Optional frequency (default 440 Hz)
1665    let (input, freq) = opt(preceded(tuple((char(','), ws_comments)), eng_value))(input)?;
1666    let (input, _) = ws_comments(input)?;
1667    // Optional waveform (default saw)
1668    let (input, wf) = opt(preceded(tuple((char(','), ws_comments)), vco_waveform))(input)?;
1669    let (input, _) = ws_comments(input)?;
1670    let (input, _) = char(')')(input)?;
1671    Ok((
1672        input,
1673        Box::new(Vco {
1674            vco_type: vt,
1675            base_freq: freq.unwrap_or(440.0),
1676            waveform: wf.unwrap_or(VcoWaveformDsl::Saw),
1677        }),
1678    ))
1679}
1680
1681fn vcf_type(input: &str) -> IResult<&str, VcfType> {
1682    alt((
1683        value(VcfType::Cem3320, tag("cem3320")),
1684        value(VcfType::As3320, tag("as3320")),
1685    ))(input)
1686}
1687
1688fn parse_vcf(input: &str) -> IResult<&str, BoxComp> {
1689    let (input, _) = tag("vcf")(input)?;
1690    let (input, _) = char('(')(input)?;
1691    let (input, _) = ws_comments(input)?;
1692    let (input, vt) = vcf_type(input)?;
1693    let (input, _) = ws_comments(input)?;
1694    let (input, _) = char(')')(input)?;
1695    Ok((input, Box::new(Vcf { vcf_type: vt })))
1696}
1697
1698fn vca_type(input: &str) -> IResult<&str, VcaType> {
1699    alt((
1700        value(VcaType::Ssm2164, tag("ssm2164")),
1701        value(VcaType::V2164, tag("v2164")),
1702    ))(input)
1703}
1704
1705fn parse_vca(input: &str) -> IResult<&str, BoxComp> {
1706    let (input, _) = tag("vca")(input)?;
1707    let (input, _) = char('(')(input)?;
1708    let (input, _) = ws_comments(input)?;
1709    let (input, vt) = vca_type(input)?;
1710    let (input, _) = ws_comments(input)?;
1711    let (input, _) = char(')')(input)?;
1712    Ok((input, Box::new(Vca { vca_type: vt })))
1713}
1714
1715fn comparator_type(input: &str) -> IResult<&str, ComparatorType> {
1716    alt((
1717        value(ComparatorType::Lm311, tag("lm311")),
1718        value(ComparatorType::Lm393, tag("lm393")),
1719    ))(input)
1720}
1721
1722fn parse_comparator(input: &str) -> IResult<&str, BoxComp> {
1723    let (input, _) = tag("comparator")(input)?;
1724    let (input, _) = char('(')(input)?;
1725    let (input, _) = ws_comments(input)?;
1726    let (input, ct) = comparator_type(input)?;
1727    let (input, _) = ws_comments(input)?;
1728    let (input, _) = char(')')(input)?;
1729    Ok((input, Box::new(Comparator { comp_type: ct })))
1730}
1731
1732fn analog_switch_type(input: &str) -> IResult<&str, AnalogSwitchType> {
1733    alt((
1734        value(AnalogSwitchType::Cd4066, tag("cd4066")),
1735        value(AnalogSwitchType::Dg411, tag("dg411")),
1736    ))(input)
1737}
1738
1739fn parse_analog_switch(input: &str) -> IResult<&str, BoxComp> {
1740    let (input, _) = tag("switch")(input)?;
1741    let (input, _) = char('(')(input)?;
1742    let (input, _) = ws_comments(input)?;
1743    let (input, st) = analog_switch_type(input)?;
1744    let (input, _) = ws_comments(input)?;
1745    let (input, _) = char(')')(input)?;
1746    Ok((input, Box::new(AnalogSwitch { switch_type: st })))
1747}
1748
1749fn matched_transistor_type(input: &str) -> IResult<&str, MatchedTransistorType> {
1750    alt((
1751        value(MatchedTransistorType::Ssm2210, tag("ssm2210")),
1752        value(MatchedTransistorType::Ca3046, tag("ca3046")),
1753        value(MatchedTransistorType::Lm394, tag("lm394")),
1754        value(MatchedTransistorType::That340, tag("that340")),
1755    ))(input)
1756}
1757
1758fn parse_matched_npn(input: &str) -> IResult<&str, BoxComp> {
1759    let (input, _) = tag("matched_npn")(input)?;
1760    let (input, _) = char('(')(input)?;
1761    let (input, _) = ws_comments(input)?;
1762    let (input, mt) = matched_transistor_type(input)?;
1763    let (input, _) = ws_comments(input)?;
1764    let (input, _) = char(')')(input)?;
1765    Ok((input, Box::new(MatchedNpn { matched_type: mt })))
1766}
1767
1768fn parse_matched_pnp(input: &str) -> IResult<&str, BoxComp> {
1769    let (input, _) = tag("matched_pnp")(input)?;
1770    let (input, _) = char('(')(input)?;
1771    let (input, _) = ws_comments(input)?;
1772    let (input, mt) = matched_transistor_type(input)?;
1773    let (input, _) = ws_comments(input)?;
1774    let (input, _) = char(')')(input)?;
1775    Ok((input, Box::new(MatchedPnp { matched_type: mt })))
1776}
1777
1778/// `tempco(2k, 3500)` — tempco resistor: nominal_r, ppm/°C
1779fn parse_tempco(input: &str) -> IResult<&str, BoxComp> {
1780    let (input, _) = tag("tempco")(input)?;
1781    let (input, _) = char('(')(input)?;
1782    let (input, _) = ws_comments(input)?;
1783    let (input, resistance) = eng_value(input)?;
1784    let (input, _) = ws_comments(input)?;
1785    let (input, _) = char(',')(input)?;
1786    let (input, _) = ws_comments(input)?;
1787    let (input, ppm) = double(input)?;
1788    let (input, _) = ws_comments(input)?;
1789    let (input, _) = char(')')(input)?;
1790    Ok((input, Box::new(Tempco { resistance, ppm })))
1791}
1792
1793// ═══════════════════════════════════════════════════════════════════════════
1794// Studio Equipment Parsers
1795// ═══════════════════════════════════════════════════════════════════════════
1796
1797/// Parse winding type modifier: `ct` for center-tap, `pp` for push-pull
1798/// Parse winding modifiers. Returns (winding_type, is_primary).
1799/// ct = center-tap secondary, ct_primary = center-tap primary, pp = push-pull
1800fn winding_modifier(input: &str) -> IResult<&str, WindingType> {
1801    alt((
1802        value(WindingType::CenterTap, tag("ct")),
1803        value(WindingType::PushPull, tag("pp")),
1804    ))(input)
1805}
1806
1807/// Parse ct_primary specifically (center-tapped primary)
1808fn winding_modifier_primary(input: &str) -> IResult<&str, WindingType> {
1809    value(WindingType::CenterTap, tag("ct_primary"))(input)
1810}
1811
1812/// `transformer(10:1, JT11P1)` — model-backed transformer
1813/// `transformer(10:1, 2H)` — explicit generic transformer
1814/// `transformer(1:4, 2H, 75, 200p)` — with positional DCR and parasitic cap
1815/// `transformer(1:1, 4H, ct)` — center-tapped secondary
1816/// `transformer(1:1, 4H, pp, ct)` — push-pull primary, center-tapped secondary
1817/// `transformer(10:1, 10H, 150, 300p, ct_primary)` — center-tapped primary
1818/// `transformer(10:1, 2H, dcr=75, Cp=200p)` — with named parasitics
1819/// `transformer(10:1, JT11P1, Lm=8H, Rc=250k)` — model-backed with overrides
1820/// `transformer(10:1, 2H, Llp=20m, Lls=200u, Lm=1.98H, Rc=100k)` — linear T model
1821///
1822/// Positional syntax: transformer(ratio, model|inductance [, dcr] [, cap] [, winding_mod])
1823/// Named syntax: transformer(ratio, model|inductance [, dcr=val] [, Cp=val] [, k=val] [, Lp=val] [, Llp=val] [, Lls=val] [, Lm=val] [, Rc=val])
1824fn parse_transformer(input: &str) -> IResult<&str, BoxComp> {
1825    let (input, _) = tag("transformer")(input)?;
1826    let (input, _) = char('(')(input)?;
1827    let (input, _) = ws_comments(input)?;
1828
1829    // Parse turns ratio: "10:1" or "1:1" or just a number
1830    let (input, ratio_num) = double(input)?;
1831    let (input, ratio) = if let Ok((input, _)) = char::<&str, nom::error::Error<&str>>(':')(input) {
1832        let (input, denom) = double(input)?;
1833        (input, ratio_num / denom)
1834    } else {
1835        (input, ratio_num)
1836    };
1837
1838    let (input, _) = ws_comments(input)?;
1839    let (input, _) = char(',')(input)?;
1840    let (input, _) = ws_comments(input)?;
1841
1842    // Parse either model name or explicit primary inductance.
1843    let (input, mut config) = if let Ok((input, inductance)) = eng_value(input) {
1844        (input, TransformerConfig::new(ratio, inductance))
1845    } else {
1846        let (input, model) = model_name_str(input)?;
1847        (input, TransformerConfig::with_model(ratio, model))
1848    };
1849    let (input, _) = ws_comments(input)?;
1850
1851    // Parse optional modifiers and named parameters
1852    let mut positional_count = 0; // Track positional numeric args after inductance
1853
1854    // Try to parse additional comma-separated options
1855    let mut remaining = input;
1856    loop {
1857        // Check for comma
1858        if let Ok((input, _)) = tuple((
1859            ws_comments,
1860            char::<&str, nom::error::Error<&str>>(','),
1861            ws_comments,
1862        ))(remaining)
1863        {
1864            remaining = input;
1865
1866            // Try ct_primary first (must come before ct to match longer tag)
1867            if let Ok((input, wt)) = winding_modifier_primary(remaining) {
1868                config.primary_type = wt;
1869                remaining = input;
1870                continue;
1871            }
1872
1873            // Try other winding modifiers
1874            if let Ok((input, wt)) = winding_modifier(remaining) {
1875                // Determine if this is for primary or secondary based on context
1876                // pp = push-pull primary, ct = center-tap secondary
1877                if wt == WindingType::PushPull {
1878                    config.primary_type = wt;
1879                } else {
1880                    // ct defaults to secondary unless already set
1881                    if config.secondary_type == WindingType::Standard {
1882                        config.secondary_type = wt;
1883                    } else {
1884                        config.primary_type = wt;
1885                    }
1886                }
1887                remaining = input;
1888                continue;
1889            }
1890
1891            // Try named parameters: dcr=value, Cp=value, k=value
1892            if let Ok((input, _)) = tag::<&str, &str, nom::error::Error<&str>>("dcr")(remaining) {
1893                let (input, _) = ws_comments(input)?;
1894                let (input, _) = char('=')(input)?;
1895                let (input, _) = ws_comments(input)?;
1896                let (input, dcr) = eng_value(input)?;
1897                config.primary_dcr = dcr;
1898                config.secondary_dcr = dcr; // Apply to both by default
1899                remaining = input;
1900                continue;
1901            }
1902
1903            if let Ok((input, _)) =
1904                alt((tag::<&str, &str, nom::error::Error<&str>>("Cp"), tag("cp")))(remaining)
1905            {
1906                let (input, _) = ws_comments(input)?;
1907                let (input, _) = char('=')(input)?;
1908                let (input, _) = ws_comments(input)?;
1909                let (input, cap) = eng_value(input)?;
1910                config.capacitance = cap;
1911                remaining = input;
1912                continue;
1913            }
1914
1915            if let Ok((input, _)) = tag::<&str, &str, nom::error::Error<&str>>("k")(remaining) {
1916                let (input, _) = ws_comments(input)?;
1917                let (input, _) = char('=')(input)?;
1918                let (input, _) = ws_comments(input)?;
1919                let (input, k) = double(input)?;
1920                config.coupling = k;
1921                remaining = input;
1922                continue;
1923            }
1924
1925            if let Ok((input, name)) = alt((
1926                tag::<&str, &str, nom::error::Error<&str>>("Llp"),
1927                tag("llp"),
1928                tag("Lls"),
1929                tag("lls"),
1930                tag("Lm"),
1931                tag("lm"),
1932                tag("Lp"),
1933                tag("lp"),
1934                tag("Rc"),
1935                tag("rc"),
1936                tag("Idc"),
1937                tag("idc"),
1938            ))(remaining)
1939            {
1940                let (input, _) = ws_comments(input)?;
1941                let (input, _) = char('=')(input)?;
1942                let (input, _) = ws_comments(input)?;
1943                let (input, value) = eng_value(input)?;
1944                match name {
1945                    "Llp" | "llp" => config.primary_leakage = Some(value),
1946                    "Lls" | "lls" => config.secondary_leakage = Some(value),
1947                    "Lm" | "lm" => config.magnetizing_inductance = Some(value),
1948                    "Lp" | "lp" => config.primary_inductance = value,
1949                    "Rc" | "rc" => config.core_loss_resistance = Some(value),
1950                    "Idc" | "idc" => config.dc_bias_current = Some(value),
1951                    _ => unreachable!(),
1952                }
1953                remaining = input;
1954                continue;
1955            }
1956
1957            // tertiary=N:M — adds a third winding with its own turns ratio
1958            if let Ok((input, _)) =
1959                tag::<&str, &str, nom::error::Error<&str>>("tertiary")(remaining)
1960            {
1961                let (input, _) = ws_comments(input)?;
1962                let (input, _) = char('=')(input)?;
1963                let (input, _) = ws_comments(input)?;
1964                let (input, t_num) = double(input)?;
1965                let (input, t_ratio) =
1966                    if let Ok((input, _)) = char::<&str, nom::error::Error<&str>>(':')(input) {
1967                        let (input, denom) = double(input)?;
1968                        (input, t_num / denom)
1969                    } else {
1970                        (input, t_num)
1971                    };
1972                config.tertiary_turns_ratio = Some(t_ratio);
1973                remaining = input;
1974                continue;
1975            }
1976
1977            // Try positional numeric values (DCR, then cap)
1978            if let Ok((input, val)) = eng_value(remaining) {
1979                match positional_count {
1980                    0 => {
1981                        // First positional = DCR
1982                        config.primary_dcr = val;
1983                        config.secondary_dcr = val;
1984                    }
1985                    1 => {
1986                        // Second positional = parasitic capacitance
1987                        config.capacitance = val;
1988                    }
1989                    _ => {
1990                        // Too many positional args, stop
1991                        break;
1992                    }
1993                }
1994                positional_count += 1;
1995                remaining = input;
1996                continue;
1997            }
1998
1999            // Unknown parameter, break
2000            break;
2001        } else {
2002            break;
2003        }
2004    }
2005
2006    let (input, _) = ws_comments(remaining)?;
2007    let (input, _) = char(')')(input)?;
2008
2009    Ok((input, Box::new(TransformerComp { config })))
2010}
2011
2012/// `tape_head()` — voltage-driven Jiles-Atherton record/playback head.
2013///
2014/// Drives the magnetic field from PORT VOLTAGE so tape saturation tracks Drive
2015/// at line level (the transformer core saturates only at hundreds of mA).
2016///
2017/// Forms:
2018///   `tape_head()`                         — default studio head
2019///   `tape_head(STUDIO-HEAD)`              — named head model
2020///   `tape_head(kv=900, a=550, isat=4m)`   — named-parameter overrides
2021///
2022/// Override keys: `kv`, `isat`, `gp`, `rp`, `ms`, `a`, `alpha`, `k`, `c`.
2023fn parse_tape_head(input: &str) -> IResult<&str, BoxComp> {
2024    let (input, _) = tag("tape_head")(input)?;
2025    let (input, _) = char('(')(input)?;
2026    let (input, _) = ws_comments(input)?;
2027
2028    let mut config = TapeHeadConfig::studio_head();
2029
2030    // A key=value override token: an override key immediately followed by '='.
2031    // Probed with `peek` so a leading bare model name is not mistaken for a key
2032    // (e.g. `tape_head(kv=...)` must NOT read "kv" as a model name).
2033    fn override_key(input: &str) -> IResult<&str, &str> {
2034        let (rest, name) = alt((
2035            tag("isat"),
2036            tag("alpha"),
2037            tag("h_bias"),
2038            tag("kv"),
2039            tag("gp"),
2040            tag("rp"),
2041            tag("ms"),
2042            tag("a"),
2043            tag("k"),
2044            tag("c"),
2045        ))(input)?;
2046        let (rest, _) = ws_comments(rest)?;
2047        let (rest, _) = peek(char('='))(rest)?;
2048        Ok((rest, name))
2049    }
2050
2051    // Optional leading model name — only if it is NOT a key=value override.
2052    let input = if peek(override_key)(input).is_ok() {
2053        input
2054    } else if let Ok((input, model)) = model_name_str(input) {
2055        config = TapeHeadConfig::with_model(model.to_string());
2056        input
2057    } else {
2058        input
2059    };
2060    let (input, _) = ws_comments(input)?;
2061
2062    // Comma-separated key=value overrides.
2063    let mut remaining = input;
2064    loop {
2065        if let Ok((input, _)) = tuple((
2066            ws_comments,
2067            char::<&str, nom::error::Error<&str>>(','),
2068            ws_comments,
2069        ))(remaining)
2070        {
2071            remaining = input;
2072        }
2073
2074        if let Ok((input, name)) = override_key(remaining) {
2075            let (input, _) = char('=')(input)?;
2076            let (input, _) = ws_comments(input)?;
2077            let (input, value) = eng_value(input)?;
2078            match name {
2079                "kv" => config.kv = value,
2080                "isat" => config.isat = value,
2081                "gp" => config.gp = value,
2082                "rp" => config.rp = value,
2083                "h_bias" => config.h_bias = value,
2084                "ms" => config.ja_ms = value,
2085                "a" => config.ja_a = value,
2086                "alpha" => config.ja_alpha = value,
2087                "k" => config.ja_k = value,
2088                "c" => config.ja_c = value,
2089                _ => unreachable!(),
2090            }
2091            remaining = input;
2092            continue;
2093        }
2094        break;
2095    }
2096
2097    let (input, _) = ws_comments(remaining)?;
2098    let (input, _) = char(')')(input)?;
2099
2100    Ok((input, Box::new(TapeHeadComp { config })))
2101}
2102
2103/// `cap_switched([1.5u, 220n, 68n, 27n])` — capacitor with switchable values
2104fn parse_cap_switched(input: &str) -> IResult<&str, BoxComp> {
2105    let (input, _) = tag("cap_switched")(input)?;
2106    let (input, _) = char('(')(input)?;
2107    let (input, _) = ws_comments(input)?;
2108
2109    // Optional opening bracket
2110    let (input, has_bracket) = opt(char('['))(input)?;
2111    let (input, _) = ws_comments(input)?;
2112
2113    // Parse comma-separated values
2114    let (input, values) =
2115        separated_list1(tuple((ws_comments, char(','), ws_comments)), eng_value)(input)?;
2116
2117    let (input, _) = ws_comments(input)?;
2118
2119    // Closing bracket if we had opening
2120    let input = if has_bracket.is_some() {
2121        let (input, _) = char(']')(input)?;
2122        let (input, _) = ws_comments(input)?;
2123        input
2124    } else {
2125        input
2126    };
2127
2128    let (input, _) = char(')')(input)?;
2129
2130    Ok((input, Box::new(CapSwitched { values })))
2131}
2132
2133/// `inductor_switched(27m, 47m, 82m, 150m)` — inductor with switchable values (multi-tap)
2134/// `inductor_switched([27m, 47m, 82m])` — with optional brackets
2135fn parse_inductor_switched(input: &str) -> IResult<&str, BoxComp> {
2136    let (input, _) = tag("inductor_switched")(input)?;
2137    let (input, _) = char('(')(input)?;
2138    let (input, _) = ws_comments(input)?;
2139
2140    // Optional opening bracket
2141    let (input, has_bracket) = opt(char('['))(input)?;
2142    let (input, _) = ws_comments(input)?;
2143
2144    // Parse comma-separated values
2145    let (input, values) =
2146        separated_list1(tuple((ws_comments, char(','), ws_comments)), eng_value)(input)?;
2147
2148    let (input, _) = ws_comments(input)?;
2149
2150    // Closing bracket if we had opening
2151    let input = if has_bracket.is_some() {
2152        let (input, _) = char(']')(input)?;
2153        let (input, _) = ws_comments(input)?;
2154        input
2155    } else {
2156        input
2157    };
2158
2159    let (input, _) = char(')')(input)?;
2160
2161    Ok((input, Box::new(InductorSwitched { values })))
2162}
2163
2164/// `resistor_switched([12k, 6.8k, 3.9k, 1.5k])` — resistor with switchable values
2165/// Used for ratio selection networks (1176), feedback networks, etc.
2166fn parse_resistor_switched(input: &str) -> IResult<&str, BoxComp> {
2167    let (input, _) = tag("resistor_switched")(input)?;
2168    let (input, _) = char('(')(input)?;
2169    let (input, _) = ws_comments(input)?;
2170
2171    // Optional opening bracket
2172    let (input, has_bracket) = opt(char('['))(input)?;
2173    let (input, _) = ws_comments(input)?;
2174
2175    // Parse comma-separated values
2176    let (input, values) =
2177        separated_list1(tuple((ws_comments, char(','), ws_comments)), eng_value)(input)?;
2178
2179    let (input, _) = ws_comments(input)?;
2180
2181    // Closing bracket if we had opening
2182    let input = if has_bracket.is_some() {
2183        let (input, _) = char(']')(input)?;
2184        let (input, _) = ws_comments(input)?;
2185        input
2186    } else {
2187        input
2188    };
2189
2190    let (input, _) = char(')')(input)?;
2191
2192    Ok((input, Box::new(ResistorSwitched { values })))
2193}
2194
2195/// `switch(2)` — simple n-position mechanical switch
2196/// Unlike AnalogSwitch (CD4066), this is a passive mechanical element.
2197/// Used for mode selection, bypass, etc.
2198fn parse_trigger_input(input: &str) -> IResult<&str, BoxComp> {
2199    let (rest, _) = tag("trigger_input()")(input)?;
2200    Ok((rest, Box::new(TriggerInputComp)))
2201}
2202
2203fn parse_switch(input: &str) -> IResult<&str, BoxComp> {
2204    let (input, _) = tag("switch")(input)?;
2205    let (input, _) = char('(')(input)?;
2206    let (input, _) = ws_comments(input)?;
2207    let (input, positions) = double(input)?;
2208    let (input, _) = ws_comments(input)?;
2209    let (input, _) = char(')')(input)?;
2210
2211    Ok((
2212        input,
2213        Box::new(Switch {
2214            positions: positions as usize,
2215        }),
2216    ))
2217}
2218
2219/// `rotary("20Hz", "30Hz", "60Hz", "100Hz")` — rotary switch with position labels
2220/// `rotary(pos1, pos2, pos3)` — rotary switch with identifier position labels
2221fn parse_rotary_switch(input: &str) -> IResult<&str, BoxComp> {
2222    let (input, _) = tag("rotary")(input)?;
2223    let (input, _) = char('(')(input)?;
2224    let (input, _) = ws_comments(input)?;
2225
2226    // Parse comma-separated position labels (quoted strings or identifiers)
2227    let (input, labels) = separated_list1(
2228        tuple((ws_comments, char(','), ws_comments)),
2229        alt((
2230            map(quoted_string, String::from),
2231            map(identifier, String::from),
2232        )),
2233    )(input)?;
2234
2235    let (input, _) = ws_comments(input)?;
2236    let (input, _) = char(')')(input)?;
2237
2238    Ok((input, Box::new(RotarySwitch { linked_ids: labels })))
2239}
2240
2241fn component_kind(input: &str) -> IResult<&str, BoxComp> {
2242    alt((
2243        alt((
2244            parse_resistor_switched, // must come before parse_resistor
2245            parse_resistor,
2246            parse_cap_switched, // must come before parse_cap
2247            parse_cap,
2248            parse_inductor_switched, // must come before parse_inductor
2249            parse_inductor,
2250            parse_diode_pair, // must come before parse_diode
2251            parse_diode,
2252            parse_zener, // zener diode with voltage parameter
2253            parse_pot,
2254        )),
2255        alt((
2256            parse_npn,
2257            parse_pnp,
2258            parse_opamp,
2259            parse_njfet,
2260            parse_pjfet,
2261            parse_photocoupler,
2262            parse_envelope_follower, // must come before parse_lfo (both are long keywords)
2263            parse_lfo,
2264            parse_triode,
2265            parse_pentode,
2266            parse_vari_mu,
2267        )),
2268        alt((
2269            parse_nmos,
2270            parse_pmos,
2271            parse_bbd,
2272            parse_spring,
2273            parse_delay_line,
2274            parse_tap,
2275            parse_neon,
2276            parse_vco,
2277            parse_vcf,
2278            parse_vca,
2279            parse_comparator,
2280        )),
2281        alt((
2282            parse_analog_switch,
2283            parse_matched_npn, // must come before parse_matched_pnp
2284            parse_matched_pnp,
2285            parse_tempco,
2286            parse_tape_head, // must come before parse_transformer (distinct tag, kept grouped)
2287            parse_transformer,
2288            parse_rotary_switch,
2289            parse_switch, // simple n-position switch
2290            parse_trigger_input,
2291        )),
2292    ))(input)
2293}
2294
2295/// `R1: resistor(4.7k)` or `R1: pot(100k, b) mirrors R2`
2296fn component_def(input: &str) -> IResult<&str, (ComponentDef, Option<String>)> {
2297    let (input, _) = ws_comments(input)?;
2298    let (input, id) = identifier(input)?;
2299    let (input, _) = ws_comments(input)?;
2300    let (input, _) = char(':')(input)?;
2301    let (input, _) = ws_comments(input)?;
2302    let (input, kind) = component_kind(input)?;
2303    // Optional `mirrors <ident>` suffix for dual-gang pots
2304    let (input, mirrors_target) = opt(tuple((
2305        ws_comments,
2306        tag("mirrors"),
2307        ws_comments,
2308        identifier,
2309    )))(input)?;
2310    let mirrors = mirrors_target.map(|(_, _, _, target)| target.to_string());
2311    Ok((
2312        input,
2313        (
2314            ComponentDef {
2315                id: id.to_string(),
2316                kind,
2317            },
2318            mirrors,
2319        ),
2320    ))
2321}
2322
2323// ---------------------------------------------------------------------------
2324// Net parsers
2325// ---------------------------------------------------------------------------
2326
2327const RESERVED_NODES: &[&str] = &[
2328    "in",
2329    "out",
2330    "gnd",
2331    "vcc",
2332    "fx_send",
2333    "fx_return",
2334    // Synth-specific CV/Gate nodes
2335    "gate",      // Note on/off (0V / +5V)
2336    "cv_pitch",  // Pitch CV (1V/Oct standard)
2337    "cv_mod",    // Modulation CV (mod wheel, aftertouch)
2338    "cv_filter", // Filter cutoff CV
2339];
2340
2341/// Parse an identifier or supply rail name (V+, V-, B+, etc.)
2342/// Supply rails can have + or - suffix which regular identifiers don't allow.
2343fn identifier_or_supply_rail(input: &str) -> IResult<&str, &str> {
2344    alt((
2345        // Special rail names with + or - (e.g., "V+", "V-", "B+")
2346        recognize(pair(alpha1, alt((char('+'), char('-'))))),
2347        // Regular identifier
2348        identifier,
2349    ))(input)
2350}
2351
2352fn pin(input: &str) -> IResult<&str, Pin> {
2353    let (input, first) = identifier_or_supply_rail(input)?;
2354
2355    // Parse all dot-separated parts: T1.primary.a -> ["primary", "a"]
2356    let (input, rest_parts) = many0(preceded(char('.'), identifier))(input)?;
2357
2358    if rest_parts.is_empty() {
2359        // No dots — reserved node or bare component
2360        if RESERVED_NODES.contains(&first) {
2361            Ok((input, Pin::Reserved(first.to_string())))
2362        } else {
2363            // Bare component name treated as reserved-style node (includes supply rails like V+, V-)
2364            Ok((input, Pin::Reserved(first.to_string())))
2365        }
2366    } else {
2367        // Has dots — component + pin (pin may include sub-parts)
2368        // T1.primary.a -> component="T1", pin="primary.a"
2369        let pin_name = rest_parts.join(".");
2370        Ok((
2371            input,
2372            Pin::ComponentPin {
2373                component: first.to_string(),
2374                pin: pin_name,
2375            },
2376        ))
2377    }
2378}
2379
2380/// Parse a fork destination: `fork(SW_time, [B8.a, B9.a])`
2381/// Routes signal dynamically based on switch position.
2382fn parse_fork(input: &str) -> IResult<&str, Pin> {
2383    let (input, _) = tag("fork")(input)?;
2384    let (input, _) = char('(')(input)?;
2385    let (input, _) = ws_comments(input)?;
2386    // Switch component ID
2387    let (input, switch_id) = identifier(input)?;
2388    let (input, _) = ws_comments(input)?;
2389    let (input, _) = char(',')(input)?;
2390    let (input, _) = ws_comments(input)?;
2391    // Destination list: [pin1, pin2, ...]
2392    let (input, _) = char('[')(input)?;
2393    let (input, _) = ws_comments(input)?;
2394    let (input, destinations) =
2395        separated_list1(tuple((ws_comments, char(','), ws_comments)), pin)(input)?;
2396    let (input, _) = ws_comments(input)?;
2397    let (input, _) = char(']')(input)?;
2398    let (input, _) = ws_comments(input)?;
2399    let (input, _) = char(')')(input)?;
2400    Ok((
2401        input,
2402        Pin::Fork {
2403            switch: switch_id.to_string(),
2404            destinations,
2405        },
2406    ))
2407}
2408
2409/// Parse a destination pin - either a fork or a regular pin.
2410fn dest_pin(input: &str) -> IResult<&str, Pin> {
2411    alt((parse_fork, pin))(input)
2412}
2413
2414/// `in -> C1.a`  or  `C1.b -> R1.a, D1.a`  or  `B7.a -> fork(SW_time, [B8.a, B9.a])`
2415fn net_def(input: &str) -> IResult<&str, NetDef> {
2416    let (input, _) = ws_comments(input)?;
2417    let (input, from) = pin(input)?;
2418    let (input, _) = ws_comments(input)?;
2419    let (input, _) = tag("->")(input)?;
2420    let (input, _) = ws_comments(input)?;
2421    // Destinations can be regular pins or fork() constructs
2422    let (input, to) =
2423        separated_list1(tuple((ws_comments, char(','), ws_comments)), dest_pin)(input)?;
2424    Ok((input, NetDef { from, to }))
2425}
2426
2427// ---------------------------------------------------------------------------
2428// Control parsers
2429// ---------------------------------------------------------------------------
2430
2431/// `Gain.position -> "Drive" [0.0, 1.0] = 0.5`
2432fn control_def(input: &str) -> IResult<&str, ControlDef> {
2433    let (input, _) = ws_comments(input)?;
2434    let (input, comp) = identifier(input)?;
2435    let (input, _) = char('.')(input)?;
2436    let (input, prop) = identifier(input)?;
2437    let (input, _) = ws_comments(input)?;
2438    let (input, _) = tag("->")(input)?;
2439    let (input, _) = ws_comments(input)?;
2440    let (input, label) = quoted_string(input)?;
2441    let (input, _) = ws_comments(input)?;
2442    // [min, max]
2443    let (input, _) = char('[')(input)?;
2444    let (input, _) = ws_comments(input)?;
2445    let (input, lo) = double(input)?;
2446    let (input, _) = ws_comments(input)?;
2447    let (input, _) = char(',')(input)?;
2448    let (input, _) = ws_comments(input)?;
2449    let (input, hi) = double(input)?;
2450    let (input, _) = ws_comments(input)?;
2451    let (input, _) = char(']')(input)?;
2452    let (input, _) = ws_comments(input)?;
2453    let (input, _) = char('=')(input)?;
2454    let (input, _) = ws_comments(input)?;
2455    let (input, def) = double(input)?;
2456    Ok((
2457        input,
2458        ControlDef {
2459            component: comp.to_string(),
2460            property: prop.to_string(),
2461            label: label.to_string(),
2462            range: (lo, hi),
2463            default: def,
2464        },
2465    ))
2466}
2467
2468fn lfo_waveform(input: &str) -> IResult<&str, LfoWaveformDsl> {
2469    alt((
2470        value(LfoWaveformDsl::Sine, tag("sine")),
2471        value(LfoWaveformDsl::Triangle, tag("triangle")),
2472        value(LfoWaveformDsl::Square, tag("square")),
2473        value(LfoWaveformDsl::SawUp, tag("saw_up")),
2474        value(LfoWaveformDsl::SawDown, tag("saw_down")),
2475        value(LfoWaveformDsl::SampleAndHold, tag("sample_hold")),
2476    ))(input)
2477}
2478
2479/// `envelope_follower(1k, 4.7u, 100k, 1u, 20k)` - attack_r, attack_c, release_r, release_c, sensitivity_r
2480/// Attack τ = R_attack × C_attack, Release τ = R_release × C_release
2481/// Sensitivity gain = R_sensitivity / 10kΩ
2482fn parse_envelope_follower(input: &str) -> IResult<&str, BoxComp> {
2483    let (input, _) = tag("envelope_follower")(input)?;
2484    let (input, _) = char('(')(input)?;
2485    let (input, _) = ws_comments(input)?;
2486    let (input, attack_r) = eng_value(input)?;
2487    let (input, _) = ws_comments(input)?;
2488    let (input, _) = char(',')(input)?;
2489    let (input, _) = ws_comments(input)?;
2490    let (input, attack_c) = eng_value(input)?;
2491    let (input, _) = ws_comments(input)?;
2492    let (input, _) = char(',')(input)?;
2493    let (input, _) = ws_comments(input)?;
2494    let (input, release_r) = eng_value(input)?;
2495    let (input, _) = ws_comments(input)?;
2496    let (input, _) = char(',')(input)?;
2497    let (input, _) = ws_comments(input)?;
2498    let (input, release_c) = eng_value(input)?;
2499    let (input, _) = ws_comments(input)?;
2500    let (input, _) = char(',')(input)?;
2501    let (input, _) = ws_comments(input)?;
2502    let (input, sensitivity_r) = eng_value(input)?;
2503    let (input, _) = ws_comments(input)?;
2504    let (input, _) = char(')')(input)?;
2505    Ok((
2506        input,
2507        Box::new(EnvelopeFollower {
2508            attack_r,
2509            attack_c,
2510            release_r,
2511            release_c,
2512            sensitivity_r,
2513        }),
2514    ))
2515}
2516
2517/// `lfo(triangle, 100k, 220n)` - waveform, timing_r, timing_c
2518/// Frequency is computed as f = 1/(2πRC)
2519fn parse_lfo(input: &str) -> IResult<&str, BoxComp> {
2520    let (input, _) = tag("lfo")(input)?;
2521    let (input, _) = char('(')(input)?;
2522    let (input, _) = ws_comments(input)?;
2523    let (input, waveform) = lfo_waveform(input)?;
2524    let (input, _) = ws_comments(input)?;
2525    let (input, _) = char(',')(input)?;
2526    let (input, _) = ws_comments(input)?;
2527    let (input, timing_r) = eng_value(input)?;
2528    let (input, _) = ws_comments(input)?;
2529    let (input, _) = char(',')(input)?;
2530    let (input, _) = ws_comments(input)?;
2531    let (input, timing_c) = eng_value(input)?;
2532    let (input, _) = ws_comments(input)?;
2533    let (input, _) = char(')')(input)?;
2534    Ok((
2535        input,
2536        Box::new(Lfo {
2537            waveform,
2538            timing_r,
2539            timing_c,
2540        }),
2541    ))
2542}
2543
2544// ---------------------------------------------------------------------------
2545// Section parsers
2546// ---------------------------------------------------------------------------
2547
2548/// One entry inside a `components { ... }` block: either a real component
2549/// definition (with optional `mirrors` target) or a file-based `use(...)`
2550/// subcircuit instance.
2551enum ComponentEntry {
2552    Component(ComponentDef, Option<String>),
2553    Use(UseInstance),
2554}
2555
2556/// Parse one `ident: eng_value` entry inside a `with { }` override block.
2557fn override_entry(input: &str) -> IResult<&str, (String, f64)> {
2558    let (input, _) = ws_comments(input)?;
2559    let (input, comp_id) = identifier(input)?;
2560    let (input, _) = ws_comments(input)?;
2561    let (input, _) = char(':')(input)?;
2562    let (input, _) = ws_comments(input)?;
2563    let (input, val) = eng_value(input)?;
2564    Ok((input, (comp_id.to_string(), val)))
2565}
2566
2567/// Parse an optional trailing `with { R1: 10k, C1: 22n }` override block.
2568/// Returns an empty Vec when the block is absent (backward compatible).
2569fn with_overrides(input: &str) -> IResult<&str, Vec<(String, f64)>> {
2570    let (input, _) = ws_comments(input)?;
2571    // Optional: if there is no `with` keyword, return empty overrides.
2572    if let Ok((input2, _)) = tag::<&str, &str, nom::error::Error<&str>>("with")(input) {
2573        let (input2, _) = ws_comments(input2)?;
2574        let (input2, _) = char('{')(input2)?;
2575        // Parse comma- or whitespace-separated `id: value` entries.
2576        let mut overrides = Vec::new();
2577        let mut input2 = input2;
2578        loop {
2579            let (rest, _) = ws_comments(input2)?;
2580            // Try to consume an optional comma separator.
2581            let rest = if let Ok((r, _)) =
2582                char::<&str, nom::error::Error<&str>>(',')(rest)
2583            {
2584                r
2585            } else {
2586                rest
2587            };
2588            let (rest, _) = ws_comments(rest)?;
2589            // Stop at closing brace.
2590            if rest.starts_with('}') {
2591                input2 = rest;
2592                break;
2593            }
2594            match override_entry(rest) {
2595                Ok((r, entry)) => {
2596                    overrides.push(entry);
2597                    input2 = r;
2598                }
2599                Err(_) => {
2600                    input2 = rest;
2601                    break;
2602                }
2603            }
2604        }
2605        let (input2, _) = ws_comments(input2)?;
2606        let (input2, _) = char('}')(input2)?;
2607        Ok((input2, overrides))
2608    } else {
2609        Ok((input, Vec::new()))
2610    }
2611}
2612
2613/// Parse a `use(...)` subcircuit instance line: `id: use("path string")`.
2614/// Tried BEFORE `component_def` so the `use(...)` form is recognized; falls
2615/// through to a normal component otherwise (backward compatible).
2616///
2617/// An optional `with { R1: 10k, C1: 22n }` block may follow the closing `)`.
2618fn use_instance(input: &str) -> IResult<&str, UseInstance> {
2619    let (input, _) = ws_comments(input)?;
2620    let (input, id) = identifier(input)?;
2621    let (input, _) = ws_comments(input)?;
2622    let (input, _) = char(':')(input)?;
2623    let (input, _) = ws_comments(input)?;
2624    let (input, _) = tag("use")(input)?;
2625    let (input, _) = ws_comments(input)?;
2626    let (input, _) = char('(')(input)?;
2627    let (input, _) = ws_comments(input)?;
2628    let (input, path) = quoted_string(input)?;
2629    let (input, _) = ws_comments(input)?;
2630    let (input, _) = char(')')(input)?;
2631    let (input, overrides) = with_overrides(input)?;
2632    Ok((
2633        input,
2634        UseInstance {
2635            id: id.to_string(),
2636            path: path.to_string(),
2637            overrides,
2638        },
2639    ))
2640}
2641
2642fn component_entry(input: &str) -> IResult<&str, ComponentEntry> {
2643    alt((
2644        nom::combinator::map(use_instance, ComponentEntry::Use),
2645        nom::combinator::map(component_def, |(c, m)| ComponentEntry::Component(c, m)),
2646    ))(input)
2647}
2648
2649fn components_section(
2650    input: &str,
2651) -> IResult<
2652    &str,
2653    (
2654        Vec<ComponentDef>,
2655        hashbrown::HashMap<String, String>,
2656        Vec<UseInstance>,
2657    ),
2658> {
2659    let (input, _) = ws_comments(input)?;
2660    let (input, _) = tag("components")(input)?;
2661    let (input, _) = ws_comments(input)?;
2662    let (input, _) = char('{')(input)?;
2663    let (input, entries) = many0(component_entry)(input)?;
2664    let (input, _) = ws_comments(input)?;
2665    let (input, _) = char('}')(input)?;
2666    let mut mirrors = hashbrown::HashMap::new();
2667    let mut comps = Vec::new();
2668    let mut uses = Vec::new();
2669    for entry in entries {
2670        match entry {
2671            ComponentEntry::Component(comp, mirror_target) => {
2672                if let Some(target) = mirror_target {
2673                    mirrors.insert(comp.id.clone(), target);
2674                }
2675                comps.push(comp);
2676            }
2677            ComponentEntry::Use(u) => uses.push(u),
2678        }
2679    }
2680    Ok((input, (comps, mirrors, uses)))
2681}
2682
2683fn nets_section(input: &str) -> IResult<&str, Vec<NetDef>> {
2684    let (input, _) = ws_comments(input)?;
2685    let (input, _) = tag("nets")(input)?;
2686    let (input, _) = ws_comments(input)?;
2687    let (input, _) = char('{')(input)?;
2688    let (input, nets) = many0(net_def)(input)?;
2689    let (input, _) = ws_comments(input)?;
2690    let (input, _) = char('}')(input)?;
2691    Ok((input, nets))
2692}
2693
2694/// Skip a line we can't parse (for forward compatibility)
2695fn skip_line(input: &str) -> IResult<&str, ()> {
2696    let (input, _) = ws_comments(input)?;
2697    // Skip until newline or closing brace
2698    let (input, _) = take_while(|c: char| c != '\n' && c != '}')(input)?;
2699    // Consume the newline if present
2700    let (input, _) = opt(char('\n'))(input)?;
2701    Ok((input, ()))
2702}
2703
2704/// Try to parse a control_def or skip the line if unparsable
2705fn control_or_skip(input: &str) -> IResult<&str, Option<ControlDef>> {
2706    let (input, _) = ws_comments(input)?;
2707
2708    // Check if we're at closing brace
2709    if input.starts_with('}') {
2710        return Err(nom::Err::Error(nom::error::Error::new(
2711            input,
2712            nom::error::ErrorKind::Char,
2713        )));
2714    }
2715
2716    // Try to parse a control_def
2717    if let Ok((remaining, ctrl)) = control_def(input) {
2718        return Ok((remaining, Some(ctrl)));
2719    }
2720
2721    // Couldn't parse, skip this line
2722    let (remaining, _) = skip_line(input)?;
2723    Ok((remaining, None))
2724}
2725
2726// ---------------------------------------------------------------------------
2727// Ports section parser
2728// ---------------------------------------------------------------------------
2729
2730/// Parse a single port definition: `name: input` or `name: output`
2731fn port_def(input: &str) -> IResult<&str, PortDef> {
2732    let (input, _) = ws_comments(input)?;
2733    let (input, name) = identifier(input)?;
2734    let (input, _) = ws_comments(input)?;
2735    let (input, _) = char(':')(input)?;
2736    let (input, _) = ws_comments(input)?;
2737    let (input, dir_str) = alt((tag("input"), tag("output")))(input)?;
2738    let direction = match dir_str {
2739        "input" => pedalkernel_rt::PortDirection::Input,
2740        "output" => pedalkernel_rt::PortDirection::Output,
2741        _ => unreachable!(),
2742    };
2743    // Optional impedance: input(10k) or output(600)
2744    let (input, impedance) = opt(delimited(char('('), eng_value, char(')')))(input)?;
2745    Ok((
2746        input,
2747        PortDef {
2748            name: name.to_string(),
2749            direction,
2750            impedance,
2751        },
2752    ))
2753}
2754
2755/// Parse the `ports { ... }` section.
2756fn ports_section(input: &str) -> IResult<&str, Vec<PortDef>> {
2757    let (input, _) = ws_comments(input)?;
2758    let (input, _) = tag("ports")(input)?;
2759    let (input, _) = ws_comments(input)?;
2760    let (input, _) = char('{')(input)?;
2761    let (input, ports) = many0(port_def)(input)?;
2762    let (input, _) = ws_comments(input)?;
2763    let (input, _) = char('}')(input)?;
2764    Ok((input, ports))
2765}
2766
2767fn controls_section(input: &str) -> IResult<&str, Vec<ControlDef>> {
2768    let (input, _) = ws_comments(input)?;
2769    let (input, _) = tag("controls")(input)?;
2770    let (input, _) = ws_comments(input)?;
2771    let (input, _) = char('{')(input)?;
2772
2773    // Parse controls, skipping lines we can't understand
2774    let (input, items) = many0(control_or_skip)(input)?;
2775    let ctrls: Vec<ControlDef> = items.into_iter().flatten().collect();
2776
2777    let (input, _) = ws_comments(input)?;
2778    let (input, _) = char('}')(input)?;
2779    Ok((input, ctrls))
2780}
2781
2782/// Internal trim pots (factory adjustments, not user-facing).
2783/// Same syntax as controls section, parsed identically.
2784fn trims_section(input: &str) -> IResult<&str, Vec<ControlDef>> {
2785    let (input, _) = ws_comments(input)?;
2786    let (input, _) = tag("trims")(input)?;
2787    let (input, _) = ws_comments(input)?;
2788    let (input, _) = char('{')(input)?;
2789
2790    // Parse trims, skipping lines we can't understand
2791    let (input, items) = many0(control_or_skip)(input)?;
2792    let trims: Vec<ControlDef> = items.into_iter().flatten().collect();
2793
2794    let (input, _) = ws_comments(input)?;
2795    let (input, _) = char('}')(input)?;
2796    Ok((input, trims))
2797}
2798
2799// ---------------------------------------------------------------------------
2800// Monitors Section
2801// ---------------------------------------------------------------------------
2802
2803/// Parse meter type: vu, ppm, peak, gr, glow, sag
2804fn meter_type(input: &str) -> IResult<&str, MeterType> {
2805    alt((
2806        value(MeterType::Vu, alt((tag("vu"), tag("VU")))),
2807        value(MeterType::Ppm, alt((tag("ppm"), tag("PPM")))),
2808        value(MeterType::Peak, alt((tag("peak"), tag("PEAK")))),
2809        value(
2810            MeterType::GainReduction,
2811            alt((tag("gr"), tag("GR"), tag("gain_reduction"))),
2812        ),
2813        value(MeterType::TubeGlow, alt((tag("glow"), tag("tube_glow")))),
2814        value(MeterType::SupplySag, alt((tag("sag"), tag("supply_sag")))),
2815    ))(input)
2816}
2817
2818/// Parse a single monitor definition:
2819/// `V1.plate_current -> "Tube 1" [vu]`
2820/// `output -> "Output Level" [ppm]`
2821fn monitor_def(input: &str) -> IResult<&str, MonitorDef> {
2822    let (input, _) = ws_comments(input)?;
2823    let (input, component) = identifier(input)?;
2824    let (input, _) = ws_comments(input)?;
2825
2826    // Optional property (defaults to "level")
2827    let (input, property) = opt(preceded(char('.'), identifier))(input)?;
2828    let property = property.unwrap_or("level").to_string();
2829
2830    let (input, _) = ws_comments(input)?;
2831    let (input, _) = tag("->")(input)?;
2832    let (input, _) = ws_comments(input)?;
2833
2834    // Label in quotes
2835    let (input, label) = quoted_string(input)?;
2836    let (input, _) = ws_comments(input)?;
2837
2838    // Meter type in brackets: [vu] or [ppm]
2839    let (input, _) = char('[')(input)?;
2840    let (input, _) = ws_comments(input)?;
2841    let (input, mtype) = meter_type(input)?;
2842    let (input, _) = ws_comments(input)?;
2843    let (input, _) = char(']')(input)?;
2844
2845    Ok((
2846        input,
2847        MonitorDef {
2848            component: component.to_string(),
2849            property,
2850            label: label.to_string(),
2851            meter_type: mtype,
2852        },
2853    ))
2854}
2855
2856/// Try to parse a monitor_def, or skip the line if we can't understand it.
2857fn monitor_or_skip(input: &str) -> IResult<&str, Option<MonitorDef>> {
2858    let (input, _) = ws_comments(input)?;
2859
2860    // Check if we're at closing brace
2861    if input.starts_with('}') {
2862        return Err(nom::Err::Error(nom::error::Error::new(
2863            input,
2864            nom::error::ErrorKind::Char,
2865        )));
2866    }
2867
2868    // Try to parse a monitor_def
2869    if let Ok((remaining, mon)) = monitor_def(input) {
2870        return Ok((remaining, Some(mon)));
2871    }
2872
2873    // Couldn't parse, skip this line
2874    let (remaining, _) = skip_line(input)?;
2875    Ok((remaining, None))
2876}
2877
2878/// Parse the monitors section.
2879fn monitors_section(input: &str) -> IResult<&str, Vec<MonitorDef>> {
2880    let (input, _) = ws_comments(input)?;
2881    let (input, _) = tag("monitors")(input)?;
2882    let (input, _) = ws_comments(input)?;
2883    let (input, _) = char('{')(input)?;
2884
2885    // Parse monitors, skipping lines we can't understand
2886    let (input, maybe_mons) = many0(monitor_or_skip)(input)?;
2887    let mons: Vec<MonitorDef> = maybe_mons.into_iter().flatten().collect();
2888
2889    let (input, _) = ws_comments(input)?;
2890    let (input, _) = char('}')(input)?;
2891    Ok((input, mons))
2892}
2893
2894// ---------------------------------------------------------------------------
2895// Sidechain section
2896// ---------------------------------------------------------------------------
2897
2898/// Parse a single sidechain field: `key: value`
2899fn sidechain_field<'a>(input: &'a str, key: &str) -> IResult<&'a str, &'a str> {
2900    let (input, _) = ws_comments(input)?;
2901    let (input, _) = tag(key)(input)?;
2902    let (input, _) = ws_comments(input)?;
2903    let (input, _) = char(':')(input)?;
2904    let (input, _) = ws_comments(input)?;
2905    let (input, val) = take_while1(|c: char| c.is_alphanumeric() || c == '_')(input)?;
2906    Ok((input, val))
2907}
2908
2909/// Parse a `sidechain { tap: ..., cv: ..., target: ... }` block.
2910fn sidechain_def(input: &str) -> IResult<&str, SidechainInfo> {
2911    let (input, _) = ws_comments(input)?;
2912    let (input, _) = tag("sidechain")(input)?;
2913    let (input, _) = ws_comments(input)?;
2914    let (input, _) = char('{')(input)?;
2915
2916    let mut tap_node = None;
2917    let mut cv_node = None;
2918    let mut target = None;
2919
2920    let mut input = input;
2921    loop {
2922        let (rest, _) = ws_comments(input)?;
2923        if let Ok((rest2, _)) = char::<&str, nom::error::Error<&str>>('}')(rest) {
2924            input = rest2;
2925            break;
2926        }
2927        if let Ok((rest2, val)) = sidechain_field(rest, "tap") {
2928            tap_node = Some(val.to_string());
2929            input = rest2;
2930        } else if let Ok((rest2, val)) = sidechain_field(rest, "cv") {
2931            cv_node = Some(val.to_string());
2932            input = rest2;
2933        } else if let Ok((rest2, val)) = sidechain_field(rest, "target") {
2934            target = Some(match val {
2935                "push_pull_grid" => SidechainTarget::PushPullGridBias,
2936                _ => SidechainTarget::PushPullGridBias, // default
2937            });
2938            input = rest2;
2939        } else {
2940            // Skip unknown line
2941            let (rest2, _) = take_till(|c| c == '\n' || c == '}')(rest)?;
2942            input = rest2;
2943        }
2944    }
2945
2946    let info = SidechainInfo {
2947        tap_node: tap_node.unwrap_or_default(),
2948        cv_node: cv_node.unwrap_or_default(),
2949        target: target.unwrap_or(SidechainTarget::PushPullGridBias),
2950    };
2951
2952    Ok((input, info))
2953}
2954
2955/// Parse a `sidechains { ... }` section containing one or more sidechain definitions.
2956fn sidechains_section(input: &str) -> IResult<&str, Vec<SidechainInfo>> {
2957    let (input, _) = ws_comments(input)?;
2958    let (input, _) = tag("sidechains")(input)?;
2959    let (input, _) = ws_comments(input)?;
2960    let (input, _) = char('{')(input)?;
2961
2962    let (input, sidechains) = many0(sidechain_def)(input)?;
2963
2964    let (input, _) = ws_comments(input)?;
2965    let (input, _) = char('}')(input)?;
2966    Ok((input, sidechains))
2967}
2968
2969// ---------------------------------------------------------------------------
2970// Top-level
2971// ---------------------------------------------------------------------------
2972
2973/// Parse an optional supply voltage declaration.
2974///
2975/// Simple form: `supply 9V` or `supply 250V`
2976/// Block form:
2977/// ```text
2978/// supply 480V {
2979///     impedance: 150        # ohms
2980///     filter_cap: 40u       # farads
2981///     rectifier: tube       # or solid_state
2982/// }
2983/// ```
2984fn supply_section(input: &str) -> IResult<&str, SupplyConfig> {
2985    let (input, _) = ws_comments(input)?;
2986    let (input, _) = tag("supply")(input)?;
2987    let (input, _) = ws_comments(input)?;
2988    // Parse the voltage value - can be like "9V", "9.0V", "250V", etc.
2989    // Also support "9v" lowercase
2990    let (input, voltage) = recognize(pair(double, opt(alt((char('V'), char('v'))))))(input)?;
2991    // Extract just the numeric part
2992    let num_str = voltage.trim_end_matches(['V', 'v']);
2993    let volts = num_str.parse::<f64>().unwrap_or(9.0);
2994
2995    // Try to parse an optional block with sag parameters
2996    let (input, block) = opt(supply_block)(input)?;
2997
2998    let config = if let Some((impedance, filter_cap, rectifier)) = block {
2999        SupplyConfig {
3000            voltage: volts,
3001            impedance,
3002            filter_cap,
3003            rectifier: rectifier.unwrap_or_default(),
3004        }
3005    } else {
3006        SupplyConfig::voltage_only(volts)
3007    };
3008
3009    Ok((input, config))
3010}
3011
3012/// Parse the body of a supply block: `{ impedance: 150, filter_cap: 40u, rectifier: tube }`
3013fn supply_block(input: &str) -> IResult<&str, (Option<f64>, Option<f64>, Option<RectifierType>)> {
3014    let (input, _) = ws_comments(input)?;
3015    let (input, _) = char('{')(input)?;
3016
3017    let mut impedance = None;
3018    let mut filter_cap = None;
3019    let mut rectifier = None;
3020
3021    let mut input = input;
3022    loop {
3023        let (rest, _) = ws_comments(input)?;
3024        // Skip optional comma separators between fields
3025        let rest = if let Ok((r, _)) = char::<&str, nom::error::Error<&str>>(',')(rest) {
3026            let (r, _) = ws_comments(r)?;
3027            r
3028        } else {
3029            rest
3030        };
3031        // Check for closing brace
3032        if let Ok((rest2, _)) = char::<&str, nom::error::Error<&str>>('}')(rest) {
3033            input = rest2;
3034            break;
3035        }
3036        // Try parsing each field
3037        if let Ok((rest2, val)) = supply_field_impedance(rest) {
3038            impedance = Some(val);
3039            input = rest2;
3040        } else if let Ok((rest2, val)) = supply_field_filter_cap(rest) {
3041            filter_cap = Some(val);
3042            input = rest2;
3043        } else if let Ok((rest2, val)) = supply_field_rectifier(rest) {
3044            rectifier = Some(val);
3045            input = rest2;
3046        } else {
3047            // Unknown field — skip to next comma, line, or closing brace
3048            let (rest2, _) = take_till(|c| c == ',' || c == '\n' || c == '}')(rest)?;
3049            input = rest2;
3050        }
3051    }
3052
3053    Ok((input, (impedance, filter_cap, rectifier)))
3054}
3055
3056/// Parse `impedance: 150` (value in ohms).
3057fn supply_field_impedance(input: &str) -> IResult<&str, f64> {
3058    let (input, _) = tag("impedance")(input)?;
3059    let (input, _) = ws_comments(input)?;
3060    let (input, _) = char(':')(input)?;
3061    let (input, _) = ws_comments(input)?;
3062    let (input, val) = eng_value(input)?;
3063    // Skip optional inline comment
3064    let (input, _) = opt(preceded(ws_comments, preceded(char('#'), not_line_ending)))(input)?;
3065    Ok((input, val))
3066}
3067
3068/// Parse `filter_cap: 40u` (value in farads).
3069fn supply_field_filter_cap(input: &str) -> IResult<&str, f64> {
3070    let (input, _) = tag("filter_cap")(input)?;
3071    let (input, _) = ws_comments(input)?;
3072    let (input, _) = char(':')(input)?;
3073    let (input, _) = ws_comments(input)?;
3074    let (input, val) = eng_value(input)?;
3075    // Skip optional inline comment
3076    let (input, _) = opt(preceded(ws_comments, preceded(char('#'), not_line_ending)))(input)?;
3077    Ok((input, val))
3078}
3079
3080/// Parse `rectifier: tube` or `rectifier: solid_state`.
3081fn supply_field_rectifier(input: &str) -> IResult<&str, RectifierType> {
3082    let (input, _) = tag("rectifier")(input)?;
3083    let (input, _) = ws_comments(input)?;
3084    let (input, _) = char(':')(input)?;
3085    let (input, _) = ws_comments(input)?;
3086    let (input, rtype) = alt((
3087        value(RectifierType::Tube, tag("tube")),
3088        value(RectifierType::SolidState, tag("solid_state")),
3089    ))(input)?;
3090    // Skip optional inline comment
3091    let (input, _) = opt(preceded(ws_comments, preceded(char('#'), not_line_ending)))(input)?;
3092    Ok((input, rtype))
3093}
3094
3095/// Parse a single named supply rail declaration.
3096///
3097/// Examples:
3098/// - `V+: 15V`
3099/// - `V-: -15V`
3100/// - `B+: 300V { impedance: 100, rectifier: tube }`
3101fn named_supply(input: &str) -> IResult<&str, NamedSupply> {
3102    let (input, _) = ws_comments(input)?;
3103    // Parse rail name - can be identifier or special chars like "V+", "V-"
3104    let (input, name) = identifier_or_supply_rail(input)?;
3105    let (input, _) = ws_comments(input)?;
3106    let (input, _) = char(':')(input)?;
3107    let (input, _) = ws_comments(input)?;
3108
3109    // Parse optional negative sign for voltage
3110    let (input, is_negative) = opt(char('-'))(input)?;
3111
3112    // Parse the voltage value
3113    let (input, voltage_str) = recognize(pair(double, opt(alt((char('V'), char('v'))))))(input)?;
3114    let num_str = voltage_str.trim_end_matches(['V', 'v']);
3115    let mut volts = num_str.parse::<f64>().unwrap_or(9.0);
3116    if is_negative.is_some() {
3117        volts = -volts;
3118    }
3119
3120    // Try to parse an optional block with sag parameters
3121    let (input, block) = opt(supply_block)(input)?;
3122
3123    let config = if let Some((impedance, filter_cap, rectifier)) = block {
3124        SupplyConfig {
3125            voltage: volts,
3126            impedance,
3127            filter_cap,
3128            rectifier: rectifier.unwrap_or_default(),
3129        }
3130    } else {
3131        SupplyConfig::voltage_only(volts)
3132    };
3133
3134    Ok((
3135        input,
3136        NamedSupply {
3137            name: name.to_string(),
3138            config,
3139        },
3140    ))
3141}
3142
3143/// Parse a `supplies { }` block with multiple named supply rails.
3144///
3145/// Example:
3146/// ```text
3147/// supplies {
3148///     V+: 15V
3149///     V-: -15V
3150///     B+: 300V { impedance: 100, rectifier: tube }
3151/// }
3152/// ```
3153fn supplies_section(input: &str) -> IResult<&str, Vec<NamedSupply>> {
3154    let (input, _) = ws_comments(input)?;
3155    let (input, _) = tag("supplies")(input)?;
3156    let (input, _) = ws_comments(input)?;
3157    let (input, _) = char('{')(input)?;
3158
3159    let (input, supplies) = many0(named_supply)(input)?;
3160
3161    let (input, _) = ws_comments(input)?;
3162    let (input, _) = char('}')(input)?;
3163
3164    Ok((input, supplies))
3165}
3166
3167// ---------------------------------------------------------------------------
3168// Subcircuit section
3169// ---------------------------------------------------------------------------
3170
3171/// Parse a `rate: 1/N` declaration where N must be a power of two.
3172fn parse_rate(input: &str) -> IResult<&str, u32> {
3173    let (input, _) = ws_comments(input)?;
3174    let (input, _) = tag("rate")(input)?;
3175    let (input, _) = ws_comments(input)?;
3176    let (input, _) = char(':')(input)?;
3177    let (input, _) = ws_comments(input)?;
3178    let (input, _) = tag("1")(input)?;
3179    let (input, _) = ws_comments(input)?;
3180    let (input, _) = char('/')(input)?;
3181    let (input, _) = ws_comments(input)?;
3182    let (input, divisor) = nom::character::complete::u32(input)?;
3183    // Validate power of two
3184    if divisor == 0 || (divisor & (divisor - 1)) != 0 {
3185        return Err(nom::Err::Failure(nom::error::Error::new(
3186            input,
3187            nom::error::ErrorKind::Verify,
3188        )));
3189    }
3190    Ok((input, divisor))
3191}
3192
3193/// Parse a single `subcircuit name { ... }` block.
3194fn subcircuit_block(input: &str) -> IResult<&str, SubcircuitDef> {
3195    let (input, _) = ws_comments(input)?;
3196    let (input, _) = tag("subcircuit")(input)?;
3197    let (input, _) = ws_comments(input)?;
3198    let (input, name) = identifier(input)?;
3199    let (input, _) = ws_comments(input)?;
3200    let (input, _) = char('{')(input)?;
3201
3202    // Optional rate declaration
3203    let (input, rate) = opt(parse_rate)(input)?;
3204
3205    // Parse inner sections (same as parse_pedal internals)
3206    // `use(...)` instances are not supported inside inline subcircuit blocks;
3207    // discard any third tuple element.
3208    let (input, (components, mirrors, _uses)) = components_section(input)?;
3209    let (input, nets) = nets_section(input)?;
3210    let (input, controls) = opt(controls_section)(input)?;
3211    let (input, trims) = opt(trims_section)(input)?;
3212    let (input, monitors) = opt(monitors_section)(input)?;
3213
3214    let (input, _) = ws_comments(input)?;
3215    let (input, _) = char('}')(input)?;
3216
3217    Ok((
3218        input,
3219        SubcircuitDef {
3220            name: name.to_string(),
3221            rate,
3222            components,
3223            nets,
3224            controls: controls.unwrap_or_default(),
3225            trims: trims.unwrap_or_default(),
3226            monitors: monitors.unwrap_or_default(),
3227            mirrors,
3228        },
3229    ))
3230}
3231
3232/// Reclassify `ComponentPin` references in top-level nets where the "component"
3233/// name matches a declared subcircuit. `audio.ctrl` parsed as `ComponentPin`
3234/// becomes `SubcircuitPort { subcircuit: "audio", port: "ctrl" }`.
3235fn resolve_subcircuit_pins(pedal: &mut PedalDef) {
3236    use std::collections::HashSet;
3237    // Both inline subcircuit names AND file-based `use(...)` instance ids are
3238    // treated as subcircuit-port owners: a net pin `id.port` resolves to a
3239    // `Pin::SubcircuitPort { subcircuit: id, port }`. The file-based ones are
3240    // later consumed by `crate::dsl_expand::expand_uses`.
3241    let mut sc_names: HashSet<&str> = pedal.subcircuits.iter().map(|s| s.name.as_str()).collect();
3242    for u in &pedal.uses {
3243        sc_names.insert(u.id.as_str());
3244    }
3245    if sc_names.is_empty() {
3246        return;
3247    }
3248
3249    fn resolve_pin(pin: &Pin, sc_names: &std::collections::HashSet<&str>) -> Pin {
3250        match pin {
3251            Pin::ComponentPin {
3252                component,
3253                pin: port,
3254            } if sc_names.contains(component.as_str()) => Pin::SubcircuitPort {
3255                subcircuit: component.clone(),
3256                port: port.clone(),
3257            },
3258            Pin::Fork {
3259                switch,
3260                destinations,
3261            } => Pin::Fork {
3262                switch: switch.clone(),
3263                destinations: destinations
3264                    .iter()
3265                    .map(|d| resolve_pin(d, sc_names))
3266                    .collect(),
3267            },
3268            other => other.clone(),
3269        }
3270    }
3271
3272    for net in &mut pedal.nets {
3273        net.from = resolve_pin(&net.from, &sc_names);
3274        net.to = net.to.iter().map(|p| resolve_pin(p, &sc_names)).collect();
3275    }
3276}
3277
3278fn parse_subtitle(input: &str) -> IResult<&str, String> {
3279    let (input, _) = tag("subtitle")(input)?;
3280    let (input, _) = ws_comments(input)?;
3281    let (input, s) = quoted_string(input)?;
3282    let (input, _) = ws_comments(input)?;
3283    Ok((input, s.to_string()))
3284}
3285
3286/// Parse the `calibrate` keyword (opt-in output level auto-normalization).
3287fn parse_calibrate(input: &str) -> IResult<&str, ()> {
3288    let (input, _) = ws_comments(input)?;
3289    let (input, _) = tag("calibrate")(input)?;
3290    let (input, _) = ws_comments(input)?;
3291    Ok((input, ()))
3292}
3293
3294// ─────────────────────────────────────────────────────────────────────────────
3295// Init block parser
3296// ─────────────────────────────────────────────────────────────────────────────
3297
3298/// Valid named initial states for NL devices.
3299const VALID_INIT_STATES: &[&str] = &["saturated", "cutoff", "active", "forward", "reverse"];
3300
3301/// Parse one `DeviceLabel: state_name` line inside an `init { ... }` block.
3302fn parse_init_hint(input: &str) -> IResult<&str, InitHint> {
3303    let (input, _) = ws_comments(input)?;
3304    let (input, label) = identifier(input)?;
3305    let (input, _) = ws_comments(input)?;
3306    let (input, _) = char(':')(input)?;
3307    let (input, _) = ws_comments(input)?;
3308    let (input, state_str) = identifier(input)?;
3309    let (input, _) = ws_comments(input)?;
3310
3311    if !VALID_INIT_STATES.contains(&state_str) {
3312        return Err(nom::Err::Failure(nom::error::Error::new(
3313            input,
3314            nom::error::ErrorKind::Tag,
3315        )));
3316    }
3317
3318    Ok((
3319        input,
3320        InitHint {
3321            device_label: label.to_string(),
3322            state: InitState::Named(state_str.to_string()),
3323        },
3324    ))
3325}
3326
3327/// Parse the optional `init { ... }` block.
3328///
3329/// ```text
3330/// init {
3331///     Q1: saturated
3332///     Q2: cutoff
3333/// }
3334/// ```
3335fn init_section(input: &str) -> IResult<&str, Vec<InitHint>> {
3336    let (input, _) = ws_comments(input)?;
3337    let (input, _) = tag("init")(input)?;
3338    let (input, _) = ws_comments(input)?;
3339    let (input, _) = char('{')(input)?;
3340    let (input, hints) = many0(parse_init_hint)(input)?;
3341    let (input, _) = ws_comments(input)?;
3342    let (input, _) = char('}')(input)?;
3343    Ok((input, hints))
3344}
3345
3346/// Parse one explicit per-device operating-point entry inside an `op { }` block.
3347///
3348/// ```text
3349/// Q1: { vbe: 0.64, vce: 5.35 }
3350/// ```
3351///
3352/// Fields may appear in any order and are comma- or whitespace-separated. Both
3353/// `vbe` and `vce` are required (a BJT's two nonlinear ports). Each entry yields
3354/// an [`InitHint`] carrying [`InitState::Explicit`].
3355fn parse_op_entry(input: &str) -> IResult<&str, InitHint> {
3356    let (input, _) = ws_comments(input)?;
3357    let (input, label) = identifier(input)?;
3358    let (input, _) = ws_comments(input)?;
3359    let (input, _) = char(':')(input)?;
3360    let (input, _) = ws_comments(input)?;
3361    let (input, _) = char('{')(input)?;
3362
3363    let mut vbe: Option<f64> = None;
3364    let mut vce: Option<f64> = None;
3365    let mut rest = input;
3366    loop {
3367        let (r, _) = ws_comments(rest)?;
3368        // End of entry?
3369        if let Ok((r2, _)) = char::<&str, nom::error::Error<&str>>('}')(r) {
3370            rest = r2;
3371            break;
3372        }
3373        let (r, field) = identifier(r)?;
3374        let (r, _) = ws_comments(r)?;
3375        let (r, _) = char(':')(r)?;
3376        let (r, _) = ws_comments(r)?;
3377        let (r, val) = double(r)?;
3378        match field {
3379            "vbe" => vbe = Some(val),
3380            "vce" => vce = Some(val),
3381            _ => {
3382                return Err(nom::Err::Failure(nom::error::Error::new(
3383                    r,
3384                    nom::error::ErrorKind::Tag,
3385                )));
3386            }
3387        }
3388        // Optional separator (comma).
3389        let (r, _) = ws_comments(r)?;
3390        let (r, _) = opt(char(','))(r)?;
3391        rest = r;
3392    }
3393    let (input, _) = ws_comments(rest)?;
3394    // Optional trailing separator between entries.
3395    let (input, _) = opt(char(','))(input)?;
3396
3397    let (vbe, vce) = match (vbe, vce) {
3398        (Some(b), Some(c)) => (b, c),
3399        _ => {
3400            return Err(nom::Err::Failure(nom::error::Error::new(
3401                input,
3402                nom::error::ErrorKind::Tag,
3403            )));
3404        }
3405    };
3406
3407    Ok((
3408        input,
3409        InitHint {
3410            device_label: label.to_string(),
3411            state: InitState::Explicit { vbe, vce },
3412        },
3413    ))
3414}
3415
3416/// Parse the optional `op { ... }` block: explicit per-device operating-point
3417/// seeds (terminal voltages) used as a pure NR warm-start.
3418///
3419/// ```text
3420/// op {
3421///     Q1: { vbe: 0.64, vce: 5.35 }
3422///     Q2: { vbe: 0.54, vce: 18.8 }
3423/// }
3424/// ```
3425fn op_section(input: &str) -> IResult<&str, Vec<InitHint>> {
3426    let (input, _) = ws_comments(input)?;
3427    let (input, _) = tag("op")(input)?;
3428    let (input, _) = ws_comments(input)?;
3429    let (input, _) = char('{')(input)?;
3430    let (input, hints) = many0(parse_op_entry)(input)?;
3431    // Optional `nodes { name: voltage, ... }` sub-block: node-voltage seeds for
3432    // reactive (capacitor) ports. Parsed AFTER the per-device entries so it does
3433    // not collide with `Label: { vbe, vce }`.
3434    let (input, node_hints) = opt(op_nodes_block)(input)?;
3435    let (input, _) = ws_comments(input)?;
3436    let (input, _) = char('}')(input)?;
3437    let mut hints = hints;
3438    if let Some(mut nh) = node_hints {
3439        hints.append(&mut nh);
3440    }
3441    Ok((input, hints))
3442}
3443
3444/// Parse the optional `nodes { name: voltage, ... }` sub-block inside `op { }`.
3445///
3446/// ```text
3447/// op {
3448///     Q3: { vbe: 0.64, vce: 5.35 }
3449///     nodes { Q3.base: 1.0308, Q3.collector: 5.7426, in: 0.0 }
3450/// }
3451/// ```
3452///
3453/// Each entry becomes an [`InitHint`] with the node name in `device_label` and
3454/// an [`InitState::NodeVoltage`].
3455fn op_nodes_block(input: &str) -> IResult<&str, Vec<InitHint>> {
3456    let (input, _) = ws_comments(input)?;
3457    let (input, _) = tag("nodes")(input)?;
3458    let (input, _) = ws_comments(input)?;
3459    let (input, _) = char('{')(input)?;
3460    let mut hints = Vec::new();
3461    let mut rest = input;
3462    loop {
3463        let (r, _) = ws_comments(rest)?;
3464        if let Ok((r2, _)) = char::<&str, nom::error::Error<&str>>('}')(r) {
3465            rest = r2;
3466            break;
3467        }
3468        // Node names may contain a dot (e.g. `Q3.base`); accept identifier with
3469        // an optional `.pin` suffix.
3470        let (r, head) = identifier(r)?;
3471        let (r, tail) = opt(|i| {
3472            let (i, _) = char('.')(i)?;
3473            identifier(i)
3474        })(r)?;
3475        let name = match tail {
3476            Some(pin) => format!("{head}.{pin}"),
3477            None => head.to_string(),
3478        };
3479        let (r, _) = ws_comments(r)?;
3480        let (r, _) = char(':')(r)?;
3481        let (r, _) = ws_comments(r)?;
3482        let (r, val) = double(r)?;
3483        hints.push(InitHint {
3484            device_label: name,
3485            state: InitState::NodeVoltage { v: val },
3486        });
3487        let (r, _) = ws_comments(r)?;
3488        let (r, _) = opt(char(','))(r)?;
3489        rest = r;
3490    }
3491    Ok((rest, hints))
3492}
3493
3494/// Parse a complete `.pedal` or `.synth` file.
3495/// Both `pedal "Name" { ... }` and `synth "Name" { ... }` produce the same AST.
3496pub fn parse_pedal(input: &str) -> IResult<&str, PedalDef> {
3497    let (input, _) = ws_comments(input)?;
3498    // Accept both "pedal" and "synth" keywords — same AST, different semantics
3499    let (input, _) = alt((tag("pedal"), tag("synth"), tag("equipment")))(input)?;
3500    let (input, _) = ws_comments(input)?;
3501    let (input, name) = quoted_string(input)?;
3502    let (input, _) = ws_comments(input)?;
3503
3504    // Optional subtitle before the opening brace:
3505    //   pedal "GOLDENROD" subtitle "Klon Centaur — transparent overdrive" {
3506    let (input, subtitle) = opt(parse_subtitle)(input)?;
3507
3508    let (input, _) = char('{')(input)?;
3509
3510    // Try multi-supply block first: `supplies { V+: 15V, V-: -15V }`
3511    // Fall back to legacy single supply: `supply 9V`
3512    // If neither, supplies will be empty (compiler defaults to 9V "vcc")
3513    let (input, supplies) = if let Ok((rest, multi)) = supplies_section(input) {
3514        (rest, multi)
3515    } else if let Ok((rest, single)) = supply_section(input) {
3516        // Convert legacy single supply to named "vcc" rail
3517        (rest, vec![NamedSupply::with_config("vcc", single)])
3518    } else {
3519        (input, Vec::new())
3520    };
3521
3522    // Parse optional subcircuit blocks (zero or more, before components)
3523    let (input, subcircuits) = many0(subcircuit_block)(input)?;
3524
3525    // Parse optional ports section (before components — ports declare
3526    // named voltage nodes that can be referenced in nets)
3527    let (input, ports) = opt(ports_section)(input)?;
3528
3529    // When subcircuits are present, the top-level components/nets become
3530    // an optional routing layer. When absent, keep existing required behavior.
3531    let (input, (components, mirrors, uses)) = if subcircuits.is_empty() {
3532        components_section(input)?
3533    } else if let Ok((rest, result)) = components_section(input) {
3534        (rest, result)
3535    } else {
3536        (input, (Vec::new(), hashbrown::HashMap::new(), Vec::new()))
3537    };
3538    let (input, nets) = if subcircuits.is_empty() {
3539        nets_section(input)?
3540    } else if let Ok((rest, result)) = nets_section(input) {
3541        (rest, result)
3542    } else {
3543        (input, Vec::new())
3544    };
3545    let (input, controls) = opt(controls_section)(input)?;
3546    let (input, trims) = opt(trims_section)(input)?;
3547    let (input, monitors) = opt(monitors_section)(input)?;
3548    let (input, sidechains) = opt(sidechains_section)(input)?;
3549    let (input, calibrate) = opt(parse_calibrate)(input)?;
3550    let (input, init_hints) = opt(init_section)(input)?;
3551    // Optional `op { }` block: explicit per-device operating-point seeds. These
3552    // are merged into the same `init_hints` vector (carried as
3553    // `InitState::Explicit`), so they flow through the existing init-hint wiring.
3554    let (input, op_hints) = opt(op_section)(input)?;
3555
3556    let (input, _) = ws_comments(input)?;
3557    let (input, _) = char('}')(input)?;
3558    let (input, _) = ws_comments(input)?;
3559
3560    let mut pedal = PedalDef {
3561        name: name.to_string(),
3562        subtitle,
3563        supplies,
3564        components,
3565        nets,
3566        controls: controls.unwrap_or_default(),
3567        trims: trims.unwrap_or_default(),
3568        monitors: monitors.unwrap_or_default(),
3569        sidechains: sidechains.unwrap_or_default(),
3570        mirrors,
3571        calibrate: calibrate.is_some(),
3572        subcircuits,
3573        ports: ports.unwrap_or_default(),
3574        init_hints: {
3575            let mut h = init_hints.unwrap_or_default();
3576            h.extend(op_hints.unwrap_or_default());
3577            h
3578        },
3579        uses,
3580    };
3581    resolve_subcircuit_pins(&mut pedal);
3582    Ok((input, pedal))
3583}
3584
3585/// Convenience wrapper that returns `Result`.
3586pub fn parse_pedal_file(src: &str) -> Result<PedalDef, String> {
3587    match parse_pedal(src) {
3588        Ok(("", def)) => Ok(def),
3589        Ok((rest, _)) => Err(format!("Trailing input: {:?}", &rest[..rest.len().min(60)])),
3590        Err(e) => Err(format!("Parse error: {e}")),
3591    }
3592}
3593
3594// ---------------------------------------------------------------------------
3595// Tests
3596// ---------------------------------------------------------------------------
3597
3598#[cfg(test)]
3599mod tests {
3600    use super::*;
3601    use crate::compiler::component::Component;
3602    use crate::compiler::components::*;
3603
3604    #[test]
3605    fn parse_eng_value_k() {
3606        let (_, v) = eng_value("4.7k").unwrap();
3607        assert!((v - 4700.0).abs() < 1e-6);
3608    }
3609
3610    #[test]
3611    fn parse_eng_value_n() {
3612        let (_, v) = eng_value("220n").unwrap();
3613        assert!((v - 220e-9).abs() < 1e-18);
3614    }
3615
3616    #[test]
3617    fn parse_eng_value_m() {
3618        let (_, v) = eng_value("100m").unwrap();
3619        assert!((v - 0.1).abs() < 1e-10);
3620    }
3621
3622    #[test]
3623    #[allow(non_snake_case)]
3624    fn parse_eng_value_M() {
3625        let (_, v) = eng_value("1M").unwrap();
3626        assert!((v - 1e6).abs() < 1e-2);
3627    }
3628
3629    #[test]
3630    fn parse_eng_value_inf() {
3631        let (_, v) = eng_value("inf").unwrap();
3632        assert!(v.is_infinite() && v.is_sign_positive());
3633    }
3634
3635    #[test]
3636    fn parse_resistor_switched_with_inf() {
3637        let (_, (c, _)) = component_def("R_sel: resistor_switched(10k, inf, 47k)").unwrap();
3638        assert_eq!(c.id, "R_sel");
3639        let rs = c
3640            .kind
3641            .as_any()
3642            .downcast_ref::<ResistorSwitched>()
3643            .expect("expected ResistorSwitched");
3644        assert_eq!(rs.values.len(), 3);
3645        assert!((rs.values[0] - 10_000.0).abs() < 1e-6);
3646        assert!(rs.values[1].is_infinite());
3647        assert!((rs.values[2] - 47_000.0).abs() < 1e-6);
3648    }
3649
3650    #[test]
3651    fn parse_resistor_switched_inf_only() {
3652        // Open circuit position only
3653        let (_, (c, _)) = component_def("R_open: resistor_switched([inf])").unwrap();
3654        let rs = c
3655            .kind
3656            .as_any()
3657            .downcast_ref::<ResistorSwitched>()
3658            .expect("expected ResistorSwitched");
3659        assert_eq!(rs.values.len(), 1);
3660        assert!(rs.values[0].is_infinite());
3661    }
3662
3663    #[test]
3664    fn parse_component_resistor() {
3665        let (_, (c, _)) = component_def("R1: resistor(4.7k)").unwrap();
3666        assert_eq!(c.id, "R1");
3667        assert_eq!(
3668            c.kind.as_any().downcast_ref::<Resistor>().unwrap(),
3669            &Resistor { value: 4700.0 }
3670        );
3671    }
3672
3673    #[test]
3674    fn parse_component_cap() {
3675        let (_, (c, _)) = component_def("C1: cap(220n)").unwrap();
3676        assert_eq!(c.id, "C1");
3677        let cap = c
3678            .kind
3679            .as_any()
3680            .downcast_ref::<Capacitor>()
3681            .expect("expected Capacitor");
3682        assert!((cap.config.value - 220e-9).abs() < 1e-18);
3683        assert_eq!(cap.config.cap_type, CapType::Film); // default
3684        assert!(cap.config.leakage.is_none());
3685        assert!(cap.config.da.is_none());
3686    }
3687
3688    #[test]
3689    fn parse_component_cap_electrolytic() {
3690        let (_, (c, _)) = component_def("C1: cap(22u, electrolytic)").unwrap();
3691        let cap = c
3692            .kind
3693            .as_any()
3694            .downcast_ref::<Capacitor>()
3695            .expect("expected Capacitor");
3696        assert!((cap.config.value - 22e-6).abs() < 1e-15);
3697        assert_eq!(cap.config.cap_type, CapType::Electrolytic);
3698        assert!(cap.config.leakage.is_none());
3699        assert!(cap.config.da.is_none());
3700    }
3701
3702    #[test]
3703    fn parse_component_cap_with_leakage() {
3704        let (_, (c, _)) = component_def("C1: cap(22u, electrolytic, leakage: 100k)").unwrap();
3705        let cap = c
3706            .kind
3707            .as_any()
3708            .downcast_ref::<Capacitor>()
3709            .expect("expected Capacitor");
3710        assert!((cap.config.value - 22e-6).abs() < 1e-15);
3711        assert_eq!(cap.config.cap_type, CapType::Electrolytic);
3712        assert!((cap.config.leakage.unwrap() - 100_000.0).abs() < 1.0);
3713        assert!(cap.config.da.is_none());
3714    }
3715
3716    #[test]
3717    fn parse_component_cap_with_da() {
3718        let (_, (c, _)) =
3719            component_def("C1: cap(22u, electrolytic, leakage: 10k, da: 0.05)").unwrap();
3720        let cap = c
3721            .kind
3722            .as_any()
3723            .downcast_ref::<Capacitor>()
3724            .expect("expected Capacitor");
3725        assert!((cap.config.value - 22e-6).abs() < 1e-15);
3726        assert_eq!(cap.config.cap_type, CapType::Electrolytic);
3727        assert!((cap.config.leakage.unwrap() - 10_000.0).abs() < 1.0);
3728        assert!((cap.config.da.unwrap() - 0.05).abs() < 0.001);
3729    }
3730
3731    #[test]
3732    fn parse_component_diode_pair() {
3733        let (_, (c, _)) = component_def("D1: diode_pair(silicon)").unwrap();
3734        assert_eq!(
3735            c.kind.as_any().downcast_ref::<DiodePair>().unwrap(),
3736            &DiodePair {
3737                diode_type: DiodeType::Silicon
3738            }
3739        );
3740    }
3741
3742    #[test]
3743    fn parse_component_pot() {
3744        let (_, (c, _)) = component_def("Gain: pot(500k)").unwrap();
3745        assert_eq!(
3746            c.kind.as_any().downcast_ref::<Potentiometer>().unwrap(),
3747            &Potentiometer {
3748                max_r: 500_000.0,
3749                taper: PotTaper::B
3750            }
3751        );
3752    }
3753
3754    #[test]
3755    fn parse_component_pot_with_taper() {
3756        // Audio taper
3757        let (_, (c, _)) = component_def("Vol: pot(100k, a)").unwrap();
3758        assert_eq!(
3759            c.kind.as_any().downcast_ref::<Potentiometer>().unwrap(),
3760            &Potentiometer {
3761                max_r: 100_000.0,
3762                taper: PotTaper::A
3763            }
3764        );
3765
3766        // Linear taper
3767        let (_, (c, _)) = component_def("Tone: pot(10k, b)").unwrap();
3768        assert_eq!(
3769            c.kind.as_any().downcast_ref::<Potentiometer>().unwrap(),
3770            &Potentiometer {
3771                max_r: 10_000.0,
3772                taper: PotTaper::B
3773            }
3774        );
3775
3776        // Reverse log taper
3777        let (_, (c, _)) = component_def("Mix: pot(50k, c)").unwrap();
3778        assert_eq!(
3779            c.kind.as_any().downcast_ref::<Potentiometer>().unwrap(),
3780            &Potentiometer {
3781                max_r: 50_000.0,
3782                taper: PotTaper::C
3783            }
3784        );
3785
3786        // Legacy names
3787        let (_, (c, _)) = component_def("P1: pot(1M, log)").unwrap();
3788        assert_eq!(
3789            c.kind.as_any().downcast_ref::<Potentiometer>().unwrap(),
3790            &Potentiometer {
3791                max_r: 1_000_000.0,
3792                taper: PotTaper::A
3793            }
3794        );
3795
3796        let (_, (c, _)) = component_def("P2: pot(25k, linear)").unwrap();
3797        assert_eq!(
3798            c.kind.as_any().downcast_ref::<Potentiometer>().unwrap(),
3799            &Potentiometer {
3800                max_r: 25_000.0,
3801                taper: PotTaper::B
3802            }
3803        );
3804    }
3805
3806    #[test]
3807    fn parse_component_pot_mirrors() {
3808        let (_, (c, m)) = component_def("Gain_B: pot(100k, b) mirrors Gain_A").unwrap();
3809        assert_eq!(c.id, "Gain_B");
3810        assert_eq!(
3811            c.kind.as_any().downcast_ref::<Potentiometer>().unwrap(),
3812            &Potentiometer {
3813                max_r: 100_000.0,
3814                taper: PotTaper::B
3815            }
3816        );
3817        assert_eq!(m, Some("Gain_A".to_string()));
3818    }
3819
3820    #[test]
3821    fn parse_component_pot_no_mirrors() {
3822        let (_, (c, m)) = component_def("Gain_A: pot(100k, b)").unwrap();
3823        assert_eq!(c.id, "Gain_A");
3824        assert_eq!(m, None);
3825    }
3826
3827    #[test]
3828    fn parse_mirrors_in_full_pedal() {
3829        let src = r#"
3830            pedal "Dual Gang Test" {
3831                components {
3832                    Gain_A: pot(100k, b)
3833                    Gain_B: pot(100k, b) mirrors Gain_A
3834                    R1: resistor(10k)
3835                }
3836                nets {
3837                    in -> R1.a
3838                    R1.b -> Gain_A.a
3839                    Gain_A.b -> Gain_B.a
3840                    Gain_B.b -> out
3841                }
3842            }
3843        "#;
3844        let pedal = parse_pedal_file(src).unwrap();
3845        assert_eq!(pedal.mirrors.len(), 1);
3846        assert_eq!(pedal.mirrors.get("Gain_B").unwrap(), "Gain_A");
3847    }
3848
3849    #[test]
3850    fn parse_calibrate_keyword() {
3851        let src = r#"
3852            pedal "Test" {
3853                components {
3854                    R1: resistor(10k)
3855                }
3856                nets {
3857                    in -> R1.a
3858                    R1.b -> out
3859                }
3860                calibrate
3861            }
3862        "#;
3863        let pedal = parse_pedal_file(src).unwrap();
3864        assert!(pedal.calibrate);
3865    }
3866
3867    #[test]
3868    fn parse_no_calibrate_keyword() {
3869        let src = r#"
3870            pedal "Test" {
3871                components {
3872                    R1: resistor(10k)
3873                }
3874                nets {
3875                    in -> R1.a
3876                    R1.b -> out
3877                }
3878            }
3879        "#;
3880        let pedal = parse_pedal_file(src).unwrap();
3881        assert!(!pedal.calibrate);
3882    }
3883
3884    #[test]
3885    fn pot_taper_curves() {
3886        // Linear (B): direct mapping
3887        assert!((PotTaper::B.apply(0.0) - 0.0).abs() < 1e-9);
3888        assert!((PotTaper::B.apply(0.5) - 0.5).abs() < 1e-9);
3889        assert!((PotTaper::B.apply(1.0) - 1.0).abs() < 1e-9);
3890
3891        // Audio (A): slow start, fast end
3892        // Using (10^x - 1)/9 formula: at 50% gives ~24% resistance
3893        let a_mid = PotTaper::A.apply(0.5);
3894        assert!(a_mid > 0.2 && a_mid < 0.3, "A taper at 50% should be ~24%");
3895        assert!((PotTaper::A.apply(0.0) - 0.0).abs() < 1e-9);
3896        assert!((PotTaper::A.apply(1.0) - 1.0).abs() < 1e-9);
3897
3898        // Reverse log (C): fast start, slow end (inverse of A)
3899        let c_mid = PotTaper::C.apply(0.5);
3900        assert!(c_mid > 0.7 && c_mid < 0.8, "C taper at 50% should be ~76%");
3901        assert!((PotTaper::C.apply(0.0) - 0.0).abs() < 1e-9);
3902        assert!((PotTaper::C.apply(1.0) - 1.0).abs() < 1e-9);
3903
3904        // A and C should be symmetric around 0.5
3905        let a_25 = PotTaper::A.apply(0.25);
3906        let c_75 = PotTaper::C.apply(0.75);
3907        assert!(
3908            (a_25 - (1.0 - c_75)).abs() < 0.01,
3909            "A and C should be symmetric"
3910        );
3911    }
3912
3913    #[test]
3914    fn parse_net_simple() {
3915        let (_, n) = net_def("in -> C1.a").unwrap();
3916        assert_eq!(n.from, Pin::Reserved("in".to_string()));
3917        assert_eq!(
3918            n.to,
3919            vec![Pin::ComponentPin {
3920                component: "C1".to_string(),
3921                pin: "a".to_string()
3922            }]
3923        );
3924    }
3925
3926    #[test]
3927    fn parse_net_multi() {
3928        let (_, n) = net_def("C1.b -> R1.a, D1.a").unwrap();
3929        assert_eq!(n.to.len(), 2);
3930    }
3931
3932    #[test]
3933    fn parse_net_fork_simple() {
3934        let (_, n) = net_def("B7.a -> fork(SW_time, [B8.a, B9.a])").unwrap();
3935        assert_eq!(
3936            n.from,
3937            Pin::ComponentPin {
3938                component: "B7".to_string(),
3939                pin: "a".to_string()
3940            }
3941        );
3942        assert_eq!(n.to.len(), 1);
3943        match &n.to[0] {
3944            Pin::Fork {
3945                switch,
3946                destinations,
3947            } => {
3948                assert_eq!(switch, "SW_time");
3949                assert_eq!(destinations.len(), 2);
3950                assert_eq!(
3951                    destinations[0],
3952                    Pin::ComponentPin {
3953                        component: "B8".to_string(),
3954                        pin: "a".to_string()
3955                    }
3956                );
3957                assert_eq!(
3958                    destinations[1],
3959                    Pin::ComponentPin {
3960                        component: "B9".to_string(),
3961                        pin: "a".to_string()
3962                    }
3963                );
3964            }
3965            _ => panic!("expected Fork"),
3966        }
3967    }
3968
3969    #[test]
3970    fn parse_net_fork_three_destinations() {
3971        let (_, n) = net_def("in -> fork(MODE, [clean.a, crunch.a, lead.a])").unwrap();
3972        assert_eq!(n.from, Pin::Reserved("in".to_string()));
3973        match &n.to[0] {
3974            Pin::Fork {
3975                switch,
3976                destinations,
3977            } => {
3978                assert_eq!(switch, "MODE");
3979                assert_eq!(destinations.len(), 3);
3980            }
3981            _ => panic!("expected Fork"),
3982        }
3983    }
3984
3985    #[test]
3986    fn parse_net_fork_with_reserved_dest() {
3987        // Fork can route to reserved nodes like gnd (for muting)
3988        let (_, n) = net_def("sig.out -> fork(MUTE, [out, gnd])").unwrap();
3989        match &n.to[0] {
3990            Pin::Fork {
3991                switch,
3992                destinations,
3993            } => {
3994                assert_eq!(switch, "MUTE");
3995                assert_eq!(destinations[0], Pin::Reserved("out".to_string()));
3996                assert_eq!(destinations[1], Pin::Reserved("gnd".to_string()));
3997            }
3998            _ => panic!("expected Fork"),
3999        }
4000    }
4001
4002    #[test]
4003    fn parse_control() {
4004        let (_, c) = control_def(r#"Gain.position -> "Drive" [0.0, 1.0] = 0.5"#).unwrap();
4005        assert_eq!(c.component, "Gain");
4006        assert_eq!(c.label, "Drive");
4007        assert!((c.default - 0.5).abs() < 1e-10);
4008    }
4009
4010    #[test]
4011    fn parse_full_pedal() {
4012        let src = r#"
4013pedal "Tube Screamer" {
4014  components {
4015    R1: resistor(4.7k)
4016    C1: cap(220n)
4017    D1: diode_pair(silicon)
4018    Gain: pot(500k)
4019  }
4020  nets {
4021    in -> C1.a
4022    C1.b -> R1.a, D1.a
4023    D1.b -> gnd
4024  }
4025  controls {
4026    Gain.position -> "Drive" [0.0, 1.0] = 0.5
4027  }
4028}
4029"#;
4030        let def = parse_pedal_file(src).unwrap();
4031        assert_eq!(def.name, "Tube Screamer");
4032        assert_eq!(def.components.len(), 4);
4033        assert_eq!(def.nets.len(), 3);
4034        assert_eq!(def.controls.len(), 1);
4035    }
4036
4037    #[test]
4038    fn parse_with_comments() {
4039        let src = r#"
4040# This is a fuzz pedal
4041pedal "Fuzz" {
4042  components {
4043    # Input cap
4044    C1: cap(100n)
4045    Q1: npn()
4046  }
4047  nets {
4048    in -> C1.a
4049    C1.b -> Q1.base
4050  }
4051}
4052"#;
4053        let def = parse_pedal_file(src).unwrap();
4054        assert_eq!(def.name, "Fuzz");
4055        assert_eq!(def.components.len(), 2);
4056    }
4057
4058    #[test]
4059    fn parse_all_component_kinds() {
4060        let src = r#"
4061pedal "All" {
4062  components {
4063    R1: resistor(10k)
4064    C1: cap(100n)
4065    L1: inductor(100m)
4066    D1: diode_pair(germanium)
4067    D2: diode(led)
4068    P1: pot(1M)
4069    Q1: npn()
4070    Q2: pnp()
4071    U1: opamp()
4072  }
4073  nets {
4074    in -> out
4075  }
4076}
4077"#;
4078        let def = parse_pedal_file(src).unwrap();
4079        assert_eq!(def.components.len(), 9);
4080    }
4081
4082    #[test]
4083    fn parse_njfet_j201() {
4084        let (_, (c, _)) = component_def("J1: njfet(j201)").unwrap();
4085        assert_eq!(c.id, "J1");
4086        assert_eq!(
4087            c.kind.as_any().downcast_ref::<NJfet>().unwrap(),
4088            &NJfet {
4089                model: "J201".to_string()
4090            }
4091        );
4092    }
4093
4094    #[test]
4095    fn parse_njfet_2n5457() {
4096        let (_, (c, _)) = component_def("J2: njfet(2n5457)").unwrap();
4097        assert_eq!(
4098            c.kind.as_any().downcast_ref::<NJfet>().unwrap(),
4099            &NJfet {
4100                model: "2N5457".to_string()
4101            }
4102        );
4103    }
4104
4105    #[test]
4106    fn parse_njfet_2n5952() {
4107        let (_, (c, _)) = component_def("J1: njfet(2n5952)").unwrap();
4108        assert_eq!(
4109            c.kind.as_any().downcast_ref::<NJfet>().unwrap(),
4110            &NJfet {
4111                model: "2N5952".to_string()
4112            }
4113        );
4114    }
4115
4116    #[test]
4117    fn parse_pjfet_2n5460() {
4118        let (_, (c, _)) = component_def("J3: pjfet(2n5460)").unwrap();
4119        assert_eq!(
4120            c.kind.as_any().downcast_ref::<PJfet>().unwrap(),
4121            &PJfet {
4122                model: "2N5460".to_string()
4123            }
4124        );
4125    }
4126
4127    #[test]
4128    fn parse_njfet_any_model() {
4129        // Any model name from the .model file should be accepted
4130        let (_, (c, _)) = component_def("J1: njfet(2N3819-VSH)").unwrap();
4131        assert_eq!(
4132            c.kind.as_any().downcast_ref::<NJfet>().unwrap(),
4133            &NJfet {
4134                model: "2N3819-VSH".to_string()
4135            }
4136        );
4137    }
4138
4139    #[test]
4140    fn parse_pedal_with_jfet() {
4141        let src = r#"
4142pedal "Tremolo" {
4143  components {
4144    C1: cap(100n)
4145    J1: njfet(j201)
4146    R1: resistor(10k)
4147  }
4148  nets {
4149    in -> C1.a
4150    C1.b -> J1.gate
4151    J1.drain -> R1.a
4152    J1.source -> gnd
4153  }
4154}
4155"#;
4156        let def = parse_pedal_file(src).unwrap();
4157        assert_eq!(def.name, "Tremolo");
4158        assert_eq!(def.components.len(), 3);
4159        assert_eq!(
4160            def.components[1]
4161                .kind
4162                .as_any()
4163                .downcast_ref::<NJfet>()
4164                .unwrap(),
4165            &NJfet {
4166                model: "J201".to_string()
4167            }
4168        );
4169    }
4170
4171    #[test]
4172    fn parse_photocoupler_vtl5c3() {
4173        let (_, (c, _)) = component_def("OC1: photocoupler(vtl5c3)").unwrap();
4174        assert_eq!(c.id, "OC1");
4175        assert_eq!(
4176            c.kind.as_any().downcast_ref::<PhotocouplerComp>().unwrap(),
4177            &PhotocouplerComp {
4178                coupler_type: PhotocouplerType::Vtl5c3
4179            }
4180        );
4181    }
4182
4183    #[test]
4184    fn parse_photocoupler_vtl5c1() {
4185        let (_, (c, _)) = component_def("OC2: photocoupler(vtl5c1)").unwrap();
4186        assert_eq!(
4187            c.kind.as_any().downcast_ref::<PhotocouplerComp>().unwrap(),
4188            &PhotocouplerComp {
4189                coupler_type: PhotocouplerType::Vtl5c1
4190            }
4191        );
4192    }
4193
4194    #[test]
4195    fn parse_photocoupler_nsl32() {
4196        let (_, (c, _)) = component_def("OC3: photocoupler(nsl32)").unwrap();
4197        assert_eq!(
4198            c.kind.as_any().downcast_ref::<PhotocouplerComp>().unwrap(),
4199            &PhotocouplerComp {
4200                coupler_type: PhotocouplerType::Nsl32
4201            }
4202        );
4203    }
4204
4205    #[test]
4206    fn parse_pedal_with_photocoupler() {
4207        let src = r#"
4208pedal "Optical Tremolo" {
4209  components {
4210    C1: cap(100n)
4211    OC1: photocoupler(vtl5c3)
4212    R1: resistor(10k)
4213  }
4214  nets {
4215    in -> C1.a
4216    C1.b -> OC1.a
4217    OC1.b -> R1.a
4218    R1.b -> out
4219  }
4220}
4221"#;
4222        let def = parse_pedal_file(src).unwrap();
4223        assert_eq!(def.name, "Optical Tremolo");
4224        assert_eq!(def.components.len(), 3);
4225        assert_eq!(
4226            def.components[1]
4227                .kind
4228                .as_any()
4229                .downcast_ref::<PhotocouplerComp>()
4230                .unwrap(),
4231            &PhotocouplerComp {
4232                coupler_type: PhotocouplerType::Vtl5c3
4233            }
4234        );
4235    }
4236
4237    #[test]
4238    fn parse_triode_12ax7() {
4239        let (_, (c, _)) = component_def("V1: triode(12ax7)").unwrap();
4240        assert_eq!(c.id, "V1");
4241        assert_eq!(
4242            c.kind.as_any().downcast_ref::<Triode>().unwrap(),
4243            &Triode {
4244                model: "12AX7".into()
4245            }
4246        );
4247    }
4248
4249    #[test]
4250    fn parse_triode_12at7() {
4251        let (_, (c, _)) = component_def("V2: triode(12at7)").unwrap();
4252        assert_eq!(
4253            c.kind.as_any().downcast_ref::<Triode>().unwrap(),
4254            &Triode {
4255                model: "12AT7".into()
4256            }
4257        );
4258    }
4259
4260    #[test]
4261    fn parse_triode_12au7() {
4262        let (_, (c, _)) = component_def("V3: triode(12au7)").unwrap();
4263        assert_eq!(
4264            c.kind.as_any().downcast_ref::<Triode>().unwrap(),
4265            &Triode {
4266                model: "12AU7".into()
4267            }
4268        );
4269    }
4270
4271    #[test]
4272    fn parse_triode_ecc83() {
4273        // ECC83 = European 12AX7 — stored as "ECC83", resolved at model lookup
4274        let (_, (c, _)) = component_def("V4: triode(ecc83)").unwrap();
4275        assert_eq!(
4276            c.kind.as_any().downcast_ref::<Triode>().unwrap(),
4277            &Triode {
4278                model: "ECC83".into()
4279            }
4280        );
4281    }
4282
4283    #[test]
4284    fn parse_triode_12ay7() {
4285        let (_, (c, _)) = component_def("V1: triode(12ay7)").unwrap();
4286        assert_eq!(c.id, "V1");
4287        assert_eq!(
4288            c.kind.as_any().downcast_ref::<Triode>().unwrap(),
4289            &Triode {
4290                model: "12AY7".into()
4291            }
4292        );
4293    }
4294
4295    #[test]
4296    fn parse_triode_6072() {
4297        // 6072 = military designation for 12AY7 — stored as "6072", resolved at model lookup
4298        let (_, (c, _)) = component_def("V1: triode(6072)").unwrap();
4299        assert_eq!(
4300            c.kind.as_any().downcast_ref::<Triode>().unwrap(),
4301            &Triode {
4302                model: "6072".into()
4303            }
4304        );
4305    }
4306
4307    #[test]
4308    fn parse_vari_mu_6386() {
4309        let (_, (c, _)) = component_def("V1: vari_mu(6386)").unwrap();
4310        assert_eq!(c.id, "V1");
4311        assert_eq!(
4312            c.kind.as_any().downcast_ref::<VariMu>().unwrap(),
4313            &VariMu {
4314                model: "6386".into()
4315            }
4316        );
4317    }
4318
4319    #[test]
4320    fn parse_lfo_component() {
4321        // lfo(triangle, 100k, 220n) -> f = 1/(2π*100000*220e-9) ≈ 7.2 Hz
4322        let (_, (c, _)) = component_def("LFO1: lfo(triangle, 100k, 220n)").unwrap();
4323        assert_eq!(c.id, "LFO1");
4324        let lfo = c.kind.as_any().downcast_ref::<Lfo>().expect("expected Lfo");
4325        assert_eq!(lfo.waveform, LfoWaveformDsl::Triangle);
4326        assert!((lfo.timing_r - 100_000.0).abs() < 1.0);
4327        assert!((lfo.timing_c - 220e-9).abs() < 1e-12);
4328    }
4329
4330    #[test]
4331    fn parse_lfo_waveforms() {
4332        assert_eq!(lfo_waveform("sine").unwrap().1, LfoWaveformDsl::Sine);
4333        assert_eq!(
4334            lfo_waveform("triangle").unwrap().1,
4335            LfoWaveformDsl::Triangle
4336        );
4337        assert_eq!(lfo_waveform("square").unwrap().1, LfoWaveformDsl::Square);
4338        assert_eq!(lfo_waveform("saw_up").unwrap().1, LfoWaveformDsl::SawUp);
4339        assert_eq!(lfo_waveform("saw_down").unwrap().1, LfoWaveformDsl::SawDown);
4340        assert_eq!(
4341            lfo_waveform("sample_hold").unwrap().1,
4342            LfoWaveformDsl::SampleAndHold
4343        );
4344    }
4345
4346    #[test]
4347    fn parse_pedal_with_lfo_component() {
4348        let src = r#"
4349pedal "Harmonic Tremolo" {
4350  components {
4351    LFO1: lfo(triangle, 100k, 220n)
4352    C1: cap(100n)
4353    J1: njfet(j201)
4354    R1: resistor(10k)
4355  }
4356  nets {
4357    in -> C1.a
4358    C1.b -> J1.drain
4359    J1.source -> gnd
4360    LFO1.out -> J1.vgs
4361  }
4362}
4363"#;
4364        let def = parse_pedal_file(src).unwrap();
4365        assert_eq!(def.name, "Harmonic Tremolo");
4366        assert_eq!(def.components.len(), 4);
4367        // Check LFO component: 100k timing resistor, 220n timing cap
4368        let lfo = def.components[0]
4369            .kind
4370            .as_any()
4371            .downcast_ref::<Lfo>()
4372            .expect("expected Lfo component");
4373        assert_eq!(lfo.waveform, LfoWaveformDsl::Triangle);
4374        assert!((lfo.timing_r - 100_000.0).abs() < 1.0);
4375        assert!((lfo.timing_c - 220e-9).abs() < 1e-12);
4376        // Check LFO.out net connection
4377        let lfo_net = def.nets.iter().find(|n| {
4378            matches!(&n.from, Pin::ComponentPin { component, pin }
4379                     if component == "LFO1" && pin == "out")
4380        });
4381        assert!(lfo_net.is_some(), "should have LFO1.out net");
4382    }
4383
4384    #[test]
4385    fn parse_envelope_follower_component() {
4386        // envelope_follower(1k, 4.7u, 100k, 1u, 20k)
4387        let (_, (c, _)) = component_def("EF1: envelope_follower(1k, 4.7u, 100k, 1u, 20k)").unwrap();
4388        assert_eq!(c.id, "EF1");
4389        let ef = c
4390            .kind
4391            .as_any()
4392            .downcast_ref::<EnvelopeFollower>()
4393            .expect("expected EnvelopeFollower");
4394        assert!((ef.attack_r - 1_000.0).abs() < 1.0);
4395        assert!((ef.attack_c - 4.7e-6).abs() < 1e-9);
4396        assert!((ef.release_r - 100_000.0).abs() < 1.0);
4397        assert!((ef.release_c - 1e-6).abs() < 1e-9);
4398        assert!((ef.sensitivity_r - 20_000.0).abs() < 1.0);
4399    }
4400
4401    #[test]
4402    fn parse_pedal_with_envelope_follower() {
4403        let src = r#"
4404pedal "Auto Wah" {
4405  components {
4406    EF1: envelope_follower(1k, 4.7u, 100k, 1.5u, 20k)
4407    C1: cap(100n)
4408    J1: njfet(j201)
4409    R1: resistor(10k)
4410  }
4411  nets {
4412    in -> C1.a
4413    C1.b -> J1.drain
4414    J1.source -> gnd
4415    EF1.out -> J1.vgs
4416    R1.a -> J1.drain
4417    R1.b -> out
4418  }
4419}
4420"#;
4421        let def = parse_pedal_file(src).unwrap();
4422        assert_eq!(def.name, "Auto Wah");
4423        assert_eq!(def.components.len(), 4);
4424        // Check EnvelopeFollower component
4425        let ef = def.components[0]
4426            .kind
4427            .as_any()
4428            .downcast_ref::<EnvelopeFollower>()
4429            .expect("expected EnvelopeFollower component");
4430        assert!((ef.attack_r - 1_000.0).abs() < 1.0);
4431        assert!((ef.attack_c - 4.7e-6).abs() < 1e-9);
4432        assert!((ef.release_r - 100_000.0).abs() < 1.0);
4433        assert!((ef.release_c - 1.5e-6).abs() < 1e-9);
4434        assert!((ef.sensitivity_r - 20_000.0).abs() < 1.0);
4435        // Check EF1.out net connection
4436        let ef_net = def.nets.iter().find(|n| {
4437            matches!(&n.from, Pin::ComponentPin { component, pin }
4438                     if component == "EF1" && pin == "out")
4439        });
4440        assert!(ef_net.is_some(), "should have EF1.out net");
4441    }
4442
4443    #[test]
4444    fn parse_opamp_generic() {
4445        let (_, (c, _)) = component_def("U1: opamp()").unwrap();
4446        assert_eq!(c.id, "U1");
4447        assert_eq!(c.kind.op_amp_type(), Some(OpAmpType::Generic));
4448    }
4449
4450    #[test]
4451    fn parse_opamp_tl071() {
4452        let (_, (c, _)) = component_def("U1: opamp(tl071)").unwrap();
4453        assert_eq!(c.kind.op_amp_type(), Some(OpAmpType::Tl071));
4454        // Electrically identical to the TL072 (single vs dual package).
4455        assert!((OpAmpType::Tl071.slew_rate() - 13.0).abs() < 0.1);
4456        assert!((OpAmpType::Tl071.gain_bandwidth() - 3e6).abs() < 1e3);
4457    }
4458
4459    #[test]
4460    fn parse_opamp_tl072() {
4461        let (_, (c, _)) = component_def("U1: opamp(tl072)").unwrap();
4462        assert_eq!(c.kind.op_amp_type(), Some(OpAmpType::Tl072));
4463    }
4464
4465    #[test]
4466    fn parse_opamp_jrc4558() {
4467        let (_, (c, _)) = component_def("U1: opamp(jrc4558)").unwrap();
4468        assert_eq!(c.kind.op_amp_type(), Some(OpAmpType::Jrc4558));
4469    }
4470
4471    #[test]
4472    fn parse_opamp_4558_alias() {
4473        // "4558" should also work as an alias for JRC4558
4474        let (_, (c, _)) = component_def("U1: opamp(4558)").unwrap();
4475        assert_eq!(c.kind.op_amp_type(), Some(OpAmpType::Jrc4558));
4476    }
4477
4478    #[test]
4479    fn parse_opamp_lm308() {
4480        let (_, (c, _)) = component_def("U1: opamp(lm308)").unwrap();
4481        assert_eq!(c.kind.op_amp_type(), Some(OpAmpType::Lm308));
4482    }
4483
4484    #[test]
4485    fn parse_opamp_ne5532() {
4486        let (_, (c, _)) = component_def("U1: opamp(ne5532)").unwrap();
4487        assert_eq!(c.kind.op_amp_type(), Some(OpAmpType::Ne5532));
4488    }
4489
4490    #[test]
4491    fn parse_opamp_njm5532_alias() {
4492        // "njm5532" and bare "5532" are aliases for NE5532.
4493        let (_, (c, _)) = component_def("U1: opamp(njm5532)").unwrap();
4494        assert_eq!(c.kind.op_amp_type(), Some(OpAmpType::Ne5532));
4495        let (_, (c2, _)) = component_def("U2: opamp(5532)").unwrap();
4496        assert_eq!(c2.kind.op_amp_type(), Some(OpAmpType::Ne5532));
4497    }
4498
4499    #[test]
4500    fn parse_opamp_ca3080() {
4501        let (_, (c, _)) = component_def("U1: opamp(ca3080)").unwrap();
4502        assert_eq!(c.kind.op_amp_type(), Some(OpAmpType::Ca3080));
4503        // CA3080 is an OTA
4504        assert!(OpAmpType::Ca3080.is_ota());
4505    }
4506
4507    #[test]
4508    fn opamp_slew_rates() {
4509        // Verify slew rates are as specified
4510        assert!((OpAmpType::Lm308.slew_rate() - 0.3).abs() < 0.01);
4511        assert!((OpAmpType::Tl072.slew_rate() - 13.0).abs() < 0.1);
4512        assert!((OpAmpType::Jrc4558.slew_rate() - 1.7).abs() < 0.1);
4513    }
4514
4515    #[test]
4516    fn opamp_gain_bandwidth() {
4517        // Verify GBW products
4518        assert!((OpAmpType::Lm308.gain_bandwidth() - 1e6).abs() < 1e3);
4519        assert!((OpAmpType::Ne5532.gain_bandwidth() - 10e6).abs() < 1e3);
4520    }
4521
4522    // ── MOSFET parser tests ──────────────────────────────────────────────
4523
4524    #[test]
4525    fn parse_nmos_2n7000() {
4526        let (_, (c, _)) = component_def("M1: nmos(2n7000)").unwrap();
4527        assert_eq!(c.id, "M1");
4528        assert_eq!(
4529            c.kind.as_any().downcast_ref::<Nmos>().unwrap(),
4530            &Nmos {
4531                mosfet_type: MosfetType::N2n7000
4532            }
4533        );
4534    }
4535
4536    #[test]
4537    fn parse_nmos_irf520() {
4538        let (_, (c, _)) = component_def("M1: nmos(irf520)").unwrap();
4539        assert_eq!(
4540            c.kind.as_any().downcast_ref::<Nmos>().unwrap(),
4541            &Nmos {
4542                mosfet_type: MosfetType::Irf520
4543            }
4544        );
4545    }
4546
4547    #[test]
4548    fn parse_pmos_bs250() {
4549        let (_, (c, _)) = component_def("M2: pmos(bs250)").unwrap();
4550        assert_eq!(c.id, "M2");
4551        assert_eq!(
4552            c.kind.as_any().downcast_ref::<Pmos>().unwrap(),
4553            &Pmos {
4554                mosfet_type: MosfetType::Bs250
4555            }
4556        );
4557    }
4558
4559    #[test]
4560    fn parse_pmos_irf9520() {
4561        let (_, (c, _)) = component_def("M2: pmos(irf9520)").unwrap();
4562        assert_eq!(
4563            c.kind.as_any().downcast_ref::<Pmos>().unwrap(),
4564            &Pmos {
4565                mosfet_type: MosfetType::Irf9520
4566            }
4567        );
4568    }
4569
4570    // ── Zener diode parser tests ────────────────────────────────────────
4571
4572    #[test]
4573    fn parse_zener_with_voltage_suffix() {
4574        let (_, (c, _)) = component_def("Z1: zener(5.1v)").unwrap();
4575        assert_eq!(c.id, "Z1");
4576        let z = c
4577            .kind
4578            .as_any()
4579            .downcast_ref::<Zener>()
4580            .expect("expected Zener");
4581        assert!((z.breakdown_voltage - 5.1).abs() < 1e-6);
4582    }
4583
4584    #[test]
4585    fn parse_zener_without_suffix() {
4586        let (_, (c, _)) = component_def("Z2: zener(3.3)").unwrap();
4587        let z = c
4588            .kind
4589            .as_any()
4590            .downcast_ref::<Zener>()
4591            .expect("expected Zener");
4592        assert!((z.breakdown_voltage - 3.3).abs() < 1e-6);
4593    }
4594
4595    #[test]
4596    fn parse_pedal_with_mosfet() {
4597        let src = r#"
4598pedal "MOSFET Drive" {
4599  components {
4600    C1: cap(100n)
4601    R1: resistor(10k)
4602    M1: nmos(2n7000)
4603    R2: resistor(1k)
4604  }
4605  nets {
4606    in -> C1.a
4607    C1.b -> R1.a, M1.drain
4608    R1.b -> gnd
4609    M1.source -> R2.a
4610    R2.b -> gnd
4611    M1.drain -> out
4612  }
4613}
4614"#;
4615        let def = parse_pedal_file(src).unwrap();
4616        assert_eq!(def.name, "MOSFET Drive");
4617        assert_eq!(def.components.len(), 4);
4618        assert!(def.components.iter().any(|c| c
4619            .kind
4620            .as_any()
4621            .downcast_ref::<Nmos>()
4622            .map_or(false, |m| m.mosfet_type == MosfetType::N2n7000)));
4623    }
4624
4625    #[test]
4626    fn parse_pedal_with_zener() {
4627        let src = r#"
4628pedal "Zener Clipper" {
4629  components {
4630    C1: cap(100n)
4631    R1: resistor(10k)
4632    Z1: zener(5.1v)
4633  }
4634  nets {
4635    in -> C1.a
4636    C1.b -> R1.a, Z1.a
4637    R1.b -> gnd
4638    Z1.b -> gnd
4639  }
4640}
4641"#;
4642        let def = parse_pedal_file(src).unwrap();
4643        assert_eq!(def.name, "Zener Clipper");
4644        assert_eq!(def.components.len(), 3);
4645        assert!(def.components.iter().any(|c| c
4646            .kind
4647            .as_any()
4648            .downcast_ref::<Zener>()
4649            .map_or(false, |z| (z.breakdown_voltage - 5.1).abs() < 1e-6)));
4650    }
4651
4652    // ── BBD parser tests ──────────────────────────────────────────────────
4653
4654    #[test]
4655    fn parse_bbd_mn3207() {
4656        let (_, (c, _)) = component_def("BBD1: bbd(mn3207)").unwrap();
4657        assert_eq!(c.id, "BBD1");
4658        assert_eq!(
4659            c.kind.as_any().downcast_ref::<Bbd>().unwrap(),
4660            &Bbd {
4661                bbd_type: BbdType::Mn3207
4662            }
4663        );
4664    }
4665
4666    #[test]
4667    fn parse_bbd_mn3007() {
4668        let (_, (c, _)) = component_def("BBD1: bbd(mn3007)").unwrap();
4669        assert_eq!(
4670            c.kind.as_any().downcast_ref::<Bbd>().unwrap(),
4671            &Bbd {
4672                bbd_type: BbdType::Mn3007
4673            }
4674        );
4675    }
4676
4677    #[test]
4678    fn parse_bbd_mn3005() {
4679        let (_, (c, _)) = component_def("BBD1: bbd(mn3005)").unwrap();
4680        assert_eq!(
4681            c.kind.as_any().downcast_ref::<Bbd>().unwrap(),
4682            &Bbd {
4683                bbd_type: BbdType::Mn3005
4684            }
4685        );
4686    }
4687
4688    #[test]
4689    fn parse_pedal_with_bbd() {
4690        let src = r#"
4691pedal "Chorus" {
4692  components {
4693    C1: cap(100n)
4694    R1: resistor(10k)
4695    BBD1: bbd(mn3207)
4696    LFO1: lfo(triangle, 100k, 47n)
4697  }
4698  nets {
4699    in -> C1.a
4700    C1.b -> R1.a, BBD1.in
4701    R1.b -> gnd
4702    BBD1.out -> out
4703    LFO1.out -> BBD1.clock
4704  }
4705}
4706"#;
4707        let def = parse_pedal_file(src).unwrap();
4708        assert_eq!(def.name, "Chorus");
4709        assert_eq!(def.components.len(), 4);
4710        assert!(def.components.iter().any(|c| c
4711            .kind
4712            .as_any()
4713            .downcast_ref::<Bbd>()
4714            .map_or(false, |b| b.bbd_type == BbdType::Mn3207)));
4715    }
4716
4717    // ── Neon bulb parser tests ─────────────────────────────────────────
4718
4719    #[test]
4720    fn parse_neon_default() {
4721        let (_, (c, _)) = component_def("NE1: neon()").unwrap();
4722        assert_eq!(c.id, "NE1");
4723        assert_eq!(
4724            c.kind.as_any().downcast_ref::<Neon>().unwrap(),
4725            &Neon {
4726                neon_type: NeonType::Ne2
4727            }
4728        );
4729    }
4730
4731    #[test]
4732    fn parse_neon_ne2() {
4733        let (_, (c, _)) = component_def("NE1: neon(ne2)").unwrap();
4734        assert_eq!(
4735            c.kind.as_any().downcast_ref::<Neon>().unwrap(),
4736            &Neon {
4737                neon_type: NeonType::Ne2
4738            }
4739        );
4740    }
4741
4742    #[test]
4743    fn parse_neon_ne2_hyphen() {
4744        let (_, (c, _)) = component_def("NE1: neon(ne-2)").unwrap();
4745        assert_eq!(
4746            c.kind.as_any().downcast_ref::<Neon>().unwrap(),
4747            &Neon {
4748                neon_type: NeonType::Ne2
4749            }
4750        );
4751    }
4752
4753    #[test]
4754    fn parse_neon_ne51() {
4755        let (_, (c, _)) = component_def("NE1: neon(ne51)").unwrap();
4756        assert_eq!(
4757            c.kind.as_any().downcast_ref::<Neon>().unwrap(),
4758            &Neon {
4759                neon_type: NeonType::Ne51
4760            }
4761        );
4762    }
4763
4764    #[test]
4765    fn parse_neon_ne83() {
4766        let (_, (c, _)) = component_def("NE1: neon(ne-83)").unwrap();
4767        assert_eq!(
4768            c.kind.as_any().downcast_ref::<Neon>().unwrap(),
4769            &Neon {
4770                neon_type: NeonType::Ne83
4771            }
4772        );
4773    }
4774
4775    #[test]
4776    fn parse_pedal_with_neon_tremolo() {
4777        let src = r#"
4778pedal "Fender Vibrato" {
4779  components {
4780    R1: resistor(1M)
4781    NE1: neon()
4782    C1: cap(0.1u)
4783    LDR1: photocoupler(vtl5c3)
4784    R2: resistor(10k)
4785  }
4786  nets {
4787    in -> R1.a
4788    R1.b -> LDR1.a
4789    LDR1.b -> out
4790    NE1.a -> C1.a
4791    C1.b -> gnd
4792    NE1.b -> R2.a
4793    R2.b -> gnd
4794  }
4795}
4796"#;
4797        let def = parse_pedal_file(src).unwrap();
4798        assert_eq!(def.name, "Fender Vibrato");
4799        assert!(def.components.iter().any(|c| c
4800            .kind
4801            .as_any()
4802            .downcast_ref::<Neon>()
4803            .map_or(false, |n| n.neon_type == NeonType::Ne2)));
4804    }
4805
4806    // ── Pentode parser tests ────────────────────────────────────────────
4807
4808    #[test]
4809    fn parse_pentode_ef86() {
4810        let (_, (c, _)) = component_def("V1: pentode(ef86)").unwrap();
4811        assert_eq!(c.id, "V1");
4812        assert_eq!(
4813            c.kind.as_any().downcast_ref::<Pentode>().unwrap(),
4814            &Pentode {
4815                model: "EF86".into()
4816            }
4817        );
4818    }
4819
4820    #[test]
4821    fn parse_pentode_el84() {
4822        let (_, (c, _)) = component_def("V1: pentode(el84)").unwrap();
4823        assert_eq!(
4824            c.kind.as_any().downcast_ref::<Pentode>().unwrap(),
4825            &Pentode {
4826                model: "EL84".into()
4827            }
4828        );
4829    }
4830
4831    #[test]
4832    fn parse_pentode_6267() {
4833        // 6267 = US designation for EF86 — stored as "6267", resolved at model lookup
4834        let (_, (c, _)) = component_def("V1: pentode(6267)").unwrap();
4835        assert_eq!(
4836            c.kind.as_any().downcast_ref::<Pentode>().unwrap(),
4837            &Pentode {
4838                model: "6267".into()
4839            }
4840        );
4841    }
4842
4843    #[test]
4844    fn parse_pentode_6bq5() {
4845        // 6BQ5 = US designation for EL84 — stored as "6BQ5", resolved at model lookup
4846        let (_, (c, _)) = component_def("V1: pentode(6bq5)").unwrap();
4847        assert_eq!(
4848            c.kind.as_any().downcast_ref::<Pentode>().unwrap(),
4849            &Pentode {
4850                model: "6BQ5".into()
4851            }
4852        );
4853    }
4854
4855    #[test]
4856    fn parse_pedal_with_pentode() {
4857        let src = r#"
4858pedal "Vox Preamp" {
4859  components {
4860    C1: cap(20n)
4861    R1: resistor(1M)
4862    V1: pentode(ef86)
4863    R2: resistor(220k)
4864    R3: resistor(2.2k)
4865    C2: cap(25u)
4866  }
4867  nets {
4868    in -> C1.a
4869    C1.b -> R1.a, V1.grid
4870    R1.b -> gnd
4871    vcc -> R2.a
4872    R2.b -> V1.plate
4873    V1.cathode -> R3.a, C2.a
4874    R3.b -> gnd
4875    C2.b -> gnd
4876    V1.plate -> out
4877  }
4878}
4879"#;
4880        let def = parse_pedal_file(src).unwrap();
4881        assert_eq!(def.name, "Vox Preamp");
4882        assert!(def.components.iter().any(|c| c
4883            .kind
4884            .as_any()
4885            .downcast_ref::<Pentode>()
4886            .map_or(false, |p| p.model == "EF86")));
4887    }
4888
4889    #[test]
4890    fn parse_pentode_6l6gc() {
4891        let (_, (c, _)) = component_def("V1: pentode(6l6gc)").unwrap();
4892        assert_eq!(
4893            c.kind.as_any().downcast_ref::<Pentode>().unwrap(),
4894            &Pentode {
4895                model: "6L6GC".into()
4896            }
4897        );
4898    }
4899
4900    #[test]
4901    fn parse_pentode_6l6_alias() {
4902        // 6L6 = original lower-dissipation variant — stored as "6L6", resolved at model lookup
4903        let (_, (c, _)) = component_def("V1: pentode(6l6)").unwrap();
4904        assert_eq!(
4905            c.kind.as_any().downcast_ref::<Pentode>().unwrap(),
4906            &Pentode {
4907                model: "6L6".into()
4908            }
4909        );
4910    }
4911
4912    #[test]
4913    fn parse_pentode_5881_alias() {
4914        // 5881 = military equivalent of 6L6GC — stored as "5881", resolved at model lookup
4915        let (_, (c, _)) = component_def("V1: pentode(5881)").unwrap();
4916        assert_eq!(
4917            c.kind.as_any().downcast_ref::<Pentode>().unwrap(),
4918            &Pentode {
4919                model: "5881".into()
4920            }
4921        );
4922    }
4923
4924    #[test]
4925    fn parse_pentode_kt66_alias() {
4926        // KT66 = British equivalent of 6L6GC — stored as "KT66", resolved at model lookup
4927        let (_, (c, _)) = component_def("V1: pentode(kt66)").unwrap();
4928        assert_eq!(
4929            c.kind.as_any().downcast_ref::<Pentode>().unwrap(),
4930            &Pentode {
4931                model: "KT66".into()
4932            }
4933        );
4934    }
4935
4936    #[test]
4937    fn parse_pentode_el34() {
4938        let (_, (c, _)) = component_def("V1: pentode(el34)").unwrap();
4939        assert_eq!(
4940            c.kind.as_any().downcast_ref::<Pentode>().unwrap(),
4941            &Pentode {
4942                model: "EL34".into()
4943            }
4944        );
4945    }
4946
4947    #[test]
4948    fn parse_pentode_6ca7_alias() {
4949        // 6CA7 = American designation for EL34 — stored as "6CA7", resolved at model lookup
4950        let (_, (c, _)) = component_def("V1: pentode(6ca7)").unwrap();
4951        assert_eq!(
4952            c.kind.as_any().downcast_ref::<Pentode>().unwrap(),
4953            &Pentode {
4954                model: "6CA7".into()
4955            }
4956        );
4957    }
4958
4959    #[test]
4960    fn parse_pentode_kt77_alias() {
4961        // KT77 = drop-in alternative for EL34 — stored as "KT77", resolved at model lookup
4962        let (_, (c, _)) = component_def("V1: pentode(kt77)").unwrap();
4963        assert_eq!(
4964            c.kind.as_any().downcast_ref::<Pentode>().unwrap(),
4965            &Pentode {
4966                model: "KT77".into()
4967            }
4968        );
4969    }
4970
4971    #[test]
4972    fn parse_pentode_6550() {
4973        let (_, (c, _)) = component_def("V1: pentode(6550)").unwrap();
4974        assert_eq!(
4975            c.kind.as_any().downcast_ref::<Pentode>().unwrap(),
4976            &Pentode {
4977                model: "6550".into()
4978            }
4979        );
4980    }
4981
4982    #[test]
4983    fn parse_pentode_kt88_alias() {
4984        // KT88 = distinct tube — stored as "KT88", resolved at model lookup
4985        let (_, (c, _)) = component_def("V1: pentode(kt88)").unwrap();
4986        assert_eq!(
4987            c.kind.as_any().downcast_ref::<Pentode>().unwrap(),
4988            &Pentode {
4989                model: "KT88".into()
4990            }
4991        );
4992    }
4993
4994    #[test]
4995    fn parse_pentode_kt90_alias() {
4996        // KT90 = higher dissipation variant of 6550 — stored as "KT90", resolved at model lookup
4997        let (_, (c, _)) = component_def("V1: pentode(kt90)").unwrap();
4998        assert_eq!(
4999            c.kind.as_any().downcast_ref::<Pentode>().unwrap(),
5000            &Pentode {
5001                model: "KT90".into()
5002            }
5003        );
5004    }
5005
5006    // ── Synth component parser tests ──────────────────────────────────
5007
5008    #[test]
5009    fn parse_synth_keyword() {
5010        let src = r#"
5011synth "Test Synth" {
5012  components {
5013    R1: resistor(10k)
5014  }
5015  nets {
5016    in -> R1.a
5017    R1.b -> out
5018  }
5019}
5020"#;
5021        let def = parse_pedal_file(src).unwrap();
5022        assert_eq!(def.name, "Test Synth");
5023    }
5024
5025    #[test]
5026    fn parse_vco_cem3340() {
5027        let (_, (c, _)) = component_def("VCO1: vco(cem3340)").unwrap();
5028        assert_eq!(c.id, "VCO1");
5029        assert_eq!(
5030            c.kind.as_any().downcast_ref::<Vco>().unwrap(),
5031            &Vco {
5032                vco_type: VcoType::Cem3340,
5033                base_freq: 440.0,
5034                waveform: VcoWaveformDsl::Saw
5035            }
5036        );
5037    }
5038
5039    #[test]
5040    fn parse_vco_as3340() {
5041        let (_, (c, _)) = component_def("VCO1: vco(as3340)").unwrap();
5042        assert_eq!(
5043            c.kind.as_any().downcast_ref::<Vco>().unwrap(),
5044            &Vco {
5045                vco_type: VcoType::As3340,
5046                base_freq: 440.0,
5047                waveform: VcoWaveformDsl::Saw
5048            }
5049        );
5050    }
5051
5052    #[test]
5053    fn parse_vco_v3340() {
5054        let (_, (c, _)) = component_def("VCO1: vco(v3340)").unwrap();
5055        assert_eq!(
5056            c.kind.as_any().downcast_ref::<Vco>().unwrap(),
5057            &Vco {
5058                vco_type: VcoType::V3340,
5059                base_freq: 440.0,
5060                waveform: VcoWaveformDsl::Saw
5061            }
5062        );
5063    }
5064
5065    #[test]
5066    fn parse_vco_with_freq() {
5067        let (_, (c, _)) = component_def("VCO1: vco(cem3340, 540)").unwrap();
5068        assert_eq!(
5069            c.kind.as_any().downcast_ref::<Vco>().unwrap(),
5070            &Vco {
5071                vco_type: VcoType::Cem3340,
5072                base_freq: 540.0,
5073                waveform: VcoWaveformDsl::Saw
5074            }
5075        );
5076    }
5077
5078    #[test]
5079    fn parse_vco_with_freq_and_waveform() {
5080        let (_, (c, _)) = component_def("VCO1: vco(cem3340, 540, pulse)").unwrap();
5081        assert_eq!(
5082            c.kind.as_any().downcast_ref::<Vco>().unwrap(),
5083            &Vco {
5084                vco_type: VcoType::Cem3340,
5085                base_freq: 540.0,
5086                waveform: VcoWaveformDsl::Pulse
5087            }
5088        );
5089    }
5090
5091    #[test]
5092    fn parse_vcf_cem3320() {
5093        let (_, (c, _)) = component_def("VCF1: vcf(cem3320)").unwrap();
5094        assert_eq!(c.id, "VCF1");
5095        assert_eq!(
5096            c.kind.as_any().downcast_ref::<Vcf>().unwrap(),
5097            &Vcf {
5098                vcf_type: VcfType::Cem3320
5099            }
5100        );
5101    }
5102
5103    #[test]
5104    fn parse_vcf_as3320() {
5105        let (_, (c, _)) = component_def("VCF1: vcf(as3320)").unwrap();
5106        assert_eq!(
5107            c.kind.as_any().downcast_ref::<Vcf>().unwrap(),
5108            &Vcf {
5109                vcf_type: VcfType::As3320
5110            }
5111        );
5112    }
5113
5114    #[test]
5115    fn parse_vca_ssm2164() {
5116        let (_, (c, _)) = component_def("VCA1: vca(ssm2164)").unwrap();
5117        assert_eq!(c.id, "VCA1");
5118        assert_eq!(
5119            c.kind.as_any().downcast_ref::<Vca>().unwrap(),
5120            &Vca {
5121                vca_type: VcaType::Ssm2164
5122            }
5123        );
5124    }
5125
5126    #[test]
5127    fn parse_vca_v2164() {
5128        let (_, (c, _)) = component_def("VCA1: vca(v2164)").unwrap();
5129        assert_eq!(
5130            c.kind.as_any().downcast_ref::<Vca>().unwrap(),
5131            &Vca {
5132                vca_type: VcaType::V2164
5133            }
5134        );
5135    }
5136
5137    #[test]
5138    fn parse_comparator_lm311() {
5139        let (_, (c, _)) = component_def("U1: comparator(lm311)").unwrap();
5140        assert_eq!(c.id, "U1");
5141        assert_eq!(
5142            c.kind.as_any().downcast_ref::<Comparator>().unwrap(),
5143            &Comparator {
5144                comp_type: ComparatorType::Lm311
5145            }
5146        );
5147    }
5148
5149    #[test]
5150    fn parse_comparator_lm393() {
5151        let (_, (c, _)) = component_def("U1: comparator(lm393)").unwrap();
5152        assert_eq!(
5153            c.kind.as_any().downcast_ref::<Comparator>().unwrap(),
5154            &Comparator {
5155                comp_type: ComparatorType::Lm393
5156            }
5157        );
5158    }
5159
5160    #[test]
5161    fn parse_analog_switch_cd4066() {
5162        let (_, (c, _)) = component_def("SW1: switch(cd4066)").unwrap();
5163        assert_eq!(c.id, "SW1");
5164        assert_eq!(
5165            c.kind.as_any().downcast_ref::<AnalogSwitch>().unwrap(),
5166            &AnalogSwitch {
5167                switch_type: AnalogSwitchType::Cd4066
5168            }
5169        );
5170    }
5171
5172    #[test]
5173    fn parse_analog_switch_dg411() {
5174        let (_, (c, _)) = component_def("SW1: switch(dg411)").unwrap();
5175        assert_eq!(
5176            c.kind.as_any().downcast_ref::<AnalogSwitch>().unwrap(),
5177            &AnalogSwitch {
5178                switch_type: AnalogSwitchType::Dg411
5179            }
5180        );
5181    }
5182
5183    #[test]
5184    fn parse_matched_npn_ssm2210() {
5185        let (_, (c, _)) = component_def("QM1: matched_npn(ssm2210)").unwrap();
5186        assert_eq!(c.id, "QM1");
5187        assert_eq!(
5188            c.kind.as_any().downcast_ref::<MatchedNpn>().unwrap(),
5189            &MatchedNpn {
5190                matched_type: MatchedTransistorType::Ssm2210
5191            }
5192        );
5193    }
5194
5195    #[test]
5196    fn parse_matched_npn_ca3046() {
5197        let (_, (c, _)) = component_def("QM1: matched_npn(ca3046)").unwrap();
5198        assert_eq!(
5199            c.kind.as_any().downcast_ref::<MatchedNpn>().unwrap(),
5200            &MatchedNpn {
5201                matched_type: MatchedTransistorType::Ca3046
5202            }
5203        );
5204    }
5205
5206    #[test]
5207    fn parse_matched_pnp_lm394() {
5208        let (_, (c, _)) = component_def("QM1: matched_pnp(lm394)").unwrap();
5209        assert_eq!(
5210            c.kind.as_any().downcast_ref::<MatchedPnp>().unwrap(),
5211            &MatchedPnp {
5212                matched_type: MatchedTransistorType::Lm394
5213            }
5214        );
5215    }
5216
5217    #[test]
5218    fn parse_tempco() {
5219        let (_, (c, _)) = component_def("RT1: tempco(2k, 3500)").unwrap();
5220        assert_eq!(c.id, "RT1");
5221        let tc = c
5222            .kind
5223            .as_any()
5224            .downcast_ref::<Tempco>()
5225            .expect("expected Tempco");
5226        assert!((tc.resistance - 2000.0).abs() < 1e-6);
5227        assert!((tc.ppm - 3500.0).abs() < 1e-6);
5228    }
5229
5230    #[test]
5231    fn parse_synth_with_cv_gate() {
5232        let src = r#"
5233synth "CV Test" {
5234  components {
5235    VCO1: vco(as3340)
5236    R1: resistor(100k)
5237    VCF1: vcf(as3320)
5238    R2: resistor(100k)
5239  }
5240  nets {
5241    cv_pitch -> R1.a
5242    R1.b -> VCO1.cv
5243    VCO1.saw -> VCF1.in
5244    cv_filter -> R2.a
5245    R2.b -> VCF1.cv
5246    VCF1.out -> out
5247    gate -> gnd
5248  }
5249}
5250"#;
5251        let def = parse_pedal_file(src).unwrap();
5252        assert_eq!(def.name, "CV Test");
5253        assert_eq!(def.components.len(), 4);
5254        // Check that cv_pitch, cv_filter, and gate are valid reserved node names
5255        let has_reserved = |name: &str| {
5256            def.nets.iter().any(|n| {
5257                matches!(&n.from, Pin::Reserved(s) if s == name)
5258                    || n.to
5259                        .iter()
5260                        .any(|p| matches!(p, Pin::Reserved(s) if s == name))
5261            })
5262        };
5263        assert!(has_reserved("cv_pitch"));
5264        assert!(has_reserved("cv_filter"));
5265        assert!(has_reserved("gate"));
5266    }
5267
5268    // ── Studio Equipment parser tests ──────────────────────────────────
5269
5270    #[test]
5271    fn parse_transformer_simple() {
5272        let (_, (c, _)) = component_def("T1: transformer(10:1, 2H)").unwrap();
5273        assert_eq!(c.id, "T1");
5274        let cfg = c.kind.transformer_config().expect("expected Transformer");
5275        assert_eq!(cfg.model, None);
5276        assert!((cfg.turns_ratio - 10.0).abs() < 1e-6);
5277        assert!((cfg.primary_inductance - 2.0).abs() < 1e-6);
5278        assert_eq!(cfg.primary_type, WindingType::Standard);
5279        assert_eq!(cfg.secondary_type, WindingType::Standard);
5280    }
5281
5282    #[test]
5283    fn parse_transformer_with_model_name() {
5284        let (_, (c, _)) = component_def("T1: transformer(10:1, JT11P1)").unwrap();
5285        let cfg = c.kind.transformer_config().expect("expected Transformer");
5286        assert_eq!(cfg.model.as_deref(), Some("JT11P1"));
5287        assert!((cfg.turns_ratio - 10.0).abs() < 1e-6);
5288        assert_eq!(cfg.primary_inductance, 0.0);
5289        assert_eq!(cfg.coupling, 0.0);
5290    }
5291
5292    #[test]
5293    fn parse_transformer_with_winding_types() {
5294        // Parser syntax: pp = push-pull, ct = center-tap
5295        let (_, (c, _)) = component_def("T2: transformer(1.5:1, 4H, pp, ct)").unwrap();
5296        let cfg = c.kind.transformer_config().expect("expected Transformer");
5297        assert!((cfg.turns_ratio - 1.5).abs() < 1e-6);
5298        assert!((cfg.primary_inductance - 4.0).abs() < 1e-6);
5299        assert_eq!(cfg.primary_type, WindingType::PushPull);
5300        assert_eq!(cfg.secondary_type, WindingType::CenterTap);
5301    }
5302
5303    #[test]
5304    fn parse_transformer_with_dcr_and_coupling() {
5305        // Parser syntax: dcr=value (applies to both), k=coupling
5306        let (_, (c, _)) = component_def("T3: transformer(5:1, 1H, dcr=50, k=0.98)").unwrap();
5307        let cfg = c.kind.transformer_config().expect("expected Transformer");
5308        assert!((cfg.turns_ratio - 5.0).abs() < 1e-6);
5309        assert!((cfg.primary_dcr - 50.0).abs() < 1e-6);
5310        assert!((cfg.secondary_dcr - 50.0).abs() < 1e-6); // Both set to same value
5311        assert!((cfg.coupling - 0.98).abs() < 1e-6);
5312    }
5313
5314    #[test]
5315    fn parse_transformer_with_linear_model_fields() {
5316        let (_, (c, _)) = component_def(
5317            "T4: transformer(10:1, JT11P1, Lp=2H, Llp=20m, Lls=200u, Lm=1.98H, Rc=100k, Idc=45m)",
5318        )
5319        .unwrap();
5320        let cfg = c.kind.transformer_config().expect("expected Transformer");
5321        assert_eq!(cfg.model.as_deref(), Some("JT11P1"));
5322        assert!((cfg.primary_inductance - 2.0).abs() < 1e-9);
5323        assert!((cfg.primary_leakage.unwrap() - 20e-3).abs() < 1e-9);
5324        assert!((cfg.secondary_leakage.unwrap() - 200e-6).abs() < 1e-12);
5325        assert!((cfg.magnetizing_inductance.unwrap() - 1.98).abs() < 1e-9);
5326        assert!((cfg.core_loss_resistance.unwrap() - 100_000.0).abs() < 1e-6);
5327        assert!((cfg.dc_bias_current.unwrap() - 45e-3).abs() < 1e-12);
5328    }
5329
5330    #[test]
5331    fn parse_transformer_with_tertiary() {
5332        let (_, (c, _)) =
5333            component_def("T_sc: transformer(4:1, 5.7H, 10, 10p, tertiary=9.5:1)").unwrap();
5334        let cfg = c.kind.transformer_config().expect("expected Transformer");
5335        assert!((cfg.turns_ratio - 4.0).abs() < 1e-6);
5336        assert!((cfg.primary_inductance - 5.7).abs() < 1e-6);
5337        assert!((cfg.primary_dcr - 10.0).abs() < 1e-6);
5338        assert!((cfg.capacitance - 10e-12).abs() < 1e-15);
5339        assert!(cfg.tertiary_turns_ratio.is_some());
5340        assert!((cfg.tertiary_turns_ratio.unwrap() - 9.5).abs() < 1e-6);
5341    }
5342
5343    #[test]
5344    fn parse_transformer_no_tertiary() {
5345        let (_, (c, _)) = component_def("T1: transformer(10:1, 2H)").unwrap();
5346        let cfg = c.kind.transformer_config().expect("expected Transformer");
5347        assert!(cfg.tertiary_turns_ratio.is_none());
5348    }
5349
5350    #[test]
5351    fn parse_cap_switched() {
5352        let (_, (c, _)) = component_def("C_lf: cap_switched(27n, 68n, 220n, 1.5u)").unwrap();
5353        assert_eq!(c.id, "C_lf");
5354        let cs = c
5355            .kind
5356            .as_any()
5357            .downcast_ref::<CapSwitched>()
5358            .expect("expected CapSwitched");
5359        assert_eq!(cs.values.len(), 4);
5360        assert!((cs.values[0] - 27e-9).abs() < 1e-12);
5361        assert!((cs.values[1] - 68e-9).abs() < 1e-12);
5362        assert!((cs.values[2] - 220e-9).abs() < 1e-12);
5363        assert!((cs.values[3] - 1.5e-6).abs() < 1e-12);
5364    }
5365
5366    #[test]
5367    fn parse_inductor_switched() {
5368        let (_, (c, _)) =
5369            component_def("L_hf: inductor_switched(27m, 33m, 47m, 68m, 82m, 150m)").unwrap();
5370        assert_eq!(c.id, "L_hf");
5371        let is = c
5372            .kind
5373            .as_any()
5374            .downcast_ref::<InductorSwitched>()
5375            .expect("expected InductorSwitched");
5376        assert_eq!(is.values.len(), 6);
5377        assert!((is.values[0] - 27e-3).abs() < 1e-6);
5378        assert!((is.values[5] - 150e-3).abs() < 1e-6);
5379    }
5380
5381    #[test]
5382    fn parse_rotary_switch() {
5383        let (_, (c, _)) = component_def(r#"SW1: rotary("20Hz", "30Hz", "60Hz", "100Hz")"#).unwrap();
5384        assert_eq!(c.id, "SW1");
5385        let rs = c
5386            .kind
5387            .as_any()
5388            .downcast_ref::<RotarySwitch>()
5389            .expect("expected RotarySwitch");
5390        assert_eq!(rs.linked_ids.len(), 4);
5391        assert_eq!(rs.linked_ids[0], "20Hz");
5392        assert_eq!(rs.linked_ids[3], "100Hz");
5393    }
5394
5395    #[test]
5396    fn parse_studio_equipment_file() {
5397        let src = r#"
5398equipment "Test EQ" {
5399  components {
5400    T1: transformer(10:1, 2H, pp, ct)
5401    C_lf: cap_switched(27n, 68n, 220n)
5402    L_hf: inductor_switched(27m, 47m, 82m)
5403    SW_freq: rotary("100Hz", "200Hz", "400Hz")
5404    R1: resistor(10k)
5405  }
5406  nets {
5407    in -> T1.a
5408    T1.b -> C_lf.a
5409    C_lf.b -> L_hf.a
5410    L_hf.b -> R1.a
5411    R1.b -> out
5412  }
5413}
5414"#;
5415        let def = parse_pedal_file(src).unwrap();
5416        assert_eq!(def.name, "Test EQ");
5417        assert_eq!(def.components.len(), 5);
5418        assert!(def.components.iter().any(|c| c.kind.is_transformer()));
5419        assert!(def.components.iter().any(|c| c
5420            .kind
5421            .as_any()
5422            .downcast_ref::<CapSwitched>()
5423            .is_some()));
5424        assert!(def.components.iter().any(|c| c
5425            .kind
5426            .as_any()
5427            .downcast_ref::<InductorSwitched>()
5428            .is_some()));
5429        assert!(def.components.iter().any(|c| c
5430            .kind
5431            .as_any()
5432            .downcast_ref::<RotarySwitch>()
5433            .is_some()));
5434    }
5435
5436    // ── Supply voltage parser tests ───────────────────────────────────────
5437
5438    #[test]
5439    fn parse_supply_9v() {
5440        let src = r#"
5441pedal "9V Pedal" {
5442    supply 9V
5443    components {
5444        R1: resistor(10k)
5445    }
5446    nets {
5447        in -> R1.a
5448        R1.b -> out
5449    }
5450}
5451"#;
5452        let def = parse_pedal_file(src).unwrap();
5453        assert_eq!(def.name, "9V Pedal");
5454        assert_eq!(def.supplies.len(), 1);
5455        let supply = &def.supplies[0];
5456        assert_eq!(supply.name, "vcc");
5457        assert_eq!(supply.config.voltage, 9.0);
5458        assert!(!supply.config.has_sag());
5459    }
5460
5461    #[test]
5462    fn parse_supply_250v_tube_amp() {
5463        let src = r#"
5464pedal "Tube Amp" {
5465    supply 250V
5466    components {
5467        V1: triode(12ax7)
5468        R1: resistor(100k)
5469    }
5470    nets {
5471        in -> V1.grid
5472        V1.plate -> R1.a
5473        R1.b -> out
5474    }
5475}
5476"#;
5477        let def = parse_pedal_file(src).unwrap();
5478        assert_eq!(def.name, "Tube Amp");
5479        assert_eq!(def.supplies[0].config.voltage, 250.0);
5480    }
5481
5482    #[test]
5483    fn parse_supply_lowercase_v() {
5484        let src = r#"
5485pedal "Test" {
5486    supply 12v
5487    components {
5488        R1: resistor(10k)
5489    }
5490    nets {
5491        in -> out
5492    }
5493}
5494"#;
5495        let def = parse_pedal_file(src).unwrap();
5496        assert_eq!(def.supplies[0].config.voltage, 12.0);
5497    }
5498
5499    #[test]
5500    fn parse_supply_decimal() {
5501        let src = r#"
5502pedal "Test" {
5503    supply 9.6V
5504    components {
5505        R1: resistor(10k)
5506    }
5507    nets {
5508        in -> out
5509    }
5510}
5511"#;
5512        let def = parse_pedal_file(src).unwrap();
5513        assert!((def.supplies[0].config.voltage - 9.6).abs() < 0.01);
5514    }
5515
5516    #[test]
5517    fn parse_no_supply_defaults_empty() {
5518        let src = r#"
5519pedal "Default" {
5520    components {
5521        R1: resistor(10k)
5522    }
5523    nets {
5524        in -> out
5525    }
5526}
5527"#;
5528        let def = parse_pedal_file(src).unwrap();
5529        assert!(def.supplies.is_empty());
5530    }
5531
5532    #[test]
5533    fn parse_supply_block_tube_rectifier() {
5534        let src = r#"
5535pedal "Tube Amp Sag" {
5536    supply 480V {
5537        impedance: 150
5538        filter_cap: 40u
5539        rectifier: tube
5540    }
5541    components {
5542        V1: triode(12ax7)
5543        R1: resistor(100k)
5544    }
5545    nets {
5546        in -> V1.grid
5547        V1.plate -> R1.a
5548        R1.b -> out
5549    }
5550}
5551"#;
5552        let def = parse_pedal_file(src).unwrap();
5553        let supply = &def.supplies[0].config;
5554        assert_eq!(supply.voltage, 480.0);
5555        assert!((supply.impedance.unwrap() - 150.0).abs() < 1e-6);
5556        assert!((supply.filter_cap.unwrap() - 40e-6).abs() < 1e-12);
5557        assert_eq!(supply.rectifier, RectifierType::Tube);
5558        assert!(supply.has_sag());
5559    }
5560
5561    #[test]
5562    fn parse_supply_block_solid_state() {
5563        let src = r#"
5564pedal "Modern Amp" {
5565    supply 480V {
5566        impedance: 5
5567        filter_cap: 220u
5568        rectifier: solid_state
5569    }
5570    components {
5571        R1: resistor(10k)
5572    }
5573    nets {
5574        in -> R1.a
5575        R1.b -> out
5576    }
5577}
5578"#;
5579        let def = parse_pedal_file(src).unwrap();
5580        let supply = &def.supplies[0].config;
5581        assert_eq!(supply.voltage, 480.0);
5582        assert!((supply.impedance.unwrap() - 5.0).abs() < 1e-6);
5583        assert!((supply.filter_cap.unwrap() - 220e-6).abs() < 1e-12);
5584        assert_eq!(supply.rectifier, RectifierType::SolidState);
5585    }
5586
5587    #[test]
5588    fn parse_supply_block_with_comments() {
5589        let src = r#"
5590pedal "Commented Supply" {
5591    supply 400V {
5592        impedance: 100    # tube rectifier + transformer resistance
5593        filter_cap: 47u   # main filter cap
5594        rectifier: tube    # GZ34
5595    }
5596    components {
5597        R1: resistor(10k)
5598    }
5599    nets {
5600        in -> R1.a
5601        R1.b -> out
5602    }
5603}
5604"#;
5605        let def = parse_pedal_file(src).unwrap();
5606        let supply = &def.supplies[0].config;
5607        assert_eq!(supply.voltage, 400.0);
5608        assert!((supply.impedance.unwrap() - 100.0).abs() < 1e-6);
5609        assert!((supply.filter_cap.unwrap() - 47e-6).abs() < 1e-12);
5610        assert_eq!(supply.rectifier, RectifierType::Tube);
5611    }
5612
5613    // ── Multiple supplies tests ────────────────────────────────────────
5614
5615    #[test]
5616    fn parse_supplies_block_dual_rail() {
5617        let src = r#"
5618pedal "Dual Supply" {
5619    supplies {
5620        V+: 15V
5621        V-: -15V
5622    }
5623    components {
5624        R1: resistor(10k)
5625    }
5626    nets {
5627        in -> R1.a
5628        R1.b -> out
5629    }
5630}
5631"#;
5632        let def = parse_pedal_file(src).unwrap();
5633        assert_eq!(def.supplies.len(), 2);
5634
5635        let vpos = &def.supplies[0];
5636        assert_eq!(vpos.name, "V+");
5637        assert_eq!(vpos.config.voltage, 15.0);
5638
5639        let vneg = &def.supplies[1];
5640        assert_eq!(vneg.name, "V-");
5641        assert_eq!(vneg.config.voltage, -15.0);
5642    }
5643
5644    #[test]
5645    fn parse_supplies_block_tube_multiple_rails() {
5646        let src = r#"
5647pedal "Tube Amp Multi-Rail" {
5648    supplies {
5649        B+: 300V { impedance: 100, rectifier: tube }
5650        bias: -50V
5651        filament: 6.3V
5652    }
5653    components {
5654        V1: triode(12ax7)
5655        R1: resistor(100k)
5656    }
5657    nets {
5658        in -> V1.grid
5659        V1.plate -> R1.a
5660        R1.b -> out
5661    }
5662}
5663"#;
5664        let def = parse_pedal_file(src).unwrap();
5665        assert_eq!(def.supplies.len(), 3);
5666
5667        let bplus = &def.supplies[0];
5668        assert_eq!(bplus.name, "B+");
5669        assert_eq!(bplus.config.voltage, 300.0);
5670        assert!(bplus.config.has_sag());
5671        assert_eq!(bplus.config.rectifier, RectifierType::Tube);
5672
5673        let bias = &def.supplies[1];
5674        assert_eq!(bias.name, "bias");
5675        assert_eq!(bias.config.voltage, -50.0);
5676        assert!(!bias.config.has_sag());
5677
5678        let filament = &def.supplies[2];
5679        assert_eq!(filament.name, "filament");
5680        assert_eq!(filament.config.voltage, 6.3);
5681    }
5682
5683    #[test]
5684    fn parse_supplies_vcc_legacy_compat() {
5685        // Legacy `supply 9V` should create "vcc" rail
5686        let src = r#"
5687pedal "Legacy" {
5688    supply 9V
5689    components {
5690        R1: resistor(10k)
5691    }
5692    nets {
5693        in -> R1.a
5694        R1.b -> out
5695    }
5696}
5697"#;
5698        let def = parse_pedal_file(src).unwrap();
5699        assert_eq!(def.supplies.len(), 1);
5700        assert_eq!(def.supplies[0].name, "vcc");
5701        assert_eq!(def.supplies[0].config.voltage, 9.0);
5702    }
5703
5704    #[test]
5705    fn pedal_def_helper_methods() {
5706        let src = r#"
5707pedal "Test Helpers" {
5708    supplies {
5709        V+: 15V
5710        V-: -15V
5711        vcc: 5V
5712    }
5713    components {
5714        R1: resistor(10k)
5715    }
5716    nets {
5717        in -> out
5718    }
5719}
5720"#;
5721        let def = parse_pedal_file(src).unwrap();
5722
5723        // primary_supply_voltage returns first supply
5724        assert_eq!(def.primary_supply_voltage(), 15.0);
5725
5726        // get_supply finds by name
5727        assert!(def.get_supply("V+").is_some());
5728        assert!(def.get_supply("V-").is_some());
5729        assert!(def.get_supply("vcc").is_some());
5730        assert!(def.get_supply("nonexistent").is_none());
5731
5732        // supply_names returns all names
5733        let names = def.supply_names();
5734        assert_eq!(names.len(), 3);
5735        assert!(names.contains(&"V+"));
5736
5737        // is_supply_rail checks if name is a rail
5738        assert!(def.is_supply_rail("V+"));
5739        assert!(def.is_supply_rail("V-"));
5740        assert!(!def.is_supply_rail("gnd"));
5741    }
5742
5743    #[test]
5744    fn parse_supply_rails_in_nets() {
5745        // Test that V+ and V- can be used in nets section (bipolar supply circuit)
5746        let src = r#"
5747pedal "Bipolar Op-Amp" {
5748    supplies {
5749        V+: 15V
5750        V-: -15V
5751    }
5752    components {
5753        R1: resistor(10k)
5754        R2: resistor(10k)
5755        C1: cap(100u)
5756    }
5757    nets {
5758        V+ -> R1.a
5759        R1.b -> R2.a, C1.a
5760        R2.b -> gnd
5761        C1.b -> gnd
5762        V- -> R2.b
5763        in -> out
5764    }
5765}
5766"#;
5767        let def = parse_pedal_file(src).unwrap();
5768        assert_eq!(def.nets.len(), 6);
5769
5770        // Check V+ is parsed as a reserved pin
5771        let v_plus_net = &def.nets[0];
5772        assert_eq!(v_plus_net.from, Pin::Reserved("V+".to_string()));
5773
5774        // Check V- is parsed as a reserved pin
5775        let v_minus_net = &def.nets[4];
5776        assert_eq!(v_minus_net.from, Pin::Reserved("V-".to_string()));
5777    }
5778
5779    // ── Monitors section tests ────────────────────────────────────────
5780
5781    #[test]
5782    fn parse_monitors_section() {
5783        let src = r#"
5784pedal "Tube Preamp" {
5785    supply 250V
5786    components {
5787        V1: triode(12ax7)
5788        R1: resistor(100k)
5789    }
5790    nets {
5791        in -> V1.grid
5792        V1.plate -> R1.a
5793        R1.b -> out
5794    }
5795    monitors {
5796        V1.plate_current -> "Tube 1" [vu]
5797        output -> "Output Level" [ppm]
5798        input -> "Input Level" [peak]
5799    }
5800}
5801"#;
5802        let def = parse_pedal_file(src).unwrap();
5803        assert_eq!(def.monitors.len(), 3);
5804
5805        let tube_mon = &def.monitors[0];
5806        assert_eq!(tube_mon.component, "V1");
5807        assert_eq!(tube_mon.property, "plate_current");
5808        assert_eq!(tube_mon.label, "Tube 1");
5809        assert_eq!(tube_mon.meter_type, MeterType::Vu);
5810
5811        let output_mon = &def.monitors[1];
5812        assert_eq!(output_mon.component, "output");
5813        assert_eq!(output_mon.property, "level");
5814        assert_eq!(output_mon.meter_type, MeterType::Ppm);
5815
5816        let input_mon = &def.monitors[2];
5817        assert_eq!(input_mon.component, "input");
5818        assert_eq!(input_mon.meter_type, MeterType::Peak);
5819    }
5820
5821    #[test]
5822    fn parse_monitors_gain_reduction() {
5823        let src = r#"
5824pedal "Compressor" {
5825    components {
5826        R1: resistor(10k)
5827    }
5828    nets {
5829        in -> R1.a
5830        R1.b -> out
5831    }
5832    monitors {
5833        GR.reduction -> "Gain Reduction" [gr]
5834        supply.sag -> "Sag" [sag]
5835    }
5836}
5837"#;
5838        let def = parse_pedal_file(src).unwrap();
5839        assert_eq!(def.monitors.len(), 2);
5840        assert_eq!(def.monitors[0].meter_type, MeterType::GainReduction);
5841        assert_eq!(def.monitors[1].meter_type, MeterType::SupplySag);
5842    }
5843
5844    #[test]
5845    fn parse_empty_monitors() {
5846        let src = r#"
5847pedal "No Monitors" {
5848    components {
5849        R1: resistor(10k)
5850    }
5851    nets {
5852        in -> out
5853    }
5854}
5855"#;
5856        let def = parse_pedal_file(src).unwrap();
5857        assert!(def.monitors.is_empty());
5858    }
5859
5860    // -----------------------------------------------------------------------
5861    // IEC 60062 embedded-decimal notation tests
5862    // -----------------------------------------------------------------------
5863
5864    #[test]
5865    fn eng_value_embedded_decimal_resistance() {
5866        let cases: &[(&str, f64)] = &[
5867            ("4k7", 4700.0),
5868            ("2k2", 2200.0),
5869            ("1M5", 1_500_000.0),
5870            ("4R7", 4.7),
5871            ("0R47", 0.47),
5872            ("100R", 100.0),
5873            ("47R", 47.0),
5874            ("10R", 10.0),
5875        ];
5876        for &(input, expected) in cases {
5877            let (rest, v) =
5878                eng_value(input).unwrap_or_else(|e| panic!("failed to parse {:?}: {}", input, e));
5879            assert!(rest.is_empty(), "trailing input {:?} for {:?}", rest, input);
5880            assert!(
5881                (v - expected).abs() < expected.abs() * 1e-9 + 1e-15,
5882                "{:?}: got {} expected {}",
5883                input,
5884                v,
5885                expected
5886            );
5887        }
5888    }
5889
5890    #[test]
5891    fn eng_value_embedded_decimal_capacitance() {
5892        let cases: &[(&str, f64)] = &[
5893            ("2u5", 2.5e-6),
5894            ("4u7", 4.7e-6),
5895            ("4n7", 4.7e-9),
5896            ("2n2", 2.2e-9),
5897            ("1p5", 1.5e-12),
5898        ];
5899        for &(input, expected) in cases {
5900            let (rest, v) =
5901                eng_value(input).unwrap_or_else(|e| panic!("failed to parse {:?}: {}", input, e));
5902            assert!(rest.is_empty(), "trailing input {:?} for {:?}", rest, input);
5903            assert!(
5904                (v - expected).abs() < expected.abs() * 1e-9,
5905                "{:?}: got {:.6e} expected {:.6e}",
5906                input,
5907                v,
5908                expected
5909            );
5910        }
5911    }
5912
5913    #[test]
5914    fn eng_value_embedded_decimal_inductance() {
5915        let cases: &[(&str, f64)] = &[("4m7", 4.7e-3), ("2m2", 2.2e-3)];
5916        for &(input, expected) in cases {
5917            let (rest, v) =
5918                eng_value(input).unwrap_or_else(|e| panic!("failed to parse {:?}: {}", input, e));
5919            assert!(rest.is_empty(), "trailing input {:?} for {:?}", rest, input);
5920            assert!(
5921                (v - expected).abs() < expected.abs() * 1e-9,
5922                "{:?}: got {:.6e} expected {:.6e}",
5923                input,
5924                v,
5925                expected
5926            );
5927        }
5928    }
5929
5930    #[test]
5931    fn eng_value_existing_formats_unchanged() {
5932        let cases: &[(&str, f64)] = &[
5933            ("10k", 10_000.0),
5934            ("100n", 100e-9),
5935            ("470p", 470e-12),
5936            ("1u", 1e-6),
5937            ("100m", 100e-3),
5938            ("1M", 1_000_000.0),
5939            ("47", 47.0),
5940            ("0.1", 0.1),
5941            ("3.3k", 3300.0),
5942            ("4.7k", 4700.0),
5943            ("220n", 220e-9),
5944        ];
5945        for &(input, expected) in cases {
5946            let (rest, v) =
5947                eng_value(input).unwrap_or_else(|e| panic!("failed to parse {:?}: {}", input, e));
5948            assert!(rest.is_empty(), "trailing input {:?} for {:?}", rest, input);
5949            assert!(
5950                (v - expected).abs() < expected.abs() * 1e-9 + 1e-15,
5951                "{:?}: got {} expected {}",
5952                input,
5953                v,
5954                expected
5955            );
5956        }
5957    }
5958
5959    #[test]
5960    fn eng_value_uppercase_k() {
5961        let (rest, v) = eng_value("4K7").unwrap();
5962        assert!(rest.is_empty());
5963        assert!((v - 4700.0).abs() < 1e-6);
5964    }
5965
5966    #[test]
5967    fn component_defs_with_shorthand() {
5968        // Resistor with embedded decimal
5969        let (_, (c, _)) = component_def("R1: resistor(4k7)").unwrap();
5970        assert_eq!(
5971            c.kind.as_any().downcast_ref::<Resistor>().unwrap(),
5972            &Resistor { value: 4700.0 }
5973        );
5974
5975        // Capacitor with embedded decimal
5976        let (_, (c, _)) = component_def("C1: cap(4n7)").unwrap();
5977        let cap = c
5978            .kind
5979            .as_any()
5980            .downcast_ref::<Capacitor>()
5981            .expect("expected Capacitor");
5982        assert!((cap.config.value - 4.7e-9).abs() < 1e-18);
5983
5984        // Inductor with embedded decimal
5985        let (_, (c, _)) = component_def("L1: inductor(4m7)").unwrap();
5986        assert_eq!(
5987            c.kind.as_any().downcast_ref::<Inductor>().unwrap(),
5988            &Inductor { value: 4.7e-3 }
5989        );
5990    }
5991
5992    #[test]
5993    fn full_pedal_with_shorthand_values() {
5994        let src = r#"
5995pedal "Shorthand Test" {
5996    components {
5997        R1: resistor(4k7)
5998        R2: resistor(2k2)
5999        R3: resistor(4R7)
6000        C1: cap(2u5)
6001        C2: cap(4n7)
6002        L1: inductor(4m7)
6003    }
6004    nets {
6005        in -> R1.a
6006        R1.b -> R2.a
6007        R2.b -> R3.a
6008        R3.b -> C1.a
6009        C1.b -> C2.a
6010        C2.b -> L1.a
6011        L1.b -> out
6012    }
6013}
6014"#;
6015        let def = parse_pedal_file(src).unwrap();
6016        assert_eq!(def.components.len(), 6);
6017    }
6018
6019    #[test]
6020    fn parse_subcircuit_basic() {
6021        let src = r#"
6022equipment "Test Compressor" {
6023    supply 30V
6024
6025    subcircuit sidechain {
6026        rate: 1/64
6027
6028        components {
6029            R1: resistor(10k)
6030            C1: cap(1u)
6031        }
6032        nets {
6033            in -> R1.a
6034            R1.b -> C1.a, out
6035            C1.b -> gnd
6036        }
6037    }
6038
6039    subcircuit audio {
6040        components {
6041            R2: resistor(1k)
6042        }
6043        nets {
6044            in -> R2.a
6045            R2.b -> out
6046        }
6047    }
6048
6049    nets {
6050        in -> audio.in, sidechain.in
6051        sidechain.out -> audio.ctrl
6052        audio.out -> out
6053    }
6054}
6055"#;
6056        let def = parse_pedal_file(src).unwrap();
6057        assert_eq!(def.subcircuits.len(), 2);
6058        assert_eq!(def.subcircuits[0].name, "sidechain");
6059        assert_eq!(def.subcircuits[0].rate, Some(64));
6060        assert_eq!(def.subcircuits[1].name, "audio");
6061        assert_eq!(def.subcircuits[1].rate, None);
6062
6063        // Top-level nets should have SubcircuitPort pins after resolution
6064        let has_sc_port = def.nets.iter().any(|n| {
6065            n.to.iter().any(
6066                |p| matches!(p, Pin::SubcircuitPort { subcircuit, .. } if subcircuit == "audio"),
6067            )
6068        });
6069        assert!(
6070            has_sc_port,
6071            "top-level nets should contain SubcircuitPort after resolution"
6072        );
6073    }
6074
6075    #[test]
6076    fn parse_subcircuit_with_controls() {
6077        let src = r#"
6078equipment "Test" {
6079    supply 9V
6080
6081    subcircuit main {
6082        components {
6083            Vol: pot(100k, a)
6084            R1: resistor(1k)
6085        }
6086        nets {
6087            in -> R1.a
6088            R1.b -> Vol.a
6089            Vol.w -> out
6090            Vol.b -> gnd
6091        }
6092        controls {
6093            Vol.position -> "Volume" [0.0, 1.0] = 0.5
6094        }
6095    }
6096
6097    nets {
6098        in -> main.in
6099        main.out -> out
6100    }
6101}
6102"#;
6103        let def = parse_pedal_file(src).unwrap();
6104        assert_eq!(def.subcircuits.len(), 1);
6105        assert_eq!(def.subcircuits[0].controls.len(), 1);
6106        assert_eq!(def.subcircuits[0].controls[0].label, "Volume");
6107    }
6108
6109    #[test]
6110    fn parse_rate_power_of_two() {
6111        // Valid rates
6112        for rate_str in &[
6113            "1/2", "1/4", "1/8", "1/16", "1/32", "1/64", "1/128", "1/256",
6114        ] {
6115            let src = format!("rate: {}", rate_str);
6116            assert!(
6117                parse_rate(&src).is_ok(),
6118                "rate {} should be valid",
6119                rate_str
6120            );
6121        }
6122    }
6123
6124    #[test]
6125    fn parse_no_subcircuits_backward_compat() {
6126        // Existing flat pedal syntax should still work
6127        let src = r#"
6128pedal "Simple" {
6129    supply 9V
6130    components {
6131        R1: resistor(1k)
6132    }
6133    nets {
6134        in -> R1.a
6135        R1.b -> out
6136    }
6137}
6138"#;
6139        let def = parse_pedal_file(src).unwrap();
6140        assert!(def.subcircuits.is_empty());
6141        assert_eq!(def.components.len(), 1);
6142    }
6143
6144    // ── Ports parser ────────────────────────────────────────────────────
6145
6146    #[test]
6147    fn parse_ports_section() {
6148        let src = r#"pedal "test" {
6149    supply 9V
6150    ports {
6151        audio_in: input
6152        cv_cutoff: input
6153        audio_out: output
6154        env_out: output
6155    }
6156    components { R1: resistor(10k) }
6157    nets { audio_in -> R1.a  R1.b -> audio_out }
6158    controls {}
6159}"#;
6160        let def = parse_pedal_file(src).unwrap();
6161        assert_eq!(def.ports.len(), 4, "should have 4 ports");
6162        assert_eq!(def.ports[0].name, "audio_in");
6163        assert_eq!(def.ports[0].direction, pedalkernel_rt::PortDirection::Input);
6164        assert_eq!(def.ports[1].name, "cv_cutoff");
6165        assert_eq!(def.ports[1].direction, pedalkernel_rt::PortDirection::Input);
6166        assert_eq!(def.ports[2].name, "audio_out");
6167        assert_eq!(
6168            def.ports[2].direction,
6169            pedalkernel_rt::PortDirection::Output
6170        );
6171        assert_eq!(def.ports[3].name, "env_out");
6172        assert_eq!(
6173            def.ports[3].direction,
6174            pedalkernel_rt::PortDirection::Output
6175        );
6176    }
6177
6178    #[test]
6179    fn parse_no_ports_section() {
6180        let src = r#"pedal "test" {
6181    supply 9V
6182    components { R1: resistor(10k) }
6183    nets { in -> R1.a  R1.b -> out }
6184    controls {}
6185}"#;
6186        let def = parse_pedal_file(src).unwrap();
6187        assert!(
6188            def.ports.is_empty(),
6189            "should have no ports when section omitted"
6190        );
6191    }
6192
6193    #[test]
6194    fn ports_used_in_nets() {
6195        // Port names should be usable in nets as reserved nodes
6196        let src = r#"pedal "test" {
6197    supply 9V
6198    ports {
6199        audio_in: input
6200        cv_cutoff: input
6201        audio_out: output
6202    }
6203    components {
6204        R1: resistor(10k)
6205        R_cv: resistor(100k)
6206    }
6207    nets {
6208        audio_in -> R1.a
6209        cv_cutoff -> R_cv.a
6210        R_cv.b -> R1.a
6211        R1.b -> audio_out
6212    }
6213    controls {}
6214}"#;
6215        let def = parse_pedal_file(src).unwrap();
6216        assert_eq!(def.ports.len(), 3);
6217        // Port names should appear in nets as reserved pins
6218        let has_audio_in = def.nets.iter().any(|n| {
6219            matches!(&n.from, Pin::Reserved(s) if s == "audio_in")
6220                || n.to
6221                    .iter()
6222                    .any(|p| matches!(p, Pin::Reserved(s) if s == "audio_in"))
6223        });
6224        assert!(has_audio_in, "audio_in should be in nets as reserved pin");
6225    }
6226
6227    // ── Port impedance syntax ───────────────────────────────────────────
6228
6229    #[test]
6230    fn parse_port_with_impedance() {
6231        let src = r#"pedal "test" {
6232    supply 9V
6233    ports {
6234        audio_in: input(10k)
6235        cv_cutoff: input(47k)
6236        audio_out: output(600)
6237    }
6238    components { R1: resistor(10k) }
6239    nets { audio_in -> R1.a  R1.b -> audio_out }
6240    controls {}
6241}"#;
6242        let def = parse_pedal_file(src).unwrap();
6243        assert_eq!(def.ports.len(), 3);
6244        assert_eq!(def.ports[0].name, "audio_in");
6245        assert_eq!(def.ports[0].impedance, Some(10_000.0));
6246        assert_eq!(def.ports[1].name, "cv_cutoff");
6247        assert_eq!(def.ports[1].impedance, Some(47_000.0));
6248        assert_eq!(def.ports[2].name, "audio_out");
6249        assert_eq!(def.ports[2].impedance, Some(600.0));
6250    }
6251
6252    #[test]
6253    fn parse_port_without_impedance_backward_compat() {
6254        let src = r#"pedal "test" {
6255    supply 9V
6256    ports {
6257        audio_in: input
6258        audio_out: output
6259    }
6260    components { R1: resistor(10k) }
6261    nets { audio_in -> R1.a  R1.b -> audio_out }
6262    controls {}
6263}"#;
6264        let def = parse_pedal_file(src).unwrap();
6265        assert_eq!(def.ports.len(), 2);
6266        assert_eq!(def.ports[0].impedance, None);
6267        assert_eq!(def.ports[1].impedance, None);
6268    }
6269
6270    #[test]
6271    fn parse_op_block_with_node_voltage_seeds() {
6272        // `op { Q1: { vbe, vce }  nodes { name: v, ... } }` — device-port seeds
6273        // PLUS reactive-port (capacitor) node-voltage seeds.
6274        let src = r#"pedal "test" {
6275    supply 24V
6276    components { Q1: npn(BC184C)  R1: resistor(10k)  C1: cap(10u) }
6277    nets { in -> C1.a  C1.b -> Q1.base, R1.a  R1.b -> gnd  Q1.collector -> out  Q1.emitter -> gnd }
6278    controls {}
6279    op {
6280        Q1: { vbe: 0.64, vce: 5.35 }
6281        nodes {
6282            Q1.base: 1.0308
6283            Q1.collector: 5.7426
6284            C1.a: 0.0
6285        }
6286    }
6287}"#;
6288        let def = parse_pedal_file(src).unwrap();
6289        let explicit: Vec<_> = def
6290            .init_hints
6291            .iter()
6292            .filter(|h| matches!(h.state, InitState::Explicit { .. }))
6293            .collect();
6294        let nodes: Vec<_> = def
6295            .init_hints
6296            .iter()
6297            .filter(|h| matches!(h.state, InitState::NodeVoltage { .. }))
6298            .collect();
6299        assert_eq!(explicit.len(), 1, "one Explicit device seed");
6300        assert_eq!(nodes.len(), 3, "three NodeVoltage seeds");
6301        let qb = nodes
6302            .iter()
6303            .find(|h| h.device_label == "Q1.base")
6304            .expect("Q1.base node seed");
6305        assert!(matches!(qb.state, InitState::NodeVoltage { v } if (v - 1.0308).abs() < 1e-6));
6306    }
6307
6308    #[test]
6309    fn parse_op_block_without_nodes_still_works() {
6310        // Back-compat: an `op { }` block with only device seeds (no nodes block).
6311        let src = r#"pedal "test" {
6312    supply 24V
6313    components { Q1: npn(BC184C) }
6314    nets { in -> Q1.base  Q1.collector -> out  Q1.emitter -> gnd }
6315    controls {}
6316    op { Q1: { vbe: 0.64, vce: 5.35 } }
6317}"#;
6318        let def = parse_pedal_file(src).unwrap();
6319        assert_eq!(def.init_hints.len(), 1);
6320        assert!(matches!(def.init_hints[0].state, InitState::Explicit { .. }));
6321    }
6322}