Skip to main content

tenferro_ad/
semantic_extension.rs

1//! Semantic-program automatic-differentiation rules for extension operations.
2//!
3//! Rules in this module are owned explicitly by [`crate::AdContext`]. They
4//! operate on opaque semantic [`ProgramValue`] tokens and never expose
5//! computegraph node keys or execution-program slots.
6
7use std::collections::HashMap;
8use std::fmt::Debug;
9use std::sync::Arc;
10
11use tenferro_ops::ext_op::ExtensionOp;
12use tenferro_runtime::program::{
13    ProgramBuildError, ProgramValue, SemanticOpRef, SemanticOperationView, SemanticProgramBuilder,
14    SemanticProvenanceView,
15};
16
17/// One optional semantic AD value.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum AdValue {
20    /// This primal, tangent, or cotangent is inactive.
21    Absent,
22    /// Active value owned by the destination semantic-program builder.
23    Value(ProgramValue),
24}
25
26impl AdValue {
27    /// Return the active semantic value, if present.
28    #[must_use]
29    pub const fn value(self) -> Option<ProgramValue> {
30        match self {
31            Self::Absent => None,
32            Self::Value(value) => Some(value),
33        }
34    }
35}
36
37/// Semantic extension AD rule role.
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
39pub enum SemanticAdRuleRole {
40    /// Definitional forward linearization.
41    Linearize,
42    /// Transpose of a linearized extension operation.
43    LinearTranspose,
44    /// Direct reverse rule expressed against primal values.
45    PrimalVjp,
46}
47
48/// Registration failures for semantic extension AD rules.
49#[derive(Debug, thiserror::Error)]
50pub enum SemanticExtensionRegistryError {
51    /// A rule with the same family and role is already present.
52    #[error("semantic extension AD {role:?} rule for family {family_id:?} is already registered")]
53    DuplicateRule {
54        /// Duplicate extension family identifier.
55        family_id: &'static str,
56        /// Duplicate semantic AD role.
57        role: SemanticAdRuleRole,
58    },
59    /// A rule family is not a namespaced, versioned identifier.
60    #[error("semantic extension AD family {family_id:?} is not namespaced and versioned")]
61    MalformedFamilyId {
62        /// Invalid extension family identifier.
63        family_id: &'static str,
64    },
65}
66
67/// Failures while dispatching or building semantic extension AD.
68#[derive(Debug, thiserror::Error)]
69pub enum SemanticAdError {
70    /// The supplied operation is not an extension operation.
71    #[error("semantic AD extension dispatch received a core operation")]
72    CoreOperation,
73    /// Observable effects make implicit differentiation unsafe.
74    #[error("semantic extension family {family_id:?} has observable effects")]
75    EffectfulExtension {
76        /// Effectful extension family.
77        family_id: &'static str,
78    },
79    /// No rule is registered for the requested family and role.
80    #[error("semantic extension family {family_id:?} has no {role:?} AD rule")]
81    MissingRule {
82        /// Extension family without a rule.
83        family_id: &'static str,
84        /// Missing semantic AD role.
85        role: SemanticAdRuleRole,
86    },
87    /// An ordered request or result field has the wrong length.
88    #[error("semantic AD field {field} expects {expected} values, got {actual}")]
89    Arity {
90        /// Name of the invalid ordered field.
91        field: &'static str,
92        /// Required field length.
93        expected: usize,
94        /// Supplied field length.
95        actual: usize,
96    },
97    /// A request or result value belongs to another builder.
98    #[error("semantic AD field {field}[{index}] does not belong to the destination builder")]
99    ForeignValue {
100        /// Name of the invalid ordered field.
101        field: &'static str,
102        /// Index of the foreign value.
103        index: usize,
104    },
105    /// A family-specific rule deliberately rejects this payload.
106    #[error("semantic extension family {family_id:?} does not support {role:?}: {message}")]
107    Unsupported {
108        /// Extension family that rejected the transform.
109        family_id: &'static str,
110        /// Rejected semantic AD role.
111        role: SemanticAdRuleRole,
112        /// Bounded family-specific diagnostic.
113        message: String,
114    },
115    /// A family-specific rule failed with a typed source error.
116    #[error("semantic extension family {family_id:?} {role:?} rule failed: {source}")]
117    Rule {
118        /// Extension family whose rule failed.
119        family_id: &'static str,
120        /// Semantic AD role being evaluated.
121        role: SemanticAdRuleRole,
122        /// Original typed rule failure.
123        #[source]
124        source: Box<dyn std::error::Error + Send + Sync + 'static>,
125    },
126    /// A family-specific semantic rule invariant was violated.
127    #[error("semantic extension family {family_id:?} {role:?} invariant failed: {message}")]
128    Invariant {
129        /// Extension family whose invariant failed.
130        family_id: &'static str,
131        /// Semantic AD role being evaluated.
132        role: SemanticAdRuleRole,
133        /// Bounded invariant diagnostic.
134        message: String,
135    },
136    /// Semantic-program construction failed inside a rule.
137    #[error("semantic extension AD program construction failed: {0}")]
138    Build(#[from] ProgramBuildError),
139}
140
141/// Ordered inputs for one semantic extension linearization rule.
142#[derive(Clone, Copy)]
143pub struct SemanticLinearizeRequest<'a> {
144    op: &'a dyn ExtensionOp,
145    primal_inputs: &'a [ProgramValue],
146    primal_outputs: &'a [ProgramValue],
147    tangent_inputs: &'a [AdValue],
148    active_outputs: &'a [bool],
149    provenance: SemanticProvenanceView<'a>,
150}
151
152impl<'a> SemanticLinearizeRequest<'a> {
153    /// Borrow the extension payload.
154    pub const fn op(self) -> &'a dyn ExtensionOp {
155        self.op
156    }
157
158    /// Borrow ordered destination-local primal inputs.
159    pub const fn primal_inputs(self) -> &'a [ProgramValue] {
160        self.primal_inputs
161    }
162
163    /// Borrow ordered destination-local primal outputs.
164    pub const fn primal_outputs(self) -> &'a [ProgramValue] {
165        self.primal_outputs
166    }
167
168    /// Borrow ordered optional tangent inputs.
169    pub const fn tangent_inputs(self) -> &'a [AdValue] {
170        self.tangent_inputs
171    }
172
173    /// Borrow the ordered active-output mask.
174    pub const fn active_outputs(self) -> &'a [bool] {
175        self.active_outputs
176    }
177
178    /// Return bounded operation provenance.
179    pub const fn provenance(self) -> SemanticProvenanceView<'a> {
180        self.provenance
181    }
182}
183
184/// Output of one semantic extension linearization rule.
185#[derive(Clone, Debug, PartialEq, Eq)]
186pub struct SemanticLinearizeResult {
187    tangent_outputs: Box<[AdValue]>,
188    residuals: Box<[ProgramValue]>,
189}
190
191impl SemanticLinearizeResult {
192    /// Construct ordered tangent outputs and residuals.
193    #[must_use]
194    pub fn new(
195        tangent_outputs: impl IntoIterator<Item = AdValue>,
196        residuals: impl IntoIterator<Item = ProgramValue>,
197    ) -> Self {
198        Self {
199            tangent_outputs: tangent_outputs.into_iter().collect(),
200            residuals: residuals.into_iter().collect(),
201        }
202    }
203
204    /// Borrow ordered optional tangent outputs.
205    pub fn tangent_outputs(&self) -> &[AdValue] {
206        &self.tangent_outputs
207    }
208
209    /// Borrow ordered residual values saved for transpose.
210    pub fn residuals(&self) -> &[ProgramValue] {
211        &self.residuals
212    }
213}
214
215/// Ordered inputs for one semantic linear-transpose rule.
216#[derive(Clone, Copy)]
217pub struct SemanticLinearTransposeRequest<'a> {
218    op: &'a dyn ExtensionOp,
219    primal_inputs: &'a [ProgramValue],
220    primal_outputs: &'a [ProgramValue],
221    cotangent_outputs: &'a [AdValue],
222    active_inputs: &'a [bool],
223    residuals: &'a [ProgramValue],
224    provenance: SemanticProvenanceView<'a>,
225}
226
227impl<'a> SemanticLinearTransposeRequest<'a> {
228    /// Borrow the extension payload.
229    pub const fn op(self) -> &'a dyn ExtensionOp {
230        self.op
231    }
232
233    /// Borrow ordered destination-local primal inputs.
234    pub const fn primal_inputs(self) -> &'a [ProgramValue] {
235        self.primal_inputs
236    }
237
238    /// Borrow ordered destination-local primal outputs.
239    pub const fn primal_outputs(self) -> &'a [ProgramValue] {
240        self.primal_outputs
241    }
242
243    /// Borrow ordered optional output cotangents.
244    pub const fn cotangent_outputs(self) -> &'a [AdValue] {
245        self.cotangent_outputs
246    }
247
248    /// Borrow the ordered active-input mask.
249    pub const fn active_inputs(self) -> &'a [bool] {
250        self.active_inputs
251    }
252
253    /// Borrow ordered residuals produced by linearization.
254    pub const fn residuals(self) -> &'a [ProgramValue] {
255        self.residuals
256    }
257
258    /// Return bounded operation provenance.
259    pub const fn provenance(self) -> SemanticProvenanceView<'a> {
260        self.provenance
261    }
262}
263
264/// Ordered inputs for one direct semantic primal-VJP rule.
265#[derive(Clone, Copy)]
266pub struct SemanticPrimalVjpRequest<'a> {
267    op: &'a dyn ExtensionOp,
268    primal_inputs: &'a [ProgramValue],
269    primal_outputs: &'a [ProgramValue],
270    cotangent_outputs: &'a [AdValue],
271    active_inputs: &'a [bool],
272    provenance: SemanticProvenanceView<'a>,
273}
274
275impl<'a> SemanticPrimalVjpRequest<'a> {
276    /// Borrow the extension payload.
277    pub const fn op(self) -> &'a dyn ExtensionOp {
278        self.op
279    }
280
281    /// Borrow ordered destination-local primal inputs.
282    pub const fn primal_inputs(self) -> &'a [ProgramValue] {
283        self.primal_inputs
284    }
285
286    /// Borrow ordered destination-local primal outputs.
287    pub const fn primal_outputs(self) -> &'a [ProgramValue] {
288        self.primal_outputs
289    }
290
291    /// Borrow ordered optional output cotangents.
292    pub const fn cotangent_outputs(self) -> &'a [AdValue] {
293        self.cotangent_outputs
294    }
295
296    /// Borrow the ordered active-input mask.
297    pub const fn active_inputs(self) -> &'a [bool] {
298        self.active_inputs
299    }
300
301    /// Return bounded operation provenance.
302    pub const fn provenance(self) -> SemanticProvenanceView<'a> {
303        self.provenance
304    }
305}
306
307/// Definitional JVP rule for one extension family.
308pub trait SemanticLinearizeRule: Debug + Send + Sync + 'static {
309    /// Return the versioned extension family handled by this rule.
310    fn family_id(&self) -> &'static str;
311
312    /// Emit ordered tangent outputs and residuals into `builder`.
313    ///
314    /// # Errors
315    ///
316    /// Returns [`SemanticAdError::Unsupported`] when the payload is outside
317    /// the rule's supported domain, or [`SemanticAdError::Build`] when emitted
318    /// semantic operations fail validation.
319    fn linearize(
320        &self,
321        request: SemanticLinearizeRequest<'_>,
322        builder: &mut SemanticProgramBuilder,
323    ) -> Result<SemanticLinearizeResult, SemanticAdError>;
324}
325
326/// Transpose rule for an extension viewed as a linear map.
327pub trait SemanticLinearTransposeRule: Debug + Send + Sync + 'static {
328    /// Return the versioned extension family handled by this rule.
329    fn family_id(&self) -> &'static str;
330
331    /// Emit ordered optional input cotangents into `builder`.
332    ///
333    /// # Errors
334    ///
335    /// Returns [`SemanticAdError::Unsupported`] when the payload is outside
336    /// the rule's supported domain, or [`SemanticAdError::Build`] when emitted
337    /// semantic operations fail validation.
338    fn linear_transpose(
339        &self,
340        request: SemanticLinearTransposeRequest<'_>,
341        builder: &mut SemanticProgramBuilder,
342    ) -> Result<Box<[AdValue]>, SemanticAdError>;
343}
344
345/// Optional direct VJP rule expressed against primal semantic values.
346pub trait SemanticPrimalVjpRule: Debug + Send + Sync + 'static {
347    /// Return the versioned extension family handled by this rule.
348    fn family_id(&self) -> &'static str;
349
350    /// Emit ordered optional input cotangents into `builder`.
351    ///
352    /// # Errors
353    ///
354    /// Returns [`SemanticAdError::Unsupported`] when the payload is outside
355    /// the rule's supported domain, or [`SemanticAdError::Build`] when emitted
356    /// semantic operations fail validation.
357    fn primal_vjp(
358        &self,
359        request: SemanticPrimalVjpRequest<'_>,
360        builder: &mut SemanticProgramBuilder,
361    ) -> Result<Box<[AdValue]>, SemanticAdError>;
362}
363
364type LinearizeMap = HashMap<&'static str, Arc<dyn SemanticLinearizeRule>>;
365type LinearTransposeMap = HashMap<&'static str, Arc<dyn SemanticLinearTransposeRule>>;
366type PrimalVjpMap = HashMap<&'static str, Arc<dyn SemanticPrimalVjpRule>>;
367
368/// Explicit clone-on-write set of semantic extension AD rules.
369#[derive(Clone, Default)]
370pub struct SemanticExtensionRuleSet {
371    linearize: Arc<LinearizeMap>,
372    linear_transpose: Arc<LinearTransposeMap>,
373    primal_vjp: Arc<PrimalVjpMap>,
374}
375
376impl Debug for SemanticExtensionRuleSet {
377    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378        let mut linearize: Vec<_> = self.linearize.keys().copied().collect();
379        let mut linear_transpose: Vec<_> = self.linear_transpose.keys().copied().collect();
380        let mut primal_vjp: Vec<_> = self.primal_vjp.keys().copied().collect();
381        linearize.sort_unstable();
382        linear_transpose.sort_unstable();
383        primal_vjp.sort_unstable();
384        formatter
385            .debug_struct("SemanticExtensionRuleSet")
386            .field("linearize", &linearize)
387            .field("linear_transpose", &linear_transpose)
388            .field("primal_vjp", &primal_vjp)
389            .finish()
390    }
391}
392
393impl SemanticExtensionRuleSet {
394    /// Construct an empty rule set.
395    #[must_use]
396    pub fn new() -> Self {
397        Self::default()
398    }
399
400    /// Register one semantic linearize rule.
401    ///
402    /// # Errors
403    ///
404    /// Returns [`SemanticExtensionRegistryError::MalformedFamilyId`] for an
405    /// invalid family or [`SemanticExtensionRegistryError::DuplicateRule`] for
406    /// an existing family in this role.
407    pub fn register_linearize(
408        &mut self,
409        rule: Arc<dyn SemanticLinearizeRule>,
410    ) -> Result<(), SemanticExtensionRegistryError> {
411        validate_insert(
412            &self.linearize,
413            rule.family_id(),
414            SemanticAdRuleRole::Linearize,
415        )?;
416        Arc::make_mut(&mut self.linearize).insert(rule.family_id(), rule);
417        Ok(())
418    }
419
420    /// Register one semantic linear-transpose rule.
421    ///
422    /// # Errors
423    ///
424    /// Returns [`SemanticExtensionRegistryError::MalformedFamilyId`] for an
425    /// invalid family or [`SemanticExtensionRegistryError::DuplicateRule`] for
426    /// an existing family in this role.
427    pub fn register_linear_transpose(
428        &mut self,
429        rule: Arc<dyn SemanticLinearTransposeRule>,
430    ) -> Result<(), SemanticExtensionRegistryError> {
431        validate_insert(
432            &self.linear_transpose,
433            rule.family_id(),
434            SemanticAdRuleRole::LinearTranspose,
435        )?;
436        Arc::make_mut(&mut self.linear_transpose).insert(rule.family_id(), rule);
437        Ok(())
438    }
439
440    /// Register one direct semantic primal-VJP rule.
441    ///
442    /// # Errors
443    ///
444    /// Returns [`SemanticExtensionRegistryError::MalformedFamilyId`] for an
445    /// invalid family or [`SemanticExtensionRegistryError::DuplicateRule`] for
446    /// an existing family in this role.
447    pub fn register_primal_vjp(
448        &mut self,
449        rule: Arc<dyn SemanticPrimalVjpRule>,
450    ) -> Result<(), SemanticExtensionRegistryError> {
451        validate_insert(
452            &self.primal_vjp,
453            rule.family_id(),
454            SemanticAdRuleRole::PrimalVjp,
455        )?;
456        Arc::make_mut(&mut self.primal_vjp).insert(rule.family_id(), rule);
457        Ok(())
458    }
459
460    /// Return a rule set containing one semantic linearize rule.
461    ///
462    /// # Errors
463    ///
464    /// Returns [`SemanticExtensionRegistryError::MalformedFamilyId`] for an
465    /// invalid family or [`SemanticExtensionRegistryError::DuplicateRule`] for
466    /// an existing linearize rule.
467    pub fn with_linearize(
468        mut self,
469        rule: Arc<dyn SemanticLinearizeRule>,
470    ) -> Result<Self, SemanticExtensionRegistryError> {
471        self.register_linearize(rule)?;
472        Ok(self)
473    }
474
475    /// Return a rule set containing one semantic linear-transpose rule.
476    ///
477    /// # Errors
478    ///
479    /// Returns [`SemanticExtensionRegistryError::MalformedFamilyId`] for an
480    /// invalid family or [`SemanticExtensionRegistryError::DuplicateRule`] for
481    /// an existing linear-transpose rule.
482    pub fn with_linear_transpose(
483        mut self,
484        rule: Arc<dyn SemanticLinearTransposeRule>,
485    ) -> Result<Self, SemanticExtensionRegistryError> {
486        self.register_linear_transpose(rule)?;
487        Ok(self)
488    }
489
490    /// Return a rule set containing one direct semantic primal-VJP rule.
491    ///
492    /// # Errors
493    ///
494    /// Returns [`SemanticExtensionRegistryError::MalformedFamilyId`] for an
495    /// invalid family or [`SemanticExtensionRegistryError::DuplicateRule`] for
496    /// an existing primal-VJP rule.
497    pub fn with_primal_vjp(
498        mut self,
499        rule: Arc<dyn SemanticPrimalVjpRule>,
500    ) -> Result<Self, SemanticExtensionRegistryError> {
501        self.register_primal_vjp(rule)?;
502        Ok(self)
503    }
504
505    /// Merge another rule set atomically.
506    ///
507    /// # Errors
508    ///
509    /// Returns [`SemanticExtensionRegistryError::MalformedFamilyId`] for an
510    /// invalid family or [`SemanticExtensionRegistryError::DuplicateRule`] for
511    /// a role-equivalent duplicate. The receiver is unchanged on failure.
512    pub fn merge(&mut self, other: Self) -> Result<(), SemanticExtensionRegistryError> {
513        let mut candidate = self.clone();
514        for rule in other.linearize.values() {
515            candidate.register_linearize(Arc::clone(rule))?;
516        }
517        for rule in other.linear_transpose.values() {
518            candidate.register_linear_transpose(Arc::clone(rule))?;
519        }
520        for rule in other.primal_vjp.values() {
521            candidate.register_primal_vjp(Arc::clone(rule))?;
522        }
523        *self = candidate;
524        Ok(())
525    }
526
527    /// Look up a semantic linearize rule by extension family.
528    #[must_use]
529    pub fn lookup_linearize(&self, family_id: &str) -> Option<Arc<dyn SemanticLinearizeRule>> {
530        self.linearize.get(family_id).cloned()
531    }
532
533    /// Look up a semantic linear-transpose rule by extension family.
534    #[must_use]
535    pub fn lookup_linear_transpose(
536        &self,
537        family_id: &str,
538    ) -> Option<Arc<dyn SemanticLinearTransposeRule>> {
539        self.linear_transpose.get(family_id).cloned()
540    }
541
542    /// Look up a direct semantic primal-VJP rule by extension family.
543    #[must_use]
544    pub fn lookup_primal_vjp(&self, family_id: &str) -> Option<Arc<dyn SemanticPrimalVjpRule>> {
545        self.primal_vjp.get(family_id).cloned()
546    }
547
548    /// Validate and dispatch one semantic extension linearization.
549    ///
550    /// # Errors
551    ///
552    /// Returns [`SemanticAdError::CoreOperation`] for a core operation,
553    /// [`SemanticAdError::EffectfulExtension`] before rule dispatch for an
554    /// effectful extension, or typed rule/arity/ownership/build failures.
555    #[allow(clippy::too_many_arguments)]
556    pub fn linearize_operation(
557        &self,
558        operation: SemanticOperationView<'_>,
559        primal_inputs: &[ProgramValue],
560        primal_outputs: &[ProgramValue],
561        tangent_inputs: &[AdValue],
562        active_outputs: &[bool],
563        builder: &mut SemanticProgramBuilder,
564    ) -> Result<SemanticLinearizeResult, SemanticAdError> {
565        let op = extension_for_dispatch(operation)?;
566        validate_operation_inputs(operation, primal_inputs, primal_outputs, builder)?;
567        validate_len("tangent_inputs", op.input_count(), tangent_inputs.len())?;
568        validate_len("active_outputs", op.output_count(), active_outputs.len())?;
569        validate_ad_values("tangent_inputs", tangent_inputs, builder)?;
570        let rule = self
571            .lookup_linearize(op.family_id())
572            .ok_or(SemanticAdError::MissingRule {
573                family_id: op.family_id(),
574                role: SemanticAdRuleRole::Linearize,
575            })?;
576        let result = rule.linearize(
577            SemanticLinearizeRequest {
578                op,
579                primal_inputs,
580                primal_outputs,
581                tangent_inputs,
582                active_outputs,
583                provenance: operation.provenance(),
584            },
585            builder,
586        )?;
587        validate_len(
588            "tangent_outputs",
589            op.output_count(),
590            result.tangent_outputs.len(),
591        )?;
592        validate_ad_values("tangent_outputs", &result.tangent_outputs, builder)?;
593        validate_values("residuals", &result.residuals, builder)?;
594        Ok(result)
595    }
596
597    /// Validate and dispatch one semantic extension linear transpose.
598    ///
599    /// # Errors
600    ///
601    /// Returns [`SemanticAdError::EffectfulExtension`] before rule dispatch,
602    /// [`SemanticAdError::MissingRule`] when no transpose rule exists,
603    /// [`SemanticAdError::Arity`] / [`SemanticAdError::ForeignValue`] for an
604    /// invalid request or result, or a typed family-rule failure.
605    #[allow(clippy::too_many_arguments)]
606    pub fn linear_transpose_operation(
607        &self,
608        operation: SemanticOperationView<'_>,
609        primal_inputs: &[ProgramValue],
610        primal_outputs: &[ProgramValue],
611        cotangent_outputs: &[AdValue],
612        active_inputs: &[bool],
613        residuals: &[ProgramValue],
614        builder: &mut SemanticProgramBuilder,
615    ) -> Result<Box<[AdValue]>, SemanticAdError> {
616        let op = extension_for_dispatch(operation)?;
617        validate_operation_inputs(operation, primal_inputs, primal_outputs, builder)?;
618        validate_len(
619            "cotangent_outputs",
620            op.output_count(),
621            cotangent_outputs.len(),
622        )?;
623        validate_len("active_inputs", op.input_count(), active_inputs.len())?;
624        validate_ad_values("cotangent_outputs", cotangent_outputs, builder)?;
625        validate_values("residuals", residuals, builder)?;
626        let rule =
627            self.lookup_linear_transpose(op.family_id())
628                .ok_or(SemanticAdError::MissingRule {
629                    family_id: op.family_id(),
630                    role: SemanticAdRuleRole::LinearTranspose,
631                })?;
632        let result = rule.linear_transpose(
633            SemanticLinearTransposeRequest {
634                op,
635                primal_inputs,
636                primal_outputs,
637                cotangent_outputs,
638                active_inputs,
639                residuals,
640                provenance: operation.provenance(),
641            },
642            builder,
643        )?;
644        validate_len("cotangent_inputs", op.input_count(), result.len())?;
645        validate_ad_values("cotangent_inputs", &result, builder)?;
646        Ok(result)
647    }
648
649    /// Validate and dispatch one direct semantic primal VJP.
650    ///
651    /// # Errors
652    ///
653    /// Returns [`SemanticAdError::EffectfulExtension`] before rule dispatch,
654    /// [`SemanticAdError::MissingRule`] when no primal-VJP rule exists,
655    /// [`SemanticAdError::Arity`] / [`SemanticAdError::ForeignValue`] for an
656    /// invalid request or result, or a typed family-rule failure.
657    #[allow(clippy::too_many_arguments)]
658    pub fn primal_vjp_operation(
659        &self,
660        operation: SemanticOperationView<'_>,
661        primal_inputs: &[ProgramValue],
662        primal_outputs: &[ProgramValue],
663        cotangent_outputs: &[AdValue],
664        active_inputs: &[bool],
665        builder: &mut SemanticProgramBuilder,
666    ) -> Result<Box<[AdValue]>, SemanticAdError> {
667        let op = extension_for_dispatch(operation)?;
668        validate_operation_inputs(operation, primal_inputs, primal_outputs, builder)?;
669        validate_len(
670            "cotangent_outputs",
671            op.output_count(),
672            cotangent_outputs.len(),
673        )?;
674        validate_len("active_inputs", op.input_count(), active_inputs.len())?;
675        validate_ad_values("cotangent_outputs", cotangent_outputs, builder)?;
676        let rule = self
677            .lookup_primal_vjp(op.family_id())
678            .ok_or(SemanticAdError::MissingRule {
679                family_id: op.family_id(),
680                role: SemanticAdRuleRole::PrimalVjp,
681            })?;
682        let result = rule.primal_vjp(
683            SemanticPrimalVjpRequest {
684                op,
685                primal_inputs,
686                primal_outputs,
687                cotangent_outputs,
688                active_inputs,
689                provenance: operation.provenance(),
690            },
691            builder,
692        )?;
693        validate_len("cotangent_inputs", op.input_count(), result.len())?;
694        validate_ad_values("cotangent_inputs", &result, builder)?;
695        Ok(result)
696    }
697}
698
699fn extension_for_dispatch(
700    operation: SemanticOperationView<'_>,
701) -> Result<&dyn ExtensionOp, SemanticAdError> {
702    let SemanticOpRef::Extension(op) = operation.op() else {
703        return Err(SemanticAdError::CoreOperation);
704    };
705    if !operation.effects().is_empty() {
706        return Err(SemanticAdError::EffectfulExtension {
707            family_id: op.family_id(),
708        });
709    }
710    Ok(op)
711}
712
713fn validate_operation_inputs(
714    operation: SemanticOperationView<'_>,
715    primal_inputs: &[ProgramValue],
716    primal_outputs: &[ProgramValue],
717    builder: &SemanticProgramBuilder,
718) -> Result<(), SemanticAdError> {
719    validate_len(
720        "primal_inputs",
721        operation.inputs().len(),
722        primal_inputs.len(),
723    )?;
724    validate_len(
725        "primal_outputs",
726        operation.outputs().len(),
727        primal_outputs.len(),
728    )?;
729    validate_values("primal_inputs", primal_inputs, builder)?;
730    validate_values("primal_outputs", primal_outputs, builder)
731}
732
733fn validate_values(
734    field: &'static str,
735    values: &[ProgramValue],
736    builder: &SemanticProgramBuilder,
737) -> Result<(), SemanticAdError> {
738    for (index, value) in values.iter().copied().enumerate() {
739        if builder.validate_value(value).is_err() {
740            return Err(SemanticAdError::ForeignValue { field, index });
741        }
742    }
743    Ok(())
744}
745
746fn validate_ad_values(
747    field: &'static str,
748    values: &[AdValue],
749    builder: &SemanticProgramBuilder,
750) -> Result<(), SemanticAdError> {
751    for (index, value) in values.iter().copied().enumerate() {
752        if let AdValue::Value(value) = value {
753            if builder.validate_value(value).is_err() {
754                return Err(SemanticAdError::ForeignValue { field, index });
755            }
756        }
757    }
758    Ok(())
759}
760
761fn validate_len(
762    field: &'static str,
763    expected: usize,
764    actual: usize,
765) -> Result<(), SemanticAdError> {
766    if expected != actual {
767        return Err(SemanticAdError::Arity {
768            field,
769            expected,
770            actual,
771        });
772    }
773    Ok(())
774}
775
776fn validate_insert<T>(
777    map: &HashMap<&'static str, T>,
778    family_id: &'static str,
779    role: SemanticAdRuleRole,
780) -> Result<(), SemanticExtensionRegistryError> {
781    if !is_valid_family_id(family_id) {
782        return Err(SemanticExtensionRegistryError::MalformedFamilyId { family_id });
783    }
784    if map.contains_key(family_id) {
785        return Err(SemanticExtensionRegistryError::DuplicateRule { family_id, role });
786    }
787    Ok(())
788}
789
790fn is_valid_family_id(family_id: &str) -> bool {
791    let Some((prefix, version)) = family_id.rsplit_once('.') else {
792        return false;
793    };
794    let Some(version) = version.strip_prefix('v') else {
795        return false;
796    };
797    let Some((crate_name, op_name)) = prefix.split_once('.') else {
798        return false;
799    };
800    !crate_name.is_empty()
801        && !op_name.is_empty()
802        && !version.is_empty()
803        && version.bytes().all(|byte| byte.is_ascii_digit())
804        && crate_name.is_ascii()
805        && op_name.is_ascii()
806        && !crate_name.chars().any(char::is_whitespace)
807        && !op_name.chars().any(char::is_whitespace)
808}