Skip to main content

wyrd/runtime_impl/
outbox.rs

1//! Host I/O surfaces for one loom frame: sense writes and act outbox.
2//!
3//! [`PortWriter`] is the only supported way to feed `SignalIn` senses on the
4//! hot path. [`Outbox`] exposes dense `SignalOut` samples, capped emits, and
5//! the exact number of emits rejected by the cap for host apply after settle.
6
7use crate::foundation::{Signal, SignalDomain, ONE, ZERO};
8
9use crate::runtime_impl::bind::Runtime;
10use crate::runtime_impl::error::HandleError;
11use crate::runtime_impl::handles::{CmdId, HostPathId, SenseId};
12
13/// One `SignalOut` level written during the last loom.
14#[derive(Copy, Clone, Debug)]
15pub struct SignalOutSample {
16    /// Dense id for the `SignalOut` host path (resolve once after bind).
17    pub path: HostPathId,
18    /// Signal level written by the loom this frame.
19    pub value: Signal,
20}
21
22/// One `EmitCommand` entry written since the last [`Runtime::begin_frame`]
23/// (subject to the per-frame emit cap).
24#[derive(Copy, Clone, Debug)]
25pub struct Emit {
26    /// Dense id for the interned `EmitCommand` name.
27    pub cmd: CmdId,
28    /// Optional payload sampled from the emit knot's `payload` port.
29    pub payload: Signal,
30}
31
32/// Borrowed view of this frame's acts and dropped-emit telemetry after loom.
33///
34/// Retained emits stay in loom write order. [`Self::dropped_emits`] reports
35/// how many later emits the configured cap rejected; both are reset by the
36/// next [`Runtime::begin_frame`].
37pub struct Outbox<'a> {
38    pub(crate) signals: &'a [SignalOutSample],
39    pub(crate) emits: &'a [Emit],
40    pub(crate) dropped_emits: usize,
41}
42
43impl Outbox<'_> {
44    /// SignalOut samples in loom write order.
45    pub fn signals(&self) -> &[SignalOutSample] {
46        self.signals
47    }
48
49    /// EmitCommand entries in loom write order (capped at bind).
50    pub fn emits(&self) -> &[Emit] {
51        self.emits
52    }
53
54    /// Number of emits rejected by the configured cap in this frame.
55    ///
56    /// The count saturates at [`usize::MAX`] and resets on
57    /// [`Runtime::begin_frame`].
58    pub fn dropped_emits(&self) -> usize {
59        self.dropped_emits
60    }
61}
62
63/// Mutable host handle for writing dense sense values before loom.
64pub struct PortWriter<'a> {
65    pub(crate) rt: &'a mut Runtime,
66}
67
68impl PortWriter<'_> {
69    /// Write a host sense by dense id (must be a `SignalIn` knot).
70    ///
71    /// # Errors
72    ///
73    /// Returns [`HandleError::InvalidSense`] if the id is out of range or not a sense.
74    #[inline]
75    pub fn set_sense(&mut self, id: SenseId, value: Signal) -> Result<(), HandleError> {
76        if id.owner != self.rt.owner {
77            return Err(HandleError::ForeignRuntime { handle: "sense" });
78        }
79        let i = usize::from(id.index);
80        let domain = match self.rt.knots.get(i).map(|k| &k.kind) {
81            Some(crate::foundation::KnotKind::SignalIn { domain }) => *domain,
82            _ => return Err(HandleError::InvalidSense { sense: id }),
83        };
84        if !domain_value_is_valid(domain, value) {
85            return Err(HandleError::DomainValue { sense: id, domain });
86        }
87        self.rt.sense_values[i] = value;
88        Ok(())
89    }
90}
91
92#[inline]
93pub(crate) fn domain_value_is_valid(domain: SignalDomain, value: Signal) -> bool {
94    match domain {
95        SignalDomain::Bool => value == ZERO || value == ONE,
96        SignalDomain::Level => {
97            #[cfg(feature = "signal-f32")]
98            {
99                value.is_finite()
100            }
101            #[cfg(feature = "signal-i32")]
102            {
103                true
104            }
105        }
106        SignalDomain::Count => {
107            #[cfg(feature = "signal-f32")]
108            {
109                value.is_finite()
110                    && value >= i32::MIN as f32
111                    && value < 2_147_483_648.0
112                    && value == (value as i32) as f32
113            }
114            #[cfg(feature = "signal-i32")]
115            {
116                true
117            }
118        }
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::authoring::Weave;
126    use crate::foundation::{KnotKind, ONE};
127
128    use crate::runtime_impl::bind::{BindOpts, Runtime};
129
130    #[test]
131    fn domain_values_are_checked_at_the_host_boundary() {
132        assert!(domain_value_is_valid(SignalDomain::Bool, ZERO));
133        assert!(domain_value_is_valid(SignalDomain::Bool, ONE));
134        assert!(!domain_value_is_valid(
135            SignalDomain::Bool,
136            crate::foundation::from_count(2)
137        ));
138
139        #[cfg(feature = "signal-f32")]
140        {
141            assert!(domain_value_is_valid(SignalDomain::Level, 0.25));
142            assert!(!domain_value_is_valid(SignalDomain::Level, f32::NAN));
143            assert!(domain_value_is_valid(SignalDomain::Count, 42.0));
144            assert!(!domain_value_is_valid(SignalDomain::Count, 1.5));
145            assert!(!domain_value_is_valid(
146                SignalDomain::Count,
147                -2_147_483_904.0,
148            ));
149            assert!(!domain_value_is_valid(SignalDomain::Count, 2_147_483_648.0));
150        }
151
152        #[cfg(feature = "signal-i32")]
153        {
154            assert!(domain_value_is_valid(SignalDomain::Level, i32::MAX));
155            assert!(domain_value_is_valid(SignalDomain::Count, i32::MIN));
156        }
157    }
158
159    #[test]
160    fn set_sense_oob_returns_error_without_mutation() {
161        let mut b = Weave::builder("x").unwrap();
162        let _k_c = b
163            .knot("c", KnotKind::constant(ONE, SignalDomain::Bool))
164            .unwrap();
165        let weave = b.build().unwrap();
166        let mut rt = Runtime::bind(weave.clone(), BindOpts::default()).unwrap();
167        let mut other_builder = Weave::builder("other").unwrap();
168        let _sense = other_builder
169            .knot("sense", KnotKind::signal_in(SignalDomain::Bool))
170            .unwrap();
171        let other = Runtime::bind(other_builder.build().unwrap(), BindOpts::default()).unwrap();
172        let invalid = other.sense_id("sense").unwrap();
173        assert_eq!(
174            rt.port_writer().set_sense(invalid, ONE),
175            Err(HandleError::ForeignRuntime { handle: "sense" })
176        );
177        assert!(rt.outbox().signals().is_empty());
178        assert!(rt.outbox().emits().is_empty());
179    }
180
181    #[test]
182    fn set_sense_rejects_same_runtime_non_sense_and_domain_values() {
183        let mut b = Weave::builder("domain-errors").unwrap();
184        let _constant = b
185            .knot("constant", KnotKind::constant(ONE, SignalDomain::Bool))
186            .unwrap();
187        let _bool_sense = b
188            .knot("bool", KnotKind::signal_in(SignalDomain::Bool))
189            .unwrap();
190        let _count_sense = b
191            .knot("count", KnotKind::signal_in(SignalDomain::Count))
192            .unwrap();
193        let mut rt = Runtime::bind(b.build().unwrap(), BindOpts::default()).unwrap();
194
195        // Knot insertion order is dense at bind: constant=0, bool=1, count=2.
196        let constant_as_sense = SenseId::new(rt.owner, 0);
197        assert_eq!(
198            rt.port_writer().set_sense(constant_as_sense, ONE),
199            Err(HandleError::InvalidSense {
200                sense: constant_as_sense
201            })
202        );
203
204        let bool_id = SenseId::new(rt.owner, 1);
205        assert_eq!(
206            rt.port_writer()
207                .set_sense(bool_id, crate::foundation::from_count(2)),
208            Err(HandleError::DomainValue {
209                sense: bool_id,
210                domain: SignalDomain::Bool,
211            })
212        );
213
214        #[cfg(feature = "signal-f32")]
215        {
216            let count_id = SenseId::new(rt.owner, 2);
217            assert_eq!(
218                rt.port_writer().set_sense(count_id, 1.5),
219                Err(HandleError::DomainValue {
220                    sense: count_id,
221                    domain: SignalDomain::Count,
222                })
223            );
224        }
225
226        #[cfg(feature = "signal-i32")]
227        {
228            let count_id = SenseId::new(rt.owner, 2);
229            rt.port_writer().set_sense(count_id, 1).unwrap();
230        }
231    }
232}