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