Skip to main content

polydat_core/compile/
mod.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Kernel compilation: assembled DAG → fast executable kernel.
5//!
6//! Everything in this module is on the path between
7//! [`assembly::PolydatAssembler`] and an executable kernel. The
8//! pipeline:
9//!
10//! ```text
11//! PolydatAssembler ──resolve──▶ ResolvedDag ──compile_with(Engine)──┐
12//!   (fusion, adapters,                                              │
13//!    round-trip lint,          ┌────────────────────────────────────┤
14//!    topo sort)                ▼                  ▼                 ▼
15//!                  Interpreter(JitMode)  Closures(Provenance)  Native(Provenance)
16//!                  cone::extract_jit_cones  closures::           hybrid::
17//!                  → PolydatKernel          CompiledKernel*      HybridKernel*
18//! ```
19//!
20//! The host names the engine ([`select::Engine`]); under
21//! `Provenance::Auto` the selector picks the provenance mode from the
22//! graph's shape. Pure native code (`jit::JitKernel*`) is the
23//! differential tier behind the hybrid kernel.
24//!
25//! - [`assembly`]: the public construction surface
26//!   ([`assembly::PolydatAssembler`] + [`assembly::WireRef`]) and
27//!   the per-engine compile paths.
28//! - [`fusion`]: graph-level subgraph fusion pass; runs during
29//!   assembly after wiring resolution.
30//! - [`roundtrip_lint`]: the structural type-round-trip lint run at
31//!   resolution.
32//! - [`cone`]: cone-level JIT inside the interpreter kernel
33//!   (SRD-105), under a [`cone::JitMode`].
34//! - [`lattice`]: the engine-mix report of a compiled program.
35//! - [`select`]: the engine and provenance enums, and the heuristic
36//!   that picks a provenance mode under `Provenance::Auto`.
37//! - [`closures`]: the closure tier, one generated op per node over
38//!   a flat u64 slot buffer, by-reference outputs as `Ref2` pairs.
39//! - [`hybrid`]: the native engine (native segments + closure steps
40//!   sharing a flat u64 buffer).
41//! - [`marshal`]: the boundary marshalling between slots and `Value`s.
42//! - `externs`: extern inputs and `shared` cells on the compiled
43//!   engines.
44//! - [`simd_plan`], `simd_tier1`: scalar-flow SIMD promotion.
45//! - `jit`: Cranelift lowering and the pure native kernels
46//!   (feature-gated on `jit`).
47
48pub mod assembly;
49pub mod closures;
50pub mod cone;
51#[cfg(all(test, feature = "jit"))]
52mod cone_tests;
53pub(crate) mod externs;
54pub mod fusion;
55#[cfg(feature = "jit")]
56pub(crate) mod fusion_units;
57pub mod hybrid;
58#[cfg(feature = "jit")]
59pub mod jit;
60pub mod lattice;
61/// The boundary marshalling a compiled node kit reads its arguments
62/// and writes its outputs through (compiled_handles.md §4): a node
63/// crate's kits use it as the core's own do.
64pub mod marshal;
65pub mod roundtrip_lint;
66pub mod select;
67pub mod simd_plan;
68#[cfg(feature = "jit")]
69pub mod simd_tier1;
70
71/// Axiom S2 typed accessors, shared by the P2 and hybrid kernel
72/// types (both expose `self.core.ref_entry(slot)`). Each returns
73/// a borrow whose lifetime ties to `&self`, so the borrow checker
74/// statically prevents holding a slice across the next
75/// `eval(&mut self)` — stale Ref reads are compile errors.
76macro_rules! ref_readers {
77    () => {
78        /// Borrow a `vec_f32` output's current contents.
79        pub fn read_vec_f32(&self, slot: usize) -> &[f32] {
80            match self.core.ref_entry(slot) {
81                crate::ast::ScratchBuf::F32(v) => v,
82                other => panic!("slot {slot} is not f32-lane scratch: {other:?}"),
83            }
84        }
85        /// Borrow a `vec_f64` output's current contents.
86        pub fn read_vec_f64(&self, slot: usize) -> &[f64] {
87            match self.core.ref_entry(slot) {
88                crate::ast::ScratchBuf::F64(v) => v,
89                other => panic!("slot {slot} is not f64-lane scratch: {other:?}"),
90            }
91        }
92        /// Borrow a `vec_f16` output's current contents.
93        pub fn read_vec_f16(&self, slot: usize) -> &[half::f16] {
94            match self.core.ref_entry(slot) {
95                crate::ast::ScratchBuf::F16(v) => v,
96                other => panic!("slot {slot} is not f16-lane scratch: {other:?}"),
97            }
98        }
99        /// Borrow a `vec_i8` output's current contents.
100        pub fn read_vec_i8(&self, slot: usize) -> &[i8] {
101            match self.core.ref_entry(slot) {
102                crate::ast::ScratchBuf::I8(v) => v,
103                other => panic!("slot {slot} is not i8-lane scratch: {other:?}"),
104            }
105        }
106        /// Borrow a `vec_i16` output's current contents.
107        pub fn read_vec_i16(&self, slot: usize) -> &[i16] {
108            match self.core.ref_entry(slot) {
109                crate::ast::ScratchBuf::I16(v) => v,
110                other => panic!("slot {slot} is not i16-lane scratch: {other:?}"),
111            }
112        }
113        /// Borrow a `vec_i32` output's current contents.
114        pub fn read_vec_i32(&self, slot: usize) -> &[i32] {
115            match self.core.ref_entry(slot) {
116                crate::ast::ScratchBuf::I32(v) => v,
117                other => panic!("slot {slot} is not i32-lane scratch: {other:?}"),
118            }
119        }
120        /// Borrow a `vec_i64` output's current contents.
121        pub fn read_vec_i64(&self, slot: usize) -> &[i64] {
122            match self.core.ref_entry(slot) {
123                crate::ast::ScratchBuf::I64(v) => v,
124                other => panic!("slot {slot} is not i64-lane scratch: {other:?}"),
125            }
126        }
127    };
128}
129pub(crate) use ref_readers;
130
131/// The accessors every compiled kernel type carries, whichever tier it
132/// belongs to: reading an output by name or by slot, the externs and
133/// the cursors it declares, and applying the coordinates a `Kernel`
134/// caller left pending before a pull or an eval. Every one of them
135/// delegates to `self.core`, so none decides anything about evaluation
136/// — which is why seven types can share one copy.
137///
138/// `$set_coords` is the coordinate writer the type uses: the provenance
139/// modes that track a changed-input mask apply coordinates through
140/// `set_inputs`, the rest through `set_coords`. It is the only thing
141/// that varies, and the closure tier and the hybrid had a macro each to
142/// vary it.
143macro_rules! kernel_accessors {
144    ($set_coords:ident) => {
145        /// The coordinate inputs.
146        pub fn coord_count(&self) -> usize {
147            self.core.coord_count
148        }
149
150        /// The slot of a named output.
151        pub fn resolve_output(&self, name: &str) -> Option<usize> {
152            self.core.output_map.get(name).copied()
153        }
154
155        /// Read an output by pre-resolved slot index. Panics on
156        /// Ref2-colored slots (axiom S2) — use `read_vec_*`.
157        #[inline]
158        pub fn get_slot(&self, slot: usize) -> u64 {
159            self.core.guard_ref_slot(slot);
160            self.core.buffer[slot]
161        }
162
163        /// Read a named output variate after `eval()`. Panics on
164        /// Ref2-colored outputs (axiom S2) — use `read_vec_*`.
165        #[inline]
166        pub fn get(&self, name: &str) -> u64 {
167            let slot = self.core.output_map[name];
168            self.core.guard_ref_slot(slot);
169            self.core.buffer[slot]
170        }
171
172        /// The named output as a typed `Value`, decoded by its port
173        /// type: a `Ref2` output is copied out through its pair
174        /// (compiled_handles.md §4), so the caller never holds a
175        /// pointer; a slot that holds `None` reads as `None`.
176        pub fn get_value(&self, name: &str) -> crate::ast::Value {
177            self.core.value_of(name)
178        }
179
180        /// A named output, its cone run if a write is pending.
181        pub fn pull_output(&mut self, name: &str) -> crate::ast::Value {
182            self.core.pull_named(name)
183        }
184
185        /// The named output through the `Kernel` trait: the pending
186        /// coordinates are applied, a round begins if a write is pending, and
187        /// only the output's cone runs.
188        fn pull_value(&mut self, name: &str) -> crate::ast::Value {
189            self.apply_pending_coords();
190            self.pull_output(name)
191        }
192
193        /// [`Self::pull_value`] by output index.
194        fn pull_value_at(&mut self, index: usize) -> crate::ast::Value {
195            self.apply_pending_coords();
196            self.core.pull_at(index)
197        }
198
199        /// The coordinates of a pending write, applied once: a pull
200        /// after the first in a round finds nothing written and skips
201        /// the comparison. The round itself begins in the core, which
202        /// clears the pending flag.
203        #[inline]
204        fn apply_pending_coords(&mut self) {
205            if self.core.drive.stale {
206                let coords = std::mem::take(&mut self.core.drive.coords);
207                self.$set_coords(&coords);
208                self.core.drive.coords = coords;
209            }
210        }
211
212        /// `eval` through the `Kernel` trait: the pending coordinates,
213        /// then every step.
214        fn eval_pending(&mut self) {
215            let coords = std::mem::take(&mut self.core.drive.coords);
216            self.eval(&coords);
217            self.core.drive.coords = coords;
218        }
219
220        /// The kernel's externs by name and declared type.
221        pub fn externs(&self) -> Vec<(&str, crate::ast::PortType)> {
222            self.core.externs.names()
223        }
224
225        /// The cursors the program declares, with the partitions the
226        /// compiler resolved where its `over` clause and extent were
227        /// constant, as `PolydatProgram::cursor_schemas` reports them.
228        pub fn cursor_schemas(&self) -> &[crate::iteration::source::SourceSchema] {
229            self.core.externs.cursor_schemas()
230        }
231
232        /// Narrow a cursor to one partition, as `narrow_cursor` does on
233        /// the interpreter: its `Ext` slot and six scalar projections
234        /// are set as externs.
235        pub fn set_cursor(
236            &mut self,
237            name: &str,
238            partition: &crate::iteration::cursor_partition::Partition,
239        ) -> Result<(), crate::kernel::WriteError> {
240            for (slot, value) in self.core.externs.cursor_writes(name, partition)? {
241                self.set_input(&slot, value)?;
242            }
243            Ok(())
244        }
245
246        crate::compile::ref_readers!();
247    };
248}
249pub(crate) use kernel_accessors;
250
251/// SRD-74's fusion rule: whether a node may join the run of fused
252/// code being formed, as far as `None` is concerned. The one predicate
253/// both fusers apply — the interpreter's cone planner and the hybrid's
254/// segment batcher — so that what one admits the other admits.
255///
256/// Fused code answers a `None` on a boundary input by making every one
257/// of its outputs `None`, because native code cannot carry one. That is
258/// SRD-74 Rule 1 and it is the right answer for a node that propagates
259/// a `None`. It is the wrong answer for a node that *consumes* one and
260/// keeps going — `to_json` writes `null`, a `printf` with an `Option`
261/// argument writes its own text — so such a node may join only when
262/// every one of its inputs comes from inside, where a `None` cannot
263/// arrive: either no boundary input is `None` and the run proceeds
264/// normally, or one is and the whole run answers `None` without any
265/// member running at all.
266///
267/// `eligible` is indexed by node and filled in topological order, so a
268/// node's producers are decided before it is.
269#[cfg(feature = "jit")]
270pub(crate) fn none_rule_admits(
271    accepts_none: bool,
272    wiring: &[crate::kernel::WireSource],
273    eligible: &[bool],
274) -> bool {
275    !accepts_none
276        || wiring
277            .iter()
278            .all(|src| matches!(src, crate::kernel::WireSource::NodeOutput(j, _) if eligible[*j]))
279}
280
281/// The highest tier a node can reach, given the types of the wires
282/// feeding it: the one answer to a question four places were asking
283/// separately.
284///
285/// The order is the one every builder walks. Native first, since a node
286/// with a lowering joins a segment; then the compiled forms, in the
287/// order `assembly::node_step_op` tries them — a copy step, the scalar
288/// `compiled_u64`, the node's own slot kit; then the interpreter.
289///
290/// The wire types are not optional. `compiled_slot` is offered per call
291/// site with the types the kernel fixed, so a caller without them can
292/// only ask the first two questions, and the three callers that
293/// reported a tier rather than selecting one did exactly that — they
294/// asked `compiled_u64().is_some()` and called a node with a slot kit
295/// `Phase1`, which is what the binary printed for `printf`.
296pub fn node_tier(
297    node: &dyn crate::ast::PolydatNode,
298    wire_types: &[crate::ast::PortType],
299) -> crate::ast::CompileLevel {
300    #[cfg(feature = "jit")]
301    if !matches!(
302        crate::compile::jit::classify_node_typed(node, wire_types),
303        crate::compile::jit::JitOp::Fallback
304    ) {
305        return crate::ast::CompileLevel::Phase3;
306    }
307    let meta = node.meta();
308    let is_copy =
309        (meta.name == "identity" || meta.name.starts_with("__port_")) && meta.outs.len() == 1;
310    let has_kit = node
311        .compiled_slot(
312            wire_types,
313            crate::compile::select::Engine::Closures(crate::compile::select::Provenance::Auto),
314        )
315        .is_some();
316    if is_copy || node.compiled_u64().is_some() || has_kit {
317        crate::ast::CompileLevel::Phase2
318    } else {
319        crate::ast::CompileLevel::Phase1
320    }
321}
322
323/// The provenance of every buffer slot: which input slots reach it,
324/// as an exact multi-word mask, for the pull-side cone guard of every
325/// compiled kernel. `input_dependents` is indexed by input slot (a
326/// multi-slot input repeats its list per slot) and lists the steps
327/// downstream of that slot; `step_output_slots` gives each step's
328/// output slots, which all take the step's mask. A coordinate slot's
329/// provenance is itself.
330pub(crate) fn slot_provenance(
331    coord_count: usize,
332    total_slots: usize,
333    step_output_slots: &[&[usize]],
334    input_dependents: &[Vec<usize>],
335) -> Vec<crate::kernel::ProvMask> {
336    use crate::kernel::ProvMask;
337    let step_count = step_output_slots.len();
338    let mut step_prov: Vec<ProvMask> = (0..step_count).map(|_| ProvMask::empty()).collect();
339    for (input_slot, deps) in input_dependents.iter().enumerate() {
340        for &step in deps {
341            if step < step_count {
342                step_prov[step].set(input_slot);
343            }
344        }
345    }
346    let mut slots: Vec<ProvMask> = (0..total_slots).map(|_| ProvMask::empty()).collect();
347    for (i, slot) in slots.iter_mut().enumerate().take(coord_count) {
348        slot.set(i);
349    }
350    for (step, outs) in step_output_slots.iter().enumerate() {
351        for &slot in outs.iter() {
352            if slot < slots.len() {
353                slots[slot] = step_prov[step].clone();
354            }
355        }
356    }
357    slots
358}
359
360/// The coordinates a host set last on a compiled kernel and whether
361/// they have been evaluated: what the [`Kernel`](crate::kernel::Kernel)
362/// trait's `set_inputs` and `pull` keep between calls.
363#[derive(Clone, Default)]
364pub(crate) struct Drive {
365    pub(crate) coords: Vec<u64>,
366    pub(crate) stale: bool,
367}
368
369/// The slot surface of a compiled kernel: the extended API, over and
370/// above the [`Kernel`](crate::kernel::Kernel) trait every engine
371/// answers.
372///
373/// Every compiled engine lays its program out over one flat `u64` slot
374/// buffer (engines.md §6). That layout is an implementation detail, and
375/// this trait is where it is admitted: a slot index instead of an
376/// output name, a raw `u64` instead of a `Value`, a borrow into the
377/// scratch a by-reference output writes. The interpreter does not
378/// implement it and cannot — its buffers are typed `Value`s and it has
379/// no slot to name — which is the point: the shape of this trait *is*
380/// the thing the compiled tiers share and the interpreter does not.
381///
382/// **This is not the surface for running a program.** Driving a kernel
383/// is `Kernel`, on every engine, and a host that never names an engine
384/// never sees this trait. Reach for it when the implementation detail
385/// is the subject: a differential test asserting on what was laid out,
386/// a benchmark measuring a tier without the `Value` construction and
387/// the name lookup a `pull` pays, a diagnostic reporting on a slot.
388///
389/// It is a subtrait rather than a wider `Kernel`, so it is opt-in at
390/// the import: a caller who does not write `use SlotKernel` does not
391/// have these methods on their kernel at all. And it is reachable
392/// without naming a kernel type, through
393/// [`PolydatAssembler::compile_slots`](crate::compile::assembly::PolydatAssembler::compile_slots),
394/// which hands back a `Box<dyn SlotKernel>` that upcasts to
395/// `Box<dyn Kernel>` wherever the ordinary surface will do.
396pub trait SlotKernel: crate::kernel::Kernel {
397    /// The buffer slot a named output writes, resolved once so a
398    /// caller reading the same output every cycle pays no lookup.
399    fn resolve_output(&self, name: &str) -> Option<usize>;
400
401    /// The raw `u64` in `slot`, as it stands: no evaluation, no
402    /// decoding. Panics on a `Ref2` slot (axiom S2), which has no
403    /// scalar to read — use the `read_vec_*` borrows.
404    fn get_slot(&self, slot: usize) -> u64;
405
406    /// [`Self::get_slot`] by output name.
407    fn get(&self, name: &str) -> u64;
408
409    /// A named output decoded by its port type, a `Ref2` output copied
410    /// out through its pair so the caller never holds a pointer. Reads
411    /// what is there; [`Kernel::pull`](crate::kernel::Kernel::pull)
412    /// evaluates first.
413    fn get_value(&self, name: &str) -> crate::ast::Value;
414
415    /// Set the coordinates, evaluate what `slot` needs, and return its
416    /// raw `u64`. The whole read in one call and one `u64`, which is
417    /// what a tier benchmark wants: `pull_at` gives the same value
418    /// through a `Value` it has to construct.
419    fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64;
420
421    /// Set the coordinates and run every step, the whole program in
422    /// one call. [`Kernel::eval`](crate::kernel::Kernel::eval) is the
423    /// same evaluation over coordinates already written with
424    /// `set_inputs`; this is the form that takes them, which is what a
425    /// loop over a coordinate range wants.
426    fn eval_at(&mut self, coords: &[u64]);
427
428    /// Borrow a `vec_f32` output's current contents. The borrow ties to
429    /// `&self`, so holding one across the next evaluation is a compile
430    /// error rather than a stale read (axiom S2).
431    fn read_vec_f32(&self, slot: usize) -> &[f32];
432    /// Borrow a `vec_f64` output's current contents.
433    fn read_vec_f64(&self, slot: usize) -> &[f64];
434    /// Borrow a `vec_f16` output's current contents.
435    fn read_vec_f16(&self, slot: usize) -> &[half::f16];
436    /// Borrow a `vec_i8` output's current contents.
437    fn read_vec_i8(&self, slot: usize) -> &[i8];
438    /// Borrow a `vec_i16` output's current contents.
439    fn read_vec_i16(&self, slot: usize) -> &[i16];
440    /// Borrow a `vec_i32` output's current contents.
441    fn read_vec_i32(&self, slot: usize) -> &[i32];
442    /// Borrow a `vec_i64` output's current contents.
443    fn read_vec_i64(&self, slot: usize) -> &[i64];
444}
445
446/// [`SlotKernel`] for a compiled kernel, forwarding to the inherent
447/// methods the type already has. The trait is the surface; the
448/// inherent copies are what it forwards to and what this crate calls.
449macro_rules! impl_slot_kernel {
450    ($ty:ident) => {
451        impl crate::compile::SlotKernel for $ty {
452            fn resolve_output(&self, name: &str) -> Option<usize> {
453                $ty::resolve_output(self, name)
454            }
455            fn get_slot(&self, slot: usize) -> u64 {
456                $ty::get_slot(self, slot)
457            }
458            fn get(&self, name: &str) -> u64 {
459                $ty::get(self, name)
460            }
461            fn get_value(&self, name: &str) -> crate::ast::Value {
462                $ty::get_value(self, name)
463            }
464            fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
465                $ty::eval_for_slot(self, coords, slot)
466            }
467            fn eval_at(&mut self, coords: &[u64]) {
468                $ty::eval(self, coords)
469            }
470            fn read_vec_f32(&self, slot: usize) -> &[f32] {
471                $ty::read_vec_f32(self, slot)
472            }
473            fn read_vec_f64(&self, slot: usize) -> &[f64] {
474                $ty::read_vec_f64(self, slot)
475            }
476            fn read_vec_f16(&self, slot: usize) -> &[half::f16] {
477                $ty::read_vec_f16(self, slot)
478            }
479            fn read_vec_i8(&self, slot: usize) -> &[i8] {
480                $ty::read_vec_i8(self, slot)
481            }
482            fn read_vec_i16(&self, slot: usize) -> &[i16] {
483                $ty::read_vec_i16(self, slot)
484            }
485            fn read_vec_i32(&self, slot: usize) -> &[i32] {
486                $ty::read_vec_i32(self, slot)
487            }
488            fn read_vec_i64(&self, slot: usize) -> &[i64] {
489                $ty::read_vec_i64(self, slot)
490            }
491        }
492    };
493}
494pub(crate) use impl_slot_kernel;
495
496/// The [`Kernel`](crate::kernel::Kernel) impl every compiled kernel
497/// shares: the type's inherent `eval_pending`, `pull_value`,
498/// `pull_value_at`, `set_input`, `set_input_at`, `set_cursor`,
499/// `mark_all_dirty`, and a `core` with `drive`, `externs`,
500/// `coord_count`, `output_types`, `output_map`, `buffer`,
501/// `traversals`, and `plan`/`invalidate_all`/`attach_cell`/
502/// `slot_value`.
503macro_rules! impl_kernel_trait {
504    ($ty:ident) => {
505        impl crate::kernel::Kernel for $ty {
506            fn engine(&self) -> crate::compile::select::Engine {
507                self.core.engine
508            }
509            fn set_inputs(&mut self, coords: &[u64]) {
510                self.core.drive.coords.clear();
511                self.core.drive.coords.extend_from_slice(coords);
512                self.core.drive.stale = true;
513            }
514            fn set_input(
515                &mut self,
516                name: &str,
517                value: crate::ast::Value,
518            ) -> Result<(), crate::kernel::WriteError> {
519                self.core.drive.stale = true;
520                $ty::set_input(self, name, value)
521            }
522            fn set_cursor(
523                &mut self,
524                name: &str,
525                partition: &crate::iteration::cursor_partition::Partition,
526            ) -> Result<(), crate::kernel::WriteError> {
527                self.core.drive.stale = true;
528                $ty::set_cursor(self, name, partition)
529            }
530            fn eval(&mut self) {
531                self.eval_pending();
532                self.core.drive.stale = false;
533            }
534            fn pull(&mut self, name: &str) -> crate::ast::Value {
535                self.pull_value(name)
536            }
537            fn input_names(&self) -> Vec<String> {
538                self.core.externs.input_names().to_vec()
539            }
540            /// In declaration order, as the interpreter lists them: the
541            /// assembler sets them on every compiled kernel.
542            fn output_names(&self) -> Vec<String> {
543                self.core.externs.output_names().to_vec()
544            }
545            fn output_type(&self, name: &str) -> Option<crate::ast::PortType> {
546                self.core.output_types.get(name).copied()
547            }
548            fn externs(&self) -> Vec<(String, crate::ast::PortType)> {
549                self.core
550                    .externs
551                    .names()
552                    .into_iter()
553                    .map(|(n, t)| (n.to_string(), t))
554                    .collect()
555            }
556            fn cursor_schemas(&self) -> &[crate::iteration::source::SourceSchema] {
557                self.core.externs.cursor_schemas()
558            }
559            fn input_value(&self, name: &str) -> Option<crate::ast::Value> {
560                self.core.externs.value(name).or_else(|| {
561                    let i = self
562                        .core
563                        .externs
564                        .input_names()
565                        .iter()
566                        .position(|n| n == name)?;
567                    if i < self.core.coord_count {
568                        let pending = self.core.drive.coords.get(i).copied();
569                        Some(crate::ast::Value::U64(
570                            pending.unwrap_or(self.core.buffer[i]),
571                        ))
572                    } else {
573                        None
574                    }
575                })
576            }
577            fn traversals(&self) -> &[crate::dsl::traversal::Traversal] {
578                &self.core.traversals
579            }
580            fn plan(&self) -> crate::EnginePlan {
581                self.core.plan()
582            }
583            fn input_index(&self, name: &str) -> Option<usize> {
584                self.core
585                    .externs
586                    .input_names()
587                    .iter()
588                    .position(|n| n == name)
589            }
590            fn set_input_at(
591                &mut self,
592                index: usize,
593                value: crate::ast::Value,
594            ) -> Result<(), crate::kernel::WriteError> {
595                self.core.drive.stale = true;
596                $ty::set_input_at(self, index, value)
597            }
598            fn output_index(&self, name: &str) -> Option<usize> {
599                self.core
600                    .externs
601                    .output_names()
602                    .iter()
603                    .position(|n| n == name)
604            }
605            fn pull_at(&mut self, index: usize) -> crate::ast::Value {
606                self.pull_value_at(index)
607            }
608            fn traverse(&mut self, index: usize) -> Result<crate::kernel::TraversalStream, String> {
609                let traversal = self.core.traversals.get(index).cloned().ok_or_else(|| {
610                    format!(
611                        "no traversal at index {index}; the program declares {}",
612                        self.core.traversals.len()
613                    )
614                })?;
615                crate::kernel::activation::open_traversal(self, traversal)
616            }
617            fn invalidate_all(&mut self) {
618                self.mark_all_dirty();
619                self.core.invalidate_all();
620            }
621            fn shared_cells(&self) -> Vec<crate::kernel::SharedCellEntry> {
622                self.core.externs.shared_cells()
623            }
624            fn output_cell(&self, name: &str) -> Option<crate::kernel::SharedCell> {
625                self.core.output_cell_for(name)
626            }
627            fn output_modifier(&self, name: &str) -> crate::dsl::ast::BindingModifier {
628                self.core.externs.output_modifier(name)
629            }
630            fn cells_in_scope(&self) -> Vec<crate::kernel::SharedCellEntry> {
631                self.core.externs.cells_in_scope()
632            }
633            fn set_transit_cells(&mut self, cells: Vec<crate::kernel::SharedCellEntry>) {
634                self.core.externs.set_transit_cells(cells);
635            }
636            fn input_port_type(&self, name: &str) -> Option<crate::ast::PortType> {
637                self.core.externs.input_port_type(name)
638            }
639            fn bind_input_cell(&mut self, name: &str, cell: crate::kernel::SharedCell) -> bool {
640                let Some(slot) = self.core.externs.bind_cell(name, cell) else {
641                    return false;
642                };
643                // The slot's value is the cell's from the next refresh,
644                // so everything downstream of it reruns.
645                self.core.dirty_input(slot);
646                true
647            }
648            fn attach_shared_cell(
649                &mut self,
650                name: &str,
651                cell: crate::kernel::SharedCell,
652            ) -> Result<(), String> {
653                self.core.attach_cell(name, cell)
654            }
655            fn into_program(
656                mut self: Box<Self>,
657            ) -> std::sync::Arc<dyn crate::kernel::KernelProgram> {
658                self.mark_all_dirty();
659                self.core.drive.stale = true;
660                std::sync::Arc::new(crate::kernel::SharedKernel(*self))
661            }
662            fn ledger(&self) -> &std::sync::Arc<crate::kernel::CompileLedger> {
663                self.core.externs.ledger()
664            }
665        }
666
667        impl crate::kernel::KernelInternals for $ty {
668            /// A compiled kernel keeps the traversals; each carries the
669            /// comprehension its producer resolved to at compile time.
670            fn set_traversals(
671                &mut self,
672                traversals: Vec<crate::dsl::traversal::Traversal>,
673                _producers: Vec<crate::dsl::traversal::Producer>,
674            ) {
675                self.core.traversals = traversals.into();
676            }
677            fn slot_value(&self, slot: usize, ty: crate::ast::PortType) -> crate::ast::Value {
678                self.core.slot_value(slot, ty)
679            }
680            fn folded_value(&self, name: &str) -> Option<crate::ast::Value> {
681                let slot = *self.core.output_map.get(name)?;
682                let ty = *self.core.output_types.get(name)?;
683                Some(self.core.slot_value(slot, ty))
684            }
685            fn set_cursor_extent(&mut self, index: usize, extent: u64) {
686                self.core.externs.set_cursor_extent(index, extent);
687            }
688            fn reset_to_program(&mut self) {
689                self.core.externs.reset_to_program(&mut self.core.buffer);
690                self.mark_all_dirty();
691            }
692        }
693    };
694}
695pub(crate) use impl_kernel_trait;
696
697/// The bookkeeping every compiled engine keeps, whatever its steps
698/// are: the evaluation round and what ran in it, the clean flags and
699/// what a write dirties, the extern writes and the cell refresh, the
700/// reference pairs a step publishes, and reading an output back.
701///
702/// Both compiled cores carry the same fields for these and, until this
703/// macro, the same eighteen method bodies byte for byte. None of them
704/// touches the step list, which is the one thing the two tiers
705/// genuinely differ about: a step on the closure tier is always a
706/// closure, and on the native tier it is a closure or a run of native
707/// code. That difference lives in the run loops, which stay per tier.
708macro_rules! shared_core_methods {
709    () => {
710        /// Axiom S9: every reference pair in the buffer names the
711        /// scratch entry that owns it. A slot is skipped when nothing
712        /// has been published into it — its step has not run, or it
713        /// carries `None`.
714        ///
715        /// The two tiers wrote this assertion separately and their
716        /// skip predicates had drifted apart: one skipped a `None`
717        /// slot only when a step owned it, the other whenever the
718        /// slot was `None`. Nothing is published either way, so the
719        /// looser test is the right one and is now the only one.
720        ///
721        /// Axiom S2 typed accessor core: resolve a Ref pair's first
722        /// slot to its kernel-owned scratch entry. The returned
723        /// borrow ties to `&self`, so holding it across the next
724        /// `eval(&mut self)` is a compile error — stale reads are
725        /// statically impossible.
726        fn ref_entry(&self, slot: usize) -> &crate::ast::ScratchBuf {
727            match self.ref_scratch.iter().find(|(s, _)| *s == slot) {
728                Some(&(_, idx)) => &self.scratch[idx],
729                None if self.ref_slots.get(slot).copied().unwrap_or(false) => panic!(
730                    "slot {slot} is a Ref pair owned by the CALLER (a kernel \
731                     input) — read it on the caller side"
732                ),
733                None => panic!("slot {slot} is not a Ref2-colored slot"),
734            }
735        }
736
737        /// Run `body` with the failure path armed: a panic inside a
738        /// step is recorded quietly and re-raised enriched, as the
739        /// interpreter re-raises a node's (A7), and every reference
740        /// pair is checked afterwards in a debug build.
741        ///
742        /// `#[inline]` is load-bearing: this wraps every `eval`, and
743        /// without it the native rung of the ladder pays a call and
744        /// about seven percent.
745        #[inline]
746        fn run_guarded(&mut self, body: impl FnOnce(&mut Self)) {
747            let capture = crate::kernel::engines::EvalPanicCaptureGuard::arm();
748            let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| body(self)));
749            drop(capture);
750            if let Err(payload) = outcome {
751                let sites = std::sync::Arc::clone(&self.sites);
752                let node = self.failing_node();
753                sites.reraise(payload, node, &self.buffer, Some(&self.none));
754            }
755            #[cfg(debug_assertions)]
756            self.validate_refs();
757        }
758
759        /// Gated to `debug_assertions` to match its call sites, which
760        /// compile out in release.
761        #[cfg(debug_assertions)]
762        fn validate_refs(&self) {
763            for &(slot, idx) in &self.ref_scratch {
764                let unpublished = self.none[slot]
765                    || matches!(self.slot_step.get(slot), Some(Some(step)) if self.ran[*step] == 0);
766                if unpublished {
767                    continue;
768                }
769                let (p, l) = self.scratch[idx].ptr_len();
770                assert!(
771                    self.buffer[slot] == p && self.buffer[slot + 1] == l,
772                    "S9 ref-validator: slot pair ({slot}, {}) = ({:#x}, {}) \
773                     does not match scratch[{idx}] = ({p:#x}, {l}) — a slot \
774                     op failed to republish or wrote the wrong slots",
775                    slot + 1,
776                    self.buffer[slot],
777                    self.buffer[slot + 1],
778                );
779            }
780        }
781
782        fn attach_cell(
783            &mut self,
784            name: &str,
785            cell: crate::kernel::SharedCell,
786        ) -> Result<(), String> {
787            let slot = self.externs.attach_cell(name, cell)?;
788            self.dirty_input(slot);
789            self.drive.stale = true;
790            Ok(())
791        }
792
793        #[inline]
794        fn begin_epoch(&mut self) {
795            if self.externs.cells_dirty() {
796                self.externs.refresh_cells(&mut self.buffer);
797            }
798            self.dirty_refreshed();
799            self.epoch += 1;
800            self.all_ran = false;
801            for &i in self.volatile_steps.iter() {
802                self.clean[i] = false;
803            }
804            self.drive.stale = false;
805        }
806
807        #[inline]
808        fn dirty_input(&mut self, slot: usize) {
809            if let Some(deps) = self.dirty.get(slot) {
810                for &i in deps {
811                    self.clean[i] = false;
812                }
813            }
814        }
815
816        #[inline]
817        fn dirty_refreshed(&mut self) {
818            if !self.externs.has_changed() {
819                return;
820            }
821            let changed = self.externs.take_changed();
822            for &slot in &changed {
823                if let Some(deps) = self.plan.input_dependents.get(slot) {
824                    for &i in deps {
825                        self.ran[i] = 0;
826                        self.clean[i] = false;
827                    }
828                    self.all_ran = false;
829                }
830            }
831            self.externs.return_changed(changed);
832        }
833
834        #[inline]
835        fn eval_all(&mut self) {
836            let fresh = self.drive.stale;
837            if fresh {
838                self.begin_epoch();
839            } else {
840                self.refresh_cells();
841            }
842            if fresh && !self.use_clean && !self.any_none {
843                self.run_guarded(|core| core.run_fresh());
844            } else {
845                let all = std::sync::Arc::clone(&self.all);
846                self.run_steps(&all);
847            }
848        }
849
850        fn extern_written(&mut self, slot: usize, unset: bool) {
851            self.none[slot] = unset;
852            let was = self.any_none;
853            self.any_none = self.externs.any_unset();
854            if was && !self.any_none {
855                self.none.fill(false);
856            }
857            self.dirty_input(slot);
858            self.drive.stale = true;
859        }
860
861        #[inline]
862        fn guard_ref_slot(&self, slot: usize) {
863            if self.ref_slots.get(slot).copied().unwrap_or(false) {
864                panic!(
865                    "S2 pointer containment: slot {slot} is Ref2-colored; raw u64 readers \
866                     would leak an interior address. Use the typed borrow-checked accessor \
867                     (read_vec_*), the boundary decode, or copy out."
868                );
869            }
870        }
871
872        fn invalidate_all(&mut self) {
873            self.clean.fill(false);
874            self.all_ran = false;
875            self.drive.stale = true;
876        }
877
878        fn pull_at(&mut self, index: usize) -> crate::ast::Value {
879            if self.resolved_outputs.len() <= index {
880                self.resolved_outputs.resize(index + 1, None);
881            }
882            if self.resolved_outputs[index].is_none() {
883                let name = self
884                    .externs
885                    .output_names()
886                    .get(index)
887                    .cloned()
888                    .unwrap_or_else(|| {
889                        panic!(
890                            "no output at index {index}; this kernel declares {}",
891                            self.externs.output_names().len()
892                        )
893                    });
894                let slot = self.output_map[&name];
895                let ty = self
896                    .output_types
897                    .get(&name)
898                    .copied()
899                    .unwrap_or(crate::ast::PortType::U64);
900                let cone = self
901                    .plan
902                    .cones
903                    .get(&name)
904                    .map(|c| std::sync::Arc::<[usize]>::from(c.as_slice()));
905                let can_fail = cone
906                    .as_ref()
907                    .is_some_and(|c| c.iter().any(|&i| self.step_can_fail(i)));
908                self.resolved_outputs[index] = Some((slot, ty, cone, can_fail));
909            }
910            if self.drive.stale {
911                self.begin_epoch();
912            } else {
913                self.refresh_cells();
914            }
915            let resolved = self.resolved_outputs[index]
916                .as_ref()
917                .expect("resolved above");
918            let (slot, ty, can_fail) = (resolved.0, resolved.1, resolved.3);
919            if let Some(order) = &resolved.2 {
920                // Borrowed across the run rather than cloned: the order
921                // lives behind an `Arc` this state holds, and running
922                // steps never touches the resolved outputs.
923                let order: *const [usize] = &**order;
924                let order = unsafe { &*order };
925                if can_fail {
926                    self.run_steps(order);
927                } else {
928                    // No step of the cone can fail, so there is no
929                    // failure to capture and attribute.
930                    self.run_order(order);
931                }
932            }
933            self.slot_value(slot, ty)
934        }
935
936        fn pull_named(&mut self, name: &str) -> crate::ast::Value {
937            if self.drive.stale {
938                self.begin_epoch();
939            } else {
940                self.refresh_cells();
941            }
942            let plan = std::sync::Arc::clone(&self.plan);
943            if let Some(order) = plan.cones.get(name) {
944                self.run_steps(order);
945            }
946            let value = self.value_of(name);
947            self.broadcast(name, &value);
948            value
949        }
950
951        /// Publish a freshly computed output through its broadcast cell,
952        /// if a descendant asked for one, so a child that bound its
953        /// matching input slot to the same cell reads the new value
954        /// (cross_fiber_invalidation.md §3.1, "broadcast outputs").
955        ///
956        /// A program nobody built a subscope under has no cells at all,
957        /// and pays the emptiness check.
958        #[inline]
959        fn broadcast(&mut self, name: &str, value: &crate::ast::Value) {
960            if !self.externs.broadcasts() {
961                return;
962            }
963            if let Some(&slot) = self.output_map.get(name)
964                && let Some(cell) = self.externs.published_output(slot)
965            {
966                cell.publish(value.clone());
967            }
968        }
969
970        /// The broadcast cell for a named output, created on the first
971        /// ask. The interpreter seeds one per output at construction;
972        /// a compiled kernel makes them only when a descendant binds to
973        /// one, so a program with no subscope under it allocates none.
974        ///
975        /// Keyed by the output's slot, which is what `output_map`
976        /// answers and what the buffer is indexed by, so the vector is
977        /// as long as the buffer rather than as long as the output list.
978        fn output_cell_for(&self, name: &str) -> Option<crate::kernel::SharedCell> {
979            let slot = *self.output_map.get(name)?;
980            let initial = self.value_of(name);
981            Some(self.externs.output_cell(slot, initial))
982        }
983
984        #[inline]
985        fn refresh_cells(&mut self) {
986            if self.externs.cells_dirty() {
987                self.externs.refresh_cells(&mut self.buffer);
988                self.dirty_refreshed();
989            }
990        }
991
992        fn republish_refs(&mut self) {
993            for &(slot, idx) in &self.ref_scratch {
994                let (p, l) = self.scratch[idx].ptr_len();
995                self.buffer[slot] = p;
996                self.buffer[slot + 1] = l;
997            }
998            self.externs.seed(&mut self.buffer, None);
999        }
1000
1001        #[inline]
1002        fn run_steps(&mut self, order: &[usize]) {
1003            self.run_guarded(|core| core.run_order(order));
1004        }
1005
1006        /// `run_steps` for the build-time constant fold: the same steps
1007        /// under the same guard, but a failure comes back as the
1008        /// message [`Attribution::reraise`] would have raised. A step
1009        /// that fails here fails before any kernel exists, so it is an
1010        /// error the builder returns rather than a panic out of a
1011        /// constructor.
1012        fn fold_steps(&mut self, order: &[usize]) -> Result<(), crate::KernelError> {
1013            let capture = crate::kernel::engines::EvalPanicCaptureGuard::arm();
1014            let outcome =
1015                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.run_order(order)));
1016            drop(capture);
1017            if let Err(payload) = outcome {
1018                let sites = std::sync::Arc::clone(&self.sites);
1019                let node = self.failing_node();
1020                return Err(crate::KernelError::ConstantFold {
1021                    reason: sites.describe(payload, node, &self.buffer, Some(&self.none)),
1022                });
1023            }
1024            Ok(())
1025        }
1026
1027        fn set_extern(
1028            &mut self,
1029            name: &str,
1030            value: crate::ast::Value,
1031        ) -> Result<usize, crate::kernel::WriteError> {
1032            let (slot, unset) = self.externs.set(name, value, &mut self.buffer)?;
1033            self.extern_written(slot, unset);
1034            Ok(slot)
1035        }
1036
1037        fn set_extern_at(
1038            &mut self,
1039            index: usize,
1040            value: crate::ast::Value,
1041        ) -> Result<usize, crate::kernel::WriteError> {
1042            let (slot, unset) = self.externs.set_at(index, value, &mut self.buffer)?;
1043            self.extern_written(slot, unset);
1044            Ok(slot)
1045        }
1046
1047        fn slot_value(&self, slot: usize, ty: crate::ast::PortType) -> crate::ast::Value {
1048            if self.none.get(slot).copied().unwrap_or(false) {
1049                return crate::ast::Value::None;
1050            }
1051            crate::compile::marshal::decode_output(&self.buffer, slot, ty)
1052        }
1053
1054        fn value_of(&self, name: &str) -> crate::ast::Value {
1055            let slot = self.output_map[name];
1056            let ty = self
1057                .output_types
1058                .get(name)
1059                .copied()
1060                .unwrap_or(crate::ast::PortType::U64);
1061            self.slot_value(slot, ty)
1062        }
1063    };
1064}
1065pub(crate) use shared_core_methods;
1066
1067/// The dirty-register plan of a compiled kernel: which steps each input
1068/// slot invalidates when it changes, and which steps each named output
1069/// needs. The evaluation loops consume only this; provenance derives it
1070/// today, and a host that knows its write and read patterns may supply
1071/// a narrower plan later without touching the loops
1072/// (docs/design/engines.md §3.1).
1073pub(crate) struct Invalidation {
1074    /// Per input slot (coordinates and externs alike), the steps that
1075    /// depend on it, transitively.
1076    pub(crate) input_dependents: Vec<Vec<usize>>,
1077    /// Per named output, the steps of its cone in evaluation order.
1078    pub(crate) cones: std::collections::HashMap<String, Vec<usize>>,
1079}
1080
1081impl Invalidation {
1082    /// The plan provenance gives: every step downstream of an input is
1083    /// invalidated by it, and every step upstream of an output is in
1084    /// its cone. `inputs` and `outputs` are each step's slots;
1085    /// `output_slots` names the outputs.
1086    pub(crate) fn from_provenance(
1087        input_dependents: Vec<Vec<usize>>,
1088        step_inputs: &[&[usize]],
1089        step_outputs: &[&[usize]],
1090        output_slots: &std::collections::HashMap<String, usize>,
1091        total_slots: usize,
1092    ) -> Self {
1093        let step_count = step_inputs.len();
1094        let mut slot_step: Vec<Option<usize>> = vec![None; total_slots];
1095        for (i, outs) in step_outputs.iter().enumerate() {
1096            for &s in outs.iter() {
1097                slot_step[s] = Some(i);
1098            }
1099        }
1100        let cones = output_slots
1101            .iter()
1102            .map(|(name, &slot)| {
1103                let mut wanted = vec![false; step_count];
1104                let mut stack: Vec<usize> = slot_step[slot].into_iter().collect();
1105                while let Some(i) = stack.pop() {
1106                    if wanted[i] {
1107                        continue;
1108                    }
1109                    wanted[i] = true;
1110                    stack.extend(step_inputs[i].iter().filter_map(|&s| slot_step[s]));
1111                }
1112                (
1113                    name.clone(),
1114                    (0..step_count).filter(|&i| wanted[i]).collect(),
1115                )
1116            })
1117            .collect();
1118        Self {
1119            input_dependents,
1120            cones,
1121        }
1122    }
1123}
1124
1125/// Where each compiled step came from, for the failure path only
1126/// (engines.md §3.4). A step's panic is caught at the step
1127/// boundary and re-raised enriched exactly as the interpreter enriches
1128/// a node's: the node's name, the outputs it feeds, the program's
1129/// diagnostic context, and its input values decoded from the buffer
1130/// where the slot types allow. `sites` is indexed by program node:
1131/// the closure and pure-native kernels have one step per node, and
1132/// the hybrid kernel names the failing member of a segment through
1133/// its tracker slot.
1134#[derive(Default)]
1135pub(crate) struct Attribution {
1136    pub(crate) sites: Vec<NodeSite>,
1137    /// The program's diagnostic context (`PolydatProgram::context`).
1138    pub(crate) context: String,
1139}
1140
1141/// One node's identity for the failure path.
1142pub(crate) struct NodeSite {
1143    pub(crate) name: String,
1144    /// The declared outputs the node feeds, sorted.
1145    pub(crate) outputs: Vec<String>,
1146    /// `(first slot, port type)` of every input port, in port order.
1147    pub(crate) inputs: Vec<(usize, crate::ast::PortType)>,
1148}
1149
1150impl Attribution {
1151    /// The inputs of `step` as diagnostic text, from the buffer, each
1152    /// copied out and printed as the interpreter prints the same value:
1153    /// `None` where the mask says so, and the port type alone where the
1154    /// slot cannot be decoded, so the report itself never fails.
1155    fn inputs_of(&self, step: usize, buffer: &[u64], none: Option<&[bool]>) -> Vec<String> {
1156        let Some(site) = self.sites.get(step) else {
1157            return Vec::new();
1158        };
1159        let _quiet = crate::kernel::engines::EvalPanicCaptureGuard::arm();
1160        site.inputs
1161            .iter()
1162            .map(|&(slot, ty)| {
1163                if none.is_some_and(|m| m.get(slot).copied().unwrap_or(false)) {
1164                    return "None".to_string();
1165                }
1166                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1167                    crate::kernel::engines::format_value_for_diag(&marshal::decode_output(
1168                        buffer, slot, ty,
1169                    ))
1170                }))
1171                .unwrap_or_else(|_| format!("{ty:?}"))
1172            })
1173            .collect()
1174    }
1175
1176    /// Re-raise a step's panic enriched as the interpreter enriches a
1177    /// node's (`kernel::engines::enrich_panic`). `step` beyond the
1178    /// sites (native code that failed before naming a step) reports an
1179    /// unknown node, as the interpreter does for an index it lacks.
1180    pub(crate) fn reraise(
1181        &self,
1182        payload: Box<dyn std::any::Any + Send>,
1183        step: usize,
1184        buffer: &[u64],
1185        none: Option<&[bool]>,
1186    ) -> ! {
1187        crate::kernel::engines::reraise_enriched(self.describe(payload, step, buffer, none))
1188    }
1189
1190    /// The same message [`Self::reraise`] raises, returned instead. The
1191    /// build-time constant fold uses it: a step that fails there fails
1192    /// before any kernel exists, so it is an error the builder returns
1193    /// and not a panic out of a constructor.
1194    pub(crate) fn describe(
1195        &self,
1196        payload: Box<dyn std::any::Any + Send>,
1197        step: usize,
1198        buffer: &[u64],
1199        none: Option<&[bool]>,
1200    ) -> String {
1201        let site = self.sites.get(step);
1202        let name = site
1203            .map(|s| s.name.clone())
1204            .unwrap_or_else(|| format!("<unknown node #{step}>"));
1205        let outputs: Vec<&str> = site
1206            .map(|s| s.outputs.iter().map(String::as_str).collect())
1207            .unwrap_or_default();
1208        let inputs = self.inputs_of(step, buffer, none);
1209        crate::kernel::engines::enrich_panic(payload, &name, &outputs, &self.context, &inputs)
1210    }
1211}