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    },
58
59    /// The value's port type did not match the slot's declared
60    /// port type and no auto-adapter exists to heal the
61    /// mismatch. Both expected and provided port types are
62    /// reported for diagnostic clarity.
63    TypeMismatch {
64        /// The slot written.
65        slot: String,
66        /// Its declared type.
67        expected: PortType,
68        /// The value's type.
69        got: PortType,
70    },
71}
72
73impl std::fmt::Display for WriteError {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        match self {
76            WriteError::UnknownWire { key } => {
77                write!(f, "unknown wire '{key}': no input slot by this name")
78            }
79            WriteError::TypeMismatch {
80                slot,
81                expected,
82                got,
83            } => {
84                write!(
85                    f,
86                    "type mismatch writing to slot '{slot}': expected {expected:?}, got {got:?} (no auto-adapter available)"
87                )?;
88                // Vec → scalar is intentionally excluded from the
89                // polyfill matrix (type_system.md §3) — there is
90                // no single natural collection-to-scalar
91                // convention. Point the author at the explicit
92                // helpers rather than leaving them to guess.
93                if matches!(got, PortType::VecF32 | PortType::VecI32)
94                    && !matches!(
95                        expected,
96                        PortType::VecF32
97                            | PortType::VecI32
98                            | PortType::Str
99                            | PortType::Bytes
100                            | PortType::Json
101                    )
102                {
103                    write!(
104                        f,
105                        " — collection → scalar requires an explicit \
106                         choice; use `vec_len(v)` for the element \
107                         count, `vec_first(v)` / `vec_last(v)` for an \
108                         element, `vec_sum(v)` / `vec_mean(v)` for an \
109                         aggregate"
110                    )?;
111                }
112                Ok(())
113            }
114        }
115    }
116}
117
118impl std::error::Error for WriteError {}
119
120/// A wire reference — either a pre-resolved index (fast path)
121/// or a name (resolved against the context's input map).
122///
123/// Lets `set_wire` / `get_wire` accept either form so callers
124/// can hold an index when they have one and a name when they
125/// don't, without needing two distinct method names.
126///
127/// Sealed: only the in-crate impls (`usize`, `&str`, `String`)
128/// are valid wire keys. External implementors are not
129/// permitted because the resolution semantics are tied to the
130/// context's input layout.
131pub trait WireKey: sealed::Sealed {
132    /// Resolve to a wire index in `metadata`. Returns `None`
133    /// when the key doesn't match a wire on this context.
134    fn resolve<M: Metadata + ?Sized>(self, metadata: &M) -> Option<usize>;
135
136    /// Diagnostic rendering of this key — used by
137    /// [`Dataflow::set_wire`] when constructing
138    /// [`WriteError::UnknownWire`] so the error names what the
139    /// caller passed.
140    fn describe(&self) -> String;
141}
142
143mod sealed {
144    pub trait Sealed {}
145    impl Sealed for usize {}
146    impl Sealed for &str {}
147    impl Sealed for String {}
148    impl Sealed for &String {}
149}
150
151impl WireKey for usize {
152    #[inline]
153    fn resolve<M: Metadata + ?Sized>(self, _: &M) -> Option<usize> {
154        Some(self)
155    }
156    #[inline]
157    fn describe(&self) -> String {
158        format!("wire[{self}]")
159    }
160}
161
162impl WireKey for &str {
163    #[inline]
164    fn resolve<M: Metadata + ?Sized>(self, metadata: &M) -> Option<usize> {
165        metadata.find_input(self)
166    }
167    #[inline]
168    fn describe(&self) -> String {
169        (*self).to_string()
170    }
171}
172
173impl WireKey for String {
174    #[inline]
175    fn resolve<M: Metadata + ?Sized>(self, metadata: &M) -> Option<usize> {
176        metadata.find_input(&self)
177    }
178    #[inline]
179    fn describe(&self) -> String {
180        self.clone()
181    }
182}
183
184impl WireKey for &String {
185    #[inline]
186    fn resolve<M: Metadata + ?Sized>(self, metadata: &M) -> Option<usize> {
187        metadata.find_input(self)
188    }
189    #[inline]
190    fn describe(&self) -> String {
191        (*self).clone()
192    }
193}
194
195/// Read-only metadata about the interpreter's kernel: structural
196/// shape, types, names, scope layering. Everything that's a property
197/// of the compiled program (or fiber-state instance) but isn't itself
198/// a runtime value. Interpreter-only: [`Kernel`] carries the names and
199/// types every engine reports.
200pub trait Metadata {
201    /// Resolve an input name to its wire index, if present.
202    fn find_input(&self, name: &str) -> Option<usize>;
203
204    /// All declared input wire names, in declaration order.
205    fn input_names(&self) -> Vec<String>;
206
207    /// All declared output wire names, in declaration order.
208    fn output_names(&self) -> Vec<String>;
209
210    /// Number of coordinate inputs (the leading prefix of the
211    /// input slot vector — written via the cycle dispatcher).
212    fn coord_count(&self) -> usize;
213
214    /// Declared port type of an input wire, if known.
215    fn input_port_type(&self, name: &str) -> Option<PortType>;
216
217    /// Declared port type of an input wire by index. The
218    /// indexed counterpart of [`input_port_type`](Self::input_port_type) — used by
219    /// the typed-write fast path so [`Dataflow::set_wire_idx`]
220    /// can look up the slot's expected type without first
221    /// reverse-resolving an index to a name.
222    fn input_port_type_by_idx(&self, idx: usize) -> Option<PortType>;
223
224    /// Declared port type of an output wire, if present.
225    /// Symmetric counterpart to [`input_port_type`](Self::input_port_type). Used by
226    /// the binder verification path
227    /// (`crate::binder::verify_against_kernel`) to look up wire
228    /// types for type-checking adapter binding shapes.
229    fn output_port_type(&self, name: &str) -> Option<PortType>;
230}
231
232/// The interpreter kernel's healing write and raw read: write inputs,
233/// read wires.
234///
235/// Four core methods. The indexed pair is the fast path; the named
236/// pair resolves against the context's metadata then delegates to the
237/// indexed pair. A write runs the boundary adapter catalog before a
238/// typed rejection, where [`Kernel::set_input`] refuses a value of
239/// another type outright. Interpreter-only.
240pub trait Dataflow: Metadata {
241    /// Write a value to wire `idx` with typed enforcement.
242    ///
243    /// Per composition_substrate.md axiom S4, the typed-write
244    /// boundary enforces T1 + T2: the value's port type must
245    /// match the slot's declared port type, with auto-adapter
246    /// healing where the implementation supports it. Mismatches
247    /// the boundary cannot heal return [`WriteError::TypeMismatch`].
248    /// An out-of-range index returns
249    /// [`WriteError::UnknownWire`].
250    fn set_wire_idx(&mut self, idx: usize, value: Value) -> Result<(), WriteError>;
251
252    /// Read the current value of wire `idx`. Out-of-range
253    /// behaviour returns the slot's default `Value::None` (the
254    /// read path is non-fallible; type information is structural
255    /// and reads cannot fail typewise).
256    fn get_wire_idx(&self, idx: usize) -> Value;
257
258    /// Write a value to a wire identified by `key` (index or
259    /// name). Returns `Ok(())` on success, `Err(WriteError)` on
260    /// failure (unknown wire or type mismatch the boundary
261    /// cannot heal).
262    #[inline]
263    fn set_wire<W: WireKey>(&mut self, key: W, value: Value) -> Result<(), WriteError> {
264        // Capture a string form of the key for diagnostic
265        // reporting before resolution consumes it. The
266        // WireKey::describe method provides this; the default
267        // impl renders index keys as "wire[N]" and name keys
268        // as the name itself.
269        let key_desc = key.describe();
270        match key.resolve(self) {
271            Some(idx) => self.set_wire_idx(idx, value),
272            None => Err(WriteError::UnknownWire { key: key_desc }),
273        }
274    }
275
276    /// Read the current value of a wire identified by `key`
277    /// (index or name). Returns `None` when the wire is not
278    /// found.
279    #[inline]
280    fn get_wire<W: WireKey>(&self, key: W) -> Option<Value> {
281        key.resolve(self).map(|idx| self.get_wire_idx(idx))
282    }
283}
284
285/// Construction interface — the two sanctioned construction
286/// paths. Per the kernel-construction invariant:
287///
288/// 1. **Root** — built from Polydat matter, no parent.
289/// 2. **Subscope** — built from Polydat matter against an existing
290///    context.
291///
292/// Both paths take the same typed Polydat matter
293/// ([`super::subcontext::PolydatMatter`]). The only
294/// difference is whether a parent context supervises
295/// construction. Nothing else is allowed.
296pub trait Construction: Sized {
297    /// Construction error type.
298    type Error;
299
300    /// Path 1: build a root context from Polydat matter. No parent.
301    /// Subscope-only fields on the matter (result-binding
302    /// rewrites, inherited-output cascade, finalize-time
303    /// contract checks) are not applicable here and are
304    /// ignored.
305    fn root(matter: super::subcontext::PolydatMatter<'_>) -> Result<Self, Self::Error>;
306
307    /// Path 2: build a subscope context against `self` from
308    /// Polydat matter. The parent supervises: cell cascade, Rule 2
309    /// rewrites, scope-coordinate threading, init-binding
310    /// contract checks all flow from `self` into the child.
311    fn subscope(&self, matter: super::subcontext::PolydatMatter<'_>) -> Result<Self, Self::Error>;
312}
313
314// ── One kernel API for every engine (engine_parity.md, step 4) ──────
315
316/// A kernel on any engine: the interpreter, the closure tier, the
317/// hybrid kernel, or pure native code. Every engine accepts every
318/// program the interpreter accepts, or refuses it at construction
319/// with a reason, and computes the same values for the same inputs;
320/// the choice of engine changes how fast a program runs and nothing
321/// else. This trait is the surface a host drives an engine through
322/// without knowing which one it has.
323///
324/// The interpreter kernel and the compiled kernels also keep their
325/// inherent methods (raw slot readers, `eval(&[u64])`, `engine_counts`)
326/// as engine-specific extras; where a name is shared, the inherent
327/// method is the one a call on the concrete type reaches, and the
328/// trait's is reached through `dyn Kernel` or `Kernel::pull(&mut k, …)`.
329pub trait Kernel: Send + internals::KernelInternals {
330    /// The engine this kernel runs on.
331    fn engine(&self) -> crate::compile::select::Engine;
332
333    /// Set the coordinate inputs for the next evaluation.
334    fn set_inputs(&mut self, coords: &[u64]);
335
336    /// Set an extern by name. One rule on every engine: the value must
337    /// satisfy the declared port type (a carrier's bit-stuffed forms
338    /// included) or be `None`, which clears the extern; a value of
339    /// another type is refused at the write, never healed. A coordinate
340    /// is set with [`Self::set_inputs`], not here. An unknown name is
341    /// an error naming the known ones.
342    fn set_input(&mut self, name: &str, value: Value) -> Result<(), String>;
343
344    /// Narrow a cursor to one partition: its `Ext` slot and its six
345    /// scalar projections are set.
346    fn set_cursor(
347        &mut self,
348        name: &str,
349        partition: &crate::iteration::cursor_partition::Partition,
350    ) -> Result<(), String>;
351
352    /// Evaluate every output for the inputs set so far.
353    fn eval(&mut self);
354
355    /// The named output for the inputs set so far, evaluating what it
356    /// needs and no more: the output's cone, on the interpreter, the
357    /// closure tier, and the hybrid kernel alike (pure native code,
358    /// being one function, evaluates the program). A side channel in
359    /// the cone fires when the output is pulled; a failing node fails
360    /// when pulled, with the same attributed message on every engine:
361    /// the node's name, the outputs it feeds, the program's context,
362    /// and its inputs. The value is owned; a handle is never returned to
363    /// the host, and a slot that holds `None` reads as `None`.
364    fn pull(&mut self, name: &str) -> Value;
365
366    /// Every input by name, the coordinates first.
367    fn input_names(&self) -> Vec<String>;
368
369    /// Every named output.
370    fn output_names(&self) -> Vec<String>;
371
372    /// The declared port type of a named output.
373    fn output_type(&self, name: &str) -> Option<PortType>;
374
375    /// The externs by name and declared type.
376    fn externs(&self) -> Vec<(String, PortType)>;
377
378    /// The cursors the program declares, with the partitions the
379    /// compiler resolved where it could.
380    fn cursor_schemas(&self) -> &[crate::iteration::source::SourceSchema];
381
382    /// What this kernel's engine decided for the program: how much of
383    /// it runs as native segments, as closure steps, and on the
384    /// interpreter. The one planning detail a kernel exposes.
385    fn plan(&self) -> crate::EnginePlan;
386
387    /// The value of a named input as the kernel holds it now, an extern
388    /// or a coordinate; `None` for a name that is not an input.
389    fn input_value(&self, name: &str) -> Option<Value>;
390
391    /// The index of a named input among [`Self::input_names`], the
392    /// coordinates first: what [`Self::set_input_at`] takes.
393    fn input_index(&self, name: &str) -> Option<usize> {
394        self.input_names().iter().position(|n| n == name)
395    }
396
397    /// [`Self::set_input`] by index, for a host that binds the same
398    /// inputs every cycle: the name is resolved once, with
399    /// [`Self::input_index`], and no lookup runs per write.
400    fn set_input_at(&mut self, index: usize, value: Value) -> Result<(), String> {
401        let name = self
402            .input_names()
403            .get(index)
404            .cloned()
405            .ok_or_else(|| format!("no input at index {index}"))?;
406        self.set_input(&name, value)
407    }
408
409    /// The index of a named output among [`Self::output_names`]: what
410    /// [`Self::pull_at`] takes.
411    fn output_index(&self, name: &str) -> Option<usize> {
412        self.output_names().iter().position(|n| n == name)
413    }
414
415    /// [`Self::pull`] by index, for a host that reads the same outputs
416    /// every cycle: the name is resolved once, with
417    /// [`Self::output_index`], and no lookup runs per pull.
418    fn pull_at(&mut self, index: usize) -> Value {
419        let name = self
420            .output_names()
421            .get(index)
422            .cloned()
423            .unwrap_or_else(|| panic!("no output at index {index}"));
424        self.pull(&name)
425    }
426
427    /// The traversals the program declares, in document order.
428    fn traversals(&self) -> &[crate::dsl::traversal::Traversal];
429
430    /// Open the traversal at `index` against this kernel's current
431    /// values (SRD 113 §3.6): the comprehension's sources see the wires
432    /// they reference as this kernel holds them now, and the cascaded
433    /// wires are snapshotted into every activation. On every engine
434    /// (engine parity, step 8).
435    fn traverse(&mut self, index: usize) -> Result<crate::kernel::TraversalStream, String>;
436
437    /// Open every traversal, in document order.
438    fn traverse_all(&mut self) -> Result<Vec<crate::kernel::TraversalStream>, String> {
439        (0..self.traversals().len())
440            .map(|i| self.traverse(i))
441            .collect()
442    }
443
444    /// Begin the next cycle with nothing current, so every step, a side
445    /// channel included, runs again when pulled. The runtime model makes
446    /// a cycle whose inputs did not move cost nothing; this is how a
447    /// host runs such a cycle anyway, as the `polydat` binary does when
448    /// every input is fixed.
449    fn invalidate_all(&mut self);
450
451    /// The cells this kernel's `shared` bindings are bound to (scope
452    /// model §6): one register per binding, which every kernel holding
453    /// the cell reads and writes.
454    fn shared_cells(&self) -> Vec<SharedCellEntry>;
455
456    /// Bind the `shared` binding `name` to `cell`, so this kernel and
457    /// every other holder of the cell read and write one register:
458    /// a write on any of them is what the others read next, and a
459    /// dependent output is recomputed. A name that is not a `shared`
460    /// binding is an error naming the ones that are.
461    fn attach_shared_cell(&mut self, name: &str, cell: SharedCell) -> Result<(), String>;
462
463    /// The program this kernel runs, shareable across threads: each
464    /// thread creates its own kernel from it with
465    /// [`KernelProgram::create_kernel`].
466    fn into_program(self: Box<Self>) -> std::sync::Arc<dyn KernelProgram>;
467}
468
469/// The construction-time hooks of a kernel, sealed: the compile path
470/// and the program sharing call them once, before a kernel is shared,
471/// and a host neither sees nor implements them.
472pub(crate) mod internals {
473    use crate::ast::{PortType, Value};
474
475    pub trait KernelInternals {
476        /// Attach the traversals the program declares and the producer
477        /// bindings they may traverse.
478        fn set_traversals(
479            &mut self,
480            traversals: Vec<crate::dsl::traversal::Traversal>,
481            producers: Vec<crate::dsl::traversal::Producer>,
482        );
483
484        /// The value at a buffer slot decoded as `ty`, for the compile
485        /// log's record of the constants folded at build; `None` on the
486        /// interpreter, whose program logs its own fold.
487        fn slot_value(&self, _slot: usize, _ty: PortType) -> Value {
488            Value::None
489        }
490
491        /// The value the build folded for output `name`, if it folded
492        /// one: what the compile path reads to resolve a cursor extent
493        /// computed from constants, on every engine.
494        fn folded_value(&self, name: &str) -> Option<Value>;
495
496        /// Record the extent of cursor `index` once the compile path
497        /// has resolved it from the folded constants.
498        fn set_cursor_extent(&mut self, index: usize, extent: u64);
499
500        /// Start over from the program: every input at its declared
501        /// default, every `shared` binding with a cell of its own,
502        /// nothing current. What a kernel created from a shared program
503        /// starts with; the interpreter's is built that way and needs
504        /// nothing.
505        fn reset_to_program(&mut self) {}
506    }
507}
508
509/// A program on some engine, shared across threads through an `Arc`;
510/// every kernel created from it computes the same values and owns its
511/// own inputs, buffers, and outputs.
512pub trait KernelProgram: Send + Sync {
513    /// The engine the program was built for.
514    fn engine(&self) -> crate::compile::select::Engine;
515
516    /// A kernel of this program for the calling thread. It starts from
517    /// the program on every engine: every input at its declared
518    /// default, whatever the kernel that became the program had been
519    /// set to; every `shared` binding with a cell of its own.
520    fn create_kernel(self: std::sync::Arc<Self>) -> Box<dyn Kernel>;
521
522    /// The interpreter's program, when this is one: the graph a
523    /// diagnostic describes node by node. `None` for a compiled
524    /// engine's program.
525    fn as_interpreter(
526        self: std::sync::Arc<Self>,
527    ) -> Option<std::sync::Arc<crate::kernel::PolydatProgram>> {
528        None
529    }
530}
531
532/// A compiled kernel as a shared program: its steps are shared, and a
533/// created kernel is a clone that owns its own buffer, table, scratch,
534/// and externs.
535pub(crate) struct SharedKernel<K>(pub(crate) K);
536
537impl<K: Kernel + Clone + Send + Sync + 'static> KernelProgram for SharedKernel<K> {
538    fn engine(&self) -> crate::compile::select::Engine {
539        self.0.engine()
540    }
541    fn create_kernel(self: std::sync::Arc<Self>) -> Box<dyn Kernel> {
542        let mut kernel = self.0.clone();
543        // A created kernel starts from the program, as an interpreter
544        // state created from one does: inputs at their defaults, cells
545        // of its own; a host sets what it wants and attaches what it
546        // shares.
547        kernel.reset_to_program();
548        Box::new(kernel)
549    }
550}