Skip to main content

polydat_core/compile/
closures.rs

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