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