Skip to main content

pedalkernel/
dsl_expand.rs

1//! File-based subcircuit expansion (flatten pass).
2//!
3//! Front-end only: turns a [`PedalDef`] containing `use("path.pedal")` instances
4//! (declared in the `components { ... }` block, collected into
5//! [`PedalDef::uses`]) into ONE flat `PedalDef` that the existing
6//! `compile_pedal` accepts unchanged. The WDF/SPQR compiler and runtime are
7//! never touched.
8//!
9//! A `use(...)` instance is FLATTENED into the parent's single component / net /
10//! control list (unlike inline `subcircuit { ... }` blocks, which partition the
11//! graph into separate rate-domain processors via
12//! [`crate::compiler::subcircuit`] — file-based uses must NOT route through
13//! that path).
14//!
15//! ## Namespacing
16//! For an instance `id`:
17//! - component `R3` → `id.R3`; every `ComponentPin { component, .. }` is
18//!   rewritten to `id.component`.
19//! - a bare internal node `Reserved(name)` (not `gnd`, not a supply rail, not a
20//!   declared port) → `Reserved("id.name")`.
21//! - `gnd` stays global.
22//! - a supply rail (`vcc` or a named supply of the sub) stays global by
23//!   default, UNLESS the parent explicitly maps `id.<rail>` to a node — then it
24//!   unifies with that node (implicit-global, overridable).
25//! - a declared port `P` (from the sub's `ports { }`, plus `in`/`out`) unifies
26//!   with whatever node the parent wired to `id.P`. A used-but-unmapped non-rail
27//!   port is a loud error.
28//!
29//! ## Controls
30//! A sub control on component `C` becomes a control on `id.C`. A parent control
31//! referencing `id.<subctrl>` re-targets/renames the namespaced sub control.
32
33use crate::compiler::components::{Capacitor, Inductor, Potentiometer, Resistor};
34use crate::dsl::{ControlDef, NetDef, PedalDef, Pin, UseInstance};
35use std::path::{Path, PathBuf};
36
37/// Default reserved rail/IO names that are always global (never namespaced) when
38/// they appear as bare `Reserved` nodes in a sub. `in`/`out` are NOT in here —
39/// they are treated as declared ports (boundary nodes) and unified with the
40/// parent.
41const GLOBAL_GND: &str = "gnd";
42
43/// Reserved names that, when used as a sub's internal node, stay global as a
44/// shared rail unless explicitly mapped by the parent.
45const DEFAULT_RAILS: &[&str] = &["vcc"];
46
47/// Expand all file-based `use(...)` subcircuit instances in `pedal` into a
48/// single flat `PedalDef`.
49///
50/// `base_dir` is the directory the parent `.pedal` file lives in; `use(...)`
51/// paths are resolved relative to it. Nested subcircuits are expanded first
52/// (depth-first), using each sub file's own directory as its base.
53///
54/// Returns a flat pedal whose `uses` is empty. Idempotent for pedals with no
55/// `use(...)` instances (returns an equivalent clone).
56pub fn expand_uses(pedal: &PedalDef, base_dir: &Path) -> Result<PedalDef, String> {
57    let mut visited = Vec::new();
58    expand_inner(pedal, base_dir, &mut visited)
59}
60
61fn expand_inner(
62    pedal: &PedalDef,
63    base_dir: &Path,
64    visited: &mut Vec<PathBuf>,
65) -> Result<PedalDef, String> {
66    if pedal.uses.is_empty() {
67        // Nothing to flatten — return a clone with uses cleared (idempotent).
68        let mut out = pedal.clone();
69        out.uses.clear();
70        return Ok(out);
71    }
72
73    let mut out = pedal.clone();
74    let uses = std::mem::take(&mut out.uses);
75
76    // Collect parent-side port/rail mappings BEFORE merging, then drop the
77    // routing nets that are fully consumed by unification.
78    for inst in &uses {
79        merge_instance(&mut out, inst, base_dir, visited)?;
80    }
81
82    out.uses.clear();
83    Ok(out)
84}
85
86/// Resolve, parse, recursively expand, namespace and merge one instance.
87fn merge_instance(
88    parent: &mut PedalDef,
89    inst: &UseInstance,
90    base_dir: &Path,
91    visited: &mut Vec<PathBuf>,
92) -> Result<(), String> {
93    let UseInstance {
94        id,
95        path,
96        overrides: _,
97    } = inst;
98
99    // ---- Resolve + cycle check ---------------------------------------------
100    let resolved = base_dir.join(path);
101    let canonical = resolved
102        .canonicalize()
103        .map_err(|e| format!("subcircuit `{id}`: cannot resolve `{path}`: {e}"))?;
104    if visited.contains(&canonical) {
105        return Err(format!(
106            "subcircuit cycle detected: `{}` is already being expanded",
107            canonical.display()
108        ));
109    }
110
111    let src = std::fs::read_to_string(&canonical).map_err(|e| {
112        format!(
113            "subcircuit `{id}`: cannot read `{}`: {e}",
114            canonical.display()
115        )
116    })?;
117    let sub_raw = crate::dsl::parse_pedal_file(&src)
118        .map_err(|e| format!("subcircuit `{id}` (`{}`): {e}", canonical.display()))?;
119
120    // ---- Recurse: expand nested uses first ---------------------------------
121    let sub_dir = canonical
122        .parent()
123        .map(|p| p.to_path_buf())
124        .unwrap_or_else(|| PathBuf::from("."));
125    visited.push(canonical.clone());
126    let mut sub = expand_inner(&sub_raw, &sub_dir, visited)?;
127    visited.pop();
128
129    // ---- Apply component-value overrides -----------------------------------
130    // Each `(comp_id, new_value)` from the `with { }` block is applied to the
131    // matching sub ComponentDef before namespacing. Error loudly if any id has
132    // no matching component (Rule 12 — do not silently ignore).
133    for (ov_id, ov_val) in &inst.overrides {
134        // Collect available ids before the mutable borrow so the error message
135        // can list them without conflicting borrows.
136        let available: Vec<String> = sub.components.iter().map(|c| c.id.clone()).collect();
137
138        let comp = sub
139            .components
140            .iter_mut()
141            .find(|c| c.id == *ov_id)
142            .ok_or_else(|| {
143                format!(
144                    "subcircuit `{id}` (`{path}`): override `{ov_id}` does not match any \
145                     component in the sub (available: {})",
146                    available.join(", ")
147                )
148            })?;
149
150        let kind = comp.kind.as_mut();
151        if let Some(r) = kind.as_any_mut().downcast_mut::<Resistor>() {
152            r.value = *ov_val;
153        } else if let Some(c) = kind.as_any_mut().downcast_mut::<Capacitor>() {
154            c.config.value = *ov_val;
155        } else if let Some(l) = kind.as_any_mut().downcast_mut::<Inductor>() {
156            l.value = *ov_val;
157        } else if let Some(p) = kind.as_any_mut().downcast_mut::<Potentiometer>() {
158            p.max_r = *ov_val;
159        } else {
160            return Err(format!(
161                "subcircuit `{id}` (`{path}`): override `{ov_id}` targets a component type \
162                 that does not support value overrides (only resistor, cap, inductor, pot)"
163            ));
164        }
165    }
166
167    // ---- Classify the sub's reserved node names ----------------------------
168    // Declared ports: from `ports { }` plus implicit in/out.
169    let mut port_names: Vec<String> = sub.ports.iter().map(|p| p.name.clone()).collect();
170    for implicit in ["in", "out"] {
171        if !port_names.iter().any(|n| n == implicit) {
172            port_names.push(implicit.to_string());
173        }
174    }
175    // Rail names: the sub's declared supplies plus the default `vcc`.
176    let mut rail_names: Vec<String> = sub.supplies.iter().map(|s| s.name.clone()).collect();
177    for r in DEFAULT_RAILS {
178        if !rail_names.iter().any(|n| n == r) {
179            rail_names.push(r.to_string());
180        }
181    }
182
183    // ---- Gather parent mappings for this instance --------------------------
184    // For each parent net mentioning `SubcircuitPort { subcircuit: id, port }`,
185    // record the node the port unifies with, and drop the routing net.
186    let mut port_map: hashbrown::HashMap<String, Pin> = hashbrown::HashMap::new();
187    let mut kept_nets: Vec<NetDef> = Vec::with_capacity(parent.nets.len());
188
189    for net in std::mem::take(&mut parent.nets) {
190        if let Some((port, target, leftover)) = consume_routing_net(&net, id) {
191            // Unify this port with `target`. If the port already mapped to a
192            // node, keep the first; emit an explicit net joining the two so the
193            // extra parent endpoints aren't lost.
194            match port_map.get(&port).cloned() {
195                None => {
196                    port_map.insert(port.clone(), target.clone());
197                }
198                Some(existing) => {
199                    kept_nets.push(NetDef {
200                        from: existing,
201                        to: vec![target.clone()],
202                    });
203                }
204            }
205            // Any leftover endpoints (port wired to >1 node) are re-attached to
206            // the unified node.
207            if let Some(extra) = leftover {
208                let anchor = port_map.get(&port).cloned().unwrap_or(target);
209                kept_nets.push(NetDef {
210                    from: anchor,
211                    to: extra,
212                });
213            }
214        } else {
215            kept_nets.push(net);
216        }
217    }
218    parent.nets = kept_nets;
219
220    // ---- Validate: every used non-rail port must be mapped -----------------
221    // A port is "used" if the sub references it as a Reserved node.
222    let sub_used_reserved = collect_reserved_names(&sub);
223    for port in &port_names {
224        let is_rail = rail_names.iter().any(|r| r == port);
225        if is_rail {
226            continue;
227        }
228        if sub_used_reserved.contains(port.as_str()) && !port_map.contains_key(port) {
229            return Err(format!(
230                "subcircuit `{id}`: port `{port}` is used internally but the parent does not wire `{id}.{port}` to any node"
231            ));
232        }
233    }
234
235    // ---- Build the reserved-node rewrite map -------------------------------
236    let rewrite_reserved = |name: &str| -> Pin {
237        if name == GLOBAL_GND {
238            return Pin::Reserved(name.to_string());
239        }
240        // Explicit parent mapping (port OR rail override) wins.
241        if let Some(target) = port_map.get(name) {
242            return target.clone();
243        }
244        if rail_names.iter().any(|r| r == name) {
245            // Implicit-global rail.
246            return Pin::Reserved(name.to_string());
247        }
248        if port_names.iter().any(|p| p == name) {
249            // Declared, unused-but-unmapped port that is not referenced
250            // internally: harmless, leave as a namespaced node.
251            return Pin::Reserved(format!("{id}.{name}"));
252        }
253        // Bare internal node → namespace it.
254        Pin::Reserved(format!("{id}.{name}"))
255    };
256
257    let ns_pin = |pin: &Pin| -> Pin { namespace_pin(pin, id, &rewrite_reserved) };
258
259    // ---- Merge namespaced components ---------------------------------------
260    for comp in &sub.components {
261        let mut c = comp.clone();
262        c.id = format!("{id}.{}", comp.id);
263        parent.components.push(c);
264    }
265
266    // ---- Merge namespaced nets ---------------------------------------------
267    for net in &sub.nets {
268        parent.nets.push(NetDef {
269            from: ns_pin(&net.from),
270            to: net.to.iter().map(|p| ns_pin(p)).collect(),
271        });
272    }
273
274    // ---- Merge namespaced mirrors ------------------------------------------
275    for (k, v) in &sub.mirrors {
276        parent
277            .mirrors
278            .insert(format!("{id}.{k}"), format!("{id}.{v}"));
279    }
280
281    // ---- Merge namespaced controls -----------------------------------------
282    // Sub controls become controls on `id.C`. Parent controls that reference
283    // `id.<sub-control-component>` already point at the namespaced component
284    // after we rewrite the parent control component below.
285    for ctrl in &sub.controls {
286        parent.controls.push(ControlDef {
287            component: format!("{id}.{}", ctrl.component),
288            property: ctrl.property.clone(),
289            label: ctrl.label.clone(),
290            range: ctrl.range,
291            default: ctrl.default,
292        });
293    }
294    for trim in &sub.trims {
295        parent.trims.push(ControlDef {
296            component: format!("{id}.{}", trim.component),
297            property: trim.property.clone(),
298            label: trim.label.clone(),
299            range: trim.range,
300            default: trim.default,
301        });
302    }
303
304    // ---- Re-expose sub controls via parent controls ------------------------
305    // A parent control written as `id.SubCtrl -> "Label" ...` references the sub
306    // by instance id; its `component` is the instance `id` and `property` is the
307    // sub control's label/component. We resolve these to the namespaced sub
308    // control: drop the auto-merged sub control with the same target and keep
309    // the parent's (relabeled/ranged) one.
310    rebind_parent_controls(parent, id, &sub);
311
312    // ---- Merge supplies (dedupe by name; parent wins) ----------------------
313    for sup in &sub.supplies {
314        if !parent.supplies.iter().any(|p| p.name == sup.name) {
315            parent.supplies.push(sup.clone());
316        }
317    }
318
319    // ---- Merge init hints (namespaced device labels) -----------------------
320    for hint in &sub.init_hints {
321        let mut h = hint.clone();
322        h.device_label = format!("{id}.{}", hint.device_label);
323        parent.init_hints.push(h);
324    }
325
326    Ok(())
327}
328
329/// If `net` is a pure routing net for instance `id` (it mentions
330/// `SubcircuitPort { subcircuit: id, port }`), return
331/// `(port, unify_target, leftover_endpoints)` and signal the net should be
332/// dropped. The common 1:1 case (`X -> id.P` or `id.P -> X`) yields a single
333/// target and no leftover.
334///
335/// Returns `None` if the net does not reference this instance's ports (it is
336/// kept verbatim).
337fn consume_routing_net(net: &NetDef, id: &str) -> Option<(String, Pin, Option<Vec<Pin>>)> {
338    let from_port = sub_port_of(&net.from, id);
339    let to_ports: Vec<(usize, String)> = net
340        .to
341        .iter()
342        .enumerate()
343        .filter_map(|(i, p)| sub_port_of(p, id).map(|port| (i, port)))
344        .collect();
345
346    match (from_port, to_ports.is_empty()) {
347        (Some(port), true) => {
348            // `id.P -> [targets...]`: the targets are the unify node(s).
349            let mut targets = net.to.clone();
350            let first = targets.remove(0);
351            let leftover = if targets.is_empty() {
352                None
353            } else {
354                Some(targets)
355            };
356            Some((port, first, leftover))
357        }
358        (None, false) => {
359            // `from -> [.., id.P, ..]`: `from` is the unify node. Other
360            // non-port destinations become leftover (re-attached to the node).
361            let port = to_ports[0].1.clone();
362            let port_idxs: std::collections::HashSet<usize> =
363                to_ports.iter().map(|(i, _)| *i).collect();
364            let leftover: Vec<Pin> = net
365                .to
366                .iter()
367                .enumerate()
368                .filter(|(i, _)| !port_idxs.contains(i))
369                .map(|(_, p)| p.clone())
370                .collect();
371            let leftover = if leftover.is_empty() {
372                None
373            } else {
374                Some(leftover)
375            };
376            Some((port, net.from.clone(), leftover))
377        }
378        (Some(port), false) => {
379            // Both sides reference instance ports — rare; treat `from`'s port as
380            // the one mapped, target = first to-port's pin (already a
381            // SubcircuitPort, which is unusual). Fall back to dropping with the
382            // from side mapped to the first to endpoint.
383            let target = net.to[0].clone();
384            Some((port, target, None))
385        }
386        (None, true) => None,
387    }
388}
389
390/// Returns the port name if `pin` is `SubcircuitPort { subcircuit: id, .. }`.
391fn sub_port_of(pin: &Pin, id: &str) -> Option<String> {
392    match pin {
393        Pin::SubcircuitPort { subcircuit, port } if subcircuit == id => Some(port.clone()),
394        _ => None,
395    }
396}
397
398/// Namespace a single pin from a sub into the parent scope.
399fn namespace_pin(pin: &Pin, id: &str, rewrite_reserved: &dyn Fn(&str) -> Pin) -> Pin {
400    match pin {
401        Pin::Reserved(name) => rewrite_reserved(name),
402        Pin::ComponentPin { component, pin } => Pin::ComponentPin {
403            component: format!("{id}.{component}"),
404            pin: pin.clone(),
405        },
406        Pin::Fork {
407            switch,
408            destinations,
409        } => Pin::Fork {
410            switch: format!("{id}.{switch}"),
411            destinations: destinations
412                .iter()
413                .map(|d| namespace_pin(d, id, rewrite_reserved))
414                .collect(),
415        },
416        // A sub should not itself contain unexpanded SubcircuitPort references
417        // (nested uses are flattened before we get here). Pass through.
418        Pin::SubcircuitPort { subcircuit, port } => Pin::SubcircuitPort {
419            subcircuit: format!("{id}.{subcircuit}"),
420            port: port.clone(),
421        },
422    }
423}
424
425/// Collect the set of bare `Reserved` node names referenced anywhere in a sub's
426/// nets (used to decide whether an unmapped port is actually "used").
427fn collect_reserved_names(sub: &PedalDef) -> std::collections::HashSet<String> {
428    let mut set = std::collections::HashSet::new();
429    let mut visit = |pin: &Pin| {
430        if let Pin::Reserved(name) = pin {
431            set.insert(name.clone());
432        }
433    };
434    for net in &sub.nets {
435        collect_pin(&net.from, &mut visit);
436        for p in &net.to {
437            collect_pin(p, &mut visit);
438        }
439    }
440    set
441}
442
443fn collect_pin(pin: &Pin, f: &mut dyn FnMut(&Pin)) {
444    f(pin);
445    if let Pin::Fork { destinations, .. } = pin {
446        for d in destinations {
447            collect_pin(d, f);
448        }
449    }
450}
451
452/// Re-bind parent controls that reference instance `id`.
453///
454/// A parent control `id.X -> "Label" [..] = ..` (parsed as
455/// `component == id`, `property == X`) re-exposes a sub control. We match it to
456/// the auto-merged sub control whose namespaced component is `id.X` OR whose
457/// label is `X`, replace that sub control's label/range/default with the
458/// parent's, and remove the now-redundant parent control entry.
459fn rebind_parent_controls(parent: &mut PedalDef, id: &str, sub: &PedalDef) {
460    // At this point `parent.controls` is [original parent controls...,
461    // auto-merged sub controls...]. Separate the parent re-exposing controls
462    // (those whose `component == id`) from the merged sub controls, then fold
463    // each re-exposing control into the matching merged sub control.
464    let all = std::mem::take(&mut parent.controls);
465    let mut reexpose: Vec<ControlDef> = Vec::new();
466    let mut merged: Vec<ControlDef> = Vec::new();
467    for c in all {
468        if c.component == id {
469            reexpose.push(c);
470        } else {
471            merged.push(c);
472        }
473    }
474
475    for ctrl in reexpose {
476        // Parent re-exposes `id.<property>`. Match the merged sub control by
477        // namespaced component name OR by sub control label.
478        let want = &ctrl.property;
479        let ns_comp = format!("{id}.{want}");
480        if let Some(slot) = merged.iter_mut().find(|c| {
481            c.component == ns_comp
482                || sub
483                    .controls
484                    .iter()
485                    .any(|sc| &sc.label == want && format!("{id}.{}", sc.component) == c.component)
486        }) {
487            slot.label = ctrl.label.clone();
488            slot.range = ctrl.range;
489            slot.default = ctrl.default;
490        } else {
491            // No matching sub control — keep it so the issue surfaces rather
492            // than being silently dropped.
493            merged.push(ctrl);
494        }
495    }
496
497    parent.controls = merged;
498}
499
500/// Path-aware entry point: read `path`, parse, and expand its `use(...)`
501/// instances relative to the file's own directory.
502///
503/// This is the entry CLI/tests should use for file-path loads so `use(...)`
504/// works end-to-end. String-only callers continue to use
505/// [`crate::dsl::parse_pedal_file`] (which leaves `uses` un-expanded).
506pub fn parse_and_expand_pedal_file(path: &Path) -> Result<PedalDef, String> {
507    let src = std::fs::read_to_string(path)
508        .map_err(|e| format!("cannot read `{}`: {e}", path.display()))?;
509    let def = crate::dsl::parse_pedal_file(&src)?;
510    let base_dir = path.parent().unwrap_or_else(|| Path::new("."));
511    expand_uses(&def, base_dir)
512}