1use crate::foundation::Signal;
9use crate::foundation::{KnotId, NumericPath, PortSlot, Seed};
10use crate::runtime_impl::bind::{ResolvedKnot, Runtime};
11use crate::runtime_impl::error::{BindRestoreError, RestoreError};
12use std::boxed::Box;
13use std::collections::BTreeSet;
14use std::string::String;
15use std::vec::Vec;
16
17#[cfg(feature = "serde-ron")]
19#[derive(Debug)]
20pub enum RuntimeStateRonCodecError {
21 Parse(ron::error::SpannedError),
23 Serialize(ron::Error),
25}
26
27#[cfg(feature = "serde-ron")]
28impl core::fmt::Display for RuntimeStateRonCodecError {
29 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
30 match self {
31 Self::Parse(error) => write!(f, "runtime checkpoint RON parse error: {error}"),
32 Self::Serialize(error) => {
33 write!(f, "runtime checkpoint RON serialization error: {error}")
34 }
35 }
36 }
37}
38#[cfg(feature = "serde-ron")]
39impl std::error::Error for RuntimeStateRonCodecError {}
40
41#[cfg(feature = "serde-json")]
43#[derive(Debug)]
44pub enum RuntimeStateJsonCodecError {
45 Parse(serde_json::Error),
47 Serialize(serde_json::Error),
49}
50#[cfg(feature = "serde-json")]
51impl core::fmt::Display for RuntimeStateJsonCodecError {
52 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
53 match self {
54 Self::Parse(error) => write!(f, "runtime checkpoint JSON parse error: {error}"),
55 Self::Serialize(error) => {
56 write!(f, "runtime checkpoint JSON serialization error: {error}")
57 }
58 }
59 }
60}
61#[cfg(feature = "serde-json")]
62impl std::error::Error for RuntimeStateJsonCodecError {}
63
64#[cfg(feature = "serde-ron")]
66pub fn runtime_state_to_ron(state: &RuntimeState) -> Result<String, RuntimeStateRonCodecError> {
67 ron::ser::to_string_pretty(state, ron::ser::PrettyConfig::default())
68 .map_err(RuntimeStateRonCodecError::Serialize)
69}
70#[cfg(feature = "serde-ron")]
72pub fn runtime_state_from_ron(text: &str) -> Result<RuntimeState, RuntimeStateRonCodecError> {
73 ron::from_str(text).map_err(RuntimeStateRonCodecError::Parse)
74}
75#[cfg(feature = "serde-json")]
77pub fn runtime_state_to_json(state: &RuntimeState) -> Result<String, RuntimeStateJsonCodecError> {
78 serde_json::to_string_pretty(state).map_err(RuntimeStateJsonCodecError::Serialize)
79}
80#[cfg(feature = "serde-json")]
82pub fn runtime_state_from_json(text: &str) -> Result<RuntimeState, RuntimeStateJsonCodecError> {
83 serde_json::from_str(text).map_err(RuntimeStateJsonCodecError::Parse)
84}
85
86pub const RUNTIME_STATE_FORMAT_VERSION: u32 = 2;
88
89#[derive(Clone, Debug)]
95pub struct RuntimeState {
96 version: u32,
97 numeric_path: NumericPath,
98 fingerprint: u64,
99 data: RuntimeStateData,
100}
101
102#[derive(Clone, Debug)]
103struct RuntimeStateData {
104 sense_values: Vec<Signal>,
105 port_vals: Vec<Signal>,
106 prev_in: Vec<Signal>,
107 prev_dec: Vec<Signal>,
108 counter: Vec<i32>,
109 flag: Vec<bool>,
110 timer_left: Vec<u16>,
111 on_start_done: Vec<bool>,
112 delay_buf: Vec<Signal>,
113 delay_head: Vec<u16>,
114 tick: u64,
115 phase: u8,
116 rng: u64,
117}
118
119impl RuntimeState {
120 pub const fn format_version(&self) -> u32 {
122 self.version
123 }
124
125 pub const fn fingerprint(&self) -> u64 {
127 self.fingerprint
128 }
129
130 pub const fn numeric_path(&self) -> NumericPath {
132 self.numeric_path
133 }
134}
135
136#[derive(Clone, Debug, Default)]
138#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
139pub struct RuntimePreset {
140 entries: Vec<RuntimePresetEntry>,
141}
142
143#[derive(Clone, Debug)]
145#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
146pub enum RuntimePresetEntry {
147 Flag {
149 knot: String,
151 value: bool,
153 },
154 Counter {
156 knot: String,
158 value: i32,
160 },
161 Sense {
163 knot: String,
165 value: Signal,
167 },
168}
169
170impl RuntimePreset {
171 pub const fn new() -> Self {
173 Self {
174 entries: Vec::new(),
175 }
176 }
177 pub fn push(&mut self, entry: RuntimePresetEntry) {
179 self.entries.push(entry);
180 }
181 pub fn entries(&self) -> &[RuntimePresetEntry] {
183 &self.entries
184 }
185}
186
187#[derive(Clone, Debug, PartialEq)]
189pub enum RuntimeStateEntry {
190 Sense {
192 knot: String,
194 value: Signal,
196 },
197 Flag {
199 knot: String,
201 value: bool,
203 },
204 Counter {
206 knot: String,
208 value: i32,
210 },
211 Timer {
213 knot: String,
215 remaining: u16,
217 },
218 Delay {
220 knot: String,
222 len: u16,
224 head: u16,
226 values: Vec<Signal>,
228 },
229 Edge {
231 knot: String,
233 previous: Signal,
235 },
236 OnStart {
238 knot: String,
240 completed: bool,
242 },
243 Random {
245 knot: String,
247 last_sample: Signal,
249 stream_is_opaque: bool,
251 },
252}
253
254#[derive(Clone, Debug, PartialEq)]
256pub struct RuntimeStateReport {
257 pub version: u32,
259 pub numeric_path: NumericPath,
261 pub fingerprint: u64,
263 pub tick: u64,
265 pub entries: Vec<RuntimeStateEntry>,
267}
268
269#[cfg(feature = "serde")]
270#[derive(serde::Serialize, serde::Deserialize)]
271#[serde(deny_unknown_fields)]
272struct RuntimeStateWire {
273 version: u32,
274 numeric_path: NumericPath,
275 fingerprint: u64,
276 sense_values: Vec<u32>,
277 port_vals: Vec<u32>,
278 prev_in: Vec<u32>,
279 prev_dec: Vec<u32>,
280 counter: Vec<i32>,
281 flag: Vec<bool>,
282 timer_left: Vec<u16>,
283 on_start_done: Vec<bool>,
284 delay_buf: Vec<u32>,
285 delay_head: Vec<u16>,
286 tick: u64,
287 phase: u8,
288 rng: u64,
289}
290
291#[cfg(feature = "serde")]
292fn signal_to_wire(value: Signal) -> u32 {
293 #[cfg(feature = "signal-f32")]
294 {
295 value.to_bits()
296 }
297 #[cfg(feature = "signal-i32")]
298 {
299 value as u32
300 }
301}
302
303#[cfg(feature = "serde")]
304fn signal_from_wire(value: u32) -> Signal {
305 #[cfg(feature = "signal-f32")]
306 {
307 f32::from_bits(value)
308 }
309 #[cfg(feature = "signal-i32")]
310 {
311 value as i32
312 }
313}
314
315#[cfg(feature = "serde")]
316impl serde::Serialize for RuntimeState {
317 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
318 where
319 S: serde::Serializer,
320 {
321 RuntimeStateWire {
322 version: self.version,
323 numeric_path: self.numeric_path,
324 fingerprint: self.fingerprint,
325 sense_values: self
326 .data
327 .sense_values
328 .iter()
329 .copied()
330 .map(signal_to_wire)
331 .collect(),
332 port_vals: self
333 .data
334 .port_vals
335 .iter()
336 .copied()
337 .map(signal_to_wire)
338 .collect(),
339 prev_in: self
340 .data
341 .prev_in
342 .iter()
343 .copied()
344 .map(signal_to_wire)
345 .collect(),
346 prev_dec: self
347 .data
348 .prev_dec
349 .iter()
350 .copied()
351 .map(signal_to_wire)
352 .collect(),
353 counter: self.data.counter.clone(),
354 flag: self.data.flag.clone(),
355 timer_left: self.data.timer_left.clone(),
356 on_start_done: self.data.on_start_done.clone(),
357 delay_buf: self
358 .data
359 .delay_buf
360 .iter()
361 .copied()
362 .map(signal_to_wire)
363 .collect(),
364 delay_head: self.data.delay_head.clone(),
365 tick: self.data.tick,
366 phase: self.data.phase,
367 rng: self.data.rng,
368 }
369 .serialize(serializer)
370 }
371}
372
373#[cfg(feature = "serde")]
374impl<'de> serde::Deserialize<'de> for RuntimeState {
375 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
376 where
377 D: serde::Deserializer<'de>,
378 {
379 let wire = RuntimeStateWire::deserialize(deserializer)?;
380 Ok(Self {
381 version: wire.version,
382 numeric_path: wire.numeric_path,
383 fingerprint: wire.fingerprint,
384 data: RuntimeStateData {
385 sense_values: wire
386 .sense_values
387 .into_iter()
388 .map(signal_from_wire)
389 .collect(),
390 port_vals: wire.port_vals.into_iter().map(signal_from_wire).collect(),
391 prev_in: wire.prev_in.into_iter().map(signal_from_wire).collect(),
392 prev_dec: wire.prev_dec.into_iter().map(signal_from_wire).collect(),
393 counter: wire.counter,
394 flag: wire.flag,
395 timer_left: wire.timer_left,
396 on_start_done: wire.on_start_done,
397 delay_buf: wire.delay_buf.into_iter().map(signal_from_wire).collect(),
398 delay_head: wire.delay_head,
399 tick: wire.tick,
400 phase: wire.phase,
401 rng: wire.rng,
402 },
403 })
404 }
405}
406
407impl Runtime {
408 pub fn bind_with_preset(
410 weave: crate::authoring::Weave,
411 opts: crate::runtime_impl::bind::BindOpts,
412 preset: &RuntimePreset,
413 ) -> Result<Self, crate::runtime_impl::error::PresetError> {
414 use crate::runtime_impl::error::PresetError;
415 let mut runtime =
416 Self::bind(weave, opts).map_err(|error| PresetError::Bind(Box::new(error)))?;
417 let mut names = BTreeSet::new();
418 for entry in &preset.entries {
419 let name = match entry {
420 RuntimePresetEntry::Flag { knot, .. }
421 | RuntimePresetEntry::Counter { knot, .. }
422 | RuntimePresetEntry::Sense { knot, .. } => knot,
423 };
424 if !names.insert(name) {
425 return Err(PresetError::Duplicate { knot: name.clone() });
426 }
427 let Some(id) = runtime.name_to_id.get(name).copied() else {
428 return Err(PresetError::Missing { knot: name.clone() });
429 };
430 match entry {
431 RuntimePresetEntry::Flag { .. }
432 if matches!(
433 runtime.knots[usize::from(id)].kind,
434 crate::foundation::KnotKind::Flag { .. }
435 ) => {}
436 RuntimePresetEntry::Counter { .. }
437 if matches!(
438 runtime.knots[usize::from(id)].kind,
439 crate::foundation::KnotKind::Counter
440 ) => {}
441 RuntimePresetEntry::Sense { value, .. } => {
442 match runtime.knots[usize::from(id)].kind {
443 crate::foundation::KnotKind::SignalIn { domain }
444 if crate::runtime_impl::outbox::domain_value_is_valid(
445 domain, *value,
446 ) => {}
447 crate::foundation::KnotKind::SignalIn { domain } => {
448 return Err(PresetError::InvalidSignal {
449 knot: name.clone(),
450 domain,
451 })
452 }
453 _ => {
454 return Err(PresetError::WrongKind {
455 knot: name.clone(),
456 expected: "SignalIn",
457 })
458 }
459 }
460 }
461 RuntimePresetEntry::Flag { .. } => {
462 return Err(PresetError::WrongKind {
463 knot: name.clone(),
464 expected: "Flag",
465 })
466 }
467 RuntimePresetEntry::Counter { .. } => {
468 return Err(PresetError::WrongKind {
469 knot: name.clone(),
470 expected: "Counter",
471 })
472 }
473 }
474 }
475 for entry in &preset.entries {
476 let name = match entry {
477 RuntimePresetEntry::Flag { knot, .. }
478 | RuntimePresetEntry::Counter { knot, .. }
479 | RuntimePresetEntry::Sense { knot, .. } => knot,
480 };
481 let id = runtime.name_to_id[name];
482 let index = usize::from(id);
483 match entry {
484 RuntimePresetEntry::Flag { value, .. } => runtime.flag[index] = *value,
485 RuntimePresetEntry::Counter { value, .. } => runtime.counter[index] = *value,
486 RuntimePresetEntry::Sense { value, .. } => runtime.sense_values[index] = *value,
487 }
488 }
489 Ok(runtime)
490 }
491
492 pub fn inspect_state(&self) -> RuntimeStateReport {
494 self.inspect_data(
495 Self::state_format_version(),
496 NumericPath::compiled(),
497 self.runtime_fingerprint(),
498 &RuntimeStateData {
499 sense_values: self.sense_values.clone(),
500 port_vals: self.port_vals.clone(),
501 prev_in: self.prev_in.clone(),
502 prev_dec: self.prev_dec.clone(),
503 counter: self.counter.clone(),
504 flag: self.flag.clone(),
505 timer_left: self.timer_left.clone(),
506 on_start_done: self.on_start_done.clone(),
507 delay_buf: self.delay_buf.clone(),
508 delay_head: self.delay_head.clone(),
509 tick: self.tick,
510 phase: self.phase,
511 rng: self.rng,
512 },
513 )
514 }
515
516 pub fn inspect_checkpoint(
518 &self,
519 state: &RuntimeState,
520 ) -> Result<RuntimeStateReport, RestoreError> {
521 self.validate_checkpoint(state)?;
522 Ok(self.inspect_data(
523 state.version,
524 state.numeric_path,
525 state.fingerprint,
526 &state.data,
527 ))
528 }
529 pub fn bind_restored(
535 weave: crate::authoring::Weave,
536 opts: crate::runtime_impl::bind::BindOpts,
537 state: &RuntimeState,
538 ) -> Result<Self, BindRestoreError> {
539 let mut runtime =
540 Self::bind(weave, opts).map_err(|error| BindRestoreError::Bind(Box::new(error)))?;
541 runtime.restore(state).map_err(BindRestoreError::Restore)?;
542 Ok(runtime)
543 }
544
545 pub fn snapshot(&self) -> RuntimeState {
582 let mut state = RuntimeState {
583 version: RUNTIME_STATE_FORMAT_VERSION,
584 numeric_path: NumericPath::compiled(),
585 fingerprint: self.runtime_fingerprint(),
586 data: RuntimeStateData {
587 sense_values: Vec::new(),
588 port_vals: Vec::new(),
589 prev_in: Vec::new(),
590 prev_dec: Vec::new(),
591 counter: Vec::new(),
592 flag: Vec::new(),
593 timer_left: Vec::new(),
594 on_start_done: Vec::new(),
595 delay_buf: Vec::new(),
596 delay_head: Vec::new(),
597 tick: 0,
598 phase: 0,
599 rng: 0,
600 },
601 };
602 self.snapshot_into(&mut state);
603 state
604 }
605
606 pub fn snapshot_into(&self, state: &mut RuntimeState) {
612 state.version = RUNTIME_STATE_FORMAT_VERSION;
613 state.numeric_path = NumericPath::compiled();
614 state.fingerprint = self.runtime_fingerprint();
615 state.data.sense_values.clone_from(&self.sense_values);
616 state.data.port_vals.clone_from(&self.port_vals);
617 state.data.prev_in.clone_from(&self.prev_in);
618 state.data.prev_dec.clone_from(&self.prev_dec);
619 state.data.counter.clone_from(&self.counter);
620 state.data.flag.clone_from(&self.flag);
621 state.data.timer_left.clone_from(&self.timer_left);
622 state.data.on_start_done.clone_from(&self.on_start_done);
623 state.data.delay_buf.clone_from(&self.delay_buf);
624 state.data.delay_head.clone_from(&self.delay_head);
625 state.data.tick = self.tick;
626 state.data.phase = self.phase;
627 state.data.rng = self.rng;
628 }
629
630 pub fn restore(&mut self, state: &RuntimeState) -> Result<(), RestoreError> {
640 self.validate_checkpoint(state)?;
641 self.sense_values.clone_from(&state.data.sense_values);
642 self.port_vals.clone_from(&state.data.port_vals);
643 self.prev_in.clone_from(&state.data.prev_in);
644 self.prev_dec.clone_from(&state.data.prev_dec);
645 self.counter.clone_from(&state.data.counter);
646 self.flag.clone_from(&state.data.flag);
647 self.timer_left.clone_from(&state.data.timer_left);
648 self.on_start_done.clone_from(&state.data.on_start_done);
649 self.delay_buf.clone_from(&state.data.delay_buf);
650 self.delay_head.clone_from(&state.data.delay_head);
651 self.out_signals.clear();
654 self.out_emits.clear();
655 self.dropped_emits = 0;
656 self.tick = state.data.tick;
657 self.phase = state.data.phase;
658 self.rng = state.data.rng;
659 Ok(())
660 }
661
662 fn validate_checkpoint(&self, state: &RuntimeState) -> Result<(), RestoreError> {
663 if state.version != RUNTIME_STATE_FORMAT_VERSION {
664 return Err(RestoreError::UnsupportedVersion {
665 found: state.version,
666 supported: RUNTIME_STATE_FORMAT_VERSION,
667 });
668 }
669 if state.numeric_path != NumericPath::compiled() {
670 return Err(RestoreError::NumericPathMismatch {
671 expected: NumericPath::compiled(),
672 found: state.numeric_path,
673 });
674 }
675 if state.data.phase != 2 {
676 return Err(RestoreError::InvalidPhase {
677 found: state.data.phase,
678 });
679 }
680 let expected = self.runtime_fingerprint();
681 if state.fingerprint != expected {
682 return Err(RestoreError::FingerprintMismatch {
683 expected,
684 found: state.fingerprint,
685 });
686 }
687
688 self.validate_snapshot_shapes(&state.data)?;
689
690 for (field, values) in [
691 ("port_vals", &state.data.port_vals),
692 ("prev_in", &state.data.prev_in),
693 ("prev_dec", &state.data.prev_dec),
694 ("delay_buf", &state.data.delay_buf),
695 ] {
696 for (knot, value) in values.iter().enumerate() {
697 #[cfg(feature = "signal-f32")]
698 if !value.is_finite() {
699 return Err(RestoreError::InvalidSignal {
700 field,
701 knot,
702 domain: crate::foundation::SignalDomain::Level,
703 });
704 }
705 #[cfg(feature = "signal-i32")]
706 let _ = (field, knot, value);
707 }
708 }
709
710 Ok(())
711 }
712
713 pub const fn state_format_version() -> u32 {
715 RUNTIME_STATE_FORMAT_VERSION
716 }
717
718 pub const fn runtime_fingerprint(&self) -> u64 {
720 self.state_fingerprint
721 }
722
723 fn validate_snapshot_shapes(&self, data: &RuntimeStateData) -> Result<(), RestoreError> {
724 check_len(
725 "sense_values",
726 self.sense_values.len(),
727 data.sense_values.len(),
728 )?;
729 check_len("port_vals", self.port_vals.len(), data.port_vals.len())?;
730 check_len("prev_in", self.prev_in.len(), data.prev_in.len())?;
731 check_len("prev_dec", self.prev_dec.len(), data.prev_dec.len())?;
732 check_len("counter", self.counter.len(), data.counter.len())?;
733 check_len("flag", self.flag.len(), data.flag.len())?;
734 check_len("timer_left", self.timer_left.len(), data.timer_left.len())?;
735 check_len(
736 "on_start_done",
737 self.on_start_done.len(),
738 data.on_start_done.len(),
739 )?;
740 check_len("delay_buf", self.delay_buf.len(), data.delay_buf.len())?;
741 check_len("delay_head", self.delay_head.len(), data.delay_head.len())?;
742 if data.rng == 0 {
743 return Err(RestoreError::InvalidRng);
744 }
745 for (knot, resolved) in self.knots.iter().enumerate() {
746 if let crate::foundation::KnotKind::Timer { ticks, .. } = resolved.kind {
747 let remaining = data.timer_left[knot];
748 if remaining > ticks {
749 return Err(RestoreError::InvalidTimer {
750 knot,
751 remaining,
752 max: ticks,
753 });
754 }
755 }
756 if let crate::foundation::KnotKind::Delay { ticks } = resolved.kind {
757 let head = data.delay_head[knot];
758 if ticks == 0 {
759 if head != 0 {
760 return Err(RestoreError::InvalidDelayHead {
761 knot,
762 head,
763 len: ticks,
764 });
765 }
766 } else if head >= ticks {
767 return Err(RestoreError::InvalidDelayHead {
768 knot,
769 head,
770 len: ticks,
771 });
772 }
773 }
774 if let crate::foundation::KnotKind::SignalIn { domain } = resolved.kind {
775 if !crate::runtime_impl::outbox::domain_value_is_valid(
776 domain,
777 data.sense_values[knot],
778 ) {
779 return Err(RestoreError::InvalidSignal {
780 field: "sense_values",
781 knot,
782 domain,
783 });
784 }
785 }
786 if let crate::foundation::KnotKind::Random { domain, .. } = resolved.kind {
787 #[cfg(feature = "signal-f32")]
788 if !f32::from_bits(data.counter[knot] as u32).is_finite() {
789 return Err(RestoreError::InvalidSignal {
790 field: "counter.random_sample",
791 knot,
792 domain,
793 });
794 }
795 #[cfg(feature = "signal-i32")]
796 let _ = domain;
797 }
798 }
799 Ok(())
800 }
801
802 fn inspect_data(
803 &self,
804 version: u32,
805 numeric_path: NumericPath,
806 fingerprint: u64,
807 data: &RuntimeStateData,
808 ) -> RuntimeStateReport {
809 let mut entries = Vec::new();
810 for (name, id) in &self.name_to_id {
811 let i = usize::from(*id);
812 match self.knots[i].kind {
813 crate::foundation::KnotKind::SignalIn { .. } => {
814 entries.push(RuntimeStateEntry::Sense {
815 knot: name.clone(),
816 value: data.sense_values[i],
817 })
818 }
819 crate::foundation::KnotKind::Flag { .. } => entries.push(RuntimeStateEntry::Flag {
820 knot: name.clone(),
821 value: data.flag[i],
822 }),
823 crate::foundation::KnotKind::Counter => entries.push(RuntimeStateEntry::Counter {
824 knot: name.clone(),
825 value: data.counter[i],
826 }),
827 crate::foundation::KnotKind::Timer { .. } => {
828 entries.push(RuntimeStateEntry::Timer {
829 knot: name.clone(),
830 remaining: data.timer_left[i],
831 })
832 }
833 crate::foundation::KnotKind::Delay { ticks } => {
834 let offset = usize::from(self.delay_off[i]);
835 let len = usize::from(ticks);
836 let head = usize::from(data.delay_head[i]);
837 let values = if len == 0 {
838 Vec::new()
839 } else {
840 (0..len)
841 .map(|n| data.delay_buf[offset + ((head + n) % len)])
842 .collect()
843 };
844 entries.push(RuntimeStateEntry::Delay {
845 knot: name.clone(),
846 len: ticks,
847 head: data.delay_head[i],
848 values,
849 });
850 }
851 crate::foundation::KnotKind::OnStart => entries.push(RuntimeStateEntry::OnStart {
852 knot: name.clone(),
853 completed: data.on_start_done[i],
854 }),
855 crate::foundation::KnotKind::Random { .. } => {
856 #[cfg(feature = "signal-f32")]
857 let last_sample = f32::from_bits(data.counter[i] as u32);
858 #[cfg(feature = "signal-i32")]
859 let last_sample = data.counter[i];
860 entries.push(RuntimeStateEntry::Random {
861 knot: name.clone(),
862 last_sample,
863 stream_is_opaque: true,
864 });
865 }
866 crate::foundation::KnotKind::RisingFromZero
867 | crate::foundation::KnotKind::FallingToZero
868 | crate::foundation::KnotKind::Change => entries.push(RuntimeStateEntry::Edge {
869 knot: name.clone(),
870 previous: data.prev_in[i],
871 }),
872 _ => {}
873 }
874 }
875 RuntimeStateReport {
876 version,
877 numeric_path,
878 fingerprint,
879 tick: data.tick,
880 entries,
881 }
882 }
883}
884
885pub(crate) fn runtime_fingerprint_for(
886 knots: &[ResolvedKnot],
887 threads: &[(KnotId, PortSlot, KnotId, PortSlot)],
888 path_names: &[String],
889 cmd_names: &[String],
890 max_emits_per_tick: u16,
891 seed_mix: u64,
892 _bind_seed: Option<Seed>,
893) -> u64 {
894 let mut hash = Fnv1a::new();
895 hash.bytes(b"wyrd-runtime-state-v1");
896 hash.usize(knots.len());
897 for knot in knots {
898 fingerprint_knot(&mut hash, &knot.kind);
899 }
900 hash.usize(threads.len());
901 for &(from, from_slot, to, to_slot) in threads {
902 hash.u16(from.get());
903 hash.u8(from_slot.get());
904 hash.u16(to.get());
905 hash.u8(to_slot.get());
906 }
907 hash.usize(path_names.len());
908 for path in path_names {
909 hash.bytes(path.as_bytes());
910 hash.u8(0xff);
911 }
912 hash.usize(cmd_names.len());
913 for command in cmd_names {
914 hash.bytes(command.as_bytes());
915 hash.u8(0xff);
916 }
917 #[cfg(feature = "signal-f32")]
918 hash.u8(1);
919 #[cfg(feature = "signal-i32")]
920 hash.u8(2);
921 hash.u16(max_emits_per_tick);
922 hash.u64(seed_mix);
923 hash.finish()
924}
925
926fn check_len(field: &'static str, expected: usize, found: usize) -> Result<(), RestoreError> {
927 if expected == found {
928 Ok(())
929 } else {
930 Err(RestoreError::ShapeMismatch {
931 field,
932 expected,
933 found,
934 })
935 }
936}
937
938struct Fnv1a(u64);
939
940impl Fnv1a {
941 const fn new() -> Self {
942 Self(0xcbf2_9ce4_8422_2325)
943 }
944
945 fn bytes(&mut self, bytes: &[u8]) {
946 for byte in bytes {
947 self.u8(*byte);
948 }
949 }
950
951 fn u8(&mut self, value: u8) {
952 self.0 ^= u64::from(value);
953 self.0 = self.0.wrapping_mul(0x0100_0000_01b3);
954 }
955
956 fn u16(&mut self, value: u16) {
957 self.bytes(&value.to_le_bytes());
958 }
959
960 fn u64(&mut self, value: u64) {
961 self.bytes(&value.to_le_bytes());
962 }
963
964 fn usize(&mut self, value: usize) {
965 self.u64(value as u64);
966 }
967
968 const fn finish(&self) -> u64 {
969 self.0
970 }
971}
972
973fn fingerprint_knot(hash: &mut Fnv1a, kind: &crate::foundation::KnotKind) {
974 use crate::foundation::KnotKind;
975 match kind {
976 KnotKind::Constant { domain, value } => {
977 hash.u8(0);
978 domain_hash(hash, *domain);
979 signal_hash(hash, *value);
980 }
981 KnotKind::SignalIn { domain } => {
982 hash.u8(1);
983 domain_hash(hash, *domain);
984 }
985 KnotKind::OnStart => hash.u8(2),
986 KnotKind::Not => hash.u8(3),
987 KnotKind::And { arity } => {
988 hash.u8(4);
989 hash.u8(*arity);
990 }
991 KnotKind::Or { arity } => {
992 hash.u8(5);
993 hash.u8(*arity);
994 }
995 KnotKind::Compare {
996 domain,
997 op,
998 rhs_const,
999 } => {
1000 hash.u8(6);
1001 domain_hash(hash, *domain);
1002 hash.u8(compare_hash(*op));
1003 option_signal_hash(hash, *rhs_const);
1004 }
1005 KnotKind::RisingFromZero => hash.u8(7),
1006 KnotKind::Flag {
1007 priority,
1008 enable_toggle,
1009 } => {
1010 hash.u8(8);
1011 hash.u8(flag_priority_hash(*priority));
1012 hash.u8(u8::from(*enable_toggle));
1013 }
1014 KnotKind::Counter => hash.u8(9),
1015 KnotKind::Timer { mode, ticks } => {
1016 hash.u8(10);
1017 hash.u8(timer_mode_hash(*mode));
1018 hash.u16(*ticks);
1019 }
1020 KnotKind::Delay { ticks } => {
1021 hash.u8(11);
1022 hash.u16(*ticks);
1023 }
1024 KnotKind::Calc { domain, op } => {
1025 hash.u8(12);
1026 domain_hash(hash, *domain);
1027 hash.u8(calc_hash(*op));
1028 }
1029 KnotKind::Map {
1030 domain,
1031 in_min,
1032 in_max,
1033 out_min,
1034 out_max,
1035 } => {
1036 hash.u8(13);
1037 domain_hash(hash, *domain);
1038 signal_hash(hash, *in_min);
1039 signal_hash(hash, *in_max);
1040 signal_hash(hash, *out_min);
1041 signal_hash(hash, *out_max);
1042 }
1043 KnotKind::Abs { domain } => {
1044 hash.u8(14);
1045 domain_hash(hash, *domain);
1046 }
1047 KnotKind::Neg { domain } => {
1048 hash.u8(15);
1049 domain_hash(hash, *domain);
1050 }
1051 KnotKind::Select => hash.u8(16),
1052 KnotKind::Digitize {
1053 domain,
1054 steps,
1055 in_min,
1056 in_max,
1057 out_min,
1058 out_max,
1059 } => {
1060 hash.u8(17);
1061 domain_hash(hash, *domain);
1062 hash.u16(*steps);
1063 signal_hash(hash, *in_min);
1064 signal_hash(hash, *in_max);
1065 signal_hash(hash, *out_min);
1066 signal_hash(hash, *out_max);
1067 }
1068 KnotKind::Threshold {
1069 domain,
1070 high,
1071 low,
1072 use_hysteresis,
1073 } => {
1074 hash.u8(18);
1075 domain_hash(hash, *domain);
1076 signal_hash(hash, *high);
1077 signal_hash(hash, *low);
1078 hash.u8(u8::from(*use_hysteresis));
1079 }
1080 KnotKind::Random {
1081 domain,
1082 require_gate,
1083 } => {
1084 hash.u8(19);
1085 domain_hash(hash, *domain);
1086 hash.u8(u8::from(*require_gate));
1087 }
1088 KnotKind::Sqrt { domain } => {
1089 hash.u8(20);
1090 domain_hash(hash, *domain);
1091 }
1092 KnotKind::Xor => hash.u8(21),
1093 KnotKind::FallingToZero => hash.u8(22),
1094 KnotKind::Change => hash.u8(23),
1095 KnotKind::Clamp { domain, min, max } => {
1096 hash.u8(24);
1097 domain_hash(hash, *domain);
1098 signal_hash(hash, *min);
1099 signal_hash(hash, *max);
1100 }
1101 KnotKind::Convert { from, to } => {
1102 hash.u8(25);
1103 domain_hash(hash, *from);
1104 domain_hash(hash, *to);
1105 }
1106 KnotKind::SignalOut { path, domain } => {
1107 hash.u8(26);
1108 hash.bytes(path.as_bytes());
1109 hash.u8(0);
1110 domain_hash(hash, *domain);
1111 }
1112 KnotKind::EmitCommand { name } => {
1113 hash.u8(27);
1114 hash.bytes(name.as_bytes());
1115 hash.u8(0);
1116 }
1117 }
1118}
1119
1120fn domain_hash(hash: &mut Fnv1a, domain: crate::foundation::SignalDomain) {
1121 hash.u8(match domain {
1122 crate::foundation::SignalDomain::Bool => 0,
1123 crate::foundation::SignalDomain::Level => 1,
1124 crate::foundation::SignalDomain::Count => 2,
1125 });
1126}
1127fn compare_hash(op: crate::foundation::CompareOp) -> u8 {
1128 match op {
1129 crate::foundation::CompareOp::Eq => 0,
1130 crate::foundation::CompareOp::Ne => 1,
1131 crate::foundation::CompareOp::Lt => 2,
1132 crate::foundation::CompareOp::Lte => 3,
1133 crate::foundation::CompareOp::Gt => 4,
1134 crate::foundation::CompareOp::Gte => 5,
1135 }
1136}
1137fn flag_priority_hash(priority: crate::foundation::FlagPriority) -> u8 {
1138 match priority {
1139 crate::foundation::FlagPriority::ResetWins => 0,
1140 crate::foundation::FlagPriority::SetWins => 1,
1141 }
1142}
1143fn timer_mode_hash(mode: crate::foundation::TimerMode) -> u8 {
1144 match mode {
1145 crate::foundation::TimerMode::FedCountdown => 0,
1146 crate::foundation::TimerMode::PulseHold => 1,
1147 }
1148}
1149fn calc_hash(op: crate::foundation::CalcOp) -> u8 {
1150 match op {
1151 crate::foundation::CalcOp::Add => 0,
1152 crate::foundation::CalcOp::Sub => 1,
1153 crate::foundation::CalcOp::Mul => 2,
1154 crate::foundation::CalcOp::Div => 3,
1155 }
1156}
1157fn option_signal_hash(hash: &mut Fnv1a, value: Option<Signal>) {
1158 match value {
1159 Some(value) => {
1160 hash.u8(1);
1161 signal_hash(hash, value);
1162 }
1163 None => hash.u8(0),
1164 }
1165}
1166fn signal_hash(hash: &mut Fnv1a, value: Signal) {
1167 #[cfg(feature = "signal-f32")]
1168 hash.u64(u64::from(value.to_bits()));
1169 #[cfg(feature = "signal-i32")]
1170 hash.bytes(&value.to_le_bytes());
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175 use super::*;
1176 use crate::foundation::{HostTime, KnotKind, SignalDomain, ONE};
1177 use crate::{BindOpts, Weave};
1178
1179 fn outbox_runtime() -> Runtime {
1180 let mut builder = Weave::builder("snapshot-shapes").unwrap();
1181 let source = builder
1182 .knot("source", KnotKind::constant(ONE, SignalDomain::Bool))
1183 .unwrap();
1184 let output = builder
1185 .knot(
1186 "output",
1187 KnotKind::signal_out("snapshot.value", SignalDomain::Bool),
1188 )
1189 .unwrap();
1190 let emit = builder
1191 .knot("emit", KnotKind::emit_command("snapshot.command"))
1192 .unwrap();
1193 for (target, port) in [(&output, "in"), (&emit, "trigger")] {
1194 let from = builder.output(&source, "out").unwrap();
1195 let to = builder.input(target, port).unwrap();
1196 builder.connect(from, to).unwrap();
1197 }
1198 let mut runtime = Runtime::bind(
1199 builder.build().unwrap(),
1200 BindOpts {
1201 max_emits_per_tick: 1,
1202 ..BindOpts::default()
1203 },
1204 )
1205 .unwrap();
1206 runtime.begin_frame(HostTime { tick: 0 });
1207 runtime.loom();
1208 runtime
1209 }
1210
1211 #[test]
1212 fn restore_rejects_version_shapes_and_invalid_runtime_state() {
1213 let mut runtime = outbox_runtime();
1214 assert_eq!(
1215 Runtime::state_format_version(),
1216 RUNTIME_STATE_FORMAT_VERSION
1217 );
1218
1219 let mut state = runtime.snapshot();
1220 state.version = 0;
1221 assert!(matches!(
1222 runtime.restore(&state),
1223 Err(RestoreError::UnsupportedVersion { .. })
1224 ));
1225
1226 let mut state = runtime.snapshot();
1227 state.data.sense_values.pop();
1228 assert!(matches!(
1229 runtime.restore(&state),
1230 Err(RestoreError::ShapeMismatch {
1231 field: "sense_values",
1232 ..
1233 })
1234 ));
1235
1236 let mut state = runtime.snapshot();
1237 state.data.on_start_done.pop();
1238 assert!(matches!(
1239 runtime.restore(&state),
1240 Err(RestoreError::ShapeMismatch {
1241 field: "on_start_done",
1242 ..
1243 })
1244 ));
1245
1246 let mut state = runtime.snapshot();
1247 state.data.rng = 0;
1248 assert!(matches!(
1249 runtime.restore(&state),
1250 Err(RestoreError::InvalidRng)
1251 ));
1252 }
1253
1254 #[test]
1255 fn restore_clears_ephemeral_outbox_effects() {
1256 let source = outbox_runtime();
1257 let state = source.snapshot();
1258 let mut destination = outbox_runtime();
1259 destination.restore(&state).unwrap();
1260 assert!(destination.outbox().signals().is_empty());
1261 assert!(destination.outbox().emits().is_empty());
1262 }
1263}