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}