1use std::cmp::Reverse;
2use std::collections::{BTreeMap, BTreeSet, BinaryHeap};
3
4use serde::{Deserialize, Serialize};
5
6use crate::errors::{DataError, DataErrorCode, DataResult};
7use crate::model::{TypeExpr, Value};
8
9pub trait Converter: Send + Sync {
26 fn id(&self) -> ConverterId;
27 fn input(&self) -> &TypeExpr;
28 fn output(&self) -> &TypeExpr;
29 fn cost(&self) -> u64;
30 fn feature_flags(&self) -> &[String] {
31 &[]
32 }
33 fn requires_gpu(&self) -> bool {
34 false
35 }
36 fn convert(&self, value: Value) -> DataResult<Value>;
37}
38
39#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
47pub struct ConverterId(pub String);
48
49#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
63pub struct ConversionProvenance {
64 pub steps: Vec<ConverterId>,
65 pub total_cost: u64,
66 pub skipped_cycles: Vec<ConverterId>,
67 pub skipped_gpu: Vec<ConverterId>,
68 pub skipped_features: Vec<ConverterId>,
69}
70
71#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
87pub struct ConversionResolution {
88 pub provenance: ConversionProvenance,
89}
90
91impl ConversionResolution {
92 pub fn notes(&self) -> Vec<String> {
94 let mut out = Vec::new();
95 if !self.provenance.skipped_cycles.is_empty() {
96 out.push(format!(
97 "skipped cycles: {:?}",
98 self.provenance.skipped_cycles
99 ));
100 }
101 if !self.provenance.skipped_gpu.is_empty() {
102 out.push(format!(
103 "skipped GPU-only converters: {:?}",
104 self.provenance.skipped_gpu
105 ));
106 }
107 if !self.provenance.skipped_features.is_empty() {
108 out.push(format!(
109 "skipped converters missing features: {:?}",
110 self.provenance.skipped_features
111 ));
112 }
113 out
114 }
115}
116
117#[derive(Clone, Debug)]
118struct Edge {
119 to: TypeExpr,
120 id: ConverterId,
121 cost: u64,
122 feature_flags: Vec<String>,
123 requires_gpu: bool,
124}
125
126#[derive(Default)]
146pub struct ConverterGraph {
147 converters: BTreeMap<ConverterId, Box<dyn Converter>>,
148 adjacency: BTreeMap<TypeExpr, BTreeSet<Edge>>,
149}
150
151pub type SharedConverterGraph = std::sync::Arc<std::sync::RwLock<ConverterGraph>>;
178
179impl ConverterGraph {
180 pub fn new() -> Self {
182 Self {
183 converters: BTreeMap::new(),
184 adjacency: BTreeMap::new(),
185 }
186 }
187
188 pub fn register(&mut self, converter: Box<dyn Converter>) {
190 let id = converter.id();
191 let input = converter.input().clone().normalize();
192 let mut flags = converter.feature_flags().to_vec();
193 flags.sort();
194 let edge = Edge {
195 to: converter.output().clone().normalize(),
196 id: id.clone(),
197 cost: converter.cost(),
198 feature_flags: flags,
199 requires_gpu: converter.requires_gpu(),
200 };
201 self.adjacency.entry(input).or_default().insert(edge);
202 self.converters.insert(id, converter);
203 }
204
205 pub fn resolve(&self, from: &TypeExpr, to: &TypeExpr) -> DataResult<ConversionResolution> {
207 self.resolve_with_context(from, to, &[], true)
208 }
209
210 pub fn resolve_with_context(
212 &self,
213 from: &TypeExpr,
214 to: &TypeExpr,
215 active_features: &[String],
216 allow_gpu: bool,
217 ) -> DataResult<ConversionResolution> {
218 let from = from.clone().normalize();
219 let to = to.clone().normalize();
220 if from == to {
221 return Ok(ConversionResolution {
222 provenance: ConversionProvenance {
223 steps: Vec::new(),
224 total_cost: 0,
225 skipped_cycles: Vec::new(),
226 skipped_gpu: Vec::new(),
227 skipped_features: Vec::new(),
228 },
229 });
230 }
231
232 #[allow(clippy::type_complexity)]
234 type HeapEntry = (
235 Reverse<u64>,
236 TypeExpr,
237 Vec<ConverterId>,
238 BTreeSet<TypeExpr>,
239 ConversionProvenance,
240 );
241 let mut dist: BTreeMap<TypeExpr, (u64, Vec<ConverterId>, ConversionProvenance)> =
242 BTreeMap::new();
243 let mut heap: BinaryHeap<HeapEntry> = BinaryHeap::new();
244
245 dist.insert(
246 from.clone(),
247 (
248 0,
249 Vec::new(),
250 ConversionProvenance {
251 steps: Vec::new(),
252 total_cost: 0,
253 skipped_cycles: Vec::new(),
254 skipped_gpu: Vec::new(),
255 skipped_features: Vec::new(),
256 },
257 ),
258 );
259 heap.push((
260 Reverse(0),
261 from.clone(),
262 Vec::new(),
263 {
264 let mut set = BTreeSet::new();
265 set.insert(from.clone());
266 set
267 },
268 ConversionProvenance {
269 steps: Vec::new(),
270 total_cost: 0,
271 skipped_cycles: Vec::new(),
272 skipped_gpu: Vec::new(),
273 skipped_features: Vec::new(),
274 },
275 ));
276
277 while let Some((Reverse(cost), node, path, visited, provenance)) = heap.pop() {
278 if let Some((known, _, _)) = dist.get(&node)
279 && *known < cost
280 {
281 continue;
282 }
283 if node == to {
284 return Ok(ConversionResolution {
285 provenance: ConversionProvenance {
286 total_cost: cost,
287 steps: path,
288 skipped_cycles: provenance.skipped_cycles,
289 skipped_gpu: provenance.skipped_gpu,
290 skipped_features: provenance.skipped_features,
291 },
292 });
293 }
294
295 if let Some(edges) = self.adjacency.get(&node) {
296 for edge in edges {
297 if edge.requires_gpu && !allow_gpu {
298 let mut prov = provenance.clone();
299 prov.skipped_gpu.push(edge.id.clone());
300 continue;
301 }
302 if !edge
303 .feature_flags
304 .iter()
305 .all(|f| active_features.contains(f))
306 {
307 let mut prov = provenance.clone();
308 prov.skipped_features.push(edge.id.clone());
309 continue;
310 }
311 if visited.contains(&edge.to) {
312 let mut prov = provenance.clone();
313 prov.skipped_cycles.push(edge.id.clone());
314 continue; }
316 let next_cost = cost.saturating_add(edge.cost);
317 let mut next_path = path.clone();
318 next_path.push(edge.id.clone());
319 let mut next_prov = provenance.clone();
320 next_prov.steps = next_path.clone();
321 next_prov.total_cost = next_cost;
322 let entry = dist.get(&edge.to);
323 let should_update = entry.map(|(c, _, _)| next_cost < *c).unwrap_or(true);
324 if should_update {
325 dist.insert(
326 edge.to.clone(),
327 (next_cost, next_path.clone(), next_prov.clone()),
328 );
329 let mut next_visited = visited.clone();
330 next_visited.insert(edge.to.clone());
331 heap.push((
332 Reverse(next_cost),
333 edge.to.clone(),
334 next_path,
335 next_visited,
336 next_prov,
337 ));
338 }
339 }
340 }
341 }
342
343 Err(DataError::new(
344 DataErrorCode::UnknownConverter,
345 "no conversion path found",
346 ))
347 }
348
349 #[cfg(feature = "async")]
351 pub async fn resolve_with_context_async(
352 &self,
353 from: &TypeExpr,
354 to: &TypeExpr,
355 active_features: &[String],
356 allow_gpu: bool,
357 ) -> DataResult<ConversionResolution> {
358 self.resolve_with_context(from, to, active_features, allow_gpu)
359 }
360
361 #[cfg(feature = "async")]
363 pub async fn resolve_async(
364 &self,
365 from: &TypeExpr,
366 to: &TypeExpr,
367 ) -> DataResult<ConversionResolution> {
368 self.resolve(from, to)
369 }
370}
371
372pub struct ConverterBuilder<F>
405where
406 F: Fn(Value) -> DataResult<Value> + Send + Sync + 'static,
407{
408 id: ConverterId,
409 input: TypeExpr,
410 output: TypeExpr,
411 cost: u64,
412 feature_flags: Vec<String>,
413 requires_gpu: bool,
414 func: F,
415}
416
417impl<F> ConverterBuilder<F>
418where
419 F: Fn(Value) -> DataResult<Value> + Send + Sync + 'static,
420{
421 pub fn new(id: impl Into<String>, input: TypeExpr, output: TypeExpr, func: F) -> Self {
422 Self {
423 id: ConverterId(id.into()),
424 input: input.normalize(),
425 output: output.normalize(),
426 cost: 1,
427 feature_flags: Vec::new(),
428 requires_gpu: false,
429 func,
430 }
431 }
432
433 pub fn cost(mut self, cost: u64) -> Self {
434 self.cost = cost;
435 self
436 }
437
438 pub fn feature_flag(mut self, flag: impl Into<String>) -> Self {
439 self.feature_flags.push(flag.into());
440 self
441 }
442
443 pub fn requires_gpu(mut self, requires: bool) -> Self {
444 self.requires_gpu = requires;
445 self
446 }
447
448 pub fn build(self) -> FnConverter<F> {
449 let mut flags = self.feature_flags;
450 flags.sort();
451 FnConverter {
452 id: self.id,
453 input: self.input,
454 output: self.output,
455 cost: self.cost,
456 feature_flags: flags,
457 requires_gpu: self.requires_gpu,
458 func: self.func,
459 }
460 }
461
462 pub fn build_boxed(self) -> Box<dyn Converter> {
463 Box::new(self.build())
464 }
465}
466
467pub struct FnConverter<F>
482where
483 F: Fn(Value) -> DataResult<Value> + Send + Sync + 'static,
484{
485 id: ConverterId,
486 input: TypeExpr,
487 output: TypeExpr,
488 cost: u64,
489 feature_flags: Vec<String>,
490 requires_gpu: bool,
491 func: F,
492}
493
494impl<F> Converter for FnConverter<F>
495where
496 F: Fn(Value) -> DataResult<Value> + Send + Sync + 'static,
497{
498 fn id(&self) -> ConverterId {
499 self.id.clone()
500 }
501
502 fn input(&self) -> &TypeExpr {
503 &self.input
504 }
505
506 fn output(&self) -> &TypeExpr {
507 &self.output
508 }
509
510 fn cost(&self) -> u64 {
511 self.cost
512 }
513
514 fn feature_flags(&self) -> &[String] {
515 &self.feature_flags
516 }
517
518 fn requires_gpu(&self) -> bool {
519 self.requires_gpu
520 }
521
522 fn convert(&self, value: Value) -> DataResult<Value> {
523 (self.func)(value)
524 }
525}
526
527impl Ord for Edge {
528 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
529 (self.cost, &self.id).cmp(&(other.cost, &other.id))
530 }
531}
532
533impl PartialOrd for Edge {
534 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
535 Some(self.cmp(other))
536 }
537}
538
539impl PartialEq for Edge {
540 fn eq(&self, other: &Self) -> bool {
541 self.id == other.id
542 }
543}
544
545impl Eq for Edge {}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550 use crate::model::{TypeExpr, Value, ValueType};
551 use once_cell::sync::Lazy;
552 use proptest::prelude::*;
553 #[cfg(feature = "async")]
554 use std::future::Future;
555 #[cfg(feature = "async")]
556 use std::pin::Pin;
557 #[cfg(feature = "async")]
558 use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
559
560 struct Identity {
561 id: ConverterId,
562 ty: TypeExpr,
563 }
564
565 impl Converter for Identity {
566 fn id(&self) -> ConverterId {
567 self.id.clone()
568 }
569 fn input(&self) -> &TypeExpr {
570 &self.ty
571 }
572 fn output(&self) -> &TypeExpr {
573 &self.ty
574 }
575 fn cost(&self) -> u64 {
576 0
577 }
578 fn convert(&self, v: Value) -> DataResult<Value> {
579 Ok(v)
580 }
581 }
582
583 struct BoolToInt;
584 impl Converter for BoolToInt {
585 fn id(&self) -> ConverterId {
586 ConverterId("bool_to_int".into())
587 }
588 fn input(&self) -> &TypeExpr {
589 static TY: Lazy<TypeExpr> = Lazy::new(|| TypeExpr::Scalar(ValueType::Bool));
590 &TY
591 }
592 fn output(&self) -> &TypeExpr {
593 static TY: Lazy<TypeExpr> = Lazy::new(|| TypeExpr::Scalar(ValueType::Int));
594 &TY
595 }
596 fn cost(&self) -> u64 {
597 1
598 }
599 fn convert(&self, v: Value) -> DataResult<Value> {
600 match v {
601 Value::Bool(b) => Ok(Value::Int(if b { 1 } else { 0 })),
602 _ => Err(DataError::new(DataErrorCode::InvalidType, "expected bool")),
603 }
604 }
605 }
606
607 struct IntToString;
608 impl Converter for IntToString {
609 fn id(&self) -> ConverterId {
610 ConverterId("int_to_string".into())
611 }
612 fn input(&self) -> &TypeExpr {
613 static TY: Lazy<TypeExpr> = Lazy::new(|| TypeExpr::Scalar(ValueType::Int));
614 &TY
615 }
616 fn output(&self) -> &TypeExpr {
617 static TY: Lazy<TypeExpr> = Lazy::new(|| TypeExpr::Scalar(ValueType::String));
618 &TY
619 }
620 fn cost(&self) -> u64 {
621 2
622 }
623 fn convert(&self, v: Value) -> DataResult<Value> {
624 match v {
625 Value::Int(i) => Ok(Value::String(i.to_string().into())),
626 _ => Err(DataError::new(DataErrorCode::InvalidType, "expected int")),
627 }
628 }
629 }
630
631 struct GpuOnly;
632 impl Converter for GpuOnly {
633 fn id(&self) -> ConverterId {
634 ConverterId("gpu_only".into())
635 }
636 fn input(&self) -> &TypeExpr {
637 static TY: Lazy<TypeExpr> = Lazy::new(|| TypeExpr::Scalar(ValueType::Int));
638 &TY
639 }
640 fn output(&self) -> &TypeExpr {
641 static TY: Lazy<TypeExpr> = Lazy::new(|| TypeExpr::Scalar(ValueType::Float));
642 &TY
643 }
644 fn cost(&self) -> u64 {
645 1
646 }
647 fn requires_gpu(&self) -> bool {
648 true
649 }
650 fn convert(&self, v: Value) -> DataResult<Value> {
651 match v {
652 Value::Int(i) => Ok(Value::Float(i as f64)),
653 _ => Err(DataError::new(DataErrorCode::InvalidType, "expected int")),
654 }
655 }
656 }
657
658 #[test]
659 fn resolves_trivial_path() {
660 let mut graph = ConverterGraph::new();
661 let ty = TypeExpr::Scalar(ValueType::Int);
662 graph.register(Box::new(Identity {
663 id: ConverterId("id".into()),
664 ty: ty.clone(),
665 }));
666 let res = graph.resolve(&ty, &ty).expect("resolve");
667 assert_eq!(res.provenance.total_cost, 0u64);
668 assert!(res.provenance.steps.is_empty());
669 }
670
671 #[test]
672 fn resolves_multi_step_path() {
673 let mut graph = ConverterGraph::new();
674 graph.register(Box::new(BoolToInt));
675 graph.register(Box::new(IntToString));
676 let from = TypeExpr::Scalar(ValueType::Bool);
677 let to = TypeExpr::Scalar(ValueType::String);
678 let res = graph.resolve(&from, &to).expect("resolve");
679 assert_eq!(res.provenance.total_cost, 3u64);
680 assert_eq!(
681 res.provenance.steps,
682 vec![
683 ConverterId("bool_to_int".into()),
684 ConverterId("int_to_string".into())
685 ]
686 );
687 }
688
689 #[test]
690 fn respects_gpu_flag() {
691 let mut graph = ConverterGraph::new();
692 graph.register(Box::new(GpuOnly));
693 let from = TypeExpr::Scalar(ValueType::Int);
694 let to = TypeExpr::Scalar(ValueType::Float);
695 let err = graph
696 .resolve_with_context(&from, &to, &[], false)
697 .unwrap_err();
698 assert_eq!(err.code(), DataErrorCode::UnknownConverter);
699 let res = graph
700 .resolve_with_context(&from, &to, &[], true)
701 .expect("resolve");
702 assert_eq!(res.provenance.steps, vec![ConverterId("gpu_only".into())]);
703 }
704
705 #[test]
706 fn cycles_do_not_hang() {
707 struct AtoB;
708 impl Converter for AtoB {
709 fn id(&self) -> ConverterId {
710 ConverterId("a_to_b".into())
711 }
712 fn input(&self) -> &TypeExpr {
713 static TY: Lazy<TypeExpr> = Lazy::new(|| TypeExpr::Scalar(ValueType::Bool));
714 &TY
715 }
716 fn output(&self) -> &TypeExpr {
717 static TY: Lazy<TypeExpr> = Lazy::new(|| TypeExpr::Scalar(ValueType::Int));
718 &TY
719 }
720 fn cost(&self) -> u64 {
721 1
722 }
723 fn convert(&self, v: Value) -> DataResult<Value> {
724 Ok(v)
725 }
726 }
727
728 struct BtoA;
729 impl Converter for BtoA {
730 fn id(&self) -> ConverterId {
731 ConverterId("b_to_a".into())
732 }
733 fn input(&self) -> &TypeExpr {
734 static TY: Lazy<TypeExpr> = Lazy::new(|| TypeExpr::Scalar(ValueType::Int));
735 &TY
736 }
737 fn output(&self) -> &TypeExpr {
738 static TY: Lazy<TypeExpr> = Lazy::new(|| TypeExpr::Scalar(ValueType::Bool));
739 &TY
740 }
741 fn cost(&self) -> u64 {
742 1
743 }
744 fn convert(&self, v: Value) -> DataResult<Value> {
745 Ok(v)
746 }
747 }
748
749 let mut graph = ConverterGraph::new();
750 graph.register(Box::new(AtoB));
751 graph.register(Box::new(BtoA));
752 let from = TypeExpr::Scalar(ValueType::Bool);
753 let to = TypeExpr::Scalar(ValueType::String);
754 let err = graph.resolve(&from, &to).unwrap_err();
755 assert!(matches!(
756 err.code(),
757 DataErrorCode::UnknownConverter | DataErrorCode::CycleDetected
758 ));
759 }
760
761 #[test]
762 fn skips_cycles_and_finds_alternate_path() {
763 struct AtoB;
764 impl Converter for AtoB {
765 fn id(&self) -> ConverterId {
766 ConverterId("a_to_b".into())
767 }
768 fn input(&self) -> &TypeExpr {
769 static TY: Lazy<TypeExpr> = Lazy::new(|| TypeExpr::Scalar(ValueType::Bool));
770 &TY
771 }
772 fn output(&self) -> &TypeExpr {
773 static TY: Lazy<TypeExpr> = Lazy::new(|| TypeExpr::Scalar(ValueType::Int));
774 &TY
775 }
776 fn cost(&self) -> u64 {
777 1
778 }
779 fn convert(&self, v: Value) -> DataResult<Value> {
780 Ok(v)
781 }
782 }
783
784 struct BtoA;
785 impl Converter for BtoA {
786 fn id(&self) -> ConverterId {
787 ConverterId("b_to_a".into())
788 }
789 fn input(&self) -> &TypeExpr {
790 static TY: Lazy<TypeExpr> = Lazy::new(|| TypeExpr::Scalar(ValueType::Int));
791 &TY
792 }
793 fn output(&self) -> &TypeExpr {
794 static TY: Lazy<TypeExpr> = Lazy::new(|| TypeExpr::Scalar(ValueType::Bool));
795 &TY
796 }
797 fn cost(&self) -> u64 {
798 1
799 }
800 fn convert(&self, v: Value) -> DataResult<Value> {
801 Ok(v)
802 }
803 }
804
805 struct BtoString;
806 impl Converter for BtoString {
807 fn id(&self) -> ConverterId {
808 ConverterId("b_to_string".into())
809 }
810 fn input(&self) -> &TypeExpr {
811 static TY: Lazy<TypeExpr> = Lazy::new(|| TypeExpr::Scalar(ValueType::Int));
812 &TY
813 }
814 fn output(&self) -> &TypeExpr {
815 static TY: Lazy<TypeExpr> = Lazy::new(|| TypeExpr::Scalar(ValueType::String));
816 &TY
817 }
818 fn cost(&self) -> u64 {
819 5
820 }
821 fn convert(&self, v: Value) -> DataResult<Value> {
822 Ok(v)
823 }
824 }
825
826 let mut graph = ConverterGraph::new();
827 graph.register(Box::new(AtoB));
828 graph.register(Box::new(BtoA));
829 graph.register(Box::new(BtoString));
830 let from = TypeExpr::Scalar(ValueType::Bool);
831 let to = TypeExpr::Scalar(ValueType::String);
832 let res = graph.resolve(&from, &to).expect("resolve");
833 assert_eq!(
834 res.provenance.steps,
835 vec![
836 ConverterId("a_to_b".into()),
837 ConverterId("b_to_string".into())
838 ]
839 );
840 }
841
842 #[test]
843 fn errors_when_no_path() {
844 let graph = ConverterGraph::new();
845 let from = TypeExpr::Scalar(ValueType::Bool);
846 let to = TypeExpr::Scalar(ValueType::String);
847 let err = graph.resolve(&from, &to).unwrap_err();
848 assert_eq!(err.code(), DataErrorCode::UnknownConverter);
849 }
850
851 #[test]
852 fn builder_sorts_feature_flags() {
853 let conv = ConverterBuilder::new(
854 "id",
855 TypeExpr::Scalar(ValueType::Int),
856 TypeExpr::Scalar(ValueType::String),
857 Ok,
858 )
859 .feature_flag("b")
860 .feature_flag("a")
861 .build();
862 assert_eq!(conv.feature_flags, vec!["a", "b"]);
863 }
864
865 proptest! {
866 #[test]
867 fn chain_costs_are_additive(len in 1usize..6) {
868 let mut graph = ConverterGraph::new();
870 let types: Vec<TypeExpr> = (0..=len).map(|i| {
871 let v = vec![TypeExpr::Scalar(ValueType::Int); i];
872 TypeExpr::Tuple(v)
873 }).collect();
874 for i in 0..len {
875 let input = types[i].clone();
876 let output = types[i + 1].clone();
877 graph.register(ConverterBuilder::new(
878 format!("c{i}"),
879 input.clone(),
880 output.clone(),
881 Ok,
882 ).cost(1).build_boxed());
883 }
884 let res = graph.resolve(&types[0], &types[len]).expect("resolve chain");
885 prop_assert_eq!(res.provenance.steps.len(), len);
886 prop_assert_eq!(res.provenance.total_cost, len as u64);
887 }
888
889 #[test]
890 fn feature_flag_filtering(allows in proptest::bool::ANY) {
891 let mut graph = ConverterGraph::new();
892 graph.register(
893 ConverterBuilder::new(
894 "flagged",
895 TypeExpr::Scalar(ValueType::Int),
896 TypeExpr::Scalar(ValueType::Float),
897 Ok,
898 )
899 .feature_flag("feat")
900 .build_boxed(),
901 );
902 let from = TypeExpr::Scalar(ValueType::Int);
903 let to = TypeExpr::Scalar(ValueType::Float);
904 let features = if allows { vec!["feat".to_string()] } else { vec![] };
905 let res = graph.resolve_with_context(&from, &to, &features, true);
906 prop_assert_eq!(res.is_ok(), allows);
907 }
908 }
909
910 #[cfg(feature = "async")]
911 fn dummy_raw_waker() -> RawWaker {
912 fn no_op(_: *const ()) {}
913 fn clone(_: *const ()) -> RawWaker {
914 dummy_raw_waker()
915 }
916 static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, no_op, no_op, no_op);
917 RawWaker::new(std::ptr::null(), &VTABLE)
918 }
919
920 #[cfg(feature = "async")]
921 fn block_on<F: Future>(mut fut: F) -> F::Output {
922 let waker: Waker = unsafe { Waker::from_raw(dummy_raw_waker()) };
923 let mut cx = Context::from_waker(&waker);
924 let mut fut = unsafe { Pin::new_unchecked(&mut fut) };
925 loop {
926 match fut.as_mut().poll(&mut cx) {
927 Poll::Ready(val) => return val,
928 Poll::Pending => continue,
929 }
930 }
931 }
932
933 #[cfg(feature = "async")]
934 #[test]
935 fn async_resolve_matches_sync() {
936 let mut graph = ConverterGraph::new();
937 graph.register(
938 ConverterBuilder::new(
939 "id",
940 TypeExpr::Scalar(ValueType::Int),
941 TypeExpr::Scalar(ValueType::Int),
942 Ok,
943 )
944 .build_boxed(),
945 );
946 let from = TypeExpr::Scalar(ValueType::Int);
947 let to = TypeExpr::Scalar(ValueType::Int);
948 let sync = graph.resolve(&from, &to).unwrap();
949 let async_res = block_on(graph.resolve_async(&from, &to)).unwrap();
950 assert_eq!(sync.provenance, async_res.provenance);
951 }
952
953 #[test]
954 fn golden_resolution_is_stable() {
955 let mut graph = ConverterGraph::new();
956 graph.register(Box::new(BoolToInt));
957 graph.register(Box::new(IntToString));
958 let from = TypeExpr::Scalar(ValueType::Bool);
959 let to = TypeExpr::Scalar(ValueType::String);
960 let res = graph.resolve(&from, &to).expect("resolve");
961 let json = serde_json::to_string(&res).expect("serialize");
962 assert_eq!(
963 json,
964 r#"{"provenance":{"steps":["bool_to_int","int_to_string"],"total_cost":3,"skipped_cycles":[],"skipped_gpu":[],"skipped_features":[]}}"#
965 );
966 }
967}