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}