Skip to main content

polydat_core/kernel/
api.rs

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