Skip to main content

polydat_core/compile/jit/
kernels.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! JIT kernel types: structs and impls for all four kernel variants.
5//!
6//! `JitCore` holds the shared buffer, slot map, and module handle.
7//! The four kernel structs (`JitKernelRaw`, `JitKernelPush`,
8//! `JitKernelPull`, `JitKernelPushPull`) wrap a `JitCore` and a
9//! compiled function pointer, providing `eval` and accessor methods.
10
11use std::collections::HashMap;
12
13use cranelift_jit::JITModule;
14
15use crate::ast::PolydatNode;
16use crate::kernel::ProvMask;
17
18/// Finalized native code, shared by every kernel created from one
19/// program. The module's memory is never written after finalization,
20/// so sharing it across threads is sound; the wrapper exists so a
21/// kernel clone is a new state over the same code. The slot kits the
22/// code calls by address live beside it, for as long as it does.
23#[derive(Clone)]
24pub struct JitCode(std::sync::Arc<FinalizedModule>);
25
26/// A JIT module after finalization, which nothing writes again, the
27/// kits its code calls, and whether the code calls anything at all.
28struct FinalizedModule {
29    #[allow(dead_code)]
30    module: JITModule,
31    #[allow(dead_code)]
32    kits: Vec<super::codegen::SlotKitRef>,
33    fallible: bool,
34}
35
36/// The scratch a native kernel's state owns: one entry per entry the
37/// steps' kits declare, and the `(first slot, entry)` pairs of the
38/// scratch-backed `Ref2` outputs among them (axiom S9(a)).
39#[derive(Clone, Default)]
40pub(crate) struct ScratchPlan {
41    pub(crate) elems: Vec<crate::ast::ScratchElem>,
42    pub(crate) refs: Vec<(usize, usize)>,
43}
44
45// SAFETY: the module is finalized before it is wrapped and never
46// touched again; only its code runs, from any thread.
47unsafe impl Send for FinalizedModule {}
48unsafe impl Sync for FinalizedModule {}
49
50impl JitCode {
51    pub(crate) fn new(
52        module: JITModule,
53        kits: Vec<super::codegen::SlotKitRef>,
54        fallible: bool,
55    ) -> Self {
56        JitCode(std::sync::Arc::new(FinalizedModule {
57            module,
58            kits,
59            fallible,
60        }))
61    }
62
63    /// Whether the code can fail: it calls a helper, and a helper can
64    /// raise a node's failure through the longjmp catch. Code with no
65    /// call is arithmetic over the buffer, which cannot fail, so the
66    /// site that runs it needs no catch around it (the jump buffer, the
67    /// panic capture, and the unwind guard are the fixed cost of an
68    /// evaluation on the pure tier).
69    pub(crate) fn fallible(&self) -> bool {
70        self.0.fallible
71    }
72}
73
74/// A raw native kernel taken apart: its entry point and its code.
75pub type JitParts = (super::codegen::NativeFn, JitCode);
76
77/// Shared fields for all JIT kernel variants. A clone is a new state
78/// of the same program: the code and the nodes are shared, everything
79/// else is the clone's own (engine_parity.md, step 4), and every extern
80/// pair in its buffer points into its own storage (axiom S3), never
81/// into the state it was cloned from.
82pub(super) struct JitCore {
83    pub(super) buffer: Vec<u64>,
84    pub(super) coord_count: usize,
85    pub(super) output_map: HashMap<String, usize>,
86    /// Slots the raw readers refuse: `Ref2` pairs (axiom S2). Set by
87    /// the assembler once the layout is known; empty means no such
88    /// slot.
89    pub(super) guard_slots: Vec<bool>,
90    /// Port type of each named output, for `get_value`'s decode.
91    pub(super) output_types: HashMap<String, crate::ast::PortType>,
92    /// The extern inputs, written through at every set.
93    pub(super) externs: crate::compile::externs::Externs,
94    /// The traversals the program declares (SRD 113), opened through the
95    /// `Kernel` trait.
96    pub(super) traversals: std::sync::Arc<[crate::dsl::traversal::Traversal]>,
97    pub(super) _module: JitCode,
98    /// Whether the code calls a helper, and so runs under the catch.
99    pub(super) fallible: bool,
100    pub(super) _nodes: std::sync::Arc<Vec<Box<dyn PolydatNode>>>,
101    /// The coordinates set through the `Kernel` trait, pending
102    /// evaluation.
103    pub(super) drive: crate::compile::Drive,
104    /// Where each step came from, for the failure path (A7).
105    pub(super) sites: std::sync::Arc<crate::compile::Attribution>,
106    /// The slot past the layout where native code names the step it
107    /// is in before calling a helper; `u64::MAX` before any.
108    pub(super) tracker: usize,
109    /// The scratch entries the steps' kits write into, owned by this
110    /// state (axiom S3); native code receives the base pointer.
111    pub(super) scratch: Vec<crate::ast::ScratchBuf>,
112    /// Axiom S9(a): (first slot of a Ref pair → scratch index) for
113    /// every scratch-backed Ref output.
114    pub(super) ref_scratch: Vec<(usize, usize)>,
115    /// The steps that are never current (runtime_model.md, R1.v): a
116    /// nondeterministic node or one downstream of it. The kernels
117    /// with a clean flag per step clear theirs at every write, and
118    /// the kernels with a cone guard run whenever one exists and a
119    /// write happened, since one native function is the program.
120    pub(super) volatile_steps: Vec<usize>,
121}
122
123impl Clone for JitCore {
124    fn clone(&self) -> Self {
125        let mut core = JitCore {
126            buffer: self.buffer.clone(),
127            coord_count: self.coord_count,
128            output_map: self.output_map.clone(),
129            guard_slots: self.guard_slots.clone(),
130            output_types: self.output_types.clone(),
131            externs: self.externs.clone(),
132            traversals: self.traversals.clone(),
133            _module: self._module.clone(),
134            fallible: self.fallible,
135            _nodes: self._nodes.clone(),
136            drive: self.drive.clone(),
137            sites: self.sites.clone(),
138            tracker: self.tracker,
139            scratch: self.scratch.clone(),
140            ref_scratch: self.ref_scratch.clone(),
141            volatile_steps: self.volatile_steps.clone(),
142        };
143        // Every pair points into this state's own storage (axiom S3):
144        // a step's scratch entry, the value an extern stores.
145        for &(slot, idx) in &core.ref_scratch {
146            let (p, l) = core.scratch[idx].ptr_len();
147            core.buffer[slot] = p;
148            core.buffer[slot + 1] = l;
149        }
150        core.externs.seed(&mut core.buffer, None);
151        core
152    }
153}
154
155impl JitCore {
156    /// The value at `slot` decoded as `ty`, a pair copied out.
157    pub(super) fn slot_value(&self, slot: usize, ty: crate::ast::PortType) -> crate::ast::Value {
158        crate::compile::marshal::decode_output(&self.buffer, slot, ty)
159    }
160
161    /// One native function is the program.
162    pub(super) fn plan(&self) -> crate::EnginePlan {
163        crate::EnginePlan {
164            native_segments: 1,
165            ..Default::default()
166        }
167    }
168
169    /// The next evaluation runs the program: one native function.
170    pub(super) fn invalidate_all(&mut self) {
171        self.drive.stale = true;
172    }
173
174    pub(super) fn new(
175        total_slots: usize,
176        coord_count: usize,
177        output_map: HashMap<String, usize>,
178        code: JitCode,
179        nodes: Vec<Box<dyn PolydatNode>>,
180        scratch: ScratchPlan,
181        volatile_steps: Vec<usize>,
182    ) -> Self {
183        Self {
184            buffer: vec![0u64; total_slots + 1],
185            coord_count,
186            output_map,
187            guard_slots: Vec::new(),
188            output_types: HashMap::new(),
189            externs: crate::compile::externs::Externs::default(),
190            traversals: Vec::new().into(),
191            fallible: code.fallible(),
192            _module: code,
193            _nodes: std::sync::Arc::new(nodes),
194            drive: crate::compile::Drive::default(),
195            sites: std::sync::Arc::default(),
196            tracker: total_slots,
197            scratch: scratch
198                .elems
199                .iter()
200                .map(|e| crate::ast::ScratchBuf::new(*e))
201                .collect(),
202            ref_scratch: scratch.refs,
203            volatile_steps,
204        }
205    }
206
207    /// Whether a write must run the program regardless of the cone
208    /// guard: a never-current step exists (R1.v).
209    #[inline]
210    fn has_volatile(&self) -> bool {
211        !self.volatile_steps.is_empty()
212    }
213
214    /// Axiom S9(a): every scratch-backed pair in the buffer names its
215    /// own entry, checked after a run in debug builds.
216    #[cfg(debug_assertions)]
217    fn validate_refs(&self) {
218        for &(slot, idx) in &self.ref_scratch {
219            let (p, l) = self.scratch[idx].ptr_len();
220            assert!(
221                self.buffer[slot] == p && self.buffer[slot + 1] == l,
222                "S9 ref-validator: slot pair ({slot}, {}) = ({:#x}, {}) does not match \
223                 scratch[{idx}] = ({p:#x}, {l})",
224                slot + 1,
225                self.buffer[slot],
226                self.buffer[slot + 1],
227            );
228        }
229    }
230
231    /// Install the extern inputs, written through into the buffer now.
232    pub(super) fn set_externs(&mut self, externs: crate::compile::externs::Externs) {
233        externs.seed(&mut self.buffer, None);
234        self.externs = externs;
235    }
236
237    /// Set an extern by name; returns its slot for dirty marking.
238    fn set_extern(&mut self, name: &str, value: crate::ast::Value) -> Result<usize, String> {
239        Ok(self.externs.set(name, value, &mut self.buffer)?.0)
240    }
241
242    /// [`Self::set_extern`] by input index.
243    fn set_extern_at(&mut self, index: usize, value: crate::ast::Value) -> Result<usize, String> {
244        Ok(self.externs.set_at(index, value, &mut self.buffer)?.0)
245    }
246
247    /// Bind a `shared` binding to `cell` (engine parity, step 9). Native
248    /// code evaluates the whole program, so the next run reads it.
249    fn attach_cell(&mut self, name: &str, cell: crate::kernel::SharedCell) -> Result<(), String> {
250        self.externs.attach_cell(name, cell)?;
251        self.drive.stale = true;
252        Ok(())
253    }
254
255    /// Run one native evaluation: take what cells other holders
256    /// published, refuse an unset extern (native code cannot carry a
257    /// `None`; engine_parity.md, A12), run inside the longjmp catch.
258    #[inline]
259    pub(super) fn run(&mut self, native: impl FnOnce()) {
260        if self.externs.cells_dirty() {
261            self.externs.refresh_cells(&mut self.buffer);
262        }
263        if let Some((name, ty)) = self.externs.first_unset() {
264            panic!(
265                "extern '{name}' ({ty}) has no value: it has no default, so set it with \
266                 set_input before the first run (native code cannot carry `None`; \
267                 docs/design/engine_parity.md, A12)"
268            );
269        }
270        // Code that calls no helper cannot fail: it runs bare. Otherwise
271        // native code names the step it is in before each helper call;
272        // a failure before any names none. The capture guard is armed
273        // for the run, so the helper's panic is recorded quietly and
274        // re-raised enriched, as the interpreter re-raises a node's (A7).
275        if !self.fallible {
276            native();
277        } else {
278            self.buffer[self.tracker] = u64::MAX;
279            let capture = crate::kernel::engines::EvalPanicCaptureGuard::arm();
280            let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
281                super::codegen::invoke_with_catch(native)
282            }));
283            drop(capture);
284            if let Err(payload) = outcome {
285                let step = self.buffer[self.tracker] as usize;
286                let sites = std::sync::Arc::clone(&self.sites);
287                sites.reraise(payload, step, &self.buffer, None);
288            }
289        }
290        #[cfg(debug_assertions)]
291        self.validate_refs();
292    }
293}
294
295macro_rules! jit_accessors {
296    () => {
297        /// Returns the number of coordinate inputs this kernel accepts.
298        pub fn coord_count(&self) -> usize {
299            self.core.coord_count
300        }
301
302        /// Returns the buffer slot index for the named output, if present.
303        pub fn resolve_output(&self, name: &str) -> Option<usize> {
304            self.core.output_map.get(name).copied()
305        }
306
307        /// Returns the raw u64 value stored in the named output slot.
308        #[inline]
309        pub fn get(&self, name: &str) -> u64 {
310            self.get_slot(self.core.output_map[name])
311        }
312
313        /// Returns the raw u64 value stored at the given buffer slot
314        /// index. Refuses a reference or handle slot (axioms S2, H1):
315        /// read those through [`Self::get_value`].
316        #[inline]
317        pub fn get_slot(&self, slot: usize) -> u64 {
318            if self.core.guard_slots.get(slot).copied().unwrap_or(false) {
319                panic!(
320                    "slot {slot} is Ref2-colored; a raw u64 read would leak an interior \
321                     address. Use get_value to decode it."
322                );
323            }
324            self.core.buffer[slot]
325        }
326
327        /// The named output as a typed `Value`, decoded by its port type:
328        /// a reference pair is copied out, so the caller never holds a
329        /// reference into the buffer.
330        pub fn get_value(&self, name: &str) -> crate::ast::Value {
331            let slot = self.core.output_map[name];
332            let ty = self
333                .core
334                .output_types
335                .get(name)
336                .copied()
337                .unwrap_or(crate::ast::PortType::U64);
338            crate::compile::marshal::decode_output(&self.core.buffer, slot, ty)
339        }
340
341        /// Record the slots raw readers must refuse and each output's
342        /// port type. Called by the assembler after construction.
343        pub(crate) fn set_slot_info(
344            &mut self,
345            guard_slots: Vec<bool>,
346            output_types: HashMap<String, crate::ast::PortType>,
347        ) {
348            self.core.guard_slots = guard_slots;
349            self.core.output_types = output_types;
350        }
351
352        /// Where each step came from, for the failure path (A7).
353        pub(crate) fn set_attribution(
354            &mut self,
355            sites: std::sync::Arc<crate::compile::Attribution>,
356        ) {
357            self.core.sites = sites;
358        }
359
360        /// Set an extern by name, as `PolydatState::set_input` does on
361        /// the interpreter. The value must be of the declared port
362        /// type. A carrier takes effect at once; a string, JSON, or
363        /// extension value is written at the start of the next run, and
364        /// every step downstream of the extern reruns.
365        pub fn set_input(&mut self, name: &str, value: crate::ast::Value) -> Result<(), String> {
366            let slot = self.core.set_extern(name, value)?;
367            self.mark_input_changed(slot);
368            Ok(())
369        }
370
371        /// [`Self::set_input`] by input index.
372        pub fn set_input_at(
373            &mut self,
374            index: usize,
375            value: crate::ast::Value,
376        ) -> Result<(), String> {
377            let slot = self.core.set_extern_at(index, value)?;
378            self.mark_input_changed(slot);
379            Ok(())
380        }
381
382        /// The kernel's externs by name and declared type.
383        pub fn externs(&self) -> Vec<(&str, crate::ast::PortType)> {
384            self.core.externs.names()
385        }
386
387        /// Every step downstream of a coordinate reruns at the next
388        /// evaluation: the state a kernel created from a shared program
389        /// starts in.
390        fn mark_all_dirty(&mut self) {
391            for i in 0..self.core.coord_count {
392                self.mark_input_changed(i);
393            }
394        }
395
396        /// The named output through the `Kernel` trait. Native code
397        /// evaluates the whole program, so a pull after new inputs is an
398        /// evaluation.
399        fn pull_value(&mut self, name: &str) -> crate::ast::Value {
400            // A cell another holder published to is a changed input.
401            if self.core.drive.stale || self.core.externs.cells_dirty() {
402                self.eval_pending();
403                self.core.drive.stale = false;
404            }
405            self.get_value(name)
406        }
407
408        /// [`Self::pull_value`] by output index: one native function is
409        /// the program, so the index names the output and nothing more.
410        fn pull_value_at(&mut self, index: usize) -> crate::ast::Value {
411            let name = self
412                .core
413                .externs
414                .output_names()
415                .get(index)
416                .cloned()
417                .unwrap_or_else(|| panic!("no output at index {index}"));
418            self.pull_value(&name)
419        }
420
421        /// `eval` through the `Kernel` trait: the pending coordinates.
422        fn eval_pending(&mut self) {
423            let coords = std::mem::take(&mut self.core.drive.coords);
424            self.eval(&coords);
425            self.core.drive.coords = coords;
426        }
427
428        /// The cursors the program declares, with the partitions the
429        /// compiler resolved where its `over` clause and extent were
430        /// constant, as `PolydatProgram::cursor_schemas` reports them.
431        pub fn cursor_schemas(&self) -> &[crate::iteration::source::SourceSchema] {
432            self.core.externs.cursor_schemas()
433        }
434
435        /// Narrow a cursor to one partition, as `narrow_cursor` does on
436        /// the interpreter: its `Ext` slot and six scalar projections
437        /// are set as externs.
438        pub fn set_cursor(
439            &mut self,
440            name: &str,
441            partition: &crate::iteration::cursor_partition::Partition,
442        ) -> Result<(), String> {
443            for (slot, value) in self.core.externs.cursor_writes(name, partition)? {
444                self.set_input(&slot, value)?;
445            }
446            Ok(())
447        }
448    };
449}
450
451// ── JitKernelRaw ───────────────────────────────────────────
452
453/// Raw JIT kernel: no provenance, all nodes evaluate unconditionally.
454#[derive(Clone)]
455#[doc(hidden)]
456pub struct JitKernelRaw {
457    pub(super) core: JitCore,
458    pub(super) code_fn: super::codegen::NativeFn,
459}
460
461impl JitKernelRaw {
462    /// Evaluate the kernel with the given coordinate values.
463    ///
464    /// Predicate violations (`is_positive`, `in_range`,
465    /// `is_one_of`) from JIT-lowered code surface as normal
466    /// Rust panics carrying the violation message. The
467    /// longjmp wrapper in `super::codegen::invoke_with_catch`
468    /// handles the transition back to Rust land.
469    #[inline]
470    pub fn eval(&mut self, coords: &[u64]) {
471        // Written one by one, as the other kernels write them: a slice
472        // copy of a runtime length is a call to memcpy, which costs
473        // more than the three stores it replaces.
474        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
475            if self.core.buffer[i] != c {
476                self.core.buffer[i] = c;
477            }
478        }
479        let code_fn = self.code_fn;
480        let buf_ptr_const = self.core.buffer.as_ptr();
481        let buf_ptr_mut = self.core.buffer.as_mut_ptr();
482        let sc = self.core.scratch.as_mut_ptr();
483        self.core.run(move || unsafe {
484            (code_fn)(buf_ptr_const, buf_ptr_mut, sc);
485        });
486    }
487
488    /// Evaluate and return the value at the given buffer slot index.
489    #[inline]
490    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
491        self.eval(coords);
492        self.core.buffer[slot]
493    }
494
495    /// Decompose into raw parts for hybrid kernel integration: the
496    /// entry point and its module.
497    pub fn into_parts(self) -> JitParts {
498        (self.code_fn, self.core._module)
499    }
500
501    /// Every run evaluates everything; a changed input needs no mark.
502    fn mark_input_changed(&mut self, _slot: usize) {}
503
504    jit_accessors!();
505}
506
507// ── JitKernelPush ──────────────────────────────────────────
508
509/// Push-only JIT kernel: per-node dirty tracking, no cone guard.
510#[derive(Clone)]
511#[doc(hidden)]
512pub struct JitKernelPush {
513    pub(super) core: JitCore,
514    pub(super) code_fn_prov: super::codegen::NativeProvFn,
515    pub(super) node_clean: Vec<u8>,
516    pub(super) input_dependents: Vec<Vec<usize>>,
517}
518
519impl JitKernelPush {
520    #[inline]
521    fn set_inputs(&mut self, coords: &[u64]) {
522        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
523            if self.core.buffer[i] != c {
524                self.core.buffer[i] = c;
525                self.mark_input_changed(i);
526            }
527        }
528        // A write makes every never-current step run again (R1.v).
529        for &step_idx in &self.core.volatile_steps {
530            self.node_clean[step_idx] = 0;
531        }
532    }
533
534    /// Every step downstream of the slot reruns, and every
535    /// never-current step with it (R1.v).
536    fn mark_input_changed(&mut self, slot: usize) {
537        if slot < self.input_dependents.len() {
538            for &step_idx in &self.input_dependents[slot] {
539                self.node_clean[step_idx] = 0;
540            }
541        }
542        for &step_idx in &self.core.volatile_steps {
543            self.node_clean[step_idx] = 0;
544        }
545    }
546
547    /// Evaluate the kernel with the given coordinate values.
548    #[inline]
549    pub fn eval(&mut self, coords: &[u64]) {
550        self.set_inputs(coords);
551        let code_fn = self.code_fn_prov;
552        let buf_const = self.core.buffer.as_ptr();
553        let buf_mut = self.core.buffer.as_mut_ptr();
554        let sc = self.core.scratch.as_mut_ptr();
555        let clean_mut = self.node_clean.as_mut_ptr();
556        self.core.run(move || unsafe {
557            (code_fn)(buf_const, buf_mut, sc, clean_mut);
558        });
559    }
560
561    /// Evaluate and return the value at the given buffer slot index.
562    #[inline]
563    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
564        self.eval(coords);
565        self.core.buffer[slot]
566    }
567
568    jit_accessors!();
569}
570
571// ── JitKernelPull ──────────────────────────────────────────
572
573/// Pull-only JIT kernel: cone guard, but all nodes run when cone is dirty.
574/// Uses the raw (non-provenance) JIT function — no per-node clean checks.
575#[derive(Clone)]
576#[doc(hidden)]
577pub struct JitKernelPull {
578    pub(super) core: JitCore,
579    pub(super) code_fn: super::codegen::NativeFn,
580    pub(super) slot_provenance: Vec<ProvMask>,
581    pub(super) changed_mask: ProvMask,
582    /// Set by `set_input`: an extern changed, so the next evaluation
583    /// runs whatever the cone guard says.
584    pub(super) force_run: bool,
585}
586
587impl JitKernelPull {
588    #[inline]
589    fn set_inputs(&mut self, coords: &[u64]) {
590        self.changed_mask.clear();
591        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
592            if self.core.buffer[i] != c {
593                self.core.buffer[i] = c;
594                self.changed_mask.set(i);
595            }
596        }
597        // A never-current step runs again after every write (R1.v),
598        // and one native function is the program.
599        if self.core.has_volatile() {
600            self.force_run = true;
601        }
602    }
603
604    /// The next evaluation runs regardless of the cone guard, since
605    /// `set_inputs` rebuilds the changed set from the coordinates alone.
606    fn mark_input_changed(&mut self, _slot: usize) {
607        self.force_run = true;
608    }
609
610    /// Evaluate the kernel with the given coordinate values.
611    #[inline]
612    pub fn eval(&mut self, coords: &[u64]) {
613        self.set_inputs(coords);
614        self.force_run = false;
615        let code_fn = self.code_fn;
616        let buf_const = self.core.buffer.as_ptr();
617        let buf_mut = self.core.buffer.as_mut_ptr();
618        let sc = self.core.scratch.as_mut_ptr();
619        self.core.run(move || unsafe {
620            (code_fn)(buf_const, buf_mut, sc);
621        });
622    }
623
624    /// Evaluate and return the value at the given buffer slot index,
625    /// skipping evaluation if the slot's provenance cone is unaffected.
626    #[inline]
627    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
628        self.set_inputs(coords);
629        if !self.force_run
630            && slot < self.slot_provenance.len()
631            && !self.slot_provenance[slot].intersects(&self.changed_mask)
632        {
633            return self.core.buffer[slot];
634        }
635        self.force_run = false;
636        let code_fn = self.code_fn;
637        let buf_const = self.core.buffer.as_ptr();
638        let buf_mut = self.core.buffer.as_mut_ptr();
639        let sc = self.core.scratch.as_mut_ptr();
640        self.core.run(move || unsafe {
641            (code_fn)(buf_const, buf_mut, sc);
642        });
643        self.core.buffer[slot]
644    }
645
646    jit_accessors!();
647}
648
649// ── JitKernelPushPull ──────────────────────────────────────
650
651/// Full optimization: push-side dirty tracking + pull-side cone guard.
652#[derive(Clone)]
653#[doc(hidden)]
654pub struct JitKernelPushPull {
655    pub(super) core: JitCore,
656    pub(super) code_fn_prov: super::codegen::NativeProvFn,
657    pub(super) node_clean: Vec<u8>,
658    pub(super) input_dependents: Vec<Vec<usize>>,
659    pub(super) slot_provenance: Vec<ProvMask>,
660    pub(super) changed_mask: ProvMask,
661    /// Set by `set_input`: an extern changed, so the next evaluation
662    /// runs whatever the cone guard says.
663    pub(super) force_run: bool,
664}
665
666impl JitKernelPushPull {
667    #[inline]
668    fn set_inputs(&mut self, coords: &[u64]) {
669        self.changed_mask.clear();
670        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
671            if self.core.buffer[i] != c {
672                self.core.buffer[i] = c;
673                self.changed_mask.set(i);
674                if i < self.input_dependents.len() {
675                    for &step_idx in &self.input_dependents[i] {
676                        self.node_clean[step_idx] = 0;
677                    }
678                }
679            }
680        }
681        // A write makes every never-current step run again (R1.v),
682        // whatever the cone guard would say of the pulled output.
683        if self.core.has_volatile() {
684            for &step_idx in &self.core.volatile_steps {
685                self.node_clean[step_idx] = 0;
686            }
687            self.force_run = true;
688        }
689    }
690
691    /// Every step downstream of the slot reruns, and the next
692    /// evaluation runs whatever the cone guard says.
693    fn mark_input_changed(&mut self, slot: usize) {
694        if slot < self.input_dependents.len() {
695            for &step_idx in &self.input_dependents[slot] {
696                self.node_clean[step_idx] = 0;
697            }
698        }
699        for &step_idx in &self.core.volatile_steps {
700            self.node_clean[step_idx] = 0;
701        }
702        self.force_run = true;
703    }
704
705    /// Evaluate the kernel with the given coordinate values.
706    #[inline]
707    pub fn eval(&mut self, coords: &[u64]) {
708        self.set_inputs(coords);
709        self.force_run = false;
710        let code_fn = self.code_fn_prov;
711        let buf_const = self.core.buffer.as_ptr();
712        let buf_mut = self.core.buffer.as_mut_ptr();
713        let sc = self.core.scratch.as_mut_ptr();
714        let clean_mut = self.node_clean.as_mut_ptr();
715        self.core.run(move || unsafe {
716            (code_fn)(buf_const, buf_mut, sc, clean_mut);
717        });
718    }
719
720    /// Evaluate and return the value at the given buffer slot index,
721    /// applying both push and pull optimizations.
722    #[inline]
723    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
724        self.set_inputs(coords);
725        if !self.force_run
726            && slot < self.slot_provenance.len()
727            && !self.slot_provenance[slot].intersects(&self.changed_mask)
728        {
729            return self.core.buffer[slot];
730        }
731        self.force_run = false;
732        let code_fn = self.code_fn_prov;
733        let buf_const = self.core.buffer.as_ptr();
734        let buf_mut = self.core.buffer.as_mut_ptr();
735        let sc = self.core.scratch.as_mut_ptr();
736        let clean_mut = self.node_clean.as_mut_ptr();
737        self.core.run(move || unsafe {
738            (code_fn)(buf_const, buf_mut, sc, clean_mut);
739        });
740        self.core.buffer[slot]
741    }
742
743    jit_accessors!();
744}
745
746// ── The engine-independent surface (engine_parity.md, step 4) ──────
747
748use crate::compile::select::{Engine, Provenance};
749
750crate::compile::impl_kernel_trait!(JitKernelRaw, Engine::Native(Provenance::Raw));
751crate::compile::impl_kernel_trait!(JitKernelPush, Engine::Native(Provenance::Push));
752crate::compile::impl_kernel_trait!(JitKernelPull, Engine::Native(Provenance::Pull));
753crate::compile::impl_kernel_trait!(JitKernelPushPull, Engine::Native(Provenance::PushPull));