Skip to main content

polydat_core/compile/
hybrid.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Hybrid kernel: per-node optimal compilation level.
5//!
6//! Splits the DAG into segments based on each node's compilation
7//! capability. JIT-able nodes are batched into native code segments.
8//! Non-JIT-able nodes run as Phase 2 closures. All segments share
9//! the same flat u64 buffer.
10//!
11//! This is the "best of all worlds" kernel — no node pays more
12//! overhead than it needs to.
13//!
14//! Three kernel types. They differ in what `set_inputs` marks and
15//! whether `eval_for_slot` consults the cone guard; the shared step
16//! loop reads the mode's `use_clean` flag per step:
17//!
18//! | Type | Push (per-step skip) | Pull (cone guard) |
19//! |------|---------------------|-------------------|
20//! | `HybridKernelRaw` | — | — |
21//! | `HybridKernelPull` | — | yes |
22//! | `HybridKernelPushPull` | yes | yes |
23
24use std::collections::HashMap;
25
26use crate::ast::SlotShape;
27use crate::ast::{CompiledU64Op, PolydatNode};
28use crate::kernel::WireSource;
29
30#[cfg(feature = "jit")]
31use crate::compile::jit::{self, JitOp};
32
33/// A step in the hybrid kernel: either JIT native code or a Phase 2 closure.
34enum HybridStep {
35    /// A batch of nodes compiled to native code via Cranelift.
36    /// The function reads/writes directly to the shared buffer.
37    #[cfg(feature = "jit")]
38    Jit(JitSegment),
39    /// A single node executed via its Phase 2 closure.
40    Closure(ClosureStep),
41}
42
43#[cfg(feature = "jit")]
44struct JitSegment {
45    code_fn: crate::compile::jit::NativeFn,
46    /// The finalized native code, shared by every kernel created from
47    /// one program.
48    _module: crate::compile::jit::JitCode,
49    /// Whether the code calls a helper, and so runs under the longjmp
50    /// catch; code with no call runs bare.
51    fallible: bool,
52    /// The slots the segment reads and writes, for cones and for the
53    /// `None` check native code cannot make itself.
54    input_slots: Vec<usize>,
55    output_slots: Vec<usize>,
56    /// The program nodes in the segment, in step order; the tracker
57    /// slot names the one a failure belongs to.
58    nodes: Vec<usize>,
59}
60
61/// A native segment planned and not yet compiled: its place in the step
62/// order and what its `JitSegment` will hold.
63#[cfg(feature = "jit")]
64struct PendingSegment {
65    step: usize,
66    batch: Vec<(JitOp, Vec<usize>, Vec<usize>)>,
67    input_slots: Vec<usize>,
68    output_slots: Vec<usize>,
69    nodes: Vec<usize>,
70}
71
72impl HybridStep {
73    fn input_slots(&self) -> &[usize] {
74        match self {
75            #[cfg(feature = "jit")]
76            HybridStep::Jit(seg) => &seg.input_slots,
77            HybridStep::Closure(cs) => &cs.input_slots,
78        }
79    }
80    fn output_slots(&self) -> &[usize] {
81        match self {
82            #[cfg(feature = "jit")]
83            HybridStep::Jit(seg) => &seg.output_slots,
84            HybridStep::Closure(cs) => &cs.output_slots,
85        }
86    }
87    /// SRD-74 Rule 2: the step runs on `None` inputs. Native code never
88    /// does; a node downstream of an unset extern is a closure.
89    fn accepts_none(&self) -> bool {
90        match self {
91            #[cfg(feature = "jit")]
92            HybridStep::Jit(_) => false,
93            HybridStep::Closure(cs) => cs.accepts_none,
94        }
95    }
96
97    /// The program node a failure in this step belongs to: a closure's
98    /// own, or the member native code named in the tracker slot.
99    #[cfg_attr(not(feature = "jit"), allow(unused_variables))]
100    fn failing_node(&self, buffer: &[u64], tracker: usize) -> usize {
101        match self {
102            #[cfg(feature = "jit")]
103            HybridStep::Jit(seg) => seg
104                .nodes
105                .get(buffer[tracker] as usize)
106                .copied()
107                .unwrap_or(usize::MAX),
108            HybridStep::Closure(cs) => cs.node,
109        }
110    }
111}
112
113/// A closure step's op: pure-scalar u64 closure, or a slot op
114/// with kernel-owned scratch for typed-slice ports
115/// (type_system_alignment.md §4, compiled_handles.md §3).
116enum ClosureOp {
117    U64(CompiledU64Op),
118    Slot(crate::ast::CompiledSlotOp),
119}
120
121struct ClosureStep {
122    op: ClosureOp,
123    input_slots: Vec<usize>,
124    output_slots: Vec<usize>,
125    /// `[start, end)` into the kernel's scratch arena.
126    scratch_range: (usize, usize),
127    /// SRD-74 Rule 2: the closure runs on `None` inputs.
128    accepts_none: bool,
129    /// The program node, for the failure path.
130    node: usize,
131}
132
133/// An output resolved for pulls by index: its slot, its type, its cone
134/// order, and whether any step of that order can fail.
135type ResolvedOutput = (
136    usize,
137    crate::ast::PortType,
138    Option<std::sync::Arc<[usize]>>,
139    bool,
140);
141
142/// Common fields shared by all hybrid kernel variants. A clone is a new
143/// state of the same program: the steps and the nodes are shared,
144/// everything else is the clone's own (engines.md §3.5), and
145/// every pair in its buffer points into its own storage (axiom S3),
146/// never into the state it was cloned from.
147struct HybridCore {
148    /// The engine this kernel runs, as it reports it: the tier and
149    /// the provenance mode it was built with. State rather than a
150    /// property of the type, so one kernel type can serve a tier
151    /// that runs native code and one that runs none.
152    engine: crate::compile::select::Engine,
153    buffer: Vec<u64>,
154    coord_count: usize,
155    steps: std::sync::Arc<Vec<HybridStep>>,
156    output_map: HashMap<String, usize>,
157    gather_buf: Vec<u64>,
158    scatter_buf: Vec<u64>,
159    /// Kernel-owned vector storage; vector-producing ports'
160    /// (ptr, len) slots view entries here (type_system_alignment.md
161    /// §4, compiled_handles.md §3).
162    scratch: Vec<crate::ast::ScratchBuf>,
163    /// Axiom S2: per-slot Ref2 mask — raw readers panic on these.
164    ref_slots: Vec<bool>,
165    /// Axiom S9(a): (first slot of a Ref pair → scratch index).
166    ref_scratch: Vec<(usize, usize)>,
167    /// Port type of each named output, for `get_value`.
168    output_types: HashMap<String, crate::ast::PortType>,
169    /// The extern inputs, written through at every set.
170    externs: crate::compile::externs::Externs,
171    /// The traversals the program declares (SRD 113), opened through the
172    /// `Kernel` trait.
173    traversals: std::sync::Arc<[crate::dsl::traversal::Traversal]>,
174    /// Per declared output, its slot, type, and cone, resolved on the
175    /// first index-keyed pull (SRD 117 step 3).
176    resolved_outputs: Vec<Option<ResolvedOutput>>,
177    /// Keep source nodes alive so JIT-baked pointers remain valid.
178    _nodes: std::sync::Arc<Vec<Box<dyn PolydatNode>>>,
179    /// The coordinates set through the `Kernel` trait, pending
180    /// evaluation; `stale` means a write happened since the last
181    /// evaluation round.
182    drive: crate::compile::Drive,
183    /// Per slot: the slot holds `None` (SRD-74 on a compiled kernel).
184    none: Vec<bool>,
185    /// Per step: the evaluation round it last ran in, so a new round
186    /// forgets every run without a scan.
187    ran: Vec<u64>,
188    /// The evaluation round: advanced by the first evaluation after a
189    /// write, so a mode without per-step currency runs a step once per
190    /// round rather than once per reader. Bookkeeping only: it wipes
191    /// nothing, and every output stands until an input in its
192    /// provenance is written. 0 is never a round.
193    epoch: u64,
194    /// Every step ran in the round: a full evaluation happened.
195    all_ran: bool,
196    /// Per step: its outputs are current for the inputs it depends on.
197    /// Cleared through the plan when an input changes, whichever call
198    /// changed it; never set for a volatile step.
199    clean: Vec<bool>,
200    /// Whether this kernel's provenance mode skips current steps.
201    use_clean: bool,
202    /// The dirty-register plan: what each input invalidates, what each
203    /// output needs.
204    plan: std::sync::Arc<crate::compile::Invalidation>,
205    /// Per step: nondeterministic or downstream of one, never current.
206    volatile: std::sync::Arc<[bool]>,
207    /// Per step: a side channel, skipped when current in every mode.
208    side: std::sync::Arc<[bool]>,
209    /// Per slot: the step that writes it.
210    slot_step: std::sync::Arc<[Option<usize>]>,
211    /// Where each step came from, for the failure path (A7).
212    sites: std::sync::Arc<crate::compile::Attribution>,
213    /// The step running, for the failure path.
214    cur_step: usize,
215    /// The slot past the layout where a segment names the member it is
216    /// in before calling a helper.
217    tracker: usize,
218    /// Every step, in order: what `eval` runs.
219    all: std::sync::Arc<[usize]>,
220    /// Per input slot, the steps an input change marks not current:
221    /// the plan's dependents in a push mode; in a raw or pull-only
222    /// mode, which never consult a pure step's currency, only the side
223    /// channels among them (an optimization over the plan, not a change
224    /// to it).
225    dirty: std::sync::Arc<[Vec<usize>]>,
226    /// Some slot holds `None`: an unset extern, which is
227    /// the only way one enters (SRD-74). When none does, the steps run
228    /// without the mask.
229    any_none: bool,
230    /// The steps that are never current, invalidated at every round.
231    volatile_steps: std::sync::Arc<[usize]>,
232}
233
234impl Clone for HybridCore {
235    fn clone(&self) -> Self {
236        let mut core = HybridCore {
237            engine: self.engine,
238            buffer: self.buffer.clone(),
239            coord_count: self.coord_count,
240            steps: self.steps.clone(),
241            output_map: self.output_map.clone(),
242            gather_buf: self.gather_buf.clone(),
243            scatter_buf: self.scatter_buf.clone(),
244            scratch: self.scratch.clone(),
245            ref_slots: self.ref_slots.clone(),
246            ref_scratch: self.ref_scratch.clone(),
247            output_types: self.output_types.clone(),
248            externs: self.externs.clone(),
249            traversals: self.traversals.clone(),
250            resolved_outputs: self.resolved_outputs.clone(),
251            _nodes: self._nodes.clone(),
252            drive: self.drive.clone(),
253            none: self.none.clone(),
254            ran: self.ran.clone(),
255            epoch: self.epoch,
256            all_ran: self.all_ran,
257            clean: self.clean.clone(),
258            use_clean: self.use_clean,
259            plan: self.plan.clone(),
260            volatile: self.volatile.clone(),
261            side: self.side.clone(),
262            slot_step: self.slot_step.clone(),
263            sites: self.sites.clone(),
264            cur_step: self.cur_step,
265            tracker: self.tracker,
266            all: self.all.clone(),
267            dirty: self.dirty.clone(),
268            any_none: self.any_none,
269            volatile_steps: self.volatile_steps.clone(),
270        };
271        core.republish_refs();
272        core
273    }
274}
275
276impl HybridCore {
277    crate::compile::shared_core_methods!();
278}
279
280impl HybridCore {
281    /// Whether this kernel skips current steps, and with it which steps
282    /// an input change marks: every dependent, or only the side
283    /// channels when a pure step's currency is never consulted.
284    fn set_use_clean(&mut self, on: bool) {
285        self.use_clean = on;
286        let side = std::sync::Arc::clone(&self.side);
287        self.dirty = self
288            .plan
289            .input_dependents
290            .iter()
291            .map(|deps| {
292                if on {
293                    deps.clone()
294                } else {
295                    deps.iter().copied().filter(|&i| side[i]).collect()
296                }
297            })
298            .collect::<Vec<_>>()
299            .into();
300    }
301
302    /// Whether step `i` can fail: a closure runs a node's Rust body,
303    /// which may panic, and a native segment can fail only when it
304    /// calls a helper (`JitCode::fallible`).
305    #[inline]
306    fn step_can_fail(&self, i: usize) -> bool {
307        match &self.steps[i] {
308            #[cfg(feature = "jit")]
309            HybridStep::Jit(seg) => seg.fallible,
310            _ => true,
311        }
312    }
313
314    /// The program node the step now running belongs to, for the
315    /// failure path (A7). A step here can be a run of native code over
316    /// several nodes, so the tracker slot names the member. Read by
317    /// `run_guarded` and by the build-time `fold_steps`, so the two
318    /// always name the same node.
319    #[inline]
320    fn failing_node(&self) -> usize {
321        self.steps[self.cur_step].failing_node(&self.buffer, self.tracker)
322    }
323
324    /// The steps of `order` that have not run in the round, in order.
325    #[inline]
326    fn run_order(&mut self, order: &[usize]) {
327        let steps = &self.steps;
328        let none_free = !self.any_none;
329        for &i in order {
330            if self.all_ran || self.ran[i] == self.epoch {
331                continue;
332            }
333            let never = self.volatile[i];
334            if (self.use_clean || self.side[i]) && self.clean[i] && !never {
335                self.ran[i] = self.epoch;
336                continue;
337            }
338            self.cur_step = i;
339            run_hybrid_step(
340                &steps[i],
341                none_free,
342                &mut self.buffer,
343                &mut self.none,
344                &mut self.gather_buf,
345                &mut self.scatter_buf,
346                &mut self.scratch,
347            );
348            self.ran[i] = self.epoch;
349            self.clean[i] = !never;
350        }
351    }
352
353    /// Every step, in order, in a round just begun, in a mode without
354    /// per-step skipping and with no `None` in play: the same steps the
355    /// general loop would run, without the bookkeeping a partial round
356    /// needs. A current side channel is still skipped, since its run is
357    /// observed.
358    #[inline]
359    fn run_fresh(&mut self) {
360        let steps = &self.steps;
361        for (i, step) in steps.iter().enumerate() {
362            if self.side[i] {
363                let never = self.volatile[i];
364                if self.clean[i] && !never {
365                    continue;
366                }
367                self.clean[i] = !never;
368            }
369            self.cur_step = i;
370            run_hybrid_step(
371                step,
372                true,
373                &mut self.buffer,
374                &mut self.none,
375                &mut self.gather_buf,
376                &mut self.scatter_buf,
377                &mut self.scratch,
378            );
379        }
380        self.all_ran = true;
381    }
382
383    /// The native segments and the closure steps.
384    fn plan(&self) -> crate::EnginePlan {
385        let (native_segments, closure_steps) = self.engine_counts();
386        crate::EnginePlan {
387            native_segments,
388            closure_steps,
389            interpreted_nodes: 0,
390        }
391    }
392}
393
394/// Everything the evaluation loops once did, kept for the raw kernel's
395/// `eval`, which evaluates every step in a new round.
396#[inline]
397fn eval_all_hybrid_steps(core: &mut HybridCore) {
398    core.drive.stale = true;
399    core.eval_all();
400}
401
402// ═══════════════════════════════════════════════════════════════
403// Raw: no provenance, no cone guard. Eval runs all steps.
404// ═══════════════════════════════════════════════════════════════
405
406impl HybridCore {
407    /// How many steps run as native segments and how many as closures:
408    /// what the per-node engine choice decided for this graph.
409    fn engine_counts(&self) -> (usize, usize) {
410        let closures = self
411            .steps
412            .iter()
413            .filter(|s| matches!(s, HybridStep::Closure(_)))
414            .count();
415        (self.steps.len() - closures, closures)
416    }
417}
418
419/// Hybrid kernel with no provenance tracking.
420///
421/// Every `eval()` call runs all steps unconditionally. Useful as a
422/// baseline and for graphs where inputs change on every evaluation.
423#[derive(Clone)]
424pub struct HybridKernelRaw {
425    core: HybridCore,
426}
427
428impl HybridKernelRaw {
429    crate::compile::kernel_accessors!(set_coords);
430    /// The coordinates, written; a changed one invalidates
431    /// its dependents through the plan, as in every mode.
432    #[inline]
433    fn set_coords(&mut self, coords: &[u64]) {
434        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
435            if self.core.buffer[i] != c {
436                self.core.buffer[i] = c;
437                self.core.dirty_input(i);
438            }
439        }
440    }
441
442    /// Evaluate all hybrid steps unconditionally: a new round.
443    #[inline]
444    pub fn eval(&mut self, coords: &[u64]) {
445        self.set_coords(coords);
446        eval_all_hybrid_steps(&mut self.core);
447    }
448
449    /// Set an extern by name, as `PolydatState::set_input` does on the
450    /// interpreter. Every run evaluates everything, so it takes effect
451    /// at the next run.
452    pub fn set_input(
453        &mut self,
454        name: &str,
455        value: crate::ast::Value,
456    ) -> Result<(), crate::kernel::WriteError> {
457        self.core.set_extern(name, value).map(|_| ())
458    }
459
460    /// [`Self::set_input`] by input index.
461    pub fn set_input_at(
462        &mut self,
463        index: usize,
464        value: crate::ast::Value,
465    ) -> Result<(), crate::kernel::WriteError> {
466        self.core.set_extern_at(index, value).map(|_| ())
467    }
468
469    /// Eval all steps and return the value at `slot`.
470    #[inline]
471    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
472        self.core.guard_ref_slot(slot);
473        self.eval(coords);
474        self.core.buffer[slot]
475    }
476
477    /// The number of native segments and of closure steps in this
478    /// kernel, in that order: what the per-node engine choice decided.
479    pub fn engine_counts(&self) -> (usize, usize) {
480        self.core.engine_counts()
481    }
482
483    /// Store owned nodes to keep JIT-baked pointers valid.
484    pub fn retain_nodes(&mut self, nodes: Vec<Box<dyn PolydatNode>>) {
485        self.core._nodes = std::sync::Arc::new(nodes);
486    }
487}
488
489// ═══════════════════════════════════════════════════════════════
490// Pull: cone guard only, no per-step skip.
491// set_inputs tracks changed_mask. eval_for_slot checks the cone
492// then runs ALL steps if dirty.
493// ═══════════════════════════════════════════════════════════════
494
495/// Hybrid kernel with pull-side cone guard.
496///
497/// `eval_for_slot()` checks whether the output's transitive input
498/// cone changed before running steps. If nothing in the cone changed,
499/// the cached value is returned without re-evaluation.
500#[derive(Clone)]
501pub struct HybridKernelPull {
502    core: HybridCore,
503    slot_provenance: Vec<crate::kernel::ProvMask>,
504    changed_mask: crate::kernel::ProvMask,
505    /// Set by `set_input`: an extern changed, so the next evaluation
506    /// runs whatever the cone guard says.
507    force_run: bool,
508}
509
510impl HybridKernelPull {
511    crate::compile::kernel_accessors!(set_inputs);
512    /// Track which inputs changed (for the cone guard), and invalidate
513    /// their dependents through the plan, as in every mode.
514    #[inline]
515    fn set_inputs(&mut self, coords: &[u64]) {
516        self.changed_mask.clear();
517        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
518            if self.core.buffer[i] != c {
519                self.core.buffer[i] = c;
520                self.changed_mask.set(i);
521                self.core.dirty_input(i);
522            }
523        }
524    }
525
526    /// Evaluate all steps (no cone guard): a new round.
527    #[inline]
528    pub fn eval(&mut self, coords: &[u64]) {
529        self.set_inputs(coords);
530        self.force_run = false;
531        eval_all_hybrid_steps(&mut self.core);
532    }
533
534    /// Cone guard: if the output's cone is clean, skip eval entirely.
535    /// Otherwise run ALL steps (no per-step skip).
536    #[inline]
537    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
538        self.core.guard_ref_slot(slot);
539        self.set_inputs(coords);
540        if !self.force_run
541            && slot < self.slot_provenance.len()
542            && !self.slot_provenance[slot].intersects(&self.changed_mask)
543        {
544            return self.core.buffer[slot];
545        }
546        self.force_run = false;
547        eval_all_hybrid_steps(&mut self.core);
548        self.core.buffer[slot]
549    }
550
551    /// Set an extern by name, as `PolydatState::set_input` does on the
552    /// interpreter. Every kind is written through at once, and the
553    /// next run runs whatever the cone guard says.
554    pub fn set_input(
555        &mut self,
556        name: &str,
557        value: crate::ast::Value,
558    ) -> Result<(), crate::kernel::WriteError> {
559        self.core.set_extern(name, value)?;
560        self.force_run = true;
561        Ok(())
562    }
563
564    /// [`Self::set_input`] by input index.
565    pub fn set_input_at(
566        &mut self,
567        index: usize,
568        value: crate::ast::Value,
569    ) -> Result<(), crate::kernel::WriteError> {
570        self.core.set_extern_at(index, value)?;
571        self.force_run = true;
572        Ok(())
573    }
574
575    /// The number of native segments and of closure steps in this
576    /// kernel, in that order: what the per-node engine choice decided.
577    pub fn engine_counts(&self) -> (usize, usize) {
578        self.core.engine_counts()
579    }
580
581    /// Store owned nodes to keep JIT-baked pointers valid.
582    pub fn retain_nodes(&mut self, nodes: Vec<Box<dyn PolydatNode>>) {
583        self.core._nodes = std::sync::Arc::new(nodes);
584    }
585}
586
587// ═══════════════════════════════════════════════════════════════
588// PushPull: push-side per-step skip + pull-side cone guard.
589// Full optimization — the production default.
590// ═══════════════════════════════════════════════════════════════
591
592/// Hybrid kernel with both push-side per-step skip and pull-side cone guard.
593///
594/// Push side: `set_inputs()` marks only steps that depend on changed inputs
595/// as dirty; clean steps are skipped during `eval()`.
596///
597/// Pull side: `eval_for_slot()` first checks whether the output's cone of
598/// influence changed at all. If not, the cached value is returned without
599/// entering the eval loop.
600#[derive(Clone)]
601pub struct HybridKernelPushPull {
602    core: HybridCore,
603    slot_provenance: Vec<crate::kernel::ProvMask>,
604    changed_mask: crate::kernel::ProvMask,
605    /// Set by `set_input`: an extern changed, so the next evaluation
606    /// runs whatever the cone guard says.
607    force_run: bool,
608}
609
610impl HybridKernelPushPull {
611    crate::compile::kernel_accessors!(set_inputs);
612    /// Set an extern by name, as `PolydatState::set_input` does on the
613    /// interpreter. Every kind is written through at once. Every step
614    /// downstream of the extern reruns, and the next evaluation runs
615    /// whatever the cone guard says.
616    pub fn set_input(
617        &mut self,
618        name: &str,
619        value: crate::ast::Value,
620    ) -> Result<(), crate::kernel::WriteError> {
621        self.core.set_extern(name, value)?;
622        self.force_run = true;
623        Ok(())
624    }
625
626    /// [`Self::set_input`] by input index.
627    pub fn set_input_at(
628        &mut self,
629        index: usize,
630        value: crate::ast::Value,
631    ) -> Result<(), crate::kernel::WriteError> {
632        self.core.set_extern_at(index, value)?;
633        self.force_run = true;
634        Ok(())
635    }
636
637    /// Track which inputs changed and dirty affected steps.
638    #[inline]
639    fn set_inputs(&mut self, coords: &[u64]) {
640        self.changed_mask.clear();
641        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
642            if self.core.buffer[i] != c {
643                self.core.buffer[i] = c;
644                self.changed_mask.set(i);
645                self.core.dirty_input(i);
646            }
647        }
648    }
649
650    /// Evaluate with push-side step skip (no cone guard): a new round.
651    #[inline]
652    pub fn eval(&mut self, coords: &[u64]) {
653        self.set_inputs(coords);
654        self.force_run = false;
655        self.core.drive.stale = true;
656        self.core.eval_all();
657    }
658
659    /// Cone guard + push-side skip: the full optimization.
660    #[inline]
661    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
662        self.core.guard_ref_slot(slot);
663        self.set_inputs(coords);
664        if !self.force_run
665            && slot < self.slot_provenance.len()
666            && !self.slot_provenance[slot].intersects(&self.changed_mask)
667        {
668            return self.core.buffer[slot];
669        }
670        self.force_run = false;
671        self.core.drive.stale = true;
672        self.core.eval_all();
673        self.core.buffer[slot]
674    }
675
676    /// The number of native segments and of closure steps in this
677    /// kernel, in that order: what the per-node engine choice decided.
678    pub fn engine_counts(&self) -> (usize, usize) {
679        self.core.engine_counts()
680    }
681
682    /// Store owned nodes to keep JIT-baked pointers valid.
683    pub fn retain_nodes(&mut self, nodes: Vec<Box<dyn PolydatNode>>) {
684        self.core._nodes = std::sync::Arc::new(nodes);
685    }
686}
687
688/// Type alias for the default hybrid kernel (PushPull — full optimization).
689///
690/// The assembler's `compile_hybrid` returns this alias. Rename uses to
691/// the concrete type if different optimization trade-offs are needed.
692pub type HybridKernel = HybridKernelPushPull;
693
694/// Flattened slot list for one node's wire inputs under per-port
695/// widths (type_system_alignment.md §6): every source
696/// contributes `slot_width` consecutive slots.
697fn flatten_input_slots(
698    wiring: &[Vec<WireSource>],
699    nodes: &[Box<dyn PolydatNode>],
700    node_idx: usize,
701    port_offsets: &[Vec<usize>],
702    input_starts: &[usize],
703    input_widths: &[usize],
704) -> Vec<usize> {
705    let mut slots = Vec::new();
706    for source in &wiring[node_idx] {
707        let (start, w) = match source {
708            WireSource::Input(c) => (
709                input_starts.get(*c).copied().unwrap_or(*c),
710                input_widths.get(*c).copied().unwrap_or(1),
711            ),
712            WireSource::NodeOutput(u, p) => (
713                port_offsets[*u][*p],
714                nodes[*u].meta().outs[*p].typ.slot_width(),
715            ),
716        };
717        slots.extend(start..start + w);
718    }
719    slots
720}
721
722/// First slot of each Ref2-colored output port of one node, in
723/// port order (axiom S3 pairing with CompiledSlotKit scratch).
724fn flatten_ref_output_starts(
725    nodes: &[Box<dyn PolydatNode>],
726    node_idx: usize,
727    port_offsets: &[Vec<usize>],
728) -> Vec<usize> {
729    nodes[node_idx]
730        .meta()
731        .outs
732        .iter()
733        .enumerate()
734        .filter(|(_, out)| out.typ.slot_color() == crate::ast::SlotColor::Ref2)
735        .map(|(p, _)| port_offsets[node_idx][p])
736        .collect()
737}
738
739/// Flattened slot list for one node's outputs.
740fn flatten_output_slots(
741    nodes: &[Box<dyn PolydatNode>],
742    node_idx: usize,
743    port_offsets: &[Vec<usize>],
744) -> Vec<usize> {
745    let mut slots = Vec::new();
746    for (p, out) in nodes[node_idx].meta().outs.iter().enumerate() {
747        let start = port_offsets[node_idx][p];
748        slots.extend(start..start + out.typ.slot_width());
749    }
750    slots
751}
752
753/// Build a hybrid kernel from resolved DAG data.
754///
755/// Each node is classified: if it can be JIT-compiled, it goes into
756/// a JIT segment. If not, it becomes a closure step. Adjacent JIT-able
757/// nodes are batched into a single JIT segment for efficiency.
758///
759/// A node this engine cannot lay out, as a refusal naming the engine.
760/// Distinct from the fold failure a built kernel can still report,
761/// which is the program's and not this engine's
762/// ([`KernelError::ConstantFold`](crate::KernelError::ConstantFold)).
763fn refused(reason: String) -> crate::KernelError {
764    crate::KernelError::Refused {
765        engine: crate::compile::select::Engine::Native(crate::compile::select::Provenance::Auto),
766        reason,
767    }
768}
769
770/// Returns a `HybridKernelPushPull` (the production default).
771#[cfg(feature = "jit")]
772#[allow(clippy::too_many_arguments)]
773pub(crate) fn build_hybrid(
774    nodes: &[Box<dyn PolydatNode>],
775    wiring: &[Vec<WireSource>],
776    coord_count: usize,
777    total_slots: usize,
778    port_offsets: &[Vec<usize>],
779    input_starts: &[usize],
780    input_widths: &[usize],
781    output_map: HashMap<String, usize>,
782    ref_slots: Vec<bool>,
783    input_types: &[crate::ast::PortType],
784    externs: crate::compile::externs::Externs,
785    constant: Vec<bool>,
786    volatile: Vec<bool>,
787    attribution: std::sync::Arc<crate::compile::Attribution>,
788) -> Result<HybridKernelPushPull, crate::KernelError> {
789    // A native segment's place is taken in step order and filled once
790    // every segment is compiled, all of them into one module.
791    let mut steps: Vec<Option<HybridStep>> = Vec::new();
792    let mut pending: Vec<PendingSegment> = Vec::new();
793    let mut scratch: Vec<crate::ast::ScratchBuf> = Vec::new();
794    let mut ref_scratch: Vec<(usize, usize)> = Vec::new();
795    let mut max_inputs = 0usize;
796    let mut max_outputs = 0usize;
797    let graph = GraphView {
798        nodes,
799        wiring,
800        port_offsets,
801        input_types,
802    };
803
804    // Classify each node
805    let classifications: Vec<(JitOp, Vec<usize>, Vec<usize>)> = nodes
806        .iter()
807        .enumerate()
808        .map(|(node_idx, node)| {
809            // Classified with the wire types known (SRD 115 §6.1), as
810            // cones and pure-P3 layouts are: a variadic node whose
811            // wires its helper cannot decode falls back to its closure.
812            let wire_types: Vec<crate::ast::PortType> = wiring[node_idx]
813                .iter()
814                .map(|src| match src {
815                    WireSource::Input(c) => input_types
816                        .get(*c)
817                        .copied()
818                        .unwrap_or(crate::ast::PortType::U64),
819                    WireSource::NodeOutput(j, p) => nodes[*j].meta().outs[*p].typ,
820                })
821                .collect();
822            let jit_op = jit::classify_node_typed(node.as_ref(), &wire_types);
823
824            let input_slots = flatten_input_slots(
825                wiring,
826                nodes,
827                node_idx,
828                port_offsets,
829                input_starts,
830                input_widths,
831            );
832            let output_slots = flatten_output_slots(nodes, node_idx, port_offsets);
833
834            max_inputs = max_inputs.max(input_slots.len());
835            max_outputs = max_outputs.max(output_slots.len());
836
837            (jit_op, input_slots, output_slots)
838        })
839        .collect();
840    // A node that would have produced a value from a `None` runs as a
841    // closure, always. A segment's answer to a `None` on one of its
842    // boundary inputs is `None` on all of its outputs — SRD-74 Rule 1,
843    // the only answer native code can give, since it cannot carry one.
844    // That answer is right for every node that propagates a `None` and
845    // wrong for a node that consumes one (`to_json` keeps going, a
846    // `printf` with an `Option` arg writes its own text), so such a
847    // node must not be inside a segment for the rule to hold. The cone
848    // planner makes the same exclusion for the same reason.
849    let mut classifications = classifications;
850    let mut eligible = vec![false; nodes.len()];
851    for (node_idx, node) in nodes.iter().enumerate() {
852        if matches!(classifications[node_idx].0, JitOp::Fallback) {
853            continue;
854        }
855        if !crate::compile::none_rule_admits(
856            node.accepts_none_inputs(),
857            &wiring[node_idx],
858            &eligible,
859        ) {
860            classifications[node_idx].0 = JitOp::Fallback;
861            continue;
862        }
863        eligible[node_idx] = true;
864    }
865    // A node downstream of an extern with no value runs as a closure
866    // too. This is the narrower case — the extern is already unset at
867    // build — and it stays because it also keeps the `None` out of
868    // segments downstream, where the boundary guard would otherwise be
869    // the only thing catching it.
870    let unset = externs.unset_slots();
871    if !unset.is_empty() {
872        let mut tainted = vec![false; nodes.len()];
873        for node_idx in 0..nodes.len() {
874            tainted[node_idx] = wiring[node_idx].iter().any(|src| match src {
875                WireSource::Input(c) => unset.contains(&input_starts[*c]),
876                WireSource::NodeOutput(j, _) => tainted[*j],
877            });
878            if tainted[node_idx] {
879                classifications[node_idx].0 = JitOp::Fallback;
880            }
881        }
882    }
883
884    // Per node, the step it runs in: its own closure step or its segment.
885    let mut node_step = vec![usize::MAX; nodes.len()];
886    // The step order: every compile-constant node first, then the rest
887    // in the graph's order. A constant depends on constants alone, so
888    // hoisting them keeps every dependency ahead of its consumer, and
889    // it keeps the cycle-time nodes contiguous: a literal between two
890    // cycle-time statements no longer cuts a segment in two (the tile
891    // ladder's twenty-hole case ran as dozens of segments that way,
892    // each paying the segment's catch and step bookkeeping).
893    let order: Vec<usize> = (0..nodes.len())
894        .filter(|&k| constant[k])
895        .chain((0..nodes.len()).filter(|&k| !constant[k]))
896        .collect();
897    let mut rank = vec![0usize; nodes.len()];
898    for (pos, &k) in order.iter().enumerate() {
899        rank[k] = pos;
900    }
901    // Segments are the fusion units (SRD-105, compile::fusion_units):
902    // connected, convex groups of native nodes, so two chains that share
903    // nothing are two segments and a pull runs only its own. Nodes fuse
904    // within one lifecycle: a segment is folded at build only if every
905    // member is compile-constant, and a volatile node never joins pure
906    // ones (the segment would be never current and rerun them at every
907    // round). A side channel never joins any other node (it would fire
908    // whenever the segment ran, rather than when its own inputs changed).
909    let is_side = |k: usize| matches!(nodes[k].purity(), crate::ast::Purity::SideChannel { .. });
910    let preds: Vec<Vec<usize>> = wiring
911        .iter()
912        .map(|w| {
913            w.iter()
914                .filter_map(|src| match src {
915                    WireSource::NodeOutput(j, _) => Some(*j),
916                    WireSource::Input(_) => None,
917                })
918                .collect()
919        })
920        .collect();
921    let fusible: Vec<bool> = (0..nodes.len())
922        .map(|k| !matches!(classifications[k].0, JitOp::Fallback) && !is_side(k))
923        .collect();
924    let class: Vec<u64> = (0..nodes.len())
925        .map(|k| constant[k] as u64 | (volatile[k] as u64) << 1)
926        .collect();
927    let plan =
928        crate::compile::fusion_units::plan_units(&preds, &fusible, &class, &rank, &|c| c & 1 == 1);
929    for members in plan.units {
930        let i = members[0];
931        if matches!(classifications[i].0, JitOp::Fallback) {
932            // This node needs a closure — scalar u64 op preferred,
933            // slot op for slice-bearing nodes (type_system_alignment.md
934            // §4, compiled_handles.md §3).
935            let (_, ref input_slots, ref output_slots) = classifications[i];
936            let step = closure_step_for(
937                &graph,
938                i,
939                input_slots.clone(),
940                output_slots.clone(),
941                &mut scratch,
942                &mut ref_scratch,
943            )?;
944            node_step[i] = steps.len();
945            steps.push(Some(HybridStep::Closure(step)));
946        } else {
947            // Each step's scratch entries are placed in the kernel's
948            // scratch (axiom S3), and its reference outputs recorded
949            // for the validator (S9(a)).
950            for &k in &members {
951                let base = scratch.len();
952                classifications[k].0.place_scratch(base);
953                let elems = classifications[k].0.scratch_elems().to_vec();
954                ref_scratch.extend(crate::compile::assembly::scratch_pairs(
955                    &nodes[k].meta().name,
956                    &flatten_ref_output_starts(nodes, k, port_offsets),
957                    &elems,
958                    base,
959                ));
960                scratch.extend(elems.iter().map(|e| crate::ast::ScratchBuf::new(*e)));
961            }
962            // One native segment for the batch: its boundary inputs are
963            // the slots the batch reads and does not write, its outputs
964            // every slot it writes. Slots closures fill with Ref pairs
965            // are Ref2 slots to the S2/S9 validator, which a segment
966            // may only load, store, and pass. Native code names the
967            // member it is in through the tracker slot, for the failure
968            // path (A7).
969            let batch: Vec<(JitOp, Vec<usize>, Vec<usize>)> = members
970                .iter()
971                .map(|&k| classifications[k].clone())
972                .collect();
973            let written: std::collections::HashSet<usize> = batch
974                .iter()
975                .flat_map(|(_, _, o)| o.iter().copied())
976                .collect();
977            let mut input_slots: Vec<usize> = Vec::new();
978            for (_, ins, _) in &batch {
979                for &s in ins {
980                    if !written.contains(&s) && !input_slots.contains(&s) {
981                        input_slots.push(s);
982                    }
983                }
984            }
985            let output_slots: Vec<usize> = batch
986                .iter()
987                .flat_map(|(_, _, o)| o.iter().copied())
988                .collect();
989            let segment = steps.len();
990            for &k in &members {
991                node_step[k] = segment;
992            }
993            steps.push(None);
994            pending.push(PendingSegment {
995                step: segment,
996                batch,
997                input_slots,
998                output_slots,
999                nodes: members,
1000            });
1001        }
1002    }
1003    // Every segment is a function of one module, so the code of a
1004    // kernel whose pulls run many segments lies together rather than a
1005    // module, and its pages, apart per segment.
1006    let batches: Vec<&[jit::JitStep]> = pending.iter().map(|p| p.batch.as_slice()).collect();
1007    let (entries, code) = if batches.is_empty() {
1008        (Vec::new(), None)
1009    } else {
1010        let (entries, code) =
1011            jit::compile_jit_entries(&batches, Some(total_slots)).map_err(refused)?;
1012        (entries, Some(code))
1013    };
1014    for (p, (code_fn, fallible)) in pending.into_iter().zip(entries) {
1015        steps[p.step] = Some(HybridStep::Jit(JitSegment {
1016            code_fn,
1017            fallible,
1018            _module: code.clone().expect("a segment was compiled"),
1019            input_slots: p.input_slots,
1020            output_slots: p.output_slots,
1021            nodes: p.nodes,
1022        }));
1023    }
1024    let steps: Vec<HybridStep> = steps
1025        .into_iter()
1026        .map(|s| s.expect("every step is placed"))
1027        .collect();
1028
1029    let output_types = output_types_of(nodes, port_offsets, input_starts, input_types, &output_map);
1030    build_pushpull_from_steps(
1031        steps,
1032        scratch,
1033        ref_scratch,
1034        ref_slots,
1035        wiring,
1036        nodes,
1037        coord_count,
1038        total_slots,
1039        output_map,
1040        max_inputs,
1041        max_outputs,
1042        input_starts,
1043        input_widths,
1044        output_types,
1045        externs,
1046        constant,
1047        volatile,
1048        attribution,
1049        node_step,
1050    )
1051}
1052
1053/// The port type of each named output, by the slot it names: a node
1054/// output port's type, or a coordinate input's declared type.
1055fn output_types_of(
1056    nodes: &[Box<dyn PolydatNode>],
1057    port_offsets: &[Vec<usize>],
1058    input_starts: &[usize],
1059    input_types: &[crate::ast::PortType],
1060    output_map: &HashMap<String, usize>,
1061) -> HashMap<String, crate::ast::PortType> {
1062    let mut slot_types: HashMap<usize, crate::ast::PortType> = HashMap::new();
1063    for (start, ty) in input_starts.iter().zip(input_types) {
1064        slot_types.insert(*start, *ty);
1065    }
1066    for (node_idx, node) in nodes.iter().enumerate() {
1067        for (p, out) in node.meta().outs.iter().enumerate() {
1068            slot_types.insert(port_offsets[node_idx][p], out.typ);
1069        }
1070    }
1071    output_map
1072        .iter()
1073        .map(|(name, slot)| {
1074            (
1075                name.clone(),
1076                slot_types
1077                    .get(slot)
1078                    .copied()
1079                    .unwrap_or(crate::ast::PortType::U64),
1080            )
1081        })
1082        .collect()
1083}
1084
1085/// Build a hybrid kernel without JIT (all closures).
1086#[cfg(not(feature = "jit"))]
1087#[allow(clippy::too_many_arguments)]
1088pub(crate) fn build_hybrid(
1089    nodes: &[Box<dyn PolydatNode>],
1090    wiring: &[Vec<WireSource>],
1091    coord_count: usize,
1092    total_slots: usize,
1093    port_offsets: &[Vec<usize>],
1094    input_starts: &[usize],
1095    input_widths: &[usize],
1096    output_map: HashMap<String, usize>,
1097    ref_slots: Vec<bool>,
1098    input_types: &[crate::ast::PortType],
1099    externs: crate::compile::externs::Externs,
1100    constant: Vec<bool>,
1101    volatile: Vec<bool>,
1102    attribution: std::sync::Arc<crate::compile::Attribution>,
1103) -> Result<HybridKernelPushPull, crate::KernelError> {
1104    let mut steps: Vec<HybridStep> = Vec::new();
1105    let mut scratch: Vec<crate::ast::ScratchBuf> = Vec::new();
1106    let mut ref_scratch: Vec<(usize, usize)> = Vec::new();
1107    let mut max_inputs = 0usize;
1108    let mut max_outputs = 0usize;
1109    let graph = GraphView {
1110        nodes,
1111        wiring,
1112        port_offsets,
1113        input_types,
1114    };
1115
1116    for node_idx in 0..nodes.len() {
1117        let input_slots = flatten_input_slots(
1118            wiring,
1119            nodes,
1120            node_idx,
1121            port_offsets,
1122            input_starts,
1123            input_widths,
1124        );
1125        let output_slots = flatten_output_slots(nodes, node_idx, port_offsets);
1126
1127        max_inputs = max_inputs.max(input_slots.len());
1128        max_outputs = max_outputs.max(output_slots.len());
1129
1130        let step = closure_step_for(
1131            &graph,
1132            node_idx,
1133            input_slots,
1134            output_slots,
1135            &mut scratch,
1136            &mut ref_scratch,
1137        )?;
1138        steps.push(HybridStep::Closure(step));
1139    }
1140    let node_step: Vec<usize> = (0..nodes.len()).collect();
1141
1142    let output_types = output_types_of(nodes, port_offsets, input_starts, input_types, &output_map);
1143    build_pushpull_from_steps(
1144        steps,
1145        scratch,
1146        ref_scratch,
1147        ref_slots,
1148        wiring,
1149        nodes,
1150        coord_count,
1151        total_slots,
1152        output_map,
1153        max_inputs,
1154        max_outputs,
1155        input_starts,
1156        input_widths,
1157        output_types,
1158        externs,
1159        constant,
1160        volatile,
1161        attribution,
1162        node_step,
1163    )
1164}
1165
1166/// The graph a builder reads a node's shape out of: the nodes, how
1167/// they are wired, where each port's slots begin, and the coordinate
1168/// and extern types a wire from an input takes. The four always travel
1169/// together and neither builder modifies them.
1170#[derive(Clone, Copy)]
1171struct GraphView<'a> {
1172    nodes: &'a [Box<dyn PolydatNode>],
1173    wiring: &'a [Vec<WireSource>],
1174    port_offsets: &'a [Vec<usize>],
1175    input_types: &'a [crate::ast::PortType],
1176}
1177
1178/// The closure step for one node: its op, its scratch placed in the
1179/// kernel's arena, and its reference outputs recorded for the S9(a)
1180/// validator. A scalar op first, then the compiler's slot copy, then
1181/// the node's own kit — the same ladder `assembly::node_step_op` walks
1182/// for the closure tier.
1183///
1184/// Both builders reach here for a node that runs as a closure: the one
1185/// with the JIT for a node it classified `Fallback`, the one without
1186/// for every node, since without the feature there is nothing else a
1187/// node can be. They had the block twice, differing in where the slots
1188/// came from, which is why it is a parameter.
1189fn closure_step_for(
1190    graph: &GraphView<'_>,
1191    node_idx: usize,
1192    input_slots: Vec<usize>,
1193    output_slots: Vec<usize>,
1194    scratch: &mut Vec<crate::ast::ScratchBuf>,
1195    ref_scratch: &mut Vec<(usize, usize)>,
1196) -> Result<ClosureStep, crate::KernelError> {
1197    let GraphView {
1198        nodes,
1199        wiring,
1200        port_offsets,
1201        input_types,
1202    } = *graph;
1203    let node = &nodes[node_idx];
1204    let scratch_start = scratch.len();
1205    let wire_types: Vec<crate::ast::PortType> = wiring[node_idx]
1206        .iter()
1207        .map(|src| match src {
1208            WireSource::Input(c) => input_types
1209                .get(*c)
1210                .copied()
1211                .unwrap_or(crate::ast::PortType::U64),
1212            WireSource::NodeOutput(j, p) => nodes[*j].meta().outs[*p].typ,
1213        })
1214        .collect();
1215    let op = if let Some(op) = node.compiled_u64() {
1216        ClosureOp::U64(op)
1217    } else if let Some(op) = crate::compile::assembly::identity_op(node.as_ref()) {
1218        ClosureOp::U64(op)
1219    } else if let Some(kit) = ref_copy_or_slot(node.as_ref(), &wire_types) {
1220        scratch.extend(kit.scratch.iter().map(|e| crate::ast::ScratchBuf::new(*e)));
1221        let starts = flatten_ref_output_starts(nodes, node_idx, port_offsets);
1222        ref_scratch.extend(crate::compile::assembly::scratch_pairs(
1223            &node.meta().name,
1224            &starts,
1225            &kit.scratch,
1226            scratch_start,
1227        ));
1228        ClosureOp::Slot(kit.op)
1229    } else {
1230        return Err(refused(format!(
1231            "node '{}' has no compiled form (docs/design/engines.md §8)",
1232            node.meta().name
1233        )));
1234    };
1235    Ok(ClosureStep {
1236        op,
1237        input_slots,
1238        output_slots,
1239        scratch_range: (scratch_start, scratch.len()),
1240        accepts_none: node.accepts_none_inputs(),
1241        node: node_idx,
1242    })
1243}
1244
1245/// Shared construction of `HybridKernelPushPull` from assembled steps.
1246///
1247/// Computes provenance bitmasks from the DAG wiring and builds the
1248/// step_dependents list for push-side invalidation and the slot_provenance
1249/// table for pull-side cone guard.
1250#[allow(clippy::too_many_arguments)]
1251fn build_pushpull_from_steps(
1252    steps: Vec<HybridStep>,
1253    scratch: Vec<crate::ast::ScratchBuf>,
1254    ref_scratch: Vec<(usize, usize)>,
1255    ref_slots: Vec<bool>,
1256    wiring: &[Vec<WireSource>],
1257    nodes: &[Box<dyn PolydatNode>],
1258    coord_count: usize,
1259    total_slots: usize,
1260    output_map: HashMap<String, usize>,
1261    max_inputs: usize,
1262    max_outputs: usize,
1263    _input_starts: &[usize],
1264    input_widths: &[usize],
1265    output_types: HashMap<String, crate::ast::PortType>,
1266    externs: crate::compile::externs::Externs,
1267    constant: Vec<bool>,
1268    volatile: Vec<bool>,
1269    attribution: std::sync::Arc<crate::compile::Attribution>,
1270    node_step: Vec<usize>,
1271) -> Result<HybridKernelPushPull, crate::KernelError> {
1272    let step_count = steps.len();
1273    debug_assert_eq!(node_step.len(), nodes.len());
1274    debug_assert!(node_step.iter().all(|&s| s < step_count));
1275    // Node lists from the runtime model become step lists: a segment
1276    // depends on what any member depends on.
1277    let to_steps = |list: &[usize]| -> Vec<usize> {
1278        let mut v: Vec<usize> = list.iter().map(|&n| node_step[n]).collect();
1279        v.sort_unstable();
1280        v.dedup();
1281        v
1282    };
1283    // One slot past the layout is the tracker (A7).
1284    let mut buffer = vec![0u64; total_slots + 1];
1285    let mut none = vec![false; total_slots];
1286    let any_none = externs.seed(&mut buffer, Some(&mut none));
1287
1288    // Compute per-node provenance and invert into per-input step dependents.
1289    // Dependents come back per node; `to_steps` folds them onto steps (a
1290    // segment depends on what any member depends on). They also come back
1291    // per-INPUT; expand to per-SLOT so the kernels' slot-indexed dirty
1292    // tracking / changed-mask bits stay coherent under multi-slot inputs
1293    // (type_system_alignment.md §6). Identity for all-scalar inputs.
1294    let node_provenance = crate::kernel::PolydatProgram::compute_provenance(nodes, wiring);
1295    let input_dependents: Vec<Vec<usize>> =
1296        crate::kernel::PolydatProgram::compute_dependents(&node_provenance, input_widths.len())
1297            .iter()
1298            .map(|d| to_steps(d))
1299            .collect();
1300    let step_dependents: Vec<Vec<usize>> = input_widths
1301        .iter()
1302        .enumerate()
1303        .flat_map(|(i, w)| {
1304            std::iter::repeat_n(input_dependents.get(i).cloned().unwrap_or_default(), *w)
1305        })
1306        .collect();
1307
1308    let step_outs: Vec<&[usize]> = steps.iter().map(|s| s.output_slots()).collect();
1309    let slot_provenance =
1310        crate::compile::slot_provenance(coord_count, total_slots, &step_outs, &step_dependents);
1311
1312    // The runtime model's lifecycle classification, passed in per node
1313    // from the one rule the interpreter's fold applies, folded onto the
1314    // steps: a segment is constant only if every member is, volatile
1315    // or a side channel if any member is.
1316    debug_assert_eq!(constant.len(), nodes.len());
1317    debug_assert_eq!(volatile.len(), nodes.len());
1318    let mut step_constant = vec![true; step_count];
1319    let mut step_volatile = vec![false; step_count];
1320    let mut side = vec![false; step_count];
1321    for (n, node) in nodes.iter().enumerate() {
1322        let s = node_step[n];
1323        step_constant[s] &= constant[n];
1324        step_volatile[s] |= volatile[n];
1325        side[s] |= matches!(node.purity(), crate::ast::Purity::SideChannel { .. });
1326    }
1327    let volatile = step_volatile;
1328    let constants: Vec<usize> = (0..step_count).filter(|&i| step_constant[i]).collect();
1329    let step_inputs: Vec<&[usize]> = steps.iter().map(|s| s.input_slots()).collect();
1330    let step_outputs: Vec<&[usize]> = steps.iter().map(|s| s.output_slots()).collect();
1331    let plan = crate::compile::Invalidation::from_provenance(
1332        step_dependents.clone(),
1333        &step_inputs,
1334        &step_outputs,
1335        &output_map,
1336        total_slots,
1337    );
1338    let mut slot_step: Vec<Option<usize>> = vec![None; total_slots];
1339    for (i, outs) in step_outputs.iter().enumerate() {
1340        for &s in outs.iter() {
1341            slot_step[s] = Some(i);
1342        }
1343    }
1344    drop(step_inputs);
1345    drop(step_outputs);
1346
1347    let dirty: Vec<Vec<usize>> = plan.input_dependents.clone();
1348    let volatile_steps: Vec<usize> = (0..step_count).filter(|&i| volatile[i]).collect();
1349    let mut kernel = HybridKernelPushPull {
1350        core: HybridCore {
1351            engine: Engine::Native(Provenance::PushPull),
1352            buffer,
1353            coord_count,
1354            steps: std::sync::Arc::new(steps),
1355            output_map,
1356            gather_buf: vec![0u64; max_inputs.max(1)],
1357            scatter_buf: vec![0u64; max_outputs.max(1)],
1358            scratch,
1359            ref_slots,
1360            ref_scratch,
1361            output_types,
1362            externs,
1363            traversals: Vec::new().into(),
1364            resolved_outputs: Vec::new(),
1365            _nodes: std::sync::Arc::new(Vec::new()),
1366            drive: crate::compile::Drive {
1367                coords: Vec::new(),
1368                stale: true,
1369            },
1370            none,
1371            ran: vec![0; step_count],
1372            epoch: 0,
1373            all_ran: false,
1374            clean: vec![false; step_count],
1375            use_clean: true,
1376            plan: std::sync::Arc::new(plan),
1377            volatile: volatile.into(),
1378            side: side.into(),
1379            slot_step: slot_step.into(),
1380            sites: attribution,
1381            cur_step: 0,
1382            tracker: total_slots,
1383            all: (0..step_count).collect::<Vec<usize>>().into(),
1384            dirty: dirty.into(),
1385            any_none,
1386            volatile_steps: volatile_steps.into(),
1387        },
1388        slot_provenance,
1389        changed_mask: crate::kernel::ProvMask::all_below(coord_count), // all dirty on first eval
1390        force_run: false,
1391    };
1392    // The compile-constant fold of the runtime model, on this engine: a
1393    // step no input reaches runs at build, once, and is current from
1394    // then on, so what is knowable at build is known at build and fails
1395    // at build.
1396    kernel.core.begin_epoch();
1397    kernel.core.fold_steps(&constants)?;
1398    kernel.core.drive.stale = true;
1399    Ok(kernel)
1400}
1401
1402/// The slot kit for a closure step: a copy of a `Ref2` value into the
1403/// step's own scratch (`identity`, a `__port_` passthrough; axiom S3),
1404/// else the node's own kit.
1405fn ref_copy_or_slot(
1406    node: &dyn PolydatNode,
1407    wire_types: &[crate::ast::PortType],
1408) -> Option<crate::ast::CompiledSlotKit> {
1409    let meta = node.meta();
1410    if (meta.name == "identity" || meta.name.starts_with("__port_"))
1411        && meta.outs.len() == 1
1412        && meta.outs[0].typ.slot_color() == crate::ast::SlotColor::Ref2
1413    {
1414        return crate::compile::assembly::ref_copy_kit(meta.outs[0].typ);
1415    }
1416    node.compiled_slot(
1417        wire_types,
1418        crate::compile::select::Engine::Native(crate::compile::select::Provenance::Auto),
1419    )
1420}
1421
1422// ── The engine-independent surface (engines.md §3.5) ──────
1423
1424impl HybridKernelRaw {
1425    /// Nothing to mark: every run evaluates everything.
1426    fn mark_all_dirty(&mut self) {}
1427}
1428
1429impl HybridKernelPull {
1430    /// The next evaluation runs whatever the cone guard says.
1431    fn mark_all_dirty(&mut self) {
1432        self.changed_mask = crate::kernel::ProvMask::all_below(self.core.coord_count);
1433        self.force_run = true;
1434    }
1435}
1436
1437impl HybridKernelPushPull {
1438    /// Every step reruns at the next evaluation.
1439    fn mark_all_dirty(&mut self) {
1440        self.core.clean.fill(false);
1441        self.changed_mask = crate::kernel::ProvMask::all_below(self.core.coord_count);
1442        self.force_run = true;
1443    }
1444
1445    /// The same program with no provenance: every run evaluates
1446    /// everything.
1447    pub(crate) fn into_raw(self) -> HybridKernelRaw {
1448        let mut core = self.core;
1449        core.set_use_clean(false);
1450        core.engine = Engine::Native(Provenance::Raw);
1451        HybridKernelRaw { core }
1452    }
1453
1454    pub(crate) fn into_pull(self) -> HybridKernelPull {
1455        let mut core = self.core;
1456        core.set_use_clean(false);
1457        core.engine = Engine::Native(Provenance::Pull);
1458        let changed_mask = crate::kernel::ProvMask::all_below(core.coord_count);
1459        HybridKernelPull {
1460            core,
1461            slot_provenance: self.slot_provenance,
1462            changed_mask,
1463            force_run: false,
1464        }
1465    }
1466}
1467
1468use crate::compile::select::{Engine, Provenance};
1469
1470crate::compile::impl_kernel_trait!(HybridKernelRaw);
1471crate::compile::impl_kernel_trait!(HybridKernelPull);
1472crate::compile::impl_kernel_trait!(HybridKernelPushPull);
1473crate::compile::impl_slot_kernel!(HybridKernelRaw);
1474crate::compile::impl_slot_kernel!(HybridKernelPull);
1475crate::compile::impl_slot_kernel!(HybridKernelPushPull);
1476
1477/// One step: SRD-74 Rule 1, then the segment or the closure. A step
1478/// that does not accept `None` emits `None` on every output when any
1479/// input is `None`, without running.
1480///
1481/// A segment is such a step and always was — native code cannot carry
1482/// a `None` — so a `None` on one of its boundary inputs makes all of
1483/// its outputs `None`, which is the same answer the closure tier and
1484/// the interpreter give. It reaches a segment only when a host cleared
1485/// an extern after the build; an extern unset at build already keeps
1486/// the nodes downstream of it out of segments, and a node that would
1487/// have *consumed* the `None` rather than propagated it is kept out
1488/// unconditionally, so this answer is never the wrong one.
1489///
1490/// With `none_free` the mask is known clear and is not read.
1491#[inline(always)]
1492fn run_hybrid_step(
1493    step: &HybridStep,
1494    none_free: bool,
1495    buffer: &mut [u64],
1496    none: &mut [bool],
1497    gather: &mut [u64],
1498    scatter: &mut [u64],
1499    scratch: &mut [crate::ast::ScratchBuf],
1500) {
1501    if !none_free && !step.accepts_none() && step.input_slots().iter().any(|&s| none[s]) {
1502        for &s in step.output_slots() {
1503            none[s] = true;
1504        }
1505        return;
1506    }
1507    match step {
1508        #[cfg(feature = "jit")]
1509        HybridStep::Jit(seg) => {
1510            // Through the setjmp wrapper when the code calls a helper,
1511            // so its failure is the longjmp the kernel catches rather
1512            // than an abort; bare when it calls nothing.
1513            let code_fn = seg.code_fn;
1514            let buf_const = buffer.as_ptr();
1515            let buf_mut = buffer.as_mut_ptr();
1516            let sc = scratch.as_mut_ptr();
1517            if seg.fallible {
1518                crate::compile::jit::invoke_with_catch(move || unsafe {
1519                    (code_fn)(buf_const, buf_mut, sc);
1520                });
1521            } else {
1522                unsafe { (code_fn)(buf_const, buf_mut, sc) };
1523            }
1524        }
1525        HybridStep::Closure(cs) => {
1526            for (i, &slot) in cs.input_slots.iter().enumerate() {
1527                gather[i] = buffer[slot];
1528            }
1529            match &cs.op {
1530                ClosureOp::U64(op) => op(
1531                    &gather[..cs.input_slots.len()],
1532                    &mut scatter[..cs.output_slots.len()],
1533                ),
1534                ClosureOp::Slot(op) => op(
1535                    &gather[..cs.input_slots.len()],
1536                    &mut scatter[..cs.output_slots.len()],
1537                    &mut scratch[cs.scratch_range.0..cs.scratch_range.1],
1538                ),
1539            }
1540            for (i, &slot) in cs.output_slots.iter().enumerate() {
1541                buffer[slot] = scatter[i];
1542            }
1543        }
1544    }
1545    if !none_free {
1546        for &s in step.output_slots() {
1547            none[s] = false;
1548        }
1549    }
1550}