1use std::collections::HashMap;
8use std::fmt::Debug;
9use std::sync::Arc;
10
11use tenferro_ops::ext_op::ExtensionOp;
12use tenferro_runtime::program::{
13 ProgramBuildError, ProgramValue, SemanticOpRef, SemanticOperationView, SemanticProgramBuilder,
14 SemanticProvenanceView,
15};
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum AdValue {
20 Absent,
22 Value(ProgramValue),
24}
25
26impl AdValue {
27 #[must_use]
29 pub const fn value(self) -> Option<ProgramValue> {
30 match self {
31 Self::Absent => None,
32 Self::Value(value) => Some(value),
33 }
34 }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
39pub enum SemanticAdRuleRole {
40 Linearize,
42 LinearTranspose,
44 PrimalVjp,
46}
47
48#[derive(Debug, thiserror::Error)]
50pub enum SemanticExtensionRegistryError {
51 #[error("semantic extension AD {role:?} rule for family {family_id:?} is already registered")]
53 DuplicateRule {
54 family_id: &'static str,
56 role: SemanticAdRuleRole,
58 },
59 #[error("semantic extension AD family {family_id:?} is not namespaced and versioned")]
61 MalformedFamilyId {
62 family_id: &'static str,
64 },
65}
66
67#[derive(Debug, thiserror::Error)]
69pub enum SemanticAdError {
70 #[error("semantic AD extension dispatch received a core operation")]
72 CoreOperation,
73 #[error("semantic extension family {family_id:?} has observable effects")]
75 EffectfulExtension {
76 family_id: &'static str,
78 },
79 #[error("semantic extension family {family_id:?} has no {role:?} AD rule")]
81 MissingRule {
82 family_id: &'static str,
84 role: SemanticAdRuleRole,
86 },
87 #[error("semantic AD field {field} expects {expected} values, got {actual}")]
89 Arity {
90 field: &'static str,
92 expected: usize,
94 actual: usize,
96 },
97 #[error("semantic AD field {field}[{index}] does not belong to the destination builder")]
99 ForeignValue {
100 field: &'static str,
102 index: usize,
104 },
105 #[error("semantic extension family {family_id:?} does not support {role:?}: {message}")]
107 Unsupported {
108 family_id: &'static str,
110 role: SemanticAdRuleRole,
112 message: String,
114 },
115 #[error("semantic extension family {family_id:?} {role:?} rule failed: {source}")]
117 Rule {
118 family_id: &'static str,
120 role: SemanticAdRuleRole,
122 #[source]
124 source: Box<dyn std::error::Error + Send + Sync + 'static>,
125 },
126 #[error("semantic extension family {family_id:?} {role:?} invariant failed: {message}")]
128 Invariant {
129 family_id: &'static str,
131 role: SemanticAdRuleRole,
133 message: String,
135 },
136 #[error("semantic extension AD program construction failed: {0}")]
138 Build(#[from] ProgramBuildError),
139}
140
141#[derive(Clone, Copy)]
143pub struct SemanticLinearizeRequest<'a> {
144 op: &'a dyn ExtensionOp,
145 primal_inputs: &'a [ProgramValue],
146 primal_outputs: &'a [ProgramValue],
147 tangent_inputs: &'a [AdValue],
148 active_outputs: &'a [bool],
149 provenance: SemanticProvenanceView<'a>,
150}
151
152impl<'a> SemanticLinearizeRequest<'a> {
153 pub const fn op(self) -> &'a dyn ExtensionOp {
155 self.op
156 }
157
158 pub const fn primal_inputs(self) -> &'a [ProgramValue] {
160 self.primal_inputs
161 }
162
163 pub const fn primal_outputs(self) -> &'a [ProgramValue] {
165 self.primal_outputs
166 }
167
168 pub const fn tangent_inputs(self) -> &'a [AdValue] {
170 self.tangent_inputs
171 }
172
173 pub const fn active_outputs(self) -> &'a [bool] {
175 self.active_outputs
176 }
177
178 pub const fn provenance(self) -> SemanticProvenanceView<'a> {
180 self.provenance
181 }
182}
183
184#[derive(Clone, Debug, PartialEq, Eq)]
186pub struct SemanticLinearizeResult {
187 tangent_outputs: Box<[AdValue]>,
188 residuals: Box<[ProgramValue]>,
189}
190
191impl SemanticLinearizeResult {
192 #[must_use]
194 pub fn new(
195 tangent_outputs: impl IntoIterator<Item = AdValue>,
196 residuals: impl IntoIterator<Item = ProgramValue>,
197 ) -> Self {
198 Self {
199 tangent_outputs: tangent_outputs.into_iter().collect(),
200 residuals: residuals.into_iter().collect(),
201 }
202 }
203
204 pub fn tangent_outputs(&self) -> &[AdValue] {
206 &self.tangent_outputs
207 }
208
209 pub fn residuals(&self) -> &[ProgramValue] {
211 &self.residuals
212 }
213}
214
215#[derive(Clone, Copy)]
217pub struct SemanticLinearTransposeRequest<'a> {
218 op: &'a dyn ExtensionOp,
219 primal_inputs: &'a [ProgramValue],
220 primal_outputs: &'a [ProgramValue],
221 cotangent_outputs: &'a [AdValue],
222 active_inputs: &'a [bool],
223 residuals: &'a [ProgramValue],
224 provenance: SemanticProvenanceView<'a>,
225}
226
227impl<'a> SemanticLinearTransposeRequest<'a> {
228 pub const fn op(self) -> &'a dyn ExtensionOp {
230 self.op
231 }
232
233 pub const fn primal_inputs(self) -> &'a [ProgramValue] {
235 self.primal_inputs
236 }
237
238 pub const fn primal_outputs(self) -> &'a [ProgramValue] {
240 self.primal_outputs
241 }
242
243 pub const fn cotangent_outputs(self) -> &'a [AdValue] {
245 self.cotangent_outputs
246 }
247
248 pub const fn active_inputs(self) -> &'a [bool] {
250 self.active_inputs
251 }
252
253 pub const fn residuals(self) -> &'a [ProgramValue] {
255 self.residuals
256 }
257
258 pub const fn provenance(self) -> SemanticProvenanceView<'a> {
260 self.provenance
261 }
262}
263
264#[derive(Clone, Copy)]
266pub struct SemanticPrimalVjpRequest<'a> {
267 op: &'a dyn ExtensionOp,
268 primal_inputs: &'a [ProgramValue],
269 primal_outputs: &'a [ProgramValue],
270 cotangent_outputs: &'a [AdValue],
271 active_inputs: &'a [bool],
272 provenance: SemanticProvenanceView<'a>,
273}
274
275impl<'a> SemanticPrimalVjpRequest<'a> {
276 pub const fn op(self) -> &'a dyn ExtensionOp {
278 self.op
279 }
280
281 pub const fn primal_inputs(self) -> &'a [ProgramValue] {
283 self.primal_inputs
284 }
285
286 pub const fn primal_outputs(self) -> &'a [ProgramValue] {
288 self.primal_outputs
289 }
290
291 pub const fn cotangent_outputs(self) -> &'a [AdValue] {
293 self.cotangent_outputs
294 }
295
296 pub const fn active_inputs(self) -> &'a [bool] {
298 self.active_inputs
299 }
300
301 pub const fn provenance(self) -> SemanticProvenanceView<'a> {
303 self.provenance
304 }
305}
306
307pub trait SemanticLinearizeRule: Debug + Send + Sync + 'static {
309 fn family_id(&self) -> &'static str;
311
312 fn linearize(
320 &self,
321 request: SemanticLinearizeRequest<'_>,
322 builder: &mut SemanticProgramBuilder,
323 ) -> Result<SemanticLinearizeResult, SemanticAdError>;
324}
325
326pub trait SemanticLinearTransposeRule: Debug + Send + Sync + 'static {
328 fn family_id(&self) -> &'static str;
330
331 fn linear_transpose(
339 &self,
340 request: SemanticLinearTransposeRequest<'_>,
341 builder: &mut SemanticProgramBuilder,
342 ) -> Result<Box<[AdValue]>, SemanticAdError>;
343}
344
345pub trait SemanticPrimalVjpRule: Debug + Send + Sync + 'static {
347 fn family_id(&self) -> &'static str;
349
350 fn primal_vjp(
358 &self,
359 request: SemanticPrimalVjpRequest<'_>,
360 builder: &mut SemanticProgramBuilder,
361 ) -> Result<Box<[AdValue]>, SemanticAdError>;
362}
363
364type LinearizeMap = HashMap<&'static str, Arc<dyn SemanticLinearizeRule>>;
365type LinearTransposeMap = HashMap<&'static str, Arc<dyn SemanticLinearTransposeRule>>;
366type PrimalVjpMap = HashMap<&'static str, Arc<dyn SemanticPrimalVjpRule>>;
367
368#[derive(Clone, Default)]
370pub struct SemanticExtensionRuleSet {
371 linearize: Arc<LinearizeMap>,
372 linear_transpose: Arc<LinearTransposeMap>,
373 primal_vjp: Arc<PrimalVjpMap>,
374}
375
376impl Debug for SemanticExtensionRuleSet {
377 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378 let mut linearize: Vec<_> = self.linearize.keys().copied().collect();
379 let mut linear_transpose: Vec<_> = self.linear_transpose.keys().copied().collect();
380 let mut primal_vjp: Vec<_> = self.primal_vjp.keys().copied().collect();
381 linearize.sort_unstable();
382 linear_transpose.sort_unstable();
383 primal_vjp.sort_unstable();
384 formatter
385 .debug_struct("SemanticExtensionRuleSet")
386 .field("linearize", &linearize)
387 .field("linear_transpose", &linear_transpose)
388 .field("primal_vjp", &primal_vjp)
389 .finish()
390 }
391}
392
393impl SemanticExtensionRuleSet {
394 #[must_use]
396 pub fn new() -> Self {
397 Self::default()
398 }
399
400 pub fn register_linearize(
408 &mut self,
409 rule: Arc<dyn SemanticLinearizeRule>,
410 ) -> Result<(), SemanticExtensionRegistryError> {
411 validate_insert(
412 &self.linearize,
413 rule.family_id(),
414 SemanticAdRuleRole::Linearize,
415 )?;
416 Arc::make_mut(&mut self.linearize).insert(rule.family_id(), rule);
417 Ok(())
418 }
419
420 pub fn register_linear_transpose(
428 &mut self,
429 rule: Arc<dyn SemanticLinearTransposeRule>,
430 ) -> Result<(), SemanticExtensionRegistryError> {
431 validate_insert(
432 &self.linear_transpose,
433 rule.family_id(),
434 SemanticAdRuleRole::LinearTranspose,
435 )?;
436 Arc::make_mut(&mut self.linear_transpose).insert(rule.family_id(), rule);
437 Ok(())
438 }
439
440 pub fn register_primal_vjp(
448 &mut self,
449 rule: Arc<dyn SemanticPrimalVjpRule>,
450 ) -> Result<(), SemanticExtensionRegistryError> {
451 validate_insert(
452 &self.primal_vjp,
453 rule.family_id(),
454 SemanticAdRuleRole::PrimalVjp,
455 )?;
456 Arc::make_mut(&mut self.primal_vjp).insert(rule.family_id(), rule);
457 Ok(())
458 }
459
460 pub fn with_linearize(
468 mut self,
469 rule: Arc<dyn SemanticLinearizeRule>,
470 ) -> Result<Self, SemanticExtensionRegistryError> {
471 self.register_linearize(rule)?;
472 Ok(self)
473 }
474
475 pub fn with_linear_transpose(
483 mut self,
484 rule: Arc<dyn SemanticLinearTransposeRule>,
485 ) -> Result<Self, SemanticExtensionRegistryError> {
486 self.register_linear_transpose(rule)?;
487 Ok(self)
488 }
489
490 pub fn with_primal_vjp(
498 mut self,
499 rule: Arc<dyn SemanticPrimalVjpRule>,
500 ) -> Result<Self, SemanticExtensionRegistryError> {
501 self.register_primal_vjp(rule)?;
502 Ok(self)
503 }
504
505 pub fn merge(&mut self, other: Self) -> Result<(), SemanticExtensionRegistryError> {
513 let mut candidate = self.clone();
514 for rule in other.linearize.values() {
515 candidate.register_linearize(Arc::clone(rule))?;
516 }
517 for rule in other.linear_transpose.values() {
518 candidate.register_linear_transpose(Arc::clone(rule))?;
519 }
520 for rule in other.primal_vjp.values() {
521 candidate.register_primal_vjp(Arc::clone(rule))?;
522 }
523 *self = candidate;
524 Ok(())
525 }
526
527 #[must_use]
529 pub fn lookup_linearize(&self, family_id: &str) -> Option<Arc<dyn SemanticLinearizeRule>> {
530 self.linearize.get(family_id).cloned()
531 }
532
533 #[must_use]
535 pub fn lookup_linear_transpose(
536 &self,
537 family_id: &str,
538 ) -> Option<Arc<dyn SemanticLinearTransposeRule>> {
539 self.linear_transpose.get(family_id).cloned()
540 }
541
542 #[must_use]
544 pub fn lookup_primal_vjp(&self, family_id: &str) -> Option<Arc<dyn SemanticPrimalVjpRule>> {
545 self.primal_vjp.get(family_id).cloned()
546 }
547
548 #[allow(clippy::too_many_arguments)]
556 pub fn linearize_operation(
557 &self,
558 operation: SemanticOperationView<'_>,
559 primal_inputs: &[ProgramValue],
560 primal_outputs: &[ProgramValue],
561 tangent_inputs: &[AdValue],
562 active_outputs: &[bool],
563 builder: &mut SemanticProgramBuilder,
564 ) -> Result<SemanticLinearizeResult, SemanticAdError> {
565 let op = extension_for_dispatch(operation)?;
566 validate_operation_inputs(operation, primal_inputs, primal_outputs, builder)?;
567 validate_len("tangent_inputs", op.input_count(), tangent_inputs.len())?;
568 validate_len("active_outputs", op.output_count(), active_outputs.len())?;
569 validate_ad_values("tangent_inputs", tangent_inputs, builder)?;
570 let rule = self
571 .lookup_linearize(op.family_id())
572 .ok_or(SemanticAdError::MissingRule {
573 family_id: op.family_id(),
574 role: SemanticAdRuleRole::Linearize,
575 })?;
576 let result = rule.linearize(
577 SemanticLinearizeRequest {
578 op,
579 primal_inputs,
580 primal_outputs,
581 tangent_inputs,
582 active_outputs,
583 provenance: operation.provenance(),
584 },
585 builder,
586 )?;
587 validate_len(
588 "tangent_outputs",
589 op.output_count(),
590 result.tangent_outputs.len(),
591 )?;
592 validate_ad_values("tangent_outputs", &result.tangent_outputs, builder)?;
593 validate_values("residuals", &result.residuals, builder)?;
594 Ok(result)
595 }
596
597 #[allow(clippy::too_many_arguments)]
606 pub fn linear_transpose_operation(
607 &self,
608 operation: SemanticOperationView<'_>,
609 primal_inputs: &[ProgramValue],
610 primal_outputs: &[ProgramValue],
611 cotangent_outputs: &[AdValue],
612 active_inputs: &[bool],
613 residuals: &[ProgramValue],
614 builder: &mut SemanticProgramBuilder,
615 ) -> Result<Box<[AdValue]>, SemanticAdError> {
616 let op = extension_for_dispatch(operation)?;
617 validate_operation_inputs(operation, primal_inputs, primal_outputs, builder)?;
618 validate_len(
619 "cotangent_outputs",
620 op.output_count(),
621 cotangent_outputs.len(),
622 )?;
623 validate_len("active_inputs", op.input_count(), active_inputs.len())?;
624 validate_ad_values("cotangent_outputs", cotangent_outputs, builder)?;
625 validate_values("residuals", residuals, builder)?;
626 let rule =
627 self.lookup_linear_transpose(op.family_id())
628 .ok_or(SemanticAdError::MissingRule {
629 family_id: op.family_id(),
630 role: SemanticAdRuleRole::LinearTranspose,
631 })?;
632 let result = rule.linear_transpose(
633 SemanticLinearTransposeRequest {
634 op,
635 primal_inputs,
636 primal_outputs,
637 cotangent_outputs,
638 active_inputs,
639 residuals,
640 provenance: operation.provenance(),
641 },
642 builder,
643 )?;
644 validate_len("cotangent_inputs", op.input_count(), result.len())?;
645 validate_ad_values("cotangent_inputs", &result, builder)?;
646 Ok(result)
647 }
648
649 #[allow(clippy::too_many_arguments)]
658 pub fn primal_vjp_operation(
659 &self,
660 operation: SemanticOperationView<'_>,
661 primal_inputs: &[ProgramValue],
662 primal_outputs: &[ProgramValue],
663 cotangent_outputs: &[AdValue],
664 active_inputs: &[bool],
665 builder: &mut SemanticProgramBuilder,
666 ) -> Result<Box<[AdValue]>, SemanticAdError> {
667 let op = extension_for_dispatch(operation)?;
668 validate_operation_inputs(operation, primal_inputs, primal_outputs, builder)?;
669 validate_len(
670 "cotangent_outputs",
671 op.output_count(),
672 cotangent_outputs.len(),
673 )?;
674 validate_len("active_inputs", op.input_count(), active_inputs.len())?;
675 validate_ad_values("cotangent_outputs", cotangent_outputs, builder)?;
676 let rule = self
677 .lookup_primal_vjp(op.family_id())
678 .ok_or(SemanticAdError::MissingRule {
679 family_id: op.family_id(),
680 role: SemanticAdRuleRole::PrimalVjp,
681 })?;
682 let result = rule.primal_vjp(
683 SemanticPrimalVjpRequest {
684 op,
685 primal_inputs,
686 primal_outputs,
687 cotangent_outputs,
688 active_inputs,
689 provenance: operation.provenance(),
690 },
691 builder,
692 )?;
693 validate_len("cotangent_inputs", op.input_count(), result.len())?;
694 validate_ad_values("cotangent_inputs", &result, builder)?;
695 Ok(result)
696 }
697}
698
699fn extension_for_dispatch(
700 operation: SemanticOperationView<'_>,
701) -> Result<&dyn ExtensionOp, SemanticAdError> {
702 let SemanticOpRef::Extension(op) = operation.op() else {
703 return Err(SemanticAdError::CoreOperation);
704 };
705 if !operation.effects().is_empty() {
706 return Err(SemanticAdError::EffectfulExtension {
707 family_id: op.family_id(),
708 });
709 }
710 Ok(op)
711}
712
713fn validate_operation_inputs(
714 operation: SemanticOperationView<'_>,
715 primal_inputs: &[ProgramValue],
716 primal_outputs: &[ProgramValue],
717 builder: &SemanticProgramBuilder,
718) -> Result<(), SemanticAdError> {
719 validate_len(
720 "primal_inputs",
721 operation.inputs().len(),
722 primal_inputs.len(),
723 )?;
724 validate_len(
725 "primal_outputs",
726 operation.outputs().len(),
727 primal_outputs.len(),
728 )?;
729 validate_values("primal_inputs", primal_inputs, builder)?;
730 validate_values("primal_outputs", primal_outputs, builder)
731}
732
733fn validate_values(
734 field: &'static str,
735 values: &[ProgramValue],
736 builder: &SemanticProgramBuilder,
737) -> Result<(), SemanticAdError> {
738 for (index, value) in values.iter().copied().enumerate() {
739 if builder.validate_value(value).is_err() {
740 return Err(SemanticAdError::ForeignValue { field, index });
741 }
742 }
743 Ok(())
744}
745
746fn validate_ad_values(
747 field: &'static str,
748 values: &[AdValue],
749 builder: &SemanticProgramBuilder,
750) -> Result<(), SemanticAdError> {
751 for (index, value) in values.iter().copied().enumerate() {
752 if let AdValue::Value(value) = value {
753 if builder.validate_value(value).is_err() {
754 return Err(SemanticAdError::ForeignValue { field, index });
755 }
756 }
757 }
758 Ok(())
759}
760
761fn validate_len(
762 field: &'static str,
763 expected: usize,
764 actual: usize,
765) -> Result<(), SemanticAdError> {
766 if expected != actual {
767 return Err(SemanticAdError::Arity {
768 field,
769 expected,
770 actual,
771 });
772 }
773 Ok(())
774}
775
776fn validate_insert<T>(
777 map: &HashMap<&'static str, T>,
778 family_id: &'static str,
779 role: SemanticAdRuleRole,
780) -> Result<(), SemanticExtensionRegistryError> {
781 if !is_valid_family_id(family_id) {
782 return Err(SemanticExtensionRegistryError::MalformedFamilyId { family_id });
783 }
784 if map.contains_key(family_id) {
785 return Err(SemanticExtensionRegistryError::DuplicateRule { family_id, role });
786 }
787 Ok(())
788}
789
790fn is_valid_family_id(family_id: &str) -> bool {
791 let Some((prefix, version)) = family_id.rsplit_once('.') else {
792 return false;
793 };
794 let Some(version) = version.strip_prefix('v') else {
795 return false;
796 };
797 let Some((crate_name, op_name)) = prefix.split_once('.') else {
798 return false;
799 };
800 !crate_name.is_empty()
801 && !op_name.is_empty()
802 && !version.is_empty()
803 && version.bytes().all(|byte| byte.is_ascii_digit())
804 && crate_name.is_ascii()
805 && op_name.is_ascii()
806 && !crate_name.chars().any(char::is_whitespace)
807 && !op_name.chars().any(char::is_whitespace)
808}