Skip to main content

rill_core/traits/
port.rs

1//! Port types and identifiers for the Rill ecosystem
2//!
3//! Ports are the connection points between nodes in the signal graph.
4//! Each output port owns a `FixedBuffer<T, BUF_SIZE>` and an optional `Action`
5//! that defines how data is produced. Input ports are connection endpoints
6//! that receive data from upstream output ports.
7
8use crate::buffer::{Buffer, FixedBuffer};
9use crate::math::Transcendental;
10use crate::time::ClockTick;
11use crate::traits::algorithm::Algorithm;
12use crate::traits::node::NodeId;
13use crate::traits::processable::Processable;
14use crate::traits::PortError;
15use crate::traits::{ProcessResult, SignalNode};
16use std::fmt;
17
18// ============================================================================
19// Port Type
20// ============================================================================
21
22/// Type of a port - what kind of signal it carries
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum PortType {
25    /// Signal port - carries signal blocks (audio, sensor, etc.)
26    Signal,
27
28    /// Control signal port - carries modulation/automation
29    Control,
30
31    /// Clock signal port - carries timing information
32    Clock,
33
34    /// Feedback port - stores state between blocks
35    Feedback,
36
37    /// Parameter port - for node parameters (special)
38    Param,
39}
40
41impl PortType {
42    /// Get the name of the port type
43    pub const fn name(&self) -> &'static str {
44        match self {
45            Self::Signal => "signal",
46            Self::Control => "control",
47            Self::Clock => "clock",
48            Self::Feedback => "feedback",
49            Self::Param => "param",
50        }
51    }
52
53    /// Check if this port carries audio-rate signals
54    pub const fn is_audio_rate(&self) -> bool {
55        matches!(self, Self::Signal)
56    }
57
58    /// Check if this port carries control-rate signals
59    pub const fn is_control_rate(&self) -> bool {
60        matches!(self, Self::Control)
61    }
62
63    /// Check if this port carries clock signals
64    pub const fn is_clock(&self) -> bool {
65        matches!(self, Self::Clock)
66    }
67}
68
69impl fmt::Display for PortType {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(f, "{}", self.name())
72    }
73}
74
75// ============================================================================
76// Port Direction
77// ============================================================================
78
79/// Direction of a port (input or output)
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
81pub enum PortDirection {
82    /// Input port (receives data into the node)
83    Input,
84
85    /// Output port (sends data out of the node)
86    Output,
87}
88
89impl PortDirection {
90    /// Get the name of the direction
91    pub const fn name(&self) -> &'static str {
92        match self {
93            Self::Input => "input",
94            Self::Output => "output",
95        }
96    }
97
98    /// Check if this is an input port
99    pub const fn is_input(&self) -> bool {
100        matches!(self, Self::Input)
101    }
102
103    /// Check if this is an output port
104    pub const fn is_output(&self) -> bool {
105        matches!(self, Self::Output)
106    }
107}
108
109impl fmt::Display for PortDirection {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        write!(f, "{}", self.name())
112    }
113}
114
115// ============================================================================
116// Port ID
117// ============================================================================
118
119/// Unique identifier for a port within a graph
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
121pub struct PortId {
122    node: NodeId,
123    port_type: PortType,
124    direction: PortDirection,
125    index: u16,
126}
127
128impl PortId {
129    /// Create a new port ID
130    pub const fn new(
131        node: NodeId,
132        port_type: PortType,
133        direction: PortDirection,
134        index: u16,
135    ) -> Self {
136        Self {
137            node,
138            port_type,
139            direction,
140            index,
141        }
142    }
143
144    // ========================================================================
145    // Signal Port Constructors
146    // ========================================================================
147
148    /// Create a new signal input port
149    pub const fn signal_in(node: NodeId, index: u16) -> Self {
150        Self::new(node, PortType::Signal, PortDirection::Input, index)
151    }
152
153    /// Create a new signal output port
154    pub const fn signal_out(node: NodeId, index: u16) -> Self {
155        Self::new(node, PortType::Signal, PortDirection::Output, index)
156    }
157
158    // ========================================================================
159    // Control Port Constructors
160    // ========================================================================
161
162    /// Create a new control input port
163    pub const fn control_in(node: NodeId, index: u16) -> Self {
164        Self::new(node, PortType::Control, PortDirection::Input, index)
165    }
166
167    /// Create a new control output port
168    pub const fn control_out(node: NodeId, index: u16) -> Self {
169        Self::new(node, PortType::Control, PortDirection::Output, index)
170    }
171
172    // ========================================================================
173    // Clock Port Constructors
174    // ========================================================================
175
176    /// Create a new clock input port
177    pub const fn clock_in(node: NodeId, index: u16) -> Self {
178        Self::new(node, PortType::Clock, PortDirection::Input, index)
179    }
180
181    /// Create a new clock output port
182    pub const fn clock_out(node: NodeId, index: u16) -> Self {
183        Self::new(node, PortType::Clock, PortDirection::Output, index)
184    }
185
186    // ========================================================================
187    // Feedback Port Constructors
188    // ========================================================================
189
190    /// Create a new feedback input port
191    pub const fn feedback_in(node: NodeId, index: u16) -> Self {
192        Self::new(node, PortType::Feedback, PortDirection::Input, index)
193    }
194
195    /// Create a new feedback output port
196    pub const fn feedback_out(node: NodeId, index: u16) -> Self {
197        Self::new(node, PortType::Feedback, PortDirection::Output, index)
198    }
199
200    // ========================================================================
201    // Parameter Port Constructors
202    // ========================================================================
203
204    /// Create a new parameter port (always input)
205    pub const fn param(node: NodeId, index: u16) -> Self {
206        Self::new(node, PortType::Param, PortDirection::Input, index)
207    }
208
209    // ========================================================================
210    // Getters
211    // ========================================================================
212
213    /// Get the node ID
214    pub const fn node_id(&self) -> NodeId {
215        self.node
216    }
217
218    /// Get the port type
219    pub const fn port_type(&self) -> PortType {
220        self.port_type
221    }
222
223    /// Get the port direction
224    pub const fn direction(&self) -> PortDirection {
225        self.direction
226    }
227
228    /// Get the port index
229    pub const fn index(&self) -> u16 {
230        self.index
231    }
232
233    // ========================================================================
234    // Predicates
235    // ========================================================================
236
237    /// Check if this is an input port
238    pub const fn is_input(&self) -> bool {
239        self.direction.is_input()
240    }
241
242    /// Check if this is an output port
243    pub const fn is_output(&self) -> bool {
244        self.direction.is_output()
245    }
246
247    /// Check if this is an audio port
248    pub const fn is_signal(&self) -> bool {
249        matches!(self.port_type, PortType::Signal)
250    }
251
252    /// Check if this is a control port
253    pub const fn is_control(&self) -> bool {
254        matches!(self.port_type, PortType::Control)
255    }
256
257    /// Check if this is a clock port
258    pub const fn is_clock(&self) -> bool {
259        matches!(self.port_type, PortType::Clock)
260    }
261
262    /// Check if this is a feedback port
263    pub const fn is_feedback(&self) -> bool {
264        matches!(self.port_type, PortType::Feedback)
265    }
266
267    /// Check if this is a parameter port
268    pub const fn is_param(&self) -> bool {
269        matches!(self.port_type, PortType::Param)
270    }
271}
272
273impl fmt::Display for PortId {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        write!(
276            f,
277            "Node({}).{}_{}[{}]",
278            self.node.inner(),
279            self.port_type.name(),
280            self.direction.name(),
281            self.index
282        )
283    }
284}
285
286// ============================================================================
287// Port Structure
288// ============================================================================
289
290/// A port on a node.
291///
292/// Each port has an owned `FixedBuffer<T, BUF_SIZE>` for its data and an optional
293/// `Action` that defines per-port processing. Output ports typically have
294/// an action; input ports may have one for preprocessing.
295///
296/// Ports can optionally participate in feedback edges:
297/// - On an output port in a feedback edge, `feedback_buffer` stores the
298///   previous block's output, snapshotted after DSP via `snapshot_feedback()`.
299/// - On an input port in a feedback edge, `feedback_buffer` holds the delayed
300///   feedback value that gets mixed into `buffer` by `pre_process()`.
301/// - `downstream` lists audio connections from this output port to input ports
302///   of other nodes, populated at build time by the graph builder.
303/// - `upstream_buffer` on input ports: direct pointer to the upstream output
304///   port's buffer for zero-copy routing. `None` for fan-in/feedback ports.
305///
306/// # Safety
307/// `upstream_buffer` is safe because the graph topology is immutable and
308/// processing is strictly single-threaded in topological order. The
309/// upstream output buffer is guaranteed to outlive the downstream input
310/// port that references it.
311pub struct Port<T: Transcendental, const BUF_SIZE: usize> {
312    /// Port identifier
313    pub id: PortId,
314    /// Port name
315    pub name: String,
316    /// Port direction (input/output)
317    pub direction: PortDirection,
318    /// Per-port processing algorithm (None for simple input ports)
319    pub action: Option<Box<dyn Algorithm<T>>>,
320    /// Pending command value from the control path
321    pub pending_command: Option<T>,
322    /// Owned audio buffer (for output ports and input ports without upstream)
323    pub buffer: FixedBuffer<T, BUF_SIZE>,
324    /// Delayed feedback state (None if not on a feedback edge)
325    pub feedback_buffer: Option<FixedBuffer<T, BUF_SIZE>>,
326    /// Downstream audio connections: (target_node_index, target_port_index).
327    /// Used for serialization and by `GraphBuilder::build()`.
328    pub downstream: Vec<(usize, usize)>,
329    /// Direct pointers to downstream input ports. Filled by
330    /// `GraphBuilder::build()`. Used by `propagate` to copy data.
331    pub downstream_input_ptrs: Vec<*mut Port<T, BUF_SIZE>>,
332    /// Unique downstream nodes (one per target, deduplicated at build time).
333    /// Filled by `GraphBuilder::build()`. Used by `propagate` to recurse
334    /// into downstream nodes — no runtime deduplication needed.
335    pub downstream_nodes: Vec<*mut crate::traits::NodeVariant<T, BUF_SIZE>>,
336    /// Pointer to the [`NodeVariant`](crate::traits::NodeVariant) that
337    /// owns this port. Set after graph construction. Enables recursive
338    /// signal propagation without a `nodes` slice.
339    pub parent: *mut crate::traits::NodeVariant<T, BUF_SIZE>,
340    /// Direct pointer to upstream output buffer for zero-copy routing.
341    /// `Some` for input ports in 1:1 or fan-out connections (first upstream).
342    /// `None` for output ports, fan-in (second+ upstream), or unconnected.
343    /// Valid for the engine's lifetime.
344    pub upstream_buffer: Option<*const FixedBuffer<T, BUF_SIZE>>,
345    /// Feedback edge targets from this output port (for serialization)
346    pub feedback_downstream: Vec<(usize, usize)>,
347
348    /// Direct pointers to `feedback_buffer` on downstream input ports.
349    ///
350    /// Set by `GraphBuilder::build()` for feedback edges.
351    /// `snapshot_feedback()` copies its buffer into each target.
352    pub feedback_ptrs: Vec<*mut Option<FixedBuffer<T, BUF_SIZE>>>,
353}
354
355impl<T: Transcendental, const BUF_SIZE: usize> fmt::Debug for Port<T, BUF_SIZE> {
356    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
357        f.debug_struct("Port")
358            .field("id", &self.id)
359            .field("name", &self.name)
360            .field("direction", &self.direction)
361            .field("has_action", &self.action.is_some())
362            .field("has_feedback", &self.feedback_buffer.is_some())
363            .field("downstream_len", &self.downstream.len())
364            .finish()
365    }
366}
367
368impl<T: Transcendental, const BUF_SIZE: usize> Port<T, BUF_SIZE> {
369    /// Create a new signal output port
370    pub fn output(node_id: NodeId, index: u16, name: &str) -> Self {
371        Self {
372            id: PortId::signal_out(node_id, index),
373            name: name.to_string(),
374            direction: PortDirection::Output,
375            action: None,
376            pending_command: None,
377            buffer: FixedBuffer::new(),
378            feedback_buffer: None,
379            downstream: Vec::new(),
380            feedback_downstream: Vec::new(),
381            feedback_ptrs: Vec::new(),
382            downstream_input_ptrs: Vec::new(),
383            downstream_nodes: Vec::new(),
384            parent: std::ptr::null_mut(),
385            upstream_buffer: None,
386        }
387    }
388
389    /// Create a new signal input port
390    pub fn input(node_id: NodeId, index: u16, name: &str) -> Self {
391        Self {
392            id: PortId::signal_in(node_id, index),
393            name: name.to_string(),
394            direction: PortDirection::Input,
395            action: None,
396            pending_command: None,
397            buffer: FixedBuffer::new(),
398            feedback_buffer: None,
399            downstream: Vec::new(),
400            feedback_downstream: Vec::new(),
401            feedback_ptrs: Vec::new(),
402            downstream_input_ptrs: Vec::new(),
403            downstream_nodes: Vec::new(),
404            parent: std::ptr::null_mut(),
405            upstream_buffer: None,
406        }
407    }
408
409    /// Create a new control output port
410    pub fn control_output(node_id: NodeId, index: u16, name: &str) -> Self {
411        Self {
412            id: PortId::control_out(node_id, index),
413            name: name.to_string(),
414            direction: PortDirection::Output,
415            action: None,
416            pending_command: None,
417            buffer: FixedBuffer::new(),
418            feedback_buffer: None,
419            downstream: Vec::new(),
420            feedback_downstream: Vec::new(),
421            feedback_ptrs: Vec::new(),
422            downstream_input_ptrs: Vec::new(),
423            downstream_nodes: Vec::new(),
424            parent: std::ptr::null_mut(),
425            upstream_buffer: None,
426        }
427    }
428
429    /// Create a new control output port with an algorithm
430    pub fn control_output_with_action(
431        node_id: NodeId,
432        index: u16,
433        name: &str,
434        action: Box<dyn Algorithm<T>>,
435    ) -> Self {
436        Self {
437            id: PortId::control_out(node_id, index),
438            name: name.to_string(),
439            direction: PortDirection::Output,
440            action: Some(action),
441            pending_command: None,
442            buffer: FixedBuffer::new(),
443            feedback_buffer: None,
444            downstream: Vec::new(),
445            feedback_downstream: Vec::new(),
446            feedback_ptrs: Vec::new(),
447            downstream_input_ptrs: Vec::new(),
448            downstream_nodes: Vec::new(),
449            parent: std::ptr::null_mut(),
450            upstream_buffer: None,
451        }
452    }
453
454    /// Create a new control input port
455    pub fn control_input(node_id: NodeId, index: u16, name: &str) -> Self {
456        Self {
457            id: PortId::control_in(node_id, index),
458            name: name.to_string(),
459            direction: PortDirection::Input,
460            action: None,
461            pending_command: None,
462            buffer: FixedBuffer::new(),
463            feedback_buffer: None,
464            downstream: Vec::new(),
465            feedback_downstream: Vec::new(),
466            feedback_ptrs: Vec::new(),
467            downstream_input_ptrs: Vec::new(),
468            downstream_nodes: Vec::new(),
469            parent: std::ptr::null_mut(),
470            upstream_buffer: None,
471        }
472    }
473
474    /// Get the port ID
475    pub fn id(&self) -> PortId {
476        self.id
477    }
478
479    /// Get the port name
480    pub fn name(&self) -> &str {
481        &self.name
482    }
483
484    /// Check if port is an input
485    pub fn is_input(&self) -> bool {
486        self.direction.is_input()
487    }
488
489    /// Check if port is an output
490    pub fn is_output(&self) -> bool {
491        self.direction.is_output()
492    }
493
494    /// Get a reference to the buffer
495    pub fn buffer(&self) -> &FixedBuffer<T, BUF_SIZE> {
496        &self.buffer
497    }
498
499    /// Get a mutable reference to the buffer
500    pub fn buffer_mut(&mut self) -> &mut FixedBuffer<T, BUF_SIZE> {
501        &mut self.buffer
502    }
503
504    /// Get the effective signal buffer for this port.
505    ///
506    /// For zero-copy input ports returns the upstream output buffer.
507    /// For output ports and copy-based input ports returns the local buffer.
508    #[allow(unsafe_code)]
509    pub fn signal_buffer(&self) -> &FixedBuffer<T, BUF_SIZE> {
510        match self.upstream_buffer {
511            Some(ptr) => unsafe { &*ptr },
512            None => &self.buffer,
513        }
514    }
515
516    /// Pre-process this port before node DSP.
517    ///
518    /// For input ports on a feedback edge, mixes the delayed feedback
519    /// (from `feedback_buffer`) into the current `buffer`.
520    /// No-op when `feedback_buffer` is `None`.
521    ///
522    /// `tick` is the current clock tick, available for future
523    /// sample-accurate or time-varying port-level processing.
524    pub fn pre_process(&mut self, _tick: &ClockTick) {
525        if let Some(ref fb) = self.feedback_buffer {
526            let arr = self.buffer.as_mut_array();
527            let fb_arr = fb.as_array();
528            for (v, &s) in arr.iter_mut().zip(fb_arr.iter()) {
529                *v += s;
530            }
531        }
532    }
533
534    /// Snapshot the buffer into `feedback_buffer` and propagate to
535    /// downstream input ports via `feedback_ptrs`.
536    ///
537    /// For output ports on a feedback edge, saves the current buffer
538    /// so it can be used as delayed feedback in the next block, then
539    /// copies it into each target input port's `feedback_buffer`.
540    /// No-op when `feedback_buffer` is `None`.
541    #[allow(unsafe_code)]
542    pub fn snapshot_feedback(&mut self) {
543        if let Some(ref mut fb) = self.feedback_buffer {
544            fb.copy_from(self.buffer.as_array());
545            for &ptr in &self.feedback_ptrs {
546                unsafe {
547                    if let Some(ref mut target) = *ptr {
548                        target.copy_from(fb.as_array());
549                    }
550                }
551            }
552        }
553    }
554
555    /// Propagate this port's buffer to all downstream input ports.
556    ///
557    /// Iterates over `downstream` and copies `buffer` into each target
558    /// input port's buffer. The caller must ensure no aliasing between
559    /// this port's node and any target node (guaranteed by DAG topology).
560    ///
561    /// `tick` is the current clock tick, available for future
562    /// sample-accurate or time-varying port-level propagation.
563    /// Copy `buffer` into every downstream input port (unless zero-copy),
564    /// run each port's algorithm, then process the downstream node and
565    /// recurse through its output ports.
566    ///
567    /// No heap allocations — `downstream_nodes` is pre‑filled at build time.
568    #[allow(unsafe_code)]
569    pub fn propagate(
570        &self,
571        buffer: &FixedBuffer<T, BUF_SIZE>,
572        ctx: &crate::traits::algorithm::ActionContext,
573    ) -> ProcessResult<()> {
574        for &ptr in &self.downstream_input_ptrs {
575            unsafe {
576                if (*ptr).upstream_buffer.is_none() {
577                    (*ptr).buffer.copy_from(buffer.as_array());
578                }
579                (*ptr).run_action(Some(buffer.as_array()), ctx)?;
580            }
581        }
582        let mut proc_ctx = crate::traits::processable::ProcessContext { clock: ctx.tick };
583        for &parent in &self.downstream_nodes {
584            unsafe {
585                let nv = &mut *parent;
586                // Pre-process input ports (feedback mix from previous block)
587                for pi in 0..nv.num_signal_inputs() {
588                    if let Some(p) = nv.input_port_mut(pi) {
589                        p.pre_process(ctx.tick);
590                    }
591                }
592                nv.process_block(&mut proc_ctx)?;
593                for po in 0..nv.num_signal_outputs() {
594                    if let Some(p) = nv.output_port_mut(po) {
595                        p.snapshot_feedback();
596                    }
597                }
598                for po in 0..nv.num_signal_outputs() {
599                    if let Some(p) = nv.output_port(po) {
600                        p.propagate(p.buffer(), ctx)?;
601                    }
602                }
603            }
604        }
605        Ok(())
606    }
607
608    /// Run the port's algorithm.
609    ///
610    /// Delivers any pending command via `Algorithm::apply_command()`, then
611    /// calls `Algorithm::process()` with the input and output slices.
612    /// When no algorithm is attached, the pending command value (if any)
613    /// is written directly into the buffer; otherwise input is passed
614    /// through or zero-filled.
615    pub fn run_action(
616        &mut self,
617        input: Option<&[T; BUF_SIZE]>,
618        ctx: &crate::traits::algorithm::ActionContext,
619    ) -> crate::traits::ProcessResult<()> {
620        match &mut self.action {
621            Some(action) => {
622                // Deliver any pending command to the algorithm
623                if let Some(cmd) = self.pending_command.take() {
624                    action.apply_command(cmd);
625                }
626                let input_slice = input.map(|arr| arr.as_slice());
627                action.process(input_slice, self.buffer.as_mut_slice(), ctx)
628            }
629            None => {
630                // No algorithm — use pending command value if set,
631                // otherwise pass through input or zero-fill.
632                if let Some(cmd) = self.pending_command.take() {
633                    self.buffer.fill(cmd);
634                } else if let Some(input_data) = input {
635                    self.buffer.copy_from(input_data);
636                } else {
637                    self.buffer.fill(T::ZERO);
638                }
639                Ok(())
640            }
641        }
642    }
643
644    /// Set a command value for this port.
645    ///
646    /// The value is stored as a pending command and delivered to the
647    /// algorithm (or written directly to the buffer) on the next
648    /// `run_action()` call.
649    pub fn set_value(&mut self, value: T) {
650        self.pending_command = Some(value);
651    }
652}
653
654// ============================================================================
655// Active Port Trait
656// ============================================================================
657
658/// Trait for ports that can actively pull/push data.
659pub trait ActivePort<T: Transcendental, const BUF_SIZE: usize> {
660    /// Pull data from the port (for input ports).
661    fn pull(&mut self) -> Option<[T; BUF_SIZE]>;
662
663    /// Push data into the port (for output ports).
664    fn push(&mut self, data: [T; BUF_SIZE]) -> Result<(), PortError>;
665
666    /// Check if the port is connected.
667    fn is_connected(&self) -> bool;
668
669    /// Called on each clock tick (optional).
670    fn on_tick(&mut self, _tick: &ClockTick) {}
671}
672
673impl<T: Transcendental, const BUF_SIZE: usize> ActivePort<T, BUF_SIZE> for Port<T, BUF_SIZE> {
674    #[inline]
675    fn pull(&mut self) -> Option<[T; BUF_SIZE]> {
676        if self.is_input() {
677            Some(*self.buffer.as_array())
678        } else {
679            None
680        }
681    }
682
683    #[inline]
684    fn push(&mut self, data: [T; BUF_SIZE]) -> Result<(), PortError> {
685        if self.is_output() {
686            self.buffer = FixedBuffer::from_array(data);
687            Ok(())
688        } else {
689            Err(PortError::NotFound(self.id.to_string()))
690        }
691    }
692
693    #[inline]
694    fn is_connected(&self) -> bool {
695        self.action.is_some()
696    }
697
698    #[inline]
699    fn on_tick(&mut self, _tick: &ClockTick) {}
700}
701
702// SAFETY: `upstream_buffer` is a raw pointer to a buffer owned by another
703// Port in the same static graph. The graph is immutable during processing
704// and runs single-threaded in topological order. The pointer target
705// outlives the pointer for the entire processing session.
706#[allow(unsafe_code)]
707unsafe impl<T: Transcendental + Send, const BUF_SIZE: usize> Send for Port<T, BUF_SIZE> {}
708#[allow(unsafe_code)]
709unsafe impl<T: Transcendental + Sync, const BUF_SIZE: usize> Sync for Port<T, BUF_SIZE> {}
710
711// ============================================================================
712// Tests
713// ============================================================================
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718
719    #[test]
720    fn test_port_id_creation() {
721        let node = NodeId(42);
722
723        let audio_in = PortId::signal_in(node, 0);
724        assert_eq!(audio_in.port_type(), PortType::Signal);
725        assert!(audio_in.is_input());
726
727        let clock_out = PortId::clock_out(node, 0);
728        assert_eq!(clock_out.port_type(), PortType::Clock);
729        assert!(clock_out.is_output());
730
731        let feedback_in = PortId::feedback_in(node, 0);
732        assert_eq!(feedback_in.port_type(), PortType::Feedback);
733        assert!(feedback_in.is_input());
734    }
735}