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