1use core::fmt;
4
5use crate::authoring::{BuildError, ValidationError};
6use crate::foundation::{NumericPath, PortSlot, SignalDomain};
7use std::boxed::Box;
8use std::string::String;
9
10use crate::runtime_impl::handles::{CmdId, HostPathId, KnotHandle, SenseId};
11
12#[derive(Clone, Debug, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum RestoreError {
16 UnsupportedVersion {
18 found: u32,
20 supported: u32,
22 },
23 FingerprintMismatch {
25 expected: u64,
27 found: u64,
29 },
30 NumericPathMismatch {
32 expected: NumericPath,
34 found: NumericPath,
36 },
37 InvalidPhase {
39 found: u8,
41 },
42 InvalidSignal {
44 field: &'static str,
46 knot: usize,
48 domain: SignalDomain,
50 },
51 InvalidTimer {
53 knot: usize,
55 remaining: u16,
57 max: u16,
59 },
60 InvalidDelayHead {
62 knot: usize,
64 head: u16,
66 len: u16,
68 },
69 InvalidRng,
71 ShapeMismatch {
73 field: &'static str,
75 expected: usize,
77 found: usize,
79 },
80 InvalidHandleIndex {
82 field: &'static str,
84 index: u16,
86 len: usize,
88 },
89}
90
91impl fmt::Display for RestoreError {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 match self {
94 Self::UnsupportedVersion { found, supported } => write!(
95 f,
96 "runtime snapshot format version {found} is unsupported (expected {supported})"
97 ),
98 Self::FingerprintMismatch { expected, found } => write!(
99 f,
100 "runtime snapshot fingerprint {found:016x} does not match {expected:016x}"
101 ),
102 Self::NumericPathMismatch { expected, found } => write!(
103 f,
104 "runtime checkpoint numeric path {found:?} does not match {expected:?}"
105 ),
106 Self::InvalidPhase { found } => write!(f, "runtime checkpoint phase {found} is invalid"),
107 Self::InvalidSignal { field, knot, domain } => write!(
108 f,
109 "runtime checkpoint field '{field}' has an invalid {domain:?} signal at knot {knot}"
110 ),
111 Self::InvalidTimer { knot, remaining, max } => write!(
112 f,
113 "runtime checkpoint timer at knot {knot} has {remaining} ticks remaining, maximum is {max}"
114 ),
115 Self::InvalidDelayHead { knot, head, len } => write!(
116 f,
117 "runtime checkpoint delay at knot {knot} has head {head}, length is {len}"
118 ),
119 Self::InvalidRng => f.write_str("runtime checkpoint has an invalid zero RNG state"),
120 Self::ShapeMismatch {
121 field,
122 expected,
123 found,
124 } => write!(
125 f,
126 "runtime snapshot field '{field}' has length {found}, expected {expected}"
127 ),
128 Self::InvalidHandleIndex { field, index, len } => write!(
129 f,
130 "runtime snapshot field '{field}' has invalid index {index} for length {len}"
131 ),
132 }
133 }
134}
135
136#[cfg(feature = "std")]
137impl std::error::Error for RestoreError {}
138
139#[derive(Clone, Debug, PartialEq, Eq)]
141#[non_exhaustive]
142pub enum BindRestoreError {
143 Bind(Box<BindError>),
145 Restore(RestoreError),
147}
148
149impl fmt::Display for BindRestoreError {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 match self {
152 Self::Bind(error) => write!(f, "cannot bind restored runtime: {error}"),
153 Self::Restore(error) => write!(f, "cannot restore runtime checkpoint: {error}"),
154 }
155 }
156}
157
158#[cfg(feature = "std")]
159impl std::error::Error for BindRestoreError {}
160
161#[derive(Clone, Debug, PartialEq, Eq)]
163#[non_exhaustive]
164pub enum PresetError {
165 Bind(Box<BindError>),
167 Duplicate {
169 knot: String,
171 },
172 Missing {
174 knot: String,
176 },
177 WrongKind {
179 knot: String,
181 expected: &'static str,
183 },
184 InvalidSignal {
186 knot: String,
188 domain: SignalDomain,
190 },
191}
192
193impl fmt::Display for PresetError {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 match self {
196 Self::Bind(error) => write!(f, "cannot bind preset runtime: {error}"),
197 Self::Duplicate { knot } => write!(f, "runtime preset names '{knot}' more than once"),
198 Self::Missing { knot } => write!(f, "runtime preset knot '{knot}' is missing"),
199 Self::WrongKind { knot, expected } => {
200 write!(f, "runtime preset knot '{knot}' is not a {expected}")
201 }
202 Self::InvalidSignal { knot, domain } => write!(
203 f,
204 "runtime preset signal '{knot}' is invalid for {domain:?}"
205 ),
206 }
207 }
208}
209
210#[cfg(feature = "std")]
211impl std::error::Error for PresetError {}
212
213#[derive(Clone, Debug, PartialEq, Eq)]
215#[non_exhaustive]
216pub enum BindError {
217 InvalidWeave {
219 weave_id: String,
221 source: ValidationError,
223 },
224 CapacityExceeded {
226 weave_id: String,
228 resource: &'static str,
230 count: usize,
232 },
233 InvalidReference {
235 weave_id: String,
237 knot: String,
239 port: String,
241 },
242 InvalidTopology {
244 weave_id: String,
246 },
247}
248
249impl fmt::Display for BindError {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 match self {
252 Self::InvalidWeave { weave_id, source } => {
253 write!(f, "cannot bind weave '{weave_id}': {source}")
254 }
255 Self::CapacityExceeded {
256 weave_id,
257 resource,
258 count,
259 } => write!(
260 f,
261 "cannot bind weave '{weave_id}': {resource} count {count} exceeds runtime capacity"
262 ),
263 Self::InvalidReference {
264 weave_id,
265 knot,
266 port,
267 } => write!(
268 f,
269 "cannot bind weave '{weave_id}': unresolved port '{knot}.{port}'"
270 ),
271 Self::InvalidTopology { weave_id } => {
272 write!(f, "cannot bind weave '{weave_id}': invalid topology")
273 }
274 }
275 }
276}
277
278#[cfg(feature = "std")]
279impl std::error::Error for BindError {
280 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
281 match self {
282 Self::InvalidWeave { source, .. } => Some(source),
283 _ => None,
284 }
285 }
286}
287
288#[derive(Clone, Copy, Debug, PartialEq, Eq)]
290#[non_exhaustive]
291pub enum HandleError {
292 ForeignRuntime {
294 handle: &'static str,
296 },
297 InvalidSense {
299 sense: SenseId,
301 },
302 InvalidHostPath {
304 path: HostPathId,
306 },
307 InvalidCommand {
309 cmd: CmdId,
311 },
312 InvalidKnot {
314 knot: KnotHandle,
316 },
317 InvalidPort {
319 knot: KnotHandle,
321 port: PortSlot,
323 },
324 DomainValue {
326 sense: SenseId,
328 domain: SignalDomain,
330 },
331}
332
333impl fmt::Display for HandleError {
334 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335 match self {
336 Self::ForeignRuntime { handle } => {
337 write!(f, "{handle} handle belongs to a different runtime")
338 }
339 Self::InvalidSense { sense } => {
340 write!(
341 f,
342 "sense handle {} is invalid for this runtime",
343 sense.get()
344 )
345 }
346 Self::InvalidHostPath { path } => {
347 write!(f, "host path handle {} is invalid", path.get())
348 }
349 Self::InvalidCommand { cmd } => {
350 write!(f, "command handle {} is invalid", cmd.get())
351 }
352 Self::InvalidKnot { knot } => {
353 write!(f, "knot handle {} is invalid for this runtime", knot.get())
354 }
355 Self::InvalidPort { knot, port } => write!(
356 f,
357 "port handle {} is invalid for knot {} in this runtime",
358 port.get(),
359 knot.get()
360 ),
361 Self::DomainValue { sense, domain } => write!(
362 f,
363 "sense value for handle {} is invalid for {domain:?} domain",
364 sense.get()
365 ),
366 }
367 }
368}
369
370#[cfg(feature = "std")]
371impl std::error::Error for HandleError {}
372
373#[derive(Clone, Copy, Debug, PartialEq, Eq)]
375pub enum RecipeEndpoint {
376 SignalIn,
378 SignalOut,
380 EmitCommand,
382 Knot,
384 Port,
386}
387
388impl fmt::Display for RecipeEndpoint {
389 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390 match self {
391 Self::SignalIn => f.write_str("SignalIn"),
392 Self::SignalOut => f.write_str("SignalOut"),
393 Self::EmitCommand => f.write_str("EmitCommand"),
394 Self::Knot => f.write_str("knot"),
395 Self::Port => f.write_str("port"),
396 }
397 }
398}
399
400#[derive(Clone, Debug, PartialEq, Eq)]
402#[non_exhaustive]
403pub enum RecipeResolveError {
404 Missing {
406 endpoint: RecipeEndpoint,
408 name: String,
410 },
411 Invalid {
413 endpoint: RecipeEndpoint,
415 name: String,
417 reason: &'static str,
419 },
420}
421
422impl fmt::Display for RecipeResolveError {
423 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424 match self {
425 Self::Missing { endpoint, name } => {
426 write!(f, "required {endpoint} endpoint '{name}' is missing")
427 }
428 Self::Invalid {
429 endpoint,
430 name,
431 reason,
432 } => write!(
433 f,
434 "required {endpoint} endpoint '{name}' is invalid: {reason}"
435 ),
436 }
437 }
438}
439
440#[cfg(feature = "std")]
441impl std::error::Error for RecipeResolveError {}
442
443#[derive(Clone, Debug, PartialEq, Eq)]
445#[non_exhaustive]
446pub enum RecipeError {
447 Build(BuildError),
449 Bind(Box<BindError>),
451 Resolve(RecipeResolveError),
453}
454
455impl From<BuildError> for RecipeError {
456 fn from(value: BuildError) -> Self {
457 Self::Build(value)
458 }
459}
460
461impl From<BindError> for RecipeError {
462 fn from(value: BindError) -> Self {
463 Self::Bind(Box::new(value))
464 }
465}
466
467impl From<RecipeResolveError> for RecipeError {
468 fn from(value: RecipeResolveError) -> Self {
469 Self::Resolve(value)
470 }
471}
472
473impl fmt::Display for RecipeError {
474 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475 match self {
476 Self::Build(source) => write!(f, "cannot build recipe: {source}"),
477 Self::Bind(source) => write!(f, "cannot bind recipe: {source}"),
478 Self::Resolve(source) => write!(f, "cannot resolve recipe ports: {source}"),
479 }
480 }
481}
482
483#[cfg(feature = "std")]
484impl std::error::Error for RecipeError {
485 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
486 match self {
487 Self::Build(source) => Some(source),
488 Self::Bind(source) => Some(source.as_ref()),
489 Self::Resolve(source) => Some(source),
490 }
491 }
492}
493
494#[derive(Clone, Debug, PartialEq)]
496#[non_exhaustive]
497pub enum ScenarioError {
498 Recipe(RecipeError),
500 Handle(HandleError),
502 MissingSignal {
504 path: String,
506 tick: u64,
508 },
509 UnexpectedSignal {
511 path: String,
513 expected: crate::Signal,
515 actual: crate::Signal,
517 tick: u64,
519 },
520 ExpectedTruthy {
522 path: String,
524 actual: crate::Signal,
526 tick: u64,
528 },
529 UnexpectedEmits {
531 command: String,
533 expected: usize,
535 actual: usize,
537 tick: u64,
539 },
540}
541
542impl From<RecipeError> for ScenarioError {
543 fn from(value: RecipeError) -> Self {
544 Self::Recipe(value)
545 }
546}
547
548impl From<HandleError> for ScenarioError {
549 fn from(value: HandleError) -> Self {
550 Self::Handle(value)
551 }
552}
553
554impl fmt::Display for ScenarioError {
555 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
556 match self {
557 Self::Recipe(source) => write!(f, "cannot start scenario: {source}"),
558 Self::Handle(source) => write!(f, "scenario handle error: {source}"),
559 Self::MissingSignal { path, tick } => {
560 write!(f, "SignalOut path '{path}' has no sample in scenario frame {tick}")
561 }
562 Self::UnexpectedSignal {
563 path,
564 expected,
565 actual,
566 tick,
567 } => write!(
568 f,
569 "SignalOut path '{path}' in scenario frame {tick} was {actual:?}, expected {expected:?}"
570 ),
571 Self::ExpectedTruthy { path, actual, tick } => write!(
572 f,
573 "SignalOut path '{path}' in scenario frame {tick} was falsey ({actual:?})"
574 ),
575 Self::UnexpectedEmits {
576 command,
577 expected,
578 actual,
579 tick,
580 } => write!(
581 f,
582 "EmitCommand '{command}' in scenario frame {tick} fired {actual} times, expected {expected}"
583 ),
584 }
585 }
586}
587
588#[cfg(feature = "std")]
589impl std::error::Error for ScenarioError {
590 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
591 match self {
592 Self::Recipe(source) => Some(source),
593 Self::Handle(source) => Some(source),
594 _ => None,
595 }
596 }
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602 use std::error::Error;
603 use std::string::ToString;
604 use std::vec::Vec;
605
606 #[test]
607 fn every_runtime_error_variant_has_a_stable_diagnostic_and_source_chain() {
608 let restore_errors = [
609 RestoreError::UnsupportedVersion {
610 found: 0,
611 supported: 1,
612 },
613 RestoreError::FingerprintMismatch {
614 expected: 1,
615 found: 2,
616 },
617 RestoreError::ShapeMismatch {
618 field: "counter",
619 expected: 3,
620 found: 4,
621 },
622 RestoreError::InvalidHandleIndex {
623 field: "out_emits.command_index",
624 index: 5,
625 len: 6,
626 },
627 ];
628 let diagnostics: Vec<String> = restore_errors.iter().map(ToString::to_string).collect();
629 assert_eq!(
630 diagnostics,
631 [
632 "runtime snapshot format version 0 is unsupported (expected 1)",
633 "runtime snapshot fingerprint 0000000000000002 does not match 0000000000000001",
634 "runtime snapshot field 'counter' has length 4, expected 3",
635 "runtime snapshot field 'out_emits.command_index' has invalid index 5 for length 6",
636 ]
637 );
638 assert!(restore_errors
639 .iter()
640 .all(|error| Error::source(error).is_none()));
641
642 let validation = ValidationError::EmptyWeave {
643 weave_id: String::from("empty"),
644 };
645 let bind_errors = [
646 BindError::InvalidWeave {
647 weave_id: String::from("invalid"),
648 source: validation.clone(),
649 },
650 BindError::CapacityExceeded {
651 weave_id: String::from("capacity"),
652 resource: "knot",
653 count: 257,
654 },
655 BindError::InvalidReference {
656 weave_id: String::from("reference"),
657 knot: String::from("target"),
658 port: String::from("input"),
659 },
660 BindError::InvalidTopology {
661 weave_id: String::from("cycle"),
662 },
663 ];
664 let diagnostics: Vec<String> = bind_errors.iter().map(ToString::to_string).collect();
665 assert_eq!(
666 diagnostics,
667 [
668 "cannot bind weave 'invalid': weave 'empty' has no knots",
669 "cannot bind weave 'capacity': knot count 257 exceeds runtime capacity",
670 "cannot bind weave 'reference': unresolved port 'target.input'",
671 "cannot bind weave 'cycle': invalid topology",
672 ]
673 );
674 assert!(Error::source(&bind_errors[0]).is_some());
675 assert!(bind_errors[1..]
676 .iter()
677 .all(|error| Error::source(error).is_none()));
678
679 let owner = 7;
680 let handles = [
681 HandleError::ForeignRuntime { handle: "sense" },
682 HandleError::InvalidSense {
683 sense: SenseId::new(owner, 1),
684 },
685 HandleError::InvalidHostPath {
686 path: HostPathId::new(owner, 2),
687 },
688 HandleError::InvalidCommand {
689 cmd: CmdId::new(owner, 3),
690 },
691 HandleError::InvalidKnot {
692 knot: KnotHandle::new(owner, 4),
693 },
694 HandleError::InvalidPort {
695 knot: KnotHandle::new(owner, 5),
696 port: PortSlot::new(6),
697 },
698 HandleError::DomainValue {
699 sense: SenseId::new(owner, 7),
700 domain: SignalDomain::Count,
701 },
702 ];
703 let diagnostics: Vec<String> = handles.iter().map(ToString::to_string).collect();
704 assert_eq!(
705 diagnostics,
706 [
707 "sense handle belongs to a different runtime",
708 "sense handle 1 is invalid for this runtime",
709 "host path handle 2 is invalid",
710 "command handle 3 is invalid",
711 "knot handle 4 is invalid for this runtime",
712 "port handle 6 is invalid for knot 5 in this runtime",
713 "sense value for handle 7 is invalid for Count domain",
714 ]
715 );
716 assert!(handles.iter().all(|error| Error::source(error).is_none()));
717
718 let endpoints = [
719 (RecipeEndpoint::SignalIn, "SignalIn"),
720 (RecipeEndpoint::SignalOut, "SignalOut"),
721 (RecipeEndpoint::EmitCommand, "EmitCommand"),
722 (RecipeEndpoint::Knot, "knot"),
723 (RecipeEndpoint::Port, "port"),
724 ];
725 assert!(endpoints
726 .iter()
727 .all(|(endpoint, label)| endpoint.to_string() == *label));
728
729 let resolve_errors = [
730 RecipeResolveError::Missing {
731 endpoint: RecipeEndpoint::SignalOut,
732 name: String::from("door.open"),
733 },
734 RecipeResolveError::Invalid {
735 endpoint: RecipeEndpoint::Port,
736 name: String::from("door.in"),
737 reason: "the port has the wrong domain",
738 },
739 ];
740 assert_eq!(
741 resolve_errors
742 .iter()
743 .map(ToString::to_string)
744 .collect::<Vec<_>>(),
745 [
746 "required SignalOut endpoint 'door.open' is missing",
747 "required port endpoint 'door.in' is invalid: the port has the wrong domain",
748 ]
749 );
750 assert!(resolve_errors
751 .iter()
752 .all(|error| Error::source(error).is_none()));
753
754 let recipe_errors = [
755 RecipeError::from(BuildError::ForeignHandle),
756 RecipeError::from(bind_errors[0].clone()),
757 RecipeError::from(resolve_errors[0].clone()),
758 ];
759 assert_eq!(
760 recipe_errors
761 .iter()
762 .map(ToString::to_string)
763 .collect::<Vec<_>>(),
764 [
765 "cannot build recipe: handle belongs to a different weave builder",
766 "cannot bind recipe: cannot bind weave 'invalid': weave 'empty' has no knots",
767 "cannot resolve recipe ports: required SignalOut endpoint 'door.open' is missing",
768 ]
769 );
770 assert!(recipe_errors
771 .iter()
772 .all(|error| Error::source(error).is_some()));
773
774 let scenario_errors = [
775 ScenarioError::from(recipe_errors[0].clone()),
776 ScenarioError::from(HandleError::ForeignRuntime { handle: "sense" }),
777 ScenarioError::MissingSignal {
778 path: String::from("door.open"),
779 tick: 4,
780 },
781 ScenarioError::UnexpectedSignal {
782 path: String::from("door.open"),
783 expected: crate::ONE,
784 actual: crate::ZERO,
785 tick: 5,
786 },
787 ScenarioError::ExpectedTruthy {
788 path: String::from("door.open"),
789 actual: crate::ZERO,
790 tick: 6,
791 },
792 ScenarioError::UnexpectedEmits {
793 command: String::from("chime"),
794 expected: 2,
795 actual: 1,
796 tick: 7,
797 },
798 ];
799 let diagnostics: Vec<String> = scenario_errors.iter().map(ToString::to_string).collect();
800 assert!(diagnostics[0].starts_with("cannot start scenario: cannot build recipe"));
801 assert!(diagnostics[1].starts_with("scenario handle error: sense"));
802 assert!(diagnostics[2].contains("no sample in scenario frame 4"));
803 assert!(diagnostics[3].contains("was") && diagnostics[3].contains("expected"));
804 assert!(diagnostics[4].contains("was falsey"));
805 assert!(diagnostics[5].contains("fired 1 times, expected 2"));
806 assert!(Error::source(&scenario_errors[0]).is_some());
807 assert!(Error::source(&scenario_errors[1]).is_some());
808 assert!(scenario_errors[2..]
809 .iter()
810 .all(|error| Error::source(error).is_none()));
811 }
812}