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