Skip to main content

tenferro_runtime/
extension.rs

1//! Public surface for out-of-tree extension primitives.
2//!
3//! This module exposes the Stage 6 `ExtensionOp` mechanism through the
4//! runtime crate. External crates implement
5//! [`tenferro_ops::ext_op::ExtensionOp`] and build traced graphs containing
6//! the extension via [`apply`].
7//!
8//! See `docs/spec/extension-op.md` for the normative contract.
9//!
10//! # Examples
11//!
12//! ```rust
13//! use tenferro_runtime::extension::{apply, ExtensionOp};
14//!
15//! // Construct an `Arc<dyn ExtensionOp>` and call `apply(op, &[input])`
16//! // to lower it into a `TracedTensor`.
17//! ```
18
19use std::sync::Arc;
20
21use computegraph::graph::GraphBuilder;
22use computegraph::types::{OperationRole, ValueRef};
23use computegraph::GraphOperation;
24use tenferro_ops::dim_expr::DimExpr;
25use tenferro_ops::std_tensor_op::StdTensorOp;
26use tenferro_ops::SymDim;
27
28use crate::checkpoint::CheckpointNode;
29use crate::error::{Error, ErrorPhase, Result};
30use crate::metadata::{
31    register_scoped_graph_analysis, registered_meta, MetadataScopeChain, RegisteredGraphAnalysis,
32};
33use crate::shape_constraint::{ConstraintScopeChain, ScopedShapeConstraint, ShapeConstraintScope};
34use crate::shape_infer::{infer_extension_output_meta_with_constraints, InferredExtensionMeta};
35use crate::traced::{merge_traced_inputs_map, next_traced_id, TracedTensor};
36
37type ExpandedOutputMetas = Vec<(tenferro_tensor::DType, Vec<SymDim>)>;
38
39pub use crate::compiler::CompilerOptions;
40#[doc(hidden)]
41pub use crate::shape_infer::{
42    infer_output_dtype, infer_output_extents, infer_output_shapes, promote_dtype,
43    promote_dtype_div_like, promote_dtype_for_binary_op, promote_dtypes,
44};
45pub use tenferro_ops::ext_op::ExtensionOp;
46pub use tenferro_ops::ExtensionFamilyId;
47
48pub use crate::extension_cache::{
49    ExtensionCacheKey, ExtensionCacheLimits, ExtensionCacheSelector, ExtensionCacheStore,
50};
51pub use crate::extension_execution_context::ExtensionExecutionContext;
52
53/// Apply an extension op in the traced graph.
54///
55/// The `op` value is cloned into a `StdTensorOp::Extension(Arc<dyn ExtensionOp>)`
56/// carrier. The returned vector contains one [`TracedTensor`] per declared
57/// output slot of the extension. Output shapes are inferred via
58/// [`ExtensionOp::infer_output_meta`] using the input shape hints.
59///
60/// `inputs.len()` must equal `op.input_count()`, and each input's
61/// `shape_hint` must be present (i.e. the extension must be used on
62/// tensors whose rank is known at graph-build time). For symbolic-shape
63/// composition, pass concrete tensors to [`crate::Runtime::run_compiled`] at
64/// evaluation time.
65///
66/// # Examples
67///
68/// ```rust
69/// # use std::any::Any;
70/// use std::sync::Arc;
71/// use tenferro_runtime::extension::{apply, ExtensionOp};
72/// use tenferro_runtime::{DType, SymDim, TracedTensor};
73/// use tenferro_ops::ExtensionShapeContext;
74///
75/// # #[derive(Clone, Debug)]
76/// # struct IdentityExt;
77/// # impl ExtensionOp for IdentityExt {
78/// #     fn family_id(&self) -> &'static str { "example.identity.v1" }
79/// #     fn payload_hash(&self, _hasher: &mut dyn std::hash::Hasher) {}
80/// #     fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
81/// #         other.as_any().downcast_ref::<IdentityExt>().is_some()
82/// #     }
83/// #     fn clone_arc(&self) -> Arc<dyn ExtensionOp> { Arc::new(self.clone()) }
84/// #     fn as_any(&self) -> &dyn Any { self }
85/// #     fn input_count(&self) -> usize { 1 }
86/// #     fn output_count(&self) -> usize { 1 }
87/// #     fn infer_output_meta(
88/// #         &self,
89/// #         ctx: &mut ExtensionShapeContext<'_>,
90/// #     ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
91/// #         Ok(vec![(ctx.input_dtype(0)?, ctx.input_shape(0)?.to_vec())])
92/// #     }
93/// # }
94/// let op: Arc<dyn ExtensionOp> = Arc::new(IdentityExt);
95/// let a = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
96/// let outputs = apply(op, &[&a])?;
97/// assert_eq!(outputs.len(), 1);
98/// # Ok::<(), tenferro_runtime::Error>(())
99/// ```
100///
101/// # Errors
102///
103/// Returns [`Error::Validation`] with `ValidationError::InvalidArgument` when
104/// the extension receives the wrong number of traced inputs or produces an
105/// unknown output shape. Canonical metadata inference failures, including a
106/// returned metadata count that differs from [`ExtensionOp::output_count`],
107/// are returned as [`Error::TensorRuntime`] containing the typed tensor
108/// validation source, while poisoned metadata state is retained as
109/// [`Error::RuntimeStateSource`].
110pub fn apply(op: Arc<dyn ExtensionOp>, inputs: &[&TracedTensor]) -> Result<Vec<TracedTensor>> {
111    if inputs.len() != op.input_count() {
112        return Err(Error::invalid_argument(
113            "extension::apply",
114            ErrorPhase::GraphBuild,
115            "inputs",
116            format!(
117                "op family {:?} expects {} inputs, got {}",
118                op.family_id(),
119                op.input_count(),
120                inputs.len()
121            ),
122        ));
123    }
124
125    // Build the carrier graph first; the graph-analysis pass below resolves
126    // registered input metadata and invokes canonical extension inference once.
127    let mut builder = GraphBuilder::<StdTensorOp>::new();
128    for input in inputs {
129        builder.add_parent(input.graph.clone());
130    }
131    let op_inputs: Vec<ValueRef<StdTensorOp>> = inputs
132        .iter()
133        .map(|t| ValueRef::External(t.graph.values()[t.val].key.clone()))
134        .collect();
135    let carrier = StdTensorOp::Extension(op.clone());
136    let outputs = builder.add_operation(carrier, op_inputs, OperationRole::Primary);
137    builder.set_outputs(outputs.clone());
138    let graph = Arc::new(builder.build());
139    let analysis = register_scoped_graph_analysis(graph.as_ref(), std::iter::empty())?;
140    let output_metas = outputs
141        .iter()
142        .map(|&output| {
143            let meta = registered_meta(&graph.values()[output].key)?;
144            let shape = meta.bound_shape().ok_or_else(|| {
145                Error::invalid_argument(
146                    "extension::apply",
147                    ErrorPhase::Compile,
148                    "output_metadata",
149                    format!(
150                        "extension family {:?} produced unknown output shape metadata",
151                        op.family_id()
152                    ),
153                )
154            })?;
155            Ok((meta.dtype, shape))
156        })
157        .collect::<Result<Vec<_>>>()?;
158    traced_outputs_from_analysis(inputs, graph, &outputs, output_metas, analysis)
159}
160
161/// Apply a core standard op in the traced graph.
162///
163/// This is an internal crate-boundary helper used by eager AD recording to keep
164/// a semantic traced graph beside the existing eager trace. Extension ops
165/// must use [`apply`] instead.
166///
167/// # Examples
168///
169/// ```rust
170/// use tenferro_ops::std_tensor_op::StdTensorOp;
171/// use tenferro_runtime::{extension, TracedTensor};
172///
173/// let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
174/// let outputs = extension::apply_standard_op(StdTensorOp::Neg, &[&x])?;
175/// assert_eq!(outputs.len(), 1);
176/// # Ok::<(), tenferro_runtime::Error>(())
177/// ```
178///
179/// # Errors
180///
181/// Returns [`Error::Validation`] with `InvalidArgument` when the op is an
182/// extension op, receives the wrong number of traced inputs, or produces
183/// metadata without a known output bound. Metadata-analysis and registry
184/// failures are returned as typed runtime errors with their source preserved.
185#[doc(hidden)]
186pub fn apply_standard_op(op: StdTensorOp, inputs: &[&TracedTensor]) -> Result<Vec<TracedTensor>> {
187    if matches!(op, StdTensorOp::Extension(_)) {
188        return Err(Error::invalid_argument(
189            "extension::apply_standard_op",
190            ErrorPhase::GraphBuild,
191            "op",
192            "Extension ops must be passed to extension::apply",
193        ));
194    }
195    let expected = op.input_count();
196    if inputs.len() != expected {
197        return Err(Error::invalid_argument(
198            "extension::apply_standard_op",
199            ErrorPhase::GraphBuild,
200            "inputs",
201            format!("op expects {expected} inputs, got {}", inputs.len()),
202        ));
203    }
204
205    let mut builder = GraphBuilder::<StdTensorOp>::new();
206    for input in inputs {
207        builder.add_parent(input.graph.clone());
208    }
209    let op_inputs: Vec<ValueRef<StdTensorOp>> = inputs
210        .iter()
211        .map(|t| ValueRef::External(t.graph.values()[t.val].key.clone()))
212        .collect();
213    let outputs = builder.add_operation(op, op_inputs, OperationRole::Primary);
214    builder.set_outputs(outputs.clone());
215    let graph = Arc::new(builder.build());
216    let analysis = register_scoped_graph_analysis(graph.as_ref(), std::iter::empty())?;
217    let output_metas = outputs
218        .iter()
219        .map(|&output| {
220            let meta = registered_meta(&graph.values()[output].key)?;
221            let shape = meta.bound_shape().ok_or_else(|| {
222                Error::invalid_argument(
223                    "extension::apply_standard_op",
224                    ErrorPhase::Compile,
225                    "output_metadata",
226                    "standard op produced unknown output shape metadata",
227                )
228            })?;
229            Ok((meta.dtype, shape))
230        })
231        .collect::<Result<Vec<_>>>()?;
232    traced_outputs_from_analysis(inputs, graph, &outputs, output_metas, analysis)
233}
234
235/// Attach an extension's inferred shape contract to an equivalent expanded output.
236///
237/// Standard extension crates use this when a traced fast path lowers an extension
238/// directly to core operations. The extension remains the single source of truth
239/// for metadata equalities while the executable graph keeps the core-operation
240/// fast path.
241///
242/// # Examples
243///
244/// ```rust
245/// # use std::{any::Any, sync::Arc};
246/// use tenferro_ops::ExtensionShapeContext;
247/// use tenferro_runtime::extension::{attach_expanded_shape_contract, ExtensionOp};
248/// use tenferro_runtime::{DType, SymDim, TracedTensor};
249///
250/// # #[derive(Clone, Debug)]
251/// # struct SameShapeAdd;
252/// # impl ExtensionOp for SameShapeAdd {
253/// #     fn family_id(&self) -> &'static str { "example.same-shape-add.v1" }
254/// #     fn payload_hash(&self, _hasher: &mut dyn std::hash::Hasher) {}
255/// #     fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
256/// #         other.as_any().downcast_ref::<Self>().is_some()
257/// #     }
258/// #     fn clone_arc(&self) -> Arc<dyn ExtensionOp> { Arc::new(self.clone()) }
259/// #     fn as_any(&self) -> &dyn Any { self }
260/// #     fn input_count(&self) -> usize { 2 }
261/// #     fn output_count(&self) -> usize { 1 }
262/// #     fn infer_output_meta(
263/// #         &self,
264/// #         ctx: &mut ExtensionShapeContext<'_>,
265/// #     ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
266/// #         ctx.require_same_shape(0, 1)?;
267/// #         Ok(vec![(ctx.input_dtype(0)?, ctx.input_shape(0)?.to_vec())])
268/// #     }
269/// # }
270/// let lhs = TracedTensor::input_symbolic_shape(DType::F64, 1)?;
271/// let rhs = TracedTensor::input_symbolic_shape(DType::F64, 1)?;
272/// let expanded = (&lhs + &rhs)?;
273/// let output = attach_expanded_shape_contract(&SameShapeAdd, &[&lhs, &rhs], expanded)?;
274/// assert_eq!(output.rank, 1);
275/// # Ok::<(), tenferro_runtime::Error>(())
276/// ```
277#[doc(hidden)]
278pub fn attach_expanded_shape_contract(
279    op: &dyn ExtensionOp,
280    inputs: &[&TracedTensor],
281    output: TracedTensor,
282) -> Result<TracedTensor> {
283    if op.output_count() != 1 {
284        return Err(Error::invalid_argument(
285            "extension::attach_expanded_shape_contract",
286            ErrorPhase::GraphBuild,
287            "outputs",
288            format!(
289                "extension family {:?} contract expects {} outputs, got one expanded output",
290                op.family_id(),
291                op.output_count(),
292            ),
293        ));
294    }
295    let (_, inferred) = infer_expanded_shape_contract(op, inputs)?;
296    attach_inferred_expanded_shape_contract(inputs, vec![output], inferred)?
297        .into_iter()
298        .next()
299        .ok_or_else(|| Error::Internal("expanded shape contract returned no output".into()))
300}
301
302fn infer_expanded_shape_contract(
303    op: &dyn ExtensionOp,
304    inputs: &[&TracedTensor],
305) -> Result<(ExpandedOutputMetas, InferredExtensionMeta)> {
306    if inputs.len() != op.input_count() {
307        return Err(Error::invalid_argument(
308            "extension::infer_expanded_shape_contract",
309            ErrorPhase::GraphBuild,
310            "inputs",
311            format!(
312                "extension family {:?} contract expects {} inputs, got {}",
313                op.family_id(),
314                op.input_count(),
315                inputs.len()
316            ),
317        ));
318    }
319    let input_dtypes: Vec<_> = inputs.iter().map(|input| input.dtype).collect();
320    let input_shapes: Vec<_> = inputs
321        .iter()
322        .enumerate()
323        .map(|(input_idx, input)| DimExpr::input_shape(input_idx, input.rank))
324        .collect();
325    let input_shape_refs: Vec<_> = input_shapes.iter().map(Vec::as_slice).collect();
326    let inferred =
327        infer_extension_output_meta_with_constraints(op, &input_dtypes, &input_shape_refs)?;
328    let input_sym_shapes = inputs
329        .iter()
330        .map(|input| {
331            (0..input.rank)
332                .map(|axis| input.axis_sym_dim(axis))
333                .collect::<Result<Vec<_>>>()
334        })
335        .collect::<Result<Vec<_>>>()?;
336    let input_sym_shape_refs = input_sym_shapes
337        .iter()
338        .map(Vec::as_slice)
339        .collect::<Vec<_>>();
340    let output_metas = inferred
341        .output_metas
342        .iter()
343        .map(|(dtype, shape)| {
344            (
345                *dtype,
346                shape
347                    .iter()
348                    .map(|dim| SymDim::from_dim_expr(dim, &input_sym_shape_refs))
349                    .collect(),
350            )
351        })
352        .collect();
353    Ok((output_metas, inferred))
354}
355
356fn attach_inferred_expanded_shape_contract(
357    inputs: &[&TracedTensor],
358    mut outputs: Vec<TracedTensor>,
359    inferred: InferredExtensionMeta,
360) -> Result<Vec<TracedTensor>> {
361    if inferred.output_metas.len() != outputs.len() {
362        return Err(Error::invalid_argument(
363            "extension::attach_expanded_shape_contract",
364            ErrorPhase::GraphBuild,
365            "outputs",
366            format!(
367                "extension contract inferred {} outputs, but expanded graph produced {}",
368                inferred.output_metas.len(),
369                outputs.len()
370            ),
371        ));
372    }
373    for (output, (dtype, local_shape)) in outputs.iter().zip(inferred.output_metas.iter()) {
374        if output.dtype != *dtype || output.rank != local_shape.len() {
375            return Err(Error::invalid_argument(
376                "extension::attach_expanded_shape_contract",
377                ErrorPhase::GraphBuild,
378                "outputs",
379                format!(
380                    "extension contract inferred output {:?} rank {}, but expanded output is {:?} rank {}",
381                    dtype,
382                    local_shape.len(),
383                    output.dtype,
384                    output.rank
385                ),
386            ));
387        }
388    }
389    if inferred.constraints.is_empty() {
390        return Ok(outputs);
391    }
392
393    let origins = outputs
394        .iter()
395        .map(|output| output.graph.values()[output.val].key.clone())
396        .collect::<Vec<_>>();
397    let input_keys = inputs
398        .iter()
399        .map(|input| input.graph.values()[input.val].key.clone())
400        .collect::<Vec<_>>();
401    let constraints = inferred
402        .constraints
403        .into_iter()
404        .map(|local| ScopedShapeConstraint {
405            origins: origins.clone(),
406            inputs: input_keys.clone(),
407            local,
408        })
409        .collect();
410    let scope = Arc::new(ShapeConstraintScope::new(constraints));
411    for output in &mut outputs {
412        output.constraint_scopes =
413            ConstraintScopeChain::with_scope(Arc::clone(&scope), [&output.constraint_scopes]);
414    }
415    Ok(outputs)
416}
417
418/// Apply an expanded core graph while retaining one extension metadata contract.
419///
420/// Metadata inference runs exactly once. Its output metadata builds the traced
421/// outputs and its equality constraints are attached to those same outputs.
422///
423/// # Examples
424///
425/// ```rust
426/// # use std::{any::Any, sync::Arc};
427/// use computegraph::types::OperationRole;
428/// use tenferro_ops::{std_tensor_op::StdTensorOp, ExtensionShapeContext};
429/// use tenferro_runtime::extension::{apply_expanded_graph_with_shape_contract, ExtensionOp};
430/// use tenferro_runtime::{DType, SymDim, TracedTensor};
431///
432/// # #[derive(Clone, Debug)]
433/// # struct SameShapeAdd;
434/// # impl ExtensionOp for SameShapeAdd {
435/// #     fn family_id(&self) -> &'static str { "example.expanded-add.v1" }
436/// #     fn payload_hash(&self, _hasher: &mut dyn std::hash::Hasher) {}
437/// #     fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
438/// #         other.as_any().downcast_ref::<Self>().is_some()
439/// #     }
440/// #     fn clone_arc(&self) -> Arc<dyn ExtensionOp> { Arc::new(self.clone()) }
441/// #     fn as_any(&self) -> &dyn Any { self }
442/// #     fn input_count(&self) -> usize { 2 }
443/// #     fn output_count(&self) -> usize { 1 }
444/// #     fn infer_output_meta(
445/// #         &self,
446/// #         ctx: &mut ExtensionShapeContext<'_>,
447/// #     ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
448/// #         ctx.require_same_shape(0, 1)?;
449/// #         Ok(vec![(ctx.input_dtype(0)?, ctx.input_shape(0)?.to_vec())])
450/// #     }
451/// # }
452/// let lhs = TracedTensor::input_symbolic_shape(DType::F64, 1)?;
453/// let rhs = TracedTensor::input_symbolic_shape(DType::F64, 1)?;
454/// let outputs = apply_expanded_graph_with_shape_contract(
455///     &SameShapeAdd,
456///     &[&lhs, &rhs],
457///     |builder, inputs| {
458///         Ok(builder.add_operation(StdTensorOp::Add, inputs.to_vec(), OperationRole::Primary))
459///     },
460/// )?;
461/// assert_eq!(outputs[0].rank, 1);
462/// # Ok::<(), tenferro_runtime::Error>(())
463/// ```
464#[doc(hidden)]
465pub fn apply_expanded_graph_with_shape_contract(
466    op: &dyn ExtensionOp,
467    inputs: &[&TracedTensor],
468    build: impl FnOnce(&mut GraphBuilder<StdTensorOp>, &[ValueRef<StdTensorOp>]) -> Result<Vec<usize>>,
469) -> Result<Vec<TracedTensor>> {
470    let (output_metas, inferred) = infer_expanded_shape_contract(op, inputs)?;
471    let outputs = apply_expanded_graph(inputs, output_metas, build)?;
472    attach_inferred_expanded_shape_contract(inputs, outputs, inferred)
473}
474
475/// Apply an extension-provided lowering as ordinary traced graph operations.
476///
477/// This is for extension crates whose operation can be expanded at graph-build
478/// time. It preserves the same parent graph and metadata merging behavior as
479/// [`apply`], but does not insert a `StdTensorOp::Extension` carrier.
480///
481/// # Errors
482///
483/// Returns [`Error::Validation`] with `InvalidArgument` when lowering produces
484/// an invalid output count or unknown output metadata, [`Error::Internal`] for
485/// an invalid graph reference, and [`Error::RuntimeStateSource`] when metadata
486/// registration cannot retain the lowered graph state.
487pub fn apply_expanded_graph(
488    inputs: &[&TracedTensor],
489    output_metas: Vec<(tenferro_tensor::DType, Vec<SymDim>)>,
490    build: impl FnOnce(&mut GraphBuilder<StdTensorOp>, &[ValueRef<StdTensorOp>]) -> Result<Vec<usize>>,
491) -> Result<Vec<TracedTensor>> {
492    let mut builder = GraphBuilder::<StdTensorOp>::new();
493    for input in inputs {
494        builder.add_parent(input.graph.clone());
495    }
496    let op_inputs: Vec<ValueRef<StdTensorOp>> = inputs
497        .iter()
498        .map(|t| ValueRef::External(t.graph.values()[t.val].key.clone()))
499        .collect();
500    let outputs = build(&mut builder, &op_inputs)?;
501    if outputs.len() != output_metas.len() {
502        return Err(Error::invalid_argument(
503            "extension::apply_expanded_graph",
504            ErrorPhase::GraphBuild,
505            "outputs",
506            format!(
507                "extension expanded graph returned {} outputs for {} output metadata entries",
508                outputs.len(),
509                output_metas.len()
510            ),
511        ));
512    }
513    builder.set_outputs(outputs.clone());
514    let graph = Arc::new(builder.build());
515    let analysis = register_scoped_graph_analysis(graph.as_ref(), std::iter::empty())?;
516    traced_outputs_from_analysis(inputs, graph, &outputs, output_metas, analysis)
517}
518
519fn traced_outputs_from_analysis(
520    inputs: &[&TracedTensor],
521    graph: Arc<computegraph::graph::Graph<StdTensorOp>>,
522    outputs: &[usize],
523    output_metas: Vec<(tenferro_tensor::DType, Vec<SymDim>)>,
524    analysis: RegisteredGraphAnalysis,
525) -> Result<Vec<TracedTensor>> {
526    let metadata_scope = Arc::new(analysis.metadata);
527    let constraint_scope = Arc::new(analysis.constraints);
528
529    let merged_map = merge_traced_inputs_map(inputs.iter().copied());
530    let mut extra_roots = Vec::new();
531    let mut checkpoint_chain = None;
532    let metadata_scopes = MetadataScopeChain::with_scope(
533        Arc::clone(&metadata_scope),
534        inputs.iter().map(|input| &input.metadata_scopes),
535    );
536    let constraint_scopes = if constraint_scope.is_empty() {
537        ConstraintScopeChain::merge(inputs.iter().map(|input| &input.constraint_scopes))
538    } else {
539        ConstraintScopeChain::with_scope(
540            constraint_scope,
541            inputs.iter().map(|input| &input.constraint_scopes),
542        )
543    };
544    for input in inputs {
545        extra_roots.extend(input.extra_roots.iter().cloned());
546        checkpoint_chain =
547            CheckpointNode::merge_chains(checkpoint_chain, input.checkpoint_chain.clone());
548    }
549    let all_inputs_concrete = inputs.iter().all(|t| t.shape_hint.is_some());
550    Ok(outputs
551        .iter()
552        .zip(output_metas)
553        .map(|(&val, (dtype, shape))| {
554            let shape_hint = if all_inputs_concrete {
555                Some(shape.clone())
556            } else {
557                None
558            };
559            TracedTensor {
560                id: next_traced_id(),
561                rank: shape.len(),
562                dtype,
563                graph: graph.clone(),
564                val,
565                data: None,
566                shape_hint,
567                inputs_map: merged_map.clone(),
568                extra_roots: extra_roots.clone(),
569                checkpoint_chain: checkpoint_chain.clone(),
570                metadata_scopes: metadata_scopes.clone(),
571                constraint_scopes: constraint_scopes.clone(),
572            }
573        })
574        .collect())
575}
576
577#[cfg(test)]
578mod tests;