Skip to main content

polydat_core/kernel/
api.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! The kernel API: one surface for every engine.
5//!
6//! A host holds a kernel as `Box<dyn Kernel>` whatever engine built
7//! it, and drives it through [`Kernel`]: the coordinate and extern
8//! writes that invalidate their dependents, `pull` for one output and `eval` for
9//! every one, the names and types of its inputs and outputs, the
10//! traversals its program declares, its cells, and `into_program`, the
11//! program shared across threads that [`KernelProgram::create_kernel`]
12//! makes a kernel of per thread. Every engine that accepts a program
13//! computes what the interpreter computes, and every call means the
14//! same thing on every engine: the one write rule at `set_input`, one
15//! `invalidate_all`, outputs in declaration order, and a created kernel
16//! starting from the program's defaults.
17//!
18//! [`PolydatKernel`] is the interpreter's kernel as a concrete type:
19//! a program (immutable, shared through `Arc<PolydatProgram>`) and
20//! one evaluation state. It implements [`Kernel`] and keeps three
21//! traits of its own, for the interpreter alone:
22//!
23//! - [`Dataflow`], the healing write: `set_wire` runs the boundary
24//!   adapter catalog before a typed rejection, where `Kernel::set_input`
25//!   refuses a value of another type outright.
26//! - [`Metadata`], structural queries the program answers directly.
27//! - [`Construction`], the subcontext protocol: a root from source
28//!   matter, a subscope built against this kernel with new matter.
29//!
30//! The construction-time hooks the compile path and the program
31//! sharing use (attaching traversals, resolving cursor extents,
32//! nesting) live on a sealed supertrait a host neither sees nor
33//! implements.
34//!
35//! [`PolydatKernel`]: super::PolydatKernel
36
37use crate::ast::{PortType, Value};
38use crate::kernel::{SharedCell, SharedCellEntry};
39
40/// Error returned by [`Dataflow::set_wire_idx`] /
41/// [`Dataflow::set_wire`] when the typed-write contract at the
42/// composition-substrate boundary cannot be satisfied.
43///
44/// Per composition_substrate.md axiom S4, "T1 + T2 ensure
45/// writes are type-checked at the boundary" — the typed-write
46/// API rejects writes whose Value variant doesn't match the
47/// declared slot port type, after first attempting auto-adapter
48/// healing. This error names the rejection reason.
49#[derive(Debug, Clone, PartialEq)]
50pub enum WriteError {
51    /// The wire key did not resolve to a known input slot.
52    /// Carries the name that was looked up; for indexed writes
53    /// the index is reported instead.
54    UnknownWire {
55        /// The name or index looked up.
56        key: String,
57        /// The kernel's input slots, as
58        /// [`Kernel::input_names`] reports them and in the same order,
59        /// coordinates included. Empty only where the writer does not
60        /// have the list.
61        ///
62        /// Coordinates are in it although writing one by name is
63        /// [`Self::CoordinateSlot`] rather than a success: a caller who
64        /// mistyped a coordinate meant a name this kernel has, and is
65        /// not helped by a list that leaves it out. The exact match is
66        /// what the other variant is for. Every engine answers alike,
67        /// and a host can check the list against `input_names`.
68        known: Vec<String>,
69    },
70
71    /// The value's port type did not match the slot's declared
72    /// port type and no auto-adapter exists to heal the
73    /// mismatch. Both expected and provided port types are
74    /// reported for diagnostic clarity.
75    TypeMismatch {
76        /// The slot written.
77        slot: String,
78        /// Its declared type.
79        expected: PortType,
80        /// The value's type.
81        got: PortType,
82    },
83
84    /// The slot is a coordinate, which advances through
85    /// `set_inputs` rather than being written by name or index.
86    /// Writing one here would put the coordinate prefix out of
87    /// step with the values a pull is about to read.
88    CoordinateSlot {
89        /// The coordinate named.
90        slot: String,
91    },
92}
93
94impl std::fmt::Display for WriteError {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            WriteError::UnknownWire { key, known } => {
98                write!(f, "unknown wire '{key}': no input slot by this name")?;
99                if !known.is_empty() {
100                    write!(f, "; this kernel's are {known:?}")?;
101                }
102                Ok(())
103            }
104            WriteError::CoordinateSlot { slot } => {
105                write!(
106                    f,
107                    "'{slot}' is a coordinate: advance it with set_inputs, not by name"
108                )
109            }
110            WriteError::TypeMismatch {
111                slot,
112                expected,
113                got,
114            } => {
115                write!(
116                    f,
117                    "type mismatch writing to slot '{slot}': expected {expected:?}, got {got:?} (no auto-adapter available)"
118                )?;
119                // Vec → scalar is intentionally excluded from the
120                // polyfill matrix (type_system.md §3) — there is
121                // no single natural collection-to-scalar
122                // convention. Point the author at the explicit
123                // helpers rather than leaving them to guess.
124                if matches!(got, PortType::VecF32 | PortType::VecI32)
125                    && !matches!(
126                        expected,
127                        PortType::VecF32
128                            | PortType::VecI32
129                            | PortType::Str
130                            | PortType::Bytes
131                            | PortType::Json
132                    )
133                {
134                    write!(
135                        f,
136                        " — collection → scalar requires an explicit \
137                         reduction node in the program (the library \
138                         provides none; `vec_dot` and `vec_norm` are \
139                         the vector reductions that exist)"
140                    )?;
141                }
142                Ok(())
143            }
144        }
145    }
146}
147
148impl std::error::Error for WriteError {}
149
150/// A wire reference — either a pre-resolved index (fast path)
151/// or a name (resolved against the context's input map).
152///
153/// Lets `set_wire` / `get_wire` accept either form so callers
154/// can hold an index when they have one and a name when they
155/// don't, without needing two distinct method names.
156///
157/// Sealed: only the in-crate impls (`usize`, `&str`, `String`)
158/// are valid wire keys. External implementors are not
159/// permitted because the resolution semantics are tied to the
160/// context's input layout.
161pub trait WireKey: sealed::Sealed {
162    /// Resolve to a wire index in `metadata`. Returns `None`
163    /// when the key doesn't match a wire on this context.
164    fn resolve<M: Metadata + ?Sized>(self, metadata: &M) -> Option<usize>;
165
166    /// Diagnostic rendering of this key — used by
167    /// [`Dataflow::set_wire`] when constructing
168    /// [`WriteError::UnknownWire`] so the error names what the
169    /// caller passed.
170    fn describe(&self) -> String;
171}
172
173mod sealed {
174    pub trait Sealed {}
175    impl Sealed for usize {}
176    impl Sealed for &str {}
177    impl Sealed for String {}
178    impl Sealed for &String {}
179}
180
181impl WireKey for usize {
182    #[inline]
183    fn resolve<M: Metadata + ?Sized>(self, _: &M) -> Option<usize> {
184        Some(self)
185    }
186    #[inline]
187    fn describe(&self) -> String {
188        format!("wire[{self}]")
189    }
190}
191
192impl WireKey for &str {
193    #[inline]
194    fn resolve<M: Metadata + ?Sized>(self, metadata: &M) -> Option<usize> {
195        metadata.find_input(self)
196    }
197    #[inline]
198    fn describe(&self) -> String {
199        (*self).to_string()
200    }
201}
202
203impl WireKey for String {
204    #[inline]
205    fn resolve<M: Metadata + ?Sized>(self, metadata: &M) -> Option<usize> {
206        metadata.find_input(&self)
207    }
208    #[inline]
209    fn describe(&self) -> String {
210        self.clone()
211    }
212}
213
214impl WireKey for &String {
215    #[inline]
216    fn resolve<M: Metadata + ?Sized>(self, metadata: &M) -> Option<usize> {
217        metadata.find_input(self)
218    }
219    #[inline]
220    fn describe(&self) -> String {
221        (*self).clone()
222    }
223}
224
225/// Read-only metadata about the interpreter's kernel: structural
226/// shape, types, names, scope layering. Everything that's a property
227/// of the compiled program (or fiber-state instance) but isn't itself
228/// a runtime value. Interpreter-only: [`Kernel`] carries the names and
229/// types every engine reports.
230pub trait Metadata {
231    /// Resolve an input name to its wire index, if present.
232    fn find_input(&self, name: &str) -> Option<usize>;
233
234    /// All declared input wire names, in declaration order.
235    fn input_names(&self) -> Vec<String>;
236
237    /// All declared output wire names, in declaration order.
238    fn output_names(&self) -> Vec<String>;
239
240    /// Number of coordinate inputs (the leading prefix of the
241    /// input slot vector — written via the cycle dispatcher).
242    fn coord_count(&self) -> usize;
243
244    /// Declared port type of an input wire, if known.
245    fn input_port_type(&self, name: &str) -> Option<PortType>;
246
247    /// Declared port type of an input wire by index. The
248    /// indexed counterpart of [`input_port_type`](Self::input_port_type) — used by
249    /// the typed-write fast path so [`Dataflow::set_wire_idx`]
250    /// can look up the slot's expected type without first
251    /// reverse-resolving an index to a name.
252    fn input_port_type_by_idx(&self, idx: usize) -> Option<PortType>;
253
254    /// Declared port type of an output wire, if present.
255    /// Symmetric counterpart to [`input_port_type`](Self::input_port_type). Used by
256    /// the binder verification path
257    /// (`crate::binder::verify_against_kernel`) to look up wire
258    /// types for type-checking adapter binding shapes.
259    fn output_port_type(&self, name: &str) -> Option<PortType>;
260}
261
262/// The interpreter kernel's healing write and raw read: write inputs,
263/// read wires.
264///
265/// Four core methods. The indexed pair is the fast path; the named
266/// pair resolves against the context's metadata then delegates to the
267/// indexed pair. A write runs the boundary adapter catalog before a
268/// typed rejection, where [`Kernel::set_input`] refuses a value of
269/// another type outright. Interpreter-only.
270pub trait Dataflow: Metadata {
271    /// Write a value to wire `idx` with typed enforcement.
272    ///
273    /// Per composition_substrate.md axiom S4, the typed-write
274    /// boundary enforces T1 + T2: the value's port type must
275    /// match the slot's declared port type, with auto-adapter
276    /// healing where the implementation supports it. Mismatches
277    /// the boundary cannot heal return [`WriteError::TypeMismatch`].
278    /// An out-of-range index returns
279    /// [`WriteError::UnknownWire`].
280    fn set_wire_idx(&mut self, idx: usize, value: Value) -> Result<(), WriteError>;
281
282    /// Read the current value of wire `idx`. Out-of-range
283    /// behaviour returns the slot's default `Value::None` (the
284    /// read path is non-fallible; type information is structural
285    /// and reads cannot fail typewise).
286    fn get_wire_idx(&self, idx: usize) -> Value;
287
288    /// Write a value to a wire identified by `key` (index or
289    /// name). Returns `Ok(())` on success, `Err(WriteError)` on
290    /// failure (unknown wire or type mismatch the boundary
291    /// cannot heal).
292    #[inline]
293    fn set_wire<W: WireKey>(&mut self, key: W, value: Value) -> Result<(), WriteError> {
294        // Capture a string form of the key for diagnostic
295        // reporting before resolution consumes it. The
296        // WireKey::describe method provides this; the default
297        // impl renders index keys as "wire[N]" and name keys
298        // as the name itself.
299        let key_desc = key.describe();
300        match key.resolve(self) {
301            Some(idx) => self.set_wire_idx(idx, value),
302            None => Err(WriteError::UnknownWire {
303                key: key_desc,
304                known: Vec::new(),
305            }),
306        }
307    }
308
309    /// Read the current value of a wire identified by `key`
310    /// (index or name). Returns `None` when the wire is not
311    /// found.
312    #[inline]
313    fn get_wire<W: WireKey>(&self, key: W) -> Option<Value> {
314        key.resolve(self).map(|idx| self.get_wire_idx(idx))
315    }
316}
317
318/// Construction interface — the two sanctioned construction
319/// paths. Per the kernel-construction invariant:
320///
321/// 1. **Root** — built from Polydat matter, no parent.
322/// 2. **Subscope** — built from Polydat matter against an existing
323///    context.
324///
325/// Both paths take the same typed Polydat matter
326/// ([`super::subcontext::PolydatMatter`]). The only
327/// difference is whether a parent context supervises
328/// construction. Nothing else is allowed.
329pub trait Construction: Sized {
330    /// Construction error type.
331    type Error;
332
333    /// Path 1: build a root context from Polydat matter. No parent.
334    /// Subscope-only fields on the matter (result-binding
335    /// rewrites, inherited-output cascade, finalize-time
336    /// contract checks) are not applicable here and are
337    /// ignored.
338    fn root(matter: super::subcontext::PolydatMatter<'_>) -> Result<Self, Self::Error>;
339
340    /// Path 2: build a subscope context against `self` from
341    /// Polydat matter. The parent supervises: cell cascade, Rule 2
342    /// rewrites, scope-coordinate threading, init-binding
343    /// contract checks all flow from `self` into the child.
344    fn subscope(&self, matter: super::subcontext::PolydatMatter<'_>) -> Result<Self, Self::Error>;
345}
346
347// ── One kernel API for every engine (engines.md §3.5) ──────
348
349/// A kernel on any engine: the interpreter, the closure tier, the
350/// hybrid kernel, or pure native code. Every engine accepts every
351/// program the interpreter accepts, or refuses it at construction
352/// with a reason, and computes the same values for the same inputs;
353/// the choice of engine changes how fast a program runs and nothing
354/// else. This trait is the surface a host drives an engine through
355/// without knowing which one it has.
356///
357/// The interpreter kernel and the compiled kernels also keep their
358/// inherent methods (raw slot readers, `eval(&[u64])`, `engine_counts`)
359/// as engine-specific extras; where a name is shared, the inherent
360/// method is the one a call on the concrete type reaches, and the
361/// trait's is reached through `dyn Kernel` or `Kernel::pull(&mut k, …)`.
362pub trait Kernel: Send + internals::KernelInternals {
363    /// The engine this kernel runs on.
364    fn engine(&self) -> crate::compile::select::Engine;
365
366    /// Set the coordinate inputs for the next evaluation.
367    fn set_inputs(&mut self, coords: &[u64]);
368
369    /// Set an extern by name. One rule on every engine: the value must
370    /// satisfy the declared port type (a carrier's bit-stuffed forms
371    /// included) or be `None`, which clears the extern; a value of
372    /// another type is refused at the write, never healed. A coordinate
373    /// is set with [`Self::set_inputs`], not here. An unknown name is
374    /// an error naming the known ones.
375    fn set_input(&mut self, name: &str, value: Value) -> Result<(), WriteError>;
376
377    /// Narrow a cursor to one partition: its `Ext` slot and its six
378    /// scalar projections are set.
379    fn set_cursor(
380        &mut self,
381        name: &str,
382        partition: &crate::iteration::cursor_partition::Partition,
383    ) -> Result<(), crate::kernel::WriteError>;
384
385    /// Evaluate every output for the inputs set so far.
386    fn eval(&mut self);
387
388    /// The named output for the inputs set so far, evaluating what it
389    /// needs and no more: the output's cone, on the interpreter, the
390    /// closure tier, and the hybrid kernel alike (pure native code,
391    /// being one function, evaluates the program). A side channel in
392    /// the cone fires when the output is pulled; a failing node fails
393    /// when pulled, with the same attributed message on every engine:
394    /// the node's name, the outputs it feeds, the program's context,
395    /// and its inputs. The value is owned; a handle is never returned to
396    /// the host, and a slot that holds `None` reads as `None`.
397    fn pull(&mut self, name: &str) -> Value;
398
399    /// Every input by name, the coordinates first.
400    fn input_names(&self) -> Vec<String>;
401
402    /// Every named output.
403    fn output_names(&self) -> Vec<String>;
404
405    /// The declared port type of a named output.
406    fn output_type(&self, name: &str) -> Option<PortType>;
407
408    /// The externs by name and declared type.
409    fn externs(&self) -> Vec<(String, PortType)>;
410
411    /// The cursors the program declares, with the partitions the
412    /// compiler resolved where it could.
413    fn cursor_schemas(&self) -> &[crate::iteration::source::SourceSchema];
414
415    /// What this kernel's engine decided for the program: how much of
416    /// it runs as native segments, as closure steps, and on the
417    /// interpreter. The one planning detail a kernel exposes.
418    fn plan(&self) -> crate::EnginePlan;
419
420    /// The value of a named input as the kernel holds it now, an extern
421    /// or a coordinate; `None` for a name that is not an input.
422    fn input_value(&self, name: &str) -> Option<Value>;
423
424    /// The index of a named input among [`Self::input_names`], the
425    /// coordinates first: what [`Self::set_input_at`] takes.
426    fn input_index(&self, name: &str) -> Option<usize> {
427        self.input_names().iter().position(|n| n == name)
428    }
429
430    /// [`Self::set_input`] by index, for a host that binds the same
431    /// inputs every cycle: the name is resolved once, with
432    /// [`Self::input_index`], and no lookup runs per write.
433    fn set_input_at(&mut self, index: usize, value: Value) -> Result<(), WriteError> {
434        let name =
435            self.input_names()
436                .get(index)
437                .cloned()
438                .ok_or_else(|| WriteError::UnknownWire {
439                    key: format!("wire[{index}]"),
440                    known: self.input_names(),
441                })?;
442        self.set_input(&name, value)
443    }
444
445    /// The index of a named output among [`Self::output_names`]: what
446    /// [`Self::pull_at`] takes.
447    fn output_index(&self, name: &str) -> Option<usize> {
448        self.output_names().iter().position(|n| n == name)
449    }
450
451    /// [`Self::pull`] by index, for a host that reads the same outputs
452    /// every cycle: the name is resolved once, with
453    /// [`Self::output_index`], and no lookup runs per pull.
454    fn pull_at(&mut self, index: usize) -> Value {
455        let name = self
456            .output_names()
457            .get(index)
458            .cloned()
459            .unwrap_or_else(|| panic!("no output at index {index}"));
460        self.pull(&name)
461    }
462
463    /// The traversals the program declares, in document order.
464    fn traversals(&self) -> &[crate::dsl::traversal::Traversal];
465
466    /// Open the traversal at `index` against this kernel's current
467    /// values (SRD 113 §3.6): the comprehension's sources see the wires
468    /// they reference as this kernel holds them now, and the cascaded
469    /// wires are snapshotted into every activation. On every engine
470    /// (engine parity, step 8).
471    fn traverse(&mut self, index: usize) -> Result<crate::kernel::TraversalStream, String>;
472
473    /// Open every traversal, in document order.
474    fn traverse_all(&mut self) -> Result<Vec<crate::kernel::TraversalStream>, String> {
475        (0..self.traversals().len())
476            .map(|i| self.traverse(i))
477            .collect()
478    }
479
480    /// Begin the next cycle with nothing current, so every step, a side
481    /// channel included, runs again when pulled. The runtime model makes
482    /// a cycle whose inputs did not move cost nothing; this is how a
483    /// host runs such a cycle anyway, as the `polydat` binary does when
484    /// every input is fixed.
485    fn invalidate_all(&mut self);
486
487    /// The cells this kernel's `shared` bindings are bound to (scope
488    /// model §6): one register per binding, which every kernel holding
489    /// the cell reads and writes.
490    fn shared_cells(&self) -> Vec<SharedCellEntry>;
491
492    /// The broadcast cell for a *computed* output, created on the first
493    /// ask: a descendant that binds its matching input slot to this
494    /// cell reads the value each of this kernel's pulls publishes
495    /// through it, rather than a copy taken once when the descendant
496    /// was built (cross_fiber_invalidation.md §3.1).
497    ///
498    /// `None` when the name is not an output of this kernel, and on an
499    /// engine that has no broadcast cells at all. The interpreter seeds
500    /// one per output at construction; the closure tier and the hybrid
501    /// make them on demand, so a program with no descendant bound to it
502    /// allocates none.
503    fn output_cell(&self, _name: &str) -> Option<SharedCell> {
504        None
505    }
506
507    /// The binding modifier a named output was declared with — `const`,
508    /// `shared`, `final`, or none. A binder reads it to decide how a
509    /// descendant takes the output: a `const` is effectively fixed for
510    /// the scope's life and is value-copied, where a computed output is
511    /// bound to its broadcast cell (scope_model.md §4).
512    ///
513    /// `NONE` for a name this kernel does not declare.
514    fn output_modifier(&self, _name: &str) -> crate::dsl::ast::BindingModifier {
515        crate::dsl::ast::BindingModifier::NONE
516    }
517
518    /// Every cell a descendant of this kernel could bind to: the ones
519    /// its own `shared` slots hold, plus the ones it carries forward
520    /// for a descendant without holding a slot for them itself. The
521    /// second kind is why an ancestral `shared` reaches a grandchild
522    /// whose parent's program never names it.
523    ///
524    /// [`Self::shared_cells`] is the first kind alone.
525    fn cells_in_scope(&self) -> Vec<SharedCellEntry> {
526        self.shared_cells()
527    }
528
529    /// Carry `cells` forward for this kernel's descendants. The binder
530    /// writes what the parent had and this kernel holds no slot for.
531    fn set_transit_cells(&mut self, _cells: Vec<SharedCellEntry>) {}
532
533    /// This kernel's place in the comprehension nest its scope was
534    /// built under, outermost last: a child's path is its own followed
535    /// by its parent's. Empty for a root, which is every kernel a host
536    /// compiles rather than binds, so the compiled engines answer
537    /// empty until one is bound under a parent.
538    fn scope_coordinates(&self) -> &[super::ScopeCoord] {
539        &[]
540    }
541
542    /// Append `outer` to this kernel's own scope-coordinate path, which
543    /// the binder does once the child's inputs are in. A no-op on an
544    /// engine that keeps no path.
545    fn extend_scope_coordinates(&mut self, _outer: &[super::ScopeCoord]) {}
546
547    /// The declared type of a named input slot, coordinates included.
548    /// The binder reads it to adapt a value the parent supplies into
549    /// the type the child's slot declares.
550    fn input_port_type(&self, _name: &str) -> Option<PortType> {
551        None
552    }
553
554    /// Bind the named input slot to `cell`, whether or not the slot was
555    /// built as a `shared` register, and answer whether it was bound.
556    ///
557    /// This is what a parent does to a child, not what a host does to
558    /// two kernels. A child declares its imports `extern`; it is the
559    /// parent binding it that decides one of them reads a register
560    /// rather than a copied value. [`Self::attach_shared_cell`] is the
561    /// host's operation and refuses a slot that is not already a
562    /// register on both sides, which is the right answer for joining
563    /// two kernels and the wrong one for building a child.
564    fn bind_input_cell(&mut self, _name: &str, _cell: SharedCell) -> bool {
565        false
566    }
567
568    /// Bind the `shared` binding `name` to `cell`, so this kernel and
569    /// every other holder of the cell read and write one register:
570    /// a write on any of them is what the others read next, and a
571    /// dependent output is recomputed. A name that is not a `shared`
572    /// binding is an error naming the ones that are.
573    fn attach_shared_cell(&mut self, name: &str, cell: SharedCell) -> Result<(), String>;
574
575    /// The program this kernel runs, shareable across threads: each
576    /// thread creates its own kernel from it with
577    /// [`KernelProgram::create_kernel`].
578    ///
579    /// **What this kernel was set to does not travel with it.** A
580    /// kernel created from the program starts at the program: every
581    /// extern at its declared default and every `shared` binding with
582    /// a cell of its own, whatever this kernel had been written to
583    /// before it became one. That holds on every engine.
584    ///
585    /// The reason is that an extern is per-kernel state, in the same
586    /// family as the coordinates: both are writes into declared slots
587    /// of a running kernel, and neither is part of the compiled
588    /// program. A host that wants a value fixed *for the program*
589    /// fixes it before compiling, with
590    /// [`transform::assign_values`](crate::dsl::transform::assign_values)
591    /// or an `extern` default in the source; a host that wants every
592    /// thread to see one register attaches a cell with
593    /// [`Self::attach_shared_cell`].
594    fn into_program(self: Box<Self>) -> std::sync::Arc<dyn KernelProgram>;
595
596    /// The compile ledger of the program tree this kernel belongs to:
597    /// what compiling it and everything opened from it has built.
598    fn ledger(&self) -> &std::sync::Arc<crate::kernel::CompileLedger>;
599}
600
601/// The construction-time hooks of a kernel, sealed: the compile path
602/// and the program sharing call them once, before a kernel is shared,
603/// and a host neither sees nor implements them.
604pub(crate) mod internals {
605    use crate::ast::{PortType, Value};
606
607    pub trait KernelInternals {
608        /// Attach the traversals the program declares and the producer
609        /// bindings they may traverse.
610        fn set_traversals(
611            &mut self,
612            traversals: Vec<crate::dsl::traversal::Traversal>,
613            producers: Vec<crate::dsl::traversal::Producer>,
614        );
615
616        /// The value at a buffer slot decoded as `ty`, for the compile
617        /// log's record of the constants folded at build; `None` on the
618        /// interpreter, whose program logs its own fold.
619        fn slot_value(&self, _slot: usize, _ty: PortType) -> Value {
620            Value::None
621        }
622
623        /// The value the build folded for output `name`, if it folded
624        /// one: what the compile path reads to resolve a cursor extent
625        /// computed from constants, on every engine.
626        fn folded_value(&self, name: &str) -> Option<Value>;
627
628        /// Record the extent of cursor `index` once the compile path
629        /// has resolved it from the folded constants.
630        fn set_cursor_extent(&mut self, index: usize, extent: u64);
631
632        /// Start over from the program: every input at its declared
633        /// default, every `shared` binding with a cell of its own,
634        /// nothing current. What a kernel created from a shared program
635        /// starts with; the interpreter's is built that way and needs
636        /// nothing.
637        fn reset_to_program(&mut self) {}
638    }
639}
640
641/// A program on some engine, shared across threads through an `Arc`;
642/// every kernel created from it computes the same values and owns its
643/// own inputs, buffers, and outputs.
644pub trait KernelProgram: Send + Sync {
645    /// The engine the program was built for.
646    fn engine(&self) -> crate::compile::select::Engine;
647
648    /// A kernel of this program for the calling thread. It starts from
649    /// the program on every engine: every input at its declared
650    /// default, whatever the kernel that became the program had been
651    /// set to; every `shared` binding with a cell of its own.
652    fn create_kernel(self: std::sync::Arc<Self>) -> Box<dyn Kernel>;
653
654    /// The interpreter's program, when this is one: the graph a
655    /// diagnostic describes node by node. `None` for a compiled
656    /// engine's program.
657    fn as_interpreter(
658        self: std::sync::Arc<Self>,
659    ) -> Option<std::sync::Arc<crate::kernel::PolydatProgram>> {
660        None
661    }
662
663    /// The compile ledger of the program tree this program belongs to.
664    fn ledger(&self) -> &std::sync::Arc<crate::kernel::CompileLedger>;
665}
666
667/// A compiled kernel as a shared program: its steps are shared, and a
668/// created kernel is a clone that owns its own buffer, table, scratch,
669/// and externs.
670pub(crate) struct SharedKernel<K>(pub(crate) K);
671
672impl<K: Kernel + Clone + Send + Sync + 'static> KernelProgram for SharedKernel<K> {
673    fn engine(&self) -> crate::compile::select::Engine {
674        self.0.engine()
675    }
676    fn create_kernel(self: std::sync::Arc<Self>) -> Box<dyn Kernel> {
677        let mut kernel = self.0.clone();
678        // A created kernel starts from the program, as an interpreter
679        // state created from one does: inputs at their defaults, cells
680        // of its own; a host sets what it wants and attaches what it
681        // shares.
682        kernel.reset_to_program();
683        Box::new(kernel)
684    }
685    fn ledger(&self) -> &std::sync::Arc<crate::kernel::CompileLedger> {
686        self.0.ledger()
687    }
688}