Skip to main content

wyrd/runtime_impl/
bind.rs

1//! Bind: turn a validated [`Weave`] into a dense, executable [`Runtime`].
2//!
3//! Interns host paths and command names, builds CSR inbound edges, topo order,
4//! kind-dispatch tags, delay rings, and sense seed lists so [`Runtime::loom`]
5//! allocates no topology after bind. Host resolves `sense_id` / `path_id` once
6//! at setup; the hot path uses only dense ids.
7
8#![allow(clippy::result_large_err)] // Preserve contextual public BindError payloads.
9
10use std::collections::BTreeMap;
11use std::string::String;
12use std::vec::Vec;
13
14use core::sync::atomic::{AtomicUsize, Ordering};
15
16use crate::authoring::{validate, Budget, Weave};
17use crate::foundation::{
18    port_slot, ports_of, CalcOp, HostTime, KnotId, KnotKind, PortDir, PortSlot, Seed, Signal, ZERO,
19};
20
21use crate::runtime_impl::error::{BindError, HandleError, RecipeEndpoint, RecipeResolveError};
22use crate::runtime_impl::handles::{CmdId, HostPathId, KnotHandle, SenseId};
23use crate::runtime_impl::outbox::{Emit, Outbox, PortWriter, SignalOutSample};
24
25/// Bind-time sense seed entry — only Sense knots, so loom need not scan all knots.
26#[derive(Clone, Copy, Debug)]
27pub(crate) enum SenseSeed {
28    Constant { kid: KnotId, value: Signal },
29    SignalIn { kid: KnotId },
30    OnStart { kid: KnotId },
31}
32
33/// Bind-time options (sandbox / host policy).
34#[derive(Clone, Debug)]
35pub struct BindOpts {
36    /// Optional host PRNG seed for `Random` knots (mixed with weave id at bind).
37    pub seed: Option<Seed>,
38    /// Hard cap on `EmitCommand` outbox entries per frame (default 8).
39    ///
40    /// Further emits in the same frame are dropped without panicking. Their
41    /// exact count is exposed through [`Outbox::dropped_emits`] until the next
42    /// [`Runtime::begin_frame`]. A cap of zero drops every emit.
43    pub max_emits_per_tick: u16,
44    /// Validate budget (default matches [`Budget::default`]).
45    pub budget: Budget,
46}
47
48impl Default for BindOpts {
49    fn default() -> Self {
50        Self {
51            seed: None,
52            max_emits_per_tick: 8,
53            budget: Budget::default(),
54        }
55    }
56}
57
58#[derive(Clone, Debug)]
59pub(crate) struct ResolvedKnot {
60    pub(crate) kind: KnotKind,
61    /// For SignalOut / Emit after intern
62    pub(crate) path: Option<HostPathId>,
63    pub(crate) cmd: Option<CmdId>,
64}
65
66/// Bound runtime: dense buffers, topo order, intern tables, stateful rune storage.
67///
68/// Sole executable artifact after bind. Sample senses through [`PortWriter`],
69/// settle with [`Self::loom`], then read [`Self::outbox`].
70pub struct Runtime {
71    pub(crate) owner: usize,
72    pub(crate) knots: Vec<ResolvedKnot>,
73    /// Author name → KnotId
74    pub(crate) name_to_id: BTreeMap<String, KnotId>,
75    pub(crate) path_names: Vec<String>,
76    pub(crate) cmd_names: Vec<String>,
77    /// CSR inbound: edges in `inbound_edges[inbound_off[ki]..inbound_off[ki+1]]`.
78    /// Each edge is (from_knot, from_slot, to_slot).
79    pub(crate) inbound_off: Vec<u32>,
80    pub(crate) inbound_edges: Vec<(KnotId, PortSlot, PortSlot)>,
81    /// Absolute `port_vals` indices of In ports to zero each loom (flat, bind-sized).
82    pub(crate) clear_port_idx: Vec<usize>,
83    pub(crate) topo: Vec<KnotId>,
84    /// Bind-time kind dispatch tags (one per knot; no per-tick from_kind).
85    pub(crate) kind_tags: Vec<crate::runtime_impl::kind_tag::KindTag>,
86    /// Only Constant / SignalIn / OnStart — loom seeds these without scanning all knots.
87    pub(crate) sense_seeds: Vec<SenseSeed>,
88    /// Host-fed sense outputs (SignalIn).
89    pub(crate) sense_values: Vec<Signal>,
90    /// Port value store: indexed by (knot_idx * MAX_PORTS + slot)
91    pub(crate) port_vals: Vec<Signal>,
92    pub(crate) max_ports: usize,
93    /// Knot state for stateful runes
94    pub(crate) prev_in: Vec<Signal>,
95    pub(crate) prev_dec: Vec<Signal>,
96    pub(crate) counter: Vec<i32>,
97    pub(crate) flag: Vec<bool>,
98    pub(crate) timer_left: Vec<u16>,
99    pub(crate) on_start_done: Vec<bool>,
100    /// Delay ring: flat buffer + per-knot (offset, len, head).
101    pub(crate) delay_buf: Vec<Signal>,
102    pub(crate) delay_off: Vec<u16>,
103    pub(crate) delay_len: Vec<u16>,
104    pub(crate) delay_head: Vec<u16>,
105    pub(crate) out_signals: Vec<SignalOutSample>,
106    pub(crate) out_emits: Vec<Emit>,
107    pub(crate) dropped_emits: usize,
108    pub(crate) max_emits_per_tick: u16,
109    pub(crate) tick: u64,
110    pub(crate) phase: u8,
111    /// Deterministic xorshift state for Random knots (never zero).
112    pub(crate) rng: u64,
113    /// `fnv1a64(weave.id)` mixed into seeds at bind and [`Self::reseed`].
114    pub(crate) seed_mix: u64,
115    /// Immutable graph and bind-policy fingerprint, computed once at bind.
116    pub(crate) state_fingerprint: u64,
117}
118
119const MAX_PORTS: usize = 8;
120static NEXT_RUNTIME_OWNER: AtomicUsize = AtomicUsize::new(1);
121
122fn fnv1a64(data: &[u8]) -> u64 {
123    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
124    for &b in data {
125        h ^= b as u64;
126        h = h.wrapping_mul(0x0100_0000_01b3);
127    }
128    h
129}
130
131/// Convert an authoring-order index to the compact runtime knot id.
132///
133/// Both bind passes use this guard: the first protects name and intern tables,
134/// and the second protects the dense hot-path arrays built from those tables.
135fn dense_knot_id(index: usize, weave_id: &str) -> Result<KnotId, BindError> {
136    KnotId::try_from(index).map_err(|_| BindError::CapacityExceeded {
137        weave_id: String::from(weave_id),
138        resource: "knot",
139        count: index + 1,
140    })
141}
142
143/// Intern a SignalOut path while preserving compact host-path ids.
144fn intern_host_path(
145    owner: usize,
146    path: &str,
147    path_index: &mut BTreeMap<String, HostPathId>,
148    path_names: &mut Vec<String>,
149    weave_id: &str,
150) -> Result<HostPathId, BindError> {
151    if let Some(id) = path_index.get(path) {
152        return Ok(*id);
153    }
154
155    let index = u16::try_from(path_names.len()).map_err(|_| BindError::CapacityExceeded {
156        weave_id: String::from(weave_id),
157        resource: "host path",
158        count: path_names.len() + 1,
159    })?;
160    let id = HostPathId::new(owner, index);
161    path_names.push(String::from(path));
162    path_index.insert(String::from(path), id);
163    Ok(id)
164}
165
166/// Intern an EmitCommand name while preserving compact command ids.
167fn intern_command(
168    owner: usize,
169    name: &str,
170    cmd_index: &mut BTreeMap<String, CmdId>,
171    cmd_names: &mut Vec<String>,
172    weave_id: &str,
173) -> Result<CmdId, BindError> {
174    if let Some(id) = cmd_index.get(name) {
175        return Ok(*id);
176    }
177
178    let index = u16::try_from(cmd_names.len()).map_err(|_| BindError::CapacityExceeded {
179        weave_id: String::from(weave_id),
180        resource: "command",
181        count: cmd_names.len() + 1,
182    })?;
183    let id = CmdId::new(owner, index);
184    cmd_names.push(String::from(name));
185    cmd_index.insert(String::from(name), id);
186    Ok(id)
187}
188
189/// Add a delay extent while preserving the error reported by the original bind
190/// phase when the host architecture's usize capacity would overflow.
191fn checked_delay_buffer_len(
192    current_len: usize,
193    len: usize,
194    weave_id: &str,
195) -> Result<usize, BindError> {
196    current_len
197        .checked_add(len)
198        .ok_or_else(|| BindError::CapacityExceeded {
199            weave_id: String::from(weave_id),
200            resource: "delay buffer",
201            count: usize::MAX,
202        })
203}
204
205/// Calculate the next delay-ring extent before allocating the backing buffer.
206///
207/// `current_len` is passed separately so the compact-index guard remains
208/// directly testable even though a validated weave cannot reach its impossible
209/// overflow state.
210fn delay_buffer_layout(
211    current_len: usize,
212    len: usize,
213    weave_id: &str,
214) -> Result<(u16, usize), BindError> {
215    let offset = u16::try_from(current_len).map_err(|_| BindError::CapacityExceeded {
216        weave_id: String::from(weave_id),
217        resource: "delay buffer offset",
218        count: current_len,
219    })?;
220    let new_len = checked_delay_buffer_len(current_len, len, weave_id)?;
221    if new_len > u16::MAX as usize {
222        return Err(BindError::CapacityExceeded {
223            weave_id: String::from(weave_id),
224            resource: "delay buffer",
225            count: new_len,
226        });
227    }
228    Ok((offset, new_len))
229}
230
231impl Runtime {
232    /// Validate and consume a weave into dense executable state.
233    ///
234    /// # Errors
235    ///
236    /// Returns [`BindError`] when budget validation fails, dense capacity is
237    /// exceeded, a port cannot be resolved, or topo order cannot be built.
238    pub fn bind(weave: Weave, opts: BindOpts) -> Result<Self, BindError> {
239        let weave_id = String::from(weave.id());
240        validate(&weave, &opts.budget).map_err(|source| BindError::InvalidWeave {
241            weave_id: weave_id.clone(),
242            source,
243        })?;
244
245        Self::bind_validated(weave, opts, weave_id)
246    }
247
248    /// Build dense state from a weave that already passed structural and budget validation.
249    ///
250    /// Keeping this phase separate makes the defensive error handling below
251    /// testable without exposing a way to bypass validation to callers.
252    fn bind_validated(weave: Weave, opts: BindOpts, weave_id: String) -> Result<Self, BindError> {
253        Self::bind_validated_with_preinterned_names(weave, opts, weave_id, Vec::new(), Vec::new())
254    }
255
256    /// Internal bind entrypoint with explicit preinterned names for capacity
257    /// validation. Normal binding always starts with empty tables.
258    ///
259    /// Bind phases: intern host paths/commands → build CSR inbound edges and
260    /// topo order → patch kind-dispatch tags → size delay rings and sense seeds
261    /// → compute the immutable state fingerprint.
262    fn bind_validated_with_preinterned_names(
263        weave: Weave,
264        opts: BindOpts,
265        weave_id: String,
266        mut path_names: Vec<String>,
267        mut cmd_names: Vec<String>,
268    ) -> Result<Self, BindError> {
269        let owner = NEXT_RUNTIME_OWNER.fetch_add(1, Ordering::Relaxed);
270        reserve_owner(owner, &weave_id)?;
271
272        let mut name_to_id = BTreeMap::new();
273        let mut path_index: BTreeMap<String, HostPathId> = BTreeMap::new();
274        let mut cmd_index: BTreeMap<String, CmdId> = BTreeMap::new();
275
276        let mut knots = Vec::with_capacity(weave.knots().len());
277        for (i, k) in weave.knots().iter().enumerate() {
278            let id = dense_knot_id(i, &weave_id)?;
279            name_to_id.insert(k.id.clone(), id);
280
281            let (path, cmd) = match &k.kind {
282                KnotKind::SignalOut { path, .. } => (
283                    Some(intern_host_path(
284                        owner,
285                        path,
286                        &mut path_index,
287                        &mut path_names,
288                        &weave_id,
289                    )?),
290                    None,
291                ),
292                KnotKind::EmitCommand { name } => (
293                    None,
294                    Some(intern_command(
295                        owner,
296                        name,
297                        &mut cmd_index,
298                        &mut cmd_names,
299                        &weave_id,
300                    )?),
301                ),
302                _ => (None, None),
303            };
304
305            knots.push(ResolvedKnot {
306                kind: k.kind.clone(),
307                path,
308                cmd,
309            });
310        }
311
312        let mut threads = Vec::new();
313        for t in weave.threads() {
314            let fk = *name_to_id
315                .get(&t.from.knot)
316                .ok_or_else(|| BindError::InvalidReference {
317                    weave_id: weave_id.clone(),
318                    knot: t.from.knot.clone(),
319                    port: t.from.port.clone(),
320                })?;
321            let tk = *name_to_id
322                .get(&t.to.knot)
323                .ok_or_else(|| BindError::InvalidReference {
324                    weave_id: weave_id.clone(),
325                    knot: t.to.knot.clone(),
326                    port: t.to.port.clone(),
327                })?;
328            let fs = port_slot(&knots[usize::from(fk)].kind, &t.from.port).ok_or_else(|| {
329                BindError::InvalidReference {
330                    weave_id: weave_id.clone(),
331                    knot: t.from.knot.clone(),
332                    port: t.from.port.clone(),
333                }
334            })?;
335            let ts = port_slot(&knots[usize::from(tk)].kind, &t.to.port).ok_or_else(|| {
336                BindError::InvalidReference {
337                    weave_id: weave_id.clone(),
338                    knot: t.to.knot.clone(),
339                    port: t.to.port.clone(),
340                }
341            })?;
342            threads.push((fk, fs, tk, ts));
343        }
344
345        let topo = topo_order(knots.len(), &threads).ok_or_else(|| BindError::InvalidTopology {
346            weave_id: weave_id.clone(),
347        })?;
348
349        let n = knots.len();
350        let mut inbound_lists: Vec<Vec<(KnotId, PortSlot, PortSlot)>> = alloc::vec![Vec::new(); n];
351        for &(f, fs, t, ts) in &threads {
352            inbound_lists[usize::from(t)].push((f, fs, ts));
353        }
354        let mut inbound_off = Vec::with_capacity(n + 1);
355        let mut inbound_edges = Vec::with_capacity(threads.len());
356        inbound_off.push(0);
357        for list in &inbound_lists {
358            inbound_edges.extend_from_slice(list);
359            inbound_off.push(inbound_edges.len() as u32);
360        }
361
362        let mut clear_port_idx = Vec::new();
363        let mut act_signals = 0usize;
364        let mut act_emits = 0usize;
365        let mut sense_seeds = Vec::new();
366        let mut kind_tags: Vec<crate::runtime_impl::kind_tag::KindTag> = knots
367            .iter()
368            .map(|k| crate::runtime_impl::kind_tag::KindTag::from_kind(&k.kind))
369            .collect();
370        for (ki, k) in knots.iter().enumerate() {
371            let kid = dense_knot_id(ki, &weave_id)?;
372            for p in ports_of(&k.kind) {
373                if p.dir == PortDir::In {
374                    // Only unwired Ins are zeroed each loom; wired Ins are gathered.
375                    let wired = inbound_lists[ki].iter().any(|&(_, _, ts)| ts == p.slot);
376                    if !wired {
377                        clear_port_idx.push(ki * MAX_PORTS + usize::from(p.slot));
378                    }
379                }
380            }
381            match &k.kind {
382                KnotKind::Constant { value, .. } => {
383                    sense_seeds.push(SenseSeed::Constant { kid, value: *value });
384                }
385                KnotKind::SignalIn { .. } => {
386                    sense_seeds.push(SenseSeed::SignalIn { kid });
387                }
388                KnotKind::OnStart => {
389                    sense_seeds.push(SenseSeed::OnStart { kid });
390                }
391                KnotKind::SignalOut { .. } => act_signals += 1,
392                KnotKind::EmitCommand { .. } => {
393                    act_emits += 1;
394                    let enable_wired = inbound_lists[ki]
395                        .iter()
396                        .any(|&(_, _, ts)| ts == PortSlot::new(1));
397                    kind_tags[ki] =
398                        crate::runtime_impl::kind_tag::KindTag::EmitCommand { enable_wired };
399                }
400                KnotKind::Random { .. } => {
401                    let mut min_wired = false;
402                    let mut max_wired = false;
403                    for &(_, _, ts) in &inbound_lists[ki] {
404                        if ts == PortSlot::new(0) {
405                            min_wired = true;
406                        } else if ts == PortSlot::new(1) {
407                            max_wired = true;
408                        }
409                    }
410                    kind_tags[ki] = kind_tags[ki].with_random_wiring(min_wired, max_wired);
411                }
412                KnotKind::Calc {
413                    domain,
414                    op: CalcOp::Div,
415                } => {
416                    if let Some(&(from, _, _)) = inbound_lists[ki]
417                        .iter()
418                        .find(|&&(_, _, ts)| ts == PortSlot::new(1))
419                    {
420                        if let KnotKind::Constant { value, .. } = knots[usize::from(from)].kind {
421                            kind_tags[ki] = crate::runtime_impl::kind_tag::KindTag::calc_div_const(
422                                *domain, value,
423                            );
424                        }
425                    }
426                }
427                _ => {}
428            }
429        }
430
431        let mut delay_buf = Vec::new();
432        let mut delay_off = alloc::vec![0u16; n];
433        let mut delay_len = alloc::vec![0u16; n];
434        let delay_head = alloc::vec![0u16; n];
435        for (i, k) in knots.iter().enumerate() {
436            if let KnotKind::Delay { ticks } = k.kind {
437                let len = ticks as usize;
438                if len > 0 {
439                    let (offset, new_len) = delay_buffer_layout(delay_buf.len(), len, &weave_id)?;
440                    delay_off[i] = offset;
441                    delay_len[i] = ticks;
442                    delay_buf.resize(new_len, ZERO);
443                }
444            }
445        }
446
447        let out_signals = Vec::with_capacity(act_signals);
448        let out_emits = Vec::with_capacity(act_emits.min(usize::from(opts.max_emits_per_tick)));
449
450        let base = opts.seed.unwrap_or(Seed(0xC0FF_EE00_D15C_AFEDu64));
451        let seed_mix = fnv1a64(weave.id().as_bytes());
452        let rng = (base.0 ^ seed_mix) | 1;
453        let state_fingerprint = crate::runtime_impl::runtime_state::runtime_fingerprint_for(
454            &knots,
455            &threads,
456            &path_names,
457            &cmd_names,
458            opts.max_emits_per_tick,
459            seed_mix,
460            opts.seed,
461        );
462
463        Ok(Runtime {
464            owner,
465            knots,
466            name_to_id,
467            path_names,
468            cmd_names,
469            inbound_off,
470            inbound_edges,
471            clear_port_idx,
472            topo,
473            kind_tags,
474            sense_seeds,
475            sense_values: alloc::vec![ZERO; n],
476            port_vals: alloc::vec![ZERO; n * MAX_PORTS],
477            max_ports: MAX_PORTS,
478            prev_in: alloc::vec![ZERO; n],
479            prev_dec: alloc::vec![ZERO; n],
480            counter: alloc::vec![0; n],
481            flag: alloc::vec![false; n],
482            timer_left: alloc::vec![0; n],
483            on_start_done: alloc::vec![false; n],
484            delay_buf,
485            delay_off,
486            delay_len,
487            delay_head,
488            out_signals,
489            out_emits,
490            dropped_emits: 0,
491            max_emits_per_tick: opts.max_emits_per_tick,
492            tick: 0,
493            phase: 0,
494            rng,
495            seed_mix,
496            state_fingerprint,
497        })
498    }
499
500    /// Restore PRNG stream (room retry). Same mix as bind: `seed ^ fnv(weave.id) | 1`.
501    pub fn reseed(&mut self, seed: Seed) {
502        self.rng = (seed.0 ^ self.seed_mix) | 1;
503    }
504
505    /// Next u32 from the bind-seeded xorshift64 stream (`rng` is never zero).
506    pub(crate) fn next_rng_u32(&mut self) -> u32 {
507        let mut x = self.rng;
508        x ^= x << 13;
509        x ^= x >> 7;
510        x ^= x << 17;
511        self.rng = x;
512        x as u32
513    }
514
515    /// Resolve a `SignalIn` author name to a dense sense id (setup only).
516    pub fn sense_id(&self, name: &str) -> Option<SenseId> {
517        let knot = self.name_to_id.get(name).copied()?;
518        if !matches!(
519            self.knots.get(usize::from(knot))?.kind,
520            KnotKind::SignalIn { .. }
521        ) {
522            return None;
523        }
524        Some(SenseId::new(self.owner, knot.get()))
525    }
526
527    /// Resolve a required `SignalIn` knot for a typed recipe port.
528    pub fn required_sense(&self, name: &str) -> Result<SenseId, RecipeResolveError> {
529        let Some(knot) = self.name_to_id.get(name).copied() else {
530            return Err(RecipeResolveError::Missing {
531                endpoint: RecipeEndpoint::SignalIn,
532                name: String::from(name),
533            });
534        };
535        if !matches!(
536            self.knots
537                .get(usize::from(knot))
538                .map(|resolved| &resolved.kind),
539            Some(KnotKind::SignalIn { .. })
540        ) {
541            return Err(RecipeResolveError::Invalid {
542                endpoint: RecipeEndpoint::SignalIn,
543                name: String::from(name),
544                reason: "the knot is not a SignalIn",
545            });
546        }
547        Ok(SenseId::new(self.owner, knot.get()))
548    }
549
550    /// Resolve an author knot name for checked tooling access.
551    pub fn knot_id(&self, name: &str) -> Option<KnotHandle> {
552        self.name_to_id
553            .get(name)
554            .map(|knot| KnotHandle::new(self.owner, knot.get()))
555    }
556
557    /// Resolve a required author knot for a typed recipe port.
558    pub fn required_knot(&self, name: &str) -> Result<KnotHandle, RecipeResolveError> {
559        self.knot_id(name)
560            .ok_or_else(|| RecipeResolveError::Missing {
561                endpoint: RecipeEndpoint::Knot,
562                name: String::from(name),
563            })
564    }
565
566    /// Resolve a `SignalOut` path string interned at bind.
567    pub fn path_id(&self, path: &str) -> Option<HostPathId> {
568        self.path_names
569            .iter()
570            .position(|p| p == path)
571            .and_then(|i| u16::try_from(i).ok())
572            .map(|index| HostPathId::new(self.owner, index))
573    }
574
575    /// Resolve a required `SignalOut` path for a typed recipe port.
576    pub fn required_path(&self, path: &str) -> Result<HostPathId, RecipeResolveError> {
577        self.path_id(path)
578            .ok_or_else(|| RecipeResolveError::Missing {
579                endpoint: RecipeEndpoint::SignalOut,
580                name: String::from(path),
581            })
582    }
583
584    /// Resolve an `EmitCommand` name interned at bind.
585    pub fn cmd_id(&self, name: &str) -> Option<CmdId> {
586        self.cmd_names
587            .iter()
588            .position(|candidate| candidate == name)
589            .and_then(|i| u16::try_from(i).ok())
590            .map(|index| CmdId::new(self.owner, index))
591    }
592
593    /// Resolve a required `EmitCommand` name for a typed recipe port.
594    pub fn required_command(&self, name: &str) -> Result<CmdId, RecipeResolveError> {
595        self.cmd_id(name)
596            .ok_or_else(|| RecipeResolveError::Missing {
597                endpoint: RecipeEndpoint::EmitCommand,
598                name: String::from(name),
599            })
600    }
601
602    /// Interned path string for a dense host path id.
603    pub fn path_name(&self, id: HostPathId) -> Result<&str, HandleError> {
604        self.ensure_owner(id.owner, "host path")?;
605        self.path_names
606            .get(usize::from(id.index))
607            .map(|s| s.as_str())
608            .ok_or(HandleError::InvalidHostPath { path: id })
609    }
610
611    /// Interned emit command name for a dense command id.
612    pub fn cmd_name(&self, id: CmdId) -> Result<&str, HandleError> {
613        self.ensure_owner(id.owner, "command")?;
614        self.cmd_names
615            .get(usize::from(id.index))
616            .map(|s| s.as_str())
617            .ok_or(HandleError::InvalidCommand { cmd: id })
618    }
619
620    /// Start a frame: set tick and clear acts and dropped-emit telemetry.
621    pub fn begin_frame(&mut self, time: HostTime) {
622        self.tick = time.tick;
623        self.phase = 1;
624        self.out_signals.clear();
625        self.out_emits.clear();
626        self.dropped_emits = 0;
627    }
628
629    /// Borrow for host sense writes (`set_sense` with dense ids only).
630    pub fn port_writer(&mut self) -> PortWriter<'_> {
631        PortWriter { rt: self }
632    }
633
634    /// Read-only view of acts and dropped-emit telemetry for this frame.
635    pub fn outbox(&self) -> Outbox<'_> {
636        Outbox {
637            signals: &self.out_signals,
638            emits: &self.out_emits,
639            dropped_emits: self.dropped_emits,
640        }
641    }
642
643    /// Capacity of the SignalOut outbox buffer (reserved at bind).
644    pub fn outbox_signals_capacity(&self) -> usize {
645        self.out_signals.capacity()
646    }
647
648    /// Length of the flat delay ring (sized at bind).
649    pub fn delay_buf_len(&self) -> usize {
650        self.delay_buf.len()
651    }
652
653    #[inline]
654    pub(crate) fn port_index(&self, knot: KnotId, slot: PortSlot) -> usize {
655        usize::from(knot) * self.max_ports + usize::from(slot)
656    }
657
658    /// Safe OOB-tolerant read (returns ZERO past end). Used by tests and host tooling.
659    #[inline]
660    pub fn get_port_checked(
661        &self,
662        knot: KnotHandle,
663        slot: PortSlot,
664    ) -> Result<Signal, HandleError> {
665        self.ensure_owner(knot.owner, "knot")?;
666        let dense = KnotId::try_from(usize::from(knot.index))
667            .map_err(|_| HandleError::InvalidKnot { knot })?;
668        let Some(resolved) = self.knots.get(usize::from(dense)) else {
669            return Err(HandleError::InvalidKnot { knot });
670        };
671        if !ports_of(&resolved.kind)
672            .iter()
673            .any(|info| info.slot == slot)
674        {
675            return Err(HandleError::InvalidPort { knot, port: slot });
676        }
677        let i = self.port_index(dense, slot);
678        self.port_vals
679            .get(i)
680            .copied()
681            .ok_or(HandleError::InvalidPort { knot, port: slot })
682    }
683
684    /// Safe OOB-tolerant write (no-op past end). Used by tests and host tooling.
685    #[inline]
686    pub fn set_port_checked(
687        &mut self,
688        knot: KnotHandle,
689        slot: PortSlot,
690        v: Signal,
691    ) -> Result<(), HandleError> {
692        self.ensure_owner(knot.owner, "knot")?;
693        let dense = KnotId::try_from(usize::from(knot.index))
694            .map_err(|_| HandleError::InvalidKnot { knot })?;
695        let Some(resolved) = self.knots.get(usize::from(dense)) else {
696            return Err(HandleError::InvalidKnot { knot });
697        };
698        if !ports_of(&resolved.kind)
699            .iter()
700            .any(|info| info.slot == slot)
701        {
702            return Err(HandleError::InvalidPort { knot, port: slot });
703        }
704        let i = self.port_index(dense, slot);
705        let p = self
706            .port_vals
707            .get_mut(i)
708            .ok_or(HandleError::InvalidPort { knot, port: slot })?;
709        *p = v;
710        Ok(())
711    }
712
713    /// Dense-id alias for [`Self::get_port_checked`] (tooling and bind-shape tests).
714    #[inline]
715    #[allow(dead_code)]
716    pub(crate) fn get_port(&self, knot: KnotId, slot: PortSlot) -> Result<Signal, HandleError> {
717        let handle = KnotHandle::new(self.owner, knot.get());
718        self.get_port_checked(handle, slot)
719    }
720
721    /// Dense-id alias for [`Self::set_port_checked`] (tooling and bind-shape tests).
722    #[inline]
723    #[allow(dead_code)]
724    pub(crate) fn set_port(
725        &mut self,
726        knot: KnotId,
727        slot: PortSlot,
728        v: Signal,
729    ) -> Result<(), HandleError> {
730        let handle = KnotHandle::new(self.owner, knot.get());
731        self.set_port_checked(handle, slot, v)
732    }
733
734    fn ensure_owner(&self, owner: usize, handle: &'static str) -> Result<(), HandleError> {
735        if owner == self.owner {
736            Ok(())
737        } else {
738            Err(HandleError::ForeignRuntime { handle })
739        }
740    }
741
742    /// Number of bind-time kind tags (equals knot count after successful bind).
743    ///
744    /// Bind-shape introspection for tests and tooling — not used on the settle hot path.
745    pub fn kind_tag_count(&self) -> usize {
746        self.kind_tags.len()
747    }
748
749    /// Flat clear-index count (all In ports across the weave).
750    ///
751    /// Bind-shape introspection for tests and tooling — not used on the settle hot path.
752    pub fn clear_port_index_count(&self) -> usize {
753        self.clear_port_idx.len()
754    }
755
756    /// CSR inbound edge count.
757    ///
758    /// Bind-shape introspection for tests and tooling — not used on the settle hot path.
759    pub fn inbound_edge_count(&self) -> usize {
760        self.inbound_edges.len()
761    }
762
763    /// Hot-path port read when `knot`/`slot` are bind-validated (in-range).
764    #[inline]
765    pub(crate) fn get_port_hot(&self, knot: KnotId, slot: PortSlot) -> Signal {
766        let i = self.port_index(knot, slot);
767        debug_assert!(i < self.port_vals.len());
768        self.port_vals[i]
769    }
770
771    /// Hot-path port write when `knot`/`slot` are bind-validated.
772    #[inline]
773    pub(crate) fn set_port_hot(&mut self, knot: KnotId, slot: PortSlot, v: Signal) {
774        let i = self.port_index(knot, slot);
775        debug_assert!(i < self.port_vals.len());
776        self.port_vals[i] = v;
777    }
778
779    pub(crate) fn push_signal_out(&mut self, path: HostPathId, value: Signal) {
780        self.out_signals.push(SignalOutSample { path, value });
781    }
782
783    pub(crate) fn push_emit(&mut self, cmd: CmdId, payload: Signal) {
784        if self.out_emits.len() >= usize::from(self.max_emits_per_tick) {
785            self.dropped_emits = self.dropped_emits.saturating_add(1);
786            return;
787        }
788        self.out_emits.push(Emit { cmd, payload });
789    }
790}
791
792fn reserve_owner(owner: usize, weave_id: &str) -> Result<(), BindError> {
793    if owner == usize::MAX {
794        return Err(BindError::CapacityExceeded {
795            weave_id: String::from(weave_id),
796            resource: "runtime owner token",
797            count: owner,
798        });
799    }
800    Ok(())
801}
802
803fn topo_order(n: usize, threads: &[(KnotId, PortSlot, KnotId, PortSlot)]) -> Option<Vec<KnotId>> {
804    let mut indeg = alloc::vec![0u32; n];
805    let mut adj: Vec<Vec<usize>> = alloc::vec![Vec::new(); n];
806    for &(f, _, t, _) in threads {
807        let a = usize::from(f);
808        let b = usize::from(t);
809        if a != b {
810            adj[a].push(b);
811            indeg[b] += 1;
812        }
813    }
814    let mut q: Vec<usize> = indeg
815        .iter()
816        .enumerate()
817        .filter_map(|(i, d)| if *d == 0 { Some(i) } else { None })
818        .collect();
819    let mut order = Vec::with_capacity(n);
820    while let Some(u) = q.pop() {
821        order.push(KnotId::try_from(u).ok()?);
822        for &v in &adj[u] {
823            indeg[v] -= 1;
824            if indeg[v] == 0 {
825                q.push(v);
826            }
827        }
828    }
829    if order.len() != n {
830        return None;
831    }
832    Some(order)
833}
834
835#[cfg(test)]
836mod tests {
837    use super::*;
838    use crate::authoring::{KnotDef, PortRefDef, ThreadDef, Weave, WeaveDef};
839    use crate::foundation::{CalcOp, FlagPriority, KnotKind, NumericPath, SignalDomain, ONE};
840    use std::vec;
841
842    fn unchecked_weave(id: &str, knots: Vec<KnotDef>, threads: Vec<ThreadDef>) -> Weave {
843        Weave::from_validated(WeaveDef {
844            id: String::from(id),
845            numeric: NumericPath::compiled(),
846            knots,
847            threads,
848        })
849    }
850
851    fn bind_unchecked(id: &str, knots: Vec<KnotDef>, threads: Vec<ThreadDef>) -> BindError {
852        Runtime::bind_validated(
853            unchecked_weave(id, knots, threads),
854            BindOpts::default(),
855            String::from(id),
856        )
857        .err()
858        .expect("malformed internal weave must not bind")
859    }
860
861    fn knot(id: &str, kind: KnotKind) -> KnotDef {
862        KnotDef {
863            id: String::from(id),
864            kind,
865        }
866    }
867
868    #[test]
869    fn sense_seeds_lists_only_sense_knots() {
870        let mut b = Weave::builder("s").unwrap();
871        let k_in = b
872            .knot("in", KnotKind::signal_in(SignalDomain::Bool))
873            .unwrap();
874        let _k_c = b
875            .knot("c", KnotKind::constant(ONE, SignalDomain::Bool))
876            .unwrap();
877        let k_n = b.knot("n", KnotKind::Not).unwrap();
878        let k_out = b
879            .knot("out", KnotKind::signal_out("y", SignalDomain::Bool))
880            .unwrap();
881        let from = b.output(&k_in, "out").unwrap();
882        let to = b.input(&k_n, "in").unwrap();
883        b.connect(from, to).unwrap();
884        let from = b.output(&k_n, "out").unwrap();
885        let to = b.input(&k_out, "in").unwrap();
886        b.connect(from, to).unwrap();
887        let weave = b.build().unwrap();
888        let rt = Runtime::bind(weave.clone(), BindOpts::default()).unwrap();
889        assert_eq!(rt.sense_seeds.len(), 2, "SignalIn + Constant only");
890        assert!(rt
891            .sense_seeds
892            .iter()
893            .any(|s| matches!(s, SenseSeed::SignalIn { .. })));
894        assert!(rt
895            .sense_seeds
896            .iter()
897            .any(|s| matches!(s, SenseSeed::Constant { value, .. } if *value == ONE)));
898        let mut b = Weave::builder("e").unwrap();
899        let k_btn = b
900            .knot("btn", KnotKind::signal_in(SignalDomain::Bool))
901            .unwrap();
902        let k_em = b.knot("em", KnotKind::emit_command("fire")).unwrap();
903        let from = b.output(&k_btn, "out").unwrap();
904        let to = b.input(&k_em, "trigger").unwrap();
905        b.connect(from, to).unwrap();
906        let weave = b.build().unwrap();
907        let rt = Runtime::bind(
908            weave.clone(),
909            BindOpts {
910                seed: Some(Seed(1)),
911                ..BindOpts::default()
912            },
913        )
914        .unwrap();
915        let em = *rt.name_to_id.get("em").expect("em knot");
916        assert!(matches!(
917            rt.kind_tags[usize::from(em)],
918            crate::runtime_impl::kind_tag::KindTag::EmitCommand {
919                enable_wired: false
920            }
921        ));
922    }
923
924    #[test]
925    fn cmd_name_and_path_name_lookup() {
926        let mut b = Weave::builder("e").unwrap();
927        let k_btn = b
928            .knot("btn", KnotKind::signal_in(SignalDomain::Bool))
929            .unwrap();
930        let k_em = b.knot("em", KnotKind::emit_command("fire")).unwrap();
931        let k_out = b
932            .knot("out", KnotKind::signal_out("y", SignalDomain::Bool))
933            .unwrap();
934        let from = b.output(&k_btn, "out").unwrap();
935        let to = b.input(&k_em, "trigger").unwrap();
936        b.connect(from, to).unwrap();
937        let from = b.output(&k_btn, "out").unwrap();
938        let to = b.input(&k_out, "in").unwrap();
939        b.connect(from, to).unwrap();
940        let weave = b.build().unwrap();
941        let rt = Runtime::bind(
942            weave.clone(),
943            BindOpts {
944                seed: Some(Seed(1)),
945                ..BindOpts::default()
946            },
947        )
948        .unwrap();
949        let cmd = rt.knots[usize::from(*rt.name_to_id.get("em").unwrap())]
950            .cmd
951            .unwrap();
952        assert_eq!(rt.cmd_name(cmd), Ok("fire"));
953        let invalid_cmd = CmdId::new(rt.owner, 99);
954        assert_eq!(
955            rt.cmd_name(invalid_cmd),
956            Err(HandleError::InvalidCommand { cmd: invalid_cmd })
957        );
958        let path = rt.path_id("y").unwrap();
959        assert_eq!(rt.path_name(path), Ok("y"));
960        let invalid_path = HostPathId::new(rt.owner, 99);
961        assert_eq!(
962            rt.path_name(invalid_path),
963            Err(HandleError::InvalidHostPath { path: invalid_path })
964        );
965    }
966
967    #[test]
968    fn topo_order_detects_cycle() {
969        let a = KnotId::try_from(0usize).unwrap();
970        let b = KnotId::try_from(1usize).unwrap();
971        let threads = [
972            (a, PortSlot::new(1), b, PortSlot::new(0)),
973            (b, PortSlot::new(1), a, PortSlot::new(0)),
974        ];
975        assert_eq!(topo_order(2, &threads), None);
976    }
977
978    #[test]
979    fn checked_port_access_reports_oob_without_mutation() {
980        let mut b = Weave::builder("x").unwrap();
981        let _k_c = b
982            .knot("c", KnotKind::constant(ONE, SignalDomain::Bool))
983            .unwrap();
984        let weave = b.build().unwrap();
985        let mut rt = Runtime::bind(weave.clone(), BindOpts::default()).unwrap();
986        let far = KnotId::try_from(999usize).unwrap();
987        let far_handle = KnotHandle::new(rt.owner, far.get());
988        assert_eq!(
989            rt.get_port(far, PortSlot::new(0)),
990            Err(HandleError::InvalidKnot { knot: far_handle })
991        );
992        assert_eq!(
993            rt.set_port(far, PortSlot::new(0), ONE),
994            Err(HandleError::InvalidKnot { knot: far_handle })
995        );
996        let _ = FlagPriority::SetWins;
997    }
998
999    #[test]
1000    fn dropped_emit_count_saturates() {
1001        let mut b = Weave::builder("emit-saturation").unwrap();
1002        let input = b
1003            .knot("input", KnotKind::signal_in(SignalDomain::Bool))
1004            .unwrap();
1005        let emit = b.knot("emit", KnotKind::emit_command("fire")).unwrap();
1006        let from = b.output(&input, "out").unwrap();
1007        let to = b.input(&emit, "trigger").unwrap();
1008        b.connect(from, to).unwrap();
1009        let mut rt = Runtime::bind(
1010            b.build().unwrap(),
1011            BindOpts {
1012                max_emits_per_tick: 0,
1013                ..BindOpts::default()
1014            },
1015        )
1016        .unwrap();
1017        let cmd = rt.cmd_id("fire").unwrap();
1018        rt.dropped_emits = usize::MAX;
1019
1020        rt.push_emit(cmd, ONE);
1021
1022        assert_eq!(rt.dropped_emits, usize::MAX);
1023        assert!(rt.out_emits.is_empty());
1024    }
1025
1026    #[test]
1027    fn emit_outbox_reservation_respects_the_runtime_cap() {
1028        let mut b = Weave::builder("emit-reservation").unwrap();
1029        let trigger = b
1030            .knot("trigger", KnotKind::constant(ONE, SignalDomain::Bool))
1031            .unwrap();
1032        for i in 0..4 {
1033            let emit = b
1034                .knot(
1035                    alloc::format!("emit-{i}"),
1036                    KnotKind::emit_command(alloc::format!("command-{i}")),
1037                )
1038                .unwrap();
1039            let from = b.output(&trigger, "out").unwrap();
1040            let to = b.input(&emit, "trigger").unwrap();
1041            b.connect(from, to).unwrap();
1042        }
1043        let weave = b.build().unwrap();
1044
1045        for (cap, expected) in [(0, 0), (2, 2), (4, 4), (8, 4)] {
1046            let rt = Runtime::bind(
1047                weave.clone(),
1048                BindOpts {
1049                    max_emits_per_tick: cap,
1050                    ..BindOpts::default()
1051                },
1052            )
1053            .unwrap();
1054            assert_eq!(rt.out_emits.capacity(), expected);
1055        }
1056    }
1057
1058    #[test]
1059    fn clear_only_unwired_ins_and_div_const_specializes() {
1060        use crate::foundation::CalcOp;
1061        let mut b = Weave::builder("fl").unwrap();
1062        let k_f = b
1063            .knot("f", KnotKind::flag(FlagPriority::SetWins, false))
1064            .unwrap();
1065        let k_o = b
1066            .knot("o", KnotKind::signal_out("y", SignalDomain::Bool))
1067            .unwrap();
1068        let from = b.output(&k_f, "out").unwrap();
1069        let to = b.input(&k_o, "in").unwrap();
1070        b.connect(from, to).unwrap();
1071        let weave = b.build().unwrap();
1072        let rt = Runtime::bind(weave.clone(), BindOpts::default()).unwrap();
1073        assert_eq!(
1074            rt.clear_port_index_count(),
1075            3,
1076            "unwired Flag Ins must clear"
1077        );
1078
1079        let mut b = Weave::builder("dv").unwrap();
1080        let k_in = b
1081            .knot("in", KnotKind::signal_in(SignalDomain::Level))
1082            .unwrap();
1083        let k_one = b
1084            .knot("one", KnotKind::constant(ONE, SignalDomain::Level))
1085            .unwrap();
1086        let k_d = b
1087            .knot(
1088                "d",
1089                KnotKind::Calc {
1090                    domain: SignalDomain::Level,
1091                    op: CalcOp::Div,
1092                },
1093            )
1094            .unwrap();
1095        let k_out = b
1096            .knot("out", KnotKind::signal_out("y", SignalDomain::Level))
1097            .unwrap();
1098        let from = b.output(&k_in, "out").unwrap();
1099        let to = b.input(&k_d, "a").unwrap();
1100        b.connect(from, to).unwrap();
1101        let from = b.output(&k_one, "out").unwrap();
1102        let to = b.input(&k_d, "b").unwrap();
1103        b.connect(from, to).unwrap();
1104        let from = b.output(&k_d, "out").unwrap();
1105        let to = b.input(&k_out, "in").unwrap();
1106        b.connect(from, to).unwrap();
1107        let weave = b.build().unwrap();
1108        let rt = Runtime::bind(weave.clone(), BindOpts::default()).unwrap();
1109        let d = *rt.name_to_id.get("d").expect("div knot");
1110        assert!(matches!(
1111            rt.kind_tags[usize::from(d)],
1112            crate::runtime_impl::kind_tag::KindTag::CalcDivLevelConst { divisor } if divisor == ONE
1113        ));
1114    }
1115
1116    /// Bind builds KindTag cache, flat clear indices, and CSR inbound.
1117    #[test]
1118    fn bind_builds_hot_path_tables() {
1119        let mut b = Weave::builder("h").unwrap();
1120        let k_a = b
1121            .knot("a", KnotKind::signal_in(SignalDomain::Bool))
1122            .unwrap();
1123        let k_n = b.knot("n", KnotKind::not()).unwrap();
1124        let k_o = b
1125            .knot("o", KnotKind::signal_out("y", SignalDomain::Bool))
1126            .unwrap();
1127        let from = b.output(&k_a, "out").unwrap();
1128        let to = b.input(&k_n, "in").unwrap();
1129        b.connect(from, to).unwrap();
1130        let from = b.output(&k_n, "out").unwrap();
1131        let to = b.input(&k_o, "in").unwrap();
1132        b.connect(from, to).unwrap();
1133        let weave = b.build().unwrap();
1134        let mut rt = Runtime::bind(weave.clone(), BindOpts::default()).unwrap();
1135        assert_eq!(rt.kind_tag_count(), weave.knots().len());
1136        assert_eq!(rt.clear_port_index_count(), 0);
1137        assert_eq!(rt.inbound_edge_count(), 2);
1138        assert_eq!(rt.inbound_off.len(), weave.knots().len() + 1);
1139        let n_id = KnotId::try_from(1usize).unwrap();
1140        rt.set_port_hot(n_id, PortSlot::new(0), ONE);
1141        assert_eq!(rt.get_port_hot(n_id, PortSlot::new(0)), ONE);
1142        let n_handle = rt.knot_id("n").unwrap();
1143        assert_eq!(rt.get_port_checked(n_handle, PortSlot::new(0)), Ok(ONE));
1144    }
1145
1146    #[test]
1147    fn defensive_bind_phase_rejects_invalid_internal_definitions() {
1148        let constant = || knot("constant", KnotKind::constant(ONE, SignalDomain::Bool));
1149        let out = || knot("out", KnotKind::signal_out("out", SignalDomain::Bool));
1150
1151        let error = bind_unchecked(
1152            "missing-from",
1153            vec![constant()],
1154            vec![ThreadDef {
1155                from: PortRefDef::new("missing", "out"),
1156                to: PortRefDef::new("constant", "out"),
1157            }],
1158        );
1159        assert!(matches!(
1160            error,
1161            BindError::InvalidReference { knot, port, .. } if knot == "missing" && port == "out"
1162        ));
1163
1164        let error = bind_unchecked(
1165            "missing-to",
1166            vec![constant()],
1167            vec![ThreadDef {
1168                from: PortRefDef::new("constant", "out"),
1169                to: PortRefDef::new("missing", "in"),
1170            }],
1171        );
1172        assert!(matches!(
1173            error,
1174            BindError::InvalidReference { knot, port, .. } if knot == "missing" && port == "in"
1175        ));
1176
1177        let error = bind_unchecked(
1178            "missing-from-port",
1179            vec![constant(), out()],
1180            vec![ThreadDef {
1181                from: PortRefDef::new("constant", "in"),
1182                to: PortRefDef::new("out", "in"),
1183            }],
1184        );
1185        assert!(matches!(
1186            error,
1187            BindError::InvalidReference { knot, port, .. } if knot == "constant" && port == "in"
1188        ));
1189
1190        let error = bind_unchecked(
1191            "missing-to-port",
1192            vec![constant(), out()],
1193            vec![ThreadDef {
1194                from: PortRefDef::new("constant", "out"),
1195                to: PortRefDef::new("out", "out"),
1196            }],
1197        );
1198        assert!(matches!(
1199            error,
1200            BindError::InvalidReference { knot, port, .. } if knot == "out" && port == "out"
1201        ));
1202
1203        let error = bind_unchecked(
1204            "cycle",
1205            vec![knot("a", KnotKind::Not), knot("b", KnotKind::Not)],
1206            vec![
1207                ThreadDef {
1208                    from: PortRefDef::new("a", "out"),
1209                    to: PortRefDef::new("b", "in"),
1210                },
1211                ThreadDef {
1212                    from: PortRefDef::new("b", "out"),
1213                    to: PortRefDef::new("a", "in"),
1214                },
1215            ],
1216        );
1217        assert!(matches!(error, BindError::InvalidTopology { .. }));
1218    }
1219
1220    #[test]
1221    fn defensive_bind_phase_checks_dense_capacities_before_allocation() {
1222        assert_eq!(
1223            reserve_owner(usize::MAX, "owner"),
1224            Err(BindError::CapacityExceeded {
1225                weave_id: String::from("owner"),
1226                resource: "runtime owner token",
1227                count: usize::MAX,
1228            })
1229        );
1230        assert_eq!(reserve_owner(1, "owner"), Ok(()));
1231
1232        let excessive_knots = (0..=(usize::from(u16::MAX) + 1))
1233            .map(|_| knot("", KnotKind::OnStart))
1234            .collect();
1235        let error = bind_unchecked("too-many-knots", excessive_knots, Vec::new());
1236        assert_eq!(
1237            error,
1238            BindError::CapacityExceeded {
1239                weave_id: String::from("too-many-knots"),
1240                resource: "knot",
1241                count: usize::from(u16::MAX) + 2,
1242            }
1243        );
1244
1245        let delay_knots = (0..256)
1246            .map(|_| knot("", KnotKind::Delay { ticks: 256 }))
1247            .collect();
1248        let error = bind_unchecked("delay-capacity", delay_knots, Vec::new());
1249        assert_eq!(
1250            error,
1251            BindError::CapacityExceeded {
1252                weave_id: String::from("delay-capacity"),
1253                resource: "delay buffer",
1254                count: usize::from(u16::MAX) + 1,
1255            }
1256        );
1257    }
1258
1259    #[test]
1260    fn defensive_compact_capacity_guards_report_the_next_entry() {
1261        let overflow_index = usize::from(u16::MAX) + 1;
1262        let mut path_index = BTreeMap::new();
1263        let mut path_names = vec![String::new(); overflow_index];
1264        assert_eq!(
1265            intern_host_path(
1266                1,
1267                "overflow",
1268                &mut path_index,
1269                &mut path_names,
1270                "path-capacity",
1271            ),
1272            Err(BindError::CapacityExceeded {
1273                weave_id: String::from("path-capacity"),
1274                resource: "host path",
1275                count: overflow_index + 1,
1276            })
1277        );
1278
1279        let mut cmd_index = BTreeMap::new();
1280        let mut cmd_names = vec![String::new(); overflow_index];
1281        assert_eq!(
1282            intern_command(
1283                1,
1284                "overflow",
1285                &mut cmd_index,
1286                &mut cmd_names,
1287                "command-capacity",
1288            ),
1289            Err(BindError::CapacityExceeded {
1290                weave_id: String::from("command-capacity"),
1291                resource: "command",
1292                count: overflow_index + 1,
1293            })
1294        );
1295
1296        assert_eq!(
1297            delay_buffer_layout(overflow_index, 1, "delay-offset-capacity"),
1298            Err(BindError::CapacityExceeded {
1299                weave_id: String::from("delay-offset-capacity"),
1300                resource: "delay buffer offset",
1301                count: overflow_index,
1302            })
1303        );
1304        assert_eq!(
1305            checked_delay_buffer_len(usize::MAX, 1, "delay-overflow"),
1306            Err(BindError::CapacityExceeded {
1307                weave_id: String::from("delay-overflow"),
1308                resource: "delay buffer",
1309                count: usize::MAX,
1310            })
1311        );
1312    }
1313
1314    #[test]
1315    fn preinterned_tables_propagate_path_and_command_capacity_errors() {
1316        let full_names = vec![String::new(); usize::from(u16::MAX) + 1];
1317        let path_error = Runtime::bind_validated_with_preinterned_names(
1318            unchecked_weave(
1319                "path-capacity",
1320                vec![knot(
1321                    "output",
1322                    KnotKind::signal_out("too-many-paths", SignalDomain::Bool),
1323                )],
1324                Vec::new(),
1325            ),
1326            BindOpts::default(),
1327            String::from("path-capacity"),
1328            full_names,
1329            Vec::new(),
1330        )
1331        .err()
1332        .expect("the next host path must exceed its compact id space");
1333        assert!(matches!(
1334            path_error,
1335            BindError::CapacityExceeded {
1336                resource: "host path",
1337                count,
1338                ..
1339            } if count == usize::from(u16::MAX) + 2
1340        ));
1341
1342        let full_names = vec![String::new(); usize::from(u16::MAX) + 1];
1343        let command_error = Runtime::bind_validated_with_preinterned_names(
1344            unchecked_weave(
1345                "command-capacity",
1346                vec![knot("emit", KnotKind::emit_command("too-many-commands"))],
1347                Vec::new(),
1348            ),
1349            BindOpts::default(),
1350            String::from("command-capacity"),
1351            Vec::new(),
1352            full_names,
1353        )
1354        .err()
1355        .expect("the next command must exceed its compact id space");
1356        assert!(matches!(
1357            command_error,
1358            BindError::CapacityExceeded {
1359                resource: "command",
1360                count,
1361                ..
1362            } if count == usize::from(u16::MAX) + 2
1363        ));
1364    }
1365
1366    #[test]
1367    fn div_with_a_dynamic_rhs_keeps_the_general_dispatch_tag() {
1368        let mut b = Weave::builder("dynamic-divisor").unwrap();
1369        let lhs = b
1370            .knot("lhs", KnotKind::signal_in(SignalDomain::Level))
1371            .unwrap();
1372        let rhs = b
1373            .knot("rhs", KnotKind::signal_in(SignalDomain::Level))
1374            .unwrap();
1375        let div = b
1376            .knot(
1377                "div",
1378                KnotKind::Calc {
1379                    domain: SignalDomain::Level,
1380                    op: CalcOp::Div,
1381                },
1382            )
1383            .unwrap();
1384        b.connect(b.output(&lhs, "out").unwrap(), b.input(&div, "a").unwrap())
1385            .unwrap();
1386        b.connect(b.output(&rhs, "out").unwrap(), b.input(&div, "b").unwrap())
1387            .unwrap();
1388
1389        let rt = Runtime::bind(b.build().unwrap(), BindOpts::default()).unwrap();
1390        let div = rt.name_to_id["div"];
1391        assert!(matches!(
1392            rt.kind_tags[usize::from(div)],
1393            crate::runtime_impl::kind_tag::KindTag::CalcDivLevel
1394        ));
1395    }
1396
1397    #[test]
1398    fn div_without_an_rhs_edge_keeps_the_general_dispatch_tag_defensively() {
1399        let weave = unchecked_weave(
1400            "unwired-divisor",
1401            vec![knot(
1402                "div",
1403                KnotKind::calc(CalcOp::Div, SignalDomain::Level),
1404            )],
1405            Vec::new(),
1406        );
1407        let runtime =
1408            Runtime::bind_validated(weave, BindOpts::default(), String::from("unwired-divisor"))
1409                .expect("the defensive bind phase can represent an unwired calc");
1410
1411        assert!(matches!(
1412            runtime.kind_tags[0],
1413            crate::runtime_impl::kind_tag::KindTag::CalcDivLevel
1414        ));
1415    }
1416
1417    #[test]
1418    fn sense_id_rejects_existing_non_sense_knots() {
1419        let mut b = Weave::builder("sense-id-kind").unwrap();
1420        b.knot("constant", KnotKind::constant(ONE, SignalDomain::Bool))
1421            .unwrap();
1422        let rt = Runtime::bind(b.build().unwrap(), BindOpts::default()).unwrap();
1423
1424        assert_eq!(rt.sense_id("constant"), None);
1425    }
1426}