Skip to main content

ferrum_interfaces/vnext/execution/
work.rs

1use super::{
2    invalid_plan, Deserialize, Deserializer, Digest, Range, Serialize, Sha256, VNextError,
3};
4use std::sync::Arc;
5
6pub const MAX_PROVIDER_WORKSPACE_SHAPE_BUCKETS: usize = 64;
7
8/// Internal evaluator dimensions. Public callers supply token/page evidence;
9/// only core lowers that evidence into aggregate formula inputs.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub(crate) struct DynamicResourceShape {
12    pub(super) sequences: u32,
13    pub(super) tokens: u64,
14    pub(super) pages: u64,
15}
16
17impl DynamicResourceShape {
18    pub(crate) fn new(sequences: u32, tokens: u64, pages: u64) -> Result<Self, VNextError> {
19        if sequences == 0 || tokens == 0 || pages == 0 {
20            return Err(invalid_plan(
21                "dynamic resource shape dimensions must be non-zero",
22            ));
23        }
24        Ok(Self {
25            sequences,
26            tokens,
27            pages,
28        })
29    }
30
31    pub(crate) const fn sequences(self) -> u32 {
32        self.sequences
33    }
34
35    pub(crate) const fn tokens(self) -> u64 {
36        self.tokens
37    }
38
39    pub(crate) const fn pages(self) -> u64 {
40        self.pages
41    }
42
43    pub(crate) const fn from_validated(sequences: u32, tokens: u64, pages: u64) -> Self {
44        Self {
45            sequences,
46            tokens,
47            pages,
48        }
49    }
50}
51
52/// Evidence for one non-empty immediate token span inside an exact full input.
53/// Counts are derived from the supplied token slice and private range rather
54/// than accepted as caller-provided aggregate dimensions.
55#[derive(Clone, Serialize)]
56pub struct TokenSpanWork {
57    pub(super) immediate_tokens: u64,
58    pub(super) full_input_tokens: u64,
59    pub(super) fit_input_tokens: u64,
60    pub(super) immediate_start_token: u64,
61    pub(super) immediate_end_token: u64,
62    pub(super) fingerprint: String,
63    // Optional process-local evidence. It does not change ordinary work
64    // identity, serialized plans, or resource admission fingerprints.
65    #[serde(skip)]
66    checkpoint_tokens: Option<Arc<[u32]>>,
67}
68
69impl std::fmt::Debug for TokenSpanWork {
70    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        formatter
72            .debug_struct("TokenSpanWork")
73            .field("immediate_tokens", &self.immediate_tokens)
74            .field("full_input_tokens", &self.full_input_tokens)
75            .field("fit_input_tokens", &self.fit_input_tokens)
76            .field("immediate_start_token", &self.immediate_start_token)
77            .field("immediate_end_token", &self.immediate_end_token)
78            .field("fingerprint", &self.fingerprint)
79            .finish()
80    }
81}
82
83impl PartialEq for TokenSpanWork {
84    fn eq(&self, other: &Self) -> bool {
85        self.immediate_tokens == other.immediate_tokens
86            && self.full_input_tokens == other.full_input_tokens
87            && self.fit_input_tokens == other.fit_input_tokens
88            && self.immediate_start_token == other.immediate_start_token
89            && self.immediate_end_token == other.immediate_end_token
90            && self.fingerprint == other.fingerprint
91    }
92}
93
94impl Eq for TokenSpanWork {}
95
96impl TokenSpanWork {
97    pub fn from_token_ids(
98        full_input: &[u32],
99        immediate_range: Range<usize>,
100    ) -> Result<Self, VNextError> {
101        Self::from_token_ids_with_fit(full_input, immediate_range, full_input.len())
102    }
103
104    /// Binds exact token evidence to a larger request fit ceiling. The fit
105    /// ceiling participates in logical admission only; current backing and
106    /// provider execution remain derived from the real token slice.
107    pub fn from_token_ids_with_fit(
108        full_input: &[u32],
109        immediate_range: Range<usize>,
110        fit_input_tokens: usize,
111    ) -> Result<Self, VNextError> {
112        if full_input.is_empty()
113            || immediate_range.start >= immediate_range.end
114            || immediate_range.end > full_input.len()
115            || fit_input_tokens < full_input.len()
116        {
117            return Err(invalid_plan(
118                "token work requires a non-empty in-bounds immediate span and a non-regressing fit ceiling",
119            ));
120        }
121        let immediate_tokens = u64::try_from(immediate_range.len())
122            .map_err(|_| invalid_plan("immediate token span exceeds u64"))?;
123        let full_input_tokens = u64::try_from(full_input.len())
124            .map_err(|_| invalid_plan("full token input exceeds u64"))?;
125        let fit_input_tokens = u64::try_from(fit_input_tokens)
126            .map_err(|_| invalid_plan("fit token input exceeds u64"))?;
127        let immediate_start_token = u64::try_from(immediate_range.start)
128            .map_err(|_| invalid_plan("token span start exceeds u64"))?;
129        let immediate_end_token = u64::try_from(immediate_range.end)
130            .map_err(|_| invalid_plan("token span end exceeds u64"))?;
131        let mut digest = Sha256::new();
132        digest.update(b"ferrum.runtime-vnext.token-span-work.v3\0");
133        digest.update(full_input_tokens.to_le_bytes());
134        digest.update(fit_input_tokens.to_le_bytes());
135        digest.update(immediate_start_token.to_le_bytes());
136        digest.update(immediate_end_token.to_le_bytes());
137        for token in full_input {
138            digest.update(token.to_le_bytes());
139        }
140        Ok(Self {
141            immediate_tokens,
142            full_input_tokens,
143            fit_input_tokens,
144            immediate_start_token,
145            immediate_end_token,
146            fingerprint: format!("{:x}", digest.finalize()),
147            checkpoint_tokens: None,
148        })
149    }
150
151    /// Retains the exact input for completed-state prefix validation. The
152    /// supplied tokens must reproduce this work's existing identity. Ordinary
153    /// execution need not retain them and remains wire-compatible.
154    pub fn with_checkpoint_tokens(mut self, tokens: Arc<[u32]>) -> Result<Self, VNextError> {
155        let start = usize::try_from(self.immediate_start_token)
156            .map_err(|_| invalid_plan("checkpoint span start exceeds usize"))?;
157        let end = usize::try_from(self.immediate_end_token)
158            .map_err(|_| invalid_plan("checkpoint span end exceeds usize"))?;
159        let fit = usize::try_from(self.fit_input_tokens)
160            .map_err(|_| invalid_plan("checkpoint fit ceiling exceeds usize"))?;
161        if Self::from_token_ids_with_fit(&tokens, start..end, fit)? != self {
162            return Err(invalid_plan(
163                "checkpoint tokens differ from admitted token work",
164            ));
165        }
166        self.checkpoint_tokens = Some(tokens);
167        Ok(self)
168    }
169
170    pub(crate) fn checkpoint_tokens(&self) -> Option<&Arc<[u32]>> {
171        self.checkpoint_tokens.as_ref()
172    }
173
174    pub const fn immediate_tokens(&self) -> u64 {
175        self.immediate_tokens
176    }
177
178    pub const fn full_input_tokens(&self) -> u64 {
179        self.full_input_tokens
180    }
181
182    pub const fn fit_input_tokens(&self) -> u64 {
183        self.fit_input_tokens
184    }
185
186    pub fn immediate_token_range(&self) -> Range<u64> {
187        self.immediate_start_token..self.immediate_end_token
188    }
189
190    pub fn fingerprint(&self) -> &str {
191        &self.fingerprint
192    }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
196pub(crate) struct CommittedPageWork {
197    pub(super) pages: u64,
198    pub(super) fingerprint: String,
199}
200
201impl CommittedPageWork {
202    pub(crate) fn new(pages: u64, fingerprint: String) -> Result<Self, VNextError> {
203        if pages == 0
204            || fingerprint.len() != 64
205            || !fingerprint
206                .bytes()
207                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
208        {
209            return Err(invalid_plan("committed page work evidence is invalid"));
210        }
211        Ok(Self { pages, fingerprint })
212    }
213
214    pub(crate) const fn pages(&self) -> u64 {
215        self.pages
216    }
217}
218
219/// Typed shape evidence shared by scoped admission and provider formula
220/// evaluation. The aggregate dimensions and fingerprint are core-derived.
221#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
222pub struct ResourceWorkShape {
223    pub(super) token_spans: Vec<TokenSpanWork>,
224    pub(super) committed_pages: Vec<CommittedPageWork>,
225    pub(super) immediate_sequences: u32,
226    pub(super) immediate_tokens: u64,
227    pub(super) immediate_pages: u64,
228    pub(super) fit_sequences: u32,
229    pub(super) fit_tokens: u64,
230    pub(super) fit_pages: u64,
231    pub(super) fingerprint: String,
232}
233
234impl ResourceWorkShape {
235    pub fn from_token_spans(token_spans: Vec<TokenSpanWork>) -> Result<Self, VNextError> {
236        Self::from_sources(token_spans, Vec::new())
237    }
238
239    pub fn single(token_span: TokenSpanWork) -> Result<Self, VNextError> {
240        Self::from_token_spans(vec![token_span])
241    }
242
243    /// Revalidates the complete input against the exact single-sequence work
244    /// admitted earlier, including its original range and fit ceiling.
245    pub(crate) fn validate_single_checkpoint_input(
246        &self,
247        tokens: Arc<[u32]>,
248    ) -> Result<(), VNextError> {
249        let [span] = self.token_spans.as_slice() else {
250            return Err(invalid_plan(
251                "checkpoint input requires one admitted sequence token span",
252            ));
253        };
254        span.clone().with_checkpoint_tokens(tokens)?;
255        Ok(())
256    }
257
258    pub(crate) fn from_sources(
259        token_spans: Vec<TokenSpanWork>,
260        committed_pages: Vec<CommittedPageWork>,
261    ) -> Result<Self, VNextError> {
262        if token_spans.is_empty() {
263            return Err(invalid_plan("resource work requires token evidence"));
264        }
265        let immediate_sequences = u32::try_from(token_spans.len())
266            .map_err(|_| invalid_plan("resource work sequence count exceeds u32"))?;
267        let immediate_tokens = token_spans.iter().try_fold(0_u64, |total, span| {
268            total
269                .checked_add(span.immediate_tokens())
270                .ok_or_else(|| invalid_plan("resource work immediate tokens overflow u64"))
271        })?;
272        let fit_tokens = token_spans.iter().try_fold(0_u64, |total, span| {
273            total
274                .checked_add(span.fit_input_tokens())
275                .ok_or_else(|| invalid_plan("resource work fit-input tokens overflow u64"))
276        })?;
277        let pages = committed_pages.iter().try_fold(0_u64, |total, page_work| {
278            total
279                .checked_add(page_work.pages())
280                .ok_or_else(|| invalid_plan("resource work pages overflow u64"))
281        })?;
282        #[derive(Serialize)]
283        struct FingerprintInput<'a> {
284            domain: &'static str,
285            token_spans: &'a [TokenSpanWork],
286            committed_pages: &'a [CommittedPageWork],
287        }
288        let bytes = serde_json::to_vec(&FingerprintInput {
289            domain: "ferrum.runtime-vnext.resource-work-shape.v1",
290            token_spans: &token_spans,
291            committed_pages: &committed_pages,
292        })
293        .map_err(|error| invalid_plan(format!("resource work encode failed: {error}")))?;
294        Ok(Self {
295            token_spans,
296            committed_pages,
297            immediate_sequences,
298            immediate_tokens,
299            immediate_pages: pages,
300            fit_sequences: immediate_sequences,
301            fit_tokens,
302            fit_pages: pages,
303            fingerprint: format!("{:x}", Sha256::digest(bytes)),
304        })
305    }
306
307    pub const fn immediate_sequences(&self) -> u32 {
308        self.immediate_sequences
309    }
310
311    pub const fn immediate_tokens(&self) -> u64 {
312        self.immediate_tokens
313    }
314
315    pub const fn immediate_pages(&self) -> u64 {
316        self.immediate_pages
317    }
318
319    pub const fn fit_sequences(&self) -> u32 {
320        self.fit_sequences
321    }
322
323    pub const fn fit_tokens(&self) -> u64 {
324        self.fit_tokens
325    }
326
327    pub const fn fit_pages(&self) -> u64 {
328        self.fit_pages
329    }
330
331    pub fn fingerprint(&self) -> &str {
332        &self.fingerprint
333    }
334
335    pub(crate) const fn immediate_shape(&self) -> DynamicResourceShape {
336        DynamicResourceShape::from_validated(
337            self.immediate_sequences,
338            self.immediate_tokens,
339            self.immediate_pages,
340        )
341    }
342
343    pub(crate) const fn fit_shape(&self) -> DynamicResourceShape {
344        DynamicResourceShape::from_validated(self.fit_sequences, self.fit_tokens, self.fit_pages)
345    }
346}
347
348#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
349#[serde(deny_unknown_fields)]
350pub struct DynamicResourceShapeBucket {
351    pub(super) maximum_sequences: u32,
352    pub(super) maximum_tokens: u64,
353    pub(super) maximum_pages: u64,
354    pub(super) bytes: u64,
355}
356
357#[derive(Deserialize)]
358#[serde(deny_unknown_fields)]
359pub(super) struct DynamicResourceShapeBucketWire {
360    pub(super) maximum_sequences: u32,
361    pub(super) maximum_tokens: u64,
362    pub(super) maximum_pages: u64,
363    pub(super) bytes: u64,
364}
365
366impl DynamicResourceShapeBucket {
367    pub fn new(
368        maximum_sequences: u32,
369        maximum_tokens: u64,
370        maximum_pages: u64,
371        bytes: u64,
372    ) -> Result<Self, VNextError> {
373        if maximum_sequences == 0 || maximum_tokens == 0 || maximum_pages == 0 || bytes == 0 {
374            return Err(invalid_plan(
375                "workspace shape bucket bounds and bytes must be non-zero",
376            ));
377        }
378        Ok(Self {
379            maximum_sequences,
380            maximum_tokens,
381            maximum_pages,
382            bytes,
383        })
384    }
385
386    pub(super) fn covers(&self, shape: DynamicResourceShape) -> bool {
387        shape.sequences <= self.maximum_sequences
388            && shape.tokens <= self.maximum_tokens
389            && shape.pages <= self.maximum_pages
390    }
391
392    pub const fn maximum_sequences(&self) -> u32 {
393        self.maximum_sequences
394    }
395
396    pub const fn maximum_tokens(&self) -> u64 {
397        self.maximum_tokens
398    }
399
400    pub const fn maximum_pages(&self) -> u64 {
401        self.maximum_pages
402    }
403
404    pub const fn bytes(&self) -> u64 {
405        self.bytes
406    }
407}
408
409impl<'de> Deserialize<'de> for DynamicResourceShapeBucket {
410    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
411    where
412        D: Deserializer<'de>,
413    {
414        let wire = DynamicResourceShapeBucketWire::deserialize(deserializer)?;
415        Self::new(
416            wire.maximum_sequences,
417            wire.maximum_tokens,
418            wire.maximum_pages,
419            wire.bytes,
420        )
421        .map_err(serde::de::Error::custom)
422    }
423}
424
425/// Core-bounded resource demand stored in an immutable memory plan. Provider
426/// estimators use `ProviderWorkspaceSizeFormula`; core adds runtime-policy
427/// bounds only after provider selection.
428#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
429#[serde(rename_all = "snake_case")]
430pub enum DynamicResourceDemand {
431    Fixed {
432        bytes: u64,
433    },
434    ActualSequences {
435        bytes_per_sequence: u64,
436        maximum_sequences: u32,
437    },
438    Tokens {
439        bytes_per_token: u64,
440        maximum_tokens: u64,
441    },
442    Affine {
443        fixed_bytes: u64,
444        bytes_per_sequence: u64,
445        maximum_sequences: u32,
446        bytes_per_token: u64,
447        maximum_tokens: u64,
448    },
449    Pages {
450        bytes_per_page: u64,
451        maximum_pages: u64,
452    },
453    BoundedShapeBuckets {
454        buckets: Vec<DynamicResourceShapeBucket>,
455    },
456}
457
458#[derive(Deserialize)]
459#[serde(rename_all = "snake_case", deny_unknown_fields)]
460pub(super) enum DynamicResourceDemandWire {
461    Fixed {
462        bytes: u64,
463    },
464    ActualSequences {
465        bytes_per_sequence: u64,
466        maximum_sequences: u32,
467    },
468    Tokens {
469        bytes_per_token: u64,
470        maximum_tokens: u64,
471    },
472    Affine {
473        fixed_bytes: u64,
474        bytes_per_sequence: u64,
475        maximum_sequences: u32,
476        bytes_per_token: u64,
477        maximum_tokens: u64,
478    },
479    Pages {
480        bytes_per_page: u64,
481        maximum_pages: u64,
482    },
483    BoundedShapeBuckets {
484        buckets: Vec<DynamicResourceShapeBucket>,
485    },
486}
487
488impl DynamicResourceDemand {
489    pub fn fixed(bytes: u64) -> Result<Self, VNextError> {
490        Self::validated(Self::Fixed { bytes })
491    }
492
493    pub fn actual_sequences(
494        bytes_per_sequence: u64,
495        maximum_sequences: u32,
496    ) -> Result<Self, VNextError> {
497        Self::validated(Self::ActualSequences {
498            bytes_per_sequence,
499            maximum_sequences,
500        })
501    }
502
503    pub fn tokens(bytes_per_token: u64, maximum_tokens: u64) -> Result<Self, VNextError> {
504        Self::validated(Self::Tokens {
505            bytes_per_token,
506            maximum_tokens,
507        })
508    }
509
510    pub fn affine(
511        fixed_bytes: u64,
512        bytes_per_sequence: u64,
513        maximum_sequences: u32,
514        bytes_per_token: u64,
515        maximum_tokens: u64,
516    ) -> Result<Self, VNextError> {
517        Self::validated(Self::Affine {
518            fixed_bytes,
519            bytes_per_sequence,
520            maximum_sequences,
521            bytes_per_token,
522            maximum_tokens,
523        })
524    }
525
526    pub fn pages(bytes_per_page: u64, maximum_pages: u64) -> Result<Self, VNextError> {
527        Self::validated(Self::Pages {
528            bytes_per_page,
529            maximum_pages,
530        })
531    }
532
533    pub fn bounded_shape_buckets(
534        buckets: Vec<DynamicResourceShapeBucket>,
535    ) -> Result<Self, VNextError> {
536        Self::validated(Self::BoundedShapeBuckets { buckets })
537    }
538
539    pub(super) fn validated(demand: Self) -> Result<Self, VNextError> {
540        demand.validate()?;
541        Ok(demand)
542    }
543
544    pub(super) fn validate(&self) -> Result<(), VNextError> {
545        let valid = match self {
546            Self::Fixed { bytes } => *bytes > 0,
547            Self::ActualSequences {
548                bytes_per_sequence,
549                maximum_sequences,
550            } => {
551                *bytes_per_sequence > 0
552                    && *maximum_sequences > 0
553                    && bytes_per_sequence
554                        .checked_mul(u64::from(*maximum_sequences))
555                        .is_some()
556            }
557            Self::Tokens {
558                bytes_per_token,
559                maximum_tokens,
560            } => {
561                *bytes_per_token > 0
562                    && *maximum_tokens > 0
563                    && bytes_per_token.checked_mul(*maximum_tokens).is_some()
564            }
565            Self::Affine {
566                fixed_bytes,
567                bytes_per_sequence,
568                maximum_sequences,
569                bytes_per_token,
570                maximum_tokens,
571            } => {
572                (*bytes_per_sequence > 0 || *bytes_per_token > 0)
573                    && *maximum_sequences > 0
574                    && *maximum_tokens > 0
575                    && bytes_per_sequence
576                        .checked_mul(u64::from(*maximum_sequences))
577                        .and_then(|sequence_bytes| fixed_bytes.checked_add(sequence_bytes))
578                        .and_then(|bytes| {
579                            bytes_per_token
580                                .checked_mul(*maximum_tokens)
581                                .and_then(|token_bytes| bytes.checked_add(token_bytes))
582                        })
583                        .is_some()
584            }
585            Self::Pages {
586                bytes_per_page,
587                maximum_pages,
588            } => {
589                *bytes_per_page > 0
590                    && *maximum_pages > 0
591                    && bytes_per_page.checked_mul(*maximum_pages).is_some()
592            }
593            Self::BoundedShapeBuckets { buckets } => {
594                !buckets.is_empty()
595                    && buckets.len() <= MAX_PROVIDER_WORKSPACE_SHAPE_BUCKETS
596                    && buckets.windows(2).all(|pair| {
597                        let previous = &pair[0];
598                        let next = &pair[1];
599                        next.maximum_sequences >= previous.maximum_sequences
600                            && next.maximum_tokens >= previous.maximum_tokens
601                            && next.maximum_pages >= previous.maximum_pages
602                            && (next.maximum_sequences > previous.maximum_sequences
603                                || next.maximum_tokens > previous.maximum_tokens
604                                || next.maximum_pages > previous.maximum_pages)
605                            && next.bytes >= previous.bytes
606                    })
607            }
608        };
609        if !valid {
610            return Err(invalid_plan(
611                "dynamic resource formula is zero, overflowing, or non-canonical",
612            ));
613        }
614        Ok(())
615    }
616
617    pub fn evaluate_bytes(&self, work: &ResourceWorkShape) -> Result<u64, VNextError> {
618        self.evaluate_shape_bytes(work.immediate_shape())
619    }
620
621    pub fn evaluate_fit_bytes(&self, work: &ResourceWorkShape) -> Result<u64, VNextError> {
622        self.evaluate_shape_bytes(work.fit_shape())
623    }
624
625    pub(crate) fn evaluate_shape_bytes(
626        &self,
627        shape: DynamicResourceShape,
628    ) -> Result<u64, VNextError> {
629        self.validate()?;
630        let bytes = match self {
631            Self::Fixed { bytes } => *bytes,
632            Self::ActualSequences {
633                bytes_per_sequence,
634                maximum_sequences,
635            } if shape.sequences <= *maximum_sequences => bytes_per_sequence
636                .checked_mul(u64::from(shape.sequences))
637                .ok_or_else(|| invalid_plan("sequence-scaled resource request overflows u64"))?,
638            Self::Tokens {
639                bytes_per_token,
640                maximum_tokens,
641            } if shape.tokens <= *maximum_tokens => bytes_per_token
642                .checked_mul(shape.tokens)
643                .ok_or_else(|| invalid_plan("token-scaled resource request overflows u64"))?,
644            Self::Affine {
645                fixed_bytes,
646                bytes_per_sequence,
647                maximum_sequences,
648                bytes_per_token,
649                maximum_tokens,
650            } if shape.sequences <= *maximum_sequences && shape.tokens <= *maximum_tokens => {
651                fixed_bytes
652                    .checked_add(
653                        bytes_per_sequence
654                            .checked_mul(u64::from(shape.sequences))
655                            .ok_or_else(|| {
656                                invalid_plan("affine sequence resource request overflows u64")
657                            })?,
658                    )
659                    .and_then(|bytes| {
660                        bytes_per_token
661                            .checked_mul(shape.tokens)
662                            .and_then(|token_bytes| bytes.checked_add(token_bytes))
663                    })
664                    .ok_or_else(|| invalid_plan("affine token resource request overflows u64"))?
665            }
666            Self::Pages {
667                bytes_per_page,
668                maximum_pages,
669            } if shape.pages <= *maximum_pages => bytes_per_page
670                .checked_mul(shape.pages)
671                .ok_or_else(|| invalid_plan("page-scaled resource request overflows u64"))?,
672            Self::BoundedShapeBuckets { buckets } => buckets
673                .iter()
674                .find(|bucket| bucket.covers(shape))
675                .map(DynamicResourceShapeBucket::bytes)
676                .ok_or_else(|| invalid_plan("actual invocation shape exceeds workspace buckets"))?,
677            _ => {
678                return Err(invalid_plan(
679                    "actual invocation shape exceeds its bounded resource formula",
680                ))
681            }
682        };
683        if bytes == 0 {
684            return Err(invalid_plan(
685                "dynamic resource request evaluates to zero bytes",
686            ));
687        }
688        Ok(bytes)
689    }
690
691    pub(crate) fn minimum_shape(&self) -> DynamicResourceShape {
692        DynamicResourceShape {
693            sequences: 1,
694            tokens: 1,
695            pages: 1,
696        }
697    }
698
699    pub(crate) fn theoretical_maximum_shape(&self) -> DynamicResourceShape {
700        match self {
701            Self::Fixed { .. } => self.minimum_shape(),
702            Self::ActualSequences {
703                maximum_sequences, ..
704            } => DynamicResourceShape {
705                sequences: *maximum_sequences,
706                tokens: 1,
707                pages: 1,
708            },
709            Self::Tokens { maximum_tokens, .. } => DynamicResourceShape {
710                sequences: 1,
711                tokens: *maximum_tokens,
712                pages: 1,
713            },
714            Self::Affine {
715                maximum_sequences,
716                maximum_tokens,
717                ..
718            } => DynamicResourceShape {
719                sequences: *maximum_sequences,
720                tokens: *maximum_tokens,
721                pages: 1,
722            },
723            Self::Pages { maximum_pages, .. } => DynamicResourceShape {
724                sequences: 1,
725                tokens: 1,
726                pages: *maximum_pages,
727            },
728            Self::BoundedShapeBuckets { buckets } => {
729                let bucket = buckets
730                    .last()
731                    .expect("validated bounded formula has at least one bucket");
732                DynamicResourceShape {
733                    sequences: bucket.maximum_sequences,
734                    tokens: bucket.maximum_tokens,
735                    pages: bucket.maximum_pages,
736                }
737            }
738        }
739    }
740
741    pub(super) fn is_fixed(&self) -> bool {
742        matches!(self, Self::Fixed { .. })
743    }
744
745    pub(super) fn is_valid_for_sequence_scope(&self) -> bool {
746        match self {
747            Self::Fixed { .. } | Self::Tokens { .. } | Self::Pages { .. } => true,
748            Self::Affine {
749                bytes_per_sequence, ..
750            } => *bytes_per_sequence == 0,
751            Self::BoundedShapeBuckets { buckets } => {
752                buckets.iter().all(|bucket| bucket.maximum_sequences == 1)
753            }
754            Self::ActualSequences { .. } => false,
755        }
756    }
757}
758
759impl<'de> Deserialize<'de> for DynamicResourceDemand {
760    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
761    where
762        D: Deserializer<'de>,
763    {
764        let demand = match DynamicResourceDemandWire::deserialize(deserializer)? {
765            DynamicResourceDemandWire::Fixed { bytes } => Self::Fixed { bytes },
766            DynamicResourceDemandWire::ActualSequences {
767                bytes_per_sequence,
768                maximum_sequences,
769            } => Self::ActualSequences {
770                bytes_per_sequence,
771                maximum_sequences,
772            },
773            DynamicResourceDemandWire::Tokens {
774                bytes_per_token,
775                maximum_tokens,
776            } => Self::Tokens {
777                bytes_per_token,
778                maximum_tokens,
779            },
780            DynamicResourceDemandWire::Affine {
781                fixed_bytes,
782                bytes_per_sequence,
783                maximum_sequences,
784                bytes_per_token,
785                maximum_tokens,
786            } => Self::Affine {
787                fixed_bytes,
788                bytes_per_sequence,
789                maximum_sequences,
790                bytes_per_token,
791                maximum_tokens,
792            },
793            DynamicResourceDemandWire::Pages {
794                bytes_per_page,
795                maximum_pages,
796            } => Self::Pages {
797                bytes_per_page,
798                maximum_pages,
799            },
800            DynamicResourceDemandWire::BoundedShapeBuckets { buckets } => {
801                Self::BoundedShapeBuckets { buckets }
802            }
803        };
804        Self::validated(demand).map_err(serde::de::Error::custom)
805    }
806}