Skip to main content

tenferro_ops/
ext_op.rs

1//! Out-of-tree extension-operation mechanism.
2//!
3//! This module implements the [`ExtensionOp`] trait and its process-local
4//! registry. Together they let external crates contribute fused primitives
5//! that participate in the [`crate::std_tensor_op::StdTensorOp`] graph through
6//! the single carrier variant
7//! `StdTensorOp::Extension(Arc<dyn ExtensionOp>)`.
8//!
9//! See `docs/spec/extension-op.md` for the normative contract. Key points:
10//!
11//! - Identity / hashing / equality are expressed on the trait so the
12//!   type-erased `Arc<dyn ExtensionOp>` carrier can satisfy
13//!   `Clone + Hash + Eq + Send + Sync + 'static` (computegraph's
14//!   `GraphOperation` requirements).
15//! - AD rules are owned by explicit [`ExtensionRuleSet`] values. A rule may
16//!   emit core [`StdTensorOp`] values and registered `Extension` values so
17//!   out-of-tree operations remain in the same graph.
18//! - Extension ops themselves do not require process-global registration.
19//!   Frontends carry them directly as `Arc<dyn ExtensionOp>`.
20
21use std::any::Any;
22use std::fmt::Debug;
23use std::hash::{Hash, Hasher};
24use std::sync::Arc;
25
26use computegraph::graph::GraphBuilder;
27#[cfg(not(feature = "autodiff"))]
28use computegraph::types::ValueRef;
29#[cfg(feature = "autodiff")]
30use computegraph::types::{LocalValueId, OperationRole, ValueKey, ValueRef};
31use tenferro_tensor::{DType, Tensor};
32#[cfg(feature = "autodiff")]
33use tidu::{ADRuleError, ADRuleKind, ADRuleResult, PrimitiveTransposeInput};
34
35#[cfg(feature = "autodiff")]
36use crate::ad::context::ShapeGuardContext;
37#[cfg(feature = "autodiff")]
38use crate::ad::PrimitiveRuleBuilder;
39use crate::std_tensor_op::StdTensorOp;
40use crate::sym_dim::SymDim;
41#[cfg(feature = "autodiff")]
42use std::collections::HashMap;
43
44/// Error returned when an extension cannot expand itself into standard ops.
45///
46/// # Examples
47///
48/// ```
49/// use tenferro_ops::ext_op::ExtensionLoweringError;
50///
51/// let err = ExtensionLoweringError::new("example extension cannot lower");
52/// assert!(err.to_string().contains("cannot lower"));
53/// ```
54#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
55#[error("{message}")]
56pub struct ExtensionLoweringError {
57    message: String,
58}
59
60impl ExtensionLoweringError {
61    /// Create a lowering error with a human-readable diagnostic.
62    ///
63    /// # Examples
64    ///
65    /// ```
66    /// use tenferro_ops::ext_op::ExtensionLoweringError;
67    ///
68    /// let err = ExtensionLoweringError::new("shape must be static");
69    /// assert_eq!(err.to_string(), "shape must be static");
70    /// ```
71    pub fn new(message: impl Into<String>) -> Self {
72        Self {
73            message: message.into(),
74        }
75    }
76}
77
78/// Result returned by [`ExtensionOp::lower_to_standard_ops`].
79pub type ExtensionLoweringResult =
80    std::result::Result<Option<Vec<ValueRef<StdTensorOp>>>, ExtensionLoweringError>;
81
82/// Host/reference implementation for an extension family.
83///
84/// This capability is optional. Backend-only extension families can omit it
85/// and still implement [`ExtensionOp`]; runtimes that specifically delegate to
86/// host reference execution must report a typed capability-missing error when
87/// this hook is absent.
88///
89/// # Examples
90///
91/// ```
92/// use tenferro_ops::ext_op::HostReference;
93/// use tenferro_tensor::Tensor;
94///
95/// #[derive(Debug)]
96/// struct IdentityHost;
97///
98/// impl HostReference for IdentityHost {
99///     fn execute(&self, inputs: &[&Tensor]) -> tenferro_tensor::Result<Vec<Tensor>> {
100///         Ok(vec![inputs[0].clone()])
101///     }
102/// }
103///
104/// let input = Tensor::from_vec_col_major(vec![1], vec![3.0_f64]).unwrap();
105/// let output = IdentityHost.execute(&[&input]).unwrap();
106/// assert_eq!(output[0].as_slice::<f64>().unwrap(), &[3.0]);
107/// ```
108pub trait HostReference: Debug + Send + Sync + 'static {
109    /// Execute the extension op on host/reference tensors.
110    fn execute(&self, inputs: &[&Tensor]) -> tenferro_tensor::Result<Vec<Tensor>>;
111}
112
113/// The contract every out-of-tree extension primitive must satisfy.
114///
115/// Implementations appear in the core graph as
116/// `StdTensorOp::Extension(Arc<dyn ExtensionOp>)`. Every method is part of the
117/// `ExtensionOp` spec (`docs/spec/extension-op.md`); the short form:
118///
119/// - identity via [`family_id`][Self::family_id] + [`payload_hash`][Self::payload_hash]
120///   + [`payload_eq`][Self::payload_eq];
121/// - fixed arity via [`input_count`][Self::input_count] / [`output_count`][Self::output_count];
122/// - shape / dtype inference via [`infer_output_meta`][Self::infer_output_meta];
123/// - optional host/reference forward execution via
124///   [`host_reference`][Self::host_reference];
125/// - optional fixed-shape standard-op expansion via
126///   [`lower_to_standard_ops`][Self::lower_to_standard_ops] for peer lowerers
127///   such as XLA that cannot execute extension runtimes;
128/// - AD via separately registered role-specific extension rules.
129///
130/// # Downcast convention
131///
132/// Implementations MUST also implement [`Any`] so that
133/// [`ExtensionOp::payload_eq`] can downcast a trait-object reference to
134/// the concrete type. The helper [`ExtensionOp::as_any`] returns
135/// `&dyn Any` for this purpose. Implementations usually define it as
136/// `fn as_any(&self) -> &dyn Any { self }`.
137///
138/// # Examples
139///
140/// ```
141/// # use std::any::Any;
142/// use std::sync::Arc;
143/// use tenferro_ops::ext_op::{ExtensionOp, HostReference};
144/// use tenferro_ops::SymDim;
145/// use tenferro_tensor::{DType, Tensor};
146///
147/// #[derive(Clone, Debug)]
148/// struct IdentityExt;
149///
150/// impl ExtensionOp for IdentityExt {
151///     fn family_id(&self) -> &'static str { "example.identity.v1" }
152///     fn payload_hash(&self, _hasher: &mut dyn std::hash::Hasher) {}
153///     fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
154///         other.as_any().downcast_ref::<IdentityExt>().is_some()
155///     }
156///     fn clone_arc(&self) -> Arc<dyn ExtensionOp> { Arc::new(self.clone()) }
157///     fn as_any(&self) -> &dyn Any { self }
158///     fn input_count(&self) -> usize { 1 }
159///     fn output_count(&self) -> usize { 1 }
160///     fn infer_output_meta(
161///         &self,
162///         dtypes: &[DType],
163///         shapes: &[&[SymDim]],
164///     ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
165///         Ok(vec![(dtypes[0], shapes[0].to_vec())])
166///     }
167///     fn host_reference(&self) -> Option<&dyn HostReference> {
168///         Some(self)
169///     }
170/// }
171///
172/// impl HostReference for IdentityExt {
173///     fn execute(&self, inputs: &[&Tensor]) -> tenferro_tensor::Result<Vec<Tensor>> {
174///         Ok(vec![inputs[0].clone()])
175///     }
176/// }
177///
178/// let op: Arc<dyn ExtensionOp> = Arc::new(IdentityExt);
179/// assert_eq!(op.input_count(), 1);
180/// ```
181pub trait ExtensionOp: Debug + Send + Sync + 'static {
182    // ----- Identity, hashing, equality (spec Section 5) -----
183
184    /// Stable, process-independent family identifier.
185    ///
186    /// MUST be unique per extension *family* (payload schema), not per
187    /// *instance*, and MUST follow the reserved format
188    /// `"<crate-name>.<op-name>.v<major>"`.
189    fn family_id(&self) -> &'static str;
190
191    /// Hash the payload (everything except `family_id`).
192    ///
193    /// Implementations MUST be pure and deterministic across calls on the same
194    /// value. Hashes MUST NOT include transient state such as allocation
195    /// addresses or atomically updated counters.
196    fn payload_hash(&self, hasher: &mut dyn Hasher);
197
198    /// Structural equality against another extension value.
199    ///
200    /// The carrier's `PartialEq` impl first compares `family_id`s. When the
201    /// family IDs match, it calls `payload_eq`. Implementations MUST return
202    /// `true` iff the payloads are semantically equal AND
203    /// `other.family_id() == self.family_id()`.
204    fn payload_eq(&self, other: &dyn ExtensionOp) -> bool;
205
206    /// Deep-clone the payload behind an `Arc`.
207    ///
208    /// The carrier's `Clone` impl uses `Arc::clone` on the fast path; this
209    /// method exists for rare cases that need a second independent `Arc`.
210    fn clone_arc(&self) -> Arc<dyn ExtensionOp>;
211
212    /// Upcast this extension to `&dyn Any` for downcasting in `payload_eq`.
213    ///
214    /// Implementations SHOULD return `self` verbatim. The method is
215    /// object-safe (no `Self: Sized` bound) so it can be called on an
216    /// `&dyn ExtensionOp`; that's what makes
217    /// `other.as_any().downcast_ref::<ConcreteType>()` work from
218    /// [`Self::payload_eq`] implementations.
219    fn as_any(&self) -> &dyn Any;
220
221    // ----- Arity (spec Section 6) -----
222
223    /// Number of primal inputs. MUST be constant for any given
224    /// `Arc<dyn ExtensionOp>` value.
225    fn input_count(&self) -> usize;
226
227    /// Number of outputs. MUST match the length of the vector returned by a
228    /// successful [`Self::infer_output_meta`] call.
229    fn output_count(&self) -> usize;
230
231    // ----- Shape and dtype inference (spec Section 7) -----
232
233    /// Infer output dtypes and shapes for each output slot.
234    ///
235    /// Implementations MUST validate arity, rank, dtype, axis, and other
236    /// input-derived metadata before indexing shape arrays. Invalid public
237    /// input must return a typed error rather than an empty sentinel or panic.
238    ///
239    /// On success, the returned vector MUST have length `self.output_count()`,
240    /// one `(dtype, shape)` entry per output slot. Shapes use [`SymDim`] so
241    /// extension ops compose with graph-global symbolic metadata.
242    fn infer_output_meta(
243        &self,
244        input_dtypes: &[DType],
245        input_shapes: &[&[SymDim]],
246    ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>>;
247
248    // ----- Optional host/reference execution (spec Section 8) -----
249
250    /// Return the host/reference implementation for this payload, when the
251    /// family has one.
252    ///
253    /// Backend-only extension families may leave the default `None`. Runtime
254    /// execution never silently falls back to this hook; a runtime must opt in
255    /// explicitly by using this capability.
256    fn host_reference(&self) -> Option<&dyn HostReference> {
257        None
258    }
259
260    /// Optionally expand this extension into standard tensor graph operations.
261    ///
262    /// Peer lowerers call this when all input metadata is known and extension
263    /// runtime dispatch is not available. Return `Ok(Some(outputs))` after
264    /// adding only standard [`StdTensorOp`] operations to `builder`. Return
265    /// `Ok(None)` when this extension family has no standard-op lowering for
266    /// the supplied metadata; strict lowerers should surface that as an
267    /// explicit unsupported-extension error. Return [`ExtensionLoweringError`]
268    /// when the payload is malformed or the lowering detects invalid metadata.
269    ///
270    /// The default implementation returns `Ok(None)` so existing extension
271    /// runtimes keep their native dispatch behavior until their owning crate
272    /// deliberately implements this hook.
273    fn lower_to_standard_ops(
274        &self,
275        _builder: &mut GraphBuilder<StdTensorOp>,
276        _inputs: &[ValueRef<StdTensorOp>],
277        _input_dtypes: &[DType],
278        _input_shapes: &[&[SymDim]],
279    ) -> ExtensionLoweringResult {
280        Ok(None)
281    }
282
283    /// Optionally return an equivalent op that produces only live outputs.
284    ///
285    /// `live_outputs` is aligned with this op's current output slots. Return
286    /// `None` when the family does not support output pruning. Return
287    /// `Some(op)` only when the new op's outputs are exactly the live output
288    /// slots, in ascending slot order, and `op.output_count()` equals the
289    /// number of `true` entries in `live_outputs`.
290    fn prune_outputs(&self, _live_outputs: &[bool]) -> Option<Arc<dyn ExtensionOp>> {
291        None
292    }
293
294    // AD rules are registered separately; see the role-specific rule traits.
295}
296
297/// Definitional JVP rule provider for an extension family.
298///
299/// Required only for families that appear in primal graphs and must be
300/// differentiable.
301#[cfg(feature = "autodiff")]
302pub trait ExtensionLinearizeRule: Debug + Send + Sync + 'static {
303    /// The extension family this rule handles.
304    fn family_id(&self) -> &'static str;
305
306    /// Emit the linear (JVP) rule.
307    fn linearize(
308        &self,
309        op: &dyn ExtensionOp,
310        builder: &mut dyn PrimitiveRuleBuilder,
311        primal_in: &[ValueKey<StdTensorOp>],
312        primal_out: &[ValueKey<StdTensorOp>],
313        tangent_in: &[Option<LocalValueId>],
314        ctx: &mut ShapeGuardContext,
315    ) -> ADRuleResult<Vec<Option<LocalValueId>>>;
316}
317
318/// Adjoint rule for an extension op viewed as a linear map in active inputs.
319///
320/// This rule is used only while `linear_transpose` walks a linearized graph.
321#[cfg(feature = "autodiff")]
322pub trait ExtensionLinearTransposeRule: Debug + Send + Sync + 'static {
323    /// The extension family this rule handles.
324    fn family_id(&self) -> &'static str;
325
326    /// Emit cotangents for active linear inputs.
327    fn linear_transpose(
328        &self,
329        op: &dyn ExtensionOp,
330        builder: &mut dyn PrimitiveRuleBuilder,
331        cotangent_out: &[Option<LocalValueId>],
332        inputs: &[PrimitiveTransposeInput<StdTensorOp>],
333        active_mask: &[bool],
334        ctx: &mut ShapeGuardContext,
335    ) -> ADRuleResult<Vec<Option<LocalValueId>>>;
336}
337
338/// Optional direct primal VJP rule provider for an extension family.
339///
340/// This is an opt-in performance or compatibility escape hatch. The default
341/// reverse-mode path remains `linearize` followed by `linear_transpose`.
342#[cfg(feature = "autodiff")]
343pub trait ExtensionPrimalVjpRule: Debug + Send + Sync + 'static {
344    /// The extension family this rule handles.
345    fn family_id(&self) -> &'static str;
346
347    /// Emit a direct VJP from the primal op.
348    fn primal_vjp(
349        &self,
350        op: &dyn ExtensionOp,
351        builder: &mut dyn PrimitiveRuleBuilder,
352        cotangent_out: &[Option<LocalValueId>],
353        inputs: &[ValueRef<StdTensorOp>],
354        ctx: &mut ShapeGuardContext,
355    ) -> ADRuleResult<Vec<Option<LocalValueId>>>;
356}
357
358/// Extension AD rule role.
359#[cfg(feature = "autodiff")]
360#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
361pub enum ExtensionRuleRole {
362    /// Definitional JVP.
363    Linearize,
364    /// Transpose of an op as a linear map.
365    LinearTranspose,
366    /// Direct VJP from the primal graph.
367    PrimalVjp,
368}
369
370/// Errors returned from extension registries.
371#[cfg(feature = "autodiff")]
372#[derive(Debug, thiserror::Error)]
373pub enum ExtensionRegistryError {
374    /// An AD rule with the same `family_id` was already registered for one role.
375    #[error("extension AD {role:?} rule for family_id {family_id:?} already registered")]
376    DuplicateRule {
377        /// Duplicate extension family id.
378        family_id: &'static str,
379        /// Duplicate AD rule role.
380        role: ExtensionRuleRole,
381    },
382    /// The `family_id` does not match the namespaced format
383    /// `"<crate-name>.<op-name>.v<major>"`.
384    #[error("family_id {family_id:?} does not match the namespaced format")]
385    MalformedFamilyId { family_id: &'static str },
386}
387
388#[cfg(feature = "autodiff")]
389type LinearizeRuleMap = HashMap<&'static str, Arc<dyn ExtensionLinearizeRule>>;
390#[cfg(feature = "autodiff")]
391type LinearTransposeRuleMap = HashMap<&'static str, Arc<dyn ExtensionLinearTransposeRule>>;
392#[cfg(feature = "autodiff")]
393type PrimalVjpRuleMap = HashMap<&'static str, Arc<dyn ExtensionPrimalVjpRule>>;
394
395/// Explicit, owned set of extension AD rules.
396///
397/// This is the rule container used by higher-level AD contexts. Extension AD
398/// intentionally has no process-global fallback; callers must pass the rule set
399/// that their graph needs.
400#[cfg(feature = "autodiff")]
401#[derive(Clone, Default)]
402pub struct ExtensionRuleSet {
403    linearize_rules: Arc<LinearizeRuleMap>,
404    linear_transpose_rules: Arc<LinearTransposeRuleMap>,
405    primal_vjp_rules: Arc<PrimalVjpRuleMap>,
406}
407
408#[cfg(feature = "autodiff")]
409impl std::fmt::Debug for ExtensionRuleSet {
410    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411        let mut linearize: Vec<_> = self.linearize_rules.keys().copied().collect();
412        let mut linear_transpose: Vec<_> = self.linear_transpose_rules.keys().copied().collect();
413        let mut primal_vjp: Vec<_> = self.primal_vjp_rules.keys().copied().collect();
414        linearize.sort_unstable();
415        linear_transpose.sort_unstable();
416        primal_vjp.sort_unstable();
417        f.debug_struct("ExtensionRuleSet")
418            .field("linearize", &linearize)
419            .field("linear_transpose", &linear_transpose)
420            .field("primal_vjp", &primal_vjp)
421            .finish()
422    }
423}
424
425#[cfg(feature = "autodiff")]
426impl ExtensionRuleSet {
427    /// Create an empty extension rule set.
428    ///
429    /// # Examples
430    ///
431    /// ```
432    /// use tenferro_ops::ExtensionRuleSet;
433    ///
434    /// let rules = ExtensionRuleSet::new();
435    /// assert!(!rules.is_linearize_registered("example.missing.v1"));
436    /// ```
437    pub fn new() -> Self {
438        Self::default()
439    }
440
441    /// Add one linearize rule to this owned set.
442    ///
443    /// # Examples
444    ///
445    /// ```
446    /// use std::sync::Arc;
447    /// use tidu::ADRuleResult;
448    /// use computegraph::types::{LocalValueId, ValueKey};
449    /// use tenferro_ops::ad::PrimitiveRuleBuilder;
450    /// use tenferro_ops::ext_op::{ExtensionLinearizeRule, ExtensionOp};
451    /// use tenferro_ops::{ExtensionRuleSet, ShapeGuardContext};
452    /// use tenferro_ops::std_tensor_op::StdTensorOp;
453    ///
454    /// #[derive(Debug)]
455    /// struct Rule;
456    ///
457    /// impl ExtensionLinearizeRule for Rule {
458    ///     fn family_id(&self) -> &'static str { "example.register_linearize.v1" }
459    ///     fn linearize(
460    ///         &self,
461    ///         _op: &dyn ExtensionOp,
462    ///         _builder: &mut dyn PrimitiveRuleBuilder,
463    ///         _primal_in: &[ValueKey<StdTensorOp>],
464    ///         _primal_out: &[ValueKey<StdTensorOp>],
465    ///         tangent_in: &[Option<LocalValueId>],
466    ///         _ctx: &mut ShapeGuardContext,
467    ///     ) -> ADRuleResult<Vec<Option<LocalValueId>>> {
468    ///         Ok(tangent_in.to_vec())
469    ///     }
470    /// }
471    ///
472    /// let mut rules = ExtensionRuleSet::new();
473    /// rules.register_linearize(Arc::new(Rule)).unwrap();
474    /// assert!(rules.lookup_linearize("example.register_linearize.v1").is_some());
475    /// ```
476    pub fn register_linearize(
477        &mut self,
478        rule: Arc<dyn ExtensionLinearizeRule>,
479    ) -> Result<(), ExtensionRegistryError> {
480        let family_id = rule.family_id();
481        validate_linearize_insert(&self.linearize_rules, family_id)?;
482        let rules = Arc::make_mut(&mut self.linearize_rules);
483        rules.insert(family_id, rule);
484        Ok(())
485    }
486
487    /// Add one linear-transpose rule to this owned set.
488    pub fn register_linear_transpose(
489        &mut self,
490        rule: Arc<dyn ExtensionLinearTransposeRule>,
491    ) -> Result<(), ExtensionRegistryError> {
492        let family_id = rule.family_id();
493        validate_linear_transpose_insert(&self.linear_transpose_rules, family_id)?;
494        let rules = Arc::make_mut(&mut self.linear_transpose_rules);
495        rules.insert(family_id, rule);
496        Ok(())
497    }
498
499    /// Add one primal-VJP rule to this owned set.
500    pub fn register_primal_vjp(
501        &mut self,
502        rule: Arc<dyn ExtensionPrimalVjpRule>,
503    ) -> Result<(), ExtensionRegistryError> {
504        let family_id = rule.family_id();
505        validate_primal_vjp_insert(&self.primal_vjp_rules, family_id)?;
506        let rules = Arc::make_mut(&mut self.primal_vjp_rules);
507        rules.insert(family_id, rule);
508        Ok(())
509    }
510
511    /// Return a new rule set containing a linearize rule.
512    ///
513    /// # Examples
514    ///
515    /// ```
516    /// use std::sync::Arc;
517    /// use tidu::ADRuleResult;
518    /// use computegraph::types::{LocalValueId, ValueKey};
519    /// use tenferro_ops::ad::PrimitiveRuleBuilder;
520    /// use tenferro_ops::ext_op::{ExtensionLinearizeRule, ExtensionOp};
521    /// use tenferro_ops::{ExtensionRuleSet, ShapeGuardContext};
522    /// use tenferro_ops::std_tensor_op::StdTensorOp;
523    ///
524    /// #[derive(Debug)]
525    /// struct Rule;
526    ///
527    /// impl ExtensionLinearizeRule for Rule {
528    ///     fn family_id(&self) -> &'static str { "example.with_linearize.v1" }
529    ///     fn linearize(
530    ///         &self,
531    ///         _op: &dyn ExtensionOp,
532    ///         _builder: &mut dyn PrimitiveRuleBuilder,
533    ///         _primal_in: &[ValueKey<StdTensorOp>],
534    ///         _primal_out: &[ValueKey<StdTensorOp>],
535    ///         tangent_in: &[Option<LocalValueId>],
536    ///         _ctx: &mut ShapeGuardContext,
537    ///     ) -> ADRuleResult<Vec<Option<LocalValueId>>> {
538    ///         Ok(tangent_in.to_vec())
539    ///     }
540    /// }
541    ///
542    /// let rules = ExtensionRuleSet::new().with_linearize(Arc::new(Rule)).unwrap();
543    /// assert!(rules.is_linearize_registered("example.with_linearize.v1"));
544    /// ```
545    pub fn with_linearize(
546        mut self,
547        rule: Arc<dyn ExtensionLinearizeRule>,
548    ) -> Result<Self, ExtensionRegistryError> {
549        self.register_linearize(rule)?;
550        Ok(self)
551    }
552
553    /// Return a new rule set containing a linear-transpose rule.
554    pub fn with_linear_transpose(
555        mut self,
556        rule: Arc<dyn ExtensionLinearTransposeRule>,
557    ) -> Result<Self, ExtensionRegistryError> {
558        self.register_linear_transpose(rule)?;
559        Ok(self)
560    }
561
562    /// Return a new rule set containing a primal-VJP rule.
563    pub fn with_primal_vjp(
564        mut self,
565        rule: Arc<dyn ExtensionPrimalVjpRule>,
566    ) -> Result<Self, ExtensionRegistryError> {
567        self.register_primal_vjp(rule)?;
568        Ok(self)
569    }
570
571    /// Merge another owned rule set into this one.
572    ///
573    /// The merge is atomic: if any rule in `other` is invalid or duplicates an
574    /// existing family, `self` is left unchanged.
575    ///
576    /// # Examples
577    ///
578    /// ```
579    /// use tenferro_ops::ExtensionRuleSet;
580    ///
581    /// let mut rules = ExtensionRuleSet::new();
582    /// rules.merge(ExtensionRuleSet::new()).unwrap();
583    /// assert!(!rules.is_linearize_registered("example.missing.v1"));
584    /// ```
585    pub fn merge(&mut self, other: ExtensionRuleSet) -> Result<(), ExtensionRegistryError> {
586        let mut linearize_rules = (*self.linearize_rules).clone();
587        let mut linear_transpose_rules = (*self.linear_transpose_rules).clone();
588        let mut primal_vjp_rules = (*self.primal_vjp_rules).clone();
589        for rule in other.linearize_rules.values() {
590            insert_linearize_rule(&mut linearize_rules, Arc::clone(rule))?;
591        }
592        for rule in other.linear_transpose_rules.values() {
593            insert_linear_transpose_rule(&mut linear_transpose_rules, Arc::clone(rule))?;
594        }
595        for rule in other.primal_vjp_rules.values() {
596            insert_primal_vjp_rule(&mut primal_vjp_rules, Arc::clone(rule))?;
597        }
598        self.linearize_rules = Arc::new(linearize_rules);
599        self.linear_transpose_rules = Arc::new(linear_transpose_rules);
600        self.primal_vjp_rules = Arc::new(primal_vjp_rules);
601        Ok(())
602    }
603
604    /// Look up a linearize rule in this set.
605    ///
606    /// # Examples
607    ///
608    /// ```
609    /// use tenferro_ops::ExtensionRuleSet;
610    ///
611    /// let rules = ExtensionRuleSet::new();
612    /// assert!(rules.lookup_linearize("example.missing.v1").is_none());
613    /// ```
614    pub fn lookup_linearize(&self, family_id: &str) -> Option<Arc<dyn ExtensionLinearizeRule>> {
615        self.linearize_rules.get(family_id).cloned()
616    }
617
618    /// Look up a linear-transpose rule in this set.
619    pub fn lookup_linear_transpose(
620        &self,
621        family_id: &str,
622    ) -> Option<Arc<dyn ExtensionLinearTransposeRule>> {
623        self.linear_transpose_rules.get(family_id).cloned()
624    }
625
626    /// Look up a primal-VJP rule in this set.
627    pub fn lookup_primal_vjp(&self, family_id: &str) -> Option<Arc<dyn ExtensionPrimalVjpRule>> {
628        self.primal_vjp_rules.get(family_id).cloned()
629    }
630
631    /// Return whether `family_id` has a linearize rule.
632    ///
633    /// # Examples
634    ///
635    /// ```
636    /// use tenferro_ops::ExtensionRuleSet;
637    ///
638    /// let rules = ExtensionRuleSet::new();
639    /// assert!(!rules.is_linearize_registered("example.missing.v1"));
640    /// ```
641    pub fn is_linearize_registered(&self, family_id: &str) -> bool {
642        self.linearize_rules.contains_key(family_id)
643    }
644
645    /// Return whether `family_id` has a linear-transpose rule.
646    pub fn is_linear_transpose_registered(&self, family_id: &str) -> bool {
647        self.linear_transpose_rules.contains_key(family_id)
648    }
649
650    /// Return whether `family_id` has a primal-VJP rule.
651    pub fn is_primal_vjp_registered(&self, family_id: &str) -> bool {
652        self.primal_vjp_rules.contains_key(family_id)
653    }
654}
655
656#[cfg(feature = "autodiff")]
657fn is_valid_family_id(family_id: &str) -> bool {
658    // Required shape: `<crate>.<op>.v<major>` with at least one non-empty
659    // `<crate>` chunk, at least one non-empty `<op>` chunk (which may itself
660    // contain `.`), and a final `.v<integer>` component. `<crate>` and
661    // `<op>` segments must be ASCII with no whitespace.
662    let mut parts = family_id.rsplitn(2, '.');
663    let Some(version_part) = parts.next() else {
664        return false;
665    };
666    let Some(prefix) = parts.next() else {
667        return false;
668    };
669    if !version_part.starts_with('v') {
670        return false;
671    }
672    let digits = &version_part[1..];
673    if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
674        return false;
675    }
676    let Some((crate_name, op_name)) = prefix.split_once('.') else {
677        return false;
678    };
679    if crate_name.is_empty() || op_name.is_empty() {
680        return false;
681    }
682    let any_invalid = |s: &str| s.chars().any(|c| c.is_whitespace() || !c.is_ascii());
683    if any_invalid(crate_name) || any_invalid(op_name) {
684        return false;
685    }
686    true
687}
688
689#[cfg(feature = "autodiff")]
690fn insert_linearize_rule(
691    rules: &mut LinearizeRuleMap,
692    rule: Arc<dyn ExtensionLinearizeRule>,
693) -> Result<(), ExtensionRegistryError> {
694    let family_id = rule.family_id();
695    validate_linearize_insert(rules, family_id)?;
696    rules.insert(family_id, rule);
697    Ok(())
698}
699
700#[cfg(feature = "autodiff")]
701fn insert_linear_transpose_rule(
702    rules: &mut LinearTransposeRuleMap,
703    rule: Arc<dyn ExtensionLinearTransposeRule>,
704) -> Result<(), ExtensionRegistryError> {
705    let family_id = rule.family_id();
706    validate_linear_transpose_insert(rules, family_id)?;
707    rules.insert(family_id, rule);
708    Ok(())
709}
710
711#[cfg(feature = "autodiff")]
712fn insert_primal_vjp_rule(
713    rules: &mut PrimalVjpRuleMap,
714    rule: Arc<dyn ExtensionPrimalVjpRule>,
715) -> Result<(), ExtensionRegistryError> {
716    let family_id = rule.family_id();
717    validate_primal_vjp_insert(rules, family_id)?;
718    rules.insert(family_id, rule);
719    Ok(())
720}
721
722#[cfg(feature = "autodiff")]
723fn validate_rule_insert(
724    contains_family: bool,
725    family_id: &'static str,
726    role: ExtensionRuleRole,
727) -> Result<(), ExtensionRegistryError> {
728    if !is_valid_family_id(family_id) {
729        return Err(ExtensionRegistryError::MalformedFamilyId { family_id });
730    }
731    if contains_family {
732        return Err(ExtensionRegistryError::DuplicateRule { family_id, role });
733    }
734    Ok(())
735}
736
737#[cfg(feature = "autodiff")]
738fn validate_linearize_insert(
739    rules: &LinearizeRuleMap,
740    family_id: &'static str,
741) -> Result<(), ExtensionRegistryError> {
742    validate_rule_insert(
743        rules.contains_key(family_id),
744        family_id,
745        ExtensionRuleRole::Linearize,
746    )
747}
748
749#[cfg(feature = "autodiff")]
750fn validate_linear_transpose_insert(
751    rules: &LinearTransposeRuleMap,
752    family_id: &'static str,
753) -> Result<(), ExtensionRegistryError> {
754    validate_rule_insert(
755        rules.contains_key(family_id),
756        family_id,
757        ExtensionRuleRole::LinearTranspose,
758    )
759}
760
761#[cfg(feature = "autodiff")]
762fn validate_primal_vjp_insert(
763    rules: &PrimalVjpRuleMap,
764    family_id: &'static str,
765) -> Result<(), ExtensionRegistryError> {
766    validate_rule_insert(
767        rules.contains_key(family_id),
768        family_id,
769        ExtensionRuleRole::PrimalVjp,
770    )
771}
772
773/// Emit a registered extension linearization rule.
774#[cfg(feature = "autodiff")]
775pub fn linearize_extension_rule(
776    op: &dyn ExtensionOp,
777    builder: &mut dyn PrimitiveRuleBuilder,
778    primal_in: &[ValueKey<StdTensorOp>],
779    primal_out: &[ValueKey<StdTensorOp>],
780    tangent_in: &[Option<LocalValueId>],
781    ctx: &mut ShapeGuardContext,
782) -> ADRuleResult<Vec<Option<LocalValueId>>> {
783    match ctx.extension_linearize_rule_for(op.family_id()) {
784        Some(rule) => rule.linearize(op, builder, primal_in, primal_out, tangent_in, ctx),
785        None => Err(ADRuleError::unsupported(op.family_id(), ADRuleKind::Jvp)),
786    }
787}
788
789/// Emit a registered extension transpose rule.
790#[cfg(feature = "autodiff")]
791pub fn transpose_extension_rule(
792    op: &dyn ExtensionOp,
793    builder: &mut dyn PrimitiveRuleBuilder,
794    cotangent_out: &[Option<LocalValueId>],
795    inputs: &[PrimitiveTransposeInput<StdTensorOp>],
796    mode: &OperationRole,
797    ctx: &mut ShapeGuardContext,
798) -> ADRuleResult<Vec<Option<LocalValueId>>> {
799    match mode {
800        OperationRole::Linearized { active_mask } => {
801            match ctx.extension_linear_transpose_rule_for(op.family_id()) {
802                Some(rule) => {
803                    rule.linear_transpose(op, builder, cotangent_out, inputs, active_mask, ctx)
804                }
805                None => Err(ADRuleError::unsupported(
806                    op.family_id(),
807                    ADRuleKind::Transpose,
808                )),
809            }
810        }
811        OperationRole::Primary => match ctx.extension_primal_vjp_rule_for(op.family_id()) {
812            Some(rule) => {
813                let value_inputs = primal_vjp_inputs(inputs)?;
814                rule.primal_vjp(op, builder, cotangent_out, &value_inputs, ctx)
815            }
816            None => Err(ADRuleError::unsupported(
817                op.family_id(),
818                ADRuleKind::Transpose,
819            )),
820        },
821    }
822}
823
824#[cfg(feature = "autodiff")]
825fn primal_vjp_inputs(
826    inputs: &[PrimitiveTransposeInput<StdTensorOp>],
827) -> ADRuleResult<Vec<ValueRef<StdTensorOp>>> {
828    inputs
829        .iter()
830        .enumerate()
831        .map(|(index, input)| match input {
832            PrimitiveTransposeInput::Residual(key) => Ok(ValueRef::External(key.clone())),
833            PrimitiveTransposeInput::Linear {
834                primal: Some(primal),
835                ..
836            } => Ok(ValueRef::External(primal.clone())),
837            PrimitiveTransposeInput::Linear { key, primal: None } => {
838                Err(ADRuleError::invalid_input(
839                    "extension transpose",
840                    ADRuleKind::Transpose,
841                    format!(
842                        "input {index} is linear-only and cannot be used as a primal VJP value: {key:?}"
843                    ),
844                ))
845            }
846        })
847        .collect()
848}
849
850/// Thin adapter that lets a generic `H: Hasher` satisfy the object-safe
851/// `&mut dyn Hasher` signature required by [`ExtensionOp::payload_hash`].
852///
853/// Only `write` and `finish` are load-bearing from the generic hasher; the
854/// various `write_u8` / `write_u16` default implementations in `Hasher`
855/// delegate to `write`. The adapter preserves that behaviour.
856pub(crate) struct DynHasherProxy<'a, H: Hasher + ?Sized> {
857    inner: &'a mut H,
858}
859
860impl<'a, H: Hasher + ?Sized> DynHasherProxy<'a, H> {
861    pub(crate) fn new(inner: &'a mut H) -> Self {
862        Self { inner }
863    }
864}
865
866impl<H: Hasher + ?Sized> Hasher for DynHasherProxy<'_, H> {
867    fn finish(&self) -> u64 {
868        self.inner.finish()
869    }
870
871    fn write(&mut self, bytes: &[u8]) {
872        self.inner.write(bytes);
873    }
874}
875
876/// Hash an `Arc<dyn ExtensionOp>` payload using the extension's
877/// [`ExtensionOp::family_id`] plus [`ExtensionOp::payload_hash`]. Shared
878/// between the `StdTensorOp::Extension` carrier's `Hash` impl and callers
879/// that need to fingerprint an `ExtensionOp` independently.
880pub(crate) fn hash_extension<H: Hasher>(op: &(dyn ExtensionOp + '_), state: &mut H) {
881    op.family_id().as_bytes().hash(state);
882    op.payload_hash(&mut DynHasherProxy::new(state));
883}
884
885/// Structural equality used by the `StdTensorOp::Extension` carrier.
886///
887/// Short-circuits on `family_id` inequality so two extensions with
888/// accidentally similar payloads but different families cannot be unified
889/// by the op interner.
890pub(crate) fn ext_op_eq(a: &dyn ExtensionOp, b: &dyn ExtensionOp) -> bool {
891    a.family_id() == b.family_id() && a.payload_eq(b)
892}