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 two kernel structs (`JitKernelRaw` and `JitKernelPushPull`)
8//! 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/// Shared fields for all JIT kernel variants. A clone is a new state
75/// of the same program: the code and the nodes are shared, everything
76/// else is the clone's own (engines.md §3.5), and every extern
77/// pair in its buffer points into its own storage (axiom S3), never
78/// into the state it was cloned from.
79pub(super) struct JitCore {
80    /// The engine this kernel runs, as it reports it: the tier and
81    /// the provenance mode it was built with. State rather than a
82    /// property of the type, so one kernel type can serve a tier
83    /// that runs native code and one that runs none.
84    pub(super) engine: crate::compile::select::Engine,
85    pub(super) buffer: Vec<u64>,
86    pub(super) coord_count: usize,
87    pub(super) output_map: HashMap<String, usize>,
88    /// Slots the raw readers refuse: `Ref2` pairs (axiom S2). Set by
89    /// the assembler once the layout is known; empty means no such
90    /// slot.
91    pub(super) guard_slots: Vec<bool>,
92    /// Port type of each named output, for `get_value`'s decode.
93    pub(super) output_types: HashMap<String, crate::ast::PortType>,
94    /// The extern inputs, written through at every set.
95    pub(super) externs: crate::compile::externs::Externs,
96    /// The traversals the program declares (SRD 113), opened through the
97    /// `Kernel` trait.
98    pub(super) traversals: std::sync::Arc<[crate::dsl::traversal::Traversal]>,
99    pub(super) _module: JitCode,
100    /// Whether the code calls a helper, and so runs under the catch.
101    pub(super) fallible: bool,
102    pub(super) _nodes: std::sync::Arc<Vec<Box<dyn PolydatNode>>>,
103    /// The coordinates set through the `Kernel` trait, pending
104    /// evaluation.
105    pub(super) drive: crate::compile::Drive,
106    /// Where each step came from, for the failure path (A7).
107    pub(super) sites: std::sync::Arc<crate::compile::Attribution>,
108    /// The slot past the layout where native code names the step it
109    /// is in before calling a helper; `u64::MAX` before any.
110    pub(super) tracker: usize,
111    /// The scratch entries the steps' kits write into, owned by this
112    /// state (axiom S3); native code receives the base pointer.
113    pub(super) scratch: Vec<crate::ast::ScratchBuf>,
114    /// Axiom S9(a): (first slot of a Ref pair → scratch index) for
115    /// every scratch-backed Ref output.
116    pub(super) ref_scratch: Vec<(usize, usize)>,
117    /// The steps that are never current (runtime_model.md, R1.v): a
118    /// nondeterministic node or one downstream of it. Every write
119    /// clears their clean flags, so the next pull whose cone holds one
120    /// runs it again.
121    pub(super) volatile_steps: Vec<usize>,
122    /// The fusion units, and which of them each output slot's cone
123    /// holds.
124    pub(super) cones: ConePlan,
125    /// A clean flag per unit: set when the unit runs, cleared by a write
126    /// to an input in its provenance (runtime_model.md R2).
127    pub(super) unit_clean: Vec<u8>,
128    /// The units of the never-current steps, cleared at every write.
129    pub(super) volatile_units: Vec<usize>,
130    /// The program's one function, taking a list of units to run.
131    pub(super) entry: super::codegen::NativeDispatchFn,
132    /// Each output by index, as the host names it: its slot and type,
133    /// filled on the first pull by index so a pull does not look an
134    /// output up by name.
135    pub(super) outputs_at: Vec<(usize, crate::ast::PortType)>,
136}
137
138/// What a pull on this tier runs: the fusion units of its output's
139/// cone, in order, as a compiled kernel walks an output's precomputed
140/// cone order (runtime_model.md R2). The units are the planner's
141/// (`compile::fusion_units`): connected, convex groups of steps, each
142/// one block of the program's function. A pull hands the function its
143/// cone's units, and the function runs the ones that are not current
144/// and nothing else, so its cost is its cone's and not the program's.
145/// Every output's cone is found at build, into a table by slot, so a
146/// pull indexes it rather than searching or hashing.
147#[derive(Clone, Default)]
148pub(super) struct ConePlan {
149    /// Each step's input slots.
150    inputs: std::sync::Arc<[Box<[usize]>]>,
151    /// The step that writes each slot, or `usize::MAX` for a slot no
152    /// step writes (an input or an extern).
153    producer: std::sync::Arc<[usize]>,
154    /// The unit each step belongs to.
155    unit_of: std::sync::Arc<[u32]>,
156    /// Each unit's steps.
157    members: std::sync::Arc<[Box<[usize]>]>,
158    /// Every unit, in order, for a full evaluation.
159    all: std::sync::Arc<[u32]>,
160    /// Each slot's cone, by the slot it ends in: the units it holds, in
161    /// order. Every output's is found at build; another slot's, one a
162    /// raw read by slot asks for, when first asked.
163    by_slot: Vec<Option<std::sync::Arc<[u32]>>>,
164}
165
166impl ConePlan {
167    pub(super) fn new(
168        steps: &[(super::codegen::JitOp, Vec<usize>, Vec<usize>)],
169        slots: usize,
170        units: &crate::compile::fusion_units::UnitPlan,
171        outputs: impl IntoIterator<Item = usize>,
172    ) -> Self {
173        let mut producer = vec![usize::MAX; slots + 1];
174        for (i, (_, _, outs)) in steps.iter().enumerate() {
175            for &s in outs {
176                if s < producer.len() {
177                    producer[s] = i;
178                }
179            }
180        }
181        let mut plan = ConePlan {
182            inputs: steps
183                .iter()
184                .map(|(_, ins, _)| ins.clone().into_boxed_slice())
185                .collect(),
186            producer: producer.into(),
187            unit_of: units.unit_of.iter().map(|&u| u as u32).collect(),
188            members: units
189                .units
190                .iter()
191                .map(|m| m.clone().into_boxed_slice())
192                .collect(),
193            all: (0..units.units.len() as u32).collect(),
194            by_slot: vec![None; slots + 1],
195        };
196        for slot in outputs {
197            plan.of(slot);
198        }
199        plan
200    }
201
202    /// The number of units.
203    pub(super) fn unit_count(&self) -> usize {
204        self.all.len()
205    }
206
207    /// The unit a step belongs to.
208    pub(super) fn unit_of(&self, step: usize) -> usize {
209        self.unit_of[step] as usize
210    }
211
212    /// Every unit, in order.
213    pub(super) fn all(&self) -> &[u32] {
214        &self.all
215    }
216
217    /// The units of the cone `slot` depends on, in order, found once.
218    /// A unit runs whole, so the set is closed over every member's
219    /// producers, not only the producers of the steps the output reads:
220    /// a member outside the cone still runs, and its inputs must be
221    /// current when it does.
222    #[inline]
223    pub(super) fn of(&mut self, slot: usize) -> &[u32] {
224        if self.by_slot.get(slot).is_none_or(|c| c.is_none()) {
225            self.find(slot);
226        }
227        self.by_slot[slot].as_deref().unwrap_or(&[])
228    }
229
230    #[cold]
231    fn find(&mut self, slot: usize) {
232        if slot >= self.by_slot.len() {
233            self.by_slot.resize(slot + 1, None);
234        }
235        {
236            let mut unit_seen = vec![false; self.members.len()];
237            let producer_of = |s: usize| self.producer.get(s).copied().filter(|&p| p != usize::MAX);
238            let mut stack: Vec<usize> = producer_of(slot).into_iter().collect();
239            let mut units: Vec<u32> = Vec::new();
240            while let Some(step) = stack.pop() {
241                let unit = self.unit_of[step];
242                if unit_seen[unit as usize] {
243                    continue;
244                }
245                unit_seen[unit as usize] = true;
246                units.push(unit);
247                for &m in self.members[unit as usize].iter() {
248                    stack.extend(self.inputs[m].iter().filter_map(|&s| producer_of(s)));
249                }
250            }
251            // Unit numbers are in dependency order, so sorted is a valid
252            // order to run them in.
253            units.sort_unstable();
254            units.dedup();
255            self.by_slot[slot] = Some(units.into());
256        }
257    }
258}
259
260impl Clone for JitCore {
261    fn clone(&self) -> Self {
262        let mut core = JitCore {
263            engine: self.engine,
264            buffer: self.buffer.clone(),
265            coord_count: self.coord_count,
266            output_map: self.output_map.clone(),
267            guard_slots: self.guard_slots.clone(),
268            output_types: self.output_types.clone(),
269            externs: self.externs.clone(),
270            traversals: self.traversals.clone(),
271            _module: self._module.clone(),
272            fallible: self.fallible,
273            _nodes: self._nodes.clone(),
274            drive: self.drive.clone(),
275            sites: self.sites.clone(),
276            tracker: self.tracker,
277            scratch: self.scratch.clone(),
278            ref_scratch: self.ref_scratch.clone(),
279            volatile_steps: self.volatile_steps.clone(),
280            cones: self.cones.clone(),
281            unit_clean: self.unit_clean.clone(),
282            volatile_units: self.volatile_units.clone(),
283            entry: self.entry,
284            outputs_at: self.outputs_at.clone(),
285        };
286        // Every pair points into this state's own storage (axiom S3):
287        // a step's scratch entry, the value an extern stores.
288        for &(slot, idx) in &core.ref_scratch {
289            let (p, l) = core.scratch[idx].ptr_len();
290            core.buffer[slot] = p;
291            core.buffer[slot + 1] = l;
292        }
293        core.externs.seed(&mut core.buffer, None);
294        core
295    }
296}
297
298impl JitCore {
299    /// Nothing to mark here: a cell another holder published to is
300    /// handled where the pending write is applied, where the kernels
301    /// mark every step dirty, since the dependents lists do not name a
302    /// cell's readers.
303    fn dirty_input(&mut self, _slot: usize) {}
304
305    /// The program's identity: the node list, which every kernel created
306    /// from the program and every fork shares, and no other program has.
307    fn program_identity(&self) -> usize {
308        std::sync::Arc::as_ptr(&self._nodes) as *const () as usize
309    }
310
311    /// The pure tier broadcasts nothing. It is the differential oracle
312    /// behind the hybrid and Tier-1's carrier, not a surface a host
313    /// composes under (engines.md §1, §8), so no descendant binds to
314    /// one of its outputs and it makes no cell to bind to.
315    fn output_cell_for(&self, _name: &str) -> Option<crate::kernel::SharedCell> {
316        None
317    }
318
319    /// Axiom S2 typed accessor core (borrow ties to `&self`), as the
320    /// closure tier and the hybrid have it. The pure tier owns the
321    /// same scratch and the same `(slot → entry)` map, so the typed
322    /// borrows read the same way here; `guard_slots` is this core's
323    /// name for the per-slot `Ref2` mask.
324    fn ref_entry(&self, slot: usize) -> &crate::ast::ScratchBuf {
325        match self.ref_scratch.iter().find(|(s, _)| *s == slot) {
326            Some(&(_, idx)) => &self.scratch[idx],
327            None if self.guard_slots.get(slot).copied().unwrap_or(false) => panic!(
328                "slot {slot} is a Ref pair owned by the CALLER (a kernel \
329                 input) — read it on the caller side"
330            ),
331            None => panic!("slot {slot} is not a Ref2-colored slot"),
332        }
333    }
334
335    /// The value at `slot` decoded as `ty`, a pair copied out.
336    pub(super) fn slot_value(&self, slot: usize, ty: crate::ast::PortType) -> crate::ast::Value {
337        crate::compile::marshal::decode_output(&self.buffer, slot, ty)
338    }
339
340    /// One native function is the program.
341    pub(super) fn plan(&self) -> crate::EnginePlan {
342        crate::EnginePlan {
343            native_segments: 1,
344            ..Default::default()
345        }
346    }
347
348    /// The next pull or evaluation applies the pending write first.
349    pub(super) fn invalidate_all(&mut self) {
350        self.drive.stale = true;
351    }
352
353    #[allow(clippy::too_many_arguments)]
354    pub(super) fn new(
355        total_slots: usize,
356        coord_count: usize,
357        output_map: HashMap<String, usize>,
358        code: JitCode,
359        nodes: Vec<Box<dyn PolydatNode>>,
360        scratch: ScratchPlan,
361        volatile_steps: Vec<usize>,
362        entry: super::codegen::NativeDispatchFn,
363        cones: ConePlan,
364    ) -> Self {
365        let mut volatile_units: Vec<usize> =
366            volatile_steps.iter().map(|&s| cones.unit_of(s)).collect();
367        volatile_units.sort_unstable();
368        volatile_units.dedup();
369        let unit_count = cones.unit_count();
370        let mut core = Self {
371            // The pure tier, not `Native`: this core belongs to a
372            // kernel that refused every node without a native lowering
373            // rather than running its closure, and `engine()` reports
374            // what ran. The raw builder overwrites the mode.
375            engine: crate::compile::select::Engine::PureNative(
376                crate::compile::select::Provenance::PushPull,
377            ),
378            buffer: vec![0u64; total_slots + 1],
379            coord_count,
380            output_map,
381            guard_slots: Vec::new(),
382            output_types: HashMap::new(),
383            externs: crate::compile::externs::Externs::coordinates_only(coord_count),
384            traversals: Vec::new().into(),
385            fallible: code.fallible(),
386            _module: code,
387            _nodes: std::sync::Arc::new(nodes),
388            drive: crate::compile::Drive::default(),
389            sites: std::sync::Arc::default(),
390            tracker: total_slots,
391            scratch: scratch
392                .elems
393                .iter()
394                .map(|e| crate::ast::ScratchBuf::new(*e))
395                .collect(),
396            ref_scratch: scratch.refs,
397            volatile_steps,
398            cones,
399            unit_clean: vec![0u8; unit_count],
400            volatile_units,
401            entry,
402            outputs_at: Vec::new(),
403        };
404        // Every pair names its own entry from the start (axiom S3), as
405        // a clone's do. A pull runs only its cone, so a step outside
406        // every cone pulled so far has not run, and its pair must still
407        // name its entry, empty until the step writes it (S9(a)).
408        for &(slot, idx) in &core.ref_scratch {
409            let (p, l) = core.scratch[idx].ptr_len();
410            core.buffer[slot] = p;
411            core.buffer[slot + 1] = l;
412        }
413        core
414    }
415
416    /// Run the units of `slot`'s cone that are not current, or of the
417    /// whole program for `None`. The function is handed the cone's
418    /// precomputed order and the clean flags: it tests each unit's flag
419    /// itself, runs the stale ones, and marks each current as it ends.
420    #[inline]
421    pub(super) fn run_units(&mut self, slot: Option<usize>) {
422        let units: &[u32] = match slot {
423            Some(s) => self.cones.of(s),
424            None => self.cones.all(),
425        };
426        // The order lives behind an `Arc` the run does not touch, so
427        // its address holds across the call.
428        let list = units.as_ptr();
429        let len = units.len() as u64;
430        let entry = self.entry;
431        let buf_const = self.buffer.as_ptr();
432        let buf_mut = self.buffer.as_mut_ptr();
433        let sc = self.scratch.as_mut_ptr();
434        let clean = self.unit_clean.as_mut_ptr();
435        self.run(move || unsafe {
436            (entry)(buf_const, buf_mut, sc, list, len, clean);
437        });
438    }
439
440    /// Every unit is dirty: a new round, or a cell another holder
441    /// published to, whose readers no dependents list names.
442    pub(super) fn dirty_all_units(&mut self) {
443        self.unit_clean.fill(0);
444    }
445
446    /// The units that hold volatile steps are dirty again: every read
447    /// does this (runtime_model.md R1.v). Volatile and non-volatile
448    /// nodes never share a unit, so the steps upstream of a volatile
449    /// step keep their currency.
450    pub(super) fn dirty_volatile_units(&mut self) {
451        for &u in &self.volatile_units {
452            self.unit_clean[u] = 0;
453        }
454    }
455
456    /// The output at `index` in the host's order: its slot and type.
457    fn output_at(&mut self, index: usize) -> (usize, crate::ast::PortType) {
458        if self.outputs_at.is_empty() {
459            self.outputs_at = self
460                .externs
461                .output_names()
462                .iter()
463                .map(|n| {
464                    let slot = self.output_map[n];
465                    let ty = self
466                        .output_types
467                        .get(n)
468                        .copied()
469                        .unwrap_or(crate::ast::PortType::U64);
470                    (slot, ty)
471                })
472                .collect();
473        }
474        *self
475            .outputs_at
476            .get(index)
477            .unwrap_or_else(|| panic!("no output at index {index}"))
478    }
479
480    /// Whether the program has a volatile step, which every read
481    /// re-evaluates (R1.v).
482    #[inline]
483    fn has_volatile(&self) -> bool {
484        !self.volatile_steps.is_empty()
485    }
486
487    /// Axiom S9(a): every scratch-backed pair in the buffer names its
488    /// own entry, checked after a run in debug builds.
489    #[cfg(debug_assertions)]
490    fn validate_refs(&self) {
491        for &(slot, idx) in &self.ref_scratch {
492            let (p, l) = self.scratch[idx].ptr_len();
493            assert!(
494                self.buffer[slot] == p && self.buffer[slot + 1] == l,
495                "S9 ref-validator: slot pair ({slot}, {}) = ({:#x}, {}) does not match \
496                 scratch[{idx}] = ({p:#x}, {l})",
497                slot + 1,
498                self.buffer[slot],
499                self.buffer[slot + 1],
500            );
501        }
502    }
503
504    /// Install the extern inputs, written through into the buffer now.
505    pub(super) fn set_externs(&mut self, externs: crate::compile::externs::Externs) {
506        externs.seed(&mut self.buffer, None);
507        self.externs = externs;
508    }
509
510    /// Set an extern by name; returns its slot for dirty marking.
511    fn set_extern(
512        &mut self,
513        name: &str,
514        value: crate::ast::Value,
515    ) -> Result<usize, crate::kernel::WriteError> {
516        Ok(self.externs.set(name, value, &mut self.buffer)?.0)
517    }
518
519    /// [`Self::set_extern`] by input index.
520    fn set_extern_at(
521        &mut self,
522        index: usize,
523        value: crate::ast::Value,
524    ) -> Result<usize, crate::kernel::WriteError> {
525        Ok(self.externs.set_at(index, value, &mut self.buffer)?.0)
526    }
527
528    /// Bind a `shared` binding to `cell` (engine parity, step 9). The
529    /// next pull or evaluation reads it.
530    fn attach_cell(&mut self, name: &str, cell: crate::kernel::SharedCell) -> Result<(), String> {
531        self.externs.attach_cell(name, cell)?;
532        self.drive.stale = true;
533        Ok(())
534    }
535
536    /// Run one native evaluation: take what cells other holders
537    /// published, refuse an unset extern (native code cannot carry a
538    /// `None`; engines.md §3.3), run inside the longjmp catch.
539    #[inline]
540    pub(super) fn run(&mut self, native: impl FnOnce()) {
541        if self.externs.cells_dirty() {
542            self.externs.refresh_cells(&mut self.buffer);
543        }
544        if let Some((name, ty)) = self.externs.first_unset() {
545            panic!(
546                "extern '{name}' ({ty}) has no value on the pure native tier, which \
547                 cannot carry a `None`: every step is native code and there is no \
548                 closure to propagate one through. Either it was declared without a \
549                 default and never set, or a host cleared it after the build. Set it \
550                 with set_input before pulling, or run this program on `native`, which \
551                 answers a cleared extern with `None` as the interpreter does \
552                 (docs/design/engines.md §3.3)"
553            );
554        }
555        // Code that calls no helper cannot fail: it runs bare. Otherwise
556        // native code names the step it is in before each helper call;
557        // a failure before any names none. The capture guard is armed
558        // for the run, so the helper's panic is recorded quietly and
559        // re-raised enriched, as the interpreter re-raises a node's (A7).
560        if !self.fallible {
561            native();
562        } else {
563            self.buffer[self.tracker] = u64::MAX;
564            let capture = crate::kernel::engines::EvalPanicCaptureGuard::arm();
565            let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
566                super::codegen::invoke_with_catch(native)
567            }));
568            drop(capture);
569            if let Err(payload) = outcome {
570                let step = self.buffer[self.tracker] as usize;
571                let sites = std::sync::Arc::clone(&self.sites);
572                sites.reraise(payload, step, &self.buffer, None);
573            }
574        }
575        #[cfg(debug_assertions)]
576        self.validate_refs();
577    }
578
579    /// The compile-constant fold of the runtime model on this tier: a
580    /// step no input reaches runs at build, once, and is current from
581    /// then on, so what is knowable at build is known at build and
582    /// fails at build.
583    ///
584    /// The other two compiled tiers keep a step list and run the
585    /// constant steps out of it. This tier has one compiled function
586    /// and no list, so the constant steps are compiled a second time
587    /// into an entry of their own, run once over this core's buffer and
588    /// scratch, and dropped with the code that held them. The steps
589    /// carry absolute slot indices, so the entry writes the same slots
590    /// the whole-program function would have.
591    ///
592    /// Externs are not consulted: a compile-constant step is one no
593    /// input reaches, extern inputs included, so a program whose
594    /// externs are still unset folds its constants anyway. That is the
595    /// difference from [`Self::run`], which refuses an unset extern
596    /// because a real evaluation reads them.
597    pub(super) fn fold_constants(
598        &mut self,
599        folded: &[(super::codegen::JitOp, Vec<usize>, Vec<usize>)],
600        origin: &[usize],
601        total_slots: usize,
602    ) -> Result<(), crate::KernelError> {
603        if folded.is_empty() {
604            return Ok(());
605        }
606        // Graph order is topological and a constant depends on
607        // constants alone, so the filtered order is a valid order.
608        let (code_fn, code) = super::codegen::compile_jit_entry(folded, Some(total_slots))
609            .map_err(|reason| crate::KernelError::ConstantFold { reason })?;
610        let buf_ptr_const = self.buffer.as_ptr();
611        let buf_ptr_mut = self.buffer.as_mut_ptr();
612        let sc = self.scratch.as_mut_ptr();
613        let native = move || unsafe {
614            (code_fn)(buf_ptr_const, buf_ptr_mut, sc);
615        };
616        if !code.fallible() {
617            native();
618        } else {
619            self.buffer[self.tracker] = u64::MAX;
620            let capture = crate::kernel::engines::EvalPanicCaptureGuard::arm();
621            let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
622                super::codegen::invoke_with_catch(native)
623            }));
624            drop(capture);
625            if let Err(payload) = outcome {
626                // The entry counts its own steps, so the tracker holds
627                // an index into `folded`; the attribution is keyed by
628                // the program's step, which `origin` gives back.
629                let step = self.buffer[self.tracker] as usize;
630                let step = origin.get(step).copied().unwrap_or(step);
631                let sites = std::sync::Arc::clone(&self.sites);
632                return Err(crate::KernelError::ConstantFold {
633                    reason: sites.describe(payload, step, &self.buffer, None),
634                });
635            }
636        }
637        // `code` owns the executable memory the call ran in, so it is
638        // kept alive to here and dropped after, not before.
639        drop(code);
640        Ok(())
641    }
642}
643
644macro_rules! jit_accessors {
645    () => {
646        crate::compile::ref_readers!();
647
648        /// Returns the number of coordinate inputs this kernel accepts.
649        pub fn coord_count(&self) -> usize {
650            self.core.externs.coordinate_count()
651        }
652
653        /// Returns the buffer slot index for the named output, if present.
654        pub fn resolve_output(&self, name: &str) -> Option<usize> {
655            self.core.output_map.get(name).copied()
656        }
657
658        /// Returns the raw u64 value stored in the named output slot.
659        #[inline]
660        pub fn get(&self, name: &str) -> u64 {
661            self.get_slot(self.core.output_map[name])
662        }
663
664        /// Returns the raw u64 value stored at the given buffer slot
665        /// index. Refuses a `Ref2` slot (axiom S2): read those
666        /// through [`Self::get_value`].
667        #[inline]
668        pub fn get_slot(&self, slot: usize) -> u64 {
669            if self.core.guard_slots.get(slot).copied().unwrap_or(false) {
670                panic!(
671                    "slot {slot} is Ref2-colored; a raw u64 read would leak an interior \
672                     address. Use get_value to decode it."
673                );
674            }
675            self.core.buffer[slot]
676        }
677
678        /// The named output as a typed `Value`, decoded by its port type:
679        /// a reference pair is copied out, so the caller never holds a
680        /// reference into the buffer.
681        pub fn get_value(&self, name: &str) -> crate::ast::Value {
682            let slot = self.core.output_map[name];
683            let ty = self
684                .core
685                .output_types
686                .get(name)
687                .copied()
688                .unwrap_or(crate::ast::PortType::U64);
689            crate::compile::marshal::decode_output(&self.core.buffer, slot, ty)
690        }
691
692        /// Record the slots raw readers must refuse and each output's
693        /// port type. Called by the assembler after construction.
694        pub(crate) fn set_slot_info(
695            &mut self,
696            guard_slots: Vec<bool>,
697            output_types: HashMap<String, crate::ast::PortType>,
698        ) {
699            self.core.guard_slots = guard_slots;
700            self.core.output_types = output_types;
701        }
702
703        /// Where each step came from, for the failure path (A7).
704        pub(crate) fn set_attribution(
705            &mut self,
706            sites: std::sync::Arc<crate::compile::Attribution>,
707        ) {
708            self.core.sites = sites;
709        }
710
711        /// Run this program's compile-constant steps once, at build; see
712        /// [`JitCore::fold_constants`]. Called by the assembler after
713        /// the attribution is in place, so a constant that fails names
714        /// its node.
715        pub(crate) fn fold_constants(
716            &mut self,
717            folded: &[(super::codegen::JitOp, Vec<usize>, Vec<usize>)],
718            origin: &[usize],
719            total_slots: usize,
720        ) -> Result<(), crate::KernelError> {
721            self.core.fold_constants(folded, origin, total_slots)
722        }
723
724        /// Set an extern by name, as `PolydatState::set_input` does on
725        /// the interpreter. The value must be of the declared port
726        /// type. The value is written through into the buffer at once,
727        /// whatever its color, and every step downstream of the extern
728        /// reruns at the next evaluation.
729        pub fn set_input(
730            &mut self,
731            name: &str,
732            value: crate::ast::Value,
733        ) -> Result<(), crate::kernel::WriteError> {
734            let slot = self.core.set_extern(name, value)?;
735            self.mark_input_changed(slot);
736            Ok(())
737        }
738
739        /// [`Self::set_input`] by input index.
740        pub fn set_input_at(
741            &mut self,
742            index: usize,
743            value: crate::ast::Value,
744        ) -> Result<(), crate::kernel::WriteError> {
745            let slot = self.core.set_extern_at(index, value)?;
746            self.mark_input_changed(slot);
747            Ok(())
748        }
749
750        /// The kernel's externs by name and declared type.
751        pub fn externs(&self) -> Vec<(&str, crate::ast::PortType)> {
752            self.core.externs.names()
753        }
754
755        /// Every step downstream of a coordinate reruns at the next
756        /// evaluation: the state a kernel created from a shared program
757        /// starts in.
758        fn mark_all_dirty(&mut self) {
759            for i in 0..self.core.coord_count {
760                self.mark_input_changed(i);
761            }
762        }
763
764        /// The named output through the `Kernel` trait: the pending
765        /// writes are applied and the output's cone runs, and nothing
766        /// else (engines.md §3.1).
767        fn pull_value(&mut self, name: &str) -> crate::ast::Value {
768            let slot = self.core.output_map[name];
769            let ty = self
770                .core
771                .output_types
772                .get(name)
773                .copied()
774                .unwrap_or(crate::ast::PortType::U64);
775            self.pull_slot(slot, ty)
776        }
777
778        /// [`Self::pull_value`] by output index, through the index's
779        /// slot and type rather than its name.
780        fn pull_value_at(&mut self, index: usize) -> crate::ast::Value {
781            let (slot, ty) = self.core.output_at(index);
782            self.pull_slot(slot, ty)
783        }
784
785        /// `eval` through the `Kernel` trait: the pending coordinates.
786        fn eval_pending(&mut self) {
787            let coords = std::mem::take(&mut self.core.drive.coords);
788            self.eval(&coords);
789            self.core.drive.coords = coords;
790        }
791
792        /// The cursors the program declares, with the partitions the
793        /// compiler resolved where its `over` clause and extent were
794        /// constant, as `PolydatProgram::cursor_schemas` reports them.
795        pub fn cursor_schemas(&self) -> &[crate::iteration::source::SourceSchema] {
796            self.core.externs.cursor_schemas()
797        }
798
799        /// Narrow a cursor to one partition, as `narrow_cursor` does on
800        /// the interpreter: its `Ext` slot and six scalar projections
801        /// are set as externs.
802        pub fn set_cursor(
803            &mut self,
804            name: &str,
805            partition: &crate::iteration::cursor_partition::Partition,
806        ) -> Result<(), crate::kernel::WriteError> {
807            for (slot, value) in self.core.externs.cursor_writes(name, partition)? {
808                self.set_input(&slot, value)?;
809            }
810            Ok(())
811        }
812    };
813}
814
815// ── JitKernelRaw ───────────────────────────────────────────
816
817/// Raw JIT kernel: no provenance. Every write begins a round in which
818/// every unit is dirty; `eval` runs them all, and a pull runs its
819/// output's cone, each unit at most once in the round.
820#[derive(Clone)]
821#[doc(hidden)]
822pub struct JitKernelRaw {
823    pub(super) core: JitCore,
824}
825
826impl JitKernelRaw {
827    /// A pull through the `Kernel` trait: a pending write begins a
828    /// round, then the output's cone runs.
829    fn pull_slot(&mut self, slot: usize, ty: crate::ast::PortType) -> crate::ast::Value {
830        if self.core.drive.stale || self.core.externs.cells_dirty() {
831            let coords = std::mem::take(&mut self.core.drive.coords);
832            self.write_coords(&coords);
833            self.core.drive.coords = coords;
834            self.core.drive.stale = false;
835            self.core.dirty_all_units();
836        } else if self.core.has_volatile() {
837            self.core.dirty_volatile_units();
838        }
839        self.core.run_units(Some(slot));
840        self.core.slot_value(slot, ty)
841    }
842
843    /// The coordinates, written into the buffer.
844    #[inline]
845    fn write_coords(&mut self, coords: &[u64]) {
846        // Written one by one, as the other kernels write them: a slice
847        // copy of a runtime length is a call to memcpy, which costs
848        // more than the three stores it replaces.
849        for (i, &c) in coords
850            .iter()
851            .enumerate()
852            .take(self.core.externs.coordinate_slots())
853        {
854            if self.core.buffer[i] != c {
855                self.core.buffer[i] = c;
856            }
857        }
858    }
859    /// Evaluate the kernel with the given coordinate values.
860    ///
861    /// Predicate violations (`is_positive`, `in_range`,
862    /// `is_one_of`) from JIT-lowered code surface as normal
863    /// Rust panics carrying the violation message. The
864    /// longjmp wrapper in `super::codegen::invoke_with_catch`
865    /// handles the transition back to Rust land when the code
866    /// calls a helper; code that calls none cannot fail and
867    /// runs bare.
868    #[inline]
869    pub fn eval(&mut self, coords: &[u64]) {
870        self.write_coords(coords);
871        self.core.dirty_all_units();
872        self.core.run_units(None);
873    }
874
875    /// Evaluate and return the value at the given buffer slot index.
876    #[inline]
877    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
878        self.eval(coords);
879        self.core.buffer[slot]
880    }
881
882    /// A write begins a round: every unit is dirty again.
883    fn mark_input_changed(&mut self, _slot: usize) {
884        self.core.dirty_all_units();
885    }
886
887    jit_accessors!();
888}
889
890// ── JitKernelPushPull ──────────────────────────────────────
891
892/// Full optimization: push-side dirty tracking + pull-side cone guard.
893#[derive(Clone)]
894#[doc(hidden)]
895pub struct JitKernelPushPull {
896    pub(super) core: JitCore,
897    /// Per input slot, the units that read it, directly or not.
898    pub(super) input_dependents: Vec<Vec<usize>>,
899    pub(super) slot_provenance: Vec<ProvMask>,
900    pub(super) changed_mask: ProvMask,
901    /// Set by `set_input`: an extern changed, so the next evaluation
902    /// runs whatever the cone guard says.
903    pub(super) force_run: bool,
904}
905
906impl JitKernelPushPull {
907    #[inline]
908    fn set_inputs(&mut self, coords: &[u64]) {
909        self.changed_mask.clear();
910        for (i, &c) in coords
911            .iter()
912            .enumerate()
913            .take(self.core.externs.coordinate_slots())
914        {
915            if self.core.buffer[i] != c {
916                self.core.buffer[i] = c;
917                self.changed_mask.set(i);
918                self.dirty_dependents(i);
919            }
920        }
921        // A write makes every never-current unit run again (R1.v),
922        // whatever the cone guard would say of the pulled output.
923        if self.core.has_volatile() {
924            self.core.dirty_volatile_units();
925            self.force_run = true;
926        }
927    }
928
929    /// The units downstream of an input slot are dirty.
930    #[inline]
931    fn dirty_dependents(&mut self, slot: usize) {
932        if let Some(units) = self.input_dependents.get(slot) {
933            for &u in units {
934                self.core.unit_clean[u] = 0;
935            }
936        }
937    }
938
939    /// Every unit downstream of the slot reruns, and the next
940    /// evaluation runs whatever the cone guard says.
941    fn mark_input_changed(&mut self, slot: usize) {
942        self.dirty_dependents(slot);
943        self.core.dirty_volatile_units();
944        self.force_run = true;
945    }
946
947    /// A pull through the `Kernel` trait: the pending coordinates dirty
948    /// their dependents, then the output's cone runs its dirty units.
949    fn pull_slot(&mut self, slot: usize, ty: crate::ast::PortType) -> crate::ast::Value {
950        if self.core.drive.stale {
951            let coords = std::mem::take(&mut self.core.drive.coords);
952            self.set_inputs(&coords);
953            self.core.drive.coords = coords;
954            self.core.drive.stale = false;
955        }
956        // A cell another holder published to is a changed input whose
957        // readers the dependents lists do not name: every unit reruns.
958        if self.core.externs.cells_dirty() {
959            self.core.dirty_all_units();
960        }
961        // Every read re-evaluates the volatile units its cone reaches.
962        if self.core.has_volatile() {
963            self.core.dirty_volatile_units();
964        }
965        self.core.run_units(Some(slot));
966        self.core.slot_value(slot, ty)
967    }
968
969    /// Evaluate the kernel with the given coordinate values.
970    #[inline]
971    pub fn eval(&mut self, coords: &[u64]) {
972        self.set_inputs(coords);
973        self.force_run = false;
974        self.core.run_units(None);
975    }
976
977    /// Evaluate and return the value at the given buffer slot index,
978    /// applying both push and pull optimizations.
979    #[inline]
980    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
981        self.set_inputs(coords);
982        if !self.force_run
983            && slot < self.slot_provenance.len()
984            && !self.slot_provenance[slot].intersects(&self.changed_mask)
985        {
986            return self.core.buffer[slot];
987        }
988        self.force_run = false;
989        self.core.run_units(Some(slot));
990        self.core.buffer[slot]
991    }
992
993    jit_accessors!();
994}
995
996// ── The engine-independent surface (engines.md §3.5) ──────
997
998crate::compile::impl_kernel_trait!(JitKernelRaw);
999crate::compile::impl_kernel_trait!(JitKernelPushPull);
1000crate::compile::impl_slot_kernel!(JitKernelRaw);
1001crate::compile::impl_slot_kernel!(JitKernelPushPull);