Skip to main content

tenferro_runtime/
trace.rs

1use std::cell::{RefCell, RefMut};
2use std::fmt;
3use std::rc::Rc;
4use std::sync::Arc;
5
6use tenferro_ops::ext_op::ExtensionOp;
7use tenferro_ops::input_key::TensorInputKey;
8use tenferro_tensor::Tensor;
9
10use crate::checkpoint::RetainedValue;
11use crate::extension_cache::ExtensionCacheStore;
12use crate::program::{
13    BindingKey, CoreSemanticOp, FrozenProgram, FrozenProgram as SnapshotProgram, ProgramBindings,
14    ProgramBuildError, ProgramFinishError, ProgramInputSpec, ProgramValue, ProgramValueMetadata,
15    SemanticProgram, SemanticProgramBuilder,
16};
17
18/// One checkpointed value: a bound input whose pre-checkpoint definition stays
19/// available to the AD snapshot.
20pub(crate) struct CheckpointAlias {
21    pub(crate) input: ProgramValue,
22    pub(crate) target: ProgramValue,
23    pub(crate) bound_shape: Vec<usize>,
24}
25
26struct TraceInner {
27    builder: SemanticProgramBuilder,
28    extension_caches: ExtensionCacheStore,
29    /// Program inputs in creation order; the index is the program input index.
30    inputs: Vec<TraceInput>,
31    aliases: Vec<CheckpointAlias>,
32}
33
34struct TraceInput {
35    value: ProgramValue,
36    key: TensorInputKey,
37}
38
39impl TraceInner {
40    fn new() -> Self {
41        Self {
42            builder: SemanticProgramBuilder::new(),
43            extension_caches: ExtensionCacheStore::new(),
44            inputs: Vec::new(),
45            aliases: Vec::new(),
46        }
47    }
48}
49
50/// Shared, cloneable handle to one mutable semantic trace.
51///
52/// Cloning the handle does not copy the trace. Values issued by one context
53/// cannot be used by another, and the handle is deliberately not `Send`: the
54/// trace is a single-threaded construction value, while eager execution keeps a
55/// frozen program instead of a live handle.
56///
57/// # Examples
58///
59/// ```
60/// use tenferro_runtime::{DType, TraceContext};
61///
62/// let trace = TraceContext::new();
63/// let x = trace.from_vec_col_major(vec![2], vec![1.0_f64, 2.0])?;
64/// let y = x.reshape([2])?;
65///
66/// // A clone is the same trace; a second context is a different one.
67/// let same = trace.clone();
68/// assert!(same.same_context(&trace));
69/// assert!(!TraceContext::new().same_context(&trace));
70/// # Ok::<(), Box<dyn std::error::Error>>(())
71/// ```
72pub struct TraceContext {
73    inner: Rc<RefCell<TraceInner>>,
74}
75
76impl Clone for TraceContext {
77    fn clone(&self) -> Self {
78        Self {
79            inner: Rc::clone(&self.inner),
80        }
81    }
82}
83
84impl fmt::Debug for TraceContext {
85    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86        // Debugging must not fail or panic while the context is borrowed, which a
87        // callback inside `with_extension_caches` or an extension's inference does.
88        let operations = self
89            .inner
90            .try_borrow()
91            .ok()
92            .map(|inner| inner.builder.operation_count());
93        formatter
94            .debug_struct("TraceContext")
95            .field("operations", &operations)
96            .finish_non_exhaustive()
97    }
98}
99
100impl TraceContext {
101    /// Construct an empty trace context.
102    ///
103    /// # Examples
104    ///
105    /// ```rust
106    /// use tenferro_runtime::TraceContext;
107    ///
108    /// let mut trace = TraceContext::new();
109    /// assert!(trace.finish(&[]).is_ok());
110    /// ```
111    pub fn new() -> Self {
112        Self {
113            inner: Rc::new(RefCell::new(TraceInner::new())),
114        }
115    }
116
117    fn borrow_mut(&self) -> Result<RefMut<'_, TraceInner>, ProgramBuildError> {
118        self.inner
119            .try_borrow_mut()
120            .map_err(|_| ProgramBuildError::ReentrantContextBorrow)
121    }
122
123    fn borrow_mut_for_finish(&self) -> Result<RefMut<'_, TraceInner>, ProgramFinishError> {
124        self.inner
125            .try_borrow_mut()
126            .map_err(|_| ProgramFinishError::ReentrantContextBorrow)
127    }
128
129    /// Add one ordered external input.
130    ///
131    /// # Errors
132    ///
133    /// Returns [`ProgramBuildError::TooManyValues`] if another value cannot be
134    /// represented, or [`ProgramBuildError::ReentrantContextBorrow`] when the
135    /// context is already borrowed by an operation in progress.
136    pub fn input(&self, spec: ProgramInputSpec) -> Result<TraceValue, ProgramBuildError> {
137        let key = crate::traced::next_input_key();
138        let mut inner = self.borrow_mut()?;
139        let value = inner.builder.input(spec)?;
140        inner.inputs.push(TraceInput { value, key });
141        Ok(TraceValue { value })
142    }
143
144    /// Add one ordered input and attach its default tensor.
145    ///
146    /// # Errors
147    ///
148    /// Returns [`ProgramBuildError::TooManyValues`] when another input cannot be
149    /// represented, [`ProgramBuildError::BindingTargetNotInput`] if the new
150    /// value is not accepted as an input, or
151    /// [`ProgramBuildError::DuplicateBinding`] if it is already bound.
152    pub fn input_with_default(
153        &self,
154        spec: ProgramInputSpec,
155        tensor: Tensor,
156    ) -> Result<TraceValue, ProgramBuildError> {
157        let input = self.input(spec)?;
158        self.bind_input(input, tensor)?;
159        Ok(input)
160    }
161
162    /// Attach a tensor default to an existing trace input.
163    ///
164    /// # Errors
165    ///
166    /// Returns [`ProgramBuildError::ForeignValue`] for another context's
167    /// value, [`ProgramBuildError::BindingTargetNotInput`] for a computed
168    /// value, or [`ProgramBuildError::DuplicateBinding`] for a repeated
169    /// binding.
170    pub fn bind_input(
171        &self,
172        input: TraceValue,
173        tensor: Tensor,
174    ) -> Result<BindingKey, ProgramBuildError> {
175        self.borrow_mut()?.builder.bind_input(input.value, tensor)
176    }
177
178    /// Add one canonical core semantic operation.
179    ///
180    /// # Errors
181    ///
182    /// Returns [`ProgramBuildError::ForeignValue`] for an input from another
183    /// context, [`ProgramBuildError::Arity`] for the wrong input count,
184    /// [`ProgramBuildError::Metadata`] /
185    /// [`ProgramBuildError::OutputMetadataCount`] when inference returns
186    /// invalid metadata, or [`ProgramBuildError::ReentrantContextBorrow`] when
187    /// the context is already borrowed.
188    pub fn add_op(
189        &self,
190        op: CoreSemanticOp,
191        inputs: &[TraceValue],
192    ) -> Result<Box<[TraceValue]>, ProgramBuildError> {
193        let inputs = program_values(inputs);
194        let outputs = self.borrow_mut()?.builder.add_op(op, &inputs)?;
195        Ok(trace_values(outputs))
196    }
197
198    /// Add one extension semantic operation.
199    ///
200    /// # Errors
201    ///
202    /// Returns [`ProgramBuildError::ForeignValue`] or
203    /// [`ProgramBuildError::Arity`] for invalid inputs,
204    /// [`ProgramBuildError::UndeclaredExtensionEffects`] /
205    /// [`ProgramBuildError::UndeclaredExtensionAliases`] for an incomplete
206    /// extension contract, [`ProgramBuildError::Metadata`] when inference
207    /// fails, or [`ProgramBuildError::ReentrantContextBorrow`] when the context
208    /// is already borrowed.
209    pub fn add_extension(
210        &self,
211        op: Arc<dyn ExtensionOp>,
212        inputs: &[TraceValue],
213    ) -> Result<Box<[TraceValue]>, ProgramBuildError> {
214        let inputs = program_values(inputs);
215        let outputs = self.borrow_mut()?.builder.add_extension(op, &inputs)?;
216        Ok(trace_values(outputs))
217    }
218
219    /// Return metadata for a context-owned value.
220    ///
221    /// # Errors
222    ///
223    /// Returns [`ProgramBuildError::ForeignValue`] for a value from another
224    /// context, or [`ProgramBuildError::ReentrantContextBorrow`] when the
225    /// context is already borrowed.
226    pub fn value_metadata(
227        &self,
228        value: TraceValue,
229    ) -> Result<ProgramValueMetadata, ProgramBuildError> {
230        self.borrow_mut()?
231            .builder
232            .value_metadata(value.value)
233            .cloned()
234    }
235
236    /// Run `f` with the generic trace-time extension cache store.
237    ///
238    /// The store is borrowed only for the call: a live context shares interior
239    /// mutability, so a `&mut` borrow cannot escape.
240    ///
241    /// # Errors
242    ///
243    /// Returns [`ProgramBuildError::ReentrantContextBorrow`] when the context is
244    /// already borrowed.
245    pub fn with_extension_caches<R>(
246        &self,
247        f: impl FnOnce(&mut ExtensionCacheStore) -> R,
248    ) -> Result<R, ProgramBuildError> {
249        let mut inner = self.borrow_mut()?;
250        Ok(f(&mut inner.extension_caches))
251    }
252
253    /// Return whether two handles refer to the same trace.
254    #[doc(hidden)]
255    pub fn same_context(&self, other: &TraceContext) -> bool {
256        std::rc::Rc::ptr_eq(&self.inner, &other.inner)
257    }
258
259    /// Add a bound input whose shape and dtype are concrete.
260    ///
261    /// AD seeds are concrete runtime values, so their extents must be exact:
262    /// leaving them symbolic would keep shape expressions unresolved at
263    /// execution staging.
264    ///
265    /// # Errors
266    ///
267    /// Returns [`ProgramBuildError::TooManyValues`] when another input cannot be
268    /// represented, or [`ProgramBuildError::ReentrantContextBorrow`] when the
269    /// context is already borrowed.
270    #[doc(hidden)]
271    pub fn bound_input_concrete(
272        &self,
273        data: Arc<RetainedValue>,
274        scalar_identity: Option<&'static str>,
275    ) -> Result<TraceValue, ProgramBuildError> {
276        let dtype = data.dtype();
277        let shape = data.shape().to_vec();
278        let metadata = ProgramValueMetadata::from_extents(
279            dtype,
280            shape.iter().map(|&size| {
281                tenferro_ops::ShapeExtent::Exact(tenferro_ops::dim_expr::DimExpr::Const(size))
282            }),
283        );
284        let mut spec = ProgramInputSpec::from_metadata(metadata);
285        if let Some(identity) = scalar_identity {
286            spec = spec.with_scalar_identity(identity);
287        }
288        self.input_bound(spec, data)
289    }
290
291    /// Return the canonical scalar identity a value's metadata declares.
292    #[doc(hidden)]
293    pub fn value_scalar_identity(&self, value: TraceValue) -> Option<&'static str> {
294        self.inner
295            .try_borrow()
296            .ok()?
297            .builder
298            .value_metadata(value.value)
299            .ok()
300            .and_then(|metadata| metadata.scalar_identity())
301    }
302
303    /// Return how many external inputs the context has added.
304    pub(crate) fn input_count(&self) -> usize {
305        self.inner
306            .try_borrow()
307            .map(|inner| inner.inputs.len())
308            .unwrap_or_default()
309    }
310
311    /// Add one input and bind retained data to it in one step.
312    pub(crate) fn input_bound(
313        &self,
314        spec: ProgramInputSpec,
315        data: Arc<RetainedValue>,
316    ) -> Result<TraceValue, ProgramBuildError> {
317        let key = crate::traced::next_input_key();
318        let mut inner = self.borrow_mut()?;
319        let value = inner.builder.input(spec)?;
320        inner.builder.bind_input_retained(value, data)?;
321        inner.inputs.push(TraceInput { value, key });
322        Ok(TraceValue { value })
323    }
324
325    /// Return the leaf key of a context input value.
326    #[doc(hidden)]
327    pub fn input_key(&self, value: TraceValue) -> Option<TensorInputKey> {
328        self.inner
329            .try_borrow()
330            .ok()?
331            .inputs
332            .iter()
333            .find(|input| input.value == value.value)
334            .map(|input| input.key.clone())
335    }
336
337    /// Return the program input index of `value` when it is an external input.
338    #[doc(hidden)]
339    pub fn input_index(&self, value: TraceValue) -> Option<usize> {
340        self.inner
341            .try_borrow()
342            .ok()?
343            .inputs
344            .iter()
345            .position(|input| input.value == value.value)
346    }
347
348    /// Return the context inputs in program input order.
349    #[doc(hidden)]
350    pub fn inputs(&self) -> Vec<(TensorInputKey, TraceValue)> {
351        self.inner
352            .try_borrow()
353            .map(|inner| {
354                inner
355                    .inputs
356                    .iter()
357                    .map(|input| (input.key.clone(), TraceValue { value: input.value }))
358                    .collect()
359            })
360            .unwrap_or_default()
361    }
362
363    /// Record a checkpoint alias and return the bound input that replaces it.
364    #[doc(hidden)]
365    pub fn checkpoint_value(
366        &self,
367        target: TraceValue,
368        data: Arc<RetainedValue>,
369    ) -> Result<TraceValue, ProgramBuildError> {
370        let key = crate::traced::next_input_key();
371        let shape = data.shape().to_vec();
372        let metadata = ProgramValueMetadata::from_extents(
373            data.dtype(),
374            shape.iter().map(|&size| {
375                tenferro_ops::ShapeExtent::Exact(tenferro_ops::dim_expr::DimExpr::Const(size))
376            }),
377        );
378        let mut inner = self.borrow_mut()?;
379        let input = inner
380            .builder
381            .input(ProgramInputSpec::from_metadata(metadata))?;
382        inner.builder.bind_input_retained(input, data)?;
383        inner.inputs.push(TraceInput { value: input, key });
384        inner.aliases.push(CheckpointAlias {
385            input,
386            target: target.value,
387            bound_shape: shape,
388        });
389        Ok(TraceValue { value: input })
390    }
391
392    /// Import a frozen program's roots into this context.
393    ///
394    /// Mapped source inputs reuse the destination values named in
395    /// [`ProgramImport::input_map`](crate::program::ProgramImport::input_map), so
396    /// a derivative program lands back on the leaves it was differentiated from.
397    ///
398    /// # Errors
399    ///
400    /// Returns the typed [`ProgramBuildError`] of the import transaction, or
401    /// [`ProgramBuildError::ReentrantContextBorrow`] when the context is already
402    /// borrowed.
403    #[doc(hidden)]
404    pub fn import_values(
405        &self,
406        request: crate::program::ProgramImport<'_>,
407    ) -> Result<Vec<TraceValue>, ProgramBuildError> {
408        let mut inner = self.borrow_mut()?;
409        let imported = inner.builder.import(request)?;
410        Ok(imported
411            .roots()
412            .iter()
413            .map(|value| TraceValue { value: *value })
414            .collect())
415    }
416
417    /// Build a traced tensor for a value of this context.
418    ///
419    /// # Errors
420    ///
421    /// Returns [`crate::Error::RuntimeStateSource`] when the value's metadata is
422    /// unavailable.
423    #[doc(hidden)]
424    pub fn derive_tensor(&self, value: TraceValue) -> crate::Result<crate::TracedTensor> {
425        crate::trace_ops::finish_derived(self, value)
426    }
427
428    /// Freeze the closure of `outputs` with checkpoint aliases substituted.
429    ///
430    /// The AD source sees the pre-checkpoint definition of every checkpointed
431    /// value, refined to the concrete bound shape, so a gradient reaches the
432    /// original leaves.
433    #[doc(hidden)]
434    pub fn ad_snapshot(&self, outputs: &[TraceValue]) -> Result<FrozenProgram, ProgramFinishError> {
435        let inner = self.borrow_mut_for_finish()?;
436        let values = program_values(outputs);
437        inner
438            .builder
439            .snapshot_pruned_with_aliases(&values, &inner.aliases)
440            .map(|(frozen, _)| frozen)
441    }
442
443    /// Freeze the forward closure with explicit declarations for named inputs.
444    ///
445    /// Inputs named here keep the declared dtype and extents instead of the
446    /// traced declaration.
447    pub(crate) fn snapshot_with_input_specs(
448        &self,
449        outputs: &[TraceValue],
450        specs: &[(TensorInputKey, ProgramInputSpec)],
451    ) -> Result<(FrozenProgram, Vec<(TensorInputKey, TraceValue)>), ProgramFinishError> {
452        let inner = self.borrow_mut_for_finish()?;
453        let values = program_values(outputs);
454        let declared: Vec<(usize, ProgramInputSpec)> = specs
455            .iter()
456            .filter_map(|(key, spec)| {
457                inner
458                    .inputs
459                    .iter()
460                    .position(|input| input.key == *key)
461                    .map(|index| (index, spec.clone()))
462            })
463            .collect();
464        let (frozen, kept) = inner
465            .builder
466            .snapshot_pruned_with_input_specs(&values, &declared)?;
467        let kept = kept
468            .into_iter()
469            .filter_map(|value| {
470                inner
471                    .inputs
472                    .iter()
473                    .find(|input| input.value == value)
474                    .map(|input| (input.key.clone(), TraceValue { value }))
475            })
476            .collect();
477        Ok((frozen, kept))
478    }
479
480    /// Freeze the forward closure and report which context inputs it kept.
481    pub(crate) fn plain_snapshot_with_inputs(
482        &self,
483        outputs: &[TraceValue],
484    ) -> Result<(FrozenProgram, Vec<(TensorInputKey, TraceValue)>), ProgramFinishError> {
485        let inner = self.borrow_mut_for_finish()?;
486        let values = program_values(outputs);
487        let (frozen, kept) = inner.builder.snapshot_pruned(&values)?;
488        let kept = kept
489            .into_iter()
490            .filter_map(|value| {
491                inner
492                    .inputs
493                    .iter()
494                    .find(|input| input.value == value)
495                    .map(|input| (input.key.clone(), TraceValue { value }))
496            })
497            .collect();
498        Ok((frozen, kept))
499    }
500
501    /// Freeze the AD source and report which context inputs it kept.
502    #[doc(hidden)]
503    pub fn ad_snapshot_with_inputs(
504        &self,
505        outputs: &[TraceValue],
506    ) -> Result<(FrozenProgram, Vec<(TensorInputKey, TraceValue)>), ProgramFinishError> {
507        let inner = self.borrow_mut_for_finish()?;
508        let values = program_values(outputs);
509        let (frozen, kept) = inner
510            .builder
511            .snapshot_pruned_with_aliases(&values, &inner.aliases)?;
512        let kept = kept
513            .into_iter()
514            .filter_map(|value| {
515                inner
516                    .inputs
517                    .iter()
518                    .find(|input| input.value == value)
519                    .map(|input| (input.key.clone(), TraceValue { value }))
520            })
521            .collect();
522        Ok((frozen, kept))
523    }
524
525    /// Freeze the ordered closure of `outputs` without consuming the context.
526    ///
527    /// Later appends do not change the returned graph, and the context stays
528    /// usable for further tracing.
529    ///
530    /// # Errors
531    ///
532    /// Returns [`ProgramFinishError::ForeignOutput`] if an output belongs to
533    /// another context, [`ProgramFinishError::ReentrantContextBorrow`] when the
534    /// context is already borrowed, or a typed structural/binding finalization
535    /// failure.
536    pub fn finish(&self, outputs: &[TraceValue]) -> Result<TracedGraph, ProgramFinishError> {
537        let frozen = self.frozen_snapshot(outputs)?;
538        Ok(TracedGraph { frozen })
539    }
540
541    /// Freeze the ordered closure of `outputs` as a copy of the program.
542    #[doc(hidden)]
543    pub fn frozen_snapshot(
544        &self,
545        outputs: &[TraceValue],
546    ) -> Result<SnapshotProgram, ProgramFinishError> {
547        let inner = self.borrow_mut_for_finish()?;
548        let values = program_values(outputs);
549        inner
550            .builder
551            .snapshot_pruned(&values)
552            .map(|(frozen, _)| frozen)
553    }
554}
555
556impl Default for TraceContext {
557    fn default() -> Self {
558        Self::new()
559    }
560}
561
562/// Opaque value handle owned by one [`TraceContext`].
563#[derive(Clone, Copy)]
564pub struct TraceValue {
565    value: ProgramValue,
566}
567
568impl TraceValue {
569    /// Return the program value behind this handle.
570    #[doc(hidden)]
571    pub fn program_value(self) -> ProgramValue {
572        self.value
573    }
574}
575
576impl fmt::Debug for TraceValue {
577    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
578        formatter.write_str("TraceValue(<opaque>)")
579    }
580}
581
582/// Immutable backend-neutral output of one completed trace.
583#[derive(Clone)]
584pub struct TracedGraph {
585    frozen: FrozenProgram,
586}
587
588impl TracedGraph {
589    /// Borrow the immutable semantic program.
590    pub fn program(&self) -> &SemanticProgram {
591        &self.frozen.program
592    }
593
594    /// Borrow process-local tensor defaults and large constants.
595    pub fn bindings(&self) -> &ProgramBindings {
596        &self.frozen.bindings
597    }
598
599    pub(crate) fn frozen(&self) -> &FrozenProgram {
600        &self.frozen
601    }
602}
603
604impl fmt::Debug for TracedGraph {
605    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
606        formatter
607            .debug_struct("TracedGraph")
608            .field("inputs", &self.program().inputs().len())
609            .field("outputs", &self.program().outputs().len())
610            .field("bindings", &self.bindings().len())
611            .field(
612                "semantic_fingerprint",
613                &self.program().semantic_fingerprint(),
614            )
615            .finish()
616    }
617}
618
619fn program_values(values: &[TraceValue]) -> Vec<ProgramValue> {
620    values.iter().map(|value| value.value).collect()
621}
622
623fn trace_values(values: Box<[ProgramValue]>) -> Box<[TraceValue]> {
624    values
625        .into_vec()
626        .into_iter()
627        .map(|value| TraceValue { value })
628        .collect()
629}
630
631#[cfg(test)]
632mod tests {
633    use tenferro_ops::dim_expr::DimExpr;
634    use tenferro_ops::ShapeExtent;
635    use tenferro_tensor::DType;
636
637    use super::*;
638
639    #[test]
640    fn unknown_input_extents_keep_ordered_semantic_inputs() {
641        let spec = || {
642            ProgramInputSpec::from_metadata(ProgramValueMetadata::from_extents(
643                DType::F64,
644                [ShapeExtent::Unknown],
645            ))
646        };
647        use crate::TraceContext;
648        let context = TraceContext::new();
649        let first = context.input(spec()).unwrap();
650        let second = context.input(spec()).unwrap();
651        let graph = context.finish(&[first, second]).unwrap();
652
653        assert_eq!(graph.program().inputs().len(), 2);
654        for input in graph.program().inputs() {
655            assert_eq!(
656                graph.program().value_metadata(*input).unwrap().shape(),
657                [ShapeExtent::Unknown]
658            );
659        }
660    }
661
662    #[test]
663    fn finish_snapshots_without_consuming_the_context() {
664        let mut builder = SemanticProgramBuilder::new();
665        let input = builder
666            .input(ProgramInputSpec::new(DType::F64, [DimExpr::Const(2)]))
667            .unwrap();
668        let output = builder.add_op(CoreSemanticOp::Neg, &[input]).unwrap()[0];
669        let frozen = builder.finish(&[output]).unwrap();
670
671        let context = TraceContext::new();
672        let value = context
673            .input(ProgramInputSpec::new(DType::F64, [DimExpr::Const(2)]))
674            .unwrap();
675        let negated = context.add_op(CoreSemanticOp::Neg, &[value]).unwrap()[0];
676        let first = context.finish(&[negated]).unwrap();
677        // The context keeps growing after the snapshot; the snapshot does not.
678        let doubled = context.add_op(CoreSemanticOp::Neg, &[negated]).unwrap()[0];
679        let second = context.finish(&[doubled]).unwrap();
680
681        assert_eq!(first.program().operations().count(), 1);
682        assert_eq!(second.program().operations().count(), 2);
683        assert_eq!(
684            first
685                .program()
686                .value_metadata(first.program().outputs()[0])
687                .unwrap(),
688            frozen
689                .program
690                .value_metadata(frozen.program.outputs()[0])
691                .unwrap()
692        );
693    }
694
695    #[test]
696    fn foreign_values_are_rejected_by_metadata_and_ops() {
697        let first = TraceContext::new();
698        let second = TraceContext::new();
699        let value = first
700            .input(ProgramInputSpec::new(DType::F64, [DimExpr::Const(1)]))
701            .unwrap();
702        let foreign = second
703            .input(ProgramInputSpec::new(DType::F64, [DimExpr::Const(1)]))
704            .unwrap();
705
706        assert!(first.value_metadata(value).is_ok());
707        assert!(matches!(
708            first.value_metadata(foreign),
709            Err(ProgramBuildError::ForeignValue)
710        ));
711        assert!(matches!(
712            first.add_op(CoreSemanticOp::Neg, &[foreign]),
713            Err(ProgramBuildError::ForeignValue)
714        ));
715    }
716
717    #[test]
718    fn formatting_a_context_inside_the_cache_callback_does_not_panic() {
719        let context = TraceContext::new();
720        let rendered = context
721            .with_extension_caches(|_| format!("{context:?}"))
722            .unwrap();
723        assert!(rendered.contains("TraceContext"));
724    }
725
726    #[test]
727    fn extension_caches_are_reached_through_a_scoped_borrow() {
728        let context = TraceContext::new();
729        let entries = context
730            .with_extension_caches(|caches| caches.len())
731            .unwrap();
732        assert_eq!(entries, 0);
733    }
734}