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::JsonStateRecordEncoder, which is implemented in the next module stage"
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}