Skip to main content

tenferro_ad/
semantic_transform.rs

1//! Whole-program automatic differentiation over semantic SSA programs.
2
3mod core_dynamic;
4mod core_indexing;
5mod core_reductions;
6mod core_structural;
7
8use std::collections::{HashMap, HashSet};
9
10use tenferro_ops::{dim_expr::DimExpr, ShapeExtent};
11use tenferro_runtime::program::{
12    CoreSemanticOp, FrozenProgram, ProgramBuildError, ProgramFinishError, ProgramImport,
13    ProgramInputSpec, ProgramQueryError, ProgramValue, ProgramValueMetadata, SemanticOpRef,
14    SemanticProgramBuilder,
15};
16use tenferro_runtime::{CompareDir, DType, DotGeneralConfig};
17
18use crate::semantic_extension::{AdValue, SemanticAdError, SemanticExtensionRuleSet};
19use core_dynamic::{dynamic_shape_vjp, linearize_dynamic_shape};
20use core_indexing::{indexing_vjp, linearize_indexing};
21use core_reductions::{linearize_nonlinear_reduction, nonlinear_reduction_vjp};
22use core_structural::{concatenate_vjp, linearize_concatenate, pad_vjp, slice_vjp};
23
24/// Semantic AD transform role used by typed diagnostics.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum SemanticTransformRole {
27    /// Forward-mode linearization.
28    Jvp,
29    /// Reverse-mode transposition.
30    Vjp,
31}
32
33/// Failures produced by whole-program semantic AD.
34#[derive(Debug, thiserror::Error)]
35pub enum SemanticAdTransformError {
36    /// An activity mask did not match the corresponding ordered value list.
37    #[error("semantic {role:?} {field} expects {expected} entries, got {actual}")]
38    ActivityArity {
39        /// Transform role.
40        role: SemanticTransformRole,
41        /// Invalid mask.
42        field: &'static str,
43        /// Required entry count.
44        expected: usize,
45        /// Supplied entry count.
46        actual: usize,
47    },
48    /// An active core operation has not yet been admitted to semantic AD.
49    #[error("semantic {role:?} does not support active core operation {op}")]
50    UnsupportedCore {
51        /// Transform role.
52        role: SemanticTransformRole,
53        /// Bounded operation diagnostic.
54        op: String,
55    },
56    /// A future semantic operation variant is unknown to this transform.
57    #[error("semantic {role:?} does not support this semantic operation variant")]
58    UnsupportedOperationVariant {
59        /// Transform role.
60        role: SemanticTransformRole,
61    },
62    /// Active derivative metadata is outside the admitted exact-shape subset.
63    #[error("semantic {role:?} does not support derivative metadata: {message}")]
64    UnsupportedMetadata {
65        /// Transform role.
66        role: SemanticTransformRole,
67        /// Bounded metadata diagnostic.
68        message: String,
69    },
70    /// Source-program metadata could not be queried.
71    #[error("semantic AD source-program query failed: {0}")]
72    Query(#[from] ProgramQueryError),
73    /// Destination semantic-program construction failed.
74    #[error("semantic AD program construction failed: {0}")]
75    Build(#[from] ProgramBuildError),
76    /// An extension-owned semantic AD rule failed.
77    #[error("semantic extension AD failed: {0}")]
78    Extension(#[from] SemanticAdError),
79    /// The transformed program could not be frozen atomically.
80    #[error("semantic AD program finalization failed: {0}")]
81    Finish(#[from] ProgramFinishError),
82    /// The semantic AD transform cache could not be accessed.
83    #[error("semantic AD transform cache failed: {0}")]
84    Cache(#[source] tenferro_runtime::Error),
85}
86
87/// One frozen derivative program plus ordered derivative input/output maps.
88///
89/// Original primal inputs retain their source order. Active derivative seed
90/// inputs are appended in source order. Program outputs contain only active
91/// derivative values, also in source order; `None` records an inactive value.
92#[derive(Clone, Debug)]
93pub struct SemanticAdProgram {
94    frozen: FrozenProgram,
95    derivative_input_indices: Box<[Option<usize>]>,
96    derivative_output_indices: Box<[Option<usize>]>,
97}
98
99struct ValueShapePlan {
100    shape: Vec<DimExpr>,
101    dynamic_axes: Vec<usize>,
102}
103
104impl SemanticAdProgram {
105    /// Borrow the frozen derivative program.
106    pub const fn frozen(&self) -> &FrozenProgram {
107        &self.frozen
108    }
109
110    /// Return transformed-program input indices for ordered derivative seeds.
111    pub fn derivative_input_indices(&self) -> &[Option<usize>] {
112        &self.derivative_input_indices
113    }
114
115    /// Return transformed-program output indices for ordered derivatives.
116    pub fn derivative_output_indices(&self) -> &[Option<usize>] {
117        &self.derivative_output_indices
118    }
119
120    /// Consume this result and return the frozen derivative program.
121    pub fn into_frozen(self) -> FrozenProgram {
122        self.frozen
123    }
124
125    pub(crate) fn with_input_prefix_bindings_from(
126        &self,
127        source: &FrozenProgram,
128    ) -> Result<Self, ProgramFinishError> {
129        Ok(Self {
130            frozen: self.frozen.with_input_prefix_bindings_from(source)?,
131            derivative_input_indices: self.derivative_input_indices.clone(),
132            derivative_output_indices: self.derivative_output_indices.clone(),
133        })
134    }
135}
136
137/// Build a forward-mode derivative program.
138///
139/// `active_inputs` follows source-program input order. Each active input gets
140/// one appended tangent seed. The result maps source outputs to compact
141/// derivative-program outputs.
142///
143/// # Errors
144///
145/// Returns [`SemanticAdTransformError::ActivityArity`] for a mask-length
146/// mismatch, [`SemanticAdTransformError::Extension`] for a rejected extension
147/// rule, or the corresponding `Query`, `Build`, `Finish`, or unsupported
148/// operation/metadata variant for failures encountered during transformation.
149pub fn semantic_jvp(
150    input: &FrozenProgram,
151    active_inputs: &[bool],
152    rules: &SemanticExtensionRuleSet,
153) -> Result<SemanticAdProgram, SemanticAdTransformError> {
154    validate_activity(
155        SemanticTransformRole::Jvp,
156        "active_inputs",
157        input.program.inputs().len(),
158        active_inputs.len(),
159    )?;
160    let mut builder = SemanticProgramBuilder::new();
161    let values = import_source(input, &mut builder)?;
162    let mut tangents = HashMap::new();
163    let mut derivative_input_indices = vec![None; input.program.inputs().len()];
164    let mut next_input = input.program.inputs().len();
165    for (index, source) in input.program.inputs().iter().copied().enumerate() {
166        if active_inputs[index] {
167            let imported_source = values[&source];
168            let tangent = builder.input(ProgramInputSpec::from_metadata(
169                builder.value_metadata(imported_source)?.clone(),
170            ))?;
171            derivative_input_indices[index] = Some(next_input);
172            next_input += 1;
173            tangents.insert(source, AdValue::Value(tangent));
174        } else {
175            tangents.insert(source, AdValue::Absent);
176        }
177    }
178
179    let live = source_output_liveness(input);
180    for operation in input.program.operations() {
181        let tangent_inputs: Vec<_> = operation
182            .inputs()
183            .iter()
184            .map(|value| tangents.get(value).copied().unwrap_or(AdValue::Absent))
185            .collect();
186        let active_outputs: Vec<_> = operation
187            .outputs()
188            .iter()
189            .map(|value| live.contains(value))
190            .collect();
191        let tangent_outputs = if tangent_inputs
192            .iter()
193            .all(|value| matches!(value, AdValue::Absent))
194        {
195            vec![AdValue::Absent; operation.outputs().len()].into_boxed_slice()
196        } else {
197            match operation.op() {
198                SemanticOpRef::Extension(_) => rules
199                    .linearize_operation(
200                        operation,
201                        &mapped_values(operation.inputs(), &values),
202                        &mapped_values(operation.outputs(), &values),
203                        &tangent_inputs,
204                        &active_outputs,
205                        &mut builder,
206                    )?
207                    .tangent_outputs()
208                    .into(),
209                SemanticOpRef::Core(op) => linearize_core(
210                    op,
211                    &mapped_values(operation.inputs(), &values),
212                    &tangent_inputs,
213                    &mut builder,
214                )?,
215                _ => {
216                    return Err(SemanticAdTransformError::UnsupportedOperationVariant {
217                        role: SemanticTransformRole::Jvp,
218                    });
219                }
220            }
221        };
222        for (source, tangent) in operation.outputs().iter().copied().zip(tangent_outputs) {
223            tangents.insert(source, tangent);
224        }
225    }
226
227    let outputs = input
228        .program
229        .outputs()
230        .iter()
231        .map(|value| tangents.get(value).copied().unwrap_or(AdValue::Absent))
232        .collect();
233    finish_derivative(builder, derivative_input_indices, outputs)
234}
235
236/// Build a reverse-mode derivative program using extension semantic rules.
237///
238/// `active_inputs` selects requested source-input cotangents and
239/// `active_outputs` selects source outputs that receive appended cotangent
240/// seeds. Both masks follow source order.
241///
242/// # Errors
243///
244/// Returns [`SemanticAdTransformError::ActivityArity`] for a mask-length
245/// mismatch, [`SemanticAdTransformError::Extension`] for a rejected extension
246/// rule, or the corresponding `Query`, `Build`, `Finish`, or unsupported
247/// operation/metadata variant for failures encountered during transformation.
248pub fn semantic_vjp(
249    input: &FrozenProgram,
250    active_inputs: &[bool],
251    active_outputs: &[bool],
252    rules: &SemanticExtensionRuleSet,
253) -> Result<SemanticAdProgram, SemanticAdTransformError> {
254    semantic_vjp_with_saved_outputs(input, active_inputs, active_outputs, rules, &[])
255        .map(|(program, _)| program)
256}
257
258// Execution-only specialization: saved primal values are coefficients of the
259// VJP, not new differentiation targets. Keep the unspecialized derivative
260// program for functional gradients so higher-order AD retains their producers.
261pub(crate) fn semantic_vjp_with_saved_outputs(
262    input: &FrozenProgram,
263    active_inputs: &[bool],
264    active_outputs: &[bool],
265    rules: &SemanticExtensionRuleSet,
266    saved_outputs: &[usize],
267) -> Result<(SemanticAdProgram, Vec<usize>), SemanticAdTransformError> {
268    validate_activity(
269        SemanticTransformRole::Vjp,
270        "active_inputs",
271        input.program.inputs().len(),
272        active_inputs.len(),
273    )?;
274    validate_activity(
275        SemanticTransformRole::Vjp,
276        "active_outputs",
277        input.program.outputs().len(),
278        active_outputs.len(),
279    )?;
280    let mut builder = SemanticProgramBuilder::new();
281    let mut values = import_source(input, &mut builder)?;
282    let forward_active = requested_input_reachability(input, active_inputs);
283    let mut cotangents = HashMap::new();
284    let mut derivative_input_indices = vec![None; input.program.outputs().len()];
285    let mut next_input = input.program.inputs().len();
286    for (index, source) in input.program.outputs().iter().copied().enumerate() {
287        if active_outputs[index] {
288            let imported_source = values[&source];
289            let cotangent = builder.input(ProgramInputSpec::from_metadata(
290                builder.value_metadata(imported_source)?.clone(),
291            ))?;
292            derivative_input_indices[index] = Some(next_input);
293            next_input += 1;
294            accumulate_cotangent(&mut builder, &mut cotangents, source, cotangent)?;
295        }
296    }
297
298    let mut saved_input_indices = Vec::with_capacity(saved_outputs.len());
299    for &output_index in saved_outputs {
300        // INVARIANT: the eager caller supplies indices of the residual roots it
301        // appended to this source program, never user-provided indices.
302        let source = input.program.outputs()[output_index];
303        let imported = values[&source];
304        let saved = builder.input(ProgramInputSpec::from_metadata(
305            builder.value_metadata(imported)?.clone(),
306        ))?;
307        saved_input_indices.push(next_input);
308        next_input += 1;
309        values.insert(source, saved);
310    }
311
312    let operations: Vec<_> = input.program.operations().collect();
313    for operation in operations.into_iter().rev() {
314        let cotangent_outputs: Vec<_> = operation
315            .outputs()
316            .iter()
317            .map(|value| {
318                cotangents
319                    .get(value)
320                    .copied()
321                    .map_or(AdValue::Absent, AdValue::Value)
322            })
323            .collect();
324        if cotangent_outputs
325            .iter()
326            .all(|value| matches!(value, AdValue::Absent))
327        {
328            continue;
329        }
330        let active_operation_inputs: Vec<_> = operation
331            .inputs()
332            .iter()
333            .map(|value| forward_active.contains(value))
334            .collect();
335        if active_operation_inputs.iter().all(|active| !active) {
336            continue;
337        }
338        let cotangent_inputs = match operation.op() {
339            SemanticOpRef::Extension(op) => {
340                if rules.lookup_primal_vjp(op.family_id()).is_some() {
341                    rules.primal_vjp_operation(
342                        operation,
343                        &mapped_values(operation.inputs(), &values),
344                        &mapped_values(operation.outputs(), &values),
345                        &cotangent_outputs,
346                        &active_operation_inputs,
347                        &mut builder,
348                    )?
349                } else {
350                    let inactive_tangents = vec![AdValue::Absent; operation.inputs().len()];
351                    let active_operation_outputs: Vec<_> = cotangent_outputs
352                        .iter()
353                        .map(|value| matches!(value, AdValue::Value(_)))
354                        .collect();
355                    let linearized = rules.linearize_operation(
356                        operation,
357                        &mapped_values(operation.inputs(), &values),
358                        &mapped_values(operation.outputs(), &values),
359                        &inactive_tangents,
360                        &active_operation_outputs,
361                        &mut builder,
362                    )?;
363                    rules.linear_transpose_operation(
364                        operation,
365                        &mapped_values(operation.inputs(), &values),
366                        &mapped_values(operation.outputs(), &values),
367                        &cotangent_outputs,
368                        &active_operation_inputs,
369                        linearized.residuals(),
370                        &mut builder,
371                    )?
372                }
373            }
374            SemanticOpRef::Core(op) => vjp_core(
375                op,
376                &mapped_values(operation.inputs(), &values),
377                &mapped_values(operation.outputs(), &values),
378                &cotangent_outputs,
379                &active_operation_inputs,
380                &mut builder,
381            )?,
382            _ => {
383                return Err(SemanticAdTransformError::UnsupportedOperationVariant {
384                    role: SemanticTransformRole::Vjp,
385                });
386            }
387        };
388        for (source, cotangent) in operation.inputs().iter().copied().zip(cotangent_inputs) {
389            if let AdValue::Value(cotangent) = cotangent {
390                accumulate_cotangent(&mut builder, &mut cotangents, source, cotangent)?;
391            }
392        }
393    }
394
395    let outputs = input
396        .program
397        .inputs()
398        .iter()
399        .enumerate()
400        .map(|(index, value)| {
401            if active_inputs[index] {
402                cotangents
403                    .get(value)
404                    .copied()
405                    .map_or(AdValue::Absent, AdValue::Value)
406            } else {
407                AdValue::Absent
408            }
409        })
410        .collect();
411    Ok((
412        finish_derivative(builder, derivative_input_indices, outputs)?,
413        saved_input_indices,
414    ))
415}
416
417fn import_source(
418    input: &FrozenProgram,
419    builder: &mut SemanticProgramBuilder,
420) -> Result<HashMap<ProgramValue, ProgramValue>, SemanticAdTransformError> {
421    let mut source_values = input.program.inputs().to_vec();
422    source_values.extend(
423        input
424            .program
425            .operations()
426            .flat_map(|operation| operation.outputs().iter().copied()),
427    );
428    let imported = builder.import(ProgramImport {
429        program: input.program.as_ref(),
430        bindings: &input.bindings,
431        roots: &source_values,
432    })?;
433    Ok(source_values
434        .into_iter()
435        .zip(imported.roots().iter().copied())
436        .collect())
437}
438
439fn mapped_values(
440    source: &[ProgramValue],
441    values: &HashMap<ProgramValue, ProgramValue>,
442) -> Vec<ProgramValue> {
443    source.iter().map(|value| values[value]).collect()
444}
445
446fn source_output_liveness(input: &FrozenProgram) -> HashSet<ProgramValue> {
447    let mut live: HashSet<_> = input.program.outputs().iter().copied().collect();
448    let operations: Vec<_> = input.program.operations().collect();
449    for operation in operations.into_iter().rev() {
450        if operation
451            .outputs()
452            .iter()
453            .any(|output| live.contains(output))
454        {
455            live.extend(operation.inputs().iter().copied());
456        }
457    }
458    live
459}
460
461fn requested_input_reachability(
462    input: &FrozenProgram,
463    active_inputs: &[bool],
464) -> HashSet<ProgramValue> {
465    let mut active: HashSet<_> = input
466        .program
467        .inputs()
468        .iter()
469        .copied()
470        .zip(active_inputs.iter().copied())
471        .filter_map(|(value, is_active)| is_active.then_some(value))
472        .collect();
473    for operation in input.program.operations() {
474        if operation
475            .inputs()
476            .iter()
477            .any(|value| active.contains(value))
478        {
479            active.extend(operation.outputs().iter().copied());
480        }
481    }
482    active
483}
484
485fn accumulate_cotangent(
486    builder: &mut SemanticProgramBuilder,
487    cotangents: &mut HashMap<ProgramValue, ProgramValue>,
488    source: ProgramValue,
489    cotangent: ProgramValue,
490) -> Result<(), ProgramBuildError> {
491    let combined = if let Some(existing) = cotangents.get(&source).copied() {
492        builder.add_op(CoreSemanticOp::Add, &[existing, cotangent])?[0]
493    } else {
494        cotangent
495    };
496    cotangents.insert(source, combined);
497    Ok(())
498}
499
500fn linearize_core(
501    op: &CoreSemanticOp,
502    primal_inputs: &[ProgramValue],
503    tangent_inputs: &[AdValue],
504    builder: &mut SemanticProgramBuilder,
505) -> Result<Box<[AdValue]>, SemanticAdTransformError> {
506    let output = match op {
507        CoreSemanticOp::Add => add_ad_values(builder, tangent_inputs[0], tangent_inputs[1])?,
508        CoreSemanticOp::Sub => sub_ad_values(builder, tangent_inputs[0], tangent_inputs[1])?,
509        CoreSemanticOp::Mul => {
510            let lhs = multiply_ad_value(builder, tangent_inputs[0], primal_inputs[1])?;
511            let rhs = multiply_ad_value(builder, tangent_inputs[1], primal_inputs[0])?;
512            add_ad_values(builder, lhs, rhs)?
513        }
514        CoreSemanticOp::Div => {
515            let lhs = divide_ad_value(builder, tangent_inputs[0], primal_inputs[1])?;
516            let rhs_numerator = multiply_ad_value(builder, tangent_inputs[1], primal_inputs[0])?;
517            let denominator =
518                builder.add_op(CoreSemanticOp::Mul, &[primal_inputs[1], primal_inputs[1]])?[0];
519            let rhs = divide_ad_value(builder, rhs_numerator, denominator)?;
520            sub_ad_values(builder, lhs, rhs)?
521        }
522        CoreSemanticOp::Pow => {
523            let lhs = if matches!(tangent_inputs[0], AdValue::Value(_)) {
524                let one = one_like(builder, primal_inputs[1], SemanticTransformRole::Jvp)?;
525                let exponent_minus_one =
526                    builder.add_op(CoreSemanticOp::Sub, &[primal_inputs[1], one])?[0];
527                let power = builder
528                    .add_op(CoreSemanticOp::Pow, &[primal_inputs[0], exponent_minus_one])?[0];
529                let coefficient =
530                    builder.add_op(CoreSemanticOp::Mul, &[primal_inputs[1], power])?[0];
531                multiply_ad_value(builder, tangent_inputs[0], coefficient)?
532            } else {
533                AdValue::Absent
534            };
535            let rhs = if matches!(tangent_inputs[1], AdValue::Value(_)) {
536                let log = builder.add_op(CoreSemanticOp::Log, &[primal_inputs[0]])?[0];
537                let power =
538                    builder.add_op(CoreSemanticOp::Pow, &[primal_inputs[0], primal_inputs[1]])?[0];
539                let coefficient = builder.add_op(CoreSemanticOp::Mul, &[log, power])?[0];
540                multiply_ad_value(builder, tangent_inputs[1], coefficient)?
541            } else {
542                AdValue::Absent
543            };
544            add_ad_values(builder, lhs, rhs)?
545        }
546        CoreSemanticOp::DotGeneral { config } => {
547            linearize_dot_general(builder, primal_inputs, tangent_inputs, config)?
548        }
549        CoreSemanticOp::Abs => {
550            let input_dtype = builder.value_metadata(primal_inputs[0])?.dtype();
551            let sign = builder.add_op(CoreSemanticOp::Sign, &[primal_inputs[0]])?[0];
552            let coefficient = if is_complex_dtype(input_dtype) {
553                builder.add_op(CoreSemanticOp::Conj, &[sign])?[0]
554            } else {
555                sign
556            };
557            let tangent = multiply_ad_value(builder, tangent_inputs[0], coefficient)?;
558            convert_ad_value(builder, tangent, input_dtype, abs_output_dtype(input_dtype))?
559        }
560        CoreSemanticOp::Sign => linearize_sign(builder, primal_inputs[0], tangent_inputs[0])?,
561        CoreSemanticOp::Maximum | CoreSemanticOp::Minimum => {
562            linearize_extrema(builder, op, primal_inputs, tangent_inputs)?
563        }
564        CoreSemanticOp::Select => select_ad_values(
565            builder,
566            primal_inputs[0],
567            tangent_inputs[1],
568            tangent_inputs[2],
569        )?,
570        CoreSemanticOp::Clamp => linearize_clamp(builder, primal_inputs, tangent_inputs)?,
571        CoreSemanticOp::Neg | CoreSemanticOp::Conj => {
572            unary_ad_value(builder, op.clone(), tangent_inputs[0])?
573        }
574        CoreSemanticOp::Exp
575        | CoreSemanticOp::Log
576        | CoreSemanticOp::Sin
577        | CoreSemanticOp::Cos
578        | CoreSemanticOp::Tanh
579        | CoreSemanticOp::Sqrt
580        | CoreSemanticOp::Rsqrt
581        | CoreSemanticOp::Expm1
582        | CoreSemanticOp::Log1p
583        | CoreSemanticOp::Erf => {
584            linearize_analytic_unary(builder, op, primal_inputs[0], tangent_inputs[0])?
585        }
586        CoreSemanticOp::Transpose { .. }
587        | CoreSemanticOp::Reshape { .. }
588        | CoreSemanticOp::BroadcastInDim { .. }
589        | CoreSemanticOp::ReduceSum { .. }
590        | CoreSemanticOp::ExtractDiag { .. }
591        | CoreSemanticOp::EmbedDiag { .. }
592        | CoreSemanticOp::Tril { .. }
593        | CoreSemanticOp::Triu { .. }
594        | CoreSemanticOp::Slice(_)
595        | CoreSemanticOp::Pad(_)
596        | CoreSemanticOp::Reverse { .. } => {
597            linearize_unary_core(builder, op.clone(), primal_inputs, tangent_inputs[0])?
598        }
599        CoreSemanticOp::ReduceSumSquares { axes } => core_reductions::linearize_sum_squares(
600            builder,
601            primal_inputs[0],
602            tangent_inputs[0],
603            axes,
604        )?,
605        CoreSemanticOp::Concatenate { axis, input_count } => {
606            linearize_concatenate(builder, primal_inputs, tangent_inputs, *axis, *input_count)?
607        }
608        CoreSemanticOp::Gather(_)
609        | CoreSemanticOp::GatherDynamicSliceSizes { .. }
610        | CoreSemanticOp::Scatter(_)
611        | CoreSemanticOp::DynamicSlice { .. }
612        | CoreSemanticOp::DynamicUpdateSlice => {
613            linearize_indexing(builder, op, primal_inputs, tangent_inputs)?
614        }
615        CoreSemanticOp::DynamicTruncate { .. } | CoreSemanticOp::PadToMatch { .. } => {
616            linearize_dynamic_shape(builder, op, primal_inputs, tangent_inputs[0])?
617        }
618        CoreSemanticOp::Convert { from, to } => {
619            if is_differentiable_dtype(*from) && is_differentiable_dtype(*to) {
620                linearize_unary_core(builder, op.clone(), primal_inputs, tangent_inputs[0])?
621            } else {
622                AdValue::Absent
623            }
624        }
625        CoreSemanticOp::ReduceProd { .. }
626        | CoreSemanticOp::ReduceMax { .. }
627        | CoreSemanticOp::ReduceMin { .. } => {
628            linearize_nonlinear_reduction(builder, op, primal_inputs, tangent_inputs[0])?
629        }
630        CoreSemanticOp::Rem
631        | CoreSemanticOp::Compare(_)
632        | CoreSemanticOp::ShapeOf { .. }
633        | CoreSemanticOp::Constant { .. } => AdValue::Absent,
634        _ => return Err(unsupported_core(SemanticTransformRole::Jvp, op)),
635    };
636    Ok([output].into())
637}
638
639// These direct semantic VJPs consume outputs, unlike the lower-level linear
640// transpose adapters, whose residual contract exposes only their inputs.
641pub(crate) fn eager_core_residual_spec(
642    op: &tenferro_ops::std_tensor_op::StdTensorOp,
643) -> crate::semantic_extension::ResidualSpec {
644    use tenferro_ops::std_tensor_op::StdTensorOp;
645    match op {
646        StdTensorOp::Exp | StdTensorOp::Tanh => crate::semantic_extension::ResidualSpec::output(0),
647        _ => tenferro_ops::ad::primitive_residual_spec(op).unwrap_or_default(),
648    }
649}
650
651fn vjp_core(
652    op: &CoreSemanticOp,
653    primal_inputs: &[ProgramValue],
654    primal_outputs: &[ProgramValue],
655    cotangent_outputs: &[AdValue],
656    active_inputs: &[bool],
657    builder: &mut SemanticProgramBuilder,
658) -> Result<Box<[AdValue]>, SemanticAdTransformError> {
659    let cotangent = cotangent_outputs[0];
660    let inputs = match op {
661        CoreSemanticOp::Add => vec![
662            active_cotangent(builder, cotangent, active_inputs[0], primal_inputs[0])?,
663            active_cotangent(builder, cotangent, active_inputs[1], primal_inputs[1])?,
664        ],
665        CoreSemanticOp::Sub => {
666            let negated = unary_ad_value(builder, CoreSemanticOp::Neg, cotangent)?;
667            vec![
668                active_cotangent(builder, cotangent, active_inputs[0], primal_inputs[0])?,
669                normalize_ad_value(builder, negated, active_inputs[1], primal_inputs[1])?,
670            ]
671        }
672        CoreSemanticOp::Mul => {
673            let rhs_coefficient = conjugate_if_complex(builder, primal_inputs[1])?;
674            let lhs_coefficient = conjugate_if_complex(builder, primal_inputs[0])?;
675            let lhs = multiply_ad_value(builder, cotangent, rhs_coefficient)?;
676            let rhs = multiply_ad_value(builder, cotangent, lhs_coefficient)?;
677            vec![
678                normalize_ad_value(builder, lhs, active_inputs[0], primal_inputs[0])?,
679                normalize_ad_value(builder, rhs, active_inputs[1], primal_inputs[1])?,
680            ]
681        }
682        CoreSemanticOp::Div => {
683            let rhs_coefficient = conjugate_if_complex(builder, primal_inputs[1])?;
684            let lhs = divide_ad_value(builder, cotangent, rhs_coefficient)?;
685            let lhs_coefficient = conjugate_if_complex(builder, primal_inputs[0])?;
686            let denominator =
687                builder.add_op(CoreSemanticOp::Mul, &[rhs_coefficient, rhs_coefficient])?[0];
688            let rhs = multiply_ad_value(builder, cotangent, lhs_coefficient)?;
689            let rhs = divide_ad_value(builder, rhs, denominator)?;
690            let rhs = unary_ad_value(builder, CoreSemanticOp::Neg, rhs)?;
691            vec![
692                normalize_ad_value(builder, lhs, active_inputs[0], primal_inputs[0])?,
693                normalize_ad_value(builder, rhs, active_inputs[1], primal_inputs[1])?,
694            ]
695        }
696        CoreSemanticOp::Pow => {
697            let lhs = if active_inputs[0] {
698                let one = one_like(builder, primal_inputs[1], SemanticTransformRole::Vjp)?;
699                let exponent_minus_one =
700                    builder.add_op(CoreSemanticOp::Sub, &[primal_inputs[1], one])?[0];
701                let power = builder
702                    .add_op(CoreSemanticOp::Pow, &[primal_inputs[0], exponent_minus_one])?[0];
703                let coefficient =
704                    builder.add_op(CoreSemanticOp::Mul, &[primal_inputs[1], power])?[0];
705                let coefficient = conjugate_if_complex(builder, coefficient)?;
706                multiply_ad_value(builder, cotangent, coefficient)?
707            } else {
708                AdValue::Absent
709            };
710            let rhs = if active_inputs[1] {
711                let log = builder.add_op(CoreSemanticOp::Log, &[primal_inputs[0]])?[0];
712                let power =
713                    builder.add_op(CoreSemanticOp::Pow, &[primal_inputs[0], primal_inputs[1]])?[0];
714                let coefficient = builder.add_op(CoreSemanticOp::Mul, &[log, power])?[0];
715                let coefficient = conjugate_if_complex(builder, coefficient)?;
716                multiply_ad_value(builder, cotangent, coefficient)?
717            } else {
718                AdValue::Absent
719            };
720            vec![
721                normalize_ad_value(builder, lhs, active_inputs[0], primal_inputs[0])?,
722                normalize_ad_value(builder, rhs, active_inputs[1], primal_inputs[1])?,
723            ]
724        }
725        CoreSemanticOp::DotGeneral { config } => {
726            dot_general_vjp(builder, primal_inputs, cotangent, active_inputs, config)?
727        }
728        CoreSemanticOp::Abs => {
729            let input_dtype = builder.value_metadata(primal_inputs[0])?.dtype();
730            let output_dtype = abs_output_dtype(input_dtype);
731            let cotangent = convert_ad_value(builder, cotangent, output_dtype, input_dtype)?;
732            let sign = builder.add_op(CoreSemanticOp::Sign, &[primal_inputs[0]])?[0];
733            let cotangent = multiply_ad_value(builder, cotangent, sign)?;
734            vec![normalize_ad_value(
735                builder,
736                cotangent,
737                active_inputs[0],
738                primal_inputs[0],
739            )?]
740        }
741        CoreSemanticOp::Sign => vec![AdValue::Absent],
742        CoreSemanticOp::Maximum | CoreSemanticOp::Minimum => {
743            extrema_vjp(builder, op, primal_inputs, cotangent, active_inputs)?
744        }
745        CoreSemanticOp::Select => {
746            let (on_true, on_false) = split_select_cotangent(
747                builder,
748                primal_inputs[0],
749                cotangent,
750                active_inputs[1],
751                active_inputs[2],
752            )?;
753            vec![
754                AdValue::Absent,
755                normalize_ad_value(builder, on_true, active_inputs[1], primal_inputs[1])?,
756                normalize_ad_value(builder, on_false, active_inputs[2], primal_inputs[2])?,
757            ]
758        }
759        CoreSemanticOp::Clamp => clamp_vjp(builder, primal_inputs, cotangent, active_inputs)?,
760        CoreSemanticOp::Neg => {
761            let negated = unary_ad_value(builder, CoreSemanticOp::Neg, cotangent)?;
762            vec![normalize_ad_value(
763                builder,
764                negated,
765                active_inputs[0],
766                primal_inputs[0],
767            )?]
768        }
769        CoreSemanticOp::Conj => {
770            let conjugated = unary_ad_value(builder, CoreSemanticOp::Conj, cotangent)?;
771            vec![normalize_ad_value(
772                builder,
773                conjugated,
774                active_inputs[0],
775                primal_inputs[0],
776            )?]
777        }
778        CoreSemanticOp::Exp
779        | CoreSemanticOp::Log
780        | CoreSemanticOp::Sin
781        | CoreSemanticOp::Cos
782        | CoreSemanticOp::Tanh
783        | CoreSemanticOp::Sqrt
784        | CoreSemanticOp::Rsqrt
785        | CoreSemanticOp::Expm1
786        | CoreSemanticOp::Log1p
787        | CoreSemanticOp::Erf => {
788            let coefficient = match op {
789                CoreSemanticOp::Exp => primal_outputs[0],
790                CoreSemanticOp::Tanh => {
791                    let y = primal_outputs[0];
792                    let square = builder.add_op(CoreSemanticOp::Mul, &[y, y])?[0];
793                    let one = one_like(builder, y, SemanticTransformRole::Vjp)?;
794                    builder.add_op(CoreSemanticOp::Sub, &[one, square])?[0]
795                }
796                _ => analytic_unary_coefficient(
797                    builder,
798                    op,
799                    primal_inputs[0],
800                    SemanticTransformRole::Vjp,
801                )?,
802            };
803            let coefficient = conjugate_if_complex(builder, coefficient)?;
804            let cotangent = multiply_ad_value(builder, cotangent, coefficient)?;
805            vec![normalize_ad_value(
806                builder,
807                cotangent,
808                active_inputs[0],
809                primal_inputs[0],
810            )?]
811        }
812        CoreSemanticOp::Transpose { perm } => {
813            let transposed = unary_ad_value(
814                builder,
815                CoreSemanticOp::Transpose {
816                    perm: inverse_permutation(perm),
817                },
818                cotangent,
819            )?;
820            primary_cotangent(builder, transposed, active_inputs, primal_inputs, false)?
821        }
822        CoreSemanticOp::Reshape { .. } => {
823            let reshaped = reshape_ad_value_to_input(builder, cotangent, primal_inputs[0])?;
824            primary_cotangent(builder, reshaped, active_inputs, primal_inputs, false)?
825        }
826        CoreSemanticOp::BroadcastInDim { dims, .. } => {
827            let reduced =
828                transpose_broadcast(builder, cotangent, primal_inputs[0], dims.as_slice())?;
829            primary_cotangent(builder, reduced, active_inputs, primal_inputs, false)?
830        }
831        CoreSemanticOp::Convert { from, to } => {
832            let converted = if is_differentiable_dtype(*from) && is_differentiable_dtype(*to) {
833                unary_ad_value(
834                    builder,
835                    CoreSemanticOp::Convert {
836                        from: *to,
837                        to: *from,
838                    },
839                    cotangent,
840                )?
841            } else {
842                AdValue::Absent
843            };
844            primary_cotangent(builder, converted, active_inputs, primal_inputs, false)?
845        }
846        CoreSemanticOp::ReduceSum { axes } => {
847            let input_shape = value_shape_plan(
848                builder,
849                primal_inputs[0],
850                SemanticTransformRole::Vjp,
851                "reduce-sum input",
852            )?;
853            let dims = (0..input_shape.shape.len())
854                .filter(|axis| !axes.contains(axis))
855                .collect();
856            let broadcast = broadcast_ad_value_in_dim_to_shape(
857                builder,
858                cotangent,
859                primal_inputs[0],
860                &input_shape,
861                dims,
862            )?;
863            let broadcast = truncate_ad_value_to_dynamic_axes(
864                builder,
865                broadcast,
866                primal_inputs[0],
867                &input_shape.dynamic_axes,
868            )?;
869            primary_cotangent(builder, broadcast, active_inputs, primal_inputs, false)?
870        }
871        CoreSemanticOp::ReduceSumSquares { axes } => core_reductions::sum_squares_vjp(
872            builder,
873            primal_inputs[0],
874            cotangent,
875            active_inputs[0],
876            axes,
877        )?,
878        CoreSemanticOp::ExtractDiag { axis_a, axis_b } => {
879            let embedded = unary_ad_value(
880                builder,
881                CoreSemanticOp::EmbedDiag {
882                    axis_a: if axis_a < axis_b { *axis_a } else { axis_a - 1 },
883                    axis_b: *axis_b,
884                },
885                cotangent,
886            )?;
887            let padded = match embedded {
888                AdValue::Absent => AdValue::Absent,
889                AdValue::Value(value) => {
890                    let value = builder.add_op(
891                        CoreSemanticOp::PadToMatch { axis: *axis_a },
892                        &[value, primal_inputs[0]],
893                    )?[0];
894                    AdValue::Value(
895                        builder.add_op(
896                            CoreSemanticOp::PadToMatch { axis: *axis_b },
897                            &[value, primal_inputs[0]],
898                        )?[0],
899                    )
900                }
901            };
902            primary_cotangent(builder, padded, active_inputs, primal_inputs, false)?
903        }
904        CoreSemanticOp::EmbedDiag { axis_a, axis_b } => {
905            let source_axis = if axis_b <= axis_a {
906                axis_a + 1
907            } else {
908                *axis_a
909            };
910            let extracted = unary_ad_value(
911                builder,
912                CoreSemanticOp::ExtractDiag {
913                    axis_a: source_axis,
914                    axis_b: *axis_b,
915                },
916                cotangent,
917            )?;
918            primary_cotangent(builder, extracted, active_inputs, primal_inputs, false)?
919        }
920        CoreSemanticOp::Tril { .. }
921        | CoreSemanticOp::Triu { .. }
922        | CoreSemanticOp::Reverse { .. } => {
923            let transformed = unary_ad_value(builder, op.clone(), cotangent)?;
924            primary_cotangent(builder, transformed, active_inputs, primal_inputs, false)?
925        }
926        CoreSemanticOp::Slice(config) => slice_vjp(
927            builder,
928            primal_inputs[0],
929            cotangent,
930            active_inputs[0],
931            config,
932        )?,
933        CoreSemanticOp::Pad(config) => pad_vjp(
934            builder,
935            primal_inputs[0],
936            cotangent,
937            active_inputs[0],
938            config,
939        )?,
940        CoreSemanticOp::Concatenate { axis, input_count } => concatenate_vjp(
941            builder,
942            primal_inputs,
943            cotangent,
944            active_inputs,
945            *axis,
946            *input_count,
947        )?,
948        CoreSemanticOp::Gather(_)
949        | CoreSemanticOp::GatherDynamicSliceSizes { .. }
950        | CoreSemanticOp::Scatter(_)
951        | CoreSemanticOp::DynamicSlice { .. }
952        | CoreSemanticOp::DynamicUpdateSlice => {
953            indexing_vjp(builder, op, primal_inputs, cotangent, active_inputs)?
954        }
955        CoreSemanticOp::DynamicTruncate { .. } | CoreSemanticOp::PadToMatch { .. } => {
956            dynamic_shape_vjp(builder, op, primal_inputs, cotangent, active_inputs)?
957        }
958        CoreSemanticOp::ReduceProd { .. }
959        | CoreSemanticOp::ReduceMax { .. }
960        | CoreSemanticOp::ReduceMin { .. } => {
961            nonlinear_reduction_vjp(builder, op, primal_inputs, cotangent, active_inputs[0])?
962        }
963        CoreSemanticOp::Rem | CoreSemanticOp::Compare(_) => {
964            vec![AdValue::Absent, AdValue::Absent]
965        }
966        CoreSemanticOp::ShapeOf { .. } => vec![AdValue::Absent],
967        CoreSemanticOp::Constant { .. } => Vec::new(),
968        _ => return Err(unsupported_core(SemanticTransformRole::Vjp, op)),
969    };
970    Ok(inputs.into_boxed_slice())
971}
972
973fn linearize_unary_core(
974    builder: &mut SemanticProgramBuilder,
975    op: CoreSemanticOp,
976    primal_inputs: &[ProgramValue],
977    tangent: AdValue,
978) -> Result<AdValue, ProgramBuildError> {
979    let AdValue::Value(tangent) = tangent else {
980        return Ok(AdValue::Absent);
981    };
982    let mut inputs = Vec::with_capacity(primal_inputs.len());
983    inputs.push(tangent);
984    inputs.extend_from_slice(&primal_inputs[1..]);
985    Ok(AdValue::Value(builder.add_op(op, &inputs)?[0]))
986}
987
988fn linearize_dot_general(
989    builder: &mut SemanticProgramBuilder,
990    primal_inputs: &[ProgramValue],
991    tangent_inputs: &[AdValue],
992    config: &DotGeneralConfig,
993) -> Result<AdValue, SemanticAdTransformError> {
994    validate_dot_general_metadata(builder, primal_inputs, config, SemanticTransformRole::Jvp)?;
995    let mut terms = Vec::with_capacity(2);
996    if let AdValue::Value(tangent) = tangent_inputs[0] {
997        terms.push(
998            builder.add_op(
999                CoreSemanticOp::DotGeneral {
1000                    config: config.clone(),
1001                },
1002                &[tangent, primal_inputs[1]],
1003            )?[0],
1004        );
1005    }
1006    if let AdValue::Value(tangent) = tangent_inputs[1] {
1007        terms.push(
1008            builder.add_op(
1009                CoreSemanticOp::DotGeneral {
1010                    config: config.clone(),
1011                },
1012                &[primal_inputs[0], tangent],
1013            )?[0],
1014        );
1015    }
1016    let mut terms = terms.into_iter();
1017    let Some(mut result) = terms.next() else {
1018        return Ok(AdValue::Absent);
1019    };
1020    for term in terms {
1021        result = builder.add_op(CoreSemanticOp::Add, &[result, term])?[0];
1022    }
1023    Ok(AdValue::Value(result))
1024}
1025
1026fn dot_general_vjp(
1027    builder: &mut SemanticProgramBuilder,
1028    primal_inputs: &[ProgramValue],
1029    cotangent: AdValue,
1030    active_inputs: &[bool],
1031    config: &DotGeneralConfig,
1032) -> Result<Vec<AdValue>, SemanticAdTransformError> {
1033    let (lhs_rank, rhs_rank) =
1034        validate_dot_general_metadata(builder, primal_inputs, config, SemanticTransformRole::Vjp)?;
1035    let lhs_free = dot_general_free_dims(
1036        lhs_rank,
1037        &config.lhs_contracting_dims,
1038        &config.lhs_batch_dims,
1039        SemanticTransformRole::Vjp,
1040    )?;
1041    let rhs_free = dot_general_free_dims(
1042        rhs_rank,
1043        &config.rhs_contracting_dims,
1044        &config.rhs_batch_dims,
1045        SemanticTransformRole::Vjp,
1046    )?;
1047    let AdValue::Value(cotangent) = cotangent else {
1048        return Ok(vec![AdValue::Absent, AdValue::Absent]);
1049    };
1050    let mut result = vec![AdValue::Absent, AdValue::Absent];
1051
1052    if active_inputs[0] {
1053        let rhs = conjugate_if_complex(builder, primal_inputs[1])?;
1054        let (transpose_config, perm) =
1055            dot_general_transpose_plan_for_lhs(config, lhs_rank, rhs_rank, &lhs_free, &rhs_free)?;
1056        let value = builder.add_op(
1057            CoreSemanticOp::DotGeneral {
1058                config: transpose_config,
1059            },
1060            &[cotangent, rhs],
1061        )?[0];
1062        let value = transpose_if_needed(builder, value, &perm)?;
1063        result[0] = normalize_ad_value(builder, AdValue::Value(value), true, primal_inputs[0])?;
1064    }
1065    if active_inputs[1] {
1066        let lhs = conjugate_if_complex(builder, primal_inputs[0])?;
1067        let (transpose_config, perm) =
1068            dot_general_transpose_plan_for_rhs(config, lhs_rank, rhs_rank, &lhs_free, &rhs_free)?;
1069        let value = builder.add_op(
1070            CoreSemanticOp::DotGeneral {
1071                config: transpose_config,
1072            },
1073            &[lhs, cotangent],
1074        )?[0];
1075        let value = transpose_if_needed(builder, value, &perm)?;
1076        result[1] = normalize_ad_value(builder, AdValue::Value(value), true, primal_inputs[1])?;
1077    }
1078    Ok(result)
1079}
1080
1081fn validate_dot_general_metadata(
1082    builder: &SemanticProgramBuilder,
1083    primal_inputs: &[ProgramValue],
1084    config: &DotGeneralConfig,
1085    role: SemanticTransformRole,
1086) -> Result<(usize, usize), SemanticAdTransformError> {
1087    let lhs_rank = builder.value_metadata(primal_inputs[0])?.shape().len();
1088    let rhs_rank = builder.value_metadata(primal_inputs[1])?.shape().len();
1089    config
1090        .validate_dims_with_ranks(lhs_rank, rhs_rank)
1091        .map_err(|error| SemanticAdTransformError::UnsupportedMetadata {
1092            role,
1093            message: format!(
1094                "invalid dot_general dimensions for ranks {lhs_rank} and {rhs_rank}: {error}"
1095            ),
1096        })?;
1097    Ok((lhs_rank, rhs_rank))
1098}
1099
1100fn dot_general_free_dims(
1101    rank: usize,
1102    contracting: &[usize],
1103    batch: &[usize],
1104    role: SemanticTransformRole,
1105) -> Result<Vec<usize>, SemanticAdTransformError> {
1106    let mut bound = vec![false; rank];
1107    for &axis in batch.iter().chain(contracting) {
1108        let Some(slot) = bound.get_mut(axis) else {
1109            return Err(SemanticAdTransformError::UnsupportedMetadata {
1110                role,
1111                message: format!("dot_general axis {axis} is out of bounds for rank {rank}"),
1112            });
1113        };
1114        *slot = true;
1115    }
1116    Ok((0..rank).filter(|axis| !bound[*axis]).collect())
1117}
1118
1119fn dot_general_transpose_plan_for_lhs(
1120    config: &DotGeneralConfig,
1121    lhs_rank: usize,
1122    rhs_rank: usize,
1123    lhs_free: &[usize],
1124    rhs_free: &[usize],
1125) -> Result<(DotGeneralConfig, Vec<usize>), SemanticAdTransformError> {
1126    let batch_count = config.lhs_batch_dims.len();
1127    let output_rank = lhs_free.len() + rhs_free.len() + batch_count;
1128    let rhs_free_positions = (lhs_free.len()..lhs_free.len() + rhs_free.len()).collect();
1129    let rhs_contracting_order = dot_general_free_dims(
1130        rhs_rank,
1131        rhs_free,
1132        &config.rhs_batch_dims,
1133        SemanticTransformRole::Vjp,
1134    )?;
1135    let mut result_order = lhs_free.to_vec();
1136    for rhs_axis in rhs_contracting_order {
1137        let Some(pair) = config
1138            .rhs_contracting_dims
1139            .iter()
1140            .position(|&axis| axis == rhs_axis)
1141        else {
1142            return Err(dot_general_transpose_metadata_error(format!(
1143                "rhs contracting axis {rhs_axis} has no lhs pair"
1144            )));
1145        };
1146        result_order.push(config.lhs_contracting_dims[pair]);
1147    }
1148    result_order.extend(config.lhs_batch_dims.iter().copied());
1149    Ok((
1150        DotGeneralConfig {
1151            lhs_contracting_dims: rhs_free_positions,
1152            rhs_contracting_dims: rhs_free.into(),
1153            lhs_batch_dims: (lhs_free.len() + rhs_free.len()..output_rank).collect(),
1154            rhs_batch_dims: config.rhs_batch_dims.clone(),
1155        },
1156        permutation_to_original_order(lhs_rank, &result_order)?,
1157    ))
1158}
1159
1160fn dot_general_transpose_plan_for_rhs(
1161    config: &DotGeneralConfig,
1162    lhs_rank: usize,
1163    rhs_rank: usize,
1164    lhs_free: &[usize],
1165    rhs_free: &[usize],
1166) -> Result<(DotGeneralConfig, Vec<usize>), SemanticAdTransformError> {
1167    let batch_count = config.lhs_batch_dims.len();
1168    let lhs_contracting_order = dot_general_free_dims(
1169        lhs_rank,
1170        lhs_free,
1171        &config.lhs_batch_dims,
1172        SemanticTransformRole::Vjp,
1173    )?;
1174    let mut result_order = Vec::with_capacity(rhs_rank);
1175    for lhs_axis in lhs_contracting_order {
1176        let Some(pair) = config
1177            .lhs_contracting_dims
1178            .iter()
1179            .position(|&axis| axis == lhs_axis)
1180        else {
1181            return Err(dot_general_transpose_metadata_error(format!(
1182                "lhs contracting axis {lhs_axis} has no rhs pair"
1183            )));
1184        };
1185        result_order.push(config.rhs_contracting_dims[pair]);
1186    }
1187    result_order.extend(rhs_free.iter().copied());
1188    result_order.extend(config.rhs_batch_dims.iter().copied());
1189    let output_rank = lhs_free.len() + rhs_free.len() + batch_count;
1190    Ok((
1191        DotGeneralConfig {
1192            lhs_contracting_dims: lhs_free.into(),
1193            rhs_contracting_dims: (0..lhs_free.len()).collect(),
1194            lhs_batch_dims: config.lhs_batch_dims.clone(),
1195            rhs_batch_dims: (lhs_free.len() + rhs_free.len()..output_rank).collect(),
1196        },
1197        permutation_to_original_order(rhs_rank, &result_order)?,
1198    ))
1199}
1200
1201fn permutation_to_original_order(
1202    rank: usize,
1203    result_order: &[usize],
1204) -> Result<Vec<usize>, SemanticAdTransformError> {
1205    let mut permutation = vec![0; rank];
1206    for (result_axis, &original_axis) in result_order.iter().enumerate() {
1207        let Some(slot) = permutation.get_mut(original_axis) else {
1208            return Err(dot_general_transpose_metadata_error(format!(
1209                "dot_general transpose axis {original_axis} is out of bounds for rank {rank}"
1210            )));
1211        };
1212        *slot = result_axis;
1213    }
1214    Ok(permutation)
1215}
1216
1217fn transpose_if_needed(
1218    builder: &mut SemanticProgramBuilder,
1219    value: ProgramValue,
1220    permutation: &[usize],
1221) -> Result<ProgramValue, ProgramBuildError> {
1222    if permutation
1223        .iter()
1224        .enumerate()
1225        .all(|(axis, &mapped)| axis == mapped)
1226    {
1227        Ok(value)
1228    } else {
1229        Ok(builder.add_op(
1230            CoreSemanticOp::Transpose {
1231                perm: permutation.to_vec(),
1232            },
1233            &[value],
1234        )?[0])
1235    }
1236}
1237
1238fn dot_general_transpose_metadata_error(message: String) -> SemanticAdTransformError {
1239    SemanticAdTransformError::UnsupportedMetadata {
1240        role: SemanticTransformRole::Vjp,
1241        message,
1242    }
1243}
1244
1245fn primary_cotangent(
1246    builder: &mut SemanticProgramBuilder,
1247    cotangent: AdValue,
1248    active_inputs: &[bool],
1249    primal_inputs: &[ProgramValue],
1250    normalize: bool,
1251) -> Result<Vec<AdValue>, SemanticAdTransformError> {
1252    let mut result = vec![AdValue::Absent; primal_inputs.len()];
1253    if active_inputs.first().copied().unwrap_or(false) {
1254        result[0] = if normalize {
1255            normalize_ad_value(builder, cotangent, true, primal_inputs[0])?
1256        } else {
1257            cotangent
1258        };
1259    }
1260    Ok(result)
1261}
1262
1263fn inverse_permutation(perm: &[usize]) -> Vec<usize> {
1264    let mut inverse = vec![0; perm.len()];
1265    for (axis, mapped) in perm.iter().copied().enumerate() {
1266        inverse[mapped] = axis;
1267    }
1268    inverse
1269}
1270
1271fn reshape_ad_value_to_input(
1272    builder: &mut SemanticProgramBuilder,
1273    value: AdValue,
1274    primal_input: ProgramValue,
1275) -> Result<AdValue, SemanticAdTransformError> {
1276    let shape = value_shape_plan(
1277        builder,
1278        primal_input,
1279        SemanticTransformRole::Vjp,
1280        "reshape input",
1281    )?;
1282    let reshaped = reshape_ad_value_to_shape(builder, value, primal_input, &shape)?;
1283    truncate_ad_value_to_dynamic_axes(builder, reshaped, primal_input, &shape.dynamic_axes)
1284}
1285
1286fn transpose_broadcast(
1287    builder: &mut SemanticProgramBuilder,
1288    value: AdValue,
1289    primal_input: ProgramValue,
1290    dims: &[usize],
1291) -> Result<AdValue, SemanticAdTransformError> {
1292    let AdValue::Value(mut value) = value else {
1293        return Ok(AdValue::Absent);
1294    };
1295    let input_shape = value_shape_plan(
1296        builder,
1297        primal_input,
1298        SemanticTransformRole::Vjp,
1299        "broadcast input",
1300    )?;
1301    let output_shape = value_shape_plan(
1302        builder,
1303        value,
1304        SemanticTransformRole::Vjp,
1305        "broadcast cotangent",
1306    )?;
1307    let input_rank = input_shape.shape.len();
1308    let output_rank = output_shape.shape.len();
1309    let metadata_error = |message| SemanticAdTransformError::UnsupportedMetadata {
1310        role: SemanticTransformRole::Vjp,
1311        message,
1312    };
1313    if dims.len() != input_rank {
1314        return Err(metadata_error(format!(
1315            "broadcast dims length {} does not match input rank {input_rank}",
1316            dims.len()
1317        )));
1318    }
1319    let mut seen = HashSet::with_capacity(dims.len());
1320    for (input_axis, &output_axis) in dims.iter().enumerate() {
1321        if output_axis >= output_rank {
1322            return Err(metadata_error(format!(
1323                "broadcast dims[{input_axis}] = {output_axis} is out of bounds for output rank {output_rank}"
1324            )));
1325        }
1326        if !seen.insert(output_axis) {
1327            return Err(metadata_error(format!(
1328                "broadcast dims[{input_axis}] = {output_axis} duplicates an earlier output axis"
1329            )));
1330        }
1331    }
1332    // INVARIANT: these repeated membership and position scans are bounded by tensor rank;
1333    // replace them with axis maps only if unusually high-rank tensors make this measurable.
1334    let mut reduce_axes: Vec<_> = (0..output_rank)
1335        .filter(|axis| !dims.contains(axis))
1336        .collect();
1337    reduce_axes.extend(
1338        dims.iter()
1339            .copied()
1340            .enumerate()
1341            .filter_map(|(input_axis, output_axis)| {
1342                (matches!(
1343                    input_shape.shape[input_axis],
1344                    tenferro_ops::dim_expr::DimExpr::Const(1)
1345                ) && input_shape.shape[input_axis] != output_shape.shape[output_axis])
1346                    .then_some(output_axis)
1347            }),
1348    );
1349    reduce_axes.sort_unstable();
1350    reduce_axes.dedup();
1351    if !reduce_axes.is_empty() {
1352        value = builder.add_op(
1353            CoreSemanticOp::ReduceSum {
1354                axes: reduce_axes.clone(),
1355            },
1356            &[value],
1357        )?[0];
1358    }
1359
1360    let remaining_output_axes: Vec<_> = (0..output_rank)
1361        .filter(|axis| !reduce_axes.contains(axis))
1362        .collect();
1363    let perm: Vec<_> = dims
1364        .iter()
1365        .copied()
1366        .filter(|axis| !reduce_axes.contains(axis))
1367        .map(|axis| {
1368            remaining_output_axes
1369                .iter()
1370                .position(|candidate| *candidate == axis)
1371                .ok_or_else(|| {
1372                    metadata_error(format!(
1373                        "broadcast output axis {axis} did not survive cotangent reduction"
1374                    ))
1375                })
1376        })
1377        .collect::<Result<_, _>>()?;
1378    if perm.iter().copied().ne(0..perm.len()) {
1379        value = builder.add_op(CoreSemanticOp::Transpose { perm }, &[value])?[0];
1380    }
1381    if builder.value_metadata(value)?.shape() != builder.value_metadata(primal_input)?.shape() {
1382        value = reshape_value_to_shape(builder, value, primal_input, &input_shape)?;
1383    }
1384    value =
1385        truncate_value_to_dynamic_axes(builder, value, primal_input, &input_shape.dynamic_axes)?;
1386    Ok(AdValue::Value(value))
1387}
1388
1389fn exact_value_shape(
1390    builder: &SemanticProgramBuilder,
1391    value: ProgramValue,
1392    role: SemanticTransformRole,
1393    field: &'static str,
1394) -> Result<Vec<tenferro_ops::dim_expr::DimExpr>, SemanticAdTransformError> {
1395    exact_shape(builder.value_metadata(value)?.shape(), role, field)
1396}
1397
1398fn is_differentiable_dtype(dtype: DType) -> bool {
1399    matches!(dtype, DType::F32 | DType::F64 | DType::C32 | DType::C64)
1400}
1401
1402fn is_complex_dtype(dtype: DType) -> bool {
1403    matches!(dtype, DType::C32 | DType::C64)
1404}
1405
1406fn abs_output_dtype(dtype: DType) -> DType {
1407    match dtype {
1408        DType::C32 => DType::F32,
1409        DType::C64 => DType::F64,
1410        other => other,
1411    }
1412}
1413
1414fn add_ad_values(
1415    builder: &mut SemanticProgramBuilder,
1416    lhs: AdValue,
1417    rhs: AdValue,
1418) -> Result<AdValue, ProgramBuildError> {
1419    match (lhs, rhs) {
1420        (AdValue::Absent, value) | (value, AdValue::Absent) => Ok(value),
1421        (AdValue::Value(lhs), AdValue::Value(rhs)) => Ok(AdValue::Value(
1422            builder.add_op(CoreSemanticOp::Add, &[lhs, rhs])?[0],
1423        )),
1424    }
1425}
1426
1427fn sub_ad_values(
1428    builder: &mut SemanticProgramBuilder,
1429    lhs: AdValue,
1430    rhs: AdValue,
1431) -> Result<AdValue, ProgramBuildError> {
1432    match (lhs, rhs) {
1433        (AdValue::Absent, AdValue::Absent) => Ok(AdValue::Absent),
1434        (value, AdValue::Absent) => Ok(value),
1435        (AdValue::Absent, AdValue::Value(rhs)) => Ok(AdValue::Value(
1436            builder.add_op(CoreSemanticOp::Neg, &[rhs])?[0],
1437        )),
1438        (AdValue::Value(lhs), AdValue::Value(rhs)) => Ok(AdValue::Value(
1439            builder.add_op(CoreSemanticOp::Sub, &[lhs, rhs])?[0],
1440        )),
1441    }
1442}
1443
1444fn unary_ad_value(
1445    builder: &mut SemanticProgramBuilder,
1446    op: CoreSemanticOp,
1447    value: AdValue,
1448) -> Result<AdValue, ProgramBuildError> {
1449    match value {
1450        AdValue::Absent => Ok(AdValue::Absent),
1451        AdValue::Value(value) => Ok(AdValue::Value(builder.add_op(op, &[value])?[0])),
1452    }
1453}
1454
1455fn multiply_ad_value(
1456    builder: &mut SemanticProgramBuilder,
1457    value: AdValue,
1458    coefficient: ProgramValue,
1459) -> Result<AdValue, ProgramBuildError> {
1460    match value {
1461        AdValue::Absent => Ok(AdValue::Absent),
1462        AdValue::Value(value) => Ok(AdValue::Value(
1463            builder.add_op(CoreSemanticOp::Mul, &[value, coefficient])?[0],
1464        )),
1465    }
1466}
1467
1468fn divide_ad_value(
1469    builder: &mut SemanticProgramBuilder,
1470    value: AdValue,
1471    denominator: ProgramValue,
1472) -> Result<AdValue, ProgramBuildError> {
1473    match value {
1474        AdValue::Absent => Ok(AdValue::Absent),
1475        AdValue::Value(value) => Ok(AdValue::Value(
1476            builder.add_op(CoreSemanticOp::Div, &[value, denominator])?[0],
1477        )),
1478    }
1479}
1480
1481fn convert_ad_value(
1482    builder: &mut SemanticProgramBuilder,
1483    value: AdValue,
1484    from: DType,
1485    to: DType,
1486) -> Result<AdValue, ProgramBuildError> {
1487    if from == to {
1488        return Ok(value);
1489    }
1490    unary_ad_value(builder, CoreSemanticOp::Convert { from, to }, value)
1491}
1492
1493fn select_ad_values(
1494    builder: &mut SemanticProgramBuilder,
1495    condition: ProgramValue,
1496    on_true: AdValue,
1497    on_false: AdValue,
1498) -> Result<AdValue, SemanticAdTransformError> {
1499    match (on_true, on_false) {
1500        (AdValue::Absent, AdValue::Absent) => Ok(AdValue::Absent),
1501        (AdValue::Value(on_true), AdValue::Value(on_false)) => Ok(AdValue::Value(
1502            builder.add_op(CoreSemanticOp::Select, &[condition, on_true, on_false])?[0],
1503        )),
1504        (AdValue::Value(on_true), AdValue::Absent) => {
1505            let zero = zero_constant_like(builder, on_true, SemanticTransformRole::Jvp)?;
1506            Ok(AdValue::Value(
1507                builder.add_op(CoreSemanticOp::Select, &[condition, on_true, zero])?[0],
1508            ))
1509        }
1510        (AdValue::Absent, AdValue::Value(on_false)) => {
1511            let zero = zero_constant_like(builder, on_false, SemanticTransformRole::Jvp)?;
1512            Ok(AdValue::Value(
1513                builder.add_op(CoreSemanticOp::Select, &[condition, zero, on_false])?[0],
1514            ))
1515        }
1516    }
1517}
1518
1519fn split_select_cotangent(
1520    builder: &mut SemanticProgramBuilder,
1521    condition: ProgramValue,
1522    cotangent: AdValue,
1523    true_active: bool,
1524    false_active: bool,
1525) -> Result<(AdValue, AdValue), SemanticAdTransformError> {
1526    if !true_active && !false_active {
1527        return Ok((AdValue::Absent, AdValue::Absent));
1528    }
1529    let AdValue::Value(cotangent) = cotangent else {
1530        return Ok((AdValue::Absent, AdValue::Absent));
1531    };
1532    let zero = zero_constant_like(builder, cotangent, SemanticTransformRole::Vjp)?;
1533    let on_true = if true_active {
1534        AdValue::Value(builder.add_op(CoreSemanticOp::Select, &[condition, cotangent, zero])?[0])
1535    } else {
1536        AdValue::Absent
1537    };
1538    let on_false = if false_active {
1539        AdValue::Value(builder.add_op(CoreSemanticOp::Select, &[condition, zero, cotangent])?[0])
1540    } else {
1541        AdValue::Absent
1542    };
1543    Ok((on_true, on_false))
1544}
1545
1546fn linearize_extrema(
1547    builder: &mut SemanticProgramBuilder,
1548    op: &CoreSemanticOp,
1549    primal_inputs: &[ProgramValue],
1550    tangent_inputs: &[AdValue],
1551) -> Result<AdValue, SemanticAdTransformError> {
1552    let output = builder.add_op(op.clone(), primal_inputs)?[0];
1553    let lhs_eq_output = builder.add_op(
1554        CoreSemanticOp::Compare(CompareDir::Eq),
1555        &[primal_inputs[0], output],
1556    )?[0];
1557    let rhs_eq_output = builder.add_op(
1558        CoreSemanticOp::Compare(CompareDir::Eq),
1559        &[primal_inputs[1], output],
1560    )?[0];
1561    let lhs = balanced_extrema_contribution(
1562        builder,
1563        tangent_inputs[0],
1564        lhs_eq_output,
1565        rhs_eq_output,
1566        SemanticTransformRole::Jvp,
1567    )?;
1568    let rhs = balanced_extrema_contribution(
1569        builder,
1570        tangent_inputs[1],
1571        rhs_eq_output,
1572        lhs_eq_output,
1573        SemanticTransformRole::Jvp,
1574    )?;
1575    Ok(add_ad_values(builder, lhs, rhs)?)
1576}
1577
1578fn extrema_vjp(
1579    builder: &mut SemanticProgramBuilder,
1580    op: &CoreSemanticOp,
1581    primal_inputs: &[ProgramValue],
1582    cotangent: AdValue,
1583    active_inputs: &[bool],
1584) -> Result<Vec<AdValue>, SemanticAdTransformError> {
1585    let output = builder.add_op(op.clone(), primal_inputs)?[0];
1586    let lhs_eq_output = builder.add_op(
1587        CoreSemanticOp::Compare(CompareDir::Eq),
1588        &[primal_inputs[0], output],
1589    )?[0];
1590    let rhs_eq_output = builder.add_op(
1591        CoreSemanticOp::Compare(CompareDir::Eq),
1592        &[primal_inputs[1], output],
1593    )?[0];
1594    let lhs = balanced_extrema_contribution(
1595        builder,
1596        cotangent,
1597        lhs_eq_output,
1598        rhs_eq_output,
1599        SemanticTransformRole::Vjp,
1600    )?;
1601    let rhs = balanced_extrema_contribution(
1602        builder,
1603        cotangent,
1604        rhs_eq_output,
1605        lhs_eq_output,
1606        SemanticTransformRole::Vjp,
1607    )?;
1608    Ok(vec![
1609        normalize_ad_value(builder, lhs, active_inputs[0], primal_inputs[0])?,
1610        normalize_ad_value(builder, rhs, active_inputs[1], primal_inputs[1])?,
1611    ])
1612}
1613
1614fn balanced_extrema_contribution(
1615    builder: &mut SemanticProgramBuilder,
1616    active: AdValue,
1617    self_eq_output: ProgramValue,
1618    other_eq_output: ProgramValue,
1619    role: SemanticTransformRole,
1620) -> Result<AdValue, SemanticAdTransformError> {
1621    let AdValue::Value(active) = active else {
1622        return Ok(AdValue::Absent);
1623    };
1624    let zero = zero_constant_like(builder, active, role)?;
1625    let selected = builder.add_op(CoreSemanticOp::Select, &[self_eq_output, active, zero])?[0];
1626    let one = one_like(builder, active, role)?;
1627    let two = builder.add_op(CoreSemanticOp::Add, &[one, one])?[0];
1628    let half = builder.add_op(CoreSemanticOp::Div, &[selected, two])?[0];
1629    Ok(AdValue::Value(
1630        builder.add_op(CoreSemanticOp::Select, &[other_eq_output, half, selected])?[0],
1631    ))
1632}
1633
1634fn linearize_clamp(
1635    builder: &mut SemanticProgramBuilder,
1636    primal_inputs: &[ProgramValue],
1637    tangent_inputs: &[AdValue],
1638) -> Result<AdValue, SemanticAdTransformError> {
1639    let masks = clamp_masks(builder, primal_inputs)?;
1640    let input = mask_ad_value(
1641        builder,
1642        tangent_inputs[0],
1643        &[masks[0], masks[1]],
1644        SemanticTransformRole::Jvp,
1645    )?;
1646    let lower = mask_ad_value(
1647        builder,
1648        tangent_inputs[1],
1649        &[masks[2], masks[3]],
1650        SemanticTransformRole::Jvp,
1651    )?;
1652    let upper = mask_ad_value(
1653        builder,
1654        tangent_inputs[2],
1655        &[masks[4]],
1656        SemanticTransformRole::Jvp,
1657    )?;
1658    let input_and_lower = add_ad_values(builder, input, lower)?;
1659    Ok(add_ad_values(builder, input_and_lower, upper)?)
1660}
1661
1662fn clamp_vjp(
1663    builder: &mut SemanticProgramBuilder,
1664    primal_inputs: &[ProgramValue],
1665    cotangent: AdValue,
1666    active_inputs: &[bool],
1667) -> Result<Vec<AdValue>, SemanticAdTransformError> {
1668    let masks = clamp_masks(builder, primal_inputs)?;
1669    let input = mask_ad_value(
1670        builder,
1671        cotangent,
1672        &[masks[0], masks[1]],
1673        SemanticTransformRole::Vjp,
1674    )?;
1675    let lower = mask_ad_value(
1676        builder,
1677        cotangent,
1678        &[masks[2], masks[3]],
1679        SemanticTransformRole::Vjp,
1680    )?;
1681    let upper = mask_ad_value(builder, cotangent, &[masks[4]], SemanticTransformRole::Vjp)?;
1682    Ok(vec![
1683        normalize_ad_value(builder, input, active_inputs[0], primal_inputs[0])?,
1684        normalize_ad_value(builder, lower, active_inputs[1], primal_inputs[1])?,
1685        normalize_ad_value(builder, upper, active_inputs[2], primal_inputs[2])?,
1686    ])
1687}
1688
1689fn clamp_masks(
1690    builder: &mut SemanticProgramBuilder,
1691    primal_inputs: &[ProgramValue],
1692) -> Result<[ProgramValue; 5], ProgramBuildError> {
1693    let input = primal_inputs[0];
1694    let lower = primal_inputs[1];
1695    let upper = primal_inputs[2];
1696    let input_gt_lower =
1697        builder.add_op(CoreSemanticOp::Compare(CompareDir::Gt), &[input, lower])?[0];
1698    let input_lt_upper =
1699        builder.add_op(CoreSemanticOp::Compare(CompareDir::Lt), &[input, upper])?[0];
1700    let lower_gt_input =
1701        builder.add_op(CoreSemanticOp::Compare(CompareDir::Gt), &[lower, input])?[0];
1702    let lower_lt_upper =
1703        builder.add_op(CoreSemanticOp::Compare(CompareDir::Lt), &[lower, upper])?[0];
1704    let max_input_lower = builder.add_op(CoreSemanticOp::Maximum, &[input, lower])?[0];
1705    let upper_lt_max_input_lower = builder.add_op(
1706        CoreSemanticOp::Compare(CompareDir::Lt),
1707        &[upper, max_input_lower],
1708    )?[0];
1709    Ok([
1710        input_gt_lower,
1711        input_lt_upper,
1712        lower_gt_input,
1713        lower_lt_upper,
1714        upper_lt_max_input_lower,
1715    ])
1716}
1717
1718fn mask_ad_value(
1719    builder: &mut SemanticProgramBuilder,
1720    active: AdValue,
1721    conditions: &[ProgramValue],
1722    role: SemanticTransformRole,
1723) -> Result<AdValue, SemanticAdTransformError> {
1724    let AdValue::Value(active) = active else {
1725        return Ok(AdValue::Absent);
1726    };
1727    let zero = zero_constant_like(builder, active, role)?;
1728    let mut value = active;
1729    for condition in conditions {
1730        value = builder.add_op(CoreSemanticOp::Select, &[*condition, value, zero])?[0];
1731    }
1732    Ok(AdValue::Value(value))
1733}
1734
1735fn linearize_analytic_unary(
1736    builder: &mut SemanticProgramBuilder,
1737    op: &CoreSemanticOp,
1738    primal_input: ProgramValue,
1739    tangent: AdValue,
1740) -> Result<AdValue, SemanticAdTransformError> {
1741    if matches!(tangent, AdValue::Absent) {
1742        return Ok(AdValue::Absent);
1743    }
1744    let coefficient =
1745        analytic_unary_coefficient(builder, op, primal_input, SemanticTransformRole::Jvp)?;
1746    Ok(multiply_ad_value(builder, tangent, coefficient)?)
1747}
1748
1749fn linearize_sign(
1750    builder: &mut SemanticProgramBuilder,
1751    primal_input: ProgramValue,
1752    tangent: AdValue,
1753) -> Result<AdValue, SemanticAdTransformError> {
1754    let AdValue::Value(tangent_value) = tangent else {
1755        return Ok(AdValue::Absent);
1756    };
1757    let input_dtype = builder.value_metadata(primal_input)?.dtype();
1758    if !is_complex_dtype(input_dtype) {
1759        return Ok(AdValue::Absent);
1760    }
1761
1762    let zero = zero_constant_like(builder, primal_input, SemanticTransformRole::Jvp)?;
1763    let zero_mask = builder.add_op(
1764        CoreSemanticOp::Compare(CompareDir::Eq),
1765        &[primal_input, zero],
1766    )?[0];
1767    let sign = builder.add_op(CoreSemanticOp::Sign, &[primal_input])?[0];
1768    let abs = builder.add_op(CoreSemanticOp::Abs, &[primal_input])?[0];
1769    let output_dtype = abs_output_dtype(input_dtype);
1770    let complex_abs = builder.add_op(
1771        CoreSemanticOp::Convert {
1772            from: output_dtype,
1773            to: input_dtype,
1774        },
1775        &[abs],
1776    )?[0];
1777    let one = one_like(builder, complex_abs, SemanticTransformRole::Jvp)?;
1778    let safe_abs = builder.add_op(CoreSemanticOp::Select, &[zero_mask, one, complex_abs])?[0];
1779    let safe_sign = builder.add_op(CoreSemanticOp::Select, &[zero_mask, zero, sign])?[0];
1780    let conj_sign = builder.add_op(CoreSemanticOp::Conj, &[safe_sign])?[0];
1781
1782    let abs_tangent_complex = multiply_ad_value(builder, AdValue::Value(tangent_value), conj_sign)?;
1783    let abs_tangent = convert_ad_value(builder, abs_tangent_complex, input_dtype, output_dtype)?;
1784    let abs_tangent = convert_ad_value(builder, abs_tangent, output_dtype, input_dtype)?;
1785    let tangent_over_abs = divide_ad_value(builder, AdValue::Value(tangent_value), safe_abs)?;
1786    let sign_times_abs_tangent = multiply_ad_value(builder, abs_tangent, safe_sign)?;
1787    let correction = divide_ad_value(builder, sign_times_abs_tangent, safe_abs)?;
1788    let derivative = sub_ad_values(builder, tangent_over_abs, correction)?;
1789    let zero_derivative = zero_constant_like(builder, tangent_value, SemanticTransformRole::Jvp)?;
1790    select_ad_values(
1791        builder,
1792        zero_mask,
1793        AdValue::Value(zero_derivative),
1794        derivative,
1795    )
1796}
1797
1798fn analytic_unary_coefficient(
1799    builder: &mut SemanticProgramBuilder,
1800    op: &CoreSemanticOp,
1801    primal_input: ProgramValue,
1802    role: SemanticTransformRole,
1803) -> Result<ProgramValue, SemanticAdTransformError> {
1804    let coefficient = match op {
1805        CoreSemanticOp::Exp | CoreSemanticOp::Expm1 => {
1806            builder.add_op(CoreSemanticOp::Exp, &[primal_input])?[0]
1807        }
1808        CoreSemanticOp::Log => {
1809            let one = one_like(builder, primal_input, role)?;
1810            builder.add_op(CoreSemanticOp::Div, &[one, primal_input])?[0]
1811        }
1812        CoreSemanticOp::Sin => builder.add_op(CoreSemanticOp::Cos, &[primal_input])?[0],
1813        CoreSemanticOp::Cos => {
1814            let sin = builder.add_op(CoreSemanticOp::Sin, &[primal_input])?[0];
1815            builder.add_op(CoreSemanticOp::Neg, &[sin])?[0]
1816        }
1817        CoreSemanticOp::Tanh => {
1818            let tanh = builder.add_op(CoreSemanticOp::Tanh, &[primal_input])?[0];
1819            let square = builder.add_op(CoreSemanticOp::Mul, &[tanh, tanh])?[0];
1820            let one = one_like(builder, primal_input, role)?;
1821            builder.add_op(CoreSemanticOp::Sub, &[one, square])?[0]
1822        }
1823        CoreSemanticOp::Sqrt => {
1824            let sqrt = builder.add_op(CoreSemanticOp::Sqrt, &[primal_input])?[0];
1825            let twice = builder.add_op(CoreSemanticOp::Add, &[sqrt, sqrt])?[0];
1826            let one = one_like(builder, primal_input, role)?;
1827            builder.add_op(CoreSemanticOp::Div, &[one, twice])?[0]
1828        }
1829        CoreSemanticOp::Rsqrt => {
1830            let rsqrt = builder.add_op(CoreSemanticOp::Rsqrt, &[primal_input])?[0];
1831            let negated = builder.add_op(CoreSemanticOp::Neg, &[rsqrt])?[0];
1832            let twice = builder.add_op(CoreSemanticOp::Add, &[primal_input, primal_input])?[0];
1833            builder.add_op(CoreSemanticOp::Div, &[negated, twice])?[0]
1834        }
1835        CoreSemanticOp::Log1p => {
1836            let one = one_like(builder, primal_input, role)?;
1837            let denominator = builder.add_op(CoreSemanticOp::Add, &[primal_input, one])?[0];
1838            builder.add_op(CoreSemanticOp::Div, &[one, denominator])?[0]
1839        }
1840        CoreSemanticOp::Erf => {
1841            // d erf(x) = 2/sqrt(pi) * exp(-x^2)
1842            let square = builder.add_op(CoreSemanticOp::Mul, &[primal_input, primal_input])?[0];
1843            let negated = builder.add_op(CoreSemanticOp::Neg, &[square])?[0];
1844            let gaussian = builder.add_op(CoreSemanticOp::Exp, &[negated])?[0];
1845            let scale = real_float_constant_like(
1846                builder,
1847                primal_input,
1848                std::f64::consts::FRAC_2_SQRT_PI,
1849                role,
1850            )?;
1851            builder.add_op(CoreSemanticOp::Mul, &[scale, gaussian])?[0]
1852        }
1853        _ => return Err(unsupported_core(role, op)),
1854    };
1855    Ok(coefficient)
1856}
1857
1858fn one_like(
1859    builder: &mut SemanticProgramBuilder,
1860    anchor: ProgramValue,
1861    role: SemanticTransformRole,
1862) -> Result<ProgramValue, SemanticAdTransformError> {
1863    let metadata = builder.value_metadata(anchor)?.clone();
1864    let dtype = metadata.dtype();
1865    let bytes = match dtype {
1866        DType::F32 => 1.0_f32.to_le_bytes().to_vec(),
1867        DType::F64 => 1.0_f64.to_le_bytes().to_vec(),
1868        DType::C32 => {
1869            let mut bytes = 1.0_f32.to_le_bytes().to_vec();
1870            bytes.extend_from_slice(&0.0_f32.to_le_bytes());
1871            bytes
1872        }
1873        DType::C64 => {
1874            let mut bytes = 1.0_f64.to_le_bytes().to_vec();
1875            bytes.extend_from_slice(&0.0_f64.to_le_bytes());
1876            bytes
1877        }
1878        _ => {
1879            return Err(SemanticAdTransformError::UnsupportedMetadata {
1880                role,
1881                message: format!("cannot construct a differentiable one for {dtype:?}"),
1882            });
1883        }
1884    };
1885    let scalar = builder.add_op(CoreSemanticOp::Constant { dtype, bytes }, &[])?[0];
1886    broadcast_scalar_like(builder, scalar, anchor, &metadata, role, "one-like anchor")
1887}
1888
1889/// Broadcast a rank-0 `scalar` to `anchor`'s (possibly dynamic) shape.
1890fn broadcast_scalar_like(
1891    builder: &mut SemanticProgramBuilder,
1892    scalar: ProgramValue,
1893    anchor: ProgramValue,
1894    metadata: &ProgramValueMetadata,
1895    role: SemanticTransformRole,
1896    what: &'static str,
1897) -> Result<ProgramValue, SemanticAdTransformError> {
1898    if metadata.shape().is_empty() {
1899        Ok(scalar)
1900    } else {
1901        let shape = shape_plan(metadata.shape(), role, what)?;
1902        let value = broadcast_value_in_dim_to_shape(builder, scalar, anchor, &shape, Vec::new())?;
1903        Ok(truncate_value_to_dynamic_axes(
1904            builder,
1905            value,
1906            anchor,
1907            &shape.dynamic_axes,
1908        )?)
1909    }
1910}
1911
1912/// Build the real constant `value` shaped like a real floating `anchor`.
1913fn real_float_constant_like(
1914    builder: &mut SemanticProgramBuilder,
1915    anchor: ProgramValue,
1916    value: f64,
1917    role: SemanticTransformRole,
1918) -> Result<ProgramValue, SemanticAdTransformError> {
1919    let metadata = builder.value_metadata(anchor)?.clone();
1920    let dtype = metadata.dtype();
1921    let bytes = match dtype {
1922        DType::F32 => (value as f32).to_le_bytes().to_vec(),
1923        DType::F64 => value.to_le_bytes().to_vec(),
1924        _ => {
1925            return Err(SemanticAdTransformError::UnsupportedMetadata {
1926                role,
1927                message: format!("cannot construct a real floating constant for {dtype:?}"),
1928            });
1929        }
1930    };
1931    let scalar = builder.add_op(CoreSemanticOp::Constant { dtype, bytes }, &[])?[0];
1932    broadcast_scalar_like(
1933        builder,
1934        scalar,
1935        anchor,
1936        &metadata,
1937        role,
1938        "real-constant anchor",
1939    )
1940}
1941
1942/// Build a true dtype-aware zero shaped like `anchor`.
1943///
1944/// A zero synthesized as `x - x` or `x + (-x)` evaluates to `NaN` when `x` is
1945/// non-finite, so emission sites that need an additive identity for masked-out
1946/// values must materialize a `Constant` zero instead of deriving it from the
1947/// active value.
1948fn zero_constant_like(
1949    builder: &mut SemanticProgramBuilder,
1950    anchor: ProgramValue,
1951    role: SemanticTransformRole,
1952) -> Result<ProgramValue, SemanticAdTransformError> {
1953    let metadata = builder.value_metadata(anchor)?.clone();
1954    let dtype = metadata.dtype();
1955    let bytes = match dtype {
1956        DType::F32 => 0.0_f32.to_le_bytes().to_vec(),
1957        DType::F64 => 0.0_f64.to_le_bytes().to_vec(),
1958        DType::I32 => 0_i32.to_le_bytes().to_vec(),
1959        DType::I64 => 0_i64.to_le_bytes().to_vec(),
1960        DType::Bool => vec![0],
1961        DType::C32 => {
1962            let mut bytes = 0.0_f32.to_le_bytes().to_vec();
1963            bytes.extend_from_slice(&0.0_f32.to_le_bytes());
1964            bytes
1965        }
1966        DType::C64 => {
1967            let mut bytes = 0.0_f64.to_le_bytes().to_vec();
1968            bytes.extend_from_slice(&0.0_f64.to_le_bytes());
1969            bytes
1970        }
1971        // INVARIANT: the semantic AD catalog is closed to the preset scalars,
1972        // so an externally defined dtype cannot reach a constant emission.
1973        DType::External(_) => unreachable!("the AD catalog is closed to the presets"),
1974    };
1975    let scalar = builder.add_op(CoreSemanticOp::Constant { dtype, bytes }, &[])?[0];
1976    if metadata.shape().is_empty() {
1977        return Ok(scalar);
1978    }
1979    let shape = shape_plan(metadata.shape(), role, "zero-like anchor")?;
1980    let zero = broadcast_value_in_dim_to_shape(builder, scalar, anchor, &shape, Vec::new())?;
1981    Ok(truncate_value_to_dynamic_axes(
1982        builder,
1983        zero,
1984        anchor,
1985        &shape.dynamic_axes,
1986    )?)
1987}
1988
1989fn active_cotangent(
1990    builder: &mut SemanticProgramBuilder,
1991    cotangent: AdValue,
1992    active: bool,
1993    primal_input: ProgramValue,
1994) -> Result<AdValue, SemanticAdTransformError> {
1995    normalize_ad_value(builder, cotangent, active, primal_input)
1996}
1997
1998fn normalize_ad_value(
1999    builder: &mut SemanticProgramBuilder,
2000    value: AdValue,
2001    active: bool,
2002    primal_input: ProgramValue,
2003) -> Result<AdValue, SemanticAdTransformError> {
2004    if !active {
2005        return Ok(AdValue::Absent);
2006    }
2007    let AdValue::Value(mut value) = value else {
2008        return Ok(AdValue::Absent);
2009    };
2010    let target_metadata = builder.value_metadata(primal_input)?.clone();
2011    let value_metadata = builder.value_metadata(value)?.clone();
2012    let target_shape = shape_plan(
2013        target_metadata.shape(),
2014        SemanticTransformRole::Vjp,
2015        "primal input",
2016    )?;
2017    let value_shape = shape_plan(
2018        value_metadata.shape(),
2019        SemanticTransformRole::Vjp,
2020        "cotangent",
2021    )?;
2022    if value_shape.shape.len() < target_shape.shape.len() {
2023        return Err(SemanticAdTransformError::UnsupportedMetadata {
2024            role: SemanticTransformRole::Vjp,
2025            message: "cotangent rank is smaller than its primal-input rank".into(),
2026        });
2027    }
2028    let leading = value_shape.shape.len() - target_shape.shape.len();
2029    let mut axes: Vec<_> = (0..leading).collect();
2030    axes.extend(
2031        target_shape
2032            .shape
2033            .iter()
2034            .zip(value_shape.shape.iter().skip(leading))
2035            .enumerate()
2036            .filter_map(|(axis, (target, actual))| {
2037                (matches!(target, tenferro_ops::dim_expr::DimExpr::Const(1)) && target != actual)
2038                    .then_some(axis + leading)
2039            }),
2040    );
2041    if !axes.is_empty() {
2042        value = builder.add_op(CoreSemanticOp::ReduceSum { axes }, &[value])?[0];
2043    }
2044    if builder.value_metadata(value)?.shape() != target_metadata.shape() {
2045        value = reshape_value_to_shape(builder, value, primal_input, &target_shape)?;
2046    }
2047    value =
2048        truncate_value_to_dynamic_axes(builder, value, primal_input, &target_shape.dynamic_axes)?;
2049    let value_dtype = builder.value_metadata(value)?.dtype();
2050    if value_dtype != target_metadata.dtype() {
2051        value = builder.add_op(
2052            CoreSemanticOp::Convert {
2053                from: value_dtype,
2054                to: target_metadata.dtype(),
2055            },
2056            &[value],
2057        )?[0];
2058    }
2059    Ok(AdValue::Value(value))
2060}
2061
2062fn exact_shape(
2063    shape: &[tenferro_ops::ShapeExtent<tenferro_ops::dim_expr::DimExpr>],
2064    role: SemanticTransformRole,
2065    field: &'static str,
2066) -> Result<Vec<tenferro_ops::dim_expr::DimExpr>, SemanticAdTransformError> {
2067    shape
2068        .iter()
2069        .map(|extent| {
2070            extent.as_exact().cloned().ok_or_else(|| {
2071                SemanticAdTransformError::UnsupportedMetadata {
2072                    role,
2073                    message: format!("{field} has a bounded or unknown extent"),
2074                }
2075            })
2076        })
2077        .collect()
2078}
2079
2080fn value_shape_plan(
2081    builder: &SemanticProgramBuilder,
2082    value: ProgramValue,
2083    role: SemanticTransformRole,
2084    field: &'static str,
2085) -> Result<ValueShapePlan, SemanticAdTransformError> {
2086    shape_plan(builder.value_metadata(value)?.shape(), role, field)
2087}
2088
2089fn shape_plan(
2090    shape: &[ShapeExtent<DimExpr>],
2091    role: SemanticTransformRole,
2092    field: &'static str,
2093) -> Result<ValueShapePlan, SemanticAdTransformError> {
2094    let mut planned_shape = Vec::with_capacity(shape.len());
2095    let mut dynamic_axes = Vec::new();
2096    for (axis, extent) in shape.iter().enumerate() {
2097        match extent {
2098            ShapeExtent::Exact(expression) => planned_shape.push(expression.clone()),
2099            ShapeExtent::UpperBound(expression) => {
2100                planned_shape.push(expression.clone());
2101                dynamic_axes.push(axis);
2102            }
2103            ShapeExtent::Unknown => {
2104                return Err(SemanticAdTransformError::UnsupportedMetadata {
2105                    role,
2106                    message: format!("{field} has an unknown extent without an upper bound"),
2107                });
2108            }
2109        }
2110    }
2111    Ok(ValueShapePlan {
2112        shape: planned_shape,
2113        dynamic_axes,
2114    })
2115}
2116
2117fn reshape_ad_value_to_shape(
2118    builder: &mut SemanticProgramBuilder,
2119    value: AdValue,
2120    shape_source: ProgramValue,
2121    shape: &ValueShapePlan,
2122) -> Result<AdValue, ProgramBuildError> {
2123    match value {
2124        AdValue::Absent => Ok(AdValue::Absent),
2125        AdValue::Value(value) => Ok(AdValue::Value(reshape_value_to_shape(
2126            builder,
2127            value,
2128            shape_source,
2129            shape,
2130        )?)),
2131    }
2132}
2133
2134fn reshape_value_to_shape(
2135    builder: &mut SemanticProgramBuilder,
2136    value: ProgramValue,
2137    shape_source: ProgramValue,
2138    shape: &ValueShapePlan,
2139) -> Result<ProgramValue, ProgramBuildError> {
2140    let mut inputs = vec![value];
2141    let to_shape = payload_shape_for_shape_source(shape, &mut inputs, shape_source);
2142    Ok(builder.add_op(CoreSemanticOp::Reshape { to_shape }, &inputs)?[0])
2143}
2144
2145fn broadcast_ad_value_in_dim_to_shape(
2146    builder: &mut SemanticProgramBuilder,
2147    value: AdValue,
2148    shape_source: ProgramValue,
2149    shape: &ValueShapePlan,
2150    dims: Vec<usize>,
2151) -> Result<AdValue, ProgramBuildError> {
2152    match value {
2153        AdValue::Absent => Ok(AdValue::Absent),
2154        AdValue::Value(value) => Ok(AdValue::Value(broadcast_value_in_dim_to_shape(
2155            builder,
2156            value,
2157            shape_source,
2158            shape,
2159            dims,
2160        )?)),
2161    }
2162}
2163
2164fn broadcast_value_in_dim_to_shape(
2165    builder: &mut SemanticProgramBuilder,
2166    value: ProgramValue,
2167    shape_source: ProgramValue,
2168    shape: &ValueShapePlan,
2169    dims: Vec<usize>,
2170) -> Result<ProgramValue, ProgramBuildError> {
2171    let mut inputs = vec![value];
2172    let shape = payload_shape_for_shape_source(shape, &mut inputs, shape_source);
2173    Ok(builder.add_op(CoreSemanticOp::BroadcastInDim { shape, dims }, &inputs)?[0])
2174}
2175
2176fn payload_shape_for_shape_source(
2177    shape: &ValueShapePlan,
2178    inputs: &mut Vec<ProgramValue>,
2179    shape_source: ProgramValue,
2180) -> Vec<DimExpr> {
2181    if DimExpr::max_input_idx_all(&shape.shape).is_none() {
2182        return shape.shape.clone();
2183    }
2184    let input_idx = inputs
2185        .iter()
2186        .position(|&input| input == shape_source)
2187        .unwrap_or_else(|| {
2188            let input_idx = inputs.len();
2189            inputs.push(shape_source);
2190            input_idx
2191        });
2192    DimExpr::input_shape(input_idx, shape.shape.len())
2193}
2194
2195fn truncate_ad_value_to_dynamic_axes(
2196    builder: &mut SemanticProgramBuilder,
2197    value: AdValue,
2198    shape_source: ProgramValue,
2199    dynamic_axes: &[usize],
2200) -> Result<AdValue, SemanticAdTransformError> {
2201    let AdValue::Value(value) = value else {
2202        return Ok(AdValue::Absent);
2203    };
2204    Ok(AdValue::Value(truncate_value_to_dynamic_axes(
2205        builder,
2206        value,
2207        shape_source,
2208        dynamic_axes,
2209    )?))
2210}
2211
2212fn truncate_value_to_dynamic_axes(
2213    builder: &mut SemanticProgramBuilder,
2214    mut value: ProgramValue,
2215    shape_source: ProgramValue,
2216    dynamic_axes: &[usize],
2217) -> Result<ProgramValue, ProgramBuildError> {
2218    for &axis in dynamic_axes {
2219        let size = builder.add_op(CoreSemanticOp::ShapeOf { axis }, &[shape_source])?[0];
2220        value = builder.add_op(CoreSemanticOp::DynamicTruncate { axis }, &[value, size])?[0];
2221    }
2222    Ok(value)
2223}
2224
2225fn conjugate_if_complex(
2226    builder: &mut SemanticProgramBuilder,
2227    value: ProgramValue,
2228) -> Result<ProgramValue, ProgramBuildError> {
2229    if matches!(
2230        builder.value_metadata(value)?.dtype(),
2231        DType::C32 | DType::C64
2232    ) {
2233        Ok(builder.add_op(CoreSemanticOp::Conj, &[value])?[0])
2234    } else {
2235        Ok(value)
2236    }
2237}
2238
2239fn finish_derivative(
2240    builder: SemanticProgramBuilder,
2241    derivative_input_indices: Vec<Option<usize>>,
2242    values: Vec<AdValue>,
2243) -> Result<SemanticAdProgram, SemanticAdTransformError> {
2244    let mut outputs = Vec::new();
2245    let derivative_output_indices = values
2246        .into_iter()
2247        .map(|value| match value {
2248            AdValue::Absent => None,
2249            AdValue::Value(value) => {
2250                let index = outputs.len();
2251                outputs.push(value);
2252                Some(index)
2253            }
2254        })
2255        .collect();
2256    let frozen = builder.finish(&outputs)?;
2257    let frozen = prune_dead_derivative_operations(frozen)?;
2258    let frozen = cancel_double_neg_derivative_operations(frozen)?;
2259    Ok(SemanticAdProgram {
2260        frozen,
2261        derivative_input_indices: derivative_input_indices.into_boxed_slice(),
2262        derivative_output_indices,
2263    })
2264}
2265
2266fn prune_dead_derivative_operations(
2267    frozen: FrozenProgram,
2268) -> Result<FrozenProgram, SemanticAdTransformError> {
2269    let mut roots = frozen.program.inputs().to_vec();
2270    let output_offset = roots.len();
2271    roots.extend_from_slice(frozen.program.outputs());
2272
2273    let mut builder = SemanticProgramBuilder::new();
2274    let imported = builder.import(ProgramImport {
2275        program: frozen.program.as_ref(),
2276        bindings: &frozen.bindings,
2277        roots: &roots,
2278    })?;
2279    let outputs = imported.roots()[output_offset..].to_vec();
2280    Ok(builder.finish(&outputs)?)
2281}
2282
2283fn cancel_double_neg_derivative_operations(
2284    frozen: FrozenProgram,
2285) -> Result<FrozenProgram, SemanticAdTransformError> {
2286    let operations = frozen.program.operations().collect::<Vec<_>>();
2287    if operations.iter().any(|operation| {
2288        !operation.effects().is_empty()
2289            || !operation.shape_guards().is_empty()
2290            || !matches!(operation.op(), SemanticOpRef::Core(_))
2291    }) {
2292        return Ok(frozen);
2293    }
2294
2295    let mut builder = SemanticProgramBuilder::new();
2296    let imported = builder.import(ProgramImport {
2297        program: frozen.program.as_ref(),
2298        bindings: &frozen.bindings,
2299        roots: frozen.program.inputs(),
2300    })?;
2301    let mut values = frozen
2302        .program
2303        .inputs()
2304        .iter()
2305        .copied()
2306        .zip(imported.roots().iter().copied())
2307        .collect::<HashMap<_, _>>();
2308    let mut neg_inputs = HashMap::<ProgramValue, ProgramValue>::new();
2309    let mut changed = false;
2310
2311    for operation in operations {
2312        let inputs = operation
2313            .inputs()
2314            .iter()
2315            .copied()
2316            .map(|value| {
2317                values.get(&value).copied().ok_or_else(|| {
2318                    SemanticAdTransformError::UnsupportedMetadata {
2319                        role: SemanticTransformRole::Jvp,
2320                        message: "derivative simplifier saw an unmapped value".into(),
2321                    }
2322                })
2323            })
2324            .collect::<Result<Vec<_>, _>>()?;
2325        let SemanticOpRef::Core(op) = operation.op() else {
2326            unreachable!("non-core operations returned above");
2327        };
2328
2329        if matches!(op, CoreSemanticOp::Neg) {
2330            let input = inputs[0];
2331            if let Some(inner) = neg_inputs.get(&input).copied() {
2332                values.insert(operation.outputs()[0], inner);
2333                changed = true;
2334                continue;
2335            }
2336            let output = builder.add_op(CoreSemanticOp::Neg, &[input])?[0];
2337            neg_inputs.insert(output, input);
2338            values.insert(operation.outputs()[0], output);
2339            continue;
2340        }
2341
2342        let outputs = builder.add_op(op.clone(), &inputs)?;
2343        for (source, output) in operation
2344            .outputs()
2345            .iter()
2346            .copied()
2347            .zip(outputs.iter().copied())
2348        {
2349            values.insert(source, output);
2350        }
2351    }
2352
2353    if !changed {
2354        return Ok(frozen);
2355    }
2356
2357    let outputs = frozen
2358        .program
2359        .outputs()
2360        .iter()
2361        .copied()
2362        .map(|value| {
2363            values.get(&value).copied().ok_or_else(|| {
2364                SemanticAdTransformError::UnsupportedMetadata {
2365                    role: SemanticTransformRole::Jvp,
2366                    message: "derivative simplifier saw an unmapped output".into(),
2367                }
2368            })
2369        })
2370        .collect::<Result<Vec<_>, _>>()?;
2371    prune_dead_derivative_operations(builder.finish(&outputs)?)
2372}
2373
2374fn validate_activity(
2375    role: SemanticTransformRole,
2376    field: &'static str,
2377    expected: usize,
2378    actual: usize,
2379) -> Result<(), SemanticAdTransformError> {
2380    if expected == actual {
2381        Ok(())
2382    } else {
2383        Err(SemanticAdTransformError::ActivityArity {
2384            role,
2385            field,
2386            expected,
2387            actual,
2388        })
2389    }
2390}
2391
2392fn unsupported_core(role: SemanticTransformRole, op: &CoreSemanticOp) -> SemanticAdTransformError {
2393    SemanticAdTransformError::UnsupportedCore {
2394        role,
2395        op: format!("{op:?}"),
2396    }
2397}