Skip to main content

wyrd/runtime_impl/
loom.rs

1//! Single-pass DAG settle: clear unwired ins, seed senses, topo eval, fill outbox.
2//!
3//! After a successful bind, loom is infallible and allocates no graph topology.
4//! Dispatch uses bind-time [`KindTag`]s; inbound edges use CSR tables. Stateful
5//! runes (Flag, Counter, Timer, Delay, edges) update per-knot storage here.
6
7use crate::foundation::{
8    is_truthy, CompareOp, FlagPriority, KnotId, PortSlot, Signal, SignalDomain, ONE, ZERO,
9};
10
11use crate::runtime_impl::bind::{Runtime, SenseSeed};
12use crate::runtime_impl::kind_tag::KindTag;
13
14#[allow(non_snake_case)]
15const fn PortSlot(value: u8) -> PortSlot {
16    PortSlot::new(value)
17}
18
19impl Runtime {
20    /// One settle pass. Never panics. No topology alloc after bind.
21    ///
22    /// Order: zero unwired inputs → seed Constant / SignalIn / OnStart →
23    /// topological eval of non-sense knots → acts append to the outbox.
24    pub fn loom(&mut self) {
25        for &idx in &self.clear_port_idx {
26            debug_assert!(idx < self.port_vals.len());
27            self.port_vals[idx] = ZERO;
28        }
29
30        let seed_n = self.sense_seeds.len();
31        for si in 0..seed_n {
32            match self.sense_seeds[si] {
33                SenseSeed::Constant { kid, value } => {
34                    self.set_port_hot(kid, PortSlot::new(0), value);
35                }
36                SenseSeed::SignalIn { kid } => {
37                    let v = self.sense_values[usize::from(kid)];
38                    self.set_port_hot(kid, PortSlot::new(0), v);
39                }
40                SenseSeed::OnStart { kid } => {
41                    let ki = usize::from(kid);
42                    let v = if !self.on_start_done[ki] {
43                        self.on_start_done[ki] = true;
44                        ONE
45                    } else {
46                        ZERO
47                    };
48                    self.set_port_hot(kid, PortSlot::new(0), v);
49                }
50            }
51        }
52
53        let topo_len = self.topo.len();
54        for ti in 0..topo_len {
55            let kid = self.topo[ti];
56            let ki = usize::from(kid);
57            if self.kind_tags[ki].is_sense() {
58                continue;
59            }
60            self.gather_inputs(kid);
61            self.eval_knot(kid);
62        }
63        // Phase 2 marks a completed loom pass; restore rejects checkpoints captured mid-settle.
64        self.phase = 2;
65    }
66
67    /// Copy inbound edge values into this knot's In ports.
68    ///
69    /// Uses a stack temp when fan-in > 2 so reads stay stable if a knot fans into
70    /// itself across slots (max ports per knot is 8).
71    fn gather_inputs(&mut self, kid: KnotId) {
72        let ki = usize::from(kid);
73        let start = self.inbound_off[ki] as usize;
74        let end = self.inbound_off[ki + 1] as usize;
75        let n = end - start;
76        if n == 0 {
77            return;
78        }
79        if n == 1 {
80            let (f, fs, ts) = self.inbound_edges[start];
81            let v = self.get_port_hot(f, fs);
82            self.set_port_hot(kid, ts, v);
83            return;
84        }
85        if n == 2 {
86            let (f0, fs0, ts0) = self.inbound_edges[start];
87            let (f1, fs1, ts1) = self.inbound_edges[start + 1];
88            let v0 = self.get_port_hot(f0, fs0);
89            let v1 = self.get_port_hot(f1, fs1);
90            self.set_port_hot(kid, ts0, v0);
91            self.set_port_hot(kid, ts1, v1);
92            return;
93        }
94        let mut tmp: [(PortSlot, Signal); 8] = [(PortSlot::new(0), ZERO); 8];
95        let n = n.min(8);
96        for (i, &(f, fs, ts)) in self.inbound_edges[start..start + n].iter().enumerate() {
97            tmp[i] = (ts, self.get_port_hot(f, fs));
98        }
99        for &(ts, v) in tmp.iter().take(n) {
100            self.set_port_hot(kid, ts, v);
101        }
102    }
103
104    fn eval_knot(&mut self, kid: KnotId) {
105        let ki = usize::from(kid);
106        let tag = self.kind_tags[ki];
107        match tag {
108            KindTag::Sense => {}
109            KindTag::Not => {
110                let i = self.get_port_hot(kid, PortSlot::new(0));
111                let o = if is_truthy(i) { ZERO } else { ONE };
112                self.set_port_hot(kid, PortSlot::new(1), o);
113            }
114            KindTag::And { arity } => {
115                let mut ok = true;
116                for s in 0..arity {
117                    if !is_truthy(self.get_port_hot(kid, PortSlot(s))) {
118                        ok = false;
119                        break;
120                    }
121                }
122                self.set_port_hot(kid, PortSlot(arity), if ok { ONE } else { ZERO });
123            }
124            KindTag::Or { arity } => {
125                let mut ok = false;
126                for s in 0..arity {
127                    if is_truthy(self.get_port_hot(kid, PortSlot(s))) {
128                        ok = true;
129                        break;
130                    }
131                }
132                self.set_port_hot(kid, PortSlot(arity), if ok { ONE } else { ZERO });
133            }
134            KindTag::RisingFromZero => {
135                let i = self.get_port_hot(kid, PortSlot::new(0));
136                let prev = self.prev_in[ki];
137                let o = if !is_truthy(prev) && is_truthy(i) {
138                    ONE
139                } else {
140                    ZERO
141                };
142                self.prev_in[ki] = i;
143                self.set_port_hot(kid, PortSlot::new(1), o);
144            }
145            KindTag::Compare { op, rhs_const } => {
146                let lhs = self.get_port_hot(kid, PortSlot::new(0));
147                let rhs = match rhs_const {
148                    Some(c) => c,
149                    None => self.get_port_hot(kid, PortSlot::new(1)),
150                };
151                let o = if compare(op, lhs, rhs) { ONE } else { ZERO };
152                self.set_port_hot(kid, PortSlot::new(2), o);
153            }
154            KindTag::Flag {
155                priority,
156                enable_toggle,
157            } => {
158                let set_l = self.get_port_hot(kid, PortSlot::new(0));
159                let reset_l = self.get_port_hot(kid, PortSlot::new(1));
160                let toggle_l = self.get_port_hot(kid, PortSlot::new(2));
161                let set = is_truthy(set_l);
162                let reset = is_truthy(reset_l);
163                let toggle = enable_toggle && !is_truthy(self.prev_in[ki]) && is_truthy(toggle_l);
164                let mut st = self.flag[ki];
165                match priority {
166                    FlagPriority::ResetWins => {
167                        if reset {
168                            st = false;
169                        } else if set {
170                            st = true;
171                        } else if toggle {
172                            st = !st;
173                        }
174                    }
175                    FlagPriority::SetWins => {
176                        if set {
177                            st = true;
178                        } else if reset {
179                            st = false;
180                        } else if toggle {
181                            st = !st;
182                        }
183                    }
184                }
185                self.prev_in[ki] = toggle_l;
186                self.flag[ki] = st;
187                self.set_port_hot(kid, PortSlot::new(3), if st { ONE } else { ZERO });
188            }
189            KindTag::Counter => {
190                let inc = self.get_port_hot(kid, PortSlot::new(0));
191                let dec = self.get_port_hot(kid, PortSlot::new(1));
192                let reset = self.get_port_hot(kid, PortSlot::new(2));
193                if is_truthy(reset) {
194                    self.counter[ki] = 0;
195                }
196                if !is_truthy(self.prev_in[ki]) && is_truthy(inc) {
197                    self.counter[ki] = self.counter[ki].saturating_add(1);
198                }
199                if !is_truthy(self.prev_dec[ki]) && is_truthy(dec) {
200                    self.counter[ki] = self.counter[ki].saturating_sub(1);
201                }
202                self.prev_in[ki] = inc;
203                self.prev_dec[ki] = dec;
204                self.set_port_hot(kid, PortSlot::new(3), crate::from_count(self.counter[ki]));
205            }
206            KindTag::TimerPulseHold { ticks } => {
207                let start = self.get_port_hot(kid, PortSlot::new(0));
208                let prev = self.prev_in[ki];
209                if !is_truthy(prev) && is_truthy(start) {
210                    self.timer_left[ki] = ticks;
211                }
212                self.prev_in[ki] = start;
213                if self.timer_left[ki] > 0 {
214                    self.set_port_hot(kid, PortSlot::new(1), ONE);
215                    self.timer_left[ki] -= 1;
216                } else {
217                    self.set_port_hot(kid, PortSlot::new(1), ZERO);
218                }
219            }
220            KindTag::TimerFedCountdown { ticks } => {
221                let feed = self.get_port_hot(kid, PortSlot::new(0));
222                let prev = self.prev_in[ki];
223                if is_truthy(feed) {
224                    if !is_truthy(prev) {
225                        self.timer_left[ki] = ticks;
226                    }
227                    if self.timer_left[ki] > 0 {
228                        self.timer_left[ki] -= 1;
229                    }
230                    let active = if self.timer_left[ki] == 0 { ONE } else { ZERO };
231                    self.set_port_hot(kid, PortSlot::new(1), active);
232                } else {
233                    self.timer_left[ki] = 0;
234                    self.set_port_hot(kid, PortSlot::new(1), ZERO);
235                }
236                self.prev_in[ki] = feed;
237            }
238            KindTag::Delay { ticks } => {
239                let i = self.get_port_hot(kid, PortSlot::new(0));
240                if ticks == 0 {
241                    self.set_port_hot(kid, PortSlot::new(1), i);
242                } else {
243                    let len = self.delay_len[ki] as usize;
244                    let off = self.delay_off[ki] as usize;
245                    let head = self.delay_head[ki] as usize;
246                    let o = self.delay_buf[off + head];
247                    self.delay_buf[off + head] = i;
248                    let next = head + 1;
249                    self.delay_head[ki] = if len.is_power_of_two() {
250                        (next & (len - 1)) as u16
251                    } else if next >= len {
252                        0
253                    } else {
254                        next as u16
255                    };
256                    self.set_port_hot(kid, PortSlot::new(1), o);
257                }
258            }
259            KindTag::CalcAdd { domain } => {
260                let a = self.get_port_hot(kid, PortSlot::new(0));
261                let b = self.get_port_hot(kid, PortSlot::new(1));
262                let out = match domain {
263                    SignalDomain::Count => count_add(a, b),
264                    _ => crate::foundation::signal_ops::sat_add(a, b),
265                };
266                self.set_port_hot(kid, PortSlot::new(2), out);
267            }
268            KindTag::CalcSub { domain } => {
269                let a = self.get_port_hot(kid, PortSlot::new(0));
270                let b = self.get_port_hot(kid, PortSlot::new(1));
271                let out = match domain {
272                    SignalDomain::Count => count_sub(a, b),
273                    _ => crate::foundation::signal_ops::sat_sub(a, b),
274                };
275                self.set_port_hot(kid, PortSlot::new(2), out);
276            }
277            KindTag::CalcMulLevel => {
278                let a = self.get_port_hot(kid, PortSlot::new(0));
279                let b = self.get_port_hot(kid, PortSlot::new(1));
280                self.set_port_hot(
281                    kid,
282                    PortSlot::new(2),
283                    crate::foundation::signal_ops::mul(a, b),
284                );
285            }
286            KindTag::CalcMulCount => {
287                let a = self.get_port_hot(kid, PortSlot::new(0));
288                let b = self.get_port_hot(kid, PortSlot::new(1));
289                self.set_port_hot(kid, PortSlot::new(2), count_mul(a, b));
290            }
291            KindTag::CalcDivLevel => {
292                let a = self.get_port_hot(kid, PortSlot::new(0));
293                let b = self.get_port_hot(kid, PortSlot::new(1));
294                self.set_port_hot(
295                    kid,
296                    PortSlot::new(2),
297                    crate::foundation::signal_ops::div(a, b),
298                );
299            }
300            KindTag::CalcDivCount => {
301                let a = self.get_port_hot(kid, PortSlot::new(0));
302                let b = self.get_port_hot(kid, PortSlot::new(1));
303                self.set_port_hot(kid, PortSlot::new(2), count_div(a, b));
304            }
305            KindTag::CalcDivLevelConst { divisor } => {
306                let a = self.get_port_hot(kid, PortSlot::new(0));
307                self.set_port_hot(
308                    kid,
309                    PortSlot::new(2),
310                    crate::foundation::signal_ops::div(a, divisor),
311                );
312            }
313            KindTag::CalcDivCountConst { divisor } => {
314                let a = self.get_port_hot(kid, PortSlot::new(0));
315                self.set_port_hot(kid, PortSlot::new(2), count_div(a, divisor));
316            }
317            KindTag::Abs { domain } => {
318                let i = self.get_port_hot(kid, PortSlot::new(0));
319                let o = match domain {
320                    SignalDomain::Count => count_abs(i),
321                    _ => level_abs(i),
322                };
323                self.set_port_hot(kid, PortSlot::new(1), o);
324            }
325            KindTag::Neg { domain } => {
326                let i = self.get_port_hot(kid, PortSlot::new(0));
327                let o = match domain {
328                    SignalDomain::Count => count_neg(i),
329                    _ => level_neg(i),
330                };
331                self.set_port_hot(kid, PortSlot::new(1), o);
332            }
333            KindTag::Map {
334                domain,
335                #[cfg(feature = "signal-f32")]
336                degenerate,
337                #[cfg(feature = "signal-f32")]
338                in_min,
339                #[cfg(feature = "signal-f32")]
340                out_min,
341                #[cfg(feature = "signal-f32")]
342                inv_in_span,
343                #[cfg(feature = "signal-f32")]
344                out_span,
345                #[cfg(feature = "signal-i32")]
346                plan,
347            } => {
348                let i = self.get_port_hot(kid, PortSlot::new(0));
349                #[cfg(feature = "signal-f32")]
350                let o = map_linear_fast(
351                    i,
352                    domain,
353                    degenerate,
354                    in_min,
355                    out_min,
356                    inv_in_span,
357                    out_span,
358                );
359                #[cfg(feature = "signal-i32")]
360                let o = {
361                    let _ = domain;
362                    plan.map(i)
363                };
364                self.set_port_hot(kid, PortSlot::new(1), o);
365            }
366            KindTag::Select => {
367                let sel = self.get_port_hot(kid, PortSlot::new(0));
368                let a = self.get_port_hot(kid, PortSlot::new(1));
369                let b = self.get_port_hot(kid, PortSlot::new(2));
370                let o = if is_truthy(sel) { b } else { a };
371                self.set_port_hot(kid, PortSlot::new(3), o);
372            }
373            KindTag::Digitize {
374                domain,
375                degenerate,
376                in_min,
377                out_min,
378                bin_scale,
379                out_scale,
380                last_f,
381                steps,
382                last,
383                #[cfg(feature = "signal-i32")]
384                den,
385                #[cfg(feature = "signal-i32")]
386                out_span,
387            } => {
388                let i = self.get_port_hot(kid, PortSlot::new(0));
389                let o = digitize_fast(
390                    i,
391                    domain,
392                    degenerate,
393                    in_min,
394                    out_min,
395                    bin_scale,
396                    out_scale,
397                    last_f,
398                    steps,
399                    last,
400                    #[cfg(feature = "signal-i32")]
401                    den,
402                    #[cfg(feature = "signal-i32")]
403                    out_span,
404                );
405                self.set_port_hot(kid, PortSlot::new(1), o);
406            }
407            KindTag::Threshold {
408                high,
409                low,
410                use_hysteresis,
411            } => {
412                let x = self.get_port_hot(kid, PortSlot::new(0));
413                let prev = self.flag[ki];
414                let mut latched = prev;
415                if use_hysteresis {
416                    if latched {
417                        if x < low {
418                            latched = false;
419                        }
420                    } else if x >= high {
421                        latched = true;
422                    }
423                } else {
424                    latched = x >= high;
425                }
426                self.flag[ki] = latched;
427                self.set_port_hot(kid, PortSlot::new(1), if latched { ONE } else { ZERO });
428                self.set_port_hot(
429                    kid,
430                    PortSlot::new(2),
431                    if !prev && latched { ONE } else { ZERO },
432                );
433                self.set_port_hot(
434                    kid,
435                    PortSlot::new(3),
436                    if prev && !latched { ONE } else { ZERO },
437                );
438            }
439            tag @ (KindTag::RandomLevel {
440                require_gate,
441                min_wired,
442                max_wired,
443            }
444            | KindTag::RandomCount {
445                require_gate,
446                min_wired,
447                max_wired,
448            }) => {
449                // Unwired min/max ports default to ZERO / ONE (not cleared gather zeros).
450                let min_v = if min_wired {
451                    self.get_port_hot(kid, PortSlot::new(0))
452                } else {
453                    ZERO
454                };
455                let max_v = if max_wired {
456                    self.get_port_hot(kid, PortSlot::new(1))
457                } else {
458                    match tag {
459                        KindTag::RandomCount { .. } => crate::foundation::from_count(1),
460                        _ => ONE,
461                    }
462                };
463                let gate = self.get_port_hot(kid, PortSlot::new(2));
464                let prev = self.prev_in[ki];
465                let rising = !is_truthy(prev) && is_truthy(gate);
466                let sample = if require_gate { rising } else { true };
467                if sample {
468                    let u = self.next_rng_u32();
469                    let o = match tag {
470                        KindTag::RandomCount { .. } => random_count_in_range(u, min_v, max_v),
471                        _ => random_level_in_range(u, min_v, max_v),
472                    };
473                    self.set_port_hot(kid, PortSlot::new(3), o);
474                    // Last sample is stashed in `counter` (shared rune storage).
475                    #[cfg(feature = "signal-f32")]
476                    {
477                        self.counter[ki] = o.to_bits() as i32;
478                    }
479                    #[cfg(feature = "signal-i32")]
480                    {
481                        self.counter[ki] = o;
482                    }
483                } else {
484                    #[cfg(feature = "signal-f32")]
485                    {
486                        self.set_port_hot(
487                            kid,
488                            PortSlot::new(3),
489                            f32::from_bits(self.counter[ki] as u32),
490                        );
491                    }
492                    #[cfg(feature = "signal-i32")]
493                    {
494                        self.set_port_hot(kid, PortSlot::new(3), self.counter[ki]);
495                    }
496                }
497                self.prev_in[ki] = gate;
498            }
499            KindTag::SqrtLevel => {
500                let i = self.get_port_hot(kid, PortSlot::new(0));
501                let o = level_sqrt(i);
502                self.set_port_hot(kid, PortSlot::new(1), o);
503            }
504            KindTag::SqrtCount => {
505                let i = self.get_port_hot(kid, PortSlot::new(0));
506                let o = count_sqrt(i);
507                self.set_port_hot(kid, PortSlot::new(1), o);
508            }
509            KindTag::ConvertBoolToLevel | KindTag::ConvertIdentity => {
510                let i = self.get_port_hot(kid, PortSlot::new(0));
511                self.set_port_hot(kid, PortSlot::new(1), i);
512            }
513            KindTag::ConvertBoolToCount => {
514                let i = self.get_port_hot(kid, PortSlot::new(0));
515                let o = crate::foundation::from_count(i32::from(is_truthy(i)));
516                self.set_port_hot(kid, PortSlot::new(1), o);
517            }
518            KindTag::ConvertLevelToBool | KindTag::ConvertCountToBool => {
519                let i = self.get_port_hot(kid, PortSlot::new(0));
520                self.set_port_hot(kid, PortSlot::new(1), if is_truthy(i) { ONE } else { ZERO });
521            }
522            KindTag::ConvertLevelToCount => {
523                let i = self.get_port_hot(kid, PortSlot::new(0));
524                self.set_port_hot(kid, PortSlot::new(1), level_to_count(i));
525            }
526            KindTag::ConvertCountToLevel => {
527                let i = self.get_port_hot(kid, PortSlot::new(0));
528                self.set_port_hot(kid, PortSlot::new(1), count_to_level(i));
529            }
530            KindTag::Xor => {
531                let a = is_truthy(self.get_port_hot(kid, PortSlot::new(0)));
532                let b = is_truthy(self.get_port_hot(kid, PortSlot::new(1)));
533                self.set_port_hot(kid, PortSlot::new(2), if a ^ b { ONE } else { ZERO });
534            }
535            KindTag::FallingToZero => {
536                let i = self.get_port_hot(kid, PortSlot::new(0));
537                let prev = self.prev_in[ki];
538                let o = if is_truthy(prev) && !is_truthy(i) {
539                    ONE
540                } else {
541                    ZERO
542                };
543                self.prev_in[ki] = i;
544                self.set_port_hot(kid, PortSlot::new(1), o);
545            }
546            KindTag::Change => {
547                let i = self.get_port_hot(kid, PortSlot::new(0));
548                let prev = self.prev_in[ki];
549                let o = if is_truthy(prev) != is_truthy(i) {
550                    ONE
551                } else {
552                    ZERO
553                };
554                self.prev_in[ki] = i;
555                self.set_port_hot(kid, PortSlot::new(1), o);
556            }
557            KindTag::Clamp { min, max } => {
558                debug_assert!(min <= max);
559                let i = self.get_port_hot(kid, PortSlot::new(0));
560                let o = if i < min {
561                    min
562                } else if i > max {
563                    max
564                } else {
565                    i
566                };
567                self.set_port_hot(kid, PortSlot::new(1), o);
568            }
569            KindTag::SignalOut => {
570                let v = self.get_port_hot(kid, PortSlot::new(0));
571                if let Some(path) = self.knots[ki].path {
572                    self.push_signal_out(path, v);
573                }
574            }
575            KindTag::EmitCommand { enable_wired } => {
576                let trig = self.get_port_hot(kid, PortSlot::new(0));
577                // Unwired enable is treated as always on.
578                let enable = if enable_wired {
579                    self.get_port_hot(kid, PortSlot::new(1))
580                } else {
581                    ONE
582                };
583                let payload = self.get_port_hot(kid, PortSlot::new(2));
584                let prev = self.prev_in[ki];
585                if !is_truthy(prev) && is_truthy(trig) && is_truthy(enable) {
586                    if let Some(cmd) = self.knots[ki].cmd {
587                        self.push_emit(cmd, payload);
588                    }
589                }
590                self.prev_in[ki] = trig;
591            }
592        }
593    }
594}
595
596fn compare(op: CompareOp, lhs: Signal, rhs: Signal) -> bool {
597    match op {
598        CompareOp::Eq => lhs == rhs,
599        CompareOp::Ne => lhs != rhs,
600        CompareOp::Lt => lhs < rhs,
601        CompareOp::Lte => lhs <= rhs,
602        CompareOp::Gt => lhs > rhs,
603        CompareOp::Gte => lhs >= rhs,
604    }
605}
606
607#[inline]
608#[cfg(feature = "signal-f32")]
609fn normalize_count(value: Signal) -> Signal {
610    const MAX_COUNT: f32 = 2_147_483_520.0;
611    if value.is_nan() {
612        ZERO
613    } else {
614        libm::truncf(value).clamp(i32::MIN as f32, MAX_COUNT)
615    }
616}
617
618#[cfg(all(test, feature = "signal-f32"))]
619#[test]
620fn normalize_count_rejects_nan() {
621    assert_eq!(normalize_count(f32::NAN), ZERO);
622}
623
624#[inline]
625fn count_add(a: Signal, b: Signal) -> Signal {
626    #[cfg(feature = "signal-f32")]
627    {
628        normalize_count(a + b)
629    }
630    #[cfg(feature = "signal-i32")]
631    {
632        a.saturating_add(b)
633    }
634}
635
636#[inline]
637fn count_sub(a: Signal, b: Signal) -> Signal {
638    #[cfg(feature = "signal-f32")]
639    {
640        normalize_count(a - b)
641    }
642    #[cfg(feature = "signal-i32")]
643    {
644        a.saturating_sub(b)
645    }
646}
647
648#[inline]
649fn count_mul(a: Signal, b: Signal) -> Signal {
650    #[cfg(feature = "signal-f32")]
651    {
652        normalize_count((a as f64 * b as f64) as f32)
653    }
654    #[cfg(feature = "signal-i32")]
655    {
656        a.saturating_mul(b)
657    }
658}
659
660#[inline]
661fn count_div(a: Signal, b: Signal) -> Signal {
662    #[cfg(feature = "signal-f32")]
663    {
664        let a = a as i32;
665        let b = b as i32;
666        if b == 0 {
667            0.0
668        } else {
669            normalize_count(a.checked_div(b).unwrap_or(i32::MAX) as f32)
670        }
671    }
672    #[cfg(feature = "signal-i32")]
673    {
674        if b == 0 {
675            0
676        } else {
677            a.checked_div(b).unwrap_or(i32::MAX)
678        }
679    }
680}
681
682#[inline]
683fn level_to_count(value: Signal) -> Signal {
684    #[cfg(feature = "signal-f32")]
685    {
686        let rounded = if value >= 0.0 {
687            value + 0.5
688        } else {
689            value - 0.5
690        };
691        normalize_count(rounded)
692    }
693    #[cfg(feature = "signal-i32")]
694    {
695        let value = value as i64;
696        let half = (ONE / 2) as i64;
697        let rounded = if value >= 0 {
698            (value + half) / (ONE as i64)
699        } else {
700            (value - half) / (ONE as i64)
701        };
702        rounded.clamp(i32::MIN as i64, i32::MAX as i64) as i32
703    }
704}
705
706#[inline]
707fn count_abs(value: Signal) -> Signal {
708    #[cfg(feature = "signal-f32")]
709    {
710        normalize_count(libm::fabsf(value))
711    }
712    #[cfg(feature = "signal-i32")]
713    {
714        value.saturating_abs()
715    }
716}
717
718#[inline]
719fn count_neg(value: Signal) -> Signal {
720    #[cfg(feature = "signal-f32")]
721    {
722        normalize_count(-value)
723    }
724    #[cfg(feature = "signal-i32")]
725    {
726        value.saturating_neg()
727    }
728}
729
730#[inline]
731fn level_abs(value: Signal) -> Signal {
732    #[cfg(feature = "signal-f32")]
733    {
734        libm::fabsf(value)
735    }
736    #[cfg(feature = "signal-i32")]
737    {
738        value.saturating_abs()
739    }
740}
741
742#[inline]
743fn level_neg(value: Signal) -> Signal {
744    #[cfg(feature = "signal-f32")]
745    {
746        -value
747    }
748    #[cfg(feature = "signal-i32")]
749    {
750        value.saturating_neg()
751    }
752}
753
754#[inline]
755fn count_to_level(value: Signal) -> Signal {
756    #[cfg(feature = "signal-f32")]
757    {
758        value
759    }
760    #[cfg(feature = "signal-i32")]
761    {
762        value.saturating_mul(ONE)
763    }
764}
765
766#[cfg(feature = "signal-f32")]
767#[inline]
768fn map_linear_fast(
769    i: Signal,
770    domain: SignalDomain,
771    degenerate: bool,
772    in_min: Signal,
773    out_min: Signal,
774    inv_in_span: Signal,
775    out_span: Signal,
776) -> Signal {
777    if degenerate {
778        return out_min;
779    }
780    let t = ((i - in_min) * inv_in_span).clamp(0.0, 1.0);
781    let mapped = out_min + t * out_span;
782    if domain == SignalDomain::Count {
783        normalize_count(mapped)
784    } else {
785        mapped
786    }
787}
788
789#[cfg(test)]
790pub(crate) fn map_linear_for_test(
791    i: Signal,
792    in_min: Signal,
793    in_max: Signal,
794    out_min: Signal,
795    out_max: Signal,
796) -> Signal {
797    map_linear_for_domain_test(SignalDomain::Level, i, in_min, in_max, out_min, out_max)
798}
799
800#[cfg(test)]
801pub(crate) fn map_linear_for_domain_test(
802    domain: SignalDomain,
803    i: Signal,
804    in_min: Signal,
805    in_max: Signal,
806    out_min: Signal,
807    out_max: Signal,
808) -> Signal {
809    map_linear_from_tag_for_test(
810        KindTag::map_precomputed(domain, in_min, in_max, out_min, out_max),
811        i,
812        out_min,
813    )
814}
815
816#[cfg(test)]
817fn map_linear_from_tag_for_test(tag: KindTag, i: Signal, fallback_out_min: Signal) -> Signal {
818    match tag {
819        KindTag::Map {
820            domain,
821            #[cfg(feature = "signal-f32")]
822            degenerate,
823            #[cfg(feature = "signal-f32")]
824            in_min,
825            #[cfg(feature = "signal-f32")]
826            out_min,
827            #[cfg(feature = "signal-f32")]
828            inv_in_span,
829            #[cfg(feature = "signal-f32")]
830            out_span,
831            #[cfg(feature = "signal-i32")]
832            plan,
833        } => {
834            #[cfg(feature = "signal-f32")]
835            {
836                map_linear_fast(
837                    i,
838                    domain,
839                    degenerate,
840                    in_min,
841                    out_min,
842                    inv_in_span,
843                    out_span,
844                )
845            }
846            #[cfg(feature = "signal-i32")]
847            {
848                let _ = domain;
849                plan.map(i)
850            }
851        }
852        _ => fallback_out_min,
853    }
854}
855
856/// Quantize `i` into `steps` bins using bind-time scales (endpoints included).
857#[inline]
858#[allow(clippy::too_many_arguments)]
859fn digitize_fast(
860    i: Signal,
861    domain: SignalDomain,
862    degenerate: bool,
863    in_min: Signal,
864    out_min: Signal,
865    bin_scale: Signal,
866    out_scale: Signal,
867    last_f: Signal,
868    steps: u16,
869    last: u16,
870    #[cfg(feature = "signal-i32")] den: i64,
871    #[cfg(feature = "signal-i32")] out_span: i64,
872) -> Signal {
873    if degenerate {
874        return out_min;
875    }
876    #[cfg(feature = "signal-f32")]
877    {
878        let _ = (steps, last);
879        let raw = (i - in_min) * bin_scale;
880        let bin = raw.max(0.0).min(last_f) as u32;
881        #[cfg(feature = "std")]
882        let mapped = { out_scale.mul_add(bin as f32, out_min) };
883        #[cfg(not(feature = "std"))]
884        let mapped = { out_scale * bin as f32 + out_min };
885        if domain == SignalDomain::Count {
886            normalize_count(mapped)
887        } else {
888            mapped
889        }
890    }
891    #[cfg(feature = "signal-i32")]
892    {
893        let _ = (domain, bin_scale, out_scale, last_f);
894        let last_i = last as i64;
895        let t = ((i as i64) - (in_min as i64)).clamp(0, den);
896        let mut bin = t * (steps as i64) / den;
897        if bin > last_i {
898            bin = last_i;
899        }
900        (out_min as i64 + bin * out_span / last_i) as i32
901    }
902}
903
904/// Public-for-tests digitize via the same precompute path as loom (shipped code).
905#[cfg(test)]
906pub(crate) fn digitize_for_test(
907    i: Signal,
908    steps: u16,
909    in_min: Signal,
910    in_max: Signal,
911    out_min: Signal,
912    out_max: Signal,
913) -> Signal {
914    digitize_for_domain_test(
915        SignalDomain::Level,
916        i,
917        steps,
918        in_min,
919        in_max,
920        out_min,
921        out_max,
922    )
923}
924
925#[cfg(test)]
926pub(crate) fn digitize_for_domain_test(
927    domain: SignalDomain,
928    i: Signal,
929    steps: u16,
930    in_min: Signal,
931    in_max: Signal,
932    out_min: Signal,
933    out_max: Signal,
934) -> Signal {
935    digitize_from_tag_for_test(
936        KindTag::digitize_precomputed(domain, steps, in_min, in_max, out_min, out_max),
937        i,
938        out_min,
939    )
940}
941
942#[cfg(test)]
943fn digitize_from_tag_for_test(tag: KindTag, i: Signal, fallback_out_min: Signal) -> Signal {
944    match tag {
945        KindTag::Digitize {
946            domain,
947            degenerate,
948            in_min,
949            out_min,
950            bin_scale,
951            out_scale,
952            last_f,
953            steps,
954            last,
955            #[cfg(feature = "signal-i32")]
956            den,
957            #[cfg(feature = "signal-i32")]
958            out_span,
959        } => digitize_fast(
960            i,
961            domain,
962            degenerate,
963            in_min,
964            out_min,
965            bin_scale,
966            out_scale,
967            last_f,
968            steps,
969            last,
970            #[cfg(feature = "signal-i32")]
971            den,
972            #[cfg(feature = "signal-i32")]
973            out_span,
974        ),
975        _ => fallback_out_min,
976    }
977}
978
979#[cfg(test)]
980mod test_helper_dispatch_tests {
981    use super::{digitize_from_tag_for_test, map_linear_from_tag_for_test, KindTag};
982    use crate::foundation::{from_count, ONE};
983
984    #[test]
985    fn nonmatching_precompute_tags_return_the_requested_fallback() {
986        assert_eq!(
987            map_linear_from_tag_for_test(KindTag::Not, ONE, from_count(7)),
988            from_count(7)
989        );
990        assert_eq!(
991            digitize_from_tag_for_test(KindTag::Not, ONE, from_count(9)),
992            from_count(9)
993        );
994    }
995}
996
997/// Map `u` into `[min_v, max_v]` (order-independent). i32 uses a wide product so
998/// full-domain spans do not overflow intermediate mul.
999fn random_level_in_range(u: u32, min_v: Signal, max_v: Signal) -> Signal {
1000    #[cfg(feature = "signal-f32")]
1001    {
1002        let t = (u as f32) / (u32::MAX as f32);
1003        let lo = min_v.min(max_v);
1004        let hi = min_v.max(max_v);
1005        lo + t * (hi - lo)
1006    }
1007    #[cfg(feature = "signal-i32")]
1008    {
1009        let lo = min_v.min(max_v) as i64;
1010        let hi = min_v.max(max_v) as i64;
1011        let span = hi - lo;
1012        if span <= 0 {
1013            return lo as i32;
1014        }
1015        let offset = ((u as u64) * (span as u64)) / (u32::MAX as u64);
1016        (lo + offset as i64) as i32
1017    }
1018}
1019
1020fn random_count_in_range(u: u32, min_v: Signal, max_v: Signal) -> Signal {
1021    #[cfg(feature = "signal-f32")]
1022    let (min_v, max_v) = (min_v as i32, max_v as i32);
1023    let lo = min_v.min(max_v) as i64;
1024    let hi = min_v.max(max_v) as i64;
1025    let span = hi - lo;
1026    if span <= 0 {
1027        return crate::foundation::from_count(lo as i32);
1028    }
1029    let offset = ((u as u64) * (span as u64)) / (u32::MAX as u64);
1030    crate::foundation::from_count((lo + offset as i64) as i32)
1031}
1032
1033fn level_sqrt(i: Signal) -> Signal {
1034    #[cfg(feature = "signal-f32")]
1035    {
1036        if i <= 0.0 {
1037            0.0
1038        } else {
1039            sqrt_f32(i)
1040        }
1041    }
1042    #[cfg(feature = "signal-i32")]
1043    {
1044        if i <= 0 {
1045            0
1046        } else {
1047            isqrt_u64((i as u64) * (ONE as u64)) as i32
1048        }
1049    }
1050}
1051
1052#[cfg(test)]
1053mod sense_dispatch_tests {
1054    use super::*;
1055    use crate::authoring::Weave;
1056    use crate::foundation::{KnotKind, SignalDomain};
1057    use crate::BindOpts;
1058
1059    #[test]
1060    fn sense_dispatch_is_a_noop_after_seeding() {
1061        let mut builder = Weave::builder("sense-dispatch").unwrap();
1062        builder
1063            .knot("constant", KnotKind::constant(ONE, SignalDomain::Bool))
1064            .unwrap();
1065        let mut runtime = Runtime::bind(builder.build().unwrap(), BindOpts::default()).unwrap();
1066        let kid = KnotId::try_from(0usize).unwrap();
1067
1068        runtime.set_port_hot(kid, PortSlot::new(0), ONE);
1069        runtime.eval_knot(kid);
1070
1071        assert_eq!(runtime.get_port_hot(kid, PortSlot::new(0)), ONE);
1072    }
1073}
1074
1075#[cfg(test)]
1076mod count_div_dispatch_tests {
1077    use super::Runtime;
1078    use crate::authoring::Weave;
1079    use crate::foundation::{from_count, CalcOp, KnotKind, PortSlot, SignalDomain};
1080    use crate::BindOpts;
1081
1082    #[test]
1083    fn dynamic_count_division_uses_the_general_dispatch_path() {
1084        let mut builder = Weave::builder("dynamic-count-divisor").unwrap();
1085        let numerator = builder
1086            .knot("numerator", KnotKind::signal_in(SignalDomain::Count))
1087            .unwrap();
1088        let denominator = builder
1089            .knot("denominator", KnotKind::signal_in(SignalDomain::Count))
1090            .unwrap();
1091        let divide = builder
1092            .knot("divide", KnotKind::calc(CalcOp::Div, SignalDomain::Count))
1093            .unwrap();
1094        let numerator_out = builder.output(&numerator, "out").unwrap();
1095        let denominator_out = builder.output(&denominator, "out").unwrap();
1096        let divide_a = builder.input(&divide, "a").unwrap();
1097        let divide_b = builder.input(&divide, "b").unwrap();
1098        builder.connect(numerator_out, divide_a).unwrap();
1099        builder.connect(denominator_out, divide_b).unwrap();
1100
1101        let mut runtime = Runtime::bind(builder.build().unwrap(), BindOpts::default()).unwrap();
1102        let numerator = runtime.sense_id("numerator").unwrap();
1103        let denominator = runtime.sense_id("denominator").unwrap();
1104        {
1105            let mut writer = runtime.port_writer();
1106            writer.set_sense(numerator, from_count(9)).unwrap();
1107            writer.set_sense(denominator, from_count(2)).unwrap();
1108        }
1109        runtime.loom();
1110
1111        let divide = runtime.knot_id("divide").unwrap();
1112        assert_eq!(
1113            runtime.get_port_checked(divide, PortSlot::new(2)),
1114            Ok(from_count(4))
1115        );
1116    }
1117}
1118
1119fn count_sqrt(i: Signal) -> Signal {
1120    #[cfg(feature = "signal-f32")]
1121    {
1122        if i <= 0.0 {
1123            0.0
1124        } else {
1125            (sqrt_f32(i) as i32) as f32
1126        }
1127    }
1128    #[cfg(feature = "signal-i32")]
1129    {
1130        if i <= 0 {
1131            0
1132        } else {
1133            isqrt_u64(i as u64) as i32
1134        }
1135    }
1136}
1137
1138/// `no_std` square root for the float signal path.
1139#[cfg(feature = "signal-f32")]
1140#[inline]
1141fn sqrt_f32(value: f32) -> f32 {
1142    #[cfg(feature = "std")]
1143    {
1144        value.sqrt()
1145    }
1146    #[cfg(not(feature = "std"))]
1147    {
1148        libm::sqrtf(value)
1149    }
1150}
1151
1152#[cfg(all(test, feature = "signal-f32"))]
1153mod sqrt_f32_tests {
1154    #[test]
1155    fn libm_matches_std_across_f32_magnitudes() {
1156        let values = [
1157            f32::from_bits(1),
1158            f32::MIN_POSITIVE,
1159            1.0e-20,
1160            0.25,
1161            2.0,
1162            1.0e20,
1163            f32::MAX,
1164        ];
1165
1166        for value in values {
1167            let expected = value.sqrt();
1168            let actual = libm::sqrtf(value);
1169            let tolerance = expected.abs() * (4.0 * f32::EPSILON);
1170            assert!(
1171                (actual - expected).abs() <= tolerance,
1172                "sqrt({value:e}): expected {expected:e}, got {actual:e}"
1173            );
1174        }
1175    }
1176
1177    #[test]
1178    fn std_dispatch_preserves_native_sqrt_semantics() {
1179        for value in [f32::from_bits(1), 2.0, f32::MAX] {
1180            assert_eq!(super::sqrt_f32(value), value.sqrt());
1181        }
1182    }
1183}
1184
1185/// Floor integer square root via restoring binary arithmetic.
1186///
1187/// This has a fixed, bounded shift/subtract loop and avoids the repeated wide
1188/// divisions Newton iteration would emit on 32-bit constrained targets.
1189#[cfg(feature = "signal-i32")]
1190#[inline]
1191fn isqrt_u64(mut n: u64) -> u64 {
1192    let mut result = 0u64;
1193    let mut bit = 1u64 << 62;
1194
1195    while bit > n {
1196        bit >>= 2;
1197    }
1198    while bit != 0 {
1199        let trial = result + bit;
1200        if n >= trial {
1201            n -= trial;
1202            result = (result >> 1) + bit;
1203        } else {
1204            result >>= 1;
1205        }
1206        bit >>= 2;
1207    }
1208    result
1209}
1210
1211#[cfg(all(test, feature = "signal-i32"))]
1212#[test]
1213fn isqrt_matches_dense_range_and_boundaries() {
1214    for n in 0u64..=65_536 {
1215        let root = isqrt_u64(n);
1216        assert!(root * root <= n, "isqrt({n}) overshot with {root}");
1217        assert!(
1218            (root + 1) * (root + 1) > n,
1219            "isqrt({n}) undershot with {root}"
1220        );
1221    }
1222    for k in 0i32..200 {
1223        let n = k * k;
1224        assert_eq!(isqrt_u64(n as u64), k as u64, "isqrt({n})");
1225        if k > 0 {
1226            assert_eq!(isqrt_u64((n - 1) as u64), (k - 1) as u64);
1227        }
1228    }
1229    assert_eq!(isqrt_u64(0), 0);
1230    assert_eq!(isqrt_u64(u64::MAX), u32::MAX as u64);
1231}
1232
1233#[cfg(all(test, feature = "signal-i32"))]
1234mod random_i32_range_tests {
1235    use super::{random_count_in_range, random_level_in_range};
1236
1237    #[test]
1238    fn full_i32_ranges_preserve_both_endpoints() {
1239        for range in [(i32::MIN, i32::MAX), (i32::MAX, i32::MIN)] {
1240            assert_eq!(random_level_in_range(0, range.0, range.1), i32::MIN);
1241            assert_eq!(random_level_in_range(u32::MAX, range.0, range.1), i32::MAX);
1242            assert_eq!(random_count_in_range(0, range.0, range.1), i32::MIN);
1243            assert_eq!(random_count_in_range(u32::MAX, range.0, range.1), i32::MAX);
1244        }
1245    }
1246}
1247
1248#[cfg(test)]
1249mod domain_math_tests {
1250    use super::{
1251        count_abs, count_add, count_div, count_mul, count_neg, count_sqrt, count_sub,
1252        count_to_level, level_sqrt, level_to_count,
1253    };
1254    use crate::foundation::{from_count, from_level};
1255
1256    #[test]
1257    fn count_arithmetic_is_integer_and_saturating() {
1258        assert_eq!(count_mul(from_count(6), from_count(7)), from_count(42));
1259        assert_eq!(
1260            count_mul(from_count(50_000), from_count(50_000)),
1261            from_count(i32::MAX)
1262        );
1263        assert_eq!(count_div(from_count(7), from_count(2)), from_count(3));
1264        assert_eq!(count_div(from_count(7), from_count(0)), from_count(0));
1265        assert_eq!(
1266            count_div(from_count(i32::MIN), from_count(-1)),
1267            from_count(i32::MAX)
1268        );
1269        assert_eq!(
1270            count_add(from_count(i32::MAX), from_count(1)),
1271            from_count(i32::MAX)
1272        );
1273        assert_eq!(
1274            count_sub(from_count(i32::MIN), from_count(1)),
1275            from_count(i32::MIN)
1276        );
1277        assert_eq!(count_abs(from_count(i32::MIN)), from_count(i32::MAX));
1278        assert_eq!(count_neg(from_count(i32::MIN)), from_count(i32::MAX));
1279    }
1280
1281    #[test]
1282    fn numeric_conversions_round_and_saturate() {
1283        assert_eq!(level_to_count(from_level(2.5)), from_count(3));
1284        assert_eq!(level_to_count(from_level(-2.5)), from_count(-3));
1285        assert_eq!(count_to_level(from_count(2)), from_level(2.0));
1286
1287        #[cfg(feature = "signal-i32")]
1288        assert_eq!(count_to_level(i32::MAX), i32::MAX);
1289    }
1290
1291    #[test]
1292    fn sqrt_respects_level_and_count_representations() {
1293        assert_eq!(level_sqrt(from_level(0.25)), from_level(0.5));
1294        assert_eq!(count_sqrt(from_count(15)), from_count(3));
1295        assert_eq!(count_sqrt(from_count(-1)), from_count(0));
1296    }
1297}
1298
1299#[cfg(test)]
1300mod digitize_tests {
1301    use super::{digitize_for_domain_test, digitize_for_test};
1302    use crate::foundation::{from_count, SignalDomain, ONE, ZERO};
1303
1304    #[test]
1305    fn digitize_precompute_matches_endpoints_and_mids() {
1306        let steps = 4u16;
1307        let in0 = from_count(0);
1308        let in4 = from_count(4);
1309        let o0 = from_count(0);
1310        let o30 = from_count(30);
1311        assert_eq!(
1312            digitize_for_test(from_count(0), steps, in0, in4, o0, o30),
1313            from_count(0)
1314        );
1315        assert_eq!(
1316            digitize_for_test(from_count(1), steps, in0, in4, o0, o30),
1317            from_count(10)
1318        );
1319        assert_eq!(
1320            digitize_for_test(from_count(2), steps, in0, in4, o0, o30),
1321            from_count(20)
1322        );
1323        assert_eq!(
1324            digitize_for_test(from_count(3), steps, in0, in4, o0, o30),
1325            from_count(30)
1326        );
1327        assert_eq!(
1328            digitize_for_test(from_count(4), steps, in0, in4, o0, o30),
1329            from_count(30)
1330        );
1331        assert_eq!(
1332            digitize_for_test(ONE, 1, ZERO, ONE, from_count(7), from_count(9)),
1333            from_count(7)
1334        );
1335    }
1336
1337    #[test]
1338    fn count_digitize_truncates_non_divisible_ranges() {
1339        assert_eq!(
1340            digitize_for_domain_test(
1341                SignalDomain::Count,
1342                from_count(1),
1343                4,
1344                from_count(0),
1345                from_count(3),
1346                from_count(0),
1347                from_count(10),
1348            ),
1349            from_count(3)
1350        );
1351    }
1352}
1353
1354#[cfg(test)]
1355mod map_tests {
1356    use super::{map_linear_for_domain_test, map_linear_for_test};
1357    use crate::foundation::{from_count, SignalDomain, ONE, ZERO};
1358
1359    #[cfg(feature = "signal-i32")]
1360    use crate::runtime_impl::kind_tag::{I32MapPlan, KindTag};
1361
1362    #[test]
1363    fn map_precompute_endpoints_mid_and_degenerate() {
1364        let i0 = from_count(0);
1365        let i4 = from_count(4);
1366        let o0 = from_count(0);
1367        let o40 = from_count(40);
1368        assert_eq!(map_linear_for_test(from_count(0), i0, i4, o0, o40), o0);
1369        assert_eq!(
1370            map_linear_for_test(from_count(2), i0, i4, o0, o40),
1371            from_count(20)
1372        );
1373        assert_eq!(map_linear_for_test(from_count(4), i0, i4, o0, o40), o40);
1374        assert_eq!(map_linear_for_test(from_count(8), i0, i4, o0, o40), o40);
1375        assert_eq!(map_linear_for_test(from_count(-2), i0, i4, o0, o40), o0);
1376        assert_eq!(
1377            map_linear_for_test(ONE, from_count(3), from_count(3), from_count(7), o40),
1378            from_count(7)
1379        );
1380        let mid = map_linear_for_test(
1381            #[cfg(feature = "signal-f32")]
1382            {
1383                0.5
1384            },
1385            #[cfg(feature = "signal-i32")]
1386            {
1387                ONE / 2
1388            },
1389            ZERO,
1390            ONE,
1391            ZERO,
1392            ONE,
1393        );
1394        #[cfg(feature = "signal-f32")]
1395        assert!((mid - 0.5).abs() < 1e-5);
1396        #[cfg(feature = "signal-i32")]
1397        assert_eq!(mid, ONE / 2);
1398    }
1399
1400    #[test]
1401    fn count_map_truncates_non_divisible_ranges() {
1402        assert_eq!(
1403            map_linear_for_domain_test(
1404                SignalDomain::Count,
1405                from_count(1),
1406                from_count(0),
1407                from_count(3),
1408                from_count(0),
1409                from_count(10),
1410            ),
1411            from_count(3)
1412        );
1413    }
1414
1415    #[cfg(feature = "signal-i32")]
1416    #[test]
1417    fn map_full_i32_range_uses_wide_intermediates() {
1418        assert_eq!(
1419            map_linear_for_test(i32::MIN, i32::MIN, i32::MAX, i32::MIN, i32::MAX),
1420            i32::MIN
1421        );
1422        assert_eq!(
1423            map_linear_for_test(i32::MAX, i32::MIN, i32::MAX, i32::MIN, i32::MAX),
1424            i32::MAX
1425        );
1426        assert_eq!(
1427            map_linear_for_test(0, i32::MIN, i32::MAX, i32::MIN, i32::MAX),
1428            0
1429        );
1430
1431        assert_eq!(
1432            map_linear_for_test(i32::MIN, i32::MIN, i32::MAX, i32::MAX, i32::MIN),
1433            i32::MAX
1434        );
1435        assert_eq!(
1436            map_linear_for_test(i32::MAX, i32::MIN, i32::MAX, i32::MAX, i32::MIN),
1437            i32::MIN
1438        );
1439        assert_eq!(
1440            map_linear_for_test(0, i32::MIN, i32::MAX, i32::MAX, i32::MIN),
1441            -1
1442        );
1443    }
1444
1445    #[cfg(feature = "signal-i32")]
1446    #[test]
1447    fn map_full_i32_output_range_clamps_inputs_outside_range() {
1448        assert_eq!(
1449            map_linear_for_test(i32::MIN, -1, 1, i32::MIN, i32::MAX),
1450            i32::MIN
1451        );
1452        assert_eq!(
1453            map_linear_for_test(i32::MAX, -1, 1, i32::MIN, i32::MAX),
1454            i32::MAX
1455        );
1456    }
1457
1458    #[cfg(feature = "signal-i32")]
1459    fn reference_i32_map(input: i32, in_min: i32, in_max: i32, out_min: i32, out_max: i32) -> i32 {
1460        let den = (in_max as i128) - (in_min as i128);
1461        if den == 0 {
1462            return out_min;
1463        }
1464        let t = ((input as i128) - (in_min as i128)).clamp(0, den);
1465        ((out_min as i128) + t * ((out_max as i128) - (out_min as i128)) / den) as i32
1466    }
1467
1468    #[cfg(feature = "signal-i32")]
1469    fn i32_map_plan(tag: KindTag) -> I32MapPlan {
1470        match tag {
1471            KindTag::Map { plan, .. } => plan,
1472            _ => panic!("map precompute must return a Map tag"),
1473        }
1474    }
1475
1476    #[cfg(feature = "signal-i32")]
1477    #[test]
1478    fn map_i32_uses_specialized_bind_plans() {
1479        let plan = |in_min, in_max, out_min, out_max| {
1480            i32_map_plan(KindTag::map_precomputed(
1481                SignalDomain::Count,
1482                in_min,
1483                in_max,
1484                out_min,
1485                out_max,
1486            ))
1487        };
1488
1489        assert!(matches!(plan(3, 3, 7, 9), I32MapPlan::Constant { .. }));
1490        assert!(matches!(plan(-8, 8, 5, 21), I32MapPlan::Unit { .. }));
1491        assert!(matches!(plan(0, 8, 0, 16), I32MapPlan::Scale { .. }));
1492        assert!(matches!(plan(0, 8, 0, 4), I32MapPlan::Shift { .. }));
1493        assert!(matches!(plan(0, 10, 0, 3), I32MapPlan::Divide { .. }));
1494    }
1495
1496    #[cfg(feature = "signal-i32")]
1497    #[test]
1498    #[should_panic(expected = "map precompute must return a Map tag")]
1499    fn i32_map_plan_rejects_non_map_tags() {
1500        let _ = i32_map_plan(KindTag::Not);
1501    }
1502
1503    #[cfg(feature = "signal-i32")]
1504    #[test]
1505    fn map_i32_matches_wide_reference_across_edge_and_seeded_ranges() {
1506        let cases = [
1507            (i32::MIN, i32::MAX, i32::MIN, i32::MAX),
1508            (i32::MIN, i32::MAX, i32::MAX, i32::MIN),
1509            (-65_536, 65_536, 0, 65_536),
1510            (0, 8, 0, 16),
1511            (0, 8, 0, 4),
1512            (0, 10, -37, 997),
1513            (3, 3, -5, 42),
1514        ];
1515        for (in_min, in_max, out_min, out_max) in cases {
1516            for input in [
1517                i32::MIN,
1518                in_min.saturating_sub(1),
1519                in_min,
1520                0,
1521                in_max,
1522                in_max.saturating_add(1),
1523                i32::MAX,
1524            ] {
1525                assert_eq!(
1526                    map_linear_for_domain_test(
1527                        SignalDomain::Count,
1528                        input,
1529                        in_min,
1530                        in_max,
1531                        out_min,
1532                        out_max,
1533                    ),
1534                    reference_i32_map(input, in_min, in_max, out_min, out_max),
1535                    "input={input}, in={in_min}..{in_max}, out={out_min}..{out_max}"
1536                );
1537            }
1538        }
1539
1540        let mut state = 0x9E37_79B9u32;
1541        for _ in 0..512 {
1542            let next = |state: &mut u32| {
1543                *state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
1544                *state as i32
1545            };
1546            let a = next(&mut state);
1547            let b = next(&mut state);
1548            let (in_min, in_max) = if a <= b { (a, b) } else { (b, a) };
1549            let out_min = next(&mut state);
1550            let out_max = next(&mut state);
1551            let input = next(&mut state);
1552            assert_eq!(
1553                map_linear_for_domain_test(
1554                    SignalDomain::Count,
1555                    input,
1556                    in_min,
1557                    in_max,
1558                    out_min,
1559                    out_max,
1560                ),
1561                reference_i32_map(input, in_min, in_max, out_min, out_max),
1562            );
1563        }
1564    }
1565}