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 `Ref2` slot (axiom S2): read those
315        /// 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. The value is written through into the buffer at once,
363        /// whatever its color, and every step downstream of the extern
364        /// reruns at the next evaluation.
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 when the code
469    /// calls a helper; code that calls none cannot fail and
470    /// runs bare.
471    #[inline]
472    pub fn eval(&mut self, coords: &[u64]) {
473        // Written one by one, as the other kernels write them: a slice
474        // copy of a runtime length is a call to memcpy, which costs
475        // more than the three stores it replaces.
476        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
477            if self.core.buffer[i] != c {
478                self.core.buffer[i] = c;
479            }
480        }
481        let code_fn = self.code_fn;
482        let buf_ptr_const = self.core.buffer.as_ptr();
483        let buf_ptr_mut = self.core.buffer.as_mut_ptr();
484        let sc = self.core.scratch.as_mut_ptr();
485        self.core.run(move || unsafe {
486            (code_fn)(buf_ptr_const, buf_ptr_mut, sc);
487        });
488    }
489
490    /// Evaluate and return the value at the given buffer slot index.
491    #[inline]
492    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
493        self.eval(coords);
494        self.core.buffer[slot]
495    }
496
497    /// Decompose into raw parts for hybrid kernel integration: the
498    /// entry point and its module.
499    pub fn into_parts(self) -> JitParts {
500        (self.code_fn, self.core._module)
501    }
502
503    /// Every run evaluates everything; a changed input needs no mark.
504    fn mark_input_changed(&mut self, _slot: usize) {}
505
506    jit_accessors!();
507}
508
509// ── JitKernelPush ──────────────────────────────────────────
510
511/// Push-only JIT kernel: per-node dirty tracking, no cone guard.
512#[derive(Clone)]
513#[doc(hidden)]
514pub struct JitKernelPush {
515    pub(super) core: JitCore,
516    pub(super) code_fn_prov: super::codegen::NativeProvFn,
517    pub(super) node_clean: Vec<u8>,
518    pub(super) input_dependents: Vec<Vec<usize>>,
519}
520
521impl JitKernelPush {
522    #[inline]
523    fn set_inputs(&mut self, coords: &[u64]) {
524        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
525            if self.core.buffer[i] != c {
526                self.core.buffer[i] = c;
527                self.mark_input_changed(i);
528            }
529        }
530        // A write makes every never-current step run again (R1.v).
531        for &step_idx in &self.core.volatile_steps {
532            self.node_clean[step_idx] = 0;
533        }
534    }
535
536    /// Every step downstream of the slot reruns, and every
537    /// never-current step with it (R1.v).
538    fn mark_input_changed(&mut self, slot: usize) {
539        if slot < self.input_dependents.len() {
540            for &step_idx in &self.input_dependents[slot] {
541                self.node_clean[step_idx] = 0;
542            }
543        }
544        for &step_idx in &self.core.volatile_steps {
545            self.node_clean[step_idx] = 0;
546        }
547    }
548
549    /// Evaluate the kernel with the given coordinate values.
550    #[inline]
551    pub fn eval(&mut self, coords: &[u64]) {
552        self.set_inputs(coords);
553        let code_fn = self.code_fn_prov;
554        let buf_const = self.core.buffer.as_ptr();
555        let buf_mut = self.core.buffer.as_mut_ptr();
556        let sc = self.core.scratch.as_mut_ptr();
557        let clean_mut = self.node_clean.as_mut_ptr();
558        self.core.run(move || unsafe {
559            (code_fn)(buf_const, buf_mut, sc, clean_mut);
560        });
561    }
562
563    /// Evaluate and return the value at the given buffer slot index.
564    #[inline]
565    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
566        self.eval(coords);
567        self.core.buffer[slot]
568    }
569
570    jit_accessors!();
571}
572
573// ── JitKernelPull ──────────────────────────────────────────
574
575/// Pull-only JIT kernel: cone guard, but all nodes run when cone is dirty.
576/// Uses the raw (non-provenance) JIT function — no per-node clean checks.
577#[derive(Clone)]
578#[doc(hidden)]
579pub struct JitKernelPull {
580    pub(super) core: JitCore,
581    pub(super) code_fn: super::codegen::NativeFn,
582    pub(super) slot_provenance: Vec<ProvMask>,
583    pub(super) changed_mask: ProvMask,
584    /// Set by `set_input`: an extern changed, so the next evaluation
585    /// runs whatever the cone guard says.
586    pub(super) force_run: bool,
587}
588
589impl JitKernelPull {
590    #[inline]
591    fn set_inputs(&mut self, coords: &[u64]) {
592        self.changed_mask.clear();
593        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
594            if self.core.buffer[i] != c {
595                self.core.buffer[i] = c;
596                self.changed_mask.set(i);
597            }
598        }
599        // A never-current step runs again after every write (R1.v),
600        // and one native function is the program.
601        if self.core.has_volatile() {
602            self.force_run = true;
603        }
604    }
605
606    /// The next evaluation runs regardless of the cone guard, since
607    /// `set_inputs` rebuilds the changed set from the coordinates alone.
608    fn mark_input_changed(&mut self, _slot: usize) {
609        self.force_run = true;
610    }
611
612    /// Evaluate the kernel with the given coordinate values.
613    #[inline]
614    pub fn eval(&mut self, coords: &[u64]) {
615        self.set_inputs(coords);
616        self.force_run = false;
617        let code_fn = self.code_fn;
618        let buf_const = self.core.buffer.as_ptr();
619        let buf_mut = self.core.buffer.as_mut_ptr();
620        let sc = self.core.scratch.as_mut_ptr();
621        self.core.run(move || unsafe {
622            (code_fn)(buf_const, buf_mut, sc);
623        });
624    }
625
626    /// Evaluate and return the value at the given buffer slot index,
627    /// skipping evaluation if the slot's provenance cone is unaffected.
628    #[inline]
629    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
630        self.set_inputs(coords);
631        if !self.force_run
632            && slot < self.slot_provenance.len()
633            && !self.slot_provenance[slot].intersects(&self.changed_mask)
634        {
635            return self.core.buffer[slot];
636        }
637        self.force_run = false;
638        let code_fn = self.code_fn;
639        let buf_const = self.core.buffer.as_ptr();
640        let buf_mut = self.core.buffer.as_mut_ptr();
641        let sc = self.core.scratch.as_mut_ptr();
642        self.core.run(move || unsafe {
643            (code_fn)(buf_const, buf_mut, sc);
644        });
645        self.core.buffer[slot]
646    }
647
648    jit_accessors!();
649}
650
651// ── JitKernelPushPull ──────────────────────────────────────
652
653/// Full optimization: push-side dirty tracking + pull-side cone guard.
654#[derive(Clone)]
655#[doc(hidden)]
656pub struct JitKernelPushPull {
657    pub(super) core: JitCore,
658    pub(super) code_fn_prov: super::codegen::NativeProvFn,
659    pub(super) node_clean: Vec<u8>,
660    pub(super) input_dependents: Vec<Vec<usize>>,
661    pub(super) slot_provenance: Vec<ProvMask>,
662    pub(super) changed_mask: ProvMask,
663    /// Set by `set_input`: an extern changed, so the next evaluation
664    /// runs whatever the cone guard says.
665    pub(super) force_run: bool,
666}
667
668impl JitKernelPushPull {
669    #[inline]
670    fn set_inputs(&mut self, coords: &[u64]) {
671        self.changed_mask.clear();
672        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
673            if self.core.buffer[i] != c {
674                self.core.buffer[i] = c;
675                self.changed_mask.set(i);
676                if i < self.input_dependents.len() {
677                    for &step_idx in &self.input_dependents[i] {
678                        self.node_clean[step_idx] = 0;
679                    }
680                }
681            }
682        }
683        // A write makes every never-current step run again (R1.v),
684        // whatever the cone guard would say of the pulled output.
685        if self.core.has_volatile() {
686            for &step_idx in &self.core.volatile_steps {
687                self.node_clean[step_idx] = 0;
688            }
689            self.force_run = true;
690        }
691    }
692
693    /// Every step downstream of the slot reruns, and the next
694    /// evaluation runs whatever the cone guard says.
695    fn mark_input_changed(&mut self, slot: usize) {
696        if slot < self.input_dependents.len() {
697            for &step_idx in &self.input_dependents[slot] {
698                self.node_clean[step_idx] = 0;
699            }
700        }
701        for &step_idx in &self.core.volatile_steps {
702            self.node_clean[step_idx] = 0;
703        }
704        self.force_run = true;
705    }
706
707    /// Evaluate the kernel with the given coordinate values.
708    #[inline]
709    pub fn eval(&mut self, coords: &[u64]) {
710        self.set_inputs(coords);
711        self.force_run = false;
712        let code_fn = self.code_fn_prov;
713        let buf_const = self.core.buffer.as_ptr();
714        let buf_mut = self.core.buffer.as_mut_ptr();
715        let sc = self.core.scratch.as_mut_ptr();
716        let clean_mut = self.node_clean.as_mut_ptr();
717        self.core.run(move || unsafe {
718            (code_fn)(buf_const, buf_mut, sc, clean_mut);
719        });
720    }
721
722    /// Evaluate and return the value at the given buffer slot index,
723    /// applying both push and pull optimizations.
724    #[inline]
725    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
726        self.set_inputs(coords);
727        if !self.force_run
728            && slot < self.slot_provenance.len()
729            && !self.slot_provenance[slot].intersects(&self.changed_mask)
730        {
731            return self.core.buffer[slot];
732        }
733        self.force_run = false;
734        let code_fn = self.code_fn_prov;
735        let buf_const = self.core.buffer.as_ptr();
736        let buf_mut = self.core.buffer.as_mut_ptr();
737        let sc = self.core.scratch.as_mut_ptr();
738        let clean_mut = self.node_clean.as_mut_ptr();
739        self.core.run(move || unsafe {
740            (code_fn)(buf_const, buf_mut, sc, clean_mut);
741        });
742        self.core.buffer[slot]
743    }
744
745    jit_accessors!();
746}
747
748// ── The engine-independent surface (engine_parity.md, step 4) ──────
749
750use crate::compile::select::{Engine, Provenance};
751
752crate::compile::impl_kernel_trait!(JitKernelRaw, Engine::Native(Provenance::Raw));
753crate::compile::impl_kernel_trait!(JitKernelPush, Engine::Native(Provenance::Push));
754crate::compile::impl_kernel_trait!(JitKernelPull, Engine::Native(Provenance::Pull));
755crate::compile::impl_kernel_trait!(JitKernelPushPull, Engine::Native(Provenance::PushPull));