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 mut reduce_axes: Vec<_> = (0..output_shape.shape.len())
1260        .filter(|axis| !dims.contains(axis))
1261        .collect();
1262    reduce_axes.extend(
1263        dims.iter()
1264            .copied()
1265            .enumerate()
1266            .filter_map(|(input_axis, output_axis)| {
1267                (matches!(
1268                    input_shape.shape[input_axis],
1269                    tenferro_ops::dim_expr::DimExpr::Const(1)
1270                ) && input_shape.shape[input_axis] != output_shape.shape[output_axis])
1271                    .then_some(output_axis)
1272            }),
1273    );
1274    reduce_axes.sort_unstable();
1275    reduce_axes.dedup();
1276    if !reduce_axes.is_empty() {
1277        value = builder.add_op(
1278            CoreSemanticOp::ReduceSum {
1279                axes: reduce_axes.clone(),
1280            },
1281            &[value],
1282        )?[0];
1283    }
1284
1285    let remaining_output_axes: Vec<_> = (0..output_shape.shape.len())
1286        .filter(|axis| !reduce_axes.contains(axis))
1287        .collect();
1288    let perm: Vec<_> = dims
1289        .iter()
1290        .copied()
1291        .filter(|axis| !reduce_axes.contains(axis))
1292        .map(|axis| {
1293            remaining_output_axes
1294                .iter()
1295                .position(|candidate| *candidate == axis)
1296                .expect("broadcast dims survive unless reduced")
1297        })
1298        .collect();
1299    if perm.iter().copied().ne(0..perm.len()) {
1300        value = builder.add_op(CoreSemanticOp::Transpose { perm }, &[value])?[0];
1301    }
1302    if builder.value_metadata(value)?.shape() != builder.value_metadata(primal_input)?.shape() {
1303        value = reshape_value_to_shape(builder, value, primal_input, &input_shape)?;
1304    }
1305    value =
1306        truncate_value_to_dynamic_axes(builder, value, primal_input, &input_shape.dynamic_axes)?;
1307    Ok(AdValue::Value(value))
1308}
1309
1310fn exact_value_shape(
1311    builder: &SemanticProgramBuilder,
1312    value: ProgramValue,
1313    role: SemanticTransformRole,
1314    field: &'static str,
1315) -> Result<Vec<tenferro_ops::dim_expr::DimExpr>, SemanticAdTransformError> {
1316    exact_shape(builder.value_metadata(value)?.shape(), role, field)
1317}
1318
1319fn is_differentiable_dtype(dtype: DType) -> bool {
1320    matches!(dtype, DType::F32 | DType::F64 | DType::C32 | DType::C64)
1321}
1322
1323fn is_complex_dtype(dtype: DType) -> bool {
1324    matches!(dtype, DType::C32 | DType::C64)
1325}
1326
1327fn abs_output_dtype(dtype: DType) -> DType {
1328    match dtype {
1329        DType::C32 => DType::F32,
1330        DType::C64 => DType::F64,
1331        other => other,
1332    }
1333}
1334
1335fn add_ad_values(
1336    builder: &mut SemanticProgramBuilder,
1337    lhs: AdValue,
1338    rhs: AdValue,
1339) -> Result<AdValue, ProgramBuildError> {
1340    match (lhs, rhs) {
1341        (AdValue::Absent, value) | (value, AdValue::Absent) => Ok(value),
1342        (AdValue::Value(lhs), AdValue::Value(rhs)) => Ok(AdValue::Value(
1343            builder.add_op(CoreSemanticOp::Add, &[lhs, rhs])?[0],
1344        )),
1345    }
1346}
1347
1348fn sub_ad_values(
1349    builder: &mut SemanticProgramBuilder,
1350    lhs: AdValue,
1351    rhs: AdValue,
1352) -> Result<AdValue, ProgramBuildError> {
1353    match (lhs, rhs) {
1354        (AdValue::Absent, AdValue::Absent) => Ok(AdValue::Absent),
1355        (value, AdValue::Absent) => Ok(value),
1356        (AdValue::Absent, AdValue::Value(rhs)) => Ok(AdValue::Value(
1357            builder.add_op(CoreSemanticOp::Neg, &[rhs])?[0],
1358        )),
1359        (AdValue::Value(lhs), AdValue::Value(rhs)) => Ok(AdValue::Value(
1360            builder.add_op(CoreSemanticOp::Sub, &[lhs, rhs])?[0],
1361        )),
1362    }
1363}
1364
1365fn unary_ad_value(
1366    builder: &mut SemanticProgramBuilder,
1367    op: CoreSemanticOp,
1368    value: AdValue,
1369) -> Result<AdValue, ProgramBuildError> {
1370    match value {
1371        AdValue::Absent => Ok(AdValue::Absent),
1372        AdValue::Value(value) => Ok(AdValue::Value(builder.add_op(op, &[value])?[0])),
1373    }
1374}
1375
1376fn multiply_ad_value(
1377    builder: &mut SemanticProgramBuilder,
1378    value: AdValue,
1379    coefficient: ProgramValue,
1380) -> Result<AdValue, ProgramBuildError> {
1381    match value {
1382        AdValue::Absent => Ok(AdValue::Absent),
1383        AdValue::Value(value) => Ok(AdValue::Value(
1384            builder.add_op(CoreSemanticOp::Mul, &[value, coefficient])?[0],
1385        )),
1386    }
1387}
1388
1389fn divide_ad_value(
1390    builder: &mut SemanticProgramBuilder,
1391    value: AdValue,
1392    denominator: ProgramValue,
1393) -> Result<AdValue, ProgramBuildError> {
1394    match value {
1395        AdValue::Absent => Ok(AdValue::Absent),
1396        AdValue::Value(value) => Ok(AdValue::Value(
1397            builder.add_op(CoreSemanticOp::Div, &[value, denominator])?[0],
1398        )),
1399    }
1400}
1401
1402fn convert_ad_value(
1403    builder: &mut SemanticProgramBuilder,
1404    value: AdValue,
1405    from: DType,
1406    to: DType,
1407) -> Result<AdValue, ProgramBuildError> {
1408    if from == to {
1409        return Ok(value);
1410    }
1411    unary_ad_value(builder, CoreSemanticOp::Convert { from, to }, value)
1412}
1413
1414fn zero_from_ad_value(
1415    builder: &mut SemanticProgramBuilder,
1416    value: AdValue,
1417) -> Result<AdValue, ProgramBuildError> {
1418    let negated = unary_ad_value(builder, CoreSemanticOp::Neg, value)?;
1419    add_ad_values(builder, value, negated)
1420}
1421
1422fn select_ad_values(
1423    builder: &mut SemanticProgramBuilder,
1424    condition: ProgramValue,
1425    on_true: AdValue,
1426    on_false: AdValue,
1427) -> Result<AdValue, ProgramBuildError> {
1428    match (on_true, on_false) {
1429        (AdValue::Absent, AdValue::Absent) => Ok(AdValue::Absent),
1430        (AdValue::Value(on_true), AdValue::Value(on_false)) => Ok(AdValue::Value(
1431            builder.add_op(CoreSemanticOp::Select, &[condition, on_true, on_false])?[0],
1432        )),
1433        (AdValue::Value(on_true), AdValue::Absent) => {
1434            let AdValue::Value(zero) = zero_from_ad_value(builder, AdValue::Value(on_true))? else {
1435                unreachable!();
1436            };
1437            Ok(AdValue::Value(
1438                builder.add_op(CoreSemanticOp::Select, &[condition, on_true, zero])?[0],
1439            ))
1440        }
1441        (AdValue::Absent, AdValue::Value(on_false)) => {
1442            let AdValue::Value(zero) = zero_from_ad_value(builder, AdValue::Value(on_false))?
1443            else {
1444                unreachable!();
1445            };
1446            Ok(AdValue::Value(
1447                builder.add_op(CoreSemanticOp::Select, &[condition, zero, on_false])?[0],
1448            ))
1449        }
1450    }
1451}
1452
1453fn split_select_cotangent(
1454    builder: &mut SemanticProgramBuilder,
1455    condition: ProgramValue,
1456    cotangent: AdValue,
1457    true_active: bool,
1458    false_active: bool,
1459) -> Result<(AdValue, AdValue), ProgramBuildError> {
1460    if !true_active && !false_active {
1461        return Ok((AdValue::Absent, AdValue::Absent));
1462    }
1463    let AdValue::Value(cotangent) = cotangent else {
1464        return Ok((AdValue::Absent, AdValue::Absent));
1465    };
1466    let AdValue::Value(zero) = zero_from_ad_value(builder, AdValue::Value(cotangent))? else {
1467        unreachable!();
1468    };
1469    let on_true = if true_active {
1470        AdValue::Value(builder.add_op(CoreSemanticOp::Select, &[condition, cotangent, zero])?[0])
1471    } else {
1472        AdValue::Absent
1473    };
1474    let on_false = if false_active {
1475        AdValue::Value(builder.add_op(CoreSemanticOp::Select, &[condition, zero, cotangent])?[0])
1476    } else {
1477        AdValue::Absent
1478    };
1479    Ok((on_true, on_false))
1480}
1481
1482fn linearize_extrema(
1483    builder: &mut SemanticProgramBuilder,
1484    op: &CoreSemanticOp,
1485    primal_inputs: &[ProgramValue],
1486    tangent_inputs: &[AdValue],
1487) -> Result<AdValue, SemanticAdTransformError> {
1488    let output = builder.add_op(op.clone(), primal_inputs)?[0];
1489    let lhs_eq_output = builder.add_op(
1490        CoreSemanticOp::Compare(CompareDir::Eq),
1491        &[primal_inputs[0], output],
1492    )?[0];
1493    let rhs_eq_output = builder.add_op(
1494        CoreSemanticOp::Compare(CompareDir::Eq),
1495        &[primal_inputs[1], output],
1496    )?[0];
1497    let lhs = balanced_extrema_contribution(
1498        builder,
1499        tangent_inputs[0],
1500        lhs_eq_output,
1501        rhs_eq_output,
1502        SemanticTransformRole::Jvp,
1503    )?;
1504    let rhs = balanced_extrema_contribution(
1505        builder,
1506        tangent_inputs[1],
1507        rhs_eq_output,
1508        lhs_eq_output,
1509        SemanticTransformRole::Jvp,
1510    )?;
1511    Ok(add_ad_values(builder, lhs, rhs)?)
1512}
1513
1514fn extrema_vjp(
1515    builder: &mut SemanticProgramBuilder,
1516    op: &CoreSemanticOp,
1517    primal_inputs: &[ProgramValue],
1518    cotangent: AdValue,
1519    active_inputs: &[bool],
1520) -> Result<Vec<AdValue>, SemanticAdTransformError> {
1521    let output = builder.add_op(op.clone(), primal_inputs)?[0];
1522    let lhs_eq_output = builder.add_op(
1523        CoreSemanticOp::Compare(CompareDir::Eq),
1524        &[primal_inputs[0], output],
1525    )?[0];
1526    let rhs_eq_output = builder.add_op(
1527        CoreSemanticOp::Compare(CompareDir::Eq),
1528        &[primal_inputs[1], output],
1529    )?[0];
1530    let lhs = balanced_extrema_contribution(
1531        builder,
1532        cotangent,
1533        lhs_eq_output,
1534        rhs_eq_output,
1535        SemanticTransformRole::Vjp,
1536    )?;
1537    let rhs = balanced_extrema_contribution(
1538        builder,
1539        cotangent,
1540        rhs_eq_output,
1541        lhs_eq_output,
1542        SemanticTransformRole::Vjp,
1543    )?;
1544    Ok(vec![
1545        normalize_ad_value(builder, lhs, active_inputs[0], primal_inputs[0])?,
1546        normalize_ad_value(builder, rhs, active_inputs[1], primal_inputs[1])?,
1547    ])
1548}
1549
1550fn balanced_extrema_contribution(
1551    builder: &mut SemanticProgramBuilder,
1552    active: AdValue,
1553    self_eq_output: ProgramValue,
1554    other_eq_output: ProgramValue,
1555    role: SemanticTransformRole,
1556) -> Result<AdValue, SemanticAdTransformError> {
1557    let AdValue::Value(active) = active else {
1558        return Ok(AdValue::Absent);
1559    };
1560    let zero = builder.add_op(CoreSemanticOp::Sub, &[active, active])?[0];
1561    let selected = builder.add_op(CoreSemanticOp::Select, &[self_eq_output, active, zero])?[0];
1562    let one = one_like(builder, active, role)?;
1563    let two = builder.add_op(CoreSemanticOp::Add, &[one, one])?[0];
1564    let half = builder.add_op(CoreSemanticOp::Div, &[selected, two])?[0];
1565    Ok(AdValue::Value(
1566        builder.add_op(CoreSemanticOp::Select, &[other_eq_output, half, selected])?[0],
1567    ))
1568}
1569
1570fn linearize_clamp(
1571    builder: &mut SemanticProgramBuilder,
1572    primal_inputs: &[ProgramValue],
1573    tangent_inputs: &[AdValue],
1574) -> Result<AdValue, ProgramBuildError> {
1575    let masks = clamp_masks(builder, primal_inputs)?;
1576    let input = mask_ad_value(builder, tangent_inputs[0], &[masks[0], masks[1]])?;
1577    let lower = mask_ad_value(builder, tangent_inputs[1], &[masks[2], masks[3]])?;
1578    let upper = mask_ad_value(builder, tangent_inputs[2], &[masks[4]])?;
1579    let input_and_lower = add_ad_values(builder, input, lower)?;
1580    add_ad_values(builder, input_and_lower, upper)
1581}
1582
1583fn clamp_vjp(
1584    builder: &mut SemanticProgramBuilder,
1585    primal_inputs: &[ProgramValue],
1586    cotangent: AdValue,
1587    active_inputs: &[bool],
1588) -> Result<Vec<AdValue>, SemanticAdTransformError> {
1589    let masks = clamp_masks(builder, primal_inputs)?;
1590    let input = mask_ad_value(builder, cotangent, &[masks[0], masks[1]])?;
1591    let lower = mask_ad_value(builder, cotangent, &[masks[2], masks[3]])?;
1592    let upper = mask_ad_value(builder, cotangent, &[masks[4]])?;
1593    Ok(vec![
1594        normalize_ad_value(builder, input, active_inputs[0], primal_inputs[0])?,
1595        normalize_ad_value(builder, lower, active_inputs[1], primal_inputs[1])?,
1596        normalize_ad_value(builder, upper, active_inputs[2], primal_inputs[2])?,
1597    ])
1598}
1599
1600fn clamp_masks(
1601    builder: &mut SemanticProgramBuilder,
1602    primal_inputs: &[ProgramValue],
1603) -> Result<[ProgramValue; 5], ProgramBuildError> {
1604    let input = primal_inputs[0];
1605    let lower = primal_inputs[1];
1606    let upper = primal_inputs[2];
1607    let input_gt_lower =
1608        builder.add_op(CoreSemanticOp::Compare(CompareDir::Gt), &[input, lower])?[0];
1609    let input_lt_upper =
1610        builder.add_op(CoreSemanticOp::Compare(CompareDir::Lt), &[input, upper])?[0];
1611    let lower_gt_input =
1612        builder.add_op(CoreSemanticOp::Compare(CompareDir::Gt), &[lower, input])?[0];
1613    let lower_lt_upper =
1614        builder.add_op(CoreSemanticOp::Compare(CompareDir::Lt), &[lower, upper])?[0];
1615    let max_input_lower = builder.add_op(CoreSemanticOp::Maximum, &[input, lower])?[0];
1616    let upper_lt_max_input_lower = builder.add_op(
1617        CoreSemanticOp::Compare(CompareDir::Lt),
1618        &[upper, max_input_lower],
1619    )?[0];
1620    Ok([
1621        input_gt_lower,
1622        input_lt_upper,
1623        lower_gt_input,
1624        lower_lt_upper,
1625        upper_lt_max_input_lower,
1626    ])
1627}
1628
1629fn mask_ad_value(
1630    builder: &mut SemanticProgramBuilder,
1631    active: AdValue,
1632    conditions: &[ProgramValue],
1633) -> Result<AdValue, ProgramBuildError> {
1634    let AdValue::Value(active) = active else {
1635        return Ok(AdValue::Absent);
1636    };
1637    let zero = builder.add_op(CoreSemanticOp::Sub, &[active, active])?[0];
1638    let mut value = active;
1639    for condition in conditions {
1640        value = builder.add_op(CoreSemanticOp::Select, &[*condition, value, zero])?[0];
1641    }
1642    Ok(AdValue::Value(value))
1643}
1644
1645fn linearize_analytic_unary(
1646    builder: &mut SemanticProgramBuilder,
1647    op: &CoreSemanticOp,
1648    primal_input: ProgramValue,
1649    tangent: AdValue,
1650) -> Result<AdValue, SemanticAdTransformError> {
1651    if matches!(tangent, AdValue::Absent) {
1652        return Ok(AdValue::Absent);
1653    }
1654    let coefficient =
1655        analytic_unary_coefficient(builder, op, primal_input, SemanticTransformRole::Jvp)?;
1656    Ok(multiply_ad_value(builder, tangent, coefficient)?)
1657}
1658
1659fn linearize_sign(
1660    builder: &mut SemanticProgramBuilder,
1661    primal_input: ProgramValue,
1662    tangent: AdValue,
1663) -> Result<AdValue, SemanticAdTransformError> {
1664    let AdValue::Value(tangent_value) = tangent else {
1665        return Ok(AdValue::Absent);
1666    };
1667    let input_dtype = builder.value_metadata(primal_input)?.dtype();
1668    if !is_complex_dtype(input_dtype) {
1669        return Ok(AdValue::Absent);
1670    }
1671
1672    let zero = builder.add_op(CoreSemanticOp::Sub, &[primal_input, primal_input])?[0];
1673    let zero_mask = builder.add_op(
1674        CoreSemanticOp::Compare(CompareDir::Eq),
1675        &[primal_input, zero],
1676    )?[0];
1677    let sign = builder.add_op(CoreSemanticOp::Sign, &[primal_input])?[0];
1678    let abs = builder.add_op(CoreSemanticOp::Abs, &[primal_input])?[0];
1679    let output_dtype = abs_output_dtype(input_dtype);
1680    let complex_abs = builder.add_op(
1681        CoreSemanticOp::Convert {
1682            from: output_dtype,
1683            to: input_dtype,
1684        },
1685        &[abs],
1686    )?[0];
1687    let one = one_like(builder, complex_abs, SemanticTransformRole::Jvp)?;
1688    let safe_abs = builder.add_op(CoreSemanticOp::Select, &[zero_mask, one, complex_abs])?[0];
1689    let safe_sign = builder.add_op(CoreSemanticOp::Select, &[zero_mask, zero, sign])?[0];
1690    let conj_sign = builder.add_op(CoreSemanticOp::Conj, &[safe_sign])?[0];
1691
1692    let abs_tangent_complex = multiply_ad_value(builder, AdValue::Value(tangent_value), conj_sign)?;
1693    let abs_tangent = convert_ad_value(builder, abs_tangent_complex, input_dtype, output_dtype)?;
1694    let abs_tangent = convert_ad_value(builder, abs_tangent, output_dtype, input_dtype)?;
1695    let tangent_over_abs = divide_ad_value(builder, AdValue::Value(tangent_value), safe_abs)?;
1696    let sign_times_abs_tangent = multiply_ad_value(builder, abs_tangent, safe_sign)?;
1697    let correction = divide_ad_value(builder, sign_times_abs_tangent, safe_abs)?;
1698    let derivative = sub_ad_values(builder, tangent_over_abs, correction)?;
1699    let zero_derivative = zero_from_ad_value(builder, AdValue::Value(tangent_value))?;
1700    Ok(select_ad_values(
1701        builder,
1702        zero_mask,
1703        zero_derivative,
1704        derivative,
1705    )?)
1706}
1707
1708fn analytic_unary_coefficient(
1709    builder: &mut SemanticProgramBuilder,
1710    op: &CoreSemanticOp,
1711    primal_input: ProgramValue,
1712    role: SemanticTransformRole,
1713) -> Result<ProgramValue, SemanticAdTransformError> {
1714    let coefficient = match op {
1715        CoreSemanticOp::Exp | CoreSemanticOp::Expm1 => {
1716            builder.add_op(CoreSemanticOp::Exp, &[primal_input])?[0]
1717        }
1718        CoreSemanticOp::Log => {
1719            let one = one_like(builder, primal_input, role)?;
1720            builder.add_op(CoreSemanticOp::Div, &[one, primal_input])?[0]
1721        }
1722        CoreSemanticOp::Sin => builder.add_op(CoreSemanticOp::Cos, &[primal_input])?[0],
1723        CoreSemanticOp::Cos => {
1724            let sin = builder.add_op(CoreSemanticOp::Sin, &[primal_input])?[0];
1725            builder.add_op(CoreSemanticOp::Neg, &[sin])?[0]
1726        }
1727        CoreSemanticOp::Tanh => {
1728            let tanh = builder.add_op(CoreSemanticOp::Tanh, &[primal_input])?[0];
1729            let square = builder.add_op(CoreSemanticOp::Mul, &[tanh, tanh])?[0];
1730            let one = one_like(builder, primal_input, role)?;
1731            builder.add_op(CoreSemanticOp::Sub, &[one, square])?[0]
1732        }
1733        CoreSemanticOp::Sqrt => {
1734            let sqrt = builder.add_op(CoreSemanticOp::Sqrt, &[primal_input])?[0];
1735            let twice = builder.add_op(CoreSemanticOp::Add, &[sqrt, sqrt])?[0];
1736            let one = one_like(builder, primal_input, role)?;
1737            builder.add_op(CoreSemanticOp::Div, &[one, twice])?[0]
1738        }
1739        CoreSemanticOp::Rsqrt => {
1740            let rsqrt = builder.add_op(CoreSemanticOp::Rsqrt, &[primal_input])?[0];
1741            let negated = builder.add_op(CoreSemanticOp::Neg, &[rsqrt])?[0];
1742            let twice = builder.add_op(CoreSemanticOp::Add, &[primal_input, primal_input])?[0];
1743            builder.add_op(CoreSemanticOp::Div, &[negated, twice])?[0]
1744        }
1745        CoreSemanticOp::Log1p => {
1746            let one = one_like(builder, primal_input, role)?;
1747            let denominator = builder.add_op(CoreSemanticOp::Add, &[primal_input, one])?[0];
1748            builder.add_op(CoreSemanticOp::Div, &[one, denominator])?[0]
1749        }
1750        _ => return Err(unsupported_core(role, op)),
1751    };
1752    Ok(coefficient)
1753}
1754
1755fn one_like(
1756    builder: &mut SemanticProgramBuilder,
1757    anchor: ProgramValue,
1758    role: SemanticTransformRole,
1759) -> Result<ProgramValue, SemanticAdTransformError> {
1760    let metadata = builder.value_metadata(anchor)?.clone();
1761    let dtype = metadata.dtype();
1762    let bytes = match dtype {
1763        DType::F32 => 1.0_f32.to_le_bytes().to_vec(),
1764        DType::F64 => 1.0_f64.to_le_bytes().to_vec(),
1765        DType::C32 => {
1766            let mut bytes = 1.0_f32.to_le_bytes().to_vec();
1767            bytes.extend_from_slice(&0.0_f32.to_le_bytes());
1768            bytes
1769        }
1770        DType::C64 => {
1771            let mut bytes = 1.0_f64.to_le_bytes().to_vec();
1772            bytes.extend_from_slice(&0.0_f64.to_le_bytes());
1773            bytes
1774        }
1775        _ => {
1776            return Err(SemanticAdTransformError::UnsupportedMetadata {
1777                role,
1778                message: format!("cannot construct a differentiable one for {dtype:?}"),
1779            });
1780        }
1781    };
1782    let scalar = builder.add_op(CoreSemanticOp::Constant { dtype, bytes }, &[])?[0];
1783    if metadata.shape().is_empty() {
1784        Ok(scalar)
1785    } else {
1786        let shape = shape_plan(metadata.shape(), role, "one-like anchor")?;
1787        let one = broadcast_value_in_dim_to_shape(builder, scalar, anchor, &shape, Vec::new())?;
1788        Ok(truncate_value_to_dynamic_axes(
1789            builder,
1790            one,
1791            anchor,
1792            &shape.dynamic_axes,
1793        )?)
1794    }
1795}
1796
1797fn active_cotangent(
1798    builder: &mut SemanticProgramBuilder,
1799    cotangent: AdValue,
1800    active: bool,
1801    primal_input: ProgramValue,
1802) -> Result<AdValue, SemanticAdTransformError> {
1803    normalize_ad_value(builder, cotangent, active, primal_input)
1804}
1805
1806fn normalize_ad_value(
1807    builder: &mut SemanticProgramBuilder,
1808    value: AdValue,
1809    active: bool,
1810    primal_input: ProgramValue,
1811) -> Result<AdValue, SemanticAdTransformError> {
1812    if !active {
1813        return Ok(AdValue::Absent);
1814    }
1815    let AdValue::Value(mut value) = value else {
1816        return Ok(AdValue::Absent);
1817    };
1818    let target_metadata = builder.value_metadata(primal_input)?.clone();
1819    let value_metadata = builder.value_metadata(value)?.clone();
1820    let target_shape = shape_plan(
1821        target_metadata.shape(),
1822        SemanticTransformRole::Vjp,
1823        "primal input",
1824    )?;
1825    let value_shape = shape_plan(
1826        value_metadata.shape(),
1827        SemanticTransformRole::Vjp,
1828        "cotangent",
1829    )?;
1830    if value_shape.shape.len() < target_shape.shape.len() {
1831        return Err(SemanticAdTransformError::UnsupportedMetadata {
1832            role: SemanticTransformRole::Vjp,
1833            message: "cotangent rank is smaller than its primal-input rank".into(),
1834        });
1835    }
1836    let leading = value_shape.shape.len() - target_shape.shape.len();
1837    let mut axes: Vec<_> = (0..leading).collect();
1838    axes.extend(
1839        target_shape
1840            .shape
1841            .iter()
1842            .zip(value_shape.shape.iter().skip(leading))
1843            .enumerate()
1844            .filter_map(|(axis, (target, actual))| {
1845                (matches!(target, tenferro_ops::dim_expr::DimExpr::Const(1)) && target != actual)
1846                    .then_some(axis + leading)
1847            }),
1848    );
1849    if !axes.is_empty() {
1850        value = builder.add_op(CoreSemanticOp::ReduceSum { axes }, &[value])?[0];
1851    }
1852    if builder.value_metadata(value)?.shape() != target_metadata.shape() {
1853        value = reshape_value_to_shape(builder, value, primal_input, &target_shape)?;
1854    }
1855    value =
1856        truncate_value_to_dynamic_axes(builder, value, primal_input, &target_shape.dynamic_axes)?;
1857    let value_dtype = builder.value_metadata(value)?.dtype();
1858    if value_dtype != target_metadata.dtype() {
1859        value = builder.add_op(
1860            CoreSemanticOp::Convert {
1861                from: value_dtype,
1862                to: target_metadata.dtype(),
1863            },
1864            &[value],
1865        )?[0];
1866    }
1867    Ok(AdValue::Value(value))
1868}
1869
1870fn exact_shape(
1871    shape: &[tenferro_ops::ShapeExtent<tenferro_ops::dim_expr::DimExpr>],
1872    role: SemanticTransformRole,
1873    field: &'static str,
1874) -> Result<Vec<tenferro_ops::dim_expr::DimExpr>, SemanticAdTransformError> {
1875    shape
1876        .iter()
1877        .map(|extent| {
1878            extent.as_exact().cloned().ok_or_else(|| {
1879                SemanticAdTransformError::UnsupportedMetadata {
1880                    role,
1881                    message: format!("{field} has a bounded or unknown extent"),
1882                }
1883            })
1884        })
1885        .collect()
1886}
1887
1888fn value_shape_plan(
1889    builder: &SemanticProgramBuilder,
1890    value: ProgramValue,
1891    role: SemanticTransformRole,
1892    field: &'static str,
1893) -> Result<ValueShapePlan, SemanticAdTransformError> {
1894    shape_plan(builder.value_metadata(value)?.shape(), role, field)
1895}
1896
1897fn shape_plan(
1898    shape: &[ShapeExtent<DimExpr>],
1899    role: SemanticTransformRole,
1900    field: &'static str,
1901) -> Result<ValueShapePlan, SemanticAdTransformError> {
1902    let mut planned_shape = Vec::with_capacity(shape.len());
1903    let mut dynamic_axes = Vec::new();
1904    for (axis, extent) in shape.iter().enumerate() {
1905        match extent {
1906            ShapeExtent::Exact(expression) => planned_shape.push(expression.clone()),
1907            ShapeExtent::UpperBound(expression) => {
1908                planned_shape.push(expression.clone());
1909                dynamic_axes.push(axis);
1910            }
1911            ShapeExtent::Unknown => {
1912                return Err(SemanticAdTransformError::UnsupportedMetadata {
1913                    role,
1914                    message: format!("{field} has an unknown extent without an upper bound"),
1915                });
1916            }
1917        }
1918    }
1919    Ok(ValueShapePlan {
1920        shape: planned_shape,
1921        dynamic_axes,
1922    })
1923}
1924
1925fn reshape_ad_value_to_shape(
1926    builder: &mut SemanticProgramBuilder,
1927    value: AdValue,
1928    shape_source: ProgramValue,
1929    shape: &ValueShapePlan,
1930) -> Result<AdValue, ProgramBuildError> {
1931    match value {
1932        AdValue::Absent => Ok(AdValue::Absent),
1933        AdValue::Value(value) => Ok(AdValue::Value(reshape_value_to_shape(
1934            builder,
1935            value,
1936            shape_source,
1937            shape,
1938        )?)),
1939    }
1940}
1941
1942fn reshape_value_to_shape(
1943    builder: &mut SemanticProgramBuilder,
1944    value: ProgramValue,
1945    shape_source: ProgramValue,
1946    shape: &ValueShapePlan,
1947) -> Result<ProgramValue, ProgramBuildError> {
1948    let mut inputs = vec![value];
1949    let to_shape = payload_shape_for_shape_source(shape, &mut inputs, shape_source);
1950    Ok(builder.add_op(CoreSemanticOp::Reshape { to_shape }, &inputs)?[0])
1951}
1952
1953fn broadcast_ad_value_in_dim_to_shape(
1954    builder: &mut SemanticProgramBuilder,
1955    value: AdValue,
1956    shape_source: ProgramValue,
1957    shape: &ValueShapePlan,
1958    dims: Vec<usize>,
1959) -> Result<AdValue, ProgramBuildError> {
1960    match value {
1961        AdValue::Absent => Ok(AdValue::Absent),
1962        AdValue::Value(value) => Ok(AdValue::Value(broadcast_value_in_dim_to_shape(
1963            builder,
1964            value,
1965            shape_source,
1966            shape,
1967            dims,
1968        )?)),
1969    }
1970}
1971
1972fn broadcast_value_in_dim_to_shape(
1973    builder: &mut SemanticProgramBuilder,
1974    value: ProgramValue,
1975    shape_source: ProgramValue,
1976    shape: &ValueShapePlan,
1977    dims: Vec<usize>,
1978) -> Result<ProgramValue, ProgramBuildError> {
1979    let mut inputs = vec![value];
1980    let shape = payload_shape_for_shape_source(shape, &mut inputs, shape_source);
1981    Ok(builder.add_op(CoreSemanticOp::BroadcastInDim { shape, dims }, &inputs)?[0])
1982}
1983
1984fn payload_shape_for_shape_source(
1985    shape: &ValueShapePlan,
1986    inputs: &mut Vec<ProgramValue>,
1987    shape_source: ProgramValue,
1988) -> Vec<DimExpr> {
1989    if DimExpr::max_input_idx_all(&shape.shape).is_none() {
1990        return shape.shape.clone();
1991    }
1992    let input_idx = inputs
1993        .iter()
1994        .position(|&input| input == shape_source)
1995        .unwrap_or_else(|| {
1996            let input_idx = inputs.len();
1997            inputs.push(shape_source);
1998            input_idx
1999        });
2000    DimExpr::input_shape(input_idx, shape.shape.len())
2001}
2002
2003fn truncate_ad_value_to_dynamic_axes(
2004    builder: &mut SemanticProgramBuilder,
2005    value: AdValue,
2006    shape_source: ProgramValue,
2007    dynamic_axes: &[usize],
2008) -> Result<AdValue, SemanticAdTransformError> {
2009    let AdValue::Value(value) = value else {
2010        return Ok(AdValue::Absent);
2011    };
2012    Ok(AdValue::Value(truncate_value_to_dynamic_axes(
2013        builder,
2014        value,
2015        shape_source,
2016        dynamic_axes,
2017    )?))
2018}
2019
2020fn truncate_value_to_dynamic_axes(
2021    builder: &mut SemanticProgramBuilder,
2022    mut value: ProgramValue,
2023    shape_source: ProgramValue,
2024    dynamic_axes: &[usize],
2025) -> Result<ProgramValue, ProgramBuildError> {
2026    for &axis in dynamic_axes {
2027        let size = builder.add_op(CoreSemanticOp::ShapeOf { axis }, &[shape_source])?[0];
2028        value = builder.add_op(CoreSemanticOp::DynamicTruncate { axis }, &[value, size])?[0];
2029    }
2030    Ok(value)
2031}
2032
2033fn conjugate_if_complex(
2034    builder: &mut SemanticProgramBuilder,
2035    value: ProgramValue,
2036) -> Result<ProgramValue, ProgramBuildError> {
2037    if matches!(
2038        builder.value_metadata(value)?.dtype(),
2039        DType::C32 | DType::C64
2040    ) {
2041        Ok(builder.add_op(CoreSemanticOp::Conj, &[value])?[0])
2042    } else {
2043        Ok(value)
2044    }
2045}
2046
2047fn finish_derivative(
2048    builder: SemanticProgramBuilder,
2049    derivative_input_indices: Vec<Option<usize>>,
2050    values: Vec<AdValue>,
2051) -> Result<SemanticAdProgram, SemanticAdTransformError> {
2052    let mut outputs = Vec::new();
2053    let derivative_output_indices = values
2054        .into_iter()
2055        .map(|value| match value {
2056            AdValue::Absent => None,
2057            AdValue::Value(value) => {
2058                let index = outputs.len();
2059                outputs.push(value);
2060                Some(index)
2061            }
2062        })
2063        .collect();
2064    let frozen = builder.finish(&outputs)?;
2065    let frozen = prune_dead_derivative_operations(frozen)?;
2066    let frozen = cancel_double_neg_derivative_operations(frozen)?;
2067    Ok(SemanticAdProgram {
2068        frozen,
2069        derivative_input_indices: derivative_input_indices.into_boxed_slice(),
2070        derivative_output_indices,
2071    })
2072}
2073
2074fn prune_dead_derivative_operations(
2075    frozen: FrozenProgram,
2076) -> Result<FrozenProgram, SemanticAdTransformError> {
2077    let mut roots = frozen.program.inputs().to_vec();
2078    let output_offset = roots.len();
2079    roots.extend_from_slice(frozen.program.outputs());
2080
2081    let mut builder = SemanticProgramBuilder::new();
2082    let imported = builder.import(ProgramImport {
2083        program: frozen.program.as_ref(),
2084        bindings: &frozen.bindings,
2085        roots: &roots,
2086    })?;
2087    let outputs = imported.roots()[output_offset..].to_vec();
2088    Ok(builder.finish(&outputs)?)
2089}
2090
2091fn cancel_double_neg_derivative_operations(
2092    frozen: FrozenProgram,
2093) -> Result<FrozenProgram, SemanticAdTransformError> {
2094    let operations = frozen.program.operations().collect::<Vec<_>>();
2095    if operations.iter().any(|operation| {
2096        !operation.effects().is_empty()
2097            || !operation.shape_guards().is_empty()
2098            || !matches!(operation.op(), SemanticOpRef::Core(_))
2099    }) {
2100        return Ok(frozen);
2101    }
2102
2103    let mut builder = SemanticProgramBuilder::new();
2104    let imported = builder.import(ProgramImport {
2105        program: frozen.program.as_ref(),
2106        bindings: &frozen.bindings,
2107        roots: frozen.program.inputs(),
2108    })?;
2109    let mut values = frozen
2110        .program
2111        .inputs()
2112        .iter()
2113        .copied()
2114        .zip(imported.roots().iter().copied())
2115        .collect::<HashMap<_, _>>();
2116    let mut neg_inputs = HashMap::<ProgramValue, ProgramValue>::new();
2117    let mut changed = false;
2118
2119    for operation in operations {
2120        let inputs = operation
2121            .inputs()
2122            .iter()
2123            .copied()
2124            .map(|value| {
2125                values.get(&value).copied().ok_or_else(|| {
2126                    SemanticAdTransformError::UnsupportedMetadata {
2127                        role: SemanticTransformRole::Jvp,
2128                        message: "derivative simplifier saw an unmapped value".into(),
2129                    }
2130                })
2131            })
2132            .collect::<Result<Vec<_>, _>>()?;
2133        let SemanticOpRef::Core(op) = operation.op() else {
2134            unreachable!("non-core operations returned above");
2135        };
2136
2137        if matches!(op, CoreSemanticOp::Neg) {
2138            let input = inputs[0];
2139            if let Some(inner) = neg_inputs.get(&input).copied() {
2140                values.insert(operation.outputs()[0], inner);
2141                changed = true;
2142                continue;
2143            }
2144            let output = builder.add_op(CoreSemanticOp::Neg, &[input])?[0];
2145            neg_inputs.insert(output, input);
2146            values.insert(operation.outputs()[0], output);
2147            continue;
2148        }
2149
2150        let outputs = builder.add_op(op.clone(), &inputs)?;
2151        for (source, output) in operation
2152            .outputs()
2153            .iter()
2154            .copied()
2155            .zip(outputs.iter().copied())
2156        {
2157            values.insert(source, output);
2158        }
2159    }
2160
2161    if !changed {
2162        return Ok(frozen);
2163    }
2164
2165    let outputs = frozen
2166        .program
2167        .outputs()
2168        .iter()
2169        .copied()
2170        .map(|value| {
2171            values.get(&value).copied().ok_or_else(|| {
2172                SemanticAdTransformError::UnsupportedMetadata {
2173                    role: SemanticTransformRole::Jvp,
2174                    message: "derivative simplifier saw an unmapped output".into(),
2175                }
2176            })
2177        })
2178        .collect::<Result<Vec<_>, _>>()?;
2179    prune_dead_derivative_operations(builder.finish(&outputs)?)
2180}
2181
2182fn validate_activity(
2183    role: SemanticTransformRole,
2184    field: &'static str,
2185    expected: usize,
2186    actual: usize,
2187) -> Result<(), SemanticAdTransformError> {
2188    if expected == actual {
2189        Ok(())
2190    } else {
2191        Err(SemanticAdTransformError::ActivityArity {
2192            role,
2193            field,
2194            expected,
2195            actual,
2196        })
2197    }
2198}
2199
2200fn unsupported_core(role: SemanticTransformRole, op: &CoreSemanticOp) -> SemanticAdTransformError {
2201    SemanticAdTransformError::UnsupportedCore {
2202        role,
2203        op: format!("{op:?}"),
2204    }
2205}