use std::cmp::Ordering;
use std::collections::HashMap;
#[cfg(feature = "autodiff")]
use std::sync::Arc;
use std::sync::{Mutex, OnceLock};
use computegraph::graph::Graph;
use computegraph::types::{ValueKey, ValueRef};
use tenferro_tensor::DType;
use crate::dim_expr::{DimExpr, DimExprEvalError};
#[cfg(feature = "autodiff")]
use crate::ext_op::{ExtensionAdRule, ExtensionRuleSet};
use crate::shape_extent::ShapeExtent;
use crate::std_tensor_op::StdTensorOp;
use crate::sym_dim::SymDim;
type MetadataMap = HashMap<ValueKey<StdTensorOp>, TensorMeta>;
type GlobalMetadataMap = HashMap<ValueKey<StdTensorOp>, GlobalMetadataEntry>;
#[derive(Clone, Debug)]
struct GlobalMetadataEntry {
meta: TensorMeta,
scoped_refs: usize,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum MetadataRegistryError {
#[error("AD global metadata registry lock poisoned")]
LockPoisoned,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum ShapeGuardError {
#[error("cannot resolve local value {local_id} without an attached graph")]
LocalWithoutAttachedGraph {
local_id: usize,
},
#[error("local value {local_id} is out of bounds for the attached graph")]
LocalOutOfBounds {
local_id: usize,
},
#[error("missing TensorMeta for {key:?}")]
MissingMetadata {
key: ValueKey<StdTensorOp>,
},
#[error("TensorMeta for {key:?} does not have an exact shape; query extents instead")]
NonExactShape {
key: ValueKey<StdTensorOp>,
},
}
pub type ShapeGuardResult<T> = Result<T, ShapeGuardError>;
#[cfg(feature = "autodiff")]
impl From<ShapeGuardError> for tidu::ADRuleError {
fn from(err: ShapeGuardError) -> Self {
tidu::ADRuleError::invalid_input(
"tenferro.shape_guard",
tidu::ADRuleKind::Jvp,
err.to_string(),
)
}
}
static GLOBAL_METADATA: OnceLock<Mutex<GlobalMetadataMap>> = OnceLock::new();
fn global_metadata_registry() -> &'static Mutex<GlobalMetadataMap> {
GLOBAL_METADATA.get_or_init(|| Mutex::new(HashMap::new()))
}
#[doc(hidden)]
#[derive(Debug)]
pub struct GlobalMetadataScope {
keys: Vec<ValueKey<StdTensorOp>>,
}
impl Drop for GlobalMetadataScope {
fn drop(&mut self) {
release_scoped_global_metadata(&self.keys);
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TensorMeta {
pub dtype: DType,
pub extents: Vec<ShapeExtent<SymDim>>,
}
impl TensorMeta {
pub fn exact(dtype: DType, shape: Vec<SymDim>) -> Self {
let extents = shape.iter().cloned().map(ShapeExtent::exact).collect();
Self { dtype, extents }
}
pub fn with_extents(dtype: DType, extents: Vec<ShapeExtent<SymDim>>) -> Self {
Self { dtype, extents }
}
pub fn rank(&self) -> usize {
self.extents.len()
}
pub fn extents(&self) -> &[ShapeExtent<SymDim>] {
&self.extents
}
pub fn exact_shape(&self) -> Option<Vec<SymDim>> {
self.extents
.iter()
.map(|extent| extent.as_exact().cloned())
.collect()
}
pub fn bound_shape(&self) -> Option<Vec<SymDim>> {
self.extents
.iter()
.map(|extent| extent.bound_expr().cloned())
.collect()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ShapeGuard {
pub dim_a: usize,
pub dim_b: usize,
pub ordering: Ordering,
}
#[derive(Clone, Debug, Default)]
pub struct ShapeGuardContext {
guards: Vec<ShapeGuard>,
metadata: MetadataMap,
use_global_registry: bool,
local_keys: Option<Vec<ValueKey<StdTensorOp>>>,
#[cfg(feature = "autodiff")]
extension_rules: Option<ExtensionRuleSet>,
}
impl ShapeGuardContext {
pub fn with_global_metadata() -> Self {
Self {
use_global_registry: true,
..Self::default()
}
}
#[doc(hidden)]
pub fn refresh_global_metadata(&mut self) {
self.use_global_registry = true;
}
#[cfg(feature = "autodiff")]
pub fn with_extension_rules(mut self, rules: ExtensionRuleSet) -> Self {
self.extension_rules = Some(rules);
self
}
#[doc(hidden)]
#[cfg(feature = "autodiff")]
pub(crate) fn extension_rule_for(&self, family_id: &str) -> Option<Arc<dyn ExtensionAdRule>> {
self.extension_rules
.as_ref()
.and_then(|rules| rules.lookup_rule(family_id))
}
pub fn guards(&self) -> &[ShapeGuard] {
&self.guards
}
pub fn clear_guards(&mut self) {
self.guards.clear();
}
pub fn shape_of(&mut self, val: &ValueRef<StdTensorOp>) -> ShapeGuardResult<Vec<SymDim>> {
let key = self.resolve_key(val)?.clone();
self.ensure_metadata_loaded(&key);
let meta = self
.metadata
.get(&key)
.ok_or_else(|| ShapeGuardError::MissingMetadata { key: key.clone() })?;
meta.exact_shape()
.ok_or(ShapeGuardError::NonExactShape { key })
}
pub fn rank_of(&mut self, val: &ValueRef<StdTensorOp>) -> ShapeGuardResult<usize> {
self.metadata_of(val).map(TensorMeta::rank)
}
pub fn extents_of(
&mut self,
val: &ValueRef<StdTensorOp>,
) -> ShapeGuardResult<&[ShapeExtent<SymDim>]> {
self.metadata_of(val).map(TensorMeta::extents)
}
pub fn exact_shape_of(
&mut self,
val: &ValueRef<StdTensorOp>,
) -> ShapeGuardResult<Option<Vec<SymDim>>> {
self.metadata_of(val).map(TensorMeta::exact_shape)
}
#[doc(hidden)]
pub fn shape_if_available(&mut self, val: &ValueRef<StdTensorOp>) -> Option<Vec<SymDim>> {
self.metadata_if_available(val)
.and_then(TensorMeta::exact_shape)
}
pub fn dtype_of(&mut self, val: &ValueRef<StdTensorOp>) -> ShapeGuardResult<DType> {
self.metadata_of(val).map(|meta| meta.dtype)
}
pub fn metadata_of(&mut self, val: &ValueRef<StdTensorOp>) -> ShapeGuardResult<&TensorMeta> {
let key = self.resolve_key(val)?.clone();
self.ensure_metadata_loaded(&key);
self.metadata
.get(&key)
.ok_or(ShapeGuardError::MissingMetadata { key })
}
#[doc(hidden)]
pub fn metadata_if_available(&mut self, val: &ValueRef<StdTensorOp>) -> Option<&TensorMeta> {
let key = self.resolve_key_if_available(val)?.clone();
self.ensure_metadata_loaded(&key);
self.metadata.get(&key)
}
#[doc(hidden)]
pub fn attach_graph(&mut self, graph: &Graph<StdTensorOp>) {
self.local_keys = Some(graph.values().iter().map(|node| node.key.clone()).collect());
}
#[doc(hidden)]
pub fn insert_metadata(&mut self, key: ValueKey<StdTensorOp>, meta: TensorMeta) {
self.metadata.insert(key, meta);
}
#[doc(hidden)]
pub fn extend_metadata<I>(&mut self, entries: I)
where
I: IntoIterator<Item = (ValueKey<StdTensorOp>, TensorMeta)>,
{
self.metadata.extend(entries);
}
fn resolve_key_if_available<'a>(
&'a self,
val: &'a ValueRef<StdTensorOp>,
) -> Option<&'a ValueKey<StdTensorOp>> {
match val {
ValueRef::External(key) => Some(key),
ValueRef::Local(local_id) => self
.local_keys
.as_ref()
.and_then(|keys| keys.get(*local_id)),
}
}
fn resolve_key<'a>(
&'a self,
val: &'a ValueRef<StdTensorOp>,
) -> ShapeGuardResult<&'a ValueKey<StdTensorOp>> {
match val {
ValueRef::External(key) => Ok(key),
ValueRef::Local(local_id) if self.local_keys.is_none() => {
Err(ShapeGuardError::LocalWithoutAttachedGraph {
local_id: *local_id,
})
}
ValueRef::Local(local_id) => self
.local_keys
.as_ref()
.and_then(|keys| keys.get(*local_id))
.ok_or(ShapeGuardError::LocalOutOfBounds {
local_id: *local_id,
}),
}
}
fn ensure_metadata_loaded(&mut self, key: &ValueKey<StdTensorOp>) {
if !self.metadata.contains_key(key) && self.use_global_registry {
if let Ok(Some(meta)) = lookup_global_metadata(key) {
self.metadata.insert(key.clone(), meta);
}
}
}
}
pub fn lookup_global_metadata(
key: &ValueKey<StdTensorOp>,
) -> Result<Option<TensorMeta>, MetadataRegistryError> {
let guard = global_metadata_registry()
.lock()
.map_err(|_| MetadataRegistryError::LockPoisoned)?;
Ok(guard.get(key).map(|entry| entry.meta.clone()))
}
#[doc(hidden)]
pub fn register_scoped_global_metadata_batch<I>(
entries: I,
) -> Result<GlobalMetadataScope, MetadataRegistryError>
where
I: IntoIterator<Item = (ValueKey<StdTensorOp>, TensorMeta)>,
{
let mut guard = global_metadata_registry()
.lock()
.map_err(|_| MetadataRegistryError::LockPoisoned)?;
let mut keys = Vec::new();
for (key, meta) in entries {
let entry = guard.entry(key.clone()).or_insert(GlobalMetadataEntry {
meta: meta.clone(),
scoped_refs: 0,
});
entry.meta = meta;
entry.scoped_refs += 1;
keys.push(key);
}
Ok(GlobalMetadataScope { keys })
}
fn release_scoped_global_metadata(keys: &[ValueKey<StdTensorOp>]) {
let Ok(mut guard) = global_metadata_registry().lock() else {
return;
};
for key in keys {
let should_remove = if let Some(entry) = guard.get_mut(key) {
entry.scoped_refs = entry.scoped_refs.saturating_sub(1);
entry.scoped_refs == 0
} else {
false
};
if should_remove {
guard.remove(key);
}
}
}
#[doc(hidden)]
pub fn resolve_dim(dim: &DimExpr) -> Result<usize, DimExprEvalError> {
dim.eval(&[])
}
#[doc(hidden)]
pub fn resolve_and_guard(
m: &DimExpr,
n: &DimExpr,
ctx: &mut ShapeGuardContext,
) -> Result<(usize, usize), DimExprEvalError> {
let m_size = resolve_dim(m)?;
let n_size = resolve_dim(n)?;
ctx.guards.push(ShapeGuard {
dim_a: m_size,
dim_b: n_size,
ordering: m_size.cmp(&n_size),
});
Ok((m_size, n_size))
}