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