1#![allow(clippy::result_large_err)] use 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#[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#[derive(Clone, Debug)]
35pub struct BindOpts {
36 pub seed: Option<Seed>,
38 pub max_emits_per_tick: u16,
44 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 pub(crate) path: Option<HostPathId>,
63 pub(crate) cmd: Option<CmdId>,
64}
65
66pub struct Runtime {
71 pub(crate) owner: usize,
72 pub(crate) knots: Vec<ResolvedKnot>,
73 pub(crate) name_to_id: BTreeMap<String, KnotId>,
75 pub(crate) path_names: Vec<String>,
76 pub(crate) cmd_names: Vec<String>,
77 pub(crate) inbound_off: Vec<u32>,
80 pub(crate) inbound_edges: Vec<(KnotId, PortSlot, PortSlot)>,
81 pub(crate) clear_port_idx: Vec<usize>,
83 pub(crate) topo: Vec<KnotId>,
84 pub(crate) kind_tags: Vec<crate::runtime_impl::kind_tag::KindTag>,
86 pub(crate) sense_seeds: Vec<SenseSeed>,
88 pub(crate) sense_values: Vec<Signal>,
90 pub(crate) port_vals: Vec<Signal>,
92 pub(crate) max_ports: usize,
93 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 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 pub(crate) rng: u64,
113 pub(crate) seed_mix: u64,
115 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
131fn 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
143fn 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
166fn 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
189fn 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
205fn 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 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 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 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 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 pub fn reseed(&mut self, seed: Seed) {
502 self.rng = (seed.0 ^ self.seed_mix) | 1;
503 }
504
505 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 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 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 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 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 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 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 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 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 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 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 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 pub fn port_writer(&mut self) -> PortWriter<'_> {
631 PortWriter { rt: self }
632 }
633
634 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 pub fn outbox_signals_capacity(&self) -> usize {
645 self.out_signals.capacity()
646 }
647
648 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 #[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 #[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 #[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 #[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 pub fn kind_tag_count(&self) -> usize {
746 self.kind_tags.len()
747 }
748
749 pub fn clear_port_index_count(&self) -> usize {
753 self.clear_port_idx.len()
754 }
755
756 pub fn inbound_edge_count(&self) -> usize {
760 self.inbound_edges.len()
761 }
762
763 #[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 #[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 #[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}