polydat_core/iteration/comprehension/surfaces/polydat_kernel.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Bridge between the algebra-layer [`KernelScope`] surface
5//! and polydat's [`PolydatKernel`] scope-binding primitives.
6//!
7//! Implements `KernelScope` for a `PolydatKernelScope` wrapper that
8//! holds a `(canonical, parent)` `PolydatKernel` pair. Each
9//! `scope(coords)` call delegates to
10//! [`PolydatKernel::for_iteration`] — the established polydat
11//! primitive for "materialize a fresh per-iteration child of
12//! `parent` based on `canonical` with these bindings."
13//!
14//! This is the load-bearing bridge for the PR 9 migration:
15//! once `KernelScope for PolydatKernelScope` exists, the algebra-
16//! layer `ScopedKernelStream<PolydatKernelScope>` can replace
17//! the legacy `ComprehensionIter` everywhere nb-activity
18//! drives comprehension dispatch.
19
20use std::sync::Arc;
21
22use crate::ast::Value;
23use crate::iteration::comprehension::strategies::{Tuple, TupleValue};
24use crate::kernel::PolydatKernel;
25
26use super::instance::KernelScope;
27
28/// Wrapper around a `(canonical, parent)` `PolydatKernel` pair that
29/// implements [`KernelScope`].
30///
31/// - `canonical` is the comprehension's prototype kernel —
32/// built once at scope-synthesis time, materialized fresh
33/// per iteration via `for_iteration`.
34/// - `parent` is the enclosing scope's kernel — provides the
35/// outer scope chain that `materialize_subscope` wires
36/// into every iteration's child.
37///
38/// `scope(coords)` converts the algebra-layer `Tuple` to a
39/// polydat `[(String, Value)]` bindings slice and calls
40/// `PolydatKernel::for_iteration(&canonical, &parent, &bindings)`.
41#[derive(Debug, Clone)]
42pub struct PolydatKernelScope {
43 canonical: Arc<PolydatKernel>,
44 parent: Arc<PolydatKernel>,
45}
46
47impl PolydatKernelScope {
48 /// Construct the scope wrapper from the comprehension's
49 /// canonical kernel and the enclosing parent kernel.
50 pub fn new(canonical: Arc<PolydatKernel>, parent: Arc<PolydatKernel>) -> Self {
51 Self { canonical, parent }
52 }
53
54 /// Access the canonical kernel — useful when the consumer
55 /// needs to share metadata (input manifest, scope
56 /// coordinates) without invoking `scope`.
57 pub fn canonical(&self) -> &Arc<PolydatKernel> {
58 &self.canonical
59 }
60
61 /// Access the parent kernel.
62 pub fn parent(&self) -> &Arc<PolydatKernel> {
63 &self.parent
64 }
65}
66
67impl KernelScope for PolydatKernelScope {
68 /// Each scope produces a fresh `Arc<PolydatKernel>` — the
69 /// per-iteration child kernel. Consumers can clone the Arc
70 /// cheaply if they need multiple references.
71 type Scoped = Arc<PolydatKernel>;
72
73 fn scope(&self, coords: &Tuple) -> Arc<PolydatKernel> {
74 let bindings: Vec<(String, Value)> = coords
75 .bindings
76 .iter()
77 .map(|(name, val)| (name.clone(), tuple_value_to_polydat_value(val)))
78 .collect();
79 PolydatKernel::for_iteration(&self.canonical, &self.parent, &bindings)
80 }
81}
82
83/// Convert an algebra-layer [`TupleValue`] to a polydat
84/// [`Value`].
85///
86/// The algebra layer's `TupleValue::I64` doesn't exist in
87/// polydat's `Value`; integers there are unconditionally
88/// `U64`. We bitcast `i64 as u64` — for the comprehension
89/// use case (iteration coordinates, typically non-negative
90/// integers), this preserves the bit pattern; consumers that
91/// care about signedness should use `as i64` to recover.
92pub fn tuple_value_to_polydat_value(val: &TupleValue) -> Value {
93 match val {
94 TupleValue::U64(n) => Value::U64(*n),
95 TupleValue::I64(n) => Value::U64(*n as u64),
96 TupleValue::F64(f) => Value::F64(*f),
97 TupleValue::Str(s) => Value::Str(Arc::from(s.as_str())),
98 TupleValue::Bool(b) => Value::Bool(*b),
99 }
100}
101
102/// Reverse conversion — polydat `Value` to algebra-layer
103/// `TupleValue`. Only the subset of `Value` variants that
104/// have a corresponding `TupleValue` are converted; richer
105/// `Value` variants (`Bytes`, `Json`, `Ext`, `Handle`,
106/// `Vec*`, etc.) return `None` so the caller can handle
107/// the unsupported case explicitly.
108pub fn polydat_value_to_tuple_value(val: &Value) -> Option<TupleValue> {
109 match val {
110 Value::U64(n) => Some(TupleValue::U64(*n)),
111 Value::F64(f) => Some(TupleValue::F64(*f)),
112 Value::Bool(b) => Some(TupleValue::Bool(*b)),
113 Value::Str(s) => Some(TupleValue::Str(s.to_string())),
114 _ => None,
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn tuple_value_to_polydat_round_trips_u64() {
124 let tv = TupleValue::U64(42);
125 let pv = tuple_value_to_polydat_value(&tv);
126 let back = polydat_value_to_tuple_value(&pv).unwrap();
127 assert_eq!(tv, back);
128 }
129
130 #[test]
131 fn tuple_value_to_polydat_round_trips_f64() {
132 let tv = TupleValue::F64(3.14);
133 let pv = tuple_value_to_polydat_value(&tv);
134 let back = polydat_value_to_tuple_value(&pv).unwrap();
135 assert_eq!(tv, back);
136 }
137
138 #[test]
139 fn tuple_value_to_polydat_round_trips_string() {
140 let tv = TupleValue::Str("hello".into());
141 let pv = tuple_value_to_polydat_value(&tv);
142 let back = polydat_value_to_tuple_value(&pv).unwrap();
143 assert_eq!(tv, back);
144 }
145
146 #[test]
147 fn tuple_value_to_polydat_round_trips_bool() {
148 let tv = TupleValue::Bool(true);
149 let pv = tuple_value_to_polydat_value(&tv);
150 let back = polydat_value_to_tuple_value(&pv).unwrap();
151 assert_eq!(tv, back);
152 }
153
154 #[test]
155 fn i64_converts_via_bitcast() {
156 let tv = TupleValue::I64(123);
157 let pv = tuple_value_to_polydat_value(&tv);
158 match pv {
159 Value::U64(n) => assert_eq!(n, 123u64),
160 other => panic!("expected U64, got {other:?}"),
161 }
162 }
163
164 #[test]
165 fn polydat_value_returns_none_for_unsupported_variants() {
166 let pv = Value::Bytes(Arc::from(&b"abc"[..]));
167 assert!(polydat_value_to_tuple_value(&pv).is_none());
168 }
169}