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