scientific_workflow/system_state/state.rs
1//! Fixed-layout, heterogeneous state values at one scientific time point.
2//!
3//! [`SystemState`] is the public typed access boundary over the private
4//! [`StateValue`](super::value::StateValue) erasure layer. Every state owns its
5//! payloads, while all states derived from one [`SystemStateSchema`] share immutable
6//! field metadata and name lookup tables.
7//!
8//! # Layout invariant
9//!
10//! The slot vector always has exactly one entry per field declared by the
11//! specification. A slot may be empty, but fields cannot be added, removed, or
12//! reordered after template loading. The first successful insertion binds a
13//! slot to that payload's concrete Rust type. Taking or clearing the payload
14//! retains this type contract, and [`SystemState::clone_structure_without_payloads`] carries all contracts
15//! into the derived blank state without cloning payloads.
16//!
17//! # Ownership and cloning
18//!
19//! `set` consumes a payload without cloning it. Initial insertion returns
20//! `None`, replacement returns ownership of the previous same-typed payload,
21//! and rejection returns ownership of the unchanged incoming payload through
22//! [`PayloadInsertError`]. `take` moves a stored payload back to the caller. These
23//! operations preserve the backing allocations of ordinary scientific owners
24//! such as `Vec<T>` and tensor containers.
25//!
26//! Explicitly cloning a `SystemState` is different: it shares the immutable
27//! specification but deep-clones every populated payload. Persistence should
28//! borrow a live state during synchronous serialization rather than invoke
29//! this expensive clone.
30//!
31//! # Mutation
32//!
33//! The owning simulation can replace, borrow mutably, extract, or clear every
34//! payload. [`SystemState::borrow_payloads`] and [`SystemState::borrow_payloads_mut`] grant
35//! coordinated access to distinct heterogeneous fields through type and name
36//! tuples, allowing one validated borrow to surround an entire scientific
37//! kernel. The state can also replace the complete time point with `set_time`
38//! or advance it transactionally with `advance`.
39//!
40//! # Type safety
41//!
42//! Typed access uses Rust's exact runtime [`TypeId`](std::any::TypeId). A type
43//! mismatch reports both type names. `take` validates the retained slot type
44//! before removing its owner, so an incorrect request cannot temporarily empty
45//! or discard scientific data.
46//!
47//! # Serialization capability
48//!
49//! New payloads must implement Serde [`Serialize`]. A crate-private accessor
50//! exposes that existing implementation as a borrowed erased trait object for
51//! the storage encoder. `SystemState` itself does not select JSON, frame
52//! records, or perform IO.
53
54use std::any::{Any, TypeId, type_name};
55use std::fmt;
56
57use serde::Serialize;
58
59use super::error::{PayloadInsertError, StateError};
60use super::schema::{StateFieldSchema, SystemStateSchema};
61use super::value::StateValue;
62
63/// The temporal coordinate associated with one [`SystemState`].
64///
65/// `iteration` is always present and provides deterministic ordering, chunk
66/// boundaries, and checkpoint identity. `physical_time` optionally records a
67/// finite domain time such as seconds or model time. Time-axis units belong to
68/// stream metadata so they are not repeated in every state.
69#[derive(Clone, Copy, Debug, PartialEq)]
70pub struct SimulationTime {
71 iteration: u64,
72 physical_time: Option<f64>,
73}
74
75impl SimulationTime {
76 /// Creates an iteration-only time point.
77 pub const fn from_iteration(iteration: u64) -> Self {
78 Self {
79 iteration,
80 physical_time: None,
81 }
82 }
83
84 /// Creates a time point with a finite physical coordinate.
85 ///
86 /// Returns `None` for `NaN` or either infinity. Negative finite values are
87 /// accepted because some scientific coordinate systems use an origin after
88 /// the beginning of a simulation or observation.
89 pub fn from_iteration_and_physical_time(iteration: u64, physical_time: f64) -> Option<Self> {
90 physical_time.is_finite().then_some(Self {
91 iteration,
92 physical_time: Some(physical_time),
93 })
94 }
95
96 /// Returns the deterministic iteration coordinate.
97 pub const fn iteration(self) -> u64 {
98 self.iteration
99 }
100
101 /// Returns the optional physical coordinate.
102 pub const fn physical_time(self) -> Option<f64> {
103 self.physical_time
104 }
105}
106
107/// A heterogeneous collection of payloads describing one system time point.
108///
109/// Fields are declared by a JSON-derived [`SystemStateSchema`]. Values are addressed
110/// by those field names but stored in compact optional slots. The type-erased
111/// representation remains private; callers always insert, borrow, mutate, and
112/// extract concrete Rust types.
113pub struct SystemState {
114 spec: SystemStateSchema,
115 time: SimulationTime,
116 slots: Vec<StateSlot>,
117}
118
119/// One fixed state field's retained type contract and optional payload.
120///
121/// A JSON template creates an unbound slot. The first accepted payload records
122/// its exact runtime type independently from the optional owner, allowing an
123/// emptied slot and every state derived from it to reject accidental retyping.
124/// This structure is private because callers interact only with concrete types
125/// through [`SystemState`].
126#[derive(Clone)]
127struct StateSlot {
128 definition: Option<ValueType>,
129 value: Option<StateValue>,
130}
131
132impl StateSlot {
133 /// Creates a payload-empty slot without a concrete type contract.
134 const fn unbound() -> Self {
135 Self {
136 definition: None,
137 value: None,
138 }
139 }
140
141 /// Creates a payload-empty slot retaining this slot's type contract.
142 const fn empty_like(&self) -> Self {
143 Self {
144 definition: self.definition,
145 value: None,
146 }
147 }
148}
149
150/// Copyable runtime identity retained after a slot's payload is removed.
151#[derive(Clone, Copy)]
152struct ValueType {
153 id: TypeId,
154 name: &'static str,
155}
156
157impl ValueType {
158 /// Captures the exact runtime identity and diagnostic name of `T`.
159 fn of<T>() -> Self
160 where
161 T: Any,
162 {
163 Self {
164 id: TypeId::of::<T>(),
165 name: type_name::<T>(),
166 }
167 }
168
169 /// Reports whether this definition names the exact concrete type `T`.
170 fn is<T>(self) -> bool
171 where
172 T: Any,
173 {
174 self.id == TypeId::of::<T>()
175 }
176}
177
178impl SystemState {
179 /// Creates an empty state from a validated specification.
180 ///
181 /// This constructor is crate-private so an external caller cannot create a
182 /// state without first loading a template. [`SystemStateSchema::create_empty_state`] is the
183 /// public initial construction path.
184 pub(crate) fn new(spec: SystemStateSchema, time: SimulationTime) -> Self {
185 let slots = (0..spec.len()).map(|_| StateSlot::unbound()).collect();
186 Self { spec, time, slots }
187 }
188
189 /// Creates another empty state with the same specification and field types.
190 ///
191 /// No payload is cloned. The immutable specification handle is shared, and
192 /// each assembly-established concrete type contract is copied into an empty
193 /// slot. A later [`SystemState::insert_payload`] must therefore use the same type even
194 /// though the derived state begins without payloads.
195 pub fn clone_structure_without_payloads(&self, time: SimulationTime) -> Self {
196 Self {
197 spec: self.spec.clone(),
198 time,
199 slots: self.slots.iter().map(StateSlot::empty_like).collect(),
200 }
201 }
202
203 /// Returns this state's temporal coordinate.
204 pub const fn simulation_time(&self) -> SimulationTime {
205 self.time
206 }
207
208 /// Replaces this state's complete temporal coordinate.
209 ///
210 /// The previous [`SimulationTime`] is returned by value. Both coordinates are
211 /// small `Copy` values, so replacement performs no heap allocation and
212 /// does not inspect, move, or clone any scientific payload.
213 ///
214 /// Replacing the complete value rather than exposing its individual fields
215 /// ensures that a physical coordinate can enter a state only through the
216 /// finite-value validation performed by
217 /// [`SimulationTime::from_iteration_and_physical_time`].
218 ///
219 /// # Collection invariants
220 ///
221 /// A state stored inside a time-ordered collection must not be passed as
222 /// `&mut SystemState` to external callers: changing its time could violate
223 /// collection ordering. The owning simulation may freely call this method
224 /// before submitting a state or encoded sample.
225 pub fn replace_simulation_time(&mut self, time: SimulationTime) -> SimulationTime {
226 std::mem::replace(&mut self.time, time)
227 }
228
229 /// Advances the iteration by one after one completed model step.
230 ///
231 /// Passing `None` increments only the authoritative iteration and
232 /// preserves the current optional physical coordinate. Passing
233 /// `Some(delta)` additionally requires an existing physical coordinate,
234 /// a finite `delta`, and a finite sum. Negative and zero finite deltas are
235 /// valid because iteration—not physical time—defines record ordering.
236 ///
237 /// On success, the new [`SimulationTime`] is stored and returned. All validation
238 /// occurs before assignment, so every error leaves the original time point
239 /// unchanged.
240 ///
241 /// # Errors
242 ///
243 /// Returns:
244 ///
245 /// - [`StateError::IterationOverflow`] when the current iteration is
246 /// `u64::MAX`;
247 /// - [`StateError::MissingPhysicalTime`] when a delta is supplied but the
248 /// current state has no physical coordinate;
249 /// - [`StateError::InvalidPhysicalAdvance`] when the delta or resulting
250 /// coordinate is not finite.
251 pub fn advance_simulation_time(
252 &mut self,
253 physical_time_increment: Option<f64>,
254 ) -> Result<SimulationTime, StateError> {
255 let next_iteration =
256 self.time
257 .iteration
258 .checked_add(1)
259 .ok_or(StateError::IterationOverflow {
260 iteration: self.time.iteration,
261 })?;
262
263 let next_physical_time = match (self.time.physical_time, physical_time_increment) {
264 (physical_time, None) => physical_time,
265 (None, Some(_)) => {
266 return Err(StateError::MissingPhysicalTime {
267 iteration: self.time.iteration,
268 });
269 }
270 (Some(current), Some(delta)) => {
271 let next = current + delta;
272 if !delta.is_finite() || !next.is_finite() {
273 return Err(StateError::InvalidPhysicalAdvance { current, delta });
274 }
275 Some(next)
276 }
277 };
278
279 let next = SimulationTime {
280 iteration: next_iteration,
281 physical_time: next_physical_time,
282 };
283 self.time = next;
284 Ok(next)
285 }
286
287 /// Returns the shared immutable field specification.
288 pub const fn schema(&self) -> &SystemStateSchema {
289 &self.spec
290 }
291
292 /// Returns the number of fields declared by the state specification.
293 ///
294 /// This count is structural and includes empty payload slots.
295 pub fn declared_field_count(&self) -> usize {
296 self.slots.len()
297 }
298
299 /// Reports whether the state specification declares no fields.
300 ///
301 /// This is consistent with [`SystemState::declared_field_count`]. To test whether a
302 /// non-empty layout currently carries no payloads, use
303 /// [`SystemState::has_no_payloads`].
304 pub fn has_no_declared_fields(&self) -> bool {
305 self.slots.is_empty()
306 }
307
308 /// Returns the number of slots that currently contain payloads.
309 pub fn populated_field_count(&self) -> usize {
310 self.slots
311 .iter()
312 .filter(|slot| slot.value.is_some())
313 .count()
314 }
315
316 /// Reports whether every declared payload slot is empty.
317 pub fn has_no_payloads(&self) -> bool {
318 self.slots.iter().all(|slot| slot.value.is_none())
319 }
320
321 /// Returns field specifications in deterministic template order.
322 pub fn field_schemas(&self) -> &[StateFieldSchema] {
323 self.spec.field_schemas()
324 }
325
326 /// Reports whether a declared field currently contains a payload.
327 ///
328 /// # Errors
329 ///
330 /// Returns [`StateError::UnknownField`] when `key` was not declared by the
331 /// JSON template.
332 pub fn contains_payload(&self, key: &str) -> Result<bool, StateError> {
333 let index = self.spec.index_of(key)?;
334 Ok(self.slots[index].value.is_some())
335 }
336
337 /// Reports whether a populated field contains the exact Rust type `T`.
338 ///
339 /// An empty declared field returns `false`.
340 ///
341 /// # Errors
342 ///
343 /// Returns [`StateError::UnknownField`] when `key` was not declared by the
344 /// JSON template.
345 pub fn payload_has_type<T>(&self, key: &str) -> Result<bool, StateError>
346 where
347 T: Any,
348 {
349 let index = self.spec.index_of(key)?;
350 Ok(self.slots[index]
351 .value
352 .as_ref()
353 .is_some_and(StateValue::is::<T>))
354 }
355
356 /// Sets or replaces a payload while preserving ownership on every outcome.
357 ///
358 /// `payload` moves into this operation and is never cloned:
359 ///
360 /// - a never-populated declared slot binds itself to `T`, receives the
361 /// payload, and returns `Ok(None)`;
362 /// - a slot bound to exactly `T` receives it and returns the displaced
363 /// payload as `Ok(Some(previous))`, or `Ok(None)` when currently empty;
364 /// - an undeclared key returns `Err(PayloadInsertError<T>)` containing the unchanged
365 /// incoming payload;
366 /// - a slot bound to another concrete type remains unchanged and returns
367 /// the incoming payload in `PayloadInsertError<T>`, even when its payload is empty.
368 ///
369 /// Returning a previous payload is deliberate assignment behavior. A
370 /// caller that does not need that owner should discard it explicitly:
371 ///
372 /// ```no_run
373 /// # use scientific_workflow::system_state::{SystemStateSchema, SimulationTime};
374 /// # fn example(spec: &SystemStateSchema) -> Result<(), Box<dyn std::error::Error>> {
375 /// let mut state = spec.create_empty_state(SimulationTime::from_iteration(0));
376 /// drop(state.insert_payload("population", vec![1_u64, 2, 3])?);
377 /// # Ok(())
378 /// # }
379 /// ```
380 ///
381 /// Type-contract validation occurs before the slot is changed.
382 /// Consequently, rejection cannot discard or temporarily remove an
383 /// existing scientific value, and `take` or `clear` cannot reopen a field
384 /// for a different type.
385 ///
386 /// # Errors
387 ///
388 /// Returns [`PayloadInsertError`] containing:
389 ///
390 /// - [`StateError::UnknownField`] when `key` is undeclared;
391 /// - [`StateError::TypeMismatch`] when the slot is bound to a different
392 /// concrete Rust type.
393 ///
394 /// In both cases [`PayloadInsertError::into_parts`] recovers the unchanged incoming
395 /// `T` without cloning it.
396 pub fn insert_payload<T>(
397 &mut self,
398 key: &str,
399 payload: T,
400 ) -> Result<Option<T>, PayloadInsertError<T>>
401 where
402 T: Serialize + Clone + Send + 'static,
403 {
404 let index = match self.spec.index_of(key) {
405 Ok(index) => index,
406 Err(error) => return Err(PayloadInsertError::new(error, payload)),
407 };
408
409 let slot = &mut self.slots[index];
410 match slot.definition {
411 Some(definition) if !definition.is::<T>() => {
412 return Err(PayloadInsertError::new(
413 StateError::TypeMismatch {
414 field: key.to_owned(),
415 expected: type_name::<T>(),
416 actual: definition.name,
417 },
418 payload,
419 ));
420 }
421 Some(_) => {}
422 None => slot.definition = Some(ValueType::of::<T>()),
423 }
424
425 let previous = slot.value.replace(StateValue::new(payload));
426 match previous {
427 None => Ok(None),
428 Some(previous) => match previous.downcast::<T>() {
429 Ok(previous) => Ok(Some(previous)),
430 Err(_) => unreachable!("a type-bound StateValue failed its consuming downcast"),
431 },
432 }
433 }
434
435 /// Borrows a populated field as the exact Rust type `T`.
436 ///
437 /// # Errors
438 ///
439 /// Returns [`StateError::UnknownField`] for an undeclared key,
440 /// [`StateError::MissingPayload`] for an empty slot, or
441 /// [`StateError::TypeMismatch`] when the stored concrete type differs from
442 /// `T`.
443 pub fn payload<T>(&self, key: &str) -> Result<&T, StateError>
444 where
445 T: Any,
446 {
447 let index = self.spec.index_of(key)?;
448 self.validate_slot::<T>(index, key)?;
449 Ok(self.slots[index]
450 .value
451 .as_ref()
452 .and_then(StateValue::downcast_ref::<T>)
453 .expect("a validated state slot must contain its bound concrete type"))
454 }
455
456 /// Mutably borrows a populated field as the exact Rust type `T`.
457 ///
458 /// Mutation occurs in place and does not clone the payload.
459 ///
460 /// # Errors
461 ///
462 /// Returns [`StateError::UnknownField`] for an undeclared key,
463 /// [`StateError::MissingPayload`] for an empty slot, or
464 /// [`StateError::TypeMismatch`] when the stored concrete type differs from
465 /// `T`.
466 pub fn payload_mut<T>(&mut self, key: &str) -> Result<&mut T, StateError>
467 where
468 T: Any,
469 {
470 let index = self.spec.index_of(key)?;
471 self.validate_slot::<T>(index, key)?;
472 Ok(self.slots[index]
473 .value
474 .as_mut()
475 .and_then(StateValue::downcast_mut::<T>)
476 .expect("a validated state slot must contain its bound concrete type"))
477 }
478
479 /// Borrows several distinct populated fields as concrete immutable types.
480 ///
481 /// `Q` is a tuple of expected payload types, while `keys` is the equally
482 /// sized tuple of field names. Tuple positions correspond exactly. The
483 /// supported arities are two through eight; single-field callers should use
484 /// [`SystemState::payload`]. The sealed tuple implementation is internal and
485 /// requires no user-defined selector, query object, or macro invocation.
486 ///
487 /// # Errors
488 ///
489 /// Validation proceeds from left to right and completes before references
490 /// are returned. The method reports an unknown field, repeated field,
491 /// retained type mismatch, or missing payload through [`StateError`]. An
492 /// error leaves every slot unchanged.
493 pub fn borrow_payloads<'state, Q>(
494 &'state self,
495 keys: Q::Keys<'_>,
496 ) -> Result<Q::Refs<'state>, StateError>
497 where
498 Q: PayloadTuple,
499 {
500 Q::borrow(self, keys)
501 }
502
503 /// Borrows several distinct populated fields as concrete mutable types.
504 ///
505 /// `Q` is a tuple of expected payload types, while `keys` is the equally
506 /// sized tuple of field names. All names, duplicate indices, retained type
507 /// contracts, and payload presence are validated before any mutable
508 /// reference is produced. Payloads remain owned by this state and are not
509 /// cloned, moved, serialized, locked, or temporarily removed.
510 ///
511 /// One call should normally surround a complete coupled kernel or sweep so
512 /// name lookup and dynamic type validation occur once outside its inner
513 /// loop. Supported arities are two through eight; single-field callers
514 /// should use [`SystemState::payload_mut`].
515 ///
516 /// # Errors
517 ///
518 /// Returns the same deterministic validation errors as
519 /// [`SystemState::borrow_payloads`]. A failure leaves the state unchanged and grants
520 /// no partial borrow.
521 pub fn borrow_payloads_mut<'state, Q>(
522 &'state mut self,
523 keys: Q::Keys<'_>,
524 ) -> Result<Q::RefsMut<'state>, StateError>
525 where
526 Q: PayloadTuple,
527 {
528 Q::borrow_mut(self, keys)
529 }
530
531 /// Removes and returns the payload from a declared field.
532 ///
533 /// A successful call moves the original concrete `T` out of its internal
534 /// box and leaves the field slot empty while retaining its type contract.
535 /// It does not invoke `Clone`. Type and presence validation occurs before
536 /// the payload owner is removed.
537 ///
538 /// # Errors
539 ///
540 /// Returns [`StateError::UnknownField`] for an undeclared key,
541 /// [`StateError::MissingPayload`] for an empty slot, or
542 /// [`StateError::TypeMismatch`] when the stored concrete type differs from
543 /// `T`.
544 pub fn take_payload<T>(&mut self, key: &str) -> Result<T, StateError>
545 where
546 T: Any + Send,
547 {
548 let index = self.spec.index_of(key)?;
549 self.validate_slot::<T>(index, key)?;
550 let value = self.slots[index]
551 .value
552 .take()
553 .expect("a validated state slot must contain a payload");
554 match value.downcast::<T>() {
555 Ok(payload) => Ok(payload),
556 Err(_) => unreachable!("a type-bound StateValue failed its consuming downcast"),
557 }
558 }
559
560 /// Drops the payload stored in one declared field.
561 ///
562 /// Returns `true` when a payload was present and dropped, or `false` when
563 /// the declared slot was already empty.
564 ///
565 /// # Errors
566 ///
567 /// Returns [`StateError::UnknownField`] when `key` was not declared by the
568 /// JSON template.
569 pub fn clear_payload(&mut self, key: &str) -> Result<bool, StateError> {
570 let index = self.spec.index_of(key)?;
571 Ok(self.slots[index].value.take().is_some())
572 }
573
574 /// Drops every payload while retaining layout, type contracts, and time.
575 pub fn clear_all_payloads(&mut self) {
576 self.slots.iter_mut().for_each(|slot| slot.value = None);
577 }
578
579 /// Returns a populated erased value for a typed immutable accessor.
580 fn value(&self, key: &str) -> Result<&StateValue, StateError> {
581 let index = self.spec.index_of(key)?;
582 self.slots[index]
583 .value
584 .as_ref()
585 .ok_or_else(|| StateError::MissingPayload {
586 field: key.to_owned(),
587 })
588 }
589
590 /// Validates one resolved slot against an expected concrete type and value.
591 fn validate_slot<T>(&self, index: usize, key: &str) -> Result<(), StateError>
592 where
593 T: Any,
594 {
595 let slot = &self.slots[index];
596 if let Some(definition) = slot.definition
597 && !definition.is::<T>()
598 {
599 return Err(StateError::TypeMismatch {
600 field: key.to_owned(),
601 expected: type_name::<T>(),
602 actual: definition.name,
603 });
604 }
605
606 if slot.value.is_none() {
607 return Err(StateError::MissingPayload {
608 field: key.to_owned(),
609 });
610 }
611 Ok(())
612 }
613
614 /// Resolves and validates a fixed-size tuple of distinct field names.
615 fn resolve_distinct<const N: usize>(&self, keys: [&str; N]) -> Result<[usize; N], StateError> {
616 let mut indices = [0; N];
617 for (position, key) in keys.iter().enumerate() {
618 let index = self.spec.index_of(key)?;
619 if indices[..position].contains(&index) {
620 return Err(StateError::RepeatedPayloadBorrow {
621 field: (*key).to_owned(),
622 });
623 }
624 indices[position] = index;
625 }
626 Ok(indices)
627 }
628
629 /// Safely separates already validated distinct slot indices.
630 fn disjoint_slots_mut<const N: usize>(&mut self, indices: [usize; N]) -> [&mut StateSlot; N] {
631 let mut positions: [(usize, usize); N] =
632 std::array::from_fn(|position| (position, indices[position]));
633 positions.sort_unstable_by_key(|(_, index)| *index);
634
635 let mut remaining = self.slots.as_mut_slice();
636 let mut base = 0;
637 let mut selected: [Option<&mut StateSlot>; N] = std::array::from_fn(|_| None);
638 for (original_position, index) in positions {
639 let relative = index - base;
640 let (_, at_index) = remaining.split_at_mut(relative);
641 let (slot, tail) = at_index
642 .split_first_mut()
643 .expect("resolved state slot index must be in bounds");
644 selected[original_position] = Some(slot);
645 remaining = tail;
646 base = index + 1;
647 }
648
649 selected
650 .map(|slot| slot.expect("one disjoint slot must be returned for every requested index"))
651 }
652
653 /// Borrows one populated payload through erased Serde serialization.
654 ///
655 /// This crate-private method is the complete format-agnostic boundary used
656 /// by the storage encoder. It performs the same declared-field and
657 /// populated-slot validation as [`SystemState::payload`], but it neither
658 /// downcasts nor exposes the private [`StateValue`] wrapper.
659 ///
660 /// The returned object refers directly to the stored concrete payload. No
661 /// clone, allocation, encoding, or ownership transfer occurs here.
662 #[allow(
663 dead_code,
664 reason = "reserved for storage::JsonStateRecordEncoder, which is implemented in the next module stage"
665 )]
666 pub(crate) fn serializable(
667 &self,
668 key: &str,
669 ) -> Result<&dyn erased_serde::Serialize, StateError> {
670 Ok(self.value(key)?.serializable())
671 }
672}
673
674/// Sealing boundary for the internally generated tuple implementations.
675mod tuple_sealed {
676 /// Prevents downstream crates from implementing the hidden tuple contract.
677 pub trait Sealed {}
678}
679
680/// Internal type-level mapping used by [`SystemState::borrow_payloads`] and
681/// [`SystemState::borrow_payloads_mut`].
682///
683/// This trait must be public because it appears in those generic methods'
684/// signatures, but it is sealed, omitted from the prelude, and hidden from
685/// generated documentation. Applications select an implementation simply by
686/// writing a supported tuple type such as `(Position, Velocity)`.
687#[doc(hidden)]
688pub trait PayloadTuple: tuple_sealed::Sealed {
689 /// Equally sized tuple of borrowed field names.
690 type Keys<'key>;
691
692 /// Equally sized tuple of immutable concrete payload references.
693 type Refs<'state>
694 where
695 Self: 'state;
696
697 /// Equally sized tuple of mutable concrete payload references.
698 type RefsMut<'state>
699 where
700 Self: 'state;
701
702 /// Resolves and immutably borrows one supported field tuple.
703 #[doc(hidden)]
704 fn borrow<'state, 'key>(
705 state: &'state SystemState,
706 keys: Self::Keys<'key>,
707 ) -> Result<Self::Refs<'state>, StateError>;
708
709 /// Resolves and mutably borrows one supported field tuple.
710 #[doc(hidden)]
711 fn borrow_mut<'state, 'key>(
712 state: &'state mut SystemState,
713 keys: Self::Keys<'key>,
714 ) -> Result<Self::RefsMut<'state>, StateError>;
715}
716
717/// Substitutes one repeated generic identifier with a common tuple element.
718macro_rules! substitute_type {
719 ($_generic:ident => $replacement:ty) => {
720 $replacement
721 };
722}
723
724/// Generates the sealed heterogeneous borrow contract for one tuple arity.
725///
726/// Public callers see only `SystemState::borrow_payloads[_mut]`; this macro centralizes
727/// validation order, exact downcasts, and tuple construction so every supported
728/// arity has identical semantics.
729macro_rules! impl_state_tuple {
730 ($(($type:ident, $key:ident, $slot:ident, $index:tt)),+ $(,)?) => {
731 impl<$($type),+> tuple_sealed::Sealed for ($($type,)+)
732 where
733 $($type: Any,)+
734 {
735 }
736
737 impl<$($type),+> PayloadTuple for ($($type,)+)
738 where
739 $($type: Any,)+
740 {
741 type Keys<'key> = ($(substitute_type!($type => &'key str),)+);
742 type Refs<'state> = ($(&'state $type,)+) where Self: 'state;
743 type RefsMut<'state> = ($(&'state mut $type,)+) where Self: 'state;
744
745 fn borrow<'state, 'key>(
746 state: &'state SystemState,
747 keys: Self::Keys<'key>,
748 ) -> Result<Self::Refs<'state>, StateError> {
749 let ($($key,)+) = keys;
750 let indices = state.resolve_distinct([$($key,)+])?;
751 $(state.validate_slot::<$type>(indices[$index], $key)?;)+
752
753 Ok(($(
754 state.slots[indices[$index]]
755 .value
756 .as_ref()
757 .and_then(StateValue::downcast_ref::<$type>)
758 .expect("a preflighted state slot must contain its bound concrete type"),
759 )+))
760 }
761
762 fn borrow_mut<'state, 'key>(
763 state: &'state mut SystemState,
764 keys: Self::Keys<'key>,
765 ) -> Result<Self::RefsMut<'state>, StateError> {
766 let ($($key,)+) = keys;
767 let indices = state.resolve_distinct([$($key,)+])?;
768 $(state.validate_slot::<$type>(indices[$index], $key)?;)+
769 let [$($slot,)+] = state.disjoint_slots_mut(indices);
770
771 Ok(($(
772 $slot
773 .value
774 .as_mut()
775 .and_then(StateValue::downcast_mut::<$type>)
776 .expect("a preflighted state slot must contain its bound concrete type"),
777 )+))
778 }
779 }
780 };
781}
782
783impl_state_tuple!((A, key_a, slot_a, 0), (B, key_b, slot_b, 1));
784impl_state_tuple!(
785 (A, key_a, slot_a, 0),
786 (B, key_b, slot_b, 1),
787 (C, key_c, slot_c, 2),
788);
789impl_state_tuple!(
790 (A, key_a, slot_a, 0),
791 (B, key_b, slot_b, 1),
792 (C, key_c, slot_c, 2),
793 (D, key_d, slot_d, 3),
794);
795impl_state_tuple!(
796 (A, key_a, slot_a, 0),
797 (B, key_b, slot_b, 1),
798 (C, key_c, slot_c, 2),
799 (D, key_d, slot_d, 3),
800 (E, key_e, slot_e, 4),
801);
802impl_state_tuple!(
803 (A, key_a, slot_a, 0),
804 (B, key_b, slot_b, 1),
805 (C, key_c, slot_c, 2),
806 (D, key_d, slot_d, 3),
807 (E, key_e, slot_e, 4),
808 (F, key_f, slot_f, 5),
809);
810impl_state_tuple!(
811 (A, key_a, slot_a, 0),
812 (B, key_b, slot_b, 1),
813 (C, key_c, slot_c, 2),
814 (D, key_d, slot_d, 3),
815 (E, key_e, slot_e, 4),
816 (F, key_f, slot_f, 5),
817 (G, key_g, slot_g, 6),
818);
819impl_state_tuple!(
820 (A, key_a, slot_a, 0),
821 (B, key_b, slot_b, 1),
822 (C, key_c, slot_c, 2),
823 (D, key_d, slot_d, 3),
824 (E, key_e, slot_e, 4),
825 (F, key_f, slot_f, 5),
826 (G, key_g, slot_g, 6),
827 (H, key_h, slot_h, 7),
828);
829
830impl Clone for SystemState {
831 /// Shares the immutable specification and deep-clones populated payloads.
832 fn clone(&self) -> Self {
833 Self {
834 spec: self.spec.clone(),
835 time: self.time,
836 slots: self.slots.clone(),
837 }
838 }
839}
840
841impl fmt::Debug for SystemState {
842 /// Formats structural metadata without formatting scientific payloads.
843 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
844 formatter
845 .debug_struct("SystemState")
846 .field("time", &self.time)
847 .field("source", &self.spec.template_path())
848 .field("fields", &self.declared_field_count())
849 .field("loaded", &self.populated_field_count())
850 .finish_non_exhaustive()
851 }
852}