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