Skip to main content

tenferro_ops/ad/
context.rs

1//! AD context for guard-based shape resolution and value metadata queries.
2//!
3//! During AD graph construction, linalg rules such as SVD, QR, and LU need
4//! concrete matrix dimensions to choose between structurally different
5//! subgraphs. `ShapeGuardContext` records those dimension comparisons as guards
6//! so cached AD graphs can later be invalidated when the observed shape
7//! relationship changes.
8
9use std::cmp::Ordering;
10use std::collections::HashMap;
11use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
12#[cfg(feature = "autodiff")]
13use std::sync::Arc;
14use std::sync::{Mutex, OnceLock};
15
16use computegraph::graph::Graph;
17use computegraph::types::{ValueKey, ValueRef};
18use tenferro_tensor::DType;
19
20#[cfg(feature = "autodiff")]
21use crate::ad::{ADRuleError, ADRuleKind, ExtensionAdDispatcher};
22use crate::dim_expr::{DimExpr, DimExprEvalError};
23use crate::shape_extent::ShapeExtent;
24use crate::std_tensor_op::StdTensorOp;
25use crate::sym_dim::SymDim;
26
27type MetadataMap = HashMap<ValueKey<StdTensorOp>, TensorMeta>;
28
29type GlobalMetadataMap = HashMap<ValueKey<StdTensorOp>, GlobalMetadataEntry>;
30
31#[derive(Clone, Debug)]
32struct GlobalMetadataEntry {
33    stack: Vec<GlobalMetadataRegistration>,
34}
35
36#[derive(Clone, Debug)]
37struct GlobalMetadataRegistration {
38    token: u64,
39    meta: TensorMeta,
40}
41
42#[derive(Clone, Debug)]
43struct ScopedGlobalMetadataRegistration {
44    key: ValueKey<StdTensorOp>,
45    token: u64,
46}
47
48/// Error returned when the process-global AD metadata registry is unavailable.
49#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
50pub enum MetadataRegistryError {
51    /// A previous panic poisoned the global metadata mutex.
52    #[error("AD global metadata registry lock poisoned")]
53    LockPoisoned,
54}
55
56/// Error returned when shape-guard metadata cannot be resolved.
57///
58/// # Examples
59///
60/// ```
61/// use tenferro_ops::ShapeGuardError;
62///
63/// let error = ShapeGuardError::LocalWithoutAttachedGraph { local_id: 0 };
64/// assert!(error.to_string().contains("attached graph"));
65/// ```
66#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
67pub enum ShapeGuardError {
68    /// A local graph value was queried before a graph was attached.
69    #[error("cannot resolve local value {local_id} without an attached graph")]
70    LocalWithoutAttachedGraph {
71        /// Graph-local value id.
72        local_id: usize,
73    },
74    /// A local graph value id is outside the attached graph's value table.
75    #[error("local value {local_id} is out of bounds for the attached graph")]
76    LocalOutOfBounds {
77        /// Graph-local value id.
78        local_id: usize,
79    },
80    /// No metadata was registered for the resolved value key.
81    #[error("missing TensorMeta for {key:?}")]
82    MissingMetadata {
83        /// Resolved value key.
84        key: ValueKey<StdTensorOp>,
85    },
86    /// Metadata exists, but at least one axis is only bounded or unknown.
87    #[error("TensorMeta for {key:?} does not have an exact shape; query extents instead")]
88    NonExactShape {
89        /// Resolved value key.
90        key: ValueKey<StdTensorOp>,
91    },
92}
93
94/// Result type used by shape-guard metadata queries.
95///
96/// The error side preserves a [`ShapeGuardFailure`] wrapper so an AD callback
97/// can retain the original [`ShapeGuardError`] even when a foreign callback
98/// protocol accepts only a rendered message.
99///
100/// # Examples
101///
102/// ```
103/// use tenferro_ops::ShapeGuardResult;
104///
105/// let result: ShapeGuardResult<()> = Ok(());
106/// assert!(result.is_ok());
107/// ```
108pub type ShapeGuardResult<T> = Result<T, ShapeGuardFailure>;
109
110#[cfg(feature = "autodiff")]
111impl From<ShapeGuardFailure> for ADRuleError {
112    fn from(err: ShapeGuardFailure) -> Self {
113        err.record_for_ad_boundary();
114        ADRuleError::invalid_input(
115            "tenferro.shape_guard",
116            ADRuleKind::Jvp,
117            err.typed_source().to_string(),
118        )
119    }
120}
121
122/// Error returned by a shape-guard metadata query.
123///
124/// The public [`ShapeGuardError`] remains the typed source. The private side
125/// channel is shared with the owning [`ShapeGuardContext`] so an external
126/// message-only AD callback can report the same typed source at the runtime
127/// boundary without changing the callback protocol.
128///
129/// # Examples
130///
131/// ```
132/// use computegraph::types::{ValueKey, ValueRef};
133/// use tenferro_ops::input_key::TensorInputKey;
134/// use tenferro_ops::std_tensor_op::StdTensorOp;
135/// use tenferro_ops::{ShapeGuardContext, ShapeGuardError};
136///
137/// let key = ValueKey::<StdTensorOp>::Input(TensorInputKey::User { id: 8 });
138/// let value = ValueRef::External(key);
139/// let mut ctx = ShapeGuardContext::default();
140/// let failure = ctx.shape_of(&value).unwrap_err();
141/// assert!(matches!(
142///     failure.typed_source(),
143///     ShapeGuardError::MissingMetadata { .. }
144/// ));
145/// ```
146#[derive(Clone, Debug)]
147pub struct ShapeGuardFailure {
148    source: ShapeGuardError,
149    #[cfg(feature = "autodiff")]
150    deferred: Arc<Mutex<Option<ShapeGuardError>>>,
151}
152
153impl ShapeGuardFailure {
154    #[cfg(feature = "autodiff")]
155    fn new(source: ShapeGuardError, deferred: Arc<Mutex<Option<ShapeGuardError>>>) -> Self {
156        Self { source, deferred }
157    }
158
159    #[cfg(not(feature = "autodiff"))]
160    fn new(source: ShapeGuardError) -> Self {
161        Self { source }
162    }
163
164    /// Return the original typed shape-guard failure.
165    pub fn typed_source(&self) -> &ShapeGuardError {
166        &self.source
167    }
168
169    /// Consume this boundary error and return its original typed failure.
170    pub fn into_typed_source(self) -> ShapeGuardError {
171        self.source
172    }
173
174    #[cfg(feature = "autodiff")]
175    fn record_for_ad_boundary(&self) {
176        if let Ok(mut deferred) = self.deferred.lock() {
177            if deferred.is_none() {
178                *deferred = Some(self.source.clone());
179            }
180        }
181    }
182}
183
184impl std::fmt::Display for ShapeGuardFailure {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        self.source.fmt(f)
187    }
188}
189
190impl std::error::Error for ShapeGuardFailure {
191    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
192        Some(&self.source)
193    }
194}
195
196impl PartialEq for ShapeGuardFailure {
197    fn eq(&self, other: &Self) -> bool {
198        self.source == other.source
199    }
200}
201
202impl Eq for ShapeGuardFailure {}
203
204/// Global metadata registry.
205///
206/// Stored as a tokenized stack per value key: duplicate scoped registrations
207/// shadow older metadata while they are live, and dropping scopes in any order
208/// removes only the matching token. `ShapeGuardContext::metadata_of` reaches into
209/// the registry lazily via [`lookup_global_metadata`] and caches the result into
210/// the context's local map.
211///
212/// Earlier designs either cloned the whole map up-front into each AD
213/// `ShapeGuardContext` or kept the map in an `Arc` and cloned on every write.
214/// Both variants were quadratic across the monotonically growing registry and
215/// dominated oracle_replay runtime.
216static GLOBAL_METADATA: OnceLock<Mutex<GlobalMetadataMap>> = OnceLock::new();
217static NEXT_GLOBAL_METADATA_TOKEN: AtomicU64 = AtomicU64::new(0);
218
219fn global_metadata_registry() -> &'static Mutex<GlobalMetadataMap> {
220    GLOBAL_METADATA.get_or_init(|| Mutex::new(HashMap::new()))
221}
222
223/// Lifetime token for graph-scoped global metadata.
224///
225/// Dropping the last frontend owner of a traced graph drops this scope and
226/// releases the metadata keys that were registered for that graph graph.
227#[doc(hidden)]
228#[derive(Debug)]
229pub struct GlobalMetadataScope {
230    registrations: Vec<ScopedGlobalMetadataRegistration>,
231}
232
233impl Drop for GlobalMetadataScope {
234    fn drop(&mut self) {
235        release_scoped_global_metadata(&self.registrations);
236    }
237}
238
239/// Per-value tensor metadata used by AD rules.
240///
241/// Shape information is stored as per-axis [`ShapeExtent`] values. Callers must
242/// explicitly choose whether they need an exact shape or only a known bound.
243///
244/// # Examples
245///
246/// ```
247/// use tenferro_ops::{SymDim, TensorMeta};
248/// use tenferro_tensor::DType;
249///
250/// let meta = TensorMeta::exact(DType::F64, vec![SymDim::from(2usize), SymDim::from(3usize)]);
251/// assert_eq!(meta.rank(), 2);
252/// ```
253#[derive(Clone, Debug, PartialEq, Eq)]
254pub struct TensorMeta {
255    /// Element dtype of the tensor value.
256    pub dtype: DType,
257    /// Per-axis shape guarantees.
258    pub extents: Vec<ShapeExtent<SymDim>>,
259}
260
261impl TensorMeta {
262    /// Construct metadata whose every axis is exact.
263    ///
264    /// # Examples
265    ///
266    /// ```
267    /// use tenferro_ops::{SymDim, TensorMeta};
268    /// use tenferro_tensor::DType;
269    ///
270    /// let meta = TensorMeta::exact(DType::F64, vec![SymDim::from(4usize)]);
271    /// assert_eq!(meta.exact_shape(), Some(vec![SymDim::from(4usize)]));
272    /// ```
273    pub fn exact(dtype: DType, shape: Vec<SymDim>) -> Self {
274        let extents = shape.iter().cloned().map(ShapeExtent::exact).collect();
275        Self { dtype, extents }
276    }
277
278    /// Construct metadata from per-axis extents.
279    ///
280    /// # Examples
281    ///
282    /// ```
283    /// use tenferro_ops::{ShapeExtent, SymDim, TensorMeta};
284    /// use tenferro_tensor::DType;
285    ///
286    /// let meta = TensorMeta::with_extents(
287    ///     DType::F64,
288    ///     vec![ShapeExtent::upper_bound(SymDim::from(8usize))],
289    /// );
290    /// assert_eq!(meta.exact_shape(), None);
291    /// ```
292    pub fn with_extents(dtype: DType, extents: Vec<ShapeExtent<SymDim>>) -> Self {
293        Self { dtype, extents }
294    }
295
296    /// Return the tensor rank known by this metadata record.
297    pub fn rank(&self) -> usize {
298        self.extents.len()
299    }
300
301    /// Return the per-axis shape guarantees.
302    ///
303    /// # Examples
304    ///
305    /// ```
306    /// use tenferro_ops::{SymDim, TensorMeta};
307    /// use tenferro_tensor::DType;
308    ///
309    /// let meta = TensorMeta::exact(DType::F64, vec![SymDim::from(4usize)]);
310    /// assert_eq!(meta.extents().len(), 1);
311    /// ```
312    pub fn extents(&self) -> &[ShapeExtent<SymDim>] {
313        &self.extents
314    }
315
316    /// Return the shape only when every axis is exact.
317    ///
318    /// # Examples
319    ///
320    /// ```
321    /// use tenferro_ops::{ShapeExtent, SymDim, TensorMeta};
322    /// use tenferro_tensor::DType;
323    ///
324    /// let meta = TensorMeta::with_extents(
325    ///     DType::F64,
326    ///     vec![ShapeExtent::upper_bound(SymDim::from(8usize))],
327    /// );
328    /// assert_eq!(meta.exact_shape(), None);
329    /// ```
330    pub fn exact_shape(&self) -> Option<Vec<SymDim>> {
331        self.extents
332            .iter()
333            .map(|extent| extent.as_exact().cloned())
334            .collect()
335    }
336
337    /// Return one known bound per axis when every axis has a bound.
338    ///
339    /// This is intentionally separate from [`TensorMeta::exact_shape`]: a bound
340    /// is not proof of the runtime size.
341    pub fn bound_shape(&self) -> Option<Vec<SymDim>> {
342        self.extents
343            .iter()
344            .map(|extent| extent.bound_expr().cloned())
345            .collect()
346    }
347}
348
349/// A recorded dimension comparison made during AD graph construction.
350///
351/// # Examples
352///
353/// ```
354/// use std::cmp::Ordering;
355/// use tenferro_ops::ShapeGuard;
356///
357/// let guard = ShapeGuard {
358///     dim_a: 5,
359///     dim_b: 3,
360///     ordering: Ordering::Greater,
361/// };
362///
363/// assert_eq!(guard.ordering, Ordering::Greater);
364/// ```
365#[derive(Clone, Debug, PartialEq, Eq)]
366pub struct ShapeGuard {
367    /// First dimension value, such as `m`.
368    pub dim_a: usize,
369    /// Second dimension value, such as `n`.
370    pub dim_b: usize,
371    /// The observed ordering `dim_a.cmp(&dim_b)`.
372    pub ordering: Ordering,
373}
374
375/// AD context providing dimension resolution, guard recording, and value metadata.
376///
377/// # Examples
378///
379/// ```
380/// use tenferro_ops::ShapeGuardContext;
381///
382/// let ctx = ShapeGuardContext::default();
383/// assert!(ctx.guards().is_empty());
384/// ```
385#[derive(Clone, Debug, Default)]
386pub struct ShapeGuardContext {
387    guards: Vec<ShapeGuard>,
388    metadata: MetadataMap,
389    shape_sources: HashMap<u64, ValueKey<StdTensorOp>>,
390    use_global_registry: bool,
391    local_keys: Option<Vec<ValueKey<StdTensorOp>>>,
392    #[cfg(feature = "autodiff")]
393    deferred_shape_error: Arc<Mutex<Option<ShapeGuardError>>>,
394    #[cfg(feature = "autodiff")]
395    extension_ad_dispatcher: Option<Arc<dyn ExtensionAdDispatcher>>,
396    #[cfg(feature = "autodiff")]
397    active_value_keys: Option<std::sync::Arc<std::collections::HashSet<ValueKey<StdTensorOp>>>>,
398    #[cfg(feature = "autodiff")]
399    transpose_primal_outputs: Option<Vec<ValueKey<StdTensorOp>>>,
400    #[cfg(feature = "autodiff")]
401    transpose_primal_outputs_used: bool,
402}
403
404impl ShapeGuardContext {
405    /// Create a context backed by the global metadata registry.
406    ///
407    /// Instead of cloning the entire global registry up-front (which used
408    /// to be O(N) per AD pass and quadratic across oracle_replay), the
409    /// context keeps a flag and lazily fetches entries from the shared
410    /// [`lookup_global_metadata`] on first miss, caching into its local
411    /// `metadata` map for subsequent reads within the same pass.
412    ///
413    /// # Examples
414    ///
415    /// ```
416    /// let ctx = tenferro_ops::ShapeGuardContext::with_global_metadata();
417    /// assert!(ctx.guards().is_empty());
418    /// ```
419    pub fn with_global_metadata() -> Self {
420        Self {
421            use_global_registry: true,
422            ..Self::default()
423        }
424    }
425
426    #[doc(hidden)]
427    /// Keep global-registry lookup enabled after a pass boundary.
428    ///
429    /// This is intentionally a no-op for cached entries: global metadata is
430    /// already read lazily on cache misses, and clearing the local cache would
431    /// also discard metadata inserted directly into this context.
432    pub fn refresh_global_metadata(&mut self) {
433        self.use_global_registry = true;
434    }
435
436    #[doc(hidden)]
437    pub fn insert_shape_source(&mut self, tensor_id: u64, key: ValueKey<StdTensorOp>) {
438        self.shape_sources.entry(tensor_id).or_insert(key);
439    }
440
441    #[doc(hidden)]
442    pub fn shape_source(&self, tensor_id: u64) -> Option<&ValueKey<StdTensorOp>> {
443        self.shape_sources.get(&tensor_id)
444    }
445
446    #[doc(hidden)]
447    #[cfg(feature = "autodiff")]
448    pub fn with_extension_ad_dispatcher(
449        mut self,
450        dispatcher: Arc<dyn ExtensionAdDispatcher>,
451    ) -> Self {
452        self.extension_ad_dispatcher = Some(dispatcher);
453        self
454    }
455
456    #[doc(hidden)]
457    #[cfg(feature = "autodiff")]
458    pub(crate) fn extension_ad_dispatcher(&self) -> Option<Arc<dyn ExtensionAdDispatcher>> {
459        self.extension_ad_dispatcher.as_ref().map(Arc::clone)
460    }
461
462    #[cfg(feature = "autodiff")]
463    pub fn with_linearize_active_values(
464        mut self,
465        keys: std::sync::Arc<std::collections::HashSet<ValueKey<StdTensorOp>>>,
466    ) -> Self {
467        self.active_value_keys = Some(keys);
468        self
469    }
470
471    /// Whether a primal value lies on a path from the current linearize targets.
472    ///
473    /// When no active set was attached, every value is treated as active so
474    /// existing callers keep the conservative full JVP graphs.
475    #[cfg(feature = "autodiff")]
476    pub fn is_value_active_in_linearize(&self, key: &ValueKey<StdTensorOp>) -> bool {
477        self.active_value_keys
478            .as_ref()
479            .is_none_or(|set| set.contains(key))
480    }
481
482    /// Primal output keys for the operation currently being transposed.
483    ///
484    /// Primary-mode extension transpose rules such as `Eigh` use these to reuse
485    /// forward eigenvectors instead of recomputing a decomposition.
486    #[cfg(feature = "autodiff")]
487    pub fn set_transpose_primal_outputs(&mut self, keys: Option<Vec<ValueKey<StdTensorOp>>>) {
488        self.transpose_primal_outputs = keys;
489        self.transpose_primal_outputs_used = false;
490    }
491
492    /// Return the current primal outputs and mark them as consumed by this rule.
493    #[cfg(feature = "autodiff")]
494    pub fn transpose_primal_outputs(&mut self) -> Option<&[ValueKey<StdTensorOp>]> {
495        if self.transpose_primal_outputs.is_some() {
496            self.transpose_primal_outputs_used = true;
497        }
498        self.transpose_primal_outputs.as_deref()
499    }
500
501    #[cfg(feature = "autodiff")]
502    pub fn transpose_primal_outputs_were_used(&self) -> bool {
503        self.transpose_primal_outputs_used
504    }
505
506    /// Returns the guards recorded so far.
507    ///
508    /// # Examples
509    ///
510    /// ```
511    /// use tenferro_ops::ShapeGuardContext;
512    ///
513    /// let ctx = ShapeGuardContext::default();
514    /// assert_eq!(ctx.guards(), &[]);
515    /// ```
516    pub fn guards(&self) -> &[ShapeGuard] {
517        &self.guards
518    }
519
520    /// Clears all recorded guards.
521    ///
522    /// # Examples
523    ///
524    /// ```
525    /// use tenferro_ops::ShapeGuardContext;
526    ///
527    /// let mut ctx = ShapeGuardContext::default();
528    /// ctx.clear_guards();
529    /// assert!(ctx.guards().is_empty());
530    /// ```
531    pub fn clear_guards(&mut self) {
532        self.guards.clear();
533    }
534
535    /// Take the first typed shape-guard failure recorded while crossing an AD
536    /// callback boundary.
537    ///
538    /// AD callbacks expose only a message-bearing error. AD
539    /// frontends call this after the callback returns and attach the typed
540    /// value to their public runtime error.
541    #[doc(hidden)]
542    #[cfg(feature = "autodiff")]
543    pub fn take_deferred_shape_error(&mut self) -> Option<ShapeGuardError> {
544        self.deferred_shape_error
545            .lock()
546            .ok()
547            .and_then(|mut error| error.take())
548    }
549
550    /// Return the shape metadata for a value reference.
551    ///
552    /// # Examples
553    ///
554    /// ```
555    /// use computegraph::types::{ValueKey, ValueRef};
556    /// use tenferro_ops::input_key::TensorInputKey;
557    /// use tenferro_ops::std_tensor_op::StdTensorOp;
558    /// use tenferro_ops::{ShapeGuardContext, SymDim, TensorMeta};
559    /// use tenferro_tensor::DType;
560    ///
561    /// let key = ValueKey::<StdTensorOp>::Input(TensorInputKey::User { id: 1 });
562    /// let value = ValueRef::External(key.clone());
563    /// let mut ctx = ShapeGuardContext::default();
564    /// ctx.insert_metadata(key, TensorMeta::exact(DType::F64, vec![SymDim::from(4usize)]));
565    ///
566    /// let shape = ctx.shape_of(&value).unwrap();
567    /// assert_eq!(shape, &[SymDim::from(4usize)]);
568    /// ```
569    ///
570    /// # Errors
571    ///
572    /// Returns [`ShapeGuardError`] when the value cannot be resolved, metadata
573    /// is missing, or the metadata does not describe an exact shape.
574    pub fn shape_of(&mut self, val: &ValueRef<StdTensorOp>) -> ShapeGuardResult<Vec<SymDim>> {
575        let key = self.resolve_key(val)?.clone();
576        self.ensure_metadata_loaded(&key);
577        let meta = self.metadata.get(&key).ok_or_else(|| {
578            self.shape_guard_failure(ShapeGuardError::MissingMetadata { key: key.clone() })
579        })?;
580        meta.exact_shape()
581            .ok_or_else(|| self.shape_guard_failure(ShapeGuardError::NonExactShape { key }))
582    }
583
584    /// Return the rank for a value reference without requiring exact extents.
585    ///
586    /// Use this when an AD rule only needs axis count or needs to build
587    /// runtime-shape references. Calling [`ShapeGuardContext::shape_of`] in those
588    /// cases would reject valid values such as `DynamicTruncate` outputs whose
589    /// runtime extent is known only as an upper bound.
590    ///
591    /// # Examples
592    ///
593    /// ```
594    /// use computegraph::types::{ValueKey, ValueRef};
595    /// use tenferro_ops::input_key::TensorInputKey;
596    /// use tenferro_ops::std_tensor_op::StdTensorOp;
597    /// use tenferro_ops::{ShapeExtent, ShapeGuardContext, SymDim, TensorMeta};
598    /// use tenferro_tensor::DType;
599    ///
600    /// let key = ValueKey::<StdTensorOp>::Input(TensorInputKey::User { id: 1 });
601    /// let value = ValueRef::External(key.clone());
602    /// let mut ctx = ShapeGuardContext::default();
603    /// ctx.insert_metadata(
604    ///     key,
605    ///     TensorMeta::with_extents(DType::F64, vec![ShapeExtent::upper_bound(SymDim::from(8usize))]),
606    /// );
607    ///
608    /// assert_eq!(ctx.rank_of(&value).unwrap(), 1);
609    /// ```
610    ///
611    /// # Errors
612    ///
613    /// Returns [`ShapeGuardError`] when the value cannot be resolved or its
614    /// metadata is unavailable.
615    pub fn rank_of(&mut self, val: &ValueRef<StdTensorOp>) -> ShapeGuardResult<usize> {
616        self.metadata_of(val).map(TensorMeta::rank)
617    }
618
619    /// Return per-axis shape guarantees for a value reference.
620    ///
621    /// # Examples
622    ///
623    /// ```
624    /// use computegraph::types::{ValueKey, ValueRef};
625    /// use tenferro_ops::input_key::TensorInputKey;
626    /// use tenferro_ops::std_tensor_op::StdTensorOp;
627    /// use tenferro_ops::{ShapeExtent, ShapeGuardContext, SymDim, TensorMeta};
628    /// use tenferro_tensor::DType;
629    ///
630    /// let key = ValueKey::<StdTensorOp>::Input(TensorInputKey::User { id: 1 });
631    /// let value = ValueRef::External(key.clone());
632    /// let mut ctx = ShapeGuardContext::default();
633    /// ctx.insert_metadata(
634    ///     key,
635    ///     TensorMeta::with_extents(DType::F64, vec![ShapeExtent::upper_bound(SymDim::from(8usize))]),
636    /// );
637    ///
638    /// let extents = ctx.extents_of(&value).unwrap();
639    /// assert_eq!(extents[0], ShapeExtent::upper_bound(SymDim::from(8usize)));
640    /// ```
641    ///
642    /// # Errors
643    ///
644    /// Returns [`ShapeGuardError`] when the value cannot be resolved or its
645    /// metadata is unavailable.
646    pub fn extents_of(
647        &mut self,
648        val: &ValueRef<StdTensorOp>,
649    ) -> ShapeGuardResult<&[ShapeExtent<SymDim>]> {
650        self.metadata_of(val).map(TensorMeta::extents)
651    }
652
653    /// Return the exact shape for a value reference, if all axes are exact.
654    ///
655    /// # Examples
656    ///
657    /// ```
658    /// use computegraph::types::{ValueKey, ValueRef};
659    /// use tenferro_ops::input_key::TensorInputKey;
660    /// use tenferro_ops::std_tensor_op::StdTensorOp;
661    /// use tenferro_ops::{ShapeExtent, ShapeGuardContext, SymDim, TensorMeta};
662    /// use tenferro_tensor::DType;
663    ///
664    /// let key = ValueKey::<StdTensorOp>::Input(TensorInputKey::User { id: 1 });
665    /// let value = ValueRef::External(key.clone());
666    /// let mut ctx = ShapeGuardContext::default();
667    /// ctx.insert_metadata(
668    ///     key,
669    ///     TensorMeta::with_extents(DType::F64, vec![ShapeExtent::upper_bound(SymDim::from(8usize))]),
670    /// );
671    ///
672    /// let maybe_shape = ctx.exact_shape_of(&value).unwrap();
673    /// assert_eq!(maybe_shape, None);
674    /// ```
675    ///
676    /// # Errors
677    ///
678    /// Returns [`ShapeGuardError`] when the value cannot be resolved or its
679    /// metadata is unavailable.
680    pub fn exact_shape_of(
681        &mut self,
682        val: &ValueRef<StdTensorOp>,
683    ) -> ShapeGuardResult<Option<Vec<SymDim>>> {
684        self.metadata_of(val).map(TensorMeta::exact_shape)
685    }
686
687    #[doc(hidden)]
688    pub fn shape_if_available(&mut self, val: &ValueRef<StdTensorOp>) -> Option<Vec<SymDim>> {
689        self.metadata_if_available(val)
690            .and_then(TensorMeta::exact_shape)
691    }
692
693    /// Return the dtype metadata for a value reference.
694    ///
695    /// # Examples
696    ///
697    /// ```
698    /// use computegraph::types::{ValueKey, ValueRef};
699    /// use tenferro_ops::input_key::TensorInputKey;
700    /// use tenferro_ops::std_tensor_op::StdTensorOp;
701    /// use tenferro_ops::{ShapeGuardContext, SymDim, TensorMeta};
702    /// use tenferro_tensor::DType;
703    ///
704    /// let key = ValueKey::<StdTensorOp>::Input(TensorInputKey::User { id: 1 });
705    /// let value = ValueRef::External(key.clone());
706    /// let mut ctx = ShapeGuardContext::default();
707    /// ctx.insert_metadata(key, TensorMeta::exact(DType::F64, vec![SymDim::from(4usize)]));
708    ///
709    /// let dtype = ctx.dtype_of(&value).unwrap();
710    /// assert_eq!(dtype, DType::F64);
711    /// ```
712    ///
713    /// # Errors
714    ///
715    /// Returns [`ShapeGuardError`] when the value cannot be resolved or its
716    /// metadata is unavailable.
717    pub fn dtype_of(&mut self, val: &ValueRef<StdTensorOp>) -> ShapeGuardResult<DType> {
718        self.metadata_of(val).map(|meta| meta.dtype)
719    }
720
721    /// Return the complete metadata record for a value reference.
722    ///
723    /// # Examples
724    ///
725    /// ```
726    /// use computegraph::types::{ValueKey, ValueRef};
727    /// use tenferro_ops::input_key::TensorInputKey;
728    /// use tenferro_ops::std_tensor_op::StdTensorOp;
729    /// use tenferro_ops::{ShapeGuardContext, SymDim, TensorMeta};
730    /// use tenferro_tensor::DType;
731    ///
732    /// let key = ValueKey::<StdTensorOp>::Input(TensorInputKey::User { id: 1 });
733    /// let value = ValueRef::External(key.clone());
734    /// let mut ctx = ShapeGuardContext::default();
735    /// ctx.insert_metadata(key, TensorMeta::exact(DType::F64, vec![SymDim::from(4usize)]));
736    ///
737    /// let meta = ctx.metadata_of(&value).unwrap();
738    /// assert_eq!(meta.dtype, DType::F64);
739    /// ```
740    ///
741    /// # Errors
742    ///
743    /// Returns [`ShapeGuardError`] when the value cannot be resolved or its
744    /// metadata is unavailable.
745    pub fn metadata_of(&mut self, val: &ValueRef<StdTensorOp>) -> ShapeGuardResult<&TensorMeta> {
746        let key = self.resolve_key(val)?.clone();
747        self.ensure_metadata_loaded(&key);
748        self.metadata
749            .get(&key)
750            .ok_or_else(|| self.shape_guard_failure(ShapeGuardError::MissingMetadata { key }))
751    }
752
753    #[doc(hidden)]
754    pub fn metadata_if_available(&mut self, val: &ValueRef<StdTensorOp>) -> Option<&TensorMeta> {
755        let key = self.resolve_key_if_available(val)?.clone();
756        self.ensure_metadata_loaded(&key);
757        self.metadata.get(&key)
758    }
759
760    #[doc(hidden)]
761    pub fn attach_graph(&mut self, graph: &Graph<StdTensorOp>) {
762        self.local_keys = Some(graph.values().iter().map(|node| node.key.clone()).collect());
763    }
764
765    #[doc(hidden)]
766    pub fn insert_metadata(&mut self, key: ValueKey<StdTensorOp>, meta: TensorMeta) {
767        self.metadata.insert(key, meta);
768    }
769
770    #[doc(hidden)]
771    pub fn extend_metadata<I>(&mut self, entries: I)
772    where
773        I: IntoIterator<Item = (ValueKey<StdTensorOp>, TensorMeta)>,
774    {
775        self.metadata.extend(entries);
776    }
777
778    fn resolve_key_if_available<'a>(
779        &'a self,
780        val: &'a ValueRef<StdTensorOp>,
781    ) -> Option<&'a ValueKey<StdTensorOp>> {
782        match val {
783            ValueRef::External(key) => Some(key),
784            ValueRef::Local(local_id) => self
785                .local_keys
786                .as_ref()
787                .and_then(|keys| keys.get(*local_id)),
788        }
789    }
790
791    fn resolve_key<'a>(
792        &'a self,
793        val: &'a ValueRef<StdTensorOp>,
794    ) -> ShapeGuardResult<&'a ValueKey<StdTensorOp>> {
795        match val {
796            ValueRef::External(key) => Ok(key),
797            ValueRef::Local(local_id) if self.local_keys.is_none() => Err(self
798                .shape_guard_failure(ShapeGuardError::LocalWithoutAttachedGraph {
799                    local_id: *local_id,
800                })),
801            ValueRef::Local(local_id) => self
802                .local_keys
803                .as_ref()
804                .and_then(|keys| keys.get(*local_id))
805                .ok_or_else(|| {
806                    self.shape_guard_failure(ShapeGuardError::LocalOutOfBounds {
807                        local_id: *local_id,
808                    })
809                }),
810        }
811    }
812
813    #[cfg(feature = "autodiff")]
814    fn shape_guard_failure(&self, source: ShapeGuardError) -> ShapeGuardFailure {
815        ShapeGuardFailure::new(source, Arc::clone(&self.deferred_shape_error))
816    }
817
818    #[cfg(not(feature = "autodiff"))]
819    fn shape_guard_failure(&self, source: ShapeGuardError) -> ShapeGuardFailure {
820        ShapeGuardFailure::new(source)
821    }
822
823    fn ensure_metadata_loaded(&mut self, key: &ValueKey<StdTensorOp>) {
824        if !self.metadata.contains_key(key) && self.use_global_registry {
825            if let Ok(Some(meta)) = lookup_global_metadata(key) {
826                self.metadata.insert(key.clone(), meta);
827            }
828        }
829    }
830}
831
832/// Look up a single metadata entry from the global registry.
833///
834/// Locks the registry briefly for a single `HashMap::get` + clone.
835///
836/// # Examples
837///
838/// ```
839/// use computegraph::types::ValueKey;
840/// use tenferro_ops::ad::context::lookup_global_metadata;
841/// use tenferro_ops::input_key::TensorInputKey;
842/// use tenferro_ops::std_tensor_op::StdTensorOp;
843///
844/// let key = ValueKey::<StdTensorOp>::Input(TensorInputKey::User { id: 99 });
845/// let meta = lookup_global_metadata(&key).unwrap();
846/// assert!(meta.is_none());
847/// ```
848///
849/// # Errors
850///
851/// Returns [`MetadataRegistryError::LockPoisoned`] when the global metadata
852/// registry lock is poisoned.
853pub fn lookup_global_metadata(
854    key: &ValueKey<StdTensorOp>,
855) -> Result<Option<TensorMeta>, MetadataRegistryError> {
856    let guard = global_metadata_registry()
857        .lock()
858        .map_err(|_| MetadataRegistryError::LockPoisoned)?;
859    Ok(guard
860        .get(key)
861        .and_then(|entry| entry.stack.last())
862        .map(|registration| registration.meta.clone()))
863}
864
865#[doc(hidden)]
866///
867/// # Errors
868///
869/// Returns [`MetadataRegistryError::LockPoisoned`] when the global metadata
870/// registry lock is poisoned.
871pub fn register_scoped_global_metadata_batch<I>(
872    entries: I,
873) -> Result<GlobalMetadataScope, MetadataRegistryError>
874where
875    I: IntoIterator<Item = (ValueKey<StdTensorOp>, TensorMeta)>,
876{
877    let mut guard = global_metadata_registry()
878        .lock()
879        .map_err(|_| MetadataRegistryError::LockPoisoned)?;
880    let mut registrations = Vec::new();
881    for (key, meta) in entries {
882        let token = NEXT_GLOBAL_METADATA_TOKEN.fetch_add(1, AtomicOrdering::Relaxed);
883        let entry = guard
884            .entry(key.clone())
885            .or_insert_with(|| GlobalMetadataEntry { stack: Vec::new() });
886        entry.stack.push(GlobalMetadataRegistration { token, meta });
887        registrations.push(ScopedGlobalMetadataRegistration { key, token });
888    }
889    Ok(GlobalMetadataScope { registrations })
890}
891
892fn release_scoped_global_metadata(registrations: &[ScopedGlobalMetadataRegistration]) {
893    let Ok(mut guard) = global_metadata_registry().lock() else {
894        // Drop cannot return an error. Failing closed here avoids reading or
895        // mutating data from a poisoned registry at the cost of leaking entries
896        // until process exit.
897        return;
898    };
899    for registration in registrations {
900        let should_remove = if let Some(entry) = guard.get_mut(&registration.key) {
901            if let Some(position) = entry
902                .stack
903                .iter()
904                .rposition(|candidate| candidate.token == registration.token)
905            {
906                entry.stack.remove(position);
907            }
908            entry.stack.is_empty()
909        } else {
910            false
911        };
912        if should_remove {
913            guard.remove(&registration.key);
914        }
915    }
916}
917
918/// Resolve a [`DimExpr`] to a concrete `usize`.
919#[doc(hidden)]
920pub fn resolve_dim(dim: &DimExpr) -> Result<usize, DimExprEvalError> {
921    dim.eval(&[])
922}
923
924/// Resolve matrix dimensions and record their ordering as a guard.
925#[doc(hidden)]
926pub fn resolve_and_guard(
927    m: &DimExpr,
928    n: &DimExpr,
929    ctx: &mut ShapeGuardContext,
930) -> Result<(usize, usize), DimExprEvalError> {
931    let m_size = resolve_dim(m)?;
932    let n_size = resolve_dim(n)?;
933    ctx.guards.push(ShapeGuard {
934        dim_a: m_size,
935        dim_b: n_size,
936        ordering: m_size.cmp(&n_size),
937    });
938    Ok((m_size, n_size))
939}