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