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