Skip to main content

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}