Skip to main content

polydat_core/compile/
closures.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! The closure tier: every node runs its generated op over a flat u64
5//! slot buffer, by-reference outputs as `Ref2` pairs into step-owned
6//! scratch (compiled_handles.md).
7//!
8//! Four kernel types, each produced by a distinct compiler path. They
9//! differ in what `set_inputs` marks and whether `eval_for_slot`
10//! consults the cone guard; the shared step loop reads the mode's
11//! `use_clean` flag per step.
12//!
13//! | Type | Push (per-node skip) | Pull (cone guard) |
14//! |------|---------------------|-------------------|
15//! | `CompiledKernelRaw` | — | — |
16//! | `CompiledKernelPush` | yes | — |
17//! | `CompiledKernelPull` | — | yes |
18//! | `CompiledKernelPushPull` | yes | yes |
19
20use std::collections::HashMap;
21
22use crate::ast::{CompiledSlotOp, CompiledU64Op, PortType, ScratchBuf, ScratchElem};
23
24/// What a P2 kernel needs beyond its steps: each named output's port
25/// type for the typed reader, the externs, the provenance, and the
26/// attribution for the failure path.
27#[derive(Default)]
28pub(crate) struct P2Extras {
29    pub(crate) output_types: HashMap<String, PortType>,
30    /// The kernel's extern inputs: seeded at build, host-settable,
31    /// written through at every set (`compile::externs`).
32    pub(crate) externs: crate::compile::externs::Externs,
33    /// Per input slot, coordinates and externs alike, the steps that
34    /// depend on it: the provenance the plan is derived from.
35    pub(crate) input_dependents: Vec<Vec<usize>>,
36    /// Where each step came from, for the failure path (A7).
37    pub(crate) attribution: std::sync::Arc<crate::compile::Attribution>,
38}
39
40/// A single evaluation step in the compiled kernel.
41/// A compiled step's op: pure-scalar u64 closure, or a slot op
42/// with kernel-owned scratch for typed-slice ports
43/// (type_system_alignment.md §4, compiled_handles.md §3).
44pub(crate) enum StepOp {
45    U64(CompiledU64Op),
46    Slot(CompiledSlotOp),
47    /// A slot copy (`identity`, the compiler's `__port_` passthrough),
48    /// run inline: no closure call, no gather.
49    Copy,
50}
51/// One compiled step plus its slice of the scratch arena.
52pub(crate) struct P2Step {
53    /// The node's name, for construction-time diagnostics.
54    pub(crate) name: String,
55    pub(crate) op: StepOp,
56    pub(crate) input_slots: Vec<usize>,
57    pub(crate) output_slots: Vec<usize>,
58    /// Scratch element declarations (consumed by build_core).
59    pub(crate) scratch: Vec<ScratchElem>,
60    /// First slot of each Ref2-colored output port, in port
61    /// order — zipped with the scratch entries to build the
62    /// slot→arena map for axiom S9(a)'s validator and the S2
63    /// accessors.
64    pub(crate) ref_output_starts: Vec<usize>,
65    /// The node handles `None` inputs itself (SRD-74 Rule 2); every
66    /// other node emits `None` when any input is `None` (Rule 1).
67    pub(crate) accepts_none: bool,
68    /// The node is nondeterministic or downstream of one (the runtime
69    /// model's per-cycle invalidation set): never current.
70    pub(crate) volatile: bool,
71    /// No input reaches the node and it is not volatile: the runtime
72    /// model's compile-constant lifecycle, folded at build.
73    pub(crate) constant: bool,
74    /// The node is a side channel: it runs exactly when the interpreter
75    /// would run it, in every provenance mode, because its run is
76    /// observable.
77    pub(crate) side: bool,
78}
79
80struct CompiledStep {
81    op: StepOp,
82    input_slots: Vec<usize>,
83    output_slots: Vec<usize>,
84    scratch_range: (usize, usize),
85    /// SRD-74 Rule 2: the closure runs on `None` inputs.
86    accepts_none: bool,
87    /// Never current: nondeterministic or downstream of one.
88    volatile: bool,
89    /// Compile-constant: folded at build, current from then on.
90    constant: bool,
91    /// A side channel: skipped when current in every mode, since a
92    /// redundant run would be observed.
93    side: bool,
94}
95
96/// An output resolved for the index-keyed pull: its slot, its type,
97/// and the steps of its cone.
98type ResolvedOutput = (usize, crate::ast::PortType, Option<std::sync::Arc<[usize]>>);
99
100/// Common fields shared by all kernel variants. A clone is a new state
101/// of the same program: the steps are shared, everything else is the
102/// clone's own (engine_parity.md, step 4), and every pair in its buffer
103/// points into its own storage (axiom S3), never into the state it was
104/// cloned from.
105struct KernelCore {
106    buffer: Vec<u64>,
107    coord_count: usize,
108    steps: std::sync::Arc<[CompiledStep]>,
109    output_map: HashMap<String, usize>,
110    gather_buf: Vec<u64>,
111    scatter_buf: Vec<u64>,
112    /// Kernel-owned vector storage; vector-producing ports'
113    /// (ptr, len) slots view entries here (type_system_alignment.md
114    /// §4, compiled_handles.md §3).
115    scratch: Vec<ScratchBuf>,
116    /// Axiom S2: per-slot Ref2 mask — the raw readers panic on
117    /// these instead of leaking addresses.
118    ref_slots: Vec<bool>,
119    /// Axiom S9(a): (first slot of a Ref pair → scratch arena
120    /// index) for every scratch-backed Ref output.
121    ref_scratch: Vec<(usize, usize)>,
122    /// Port type of each named output, for `get_value`.
123    output_types: HashMap<String, PortType>,
124    /// The extern inputs, written through at every set.
125    externs: crate::compile::externs::Externs,
126    /// The traversals the program declares (SRD 113), opened through the
127    /// `Kernel` trait.
128    traversals: std::sync::Arc<[crate::dsl::traversal::Traversal]>,
129    /// Per declared output, its slot, type, and cone, resolved on the
130    /// first index-keyed pull (SRD 117 step 3).
131    resolved_outputs: Vec<Option<ResolvedOutput>>,
132    /// The coordinates set through the `Kernel` trait, pending
133    /// evaluation; `stale` means a write happened since the last
134    /// evaluation round.
135    drive: crate::compile::Drive,
136    /// Per slot: the slot holds `None` (SRD-74 on a compiled kernel):
137    /// an unset extern, or an output of a step that propagated one.
138    none: Vec<bool>,
139    /// Per step: the evaluation round it last ran in, so a new round
140    /// forgets every run without a scan.
141    ran: Vec<u64>,
142    /// The evaluation round: advanced by the first evaluation after a
143    /// write, so a mode without per-step currency runs a step once per
144    /// round rather than once per reader. Bookkeeping only: it wipes
145    /// nothing, and every output stands until an input in its
146    /// provenance is written. 0 is never a round.
147    epoch: u64,
148    /// Every step ran in the round: a full evaluation happened.
149    all_ran: bool,
150    /// Per step: its outputs are current for the inputs it depends on.
151    /// Cleared through the plan when an input changes, whichever call
152    /// changed it; never set for a volatile step.
153    clean: Vec<bool>,
154    /// Whether this kernel's provenance mode skips current steps
155    /// (push-side); a mode without per-step skipping runs every step
156    /// in the cone once per round.
157    use_clean: bool,
158    /// The dirty-register plan: what each input invalidates, what each
159    /// output needs.
160    plan: std::sync::Arc<crate::compile::Invalidation>,
161    /// Per slot: the step that writes it, for the validator.
162    slot_step: std::sync::Arc<[Option<usize>]>,
163    /// Where each step came from, for the failure path (A7).
164    sites: std::sync::Arc<crate::compile::Attribution>,
165    /// The step running, for the failure path.
166    cur_step: usize,
167    /// Every step, in order: what `eval` runs.
168    all: std::sync::Arc<[usize]>,
169    /// Per input slot, the steps an input change marks not current:
170    /// the plan's dependents in a push mode; in a raw or pull-only
171    /// mode, which never consult a pure step's currency, only the side
172    /// channels among them (an optimization over the plan, not a change
173    /// to it).
174    dirty: std::sync::Arc<[Vec<usize>]>,
175    /// The steps that are never current.
176    volatile_steps: std::sync::Arc<[usize]>,
177    /// Some slot holds `None`: an unset extern, which is
178    /// the only way one enters (SRD-74). When none does, the steps run
179    /// without the mask.
180    any_none: bool,
181}
182
183impl Clone for KernelCore {
184    fn clone(&self) -> Self {
185        let mut core = KernelCore {
186            buffer: self.buffer.clone(),
187            coord_count: self.coord_count,
188            steps: self.steps.clone(),
189            output_map: self.output_map.clone(),
190            gather_buf: self.gather_buf.clone(),
191            scatter_buf: self.scatter_buf.clone(),
192            scratch: self.scratch.clone(),
193            ref_slots: self.ref_slots.clone(),
194            ref_scratch: self.ref_scratch.clone(),
195            output_types: self.output_types.clone(),
196            externs: self.externs.clone(),
197            traversals: self.traversals.clone(),
198            resolved_outputs: self.resolved_outputs.clone(),
199            drive: self.drive.clone(),
200            none: self.none.clone(),
201            ran: self.ran.clone(),
202            epoch: self.epoch,
203            all_ran: self.all_ran,
204            clean: self.clean.clone(),
205            use_clean: self.use_clean,
206            plan: self.plan.clone(),
207            slot_step: self.slot_step.clone(),
208            sites: self.sites.clone(),
209            cur_step: self.cur_step,
210            all: self.all.clone(),
211            dirty: self.dirty.clone(),
212            volatile_steps: self.volatile_steps.clone(),
213            any_none: self.any_none,
214        };
215        core.republish_refs();
216        core
217    }
218}
219
220impl KernelCore {
221    /// Point every pair in the buffer into this state's own storage: a
222    /// step's scratch entry for its `Ref2` outputs, the stored value
223    /// for an extern's (axiom S3). What a clone needs, whose buffer
224    /// was copied from a state whose storage it does not share.
225    fn republish_refs(&mut self) {
226        for &(slot, idx) in &self.ref_scratch {
227            let (p, l) = self.scratch[idx].ptr_len();
228            self.buffer[slot] = p;
229            self.buffer[slot + 1] = l;
230        }
231        self.externs.seed(&mut self.buffer, None);
232    }
233}
234
235impl KernelCore {
236    /// Begin an evaluation round after a write: take what cells other
237    /// holders published, forget what ran in the last round, and
238    /// invalidate the volatile steps, as the interpreter does at every
239    /// write. Nothing else changes: every output stands until an input
240    /// in its provenance is written (runtime_model.md, R1).
241    #[inline]
242    fn begin_epoch(&mut self) {
243        if self.externs.cells_dirty() {
244            self.externs.refresh_cells(&mut self.buffer);
245        }
246        self.dirty_refreshed();
247        self.epoch += 1;
248        self.all_ran = false;
249        for &i in self.volatile_steps.iter() {
250            self.clean[i] = false;
251        }
252        self.drive.stale = false;
253    }
254
255    /// Every dependent of a slot a cell refresh changed runs again,
256    /// between writes too, as the interpreter re-evaluates a node
257    /// whose cell moved on its next read: the plan's dependents, whatever
258    /// the mode, are neither run nor current.
259    #[inline]
260    fn dirty_refreshed(&mut self) {
261        if !self.externs.has_changed() {
262            return;
263        }
264        let changed = self.externs.take_changed();
265        for &slot in &changed {
266            if let Some(deps) = self.plan.input_dependents.get(slot) {
267                for &i in deps {
268                    self.ran[i] = 0;
269                    self.clean[i] = false;
270                }
271                self.all_ran = false;
272            }
273        }
274        self.externs.return_changed(changed);
275    }
276
277    /// Take the current value of every cell another holder published
278    /// to, and mark its dependents, so a pull between writes sees the
279    /// register as the interpreter's revision check does.
280    #[inline]
281    fn refresh_cells(&mut self) {
282        if self.externs.cells_dirty() {
283            self.externs.refresh_cells(&mut self.buffer);
284            self.dirty_refreshed();
285        }
286    }
287
288    /// Bind a `shared` binding to `cell` (engine parity, step 9): this
289    /// kernel reads and writes that register from now on.
290    fn attach_cell(&mut self, name: &str, cell: crate::kernel::SharedCell) -> Result<(), String> {
291        let slot = self.externs.attach_cell(name, cell)?;
292        self.dirty_input(slot);
293        self.drive.stale = true;
294        Ok(())
295    }
296
297    /// An input slot changed, through whichever call: every step the
298    /// plan lists for it is no longer current.
299    #[inline]
300    fn dirty_input(&mut self, slot: usize) {
301        if let Some(deps) = self.dirty.get(slot) {
302            for &i in deps {
303                self.clean[i] = false;
304            }
305        }
306    }
307
308    /// Run the steps of `order` that have not run in this round and are
309    /// not current: one rule for every step, whatever reaches it; a
310    /// volatile step is never current.
311    #[inline]
312    fn run_steps(&mut self, order: &[usize]) {
313        self.run_guarded(|core| core.run_order(order));
314    }
315
316    /// Run `body` with the capture guard armed, so a step's panic is
317    /// recorded quietly and re-raised enriched, as the interpreter
318    /// re-raises a node's (A7).
319    #[inline]
320    fn run_guarded(&mut self, body: impl FnOnce(&mut Self)) {
321        let capture = crate::kernel::engines::EvalPanicCaptureGuard::arm();
322        let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| body(self)));
323        drop(capture);
324        if let Err(payload) = outcome {
325            let sites = std::sync::Arc::clone(&self.sites);
326            sites.reraise(payload, self.cur_step, &self.buffer, Some(&self.none));
327        }
328        #[cfg(debug_assertions)]
329        self.validate_refs();
330    }
331
332    /// The steps of `order` that have not run in the round, in order.
333    #[inline]
334    fn run_order(&mut self, order: &[usize]) {
335        let steps = &self.steps;
336        let none_free = !self.any_none;
337        for &i in order {
338            if self.all_ran || self.ran[i] == self.epoch {
339                continue;
340            }
341            let step = &steps[i];
342            // A pure step may be recomputed redundantly in a mode
343            // without per-step skipping; a side channel may not.
344            if (self.use_clean || step.side) && self.clean[i] && !step.volatile {
345                self.ran[i] = self.epoch;
346                continue;
347            }
348            self.cur_step = i;
349            if none_free {
350                run_step_fast(
351                    step,
352                    &mut self.buffer,
353                    &mut self.gather_buf,
354                    &mut self.scatter_buf,
355                    &mut self.scratch,
356                );
357            } else {
358                run_step(
359                    step,
360                    &mut self.buffer,
361                    &mut self.none,
362                    &mut self.gather_buf,
363                    &mut self.scatter_buf,
364                    &mut self.scratch,
365                );
366            }
367            self.ran[i] = self.epoch;
368            self.clean[i] = !step.volatile;
369        }
370    }
371
372    /// Every step, in order, in a round just begun, in a mode without
373    /// per-step skipping and with no `None` in play: the same steps the
374    /// general loop would run, without the bookkeeping a partial round
375    /// needs. A current side channel is still skipped, since its run is
376    /// observed.
377    #[inline]
378    fn run_fresh(&mut self) {
379        let steps = &self.steps;
380        for (i, step) in steps.iter().enumerate() {
381            if step.side {
382                if self.clean[i] && !step.volatile {
383                    continue;
384                }
385                self.clean[i] = !step.volatile;
386            }
387            self.cur_step = i;
388            run_step_fast(
389                step,
390                &mut self.buffer,
391                &mut self.gather_buf,
392                &mut self.scatter_buf,
393                &mut self.scratch,
394            );
395        }
396        self.all_ran = true;
397    }
398
399    /// Evaluate every output: begin a round if a write happened, then
400    /// run every step that has not run.
401    #[inline]
402    fn eval_all(&mut self) {
403        let fresh = self.drive.stale;
404        if fresh {
405            self.begin_epoch();
406        } else {
407            self.refresh_cells();
408        }
409        if fresh && !self.use_clean && !self.any_none {
410            self.run_guarded(|core| core.run_fresh());
411        } else {
412            let all = std::sync::Arc::clone(&self.all);
413            self.run_steps(&all);
414        }
415    }
416
417    /// The named output for the current inputs, running only its cone
418    /// (A6): the interpreter's `pull`, on a compiled kernel.
419    fn pull_named(&mut self, name: &str) -> crate::ast::Value {
420        if self.drive.stale {
421            self.begin_epoch();
422        } else {
423            self.refresh_cells();
424        }
425        let plan = std::sync::Arc::clone(&self.plan);
426        if let Some(order) = plan.cones.get(name) {
427            self.run_steps(order);
428        }
429        self.value_of(name)
430    }
431
432    /// [`Self::pull_named`] by output index: the name is resolved to
433    /// its slot, type, and cone once, so a pull costs no string lookup
434    /// (SRD 117 step 3).
435    fn pull_at(&mut self, index: usize) -> crate::ast::Value {
436        if self.resolved_outputs.len() <= index {
437            self.resolved_outputs.resize(index + 1, None);
438        }
439        if self.resolved_outputs[index].is_none() {
440            let name = self
441                .externs
442                .output_names()
443                .get(index)
444                .cloned()
445                .unwrap_or_else(|| {
446                    panic!(
447                        "no output at index {index}; this kernel declares {}",
448                        self.externs.output_names().len()
449                    )
450                });
451            let slot = self.output_map[&name];
452            let ty = self
453                .output_types
454                .get(&name)
455                .copied()
456                .unwrap_or(crate::ast::PortType::U64);
457            let cone = self
458                .plan
459                .cones
460                .get(&name)
461                .map(|c| std::sync::Arc::from(c.as_slice()));
462            self.resolved_outputs[index] = Some((slot, ty, cone));
463        }
464        if self.drive.stale {
465            self.begin_epoch();
466        } else {
467            self.refresh_cells();
468        }
469        let (slot, ty, cone) = self.resolved_outputs[index]
470            .clone()
471            .expect("resolved above");
472        if let Some(order) = cone {
473            self.run_steps(&order);
474        }
475        self.slot_value(slot, ty)
476    }
477
478    /// The named output as a typed `Value`, `None` where the slot holds
479    /// one; a vector from scratch; a handle copied out.
480    fn value_of(&self, name: &str) -> crate::ast::Value {
481        let slot = self.output_map[name];
482        let ty = self
483            .output_types
484            .get(name)
485            .copied()
486            .unwrap_or(crate::ast::PortType::U64);
487        self.slot_value(slot, ty)
488    }
489
490    /// The value at `slot` decoded as `ty`: `None` where the mask says
491    /// so, a Ref pair copied out through the pair.
492    fn slot_value(&self, slot: usize, ty: crate::ast::PortType) -> crate::ast::Value {
493        if self.none.get(slot).copied().unwrap_or(false) {
494            return crate::ast::Value::None;
495        }
496        crate::compile::marshal::decode_output(&self.buffer, slot, ty)
497    }
498
499    /// Every step is a closure.
500    fn plan(&self) -> crate::EnginePlan {
501        crate::EnginePlan {
502            closure_steps: self.steps.len(),
503            ..Default::default()
504        }
505    }
506
507    /// Nothing is current: every step runs at the next evaluation.
508    fn invalidate_all(&mut self) {
509        self.clean.fill(false);
510        self.all_ran = false;
511        self.drive.stale = true;
512    }
513
514    /// Set an extern by name; returns its slot. The plan invalidates
515    /// what depends on it, as a changed coordinate is invalidated, and
516    /// the next evaluation begins a round.
517    fn set_extern(&mut self, name: &str, value: crate::ast::Value) -> Result<usize, String> {
518        let (slot, unset) = self.externs.set(name, value, &mut self.buffer)?;
519        self.extern_written(slot, unset);
520        Ok(slot)
521    }
522
523    /// [`Self::set_extern`] by input index.
524    fn set_extern_at(&mut self, index: usize, value: crate::ast::Value) -> Result<usize, String> {
525        let (slot, unset) = self.externs.set_at(index, value, &mut self.buffer)?;
526        self.extern_written(slot, unset);
527        Ok(slot)
528    }
529
530    /// An extern was written: its dependents are no longer current, the
531    /// `None` mask records whether it is unset (SRD-74 on a compiled
532    /// kernel), and the next evaluation begins a round. When the last
533    /// unset extern is set, no slot can hold a `None` any more, so the
534    /// mask is cleared and the steps run without it.
535    fn extern_written(&mut self, slot: usize, unset: bool) {
536        self.none[slot] = unset;
537        let was = self.any_none;
538        self.any_none = self.externs.any_unset();
539        if was && !self.any_none {
540            self.none.fill(false);
541        }
542        self.dirty_input(slot);
543        self.drive.stale = true;
544    }
545
546    /// Axiom S9(a) — deterministic Ref validation: every
547    /// scratch-backed Ref pair in the buffer must equal its
548    /// owning entry's current `(as_ptr(), len())`. Run after
549    /// every eval in debug/test builds; a violation names the
550    /// slot instead of dangling. Gated to `debug_assertions` to
551    /// match its call sites, which compile out in release.
552    #[cfg(debug_assertions)]
553    fn validate_refs(&self) {
554        for &(slot, idx) in &self.ref_scratch {
555            // A step that has never run has not published; one that
556            // propagated `None` left its slots as they were.
557            if let Some(Some(step)) = self.slot_step.get(slot)
558                && (self.ran[*step] == 0 || self.none[slot])
559            {
560                continue;
561            }
562            let (p, l) = self.scratch[idx].ptr_len();
563            assert!(
564                self.buffer[slot] == p && self.buffer[slot + 1] == l,
565                "S9 ref-validator: slot pair ({slot}, {}) = ({:#x}, {}) \
566                 does not match scratch[{idx}] = ({p:#x}, {l}) — a slot \
567                 op failed to republish or wrote the wrong slots",
568                slot + 1,
569                self.buffer[slot],
570                self.buffer[slot + 1],
571            );
572        }
573    }
574
575    /// Axiom S2 guard for the raw u64 readers.
576    #[inline]
577    fn guard_ref_slot(&self, slot: usize) {
578        if self.ref_slots.get(slot).copied().unwrap_or(false) {
579            panic!(
580                "S2 pointer containment: slot {slot} is Ref2-colored; raw u64 readers \
581                 would leak an interior address. Use the typed borrow-checked accessor \
582                 (read_vec_*), the boundary decode, or copy out."
583            );
584        }
585    }
586
587    /// Axiom S2 typed accessor core: resolve a Ref pair's first
588    /// slot to its kernel-owned scratch entry. The returned
589    /// borrow ties to `&self`, so holding it across the next
590    /// `eval(&mut self)` is a compile error — stale reads are
591    /// statically impossible.
592    fn ref_entry(&self, slot: usize) -> &ScratchBuf {
593        match self.ref_scratch.iter().find(|(s, _)| *s == slot) {
594            Some(&(_, idx)) => &self.scratch[idx],
595            None if self.ref_slots.get(slot).copied().unwrap_or(false) => panic!(
596                "slot {slot} is a Ref pair owned by the CALLER (a kernel \
597                 input) — read it on the caller side"
598            ),
599            None => panic!("slot {slot} is not a Ref2-colored slot"),
600        }
601    }
602}
603
604/// Build kernel core from raw step data. `use_clean` is whether the
605/// kernel's provenance mode skips current steps.
606fn build_core(
607    coord_count: usize,
608    total_slots: usize,
609    steps: Vec<P2Step>,
610    output_map: HashMap<String, usize>,
611    ref_slots: Vec<bool>,
612    extras: P2Extras,
613    use_clean: bool,
614) -> KernelCore {
615    let P2Extras {
616        output_types,
617        externs,
618        input_dependents,
619        attribution,
620    } = extras;
621    let max_inputs = steps.iter().map(|s| s.input_slots.len()).max().unwrap_or(0);
622    let max_outputs = steps
623        .iter()
624        .map(|s| s.output_slots.len())
625        .max()
626        .unwrap_or(0);
627    let mut scratch: Vec<ScratchBuf> = Vec::new();
628    let mut ref_scratch: Vec<(usize, usize)> = Vec::new();
629    let compiled_steps: Vec<CompiledStep> = steps
630        .into_iter()
631        .map(|step| {
632            let start = scratch.len();
633            scratch.extend(step.scratch.iter().map(|e| ScratchBuf::new(*e)));
634            ref_scratch.extend(crate::compile::assembly::scratch_pairs(
635                &step.name,
636                &step.ref_output_starts,
637                &step.scratch,
638                start,
639            ));
640            CompiledStep {
641                op: step.op,
642                input_slots: step.input_slots,
643                output_slots: step.output_slots,
644                scratch_range: (start, scratch.len()),
645                accepts_none: step.accepts_none,
646                volatile: step.volatile,
647                constant: step.constant,
648                side: step.side,
649            }
650        })
651        .collect();
652    let mut slot_step: Vec<Option<usize>> = vec![None; total_slots];
653    for (i, step) in compiled_steps.iter().enumerate() {
654        for &s in &step.output_slots {
655            slot_step[s] = Some(i);
656        }
657    }
658    let step_inputs: Vec<&[usize]> = compiled_steps
659        .iter()
660        .map(|s| s.input_slots.as_slice())
661        .collect();
662    let step_outputs: Vec<&[usize]> = compiled_steps
663        .iter()
664        .map(|s| s.output_slots.as_slice())
665        .collect();
666    let plan = crate::compile::Invalidation::from_provenance(
667        input_dependents,
668        &step_inputs,
669        &step_outputs,
670        &output_map,
671        total_slots,
672    );
673    let dirty: Vec<Vec<usize>> = plan
674        .input_dependents
675        .iter()
676        .map(|deps| {
677            if use_clean {
678                deps.clone()
679            } else {
680                deps.iter()
681                    .copied()
682                    .filter(|&i| compiled_steps[i].side)
683                    .collect()
684            }
685        })
686        .collect();
687    let volatile_steps: Vec<usize> = (0..compiled_steps.len())
688        .filter(|&i| compiled_steps[i].volatile)
689        .collect();
690    let mut buffer = vec![0u64; total_slots];
691    let mut none = vec![false; total_slots];
692    let any_none = externs.seed(&mut buffer, Some(&mut none));
693    let step_count = compiled_steps.len();
694    let constants: Vec<usize> = compiled_steps
695        .iter()
696        .enumerate()
697        .filter(|(_, s)| s.constant)
698        .map(|(i, _)| i)
699        .collect();
700    let mut core = KernelCore {
701        buffer,
702        coord_count,
703        steps: compiled_steps.into(),
704        output_map,
705        gather_buf: vec![0u64; max_inputs],
706        scatter_buf: vec![0u64; max_outputs],
707        scratch,
708        ref_slots,
709        ref_scratch,
710        output_types,
711        externs,
712        traversals: Vec::new().into(),
713        resolved_outputs: Vec::new(),
714        drive: crate::compile::Drive {
715            coords: Vec::new(),
716            stale: true,
717        },
718        none,
719        ran: vec![0; step_count],
720        epoch: 0,
721        all_ran: false,
722        clean: vec![false; step_count],
723        use_clean,
724        plan: std::sync::Arc::new(plan),
725        slot_step: slot_step.into(),
726        sites: attribution,
727        cur_step: 0,
728        all: (0..step_count).collect::<Vec<usize>>().into(),
729        dirty: dirty.into(),
730        volatile_steps: volatile_steps.into(),
731        any_none,
732    };
733    // The compile-constant fold of the runtime model, on this engine: a
734    // step no input reaches runs at build, once, and is current from
735    // then on, so what is knowable at build is known at build and fails
736    // at build.
737    core.begin_epoch();
738    core.run_steps(&constants);
739    core.drive.stale = true;
740    core
741}
742
743/// The provenance of every slot, from the steps' output slots
744/// ([`crate::compile::slot_provenance`]).
745fn compute_slot_provenance(
746    coord_count: usize,
747    total_slots: usize,
748    input_dependents: &[Vec<usize>],
749    steps: &[CompiledStep],
750) -> Vec<crate::kernel::ProvMask> {
751    let outs: Vec<&[usize]> = steps.iter().map(|s| s.output_slots.as_slice()).collect();
752    crate::compile::slot_provenance(coord_count, total_slots, &outs, input_dependents)
753}
754
755// ── Shared accessor methods ────────────────────────────────────
756
757macro_rules! kernel_accessors {
758    () => {
759        /// The coordinate inputs.
760        pub fn coord_count(&self) -> usize {
761            self.core.coord_count
762        }
763
764        /// The slot of a named output.
765        pub fn resolve_output(&self, name: &str) -> Option<usize> {
766            self.core.output_map.get(name).copied()
767        }
768
769        /// Read an output by pre-resolved slot index. Panics on
770        /// Ref2-colored slots (axiom S2) — use `read_vec_*`.
771        #[inline]
772        pub fn get_slot(&self, slot: usize) -> u64 {
773            self.core.guard_ref_slot(slot);
774            self.core.buffer[slot]
775        }
776
777        /// Read a named output variate after `eval()`. Panics on
778        /// Ref2-colored outputs (axiom S2) — use `read_vec_*`.
779        #[inline]
780        pub fn get(&self, name: &str) -> u64 {
781            let slot = self.core.output_map[name];
782            self.core.guard_ref_slot(slot);
783            self.core.buffer[slot]
784        }
785
786        /// The named output as a typed `Value`, decoded by its port
787        /// type: a `Ref2` output is copied out through its pair
788        /// (compiled_handles.md §4), so the caller never holds a
789        /// pointer; a slot that holds `None` reads as `None`.
790        pub fn get_value(&self, name: &str) -> crate::ast::Value {
791            self.core.value_of(name)
792        }
793
794        /// The named output through the `Kernel` trait: the pending
795        /// coordinates are applied, a round begins if a write is pending, and
796        /// only the output's cone runs.
797        fn pull_value(&mut self, name: &str) -> crate::ast::Value {
798            let coords = std::mem::take(&mut self.core.drive.coords);
799            self.set_coords(&coords);
800            self.core.drive.coords = coords;
801            self.pull_output(name)
802        }
803
804        /// [`Self::pull_value`] by output index.
805        fn pull_value_at(&mut self, index: usize) -> crate::ast::Value {
806            let coords = std::mem::take(&mut self.core.drive.coords);
807            self.set_coords(&coords);
808            self.core.drive.coords = coords;
809            self.core.pull_at(index)
810        }
811
812        /// `eval` through the `Kernel` trait: the pending coordinates,
813        /// then every step.
814        fn eval_pending(&mut self) {
815            let coords = std::mem::take(&mut self.core.drive.coords);
816            self.eval(&coords);
817            self.core.drive.coords = coords;
818        }
819
820        /// Set an extern by name, as `PolydatState::set_input` does on
821        /// the interpreter. The value must be of the declared port
822        /// type. Every kind is written through at once, and every step
823        /// downstream of the extern reruns.
824        pub fn set_input(&mut self, name: &str, value: crate::ast::Value) -> Result<(), String> {
825            let slot = self.core.set_extern(name, value)?;
826            self.mark_input_changed(slot);
827            Ok(())
828        }
829
830        /// [`Self::set_input`] by input index.
831        pub fn set_input_at(
832            &mut self,
833            index: usize,
834            value: crate::ast::Value,
835        ) -> Result<(), String> {
836            let slot = self.core.set_extern_at(index, value)?;
837            self.mark_input_changed(slot);
838            Ok(())
839        }
840
841        /// The kernel's externs by name and declared type.
842        pub fn externs(&self) -> Vec<(&str, crate::ast::PortType)> {
843            self.core.externs.names()
844        }
845
846        /// Every step downstream of a coordinate reruns at the next
847        /// evaluation: the state a kernel created from a shared program
848        /// starts in.
849        fn mark_all_dirty(&mut self) {
850            for i in 0..self.core.coord_count {
851                self.mark_input_changed(i);
852            }
853        }
854
855        /// The cursors the program declares, with the partitions the
856        /// compiler resolved where its `over` clause and extent were
857        /// constant, as `PolydatProgram::cursor_schemas` reports them.
858        pub fn cursor_schemas(&self) -> &[crate::iteration::source::SourceSchema] {
859            self.core.externs.cursor_schemas()
860        }
861
862        /// Narrow a cursor to one partition, as `narrow_cursor` does on
863        /// the interpreter: its `Ext` slot and six scalar projections
864        /// are set as externs.
865        pub fn set_cursor(
866            &mut self,
867            name: &str,
868            partition: &crate::iteration::cursor_partition::Partition,
869        ) -> Result<(), String> {
870            for (slot, value) in self.core.externs.cursor_writes(name, partition)? {
871                self.set_input(&slot, value)?;
872            }
873            Ok(())
874        }
875
876        crate::compile::ref_readers!();
877    };
878}
879
880// ═══════════════════════════════════════════════════════════════
881// Raw: no provenance, no cone guard. Eval runs all steps.
882// ═══════════════════════════════════════════════════════════════
883
884#[derive(Clone)]
885/// The closure tier with no provenance: every evaluation runs every step.
886pub struct CompiledKernelRaw {
887    core: KernelCore,
888}
889
890impl CompiledKernelRaw {
891    pub(crate) fn new(
892        coord_count: usize,
893        total_slots: usize,
894        steps: Vec<P2Step>,
895        output_map: HashMap<String, usize>,
896        ref_slots: Vec<bool>,
897        extras: P2Extras,
898    ) -> Self {
899        Self {
900            core: build_core(
901                coord_count,
902                total_slots,
903                steps,
904                output_map,
905                ref_slots,
906                extras,
907                false,
908            ),
909        }
910    }
911
912    /// The plan invalidates what depends on the input; this mode runs
913    /// every step of a cone once per round regardless.
914    fn mark_input_changed(&mut self, slot: usize) {
915        self.core.dirty_input(slot);
916    }
917
918    /// The coordinates, written; a changed one invalidates
919    /// its dependents through the plan, as in every mode.
920    #[inline]
921    fn set_coords(&mut self, coords: &[u64]) {
922        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
923            if self.core.buffer[i] != c {
924                self.core.buffer[i] = c;
925                self.core.dirty_input(i);
926            }
927        }
928    }
929
930    /// Evaluate every step for `coords`: a new round.
931    #[inline]
932    pub fn eval(&mut self, coords: &[u64]) {
933        self.set_coords(coords);
934        self.core.drive.stale = true;
935        self.core.eval_all();
936    }
937
938    fn pull_output(&mut self, name: &str) -> crate::ast::Value {
939        self.core.pull_named(name)
940    }
941
942    /// Eval + return a specific slot. No cone guard — always evaluates.
943    #[inline]
944    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
945        self.core.guard_ref_slot(slot);
946        self.eval(coords);
947        self.core.buffer[slot]
948    }
949
950    kernel_accessors!();
951}
952
953// ═══════════════════════════════════════════════════════════════
954// Push: per-step skip, no cone guard. A changed input invalidates
955// its dependents through the plan; a current step is skipped.
956// ═══════════════════════════════════════════════════════════════
957
958#[derive(Clone)]
959/// The closure tier with per-step skipping: a changed input invalidates
960/// its dependents through the plan, and a current step is skipped.
961pub struct CompiledKernelPush {
962    core: KernelCore,
963}
964
965impl CompiledKernelPush {
966    pub(crate) fn new(
967        coord_count: usize,
968        total_slots: usize,
969        steps: Vec<P2Step>,
970        output_map: HashMap<String, usize>,
971        input_dependents: Vec<Vec<usize>>,
972        ref_slots: Vec<bool>,
973        extras: P2Extras,
974    ) -> Self {
975        // The plan in `extras` carries the dependents.
976        let _ = input_dependents;
977        Self {
978            core: build_core(
979                coord_count,
980                total_slots,
981                steps,
982                output_map,
983                ref_slots,
984                extras,
985                true,
986            ),
987        }
988    }
989
990    #[inline]
991    fn set_coords(&mut self, coords: &[u64]) {
992        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
993            if self.core.buffer[i] != c {
994                self.core.buffer[i] = c;
995                self.core.dirty_input(i);
996            }
997        }
998    }
999
1000    /// Every step downstream of the slot reruns.
1001    fn mark_input_changed(&mut self, slot: usize) {
1002        self.core.dirty_input(slot);
1003    }
1004
1005    /// Evaluate every step that is not current for `coords`: a new round.
1006    #[inline]
1007    pub fn eval(&mut self, coords: &[u64]) {
1008        self.set_coords(coords);
1009        self.core.drive.stale = true;
1010        self.core.eval_all();
1011    }
1012
1013    fn pull_output(&mut self, name: &str) -> crate::ast::Value {
1014        self.core.pull_named(name)
1015    }
1016
1017    /// Eval + return a specific slot. No cone guard — always enters eval loop.
1018    #[inline]
1019    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
1020        self.core.guard_ref_slot(slot);
1021        self.eval(coords);
1022        self.core.buffer[slot]
1023    }
1024
1025    kernel_accessors!();
1026}
1027
1028// ═══════════════════════════════════════════════════════════════
1029// Pull: cone guard only, no per-step skip.
1030// set_inputs tracks changed_mask. eval_for_slot checks cone
1031// then runs ALL steps if dirty.
1032// ═══════════════════════════════════════════════════════════════
1033
1034#[derive(Clone)]
1035/// The closure tier with the cone guard: an output whose cone no changed
1036/// input reaches is not recomputed.
1037pub struct CompiledKernelPull {
1038    core: KernelCore,
1039    slot_provenance: Vec<crate::kernel::ProvMask>,
1040    changed_mask: crate::kernel::ProvMask,
1041    /// Set by `set_input`: an extern changed, so the next evaluation
1042    /// runs whatever the cone guard says.
1043    force_run: bool,
1044}
1045
1046impl CompiledKernelPull {
1047    pub(crate) fn new(
1048        coord_count: usize,
1049        total_slots: usize,
1050        steps: Vec<P2Step>,
1051        output_map: HashMap<String, usize>,
1052        input_dependents: &[Vec<usize>],
1053        ref_slots: Vec<bool>,
1054        extras: P2Extras,
1055    ) -> Self {
1056        let core = build_core(
1057            coord_count,
1058            total_slots,
1059            steps,
1060            output_map,
1061            ref_slots,
1062            extras,
1063            false,
1064        );
1065        let slot_provenance =
1066            compute_slot_provenance(coord_count, total_slots, input_dependents, &core.steps);
1067        Self {
1068            core,
1069            slot_provenance,
1070            changed_mask: crate::kernel::ProvMask::all_below(coord_count), // all dirty initially
1071            force_run: false,
1072        }
1073    }
1074
1075    /// Track which inputs changed (for the cone guard), and invalidate
1076    /// their dependents through the plan, as in every mode.
1077    #[inline]
1078    fn set_coords(&mut self, coords: &[u64]) {
1079        self.changed_mask.clear();
1080        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
1081            if self.core.buffer[i] != c {
1082                self.core.buffer[i] = c;
1083                self.changed_mask.set(i);
1084                self.core.dirty_input(i);
1085            }
1086        }
1087    }
1088
1089    /// The next evaluation runs regardless of the cone guard, since
1090    /// `set_inputs` rebuilds the changed set from the coordinates alone.
1091    fn mark_input_changed(&mut self, slot: usize) {
1092        self.core.dirty_input(slot);
1093        self.force_run = true;
1094    }
1095
1096    /// Evaluate eagerly (no cone guard). Runs all steps: a new round.
1097    #[inline]
1098    pub fn eval(&mut self, coords: &[u64]) {
1099        self.set_coords(coords);
1100        self.force_run = false;
1101        self.core.drive.stale = true;
1102        self.core.eval_all();
1103    }
1104
1105    fn pull_output(&mut self, name: &str) -> crate::ast::Value {
1106        self.core.pull_named(name)
1107    }
1108
1109    /// Cone guard: if the output's cone is clean, skip eval entirely.
1110    /// Otherwise run ALL steps (no per-node skip).
1111    #[inline]
1112    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
1113        self.core.guard_ref_slot(slot);
1114        self.set_coords(coords);
1115        if !self.force_run
1116            && slot < self.slot_provenance.len()
1117            && !self.slot_provenance[slot].intersects(&self.changed_mask)
1118        {
1119            return self.core.buffer[slot];
1120        }
1121        self.force_run = false;
1122        self.core.drive.stale = true;
1123        self.core.eval_all();
1124        self.core.buffer[slot]
1125    }
1126
1127    kernel_accessors!();
1128}
1129
1130// ═══════════════════════════════════════════════════════════════
1131// PushPull: push-side per-step skip + pull-side cone guard.
1132// Full optimization.
1133// ═══════════════════════════════════════════════════════════════
1134
1135#[derive(Clone)]
1136/// The closure tier with per-step skipping and the cone guard.
1137pub struct CompiledKernelPushPull {
1138    core: KernelCore,
1139    slot_provenance: Vec<crate::kernel::ProvMask>,
1140    changed_mask: crate::kernel::ProvMask,
1141    /// Set by `set_input`: an extern changed, so the next evaluation
1142    /// runs whatever the cone guard says.
1143    force_run: bool,
1144}
1145
1146impl CompiledKernelPushPull {
1147    pub(crate) fn new(
1148        coord_count: usize,
1149        total_slots: usize,
1150        steps: Vec<P2Step>,
1151        output_map: HashMap<String, usize>,
1152        input_dependents: Vec<Vec<usize>>,
1153        ref_slots: Vec<bool>,
1154        extras: P2Extras,
1155    ) -> Self {
1156        let core = build_core(
1157            coord_count,
1158            total_slots,
1159            steps,
1160            output_map,
1161            ref_slots,
1162            extras,
1163            true,
1164        );
1165        let slot_provenance =
1166            compute_slot_provenance(coord_count, total_slots, &input_dependents, &core.steps);
1167        Self {
1168            core,
1169            slot_provenance,
1170            changed_mask: crate::kernel::ProvMask::all_below(coord_count),
1171            force_run: false,
1172        }
1173    }
1174
1175    #[inline]
1176    fn set_coords(&mut self, coords: &[u64]) {
1177        self.changed_mask.clear();
1178        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
1179            if self.core.buffer[i] != c {
1180                self.core.buffer[i] = c;
1181                self.changed_mask.set(i);
1182                self.core.dirty_input(i);
1183            }
1184        }
1185    }
1186
1187    /// Every step downstream of the slot reruns, and the next
1188    /// evaluation runs whatever the cone guard says.
1189    fn mark_input_changed(&mut self, slot: usize) {
1190        self.core.dirty_input(slot);
1191        self.force_run = true;
1192    }
1193
1194    /// Eval with push-side skip (no cone guard): a new round.
1195    #[inline]
1196    pub fn eval(&mut self, coords: &[u64]) {
1197        self.set_coords(coords);
1198        self.force_run = false;
1199        self.core.drive.stale = true;
1200        self.core.eval_all();
1201    }
1202
1203    fn pull_output(&mut self, name: &str) -> crate::ast::Value {
1204        self.core.pull_named(name)
1205    }
1206
1207    /// Cone guard + push-side skip: the full optimization.
1208    #[inline]
1209    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
1210        self.core.guard_ref_slot(slot);
1211        self.set_coords(coords);
1212        if !self.force_run
1213            && slot < self.slot_provenance.len()
1214            && !self.slot_provenance[slot].intersects(&self.changed_mask)
1215        {
1216            return self.core.buffer[slot];
1217        }
1218        self.force_run = false;
1219        self.core.drive.stale = true;
1220        self.core.eval_all();
1221        self.core.buffer[slot]
1222    }
1223
1224    kernel_accessors!();
1225}
1226
1227// ── The engine-independent surface (engine_parity.md, step 4) ──────
1228
1229use crate::compile::select::{Engine, Provenance};
1230
1231crate::compile::impl_kernel_trait!(CompiledKernelRaw, Engine::Closures(Provenance::Raw));
1232crate::compile::impl_kernel_trait!(CompiledKernelPush, Engine::Closures(Provenance::Push));
1233crate::compile::impl_kernel_trait!(CompiledKernelPull, Engine::Closures(Provenance::Pull));
1234crate::compile::impl_kernel_trait!(
1235    CompiledKernelPushPull,
1236    Engine::Closures(Provenance::PushPull)
1237);
1238
1239/// One step: SRD-74 Rule 1, then gather, run the closure, scatter. A
1240/// node that does not accept `None` emits `None` on every output when
1241/// any input is `None`, without running.
1242#[inline(always)]
1243fn run_step(
1244    step: &CompiledStep,
1245    buffer: &mut [u64],
1246    none: &mut [bool],
1247    gather: &mut [u64],
1248    scatter: &mut [u64],
1249    scratch: &mut [ScratchBuf],
1250) {
1251    let mut any_none = false;
1252    for (i, &s) in step.input_slots.iter().enumerate() {
1253        gather[i] = buffer[s];
1254        any_none |= none[s];
1255    }
1256    if any_none && !step.accepts_none {
1257        for &s in &step.output_slots {
1258            none[s] = true;
1259        }
1260        return;
1261    }
1262    if matches!(step.op, StepOp::Copy) {
1263        for (&i, &o) in step.input_slots.iter().zip(&step.output_slots) {
1264            buffer[o] = buffer[i];
1265            none[o] = false;
1266        }
1267        return;
1268    }
1269    let (n_in, n_out) = (step.input_slots.len(), step.output_slots.len());
1270    match &step.op {
1271        StepOp::Copy => unreachable!(),
1272        StepOp::U64(op) => op(&gather[..n_in], &mut scatter[..n_out]),
1273        StepOp::Slot(op) => op(
1274            &gather[..n_in],
1275            &mut scatter[..n_out],
1276            &mut scratch[step.scratch_range.0..step.scratch_range.1],
1277        ),
1278    }
1279    for (i, &s) in step.output_slots.iter().enumerate() {
1280        buffer[s] = scatter[i];
1281        none[s] = false;
1282    }
1283}
1284
1285/// [`run_step`] when no slot holds `None`: gather, run, scatter.
1286#[inline(always)]
1287fn run_step_fast(
1288    step: &CompiledStep,
1289    buffer: &mut [u64],
1290    gather: &mut [u64],
1291    scatter: &mut [u64],
1292    scratch: &mut [ScratchBuf],
1293) {
1294    if matches!(step.op, StepOp::Copy) {
1295        for (&i, &o) in step.input_slots.iter().zip(&step.output_slots) {
1296            buffer[o] = buffer[i];
1297        }
1298        return;
1299    }
1300    for (i, &s) in step.input_slots.iter().enumerate() {
1301        gather[i] = buffer[s];
1302    }
1303    let (n_in, n_out) = (step.input_slots.len(), step.output_slots.len());
1304    match &step.op {
1305        StepOp::Copy => unreachable!(),
1306        StepOp::U64(op) => op(&gather[..n_in], &mut scatter[..n_out]),
1307        StepOp::Slot(op) => op(
1308            &gather[..n_in],
1309            &mut scatter[..n_out],
1310            &mut scratch[step.scratch_range.0..step.scratch_range.1],
1311        ),
1312    }
1313    for (i, &s) in step.output_slots.iter().enumerate() {
1314        buffer[s] = scatter[i];
1315    }
1316}