Skip to main content

polydat_core/kernel/
activation.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! The activation runtime for `for` traversals (SRD 113 §3.4, §3.6,
5//! §5).
6//!
7//! A [`TraversalStream`] dispenses one [`Activation`] per tuple of a
8//! compiled traversal. An activation is a fresh state over the body's
9//! program, compiled once at parent compile time, with the tuple's
10//! elements and the parent's cascaded wires bound and every `over`
11//! cursor narrowed. Activation never compiles: the cost of the second
12//! activation is the cost of the first minus nothing, because the first
13//! did not compile either.
14//!
15//! Cycles follow the rule in §3.4: a body with a cursor iterates its
16//! narrowest cursor extent, one cycle per ordinal, with the cursor's
17//! ordinal written before each pull; a body without a cursor has one
18//! cycle per activation.
19
20use std::collections::HashMap;
21use std::sync::Arc;
22
23use crate::ast::Value;
24use crate::dsl::traversal::Traversal;
25use crate::iteration::comprehension::runtime::{RuntimeTuple, evaluate_for_iteration};
26use crate::iteration::cursor_partition::{cursor_extent_on, cursor_over_partitions_on};
27use crate::kernel::Kernel;
28
29use super::{PolydatKernel, PolydatProgram};
30
31/// The interval of ordinals an activation's cursor iterates.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct CursorSlice {
34    /// The cursor's name.
35    pub cursor: String,
36    /// The first ordinal of the slice.
37    pub start: u64,
38    /// One past the last ordinal.
39    pub end: u64,
40}
41
42impl CursorSlice {
43    /// Ordinals in the slice.
44    pub fn len(&self) -> u64 {
45        self.end.saturating_sub(self.start)
46    }
47
48    /// Whether the slice has no ordinal.
49    pub fn is_empty(&self) -> bool {
50        self.len() == 0
51    }
52}
53
54/// One child scope of a traversal: the tuple it was activated for, a
55/// fresh kernel over the body's program, and its cursor slice if the
56/// body declares a cursor. The kernel is the interpreter's by default;
57/// [`TraversalStream::activation_on`] builds one on any engine behind
58/// the [`Kernel`] trait (engine parity, step 8).
59pub struct Activation<K = PolydatKernel> {
60    /// Position of this activation's tuple in the traversal's dispense
61    /// order.
62    pub index: u64,
63    /// The tuple, in element order.
64    pub coords: Vec<(String, Value)>,
65    /// A fresh kernel over the body's shared program.
66    pub kernel: K,
67    /// The narrowest cursor slice, when the body declares a cursor.
68    pub cursor: Option<CursorSlice>,
69}
70
71impl std::fmt::Debug for Activation {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        f.debug_struct("Activation")
74            .field("index", &self.index)
75            .field("coords", &self.coords)
76            .field("cursor", &self.cursor)
77            .field("program_nodes", &self.kernel.program().node_count())
78            .finish()
79    }
80}
81
82impl std::fmt::Debug for Activation<Box<dyn Kernel>> {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        f.debug_struct("Activation")
85            .field("index", &self.index)
86            .field("coords", &self.coords)
87            .field("cursor", &self.cursor)
88            .field("engine", &self.kernel.engine())
89            .finish()
90    }
91}
92
93impl<K> Activation<K> {
94    /// Cycles this activation runs under §3.4: the cursor slice length,
95    /// or one when the body has no cursor.
96    pub fn cycle_count(&self) -> u64 {
97        match &self.cursor {
98            Some(slice) => slice.len(),
99            None => 1,
100        }
101    }
102
103    /// The value of one coordinate.
104    pub fn coord(&self, name: &str) -> Option<&Value> {
105        self.coords.iter().find(|(n, _)| n == name).map(|(_, v)| v)
106    }
107}
108
109impl Activation<Box<dyn Kernel>> {
110    /// Position the kernel at cycle `i` and return it ready to pull, as
111    /// the interpreter activation's `cycle` does.
112    pub fn cycle(&mut self, i: u64) -> &mut dyn Kernel {
113        self.kernel.set_inputs(&[i]);
114        if let Some(slice) = &self.cursor {
115            let ordinal = slice.start.saturating_add(i);
116            let slot = format!("{}__ordinal", slice.cursor);
117            // A body without the projection has no slot to write.
118            let _ = self.kernel.set_input(&slot, Value::U64(ordinal));
119        }
120        self.kernel.as_mut()
121    }
122
123    /// Run `f` once per cycle, in order.
124    pub fn for_each_cycle(&mut self, mut f: impl FnMut(u64, &mut dyn Kernel)) {
125        for i in 0..self.cycle_count() {
126            let kernel = self.cycle(i);
127            f(i, kernel);
128        }
129    }
130}
131
132impl Activation {
133    /// Position the kernel at cycle `i` and return it ready to pull.
134    /// The body's `cycle` coordinate is the local index; the cursor's
135    /// ordinal slot receives the absolute ordinal.
136    pub fn cycle(&mut self, i: u64) -> &mut PolydatKernel {
137        self.kernel.set_inputs(&[i]);
138        if let Some(slice) = &self.cursor {
139            let ordinal = slice.start.saturating_add(i);
140            let slot = format!("{}__ordinal", slice.cursor);
141            if let Some(idx) = self.kernel.program().find_input(&slot) {
142                self.kernel.state().set_input(idx, Value::U64(ordinal));
143            }
144        }
145        &mut self.kernel
146    }
147
148    /// Run `f` once per cycle, in order.
149    pub fn for_each_cycle(&mut self, mut f: impl FnMut(u64, &mut PolydatKernel)) {
150        for i in 0..self.cycle_count() {
151            let kernel = self.cycle(i);
152            f(i, kernel);
153        }
154    }
155}
156
157/// Dispenses activations for one traversal of a kernel.
158pub struct TraversalStream {
159    traversal: Traversal,
160    tuples: Vec<RuntimeTuple>,
161    cascade: Vec<(String, Value)>,
162    next: usize,
163}
164
165impl TraversalStream {
166    /// Number of activations the traversal dispenses.
167    pub fn len(&self) -> usize {
168        self.tuples.len()
169    }
170
171    /// Whether the traversal dispenses no activation.
172    pub fn is_empty(&self) -> bool {
173        self.tuples.is_empty()
174    }
175
176    /// The traversal this stream dispenses.
177    pub fn traversal(&self) -> &Traversal {
178        &self.traversal
179    }
180
181    /// Move the dispense position. Every strategy is a decidable
182    /// permutation, so seeking costs nothing beyond the index.
183    pub fn seek(&mut self, index: usize) {
184        self.next = index.min(self.tuples.len());
185    }
186
187    /// Current dispense position.
188    pub fn position(&self) -> usize {
189        self.next
190    }
191
192    /// The next activation, or `None` when exhausted.
193    pub fn advance(&mut self) -> Result<Option<Activation>, String> {
194        if self.next >= self.tuples.len() {
195            return Ok(None);
196        }
197        let i = self.next;
198        self.next += 1;
199        self.activation(i).map(Some)
200    }
201
202    /// Build the activation at `index` without moving the dispense
203    /// position. Fibers partition a traversal by calling this over
204    /// disjoint index ranges.
205    pub fn activation(&self, index: usize) -> Result<Activation, String> {
206        let tuple = self.tuples.get(index).ok_or_else(|| {
207            format!(
208                "activation index {index} is out of range; traversal has {} tuples",
209                self.tuples.len()
210            )
211        })?;
212        let program = self.traversal.program.clone();
213        let mut kernel = PolydatKernel::from_program(program);
214        bind_by_name(&mut kernel, tuple);
215        bind_by_name(&mut kernel, &self.cascade);
216        let cursor = narrow_cursors(&mut kernel)?;
217        Ok(Activation {
218            index: index as u64,
219            coords: tuple.clone(),
220            kernel,
221            cursor,
222        })
223    }
224
225    /// [`Self::activation_on`] on [`Engine::default`](crate::Engine::default):
226    /// the activation at `index` as a compiled kernel, with the JIT where
227    /// the build has it.
228    pub fn activate(&self, index: usize) -> Result<Activation<Box<dyn Kernel>>, String> {
229        self.activation_on(index, crate::Engine::default())
230    }
231
232    /// [`Self::activation`] on `engine` (engine parity, step 8): a fresh
233    /// kernel over the body's program for that engine, compiled once
234    /// per engine and shared by every activation after, driven through
235    /// the [`Kernel`] trait with the same elements, cascade, and cursor
236    /// narrowing. An engine that cannot run the body says so by name.
237    pub fn activation_on(
238        &self,
239        index: usize,
240        engine: crate::Engine,
241    ) -> Result<Activation<Box<dyn Kernel>>, String> {
242        let tuple = self.tuples.get(index).ok_or_else(|| {
243            format!(
244                "activation index {index} is out of range; traversal has {} tuples",
245                self.tuples.len()
246            )
247        })?;
248        let program = self
249            .traversal
250            .program_on(engine)
251            .map_err(|e| e.to_string())?;
252        let mut kernel = program.create_kernel();
253        bind_by_name_on(kernel.as_mut(), tuple)?;
254        bind_by_name_on(kernel.as_mut(), &self.cascade)?;
255        let cursor = narrow_cursors_on(kernel.as_mut())?;
256        Ok(Activation {
257            index: index as u64,
258            coords: tuple.clone(),
259            kernel,
260            cursor,
261        })
262    }
263}
264
265/// Bind the inputs the body declares among `values`, through the trait.
266fn bind_by_name_on(kernel: &mut dyn Kernel, values: &[(String, Value)]) -> Result<(), String> {
267    let declared: std::collections::HashSet<String> = kernel.input_names().into_iter().collect();
268    for (name, value) in values {
269        if declared.contains(name) {
270            kernel.set_input(name, value.clone())?;
271        }
272    }
273    Ok(())
274}
275
276fn bind_by_name(kernel: &mut PolydatKernel, values: &[(String, Value)]) {
277    for (name, value) in values {
278        if let Some(idx) = kernel.program().find_input(name) {
279            kernel.state().set_input(idx, value.clone());
280        }
281    }
282}
283
284/// Resolve every `over` clause in the body and narrow its cursor. Returns
285/// the narrowest slice, or the full extent of the first cursor when none
286/// has an `over` clause. One routine for every engine, through the
287/// trait.
288fn narrow_cursors(kernel: &mut PolydatKernel) -> Result<Option<CursorSlice>, String> {
289    narrow_cursors_on(kernel)
290}
291
292fn narrow_cursors_on(kernel: &mut dyn Kernel) -> Result<Option<CursorSlice>, String> {
293    let schemas: Vec<crate::iteration::source::SourceSchema> = kernel.cursor_schemas().to_vec();
294    let mut narrowest: Option<CursorSlice> = None;
295    for schema in &schemas {
296        let slice = if schema.partition_output.is_some() {
297            let parts = cursor_over_partitions_on(kernel, schema)?;
298            let partition = match parts.len() {
299                1 => parts[0],
300                0 => {
301                    return Err(format!(
302                        "cursor '{}': its `over` value resolved to no partitions",
303                        schema.name
304                    ));
305                }
306                n => {
307                    return Err(format!(
308                        "cursor '{}': its `over` value resolved to {n} partitions; inside a traversal, bind the list \
309                     with an enclosing `for p in ...` and declare the cursor `over p`",
310                        schema.name
311                    ));
312                }
313            };
314            kernel.set_cursor(&schema.name, &partition)?;
315            CursorSlice {
316                cursor: schema.name.clone(),
317                start: partition.start_ord,
318                end: partition.end_ord,
319            }
320        } else {
321            let extent = cursor_extent_on(kernel, schema);
322            CursorSlice {
323                cursor: schema.name.clone(),
324                start: 0,
325                end: extent,
326            }
327        };
328        narrowest = Some(match narrowest {
329            Some(prev) if prev.len() <= slice.len() => prev,
330            _ => slice,
331        });
332    }
333    Ok(narrowest)
334}
335
336impl PolydatKernel {
337    /// A fresh kernel over a shared, already compiled program: the host
338    /// side of one program, many states.
339    pub fn over(program: Arc<PolydatProgram>) -> Self {
340        PolydatKernel::from_program(program)
341    }
342
343    /// Open the traversal at `index` among this program's top-level
344    /// `for` statements, evaluated against this kernel's current values.
345    ///
346    /// Comprehension sources that reference this kernel's wires see the
347    /// values currently set on it. Cascade externs are snapshotted from
348    /// this kernel now and bound into every activation.
349    pub fn traverse(&mut self, index: usize) -> Result<TraversalStream, String> {
350        let program = self.program().clone();
351        let traversal = program.traversals().get(index).cloned().ok_or_else(|| {
352            format!(
353                "no traversal at index {index}; the program declares {}",
354                program.traversals().len()
355            )
356        })?;
357        open_traversal(self, traversal)
358    }
359}
360
361/// Identity of the program an activation runs over, for callers that
362/// want to assert the one-program-per-position property.
363pub fn program_identity(kernel: &PolydatKernel) -> *const PolydatProgram {
364    Arc::as_ptr(kernel.program())
365}
366
367/// Open `traversal` against `parent`'s current values, on any engine
368/// (engine parity, step 8): the cascaded wires are snapshotted through
369/// the [`Kernel`] trait, and the comprehension is evaluated in the
370/// body's scope, the body's program with those wires bound, where a
371/// source or predicate resolves every name it can reference and a
372/// tuple's own elements are layered in front as it is built. Nothing
373/// here needs the opening kernel beyond the snapshot.
374pub fn open_traversal(
375    parent: &mut dyn Kernel,
376    traversal: Traversal,
377) -> Result<TraversalStream, String> {
378    let mut cascade = Vec::with_capacity(traversal.cascade.len());
379    for (name, _) in &traversal.cascade {
380        let value = if parent.output_type(name).is_some() {
381            parent.pull(name)
382        } else {
383            parent.input_value(name).unwrap_or(Value::None)
384        };
385        cascade.push((name.clone(), value));
386    }
387    let mut canonical = PolydatKernel::from_program(traversal.program.clone());
388    bind_by_name(&mut canonical, &cascade);
389    let params: HashMap<String, String> = HashMap::new();
390    let tuples = evaluate_for_iteration(&traversal.comprehension, &canonical, &params, |_| Ok(()))
391        .map_err(|e| {
392            format!(
393                "`for {}` at line {}, col {}: {e}",
394                traversal.source_text, traversal.span.line, traversal.span.col
395            )
396        })?;
397    Ok(TraversalStream {
398        traversal,
399        tuples,
400        cascade,
401        next: 0,
402    })
403}