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}