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 monomorphic kernel types, each with no runtime branching:
15//!
16//! | Type | Push (per-step skip) | Pull (cone guard) |
17//! |------|---------------------|-------------------|
18//! | `HybridKernelRaw` | — | — |
19//! | `HybridKernelPull` | — | yes |
20//! | `HybridKernelPushPull` | yes | yes |
21
22use std::collections::HashMap;
23
24use crate::ast::SlotShape;
25use crate::ast::{CompiledU64Op, PolydatNode};
26use crate::kernel::WireSource;
27
28#[cfg(feature = "jit")]
29use crate::compile::jit::{self, JitOp};
30
31/// A step in the hybrid kernel: either JIT native code or a Phase 2 closure.
32enum HybridStep {
33    /// A batch of nodes compiled to native code via Cranelift.
34    /// The function reads/writes directly to the shared buffer.
35    #[cfg(feature = "jit")]
36    Jit(JitSegment),
37    /// A single node executed via its Phase 2 closure.
38    Closure(ClosureStep),
39}
40
41#[cfg(feature = "jit")]
42struct JitSegment {
43    code_fn: crate::compile::jit::NativeFn,
44    /// The finalized native code, shared by every kernel created from
45    /// one program.
46    _module: crate::compile::jit::JitCode,
47    /// Whether the code calls a helper, and so runs under the longjmp
48    /// catch; code with no call runs bare.
49    fallible: bool,
50    /// The slots the segment reads and writes, for cones and for the
51    /// `None` check native code cannot make itself.
52    input_slots: Vec<usize>,
53    output_slots: Vec<usize>,
54    /// The program nodes in the segment, in step order; the tracker
55    /// slot names the one a failure belongs to.
56    nodes: Vec<usize>,
57}
58
59impl HybridStep {
60    fn input_slots(&self) -> &[usize] {
61        match self {
62            #[cfg(feature = "jit")]
63            HybridStep::Jit(seg) => &seg.input_slots,
64            HybridStep::Closure(cs) => &cs.input_slots,
65        }
66    }
67    fn output_slots(&self) -> &[usize] {
68        match self {
69            #[cfg(feature = "jit")]
70            HybridStep::Jit(seg) => &seg.output_slots,
71            HybridStep::Closure(cs) => &cs.output_slots,
72        }
73    }
74    /// SRD-74 Rule 2: the step runs on `None` inputs. Native code never
75    /// does; a node downstream of an unset extern is a closure.
76    fn accepts_none(&self) -> bool {
77        match self {
78            #[cfg(feature = "jit")]
79            HybridStep::Jit(_) => false,
80            HybridStep::Closure(cs) => cs.accepts_none,
81        }
82    }
83
84    /// The program node a failure in this step belongs to: a closure's
85    /// own, or the member native code named in the tracker slot.
86    #[cfg_attr(not(feature = "jit"), allow(unused_variables))]
87    fn failing_node(&self, buffer: &[u64], tracker: usize) -> usize {
88        match self {
89            #[cfg(feature = "jit")]
90            HybridStep::Jit(seg) => seg
91                .nodes
92                .get(buffer[tracker] as usize)
93                .copied()
94                .unwrap_or(usize::MAX),
95            HybridStep::Closure(cs) => cs.node,
96        }
97    }
98}
99
100/// A closure step's op: pure-scalar u64 closure, or a slot op
101/// with kernel-owned scratch for typed-slice ports (§8.4 L3).
102enum ClosureOp {
103    U64(CompiledU64Op),
104    Slot(crate::ast::CompiledSlotOp),
105}
106
107struct ClosureStep {
108    op: ClosureOp,
109    input_slots: Vec<usize>,
110    output_slots: Vec<usize>,
111    /// `[start, end)` into the kernel's scratch arena.
112    scratch_range: (usize, usize),
113    /// SRD-74 Rule 2: the closure runs on `None` inputs.
114    accepts_none: bool,
115    /// The program node, for the failure path.
116    node: usize,
117}
118
119/// An output resolved for the index-keyed pull: its slot, its type,
120/// and the steps of its cone.
121type ResolvedOutput = (usize, crate::ast::PortType, Option<std::sync::Arc<[usize]>>);
122
123/// Common fields shared by all hybrid kernel variants. A clone is a new
124/// state of the same program: the steps and the nodes are shared,
125/// everything else is the clone's own (engine_parity.md, step 4), and
126/// every pair in its buffer points into its own storage (axiom S3),
127/// never into the state it was cloned from.
128struct HybridCore {
129    buffer: Vec<u64>,
130    coord_count: usize,
131    steps: std::sync::Arc<Vec<HybridStep>>,
132    output_map: HashMap<String, usize>,
133    gather_buf: Vec<u64>,
134    scatter_buf: Vec<u64>,
135    /// Kernel-owned vector storage; vector-producing ports'
136    /// (ptr, len) slots view entries here (§8.4 layer 3).
137    scratch: Vec<crate::ast::ScratchBuf>,
138    /// Axiom S2: per-slot Ref2 mask — raw readers panic on these.
139    ref_slots: Vec<bool>,
140    /// Axiom S9(a): (first slot of a Ref pair → scratch index).
141    ref_scratch: Vec<(usize, usize)>,
142    /// Port type of each named output, for `get_value`.
143    output_types: HashMap<String, crate::ast::PortType>,
144    /// The extern inputs, written through at every set.
145    externs: crate::compile::externs::Externs,
146    /// The traversals the program declares (SRD 113), opened through the
147    /// `Kernel` trait.
148    traversals: std::sync::Arc<[crate::dsl::traversal::Traversal]>,
149    /// Per declared output, its slot, type, and cone, resolved on the
150    /// first index-keyed pull (SRD 117 step 3).
151    resolved_outputs: Vec<Option<ResolvedOutput>>,
152    /// Keep source nodes alive so JIT-baked pointers remain valid.
153    _nodes: std::sync::Arc<Vec<Box<dyn PolydatNode>>>,
154    /// The coordinates set through the `Kernel` trait, pending
155    /// evaluation; `stale` means a write happened since the last
156    /// evaluation round.
157    drive: crate::compile::Drive,
158    /// Per slot: the slot holds `None` (SRD-74 on a compiled kernel).
159    none: Vec<bool>,
160    /// Per step: the evaluation round it last ran in, so a new round
161    /// forgets every run without a scan.
162    ran: Vec<u64>,
163    /// The evaluation round: advanced by the first evaluation after a
164    /// write, so a mode without per-step currency runs a step once per
165    /// round rather than once per reader. Bookkeeping only: it wipes
166    /// nothing, and every output stands until an input in its
167    /// provenance is written. 0 is never a round.
168    epoch: u64,
169    /// Every step ran in the round: a full evaluation happened.
170    all_ran: bool,
171    /// Per step: its outputs are current for the inputs it depends on.
172    /// Cleared through the plan when an input changes, whichever call
173    /// changed it; never set for a volatile step.
174    clean: Vec<bool>,
175    /// Whether this kernel's provenance mode skips current steps.
176    use_clean: bool,
177    /// The dirty-register plan: what each input invalidates, what each
178    /// output needs.
179    plan: std::sync::Arc<crate::compile::Invalidation>,
180    /// Per step: nondeterministic or downstream of one, never current.
181    volatile: std::sync::Arc<[bool]>,
182    /// Per step: a side channel, skipped when current in every mode.
183    side: std::sync::Arc<[bool]>,
184    /// Per slot: the step that writes it.
185    slot_step: std::sync::Arc<[Option<usize>]>,
186    /// Where each step came from, for the failure path (A7).
187    sites: std::sync::Arc<crate::compile::Attribution>,
188    /// The step running, for the failure path.
189    cur_step: usize,
190    /// The slot past the layout where a segment names the member it is
191    /// in before calling a helper.
192    tracker: usize,
193    /// Every step, in order: what `eval` runs.
194    all: std::sync::Arc<[usize]>,
195    /// Per input slot, the steps an input change marks not current:
196    /// the plan's dependents in a push mode; in a raw or pull-only
197    /// mode, which never consult a pure step's currency, only the side
198    /// channels among them (an optimization over the plan, not a change
199    /// to it).
200    dirty: std::sync::Arc<[Vec<usize>]>,
201    /// Some slot holds `None`: an unset extern, which is
202    /// the only way one enters (SRD-74). When none does, the steps run
203    /// without the mask.
204    any_none: bool,
205    /// The steps that are never current, invalidated at every round.
206    volatile_steps: std::sync::Arc<[usize]>,
207}
208
209impl Clone for HybridCore {
210    fn clone(&self) -> Self {
211        let mut core = HybridCore {
212            buffer: self.buffer.clone(),
213            coord_count: self.coord_count,
214            steps: self.steps.clone(),
215            output_map: self.output_map.clone(),
216            gather_buf: self.gather_buf.clone(),
217            scatter_buf: self.scatter_buf.clone(),
218            scratch: self.scratch.clone(),
219            ref_slots: self.ref_slots.clone(),
220            ref_scratch: self.ref_scratch.clone(),
221            output_types: self.output_types.clone(),
222            externs: self.externs.clone(),
223            traversals: self.traversals.clone(),
224            resolved_outputs: self.resolved_outputs.clone(),
225            _nodes: self._nodes.clone(),
226            drive: self.drive.clone(),
227            none: self.none.clone(),
228            ran: self.ran.clone(),
229            epoch: self.epoch,
230            all_ran: self.all_ran,
231            clean: self.clean.clone(),
232            use_clean: self.use_clean,
233            plan: self.plan.clone(),
234            volatile: self.volatile.clone(),
235            side: self.side.clone(),
236            slot_step: self.slot_step.clone(),
237            sites: self.sites.clone(),
238            cur_step: self.cur_step,
239            tracker: self.tracker,
240            all: self.all.clone(),
241            dirty: self.dirty.clone(),
242            any_none: self.any_none,
243            volatile_steps: self.volatile_steps.clone(),
244        };
245        core.republish_refs();
246        core
247    }
248}
249
250impl HybridCore {
251    /// Point every pair in the buffer into this state's own storage: a
252    /// step's scratch entry for its `Ref2` outputs, the stored value
253    /// for an extern's (axiom S3). What a clone needs, whose buffer
254    /// was copied from a state whose storage it does not share.
255    fn republish_refs(&mut self) {
256        for &(slot, idx) in &self.ref_scratch {
257            let (p, l) = self.scratch[idx].ptr_len();
258            self.buffer[slot] = p;
259            self.buffer[slot + 1] = l;
260        }
261        self.externs.seed(&mut self.buffer, None);
262    }
263}
264
265impl HybridCore {
266    /// Axiom S9(a) — deterministic Ref validation (see
267    /// `jit_boundary.md` §"Slot-state axioms"). Gated to
268    /// `debug_assertions` to match its call sites, which compile
269    /// out in release.
270    #[cfg(debug_assertions)]
271    fn validate_refs(&self) {
272        // A step that has never run, or that propagated `None`, left
273        // its slots as they were.
274        let skip = |slot: usize| {
275            self.none[slot]
276                || matches!(self.slot_step.get(slot), Some(Some(step)) if self.ran[*step] == 0)
277        };
278        for &(slot, idx) in &self.ref_scratch {
279            if skip(slot) {
280                continue;
281            }
282            let (p, l) = self.scratch[idx].ptr_len();
283            assert!(
284                self.buffer[slot] == p && self.buffer[slot + 1] == l,
285                "S9 ref-validator: slot pair ({slot}, {}) = ({:#x}, {}) \
286                 does not match scratch[{idx}] = ({p:#x}, {l})",
287                slot + 1,
288                self.buffer[slot],
289                self.buffer[slot + 1],
290            );
291        }
292    }
293
294    /// Axiom S2 guard for raw u64 readers.
295    #[inline]
296    fn guard_ref_slot(&self, slot: usize) {
297        if self.ref_slots.get(slot).copied().unwrap_or(false) {
298            panic!(
299                "S2 pointer containment: slot {slot} is Ref2-colored; raw u64 readers \
300                 would leak an interior address. Use the typed borrow-checked accessor \
301                 (read_vec_*), the boundary decode, or copy out."
302            );
303        }
304    }
305
306    /// Axiom S2 typed accessor core (borrow ties to &self).
307    fn ref_entry(&self, slot: usize) -> &crate::ast::ScratchBuf {
308        match self.ref_scratch.iter().find(|(s, _)| *s == slot) {
309            Some(&(_, idx)) => &self.scratch[idx],
310            None if self.ref_slots.get(slot).copied().unwrap_or(false) => panic!(
311                "slot {slot} is a Ref pair owned by the CALLER (a kernel \
312                 input) — read it on the caller side"
313            ),
314            None => panic!("slot {slot} is not a Ref2-colored slot"),
315        }
316    }
317}
318
319impl HybridCore {
320    /// Begin an evaluation round after a write: take what cells other
321    /// holders published, forget what ran in the last round, and
322    /// invalidate the volatile steps, as the interpreter does at every
323    /// write. Nothing else changes: every output stands until an input
324    /// in its provenance is written (runtime_model.md, R1).
325    #[inline]
326    fn begin_epoch(&mut self) {
327        if self.externs.cells_dirty() {
328            self.externs.refresh_cells(&mut self.buffer);
329        }
330        self.dirty_refreshed();
331        self.epoch += 1;
332        self.all_ran = false;
333        for &i in self.volatile_steps.iter() {
334            self.clean[i] = false;
335        }
336        self.drive.stale = false;
337    }
338
339    /// Whether this kernel skips current steps, and with it which steps
340    /// an input change marks: every dependent, or only the side
341    /// channels when a pure step's currency is never consulted.
342    #[cfg(feature = "jit")]
343    fn set_use_clean(&mut self, on: bool) {
344        self.use_clean = on;
345        let side = std::sync::Arc::clone(&self.side);
346        self.dirty = self
347            .plan
348            .input_dependents
349            .iter()
350            .map(|deps| {
351                if on {
352                    deps.clone()
353                } else {
354                    deps.iter().copied().filter(|&i| side[i]).collect()
355                }
356            })
357            .collect::<Vec<_>>()
358            .into();
359    }
360
361    /// Every dependent of a slot a cell refresh changed runs again,
362    /// between writes too, as the interpreter re-evaluates a node
363    /// whose cell moved on its next read: the plan's dependents, whatever
364    /// the mode, are neither run nor current.
365    #[inline]
366    fn dirty_refreshed(&mut self) {
367        if !self.externs.has_changed() {
368            return;
369        }
370        let changed = self.externs.take_changed();
371        for &slot in &changed {
372            if let Some(deps) = self.plan.input_dependents.get(slot) {
373                for &i in deps {
374                    self.ran[i] = 0;
375                    self.clean[i] = false;
376                }
377                self.all_ran = false;
378            }
379        }
380        self.externs.return_changed(changed);
381    }
382
383    /// Take the current value of every cell another holder published
384    /// to, and mark its dependents, so a pull between writes sees the
385    /// register as the interpreter's revision check does.
386    #[inline]
387    fn refresh_cells(&mut self) {
388        if self.externs.cells_dirty() {
389            self.externs.refresh_cells(&mut self.buffer);
390            self.dirty_refreshed();
391        }
392    }
393
394    /// Bind a `shared` binding to `cell` (engine parity, step 9): this
395    /// kernel reads and writes that register from now on.
396    fn attach_cell(&mut self, name: &str, cell: crate::kernel::SharedCell) -> Result<(), String> {
397        let slot = self.externs.attach_cell(name, cell)?;
398        self.dirty_input(slot);
399        self.drive.stale = true;
400        Ok(())
401    }
402
403    /// An input slot changed, through whichever call: every step the
404    /// plan lists for it is no longer current.
405    #[inline]
406    fn dirty_input(&mut self, slot: usize) {
407        if let Some(deps) = self.dirty.get(slot) {
408            for &i in deps {
409                self.clean[i] = false;
410            }
411        }
412    }
413
414    /// Run the steps of `order` that have not run in this round and are
415    /// not current, as the closure kernels do: one rule for every step,
416    /// whatever reaches it; a volatile step is never current.
417    #[inline]
418    fn run_steps(&mut self, order: &[usize]) {
419        self.run_guarded(|core| core.run_order(order));
420    }
421
422    /// Run `body` with the capture guard armed, so a step's panic is
423    /// recorded quietly and re-raised enriched, as the interpreter
424    /// re-raises a node's (A7).
425    #[inline]
426    fn run_guarded(&mut self, body: impl FnOnce(&mut Self)) {
427        let capture = crate::kernel::engines::EvalPanicCaptureGuard::arm();
428        let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| body(self)));
429        drop(capture);
430        if let Err(payload) = outcome {
431            let sites = std::sync::Arc::clone(&self.sites);
432            let node = self.steps[self.cur_step].failing_node(&self.buffer, self.tracker);
433            sites.reraise(payload, node, &self.buffer, Some(&self.none));
434        }
435        #[cfg(debug_assertions)]
436        self.validate_refs();
437    }
438
439    /// The steps of `order` that have not run in the round, in order.
440    #[inline]
441    fn run_order(&mut self, order: &[usize]) {
442        let steps = &self.steps;
443        let none_free = !self.any_none;
444        for &i in order {
445            if self.all_ran || self.ran[i] == self.epoch {
446                continue;
447            }
448            let never = self.volatile[i];
449            if (self.use_clean || self.side[i]) && self.clean[i] && !never {
450                self.ran[i] = self.epoch;
451                continue;
452            }
453            self.cur_step = i;
454            run_hybrid_step(
455                &steps[i],
456                none_free,
457                &mut self.buffer,
458                &mut self.none,
459                &mut self.gather_buf,
460                &mut self.scatter_buf,
461                &mut self.scratch,
462            );
463            self.ran[i] = self.epoch;
464            self.clean[i] = !never;
465        }
466    }
467
468    /// Every step, in order, in a round just begun, in a mode without
469    /// per-step skipping and with no `None` in play: the same steps the
470    /// general loop would run, without the bookkeeping a partial round
471    /// needs. A current side channel is still skipped, since its run is
472    /// observed.
473    #[inline]
474    fn run_fresh(&mut self) {
475        let steps = &self.steps;
476        for (i, step) in steps.iter().enumerate() {
477            if self.side[i] {
478                let never = self.volatile[i];
479                if self.clean[i] && !never {
480                    continue;
481                }
482                self.clean[i] = !never;
483            }
484            self.cur_step = i;
485            run_hybrid_step(
486                step,
487                true,
488                &mut self.buffer,
489                &mut self.none,
490                &mut self.gather_buf,
491                &mut self.scatter_buf,
492                &mut self.scratch,
493            );
494        }
495        self.all_ran = true;
496    }
497
498    /// Evaluate every output: begin a round if a write is pending, then run
499    /// every step that has not run.
500    #[inline]
501    fn eval_all(&mut self) {
502        let fresh = self.drive.stale;
503        if fresh {
504            self.begin_epoch();
505        } else {
506            self.refresh_cells();
507        }
508        if fresh && !self.use_clean && !self.any_none {
509            self.run_guarded(|core| core.run_fresh());
510        } else {
511            let all = std::sync::Arc::clone(&self.all);
512            self.run_steps(&all);
513        }
514    }
515
516    /// The named output for the current inputs, running only its cone.
517    fn pull_named(&mut self, name: &str) -> crate::ast::Value {
518        if self.drive.stale {
519            self.begin_epoch();
520        } else {
521            self.refresh_cells();
522        }
523        let plan = std::sync::Arc::clone(&self.plan);
524        if let Some(order) = plan.cones.get(name) {
525            self.run_steps(order);
526        }
527        self.value_of(name)
528    }
529
530    /// [`Self::pull_named`] by output index: the name is resolved to
531    /// its slot, type, and cone once, so a pull costs no string lookup
532    /// (SRD 117 step 3).
533    fn pull_at(&mut self, index: usize) -> crate::ast::Value {
534        if self.resolved_outputs.len() <= index {
535            self.resolved_outputs.resize(index + 1, None);
536        }
537        if self.resolved_outputs[index].is_none() {
538            let name = self
539                .externs
540                .output_names()
541                .get(index)
542                .cloned()
543                .unwrap_or_else(|| {
544                    panic!(
545                        "no output at index {index}; this kernel declares {}",
546                        self.externs.output_names().len()
547                    )
548                });
549            let slot = self.output_map[&name];
550            let ty = self
551                .output_types
552                .get(&name)
553                .copied()
554                .unwrap_or(crate::ast::PortType::U64);
555            let cone = self
556                .plan
557                .cones
558                .get(&name)
559                .map(|c| std::sync::Arc::from(c.as_slice()));
560            self.resolved_outputs[index] = Some((slot, ty, cone));
561        }
562        if self.drive.stale {
563            self.begin_epoch();
564        } else {
565            self.refresh_cells();
566        }
567        let (slot, ty, cone) = self.resolved_outputs[index]
568            .clone()
569            .expect("resolved above");
570        if let Some(order) = cone {
571            self.run_steps(&order);
572        }
573        self.slot_value(slot, ty)
574    }
575
576    /// The named output as a typed `Value`, `None` where the slot holds
577    /// one; a vector from scratch; a handle copied out.
578    fn value_of(&self, name: &str) -> crate::ast::Value {
579        let slot = self.output_map[name];
580        let ty = self
581            .output_types
582            .get(name)
583            .copied()
584            .unwrap_or(crate::ast::PortType::U64);
585        self.slot_value(slot, ty)
586    }
587
588    /// The value at `slot` decoded as `ty`: `None` where the mask says
589    /// so, a Ref pair copied out through the pair.
590    fn slot_value(&self, slot: usize, ty: crate::ast::PortType) -> crate::ast::Value {
591        if self.none.get(slot).copied().unwrap_or(false) {
592            return crate::ast::Value::None;
593        }
594        crate::compile::marshal::decode_output(&self.buffer, slot, ty)
595    }
596
597    /// The native segments and the closure steps.
598    fn plan(&self) -> crate::EnginePlan {
599        let (native_segments, closure_steps) = self.engine_counts();
600        crate::EnginePlan {
601            native_segments,
602            closure_steps,
603            interpreted_nodes: 0,
604        }
605    }
606
607    /// Nothing is current: every step runs at the next evaluation.
608    fn invalidate_all(&mut self) {
609        self.clean.fill(false);
610        self.all_ran = false;
611        self.drive.stale = true;
612    }
613}
614
615/// Everything the evaluation loops once did, kept for the raw kernel's
616/// `eval`, which evaluates every step in a new round.
617#[inline]
618fn eval_all_hybrid_steps(core: &mut HybridCore) {
619    core.drive.stale = true;
620    core.eval_all();
621}
622
623// ═══════════════════════════════════════════════════════════════
624// Raw: no provenance, no cone guard. Eval runs all steps.
625// ═══════════════════════════════════════════════════════════════
626
627impl HybridCore {
628    /// How many steps run as native segments and how many as closures:
629    /// what the per-node engine choice decided for this graph.
630    fn engine_counts(&self) -> (usize, usize) {
631        let closures = self
632            .steps
633            .iter()
634            .filter(|s| matches!(s, HybridStep::Closure(_)))
635            .count();
636        (self.steps.len() - closures, closures)
637    }
638
639    /// Set an extern by name; returns its slot for dirty marking.
640    /// Set an extern by name; returns its slot. The plan invalidates
641    /// what depends on it, as a changed coordinate is invalidated, and
642    /// the next evaluation begins a round.
643    fn set_extern(&mut self, name: &str, value: crate::ast::Value) -> Result<usize, String> {
644        let (slot, unset) = self.externs.set(name, value, &mut self.buffer)?;
645        self.extern_written(slot, unset);
646        Ok(slot)
647    }
648
649    /// [`Self::set_extern`] by input index.
650    fn set_extern_at(&mut self, index: usize, value: crate::ast::Value) -> Result<usize, String> {
651        let (slot, unset) = self.externs.set_at(index, value, &mut self.buffer)?;
652        self.extern_written(slot, unset);
653        Ok(slot)
654    }
655
656    /// An extern was written: its dependents are no longer current, the
657    /// `None` mask records whether it is unset (SRD-74 on a compiled
658    /// kernel), and the next evaluation begins a round. When the last
659    /// unset extern is set, no slot can hold a `None` any more, so the
660    /// mask is cleared and the steps run without it.
661    fn extern_written(&mut self, slot: usize, unset: bool) {
662        self.none[slot] = unset;
663        let was = self.any_none;
664        self.any_none = self.externs.any_unset();
665        if was && !self.any_none {
666            self.none.fill(false);
667        }
668        self.dirty_input(slot);
669        self.drive.stale = true;
670    }
671}
672
673/// Hybrid kernel with no provenance tracking.
674///
675/// Every `eval()` call runs all steps unconditionally. Useful as a
676/// baseline and for graphs where inputs change on every evaluation.
677#[derive(Clone)]
678pub struct HybridKernelRaw {
679    core: HybridCore,
680}
681
682impl HybridKernelRaw {
683    /// The coordinates, written; a changed one invalidates
684    /// its dependents through the plan, as in every mode.
685    #[inline]
686    fn set_coords(&mut self, coords: &[u64]) {
687        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
688            if self.core.buffer[i] != c {
689                self.core.buffer[i] = c;
690                self.core.dirty_input(i);
691            }
692        }
693    }
694
695    /// Evaluate all hybrid steps unconditionally: a new round.
696    #[inline]
697    pub fn eval(&mut self, coords: &[u64]) {
698        self.set_coords(coords);
699        eval_all_hybrid_steps(&mut self.core);
700    }
701
702    #[cfg(feature = "jit")]
703    fn pull_output(&mut self, name: &str) -> crate::ast::Value {
704        self.core.pull_named(name)
705    }
706
707    /// Set an extern by name, as `PolydatState::set_input` does on the
708    /// interpreter. Every run evaluates everything, so it takes effect
709    /// at the next run.
710    pub fn set_input(&mut self, name: &str, value: crate::ast::Value) -> Result<(), String> {
711        self.core.set_extern(name, value).map(|_| ())
712    }
713
714    /// [`Self::set_input`] by input index.
715    pub fn set_input_at(&mut self, index: usize, value: crate::ast::Value) -> Result<(), String> {
716        self.core.set_extern_at(index, value).map(|_| ())
717    }
718
719    /// The kernel's externs by name and declared type.
720    pub fn externs(&self) -> Vec<(&str, crate::ast::PortType)> {
721        self.core.externs.names()
722    }
723
724    /// The cursors the program declares, with the partitions the
725    /// compiler resolved where its `over` clause and extent were
726    /// constant, as `PolydatProgram::cursor_schemas` reports them.
727    pub fn cursor_schemas(&self) -> &[crate::iteration::source::SourceSchema] {
728        self.core.externs.cursor_schemas()
729    }
730
731    /// Narrow a cursor to one partition, as `narrow_cursor` does on
732    /// the interpreter: its `Ext` slot and six scalar projections are
733    /// set as externs.
734    pub fn set_cursor(
735        &mut self,
736        name: &str,
737        partition: &crate::iteration::cursor_partition::Partition,
738    ) -> Result<(), String> {
739        for (slot, value) in self.core.externs.cursor_writes(name, partition)? {
740            self.set_input(&slot, value)?;
741        }
742        Ok(())
743    }
744
745    /// Eval all steps and return the value at `slot`.
746    #[inline]
747    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
748        self.core.guard_ref_slot(slot);
749        self.eval(coords);
750        self.core.buffer[slot]
751    }
752
753    /// Read a named output after `eval()`. Panics on Ref2 slots
754    /// (axiom S2) — use `read_vec_*`.
755    #[inline]
756    pub fn get(&self, name: &str) -> u64 {
757        let slot = self.core.output_map[name];
758        self.core.guard_ref_slot(slot);
759        self.core.buffer[slot]
760    }
761
762    /// Read by slot index. Panics on Ref2 slots (axiom S2) —
763    /// use `read_vec_*`.
764    #[inline]
765    pub fn get_slot(&self, slot: usize) -> u64 {
766        self.core.guard_ref_slot(slot);
767        self.core.buffer[slot]
768    }
769
770    crate::compile::ref_readers!();
771
772    /// The named output as a typed `Value`, decoded by its port type:
773    /// a handle slot is copied out of the arena or the value table
774    /// (SRD 115 §5), so the caller never holds a handle.
775    pub fn get_value(&self, name: &str) -> crate::ast::Value {
776        self.core.value_of(name)
777    }
778
779    /// Number of coordinate inputs.
780    pub fn coord_count(&self) -> usize {
781        self.core.coord_count
782    }
783
784    /// The number of native segments and of closure steps in this
785    /// kernel, in that order: what the per-node engine choice decided.
786    pub fn engine_counts(&self) -> (usize, usize) {
787        self.core.engine_counts()
788    }
789
790    /// Resolve an output name to its buffer slot.
791    pub fn resolve_output(&self, name: &str) -> Option<usize> {
792        self.core.output_map.get(name).copied()
793    }
794
795    /// Store owned nodes to keep JIT-baked pointers valid.
796    pub fn retain_nodes(&mut self, nodes: Vec<Box<dyn PolydatNode>>) {
797        self.core._nodes = std::sync::Arc::new(nodes);
798    }
799}
800
801// ═══════════════════════════════════════════════════════════════
802// Pull: cone guard only, no per-step skip.
803// set_inputs tracks changed_mask. eval_for_slot checks the cone
804// then runs ALL steps if dirty.
805// ═══════════════════════════════════════════════════════════════
806
807/// Hybrid kernel with pull-side cone guard.
808///
809/// `eval_for_slot()` checks whether the output's transitive input
810/// cone changed before running steps. If nothing in the cone changed,
811/// the cached value is returned without re-evaluation.
812#[derive(Clone)]
813pub struct HybridKernelPull {
814    core: HybridCore,
815    slot_provenance: Vec<crate::kernel::ProvMask>,
816    changed_mask: crate::kernel::ProvMask,
817    /// Set by `set_input`: an extern changed, so the next evaluation
818    /// runs whatever the cone guard says.
819    force_run: bool,
820}
821
822impl HybridKernelPull {
823    /// Track which inputs changed (for the cone guard), and invalidate
824    /// their dependents through the plan, as in every mode.
825    #[inline]
826    fn set_inputs(&mut self, coords: &[u64]) {
827        self.changed_mask.clear();
828        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
829            if self.core.buffer[i] != c {
830                self.core.buffer[i] = c;
831                self.changed_mask.set(i);
832                self.core.dirty_input(i);
833            }
834        }
835    }
836
837    /// Evaluate all steps (no cone guard): a new round.
838    #[inline]
839    pub fn eval(&mut self, coords: &[u64]) {
840        self.set_inputs(coords);
841        self.force_run = false;
842        eval_all_hybrid_steps(&mut self.core);
843    }
844
845    fn pull_output(&mut self, name: &str) -> crate::ast::Value {
846        self.core.pull_named(name)
847    }
848
849    /// Cone guard: if the output's cone is clean, skip eval entirely.
850    /// Otherwise run ALL steps (no per-step skip).
851    #[inline]
852    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
853        self.core.guard_ref_slot(slot);
854        self.set_inputs(coords);
855        if !self.force_run
856            && slot < self.slot_provenance.len()
857            && !self.slot_provenance[slot].intersects(&self.changed_mask)
858        {
859            return self.core.buffer[slot];
860        }
861        self.force_run = false;
862        eval_all_hybrid_steps(&mut self.core);
863        self.core.buffer[slot]
864    }
865
866    /// Set an extern by name, as `PolydatState::set_input` does on the
867    /// interpreter. A carrier takes effect at once; a string, JSON, or
868    /// extension value is written at the start of the next run, which
869    /// runs whatever the cone guard says.
870    pub fn set_input(&mut self, name: &str, value: crate::ast::Value) -> Result<(), String> {
871        self.core.set_extern(name, value)?;
872        self.force_run = true;
873        Ok(())
874    }
875
876    /// [`Self::set_input`] by input index.
877    pub fn set_input_at(&mut self, index: usize, value: crate::ast::Value) -> Result<(), String> {
878        self.core.set_extern_at(index, value)?;
879        self.force_run = true;
880        Ok(())
881    }
882
883    /// The kernel's externs by name and declared type.
884    pub fn externs(&self) -> Vec<(&str, crate::ast::PortType)> {
885        self.core.externs.names()
886    }
887
888    /// The cursors the program declares, with the partitions the
889    /// compiler resolved where its `over` clause and extent were
890    /// constant, as `PolydatProgram::cursor_schemas` reports them.
891    pub fn cursor_schemas(&self) -> &[crate::iteration::source::SourceSchema] {
892        self.core.externs.cursor_schemas()
893    }
894
895    /// Narrow a cursor to one partition, as `narrow_cursor` does on
896    /// the interpreter: its `Ext` slot and six scalar projections are
897    /// set as externs.
898    pub fn set_cursor(
899        &mut self,
900        name: &str,
901        partition: &crate::iteration::cursor_partition::Partition,
902    ) -> Result<(), String> {
903        for (slot, value) in self.core.externs.cursor_writes(name, partition)? {
904            self.set_input(&slot, value)?;
905        }
906        Ok(())
907    }
908
909    /// Read a named output after `eval()`. Panics on Ref2 slots
910    /// (axiom S2) — use `read_vec_*`.
911    #[inline]
912    pub fn get(&self, name: &str) -> u64 {
913        let slot = self.core.output_map[name];
914        self.core.guard_ref_slot(slot);
915        self.core.buffer[slot]
916    }
917
918    /// Read by slot index. Panics on Ref2 slots (axiom S2) —
919    /// use `read_vec_*`.
920    #[inline]
921    pub fn get_slot(&self, slot: usize) -> u64 {
922        self.core.guard_ref_slot(slot);
923        self.core.buffer[slot]
924    }
925
926    crate::compile::ref_readers!();
927
928    /// The named output as a typed `Value`, decoded by its port type:
929    /// a handle slot is copied out of the arena or the value table
930    /// (SRD 115 §5), so the caller never holds a handle.
931    pub fn get_value(&self, name: &str) -> crate::ast::Value {
932        self.core.value_of(name)
933    }
934
935    /// Number of coordinate inputs.
936    pub fn coord_count(&self) -> usize {
937        self.core.coord_count
938    }
939
940    /// The number of native segments and of closure steps in this
941    /// kernel, in that order: what the per-node engine choice decided.
942    pub fn engine_counts(&self) -> (usize, usize) {
943        self.core.engine_counts()
944    }
945
946    /// Resolve an output name to its buffer slot.
947    pub fn resolve_output(&self, name: &str) -> Option<usize> {
948        self.core.output_map.get(name).copied()
949    }
950
951    /// Store owned nodes to keep JIT-baked pointers valid.
952    pub fn retain_nodes(&mut self, nodes: Vec<Box<dyn PolydatNode>>) {
953        self.core._nodes = std::sync::Arc::new(nodes);
954    }
955}
956
957// ═══════════════════════════════════════════════════════════════
958// PushPull: push-side per-step skip + pull-side cone guard.
959// Full optimization — the production default.
960// ═══════════════════════════════════════════════════════════════
961
962/// Hybrid kernel with both push-side per-step skip and pull-side cone guard.
963///
964/// Push side: `set_inputs()` marks only steps that depend on changed inputs
965/// as dirty; clean steps are skipped during `eval()`.
966///
967/// Pull side: `eval_for_slot()` first checks whether the output's cone of
968/// influence changed at all. If not, the cached value is returned without
969/// entering the eval loop.
970#[derive(Clone)]
971pub struct HybridKernelPushPull {
972    core: HybridCore,
973    slot_provenance: Vec<crate::kernel::ProvMask>,
974    changed_mask: crate::kernel::ProvMask,
975    /// Set by `set_input`: an extern changed, so the next evaluation
976    /// runs whatever the cone guard says.
977    force_run: bool,
978}
979
980impl HybridKernelPushPull {
981    /// Set an extern by name, as `PolydatState::set_input` does on the
982    /// interpreter. A carrier takes effect at once; a string, JSON, or
983    /// extension value is written at the start of the next run. Every
984    /// step downstream of the extern reruns, and the next evaluation
985    /// runs whatever the cone guard says.
986    pub fn set_input(&mut self, name: &str, value: crate::ast::Value) -> Result<(), String> {
987        self.core.set_extern(name, value)?;
988        self.force_run = true;
989        Ok(())
990    }
991
992    /// [`Self::set_input`] by input index.
993    pub fn set_input_at(&mut self, index: usize, value: crate::ast::Value) -> Result<(), String> {
994        self.core.set_extern_at(index, value)?;
995        self.force_run = true;
996        Ok(())
997    }
998
999    /// The kernel's externs by name and declared type.
1000    pub fn externs(&self) -> Vec<(&str, crate::ast::PortType)> {
1001        self.core.externs.names()
1002    }
1003
1004    /// The cursors the program declares, with the partitions the
1005    /// compiler resolved where its `over` clause and extent were
1006    /// constant, as `PolydatProgram::cursor_schemas` reports them.
1007    pub fn cursor_schemas(&self) -> &[crate::iteration::source::SourceSchema] {
1008        self.core.externs.cursor_schemas()
1009    }
1010
1011    /// Narrow a cursor to one partition, as `narrow_cursor` does on
1012    /// the interpreter: its `Ext` slot and six scalar projections are
1013    /// set as externs.
1014    pub fn set_cursor(
1015        &mut self,
1016        name: &str,
1017        partition: &crate::iteration::cursor_partition::Partition,
1018    ) -> Result<(), String> {
1019        for (slot, value) in self.core.externs.cursor_writes(name, partition)? {
1020            self.set_input(&slot, value)?;
1021        }
1022        Ok(())
1023    }
1024
1025    /// Track which inputs changed and dirty affected steps.
1026    #[inline]
1027    fn set_inputs(&mut self, coords: &[u64]) {
1028        self.changed_mask.clear();
1029        for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
1030            if self.core.buffer[i] != c {
1031                self.core.buffer[i] = c;
1032                self.changed_mask.set(i);
1033                self.core.dirty_input(i);
1034            }
1035        }
1036    }
1037
1038    /// Evaluate with push-side step skip (no cone guard): a new round.
1039    #[inline]
1040    pub fn eval(&mut self, coords: &[u64]) {
1041        self.set_inputs(coords);
1042        self.force_run = false;
1043        self.core.drive.stale = true;
1044        self.core.eval_all();
1045    }
1046
1047    fn pull_output(&mut self, name: &str) -> crate::ast::Value {
1048        self.core.pull_named(name)
1049    }
1050
1051    /// Cone guard + push-side skip: the full optimization.
1052    #[inline]
1053    pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
1054        self.core.guard_ref_slot(slot);
1055        self.set_inputs(coords);
1056        if !self.force_run
1057            && slot < self.slot_provenance.len()
1058            && !self.slot_provenance[slot].intersects(&self.changed_mask)
1059        {
1060            return self.core.buffer[slot];
1061        }
1062        self.force_run = false;
1063        self.core.drive.stale = true;
1064        self.core.eval_all();
1065        self.core.buffer[slot]
1066    }
1067
1068    /// Read a named output after `eval()`. Panics on Ref2 slots
1069    /// (axiom S2) — use `read_vec_*`.
1070    #[inline]
1071    pub fn get(&self, name: &str) -> u64 {
1072        let slot = self.core.output_map[name];
1073        self.core.guard_ref_slot(slot);
1074        self.core.buffer[slot]
1075    }
1076
1077    /// Read by slot index. Panics on Ref2 slots (axiom S2) —
1078    /// use `read_vec_*`.
1079    #[inline]
1080    pub fn get_slot(&self, slot: usize) -> u64 {
1081        self.core.guard_ref_slot(slot);
1082        self.core.buffer[slot]
1083    }
1084
1085    crate::compile::ref_readers!();
1086
1087    /// The named output as a typed `Value`, decoded by its port type:
1088    /// a handle slot is copied out of the arena or the value table
1089    /// (SRD 115 §5), so the caller never holds a handle.
1090    pub fn get_value(&self, name: &str) -> crate::ast::Value {
1091        self.core.value_of(name)
1092    }
1093
1094    /// Number of coordinate inputs.
1095    pub fn coord_count(&self) -> usize {
1096        self.core.coord_count
1097    }
1098
1099    /// The number of native segments and of closure steps in this
1100    /// kernel, in that order: what the per-node engine choice decided.
1101    pub fn engine_counts(&self) -> (usize, usize) {
1102        self.core.engine_counts()
1103    }
1104
1105    /// Resolve an output name to its buffer slot.
1106    pub fn resolve_output(&self, name: &str) -> Option<usize> {
1107        self.core.output_map.get(name).copied()
1108    }
1109
1110    /// Store owned nodes to keep JIT-baked pointers valid.
1111    pub fn retain_nodes(&mut self, nodes: Vec<Box<dyn PolydatNode>>) {
1112        self.core._nodes = std::sync::Arc::new(nodes);
1113    }
1114}
1115
1116/// Type alias for the default hybrid kernel (PushPull — full optimization).
1117///
1118/// Assembler and bench code that references `HybridKernel` uses the full
1119/// push+pull variant. Rename uses to the concrete type if different
1120/// optimization trade-offs are needed.
1121pub type HybridKernel = HybridKernelPushPull;
1122
1123/// Flattened slot list for one node's wire inputs under per-port
1124/// widths (type_system_alignment.md §8.4 layer 1): every source
1125/// contributes `slot_width` consecutive slots.
1126fn flatten_input_slots(
1127    wiring: &[Vec<WireSource>],
1128    nodes: &[Box<dyn PolydatNode>],
1129    node_idx: usize,
1130    port_offsets: &[Vec<usize>],
1131    input_starts: &[usize],
1132    input_widths: &[usize],
1133) -> Vec<usize> {
1134    let mut slots = Vec::new();
1135    for source in &wiring[node_idx] {
1136        let (start, w) = match source {
1137            WireSource::Input(c) => (
1138                input_starts.get(*c).copied().unwrap_or(*c),
1139                input_widths.get(*c).copied().unwrap_or(1),
1140            ),
1141            WireSource::NodeOutput(u, p) => (
1142                port_offsets[*u][*p],
1143                nodes[*u].meta().outs[*p].typ.slot_width(),
1144            ),
1145        };
1146        slots.extend(start..start + w);
1147    }
1148    slots
1149}
1150
1151/// First slot of each Ref2-colored output port of one node, in
1152/// port order (axiom S3 pairing with CompiledSlotKit scratch).
1153fn flatten_ref_output_starts(
1154    nodes: &[Box<dyn PolydatNode>],
1155    node_idx: usize,
1156    port_offsets: &[Vec<usize>],
1157) -> Vec<usize> {
1158    nodes[node_idx]
1159        .meta()
1160        .outs
1161        .iter()
1162        .enumerate()
1163        .filter(|(_, out)| out.typ.slot_color() == crate::ast::SlotColor::Ref2)
1164        .map(|(p, _)| port_offsets[node_idx][p])
1165        .collect()
1166}
1167
1168/// Flattened slot list for one node's outputs.
1169fn flatten_output_slots(
1170    nodes: &[Box<dyn PolydatNode>],
1171    node_idx: usize,
1172    port_offsets: &[Vec<usize>],
1173) -> Vec<usize> {
1174    let mut slots = Vec::new();
1175    for (p, out) in nodes[node_idx].meta().outs.iter().enumerate() {
1176        let start = port_offsets[node_idx][p];
1177        slots.extend(start..start + out.typ.slot_width());
1178    }
1179    slots
1180}
1181
1182/// Build a hybrid kernel from resolved DAG data.
1183///
1184/// Each node is classified: if it can be JIT-compiled, it goes into
1185/// a JIT segment. If not, it becomes a closure step. Adjacent JIT-able
1186/// nodes are batched into a single JIT segment for efficiency.
1187///
1188/// Returns a `HybridKernelPushPull` (the production default).
1189#[cfg(feature = "jit")]
1190#[allow(clippy::too_many_arguments)]
1191pub(crate) fn build_hybrid(
1192    nodes: &[Box<dyn PolydatNode>],
1193    wiring: &[Vec<WireSource>],
1194    coord_count: usize,
1195    total_slots: usize,
1196    port_offsets: &[Vec<usize>],
1197    input_starts: &[usize],
1198    input_widths: &[usize],
1199    output_map: HashMap<String, usize>,
1200    ref_slots: Vec<bool>,
1201    input_types: &[crate::ast::PortType],
1202    externs: crate::compile::externs::Externs,
1203    constant: Vec<bool>,
1204    volatile: Vec<bool>,
1205    attribution: std::sync::Arc<crate::compile::Attribution>,
1206) -> Result<HybridKernelPushPull, String> {
1207    let mut steps: Vec<HybridStep> = Vec::new();
1208    let mut scratch: Vec<crate::ast::ScratchBuf> = Vec::new();
1209    let mut ref_scratch: Vec<(usize, usize)> = Vec::new();
1210    let mut max_inputs = 0usize;
1211    let mut max_outputs = 0usize;
1212
1213    // Classify each node
1214    let classifications: Vec<(JitOp, Vec<usize>, Vec<usize>)> = nodes
1215        .iter()
1216        .enumerate()
1217        .map(|(node_idx, node)| {
1218            // Classified with the wire types known (SRD 115 §6.1), as
1219            // cones and pure-P3 layouts are: a variadic node whose
1220            // wires its helper cannot decode falls back to its closure.
1221            let wire_types: Vec<crate::ast::PortType> = wiring[node_idx]
1222                .iter()
1223                .map(|src| match src {
1224                    WireSource::Input(c) => input_types
1225                        .get(*c)
1226                        .copied()
1227                        .unwrap_or(crate::ast::PortType::U64),
1228                    WireSource::NodeOutput(j, p) => nodes[*j].meta().outs[*p].typ,
1229                })
1230                .collect();
1231            let jit_op = jit::classify_node_typed(node.as_ref(), &wire_types);
1232
1233            let input_slots = flatten_input_slots(
1234                wiring,
1235                nodes,
1236                node_idx,
1237                port_offsets,
1238                input_starts,
1239                input_widths,
1240            );
1241            let output_slots = flatten_output_slots(nodes, node_idx, port_offsets);
1242
1243            max_inputs = max_inputs.max(input_slots.len());
1244            max_outputs = max_outputs.max(output_slots.len());
1245
1246            (jit_op, input_slots, output_slots)
1247        })
1248        .collect();
1249    // A node downstream of an extern with no value runs as a closure:
1250    // a `None` propagates through closures as it does on the
1251    // interpreter (SRD-74), and native code cannot carry one
1252    // (engine_parity.md, A12).
1253    let mut classifications = classifications;
1254    let unset = externs.unset_slots();
1255    if !unset.is_empty() {
1256        let mut tainted = vec![false; nodes.len()];
1257        for node_idx in 0..nodes.len() {
1258            tainted[node_idx] = wiring[node_idx].iter().any(|src| match src {
1259                WireSource::Input(c) => unset.contains(&input_starts[*c]),
1260                WireSource::NodeOutput(j, _) => tainted[*j],
1261            });
1262            if tainted[node_idx] {
1263                classifications[node_idx].0 = JitOp::Fallback;
1264            }
1265        }
1266    }
1267
1268    // Per node, the step it runs in: its own closure step or its segment.
1269    let mut node_step = vec![usize::MAX; nodes.len()];
1270    // The step order: every compile-constant node first, then the rest
1271    // in the graph's order. A constant depends on constants alone, so
1272    // hoisting them keeps every dependency ahead of its consumer, and
1273    // it keeps the cycle-time nodes contiguous: a literal between two
1274    // cycle-time statements no longer cuts a segment in two (the tile
1275    // ladder's twenty-hole case ran as dozens of segments that way,
1276    // each paying the segment's catch and step bookkeeping).
1277    let order: Vec<usize> = (0..nodes.len())
1278        .filter(|&k| constant[k])
1279        .chain((0..nodes.len()).filter(|&k| !constant[k]))
1280        .collect();
1281    // Batch adjacent JIT-able nodes into segments
1282    let mut pos = 0;
1283    while pos < order.len() {
1284        let i = order[pos];
1285        if matches!(classifications[i].0, JitOp::Fallback) {
1286            // This node needs a closure — scalar u64 op preferred,
1287            // slot op for slice-bearing nodes (§8.4 layer 3).
1288            let node = &nodes[i];
1289            let (_, ref input_slots, ref output_slots) = classifications[i];
1290            let scratch_start = scratch.len();
1291            let wire_types: Vec<crate::ast::PortType> = wiring[i]
1292                .iter()
1293                .map(|src| match src {
1294                    WireSource::Input(c) => input_types
1295                        .get(*c)
1296                        .copied()
1297                        .unwrap_or(crate::ast::PortType::U64),
1298                    WireSource::NodeOutput(j, p) => nodes[*j].meta().outs[*p].typ,
1299                })
1300                .collect();
1301            let op = if let Some(op) = node.compiled_u64() {
1302                ClosureOp::U64(op)
1303            } else if let Some(op) = crate::compile::assembly::identity_op(node.as_ref()) {
1304                ClosureOp::U64(op)
1305            } else if let Some(kit) = ref_copy_or_slot(node.as_ref(), &wire_types) {
1306                scratch.extend(kit.scratch.iter().map(|e| crate::ast::ScratchBuf::new(*e)));
1307                let starts = flatten_ref_output_starts(nodes, i, port_offsets);
1308                ref_scratch.extend(crate::compile::assembly::scratch_pairs(
1309                    &node.meta().name,
1310                    &starts,
1311                    &kit.scratch,
1312                    scratch_start,
1313                ));
1314                ClosureOp::Slot(kit.op)
1315            } else {
1316                return Err(format!(
1317                    "node '{}' has no compiled form and can't be JIT-compiled",
1318                    node.meta().name
1319                ));
1320            };
1321            node_step[i] = steps.len();
1322            steps.push(HybridStep::Closure(ClosureStep {
1323                op,
1324                input_slots: input_slots.clone(),
1325                output_slots: output_slots.clone(),
1326                scratch_range: (scratch_start, scratch.len()),
1327                accepts_none: node.accepts_none_inputs(),
1328                node: i,
1329            }));
1330            pos += 1;
1331        } else {
1332            // Batch consecutive JIT-able nodes of one lifecycle: a segment is
1333            // folded at build only if every member is compile-constant, so
1334            // a constant node never joins a segment that is not, or the
1335            // constant steps after it would run before their producer.
1336            // A segment is one step to the plan, so a volatile node
1337            // never joins pure ones (the segment would be never current
1338            // and rerun them at every round), and a side channel never
1339            // joins any other node (it would fire whenever the segment
1340            // ran, rather than when its own inputs changed).
1341            let is_side =
1342                |k: usize| matches!(nodes[k].purity(), crate::ast::Purity::SideChannel { .. });
1343            let batch_start = pos;
1344            let first = order[batch_start];
1345            while pos < order.len()
1346                && !matches!(classifications[order[pos]].0, JitOp::Fallback)
1347                && constant[order[pos]] == constant[first]
1348                && volatile[order[pos]] == volatile[first]
1349                && !is_side(order[pos])
1350                && !is_side(first)
1351            {
1352                pos += 1;
1353            }
1354            if pos == batch_start {
1355                pos += 1;
1356            }
1357            let members: Vec<usize> = order[batch_start..pos].to_vec();
1358            // Each step's scratch entries are placed in the kernel's
1359            // scratch (axiom S3), and its reference outputs recorded
1360            // for the validator (S9(a)).
1361            for &k in &members {
1362                let base = scratch.len();
1363                classifications[k].0.place_scratch(base);
1364                let elems = classifications[k].0.scratch_elems().to_vec();
1365                ref_scratch.extend(crate::compile::assembly::scratch_pairs(
1366                    &nodes[k].meta().name,
1367                    &flatten_ref_output_starts(nodes, k, port_offsets),
1368                    &elems,
1369                    base,
1370                ));
1371                scratch.extend(elems.iter().map(|e| crate::ast::ScratchBuf::new(*e)));
1372            }
1373            // One native segment for the batch: its boundary inputs are
1374            // the slots the batch reads and does not write, its outputs
1375            // every slot it writes. Table-kind slots are numbered across
1376            // every segment so the kernel's one value table serves them
1377            // all (SRD 115 §3); slots closures fill with handles or Ref
1378            // pairs are handle slots to the H1 verifier, which a segment
1379            // may only load, store, and pass. Native code names the
1380            // member it is in through the tracker slot, for the failure
1381            // path (A7).
1382            let batch: Vec<(JitOp, Vec<usize>, Vec<usize>)> = members
1383                .iter()
1384                .map(|&k| classifications[k].clone())
1385                .collect();
1386            let written: std::collections::HashSet<usize> = batch
1387                .iter()
1388                .flat_map(|(_, _, o)| o.iter().copied())
1389                .collect();
1390            let mut input_slots: Vec<usize> = Vec::new();
1391            for (_, ins, _) in &batch {
1392                for &s in ins {
1393                    if !written.contains(&s) && !input_slots.contains(&s) {
1394                        input_slots.push(s);
1395                    }
1396                }
1397            }
1398            let output_slots: Vec<usize> = batch
1399                .iter()
1400                .flat_map(|(_, _, o)| o.iter().copied())
1401                .collect();
1402            let (code_fn, code) = jit::compile_jit_entry(&batch, Some(total_slots))?;
1403            let segment = steps.len();
1404            for &k in &members {
1405                node_step[k] = segment;
1406            }
1407            steps.push(HybridStep::Jit(JitSegment {
1408                code_fn,
1409                fallible: code.fallible(),
1410                _module: code,
1411                input_slots,
1412                output_slots,
1413                nodes: members,
1414            }));
1415        }
1416    }
1417
1418    let output_types = output_types_of(nodes, port_offsets, input_starts, input_types, &output_map);
1419    build_pushpull_from_steps(
1420        steps,
1421        scratch,
1422        ref_scratch,
1423        ref_slots,
1424        wiring,
1425        nodes,
1426        coord_count,
1427        total_slots,
1428        output_map,
1429        max_inputs,
1430        max_outputs,
1431        input_starts,
1432        input_widths,
1433        output_types,
1434        externs,
1435        constant,
1436        volatile,
1437        attribution,
1438        node_step,
1439    )
1440}
1441
1442/// The port type of each named output, by the slot it names: a node
1443/// output port's type, or a coordinate input's declared type.
1444fn output_types_of(
1445    nodes: &[Box<dyn PolydatNode>],
1446    port_offsets: &[Vec<usize>],
1447    input_starts: &[usize],
1448    input_types: &[crate::ast::PortType],
1449    output_map: &HashMap<String, usize>,
1450) -> HashMap<String, crate::ast::PortType> {
1451    let mut slot_types: HashMap<usize, crate::ast::PortType> = HashMap::new();
1452    for (start, ty) in input_starts.iter().zip(input_types) {
1453        slot_types.insert(*start, *ty);
1454    }
1455    for (node_idx, node) in nodes.iter().enumerate() {
1456        for (p, out) in node.meta().outs.iter().enumerate() {
1457            slot_types.insert(port_offsets[node_idx][p], out.typ);
1458        }
1459    }
1460    output_map
1461        .iter()
1462        .map(|(name, slot)| {
1463            (
1464                name.clone(),
1465                slot_types
1466                    .get(slot)
1467                    .copied()
1468                    .unwrap_or(crate::ast::PortType::U64),
1469            )
1470        })
1471        .collect()
1472}
1473
1474/// Build a hybrid kernel without JIT (all closures).
1475#[cfg(not(feature = "jit"))]
1476#[allow(clippy::too_many_arguments)]
1477pub(crate) fn build_hybrid(
1478    nodes: &[Box<dyn PolydatNode>],
1479    wiring: &[Vec<WireSource>],
1480    coord_count: usize,
1481    total_slots: usize,
1482    port_offsets: &[Vec<usize>],
1483    input_starts: &[usize],
1484    input_widths: &[usize],
1485    output_map: HashMap<String, usize>,
1486    ref_slots: Vec<bool>,
1487    input_types: &[crate::ast::PortType],
1488    externs: crate::compile::externs::Externs,
1489    constant: Vec<bool>,
1490    volatile: Vec<bool>,
1491    attribution: std::sync::Arc<crate::compile::Attribution>,
1492) -> Result<HybridKernelPushPull, String> {
1493    let mut steps: Vec<HybridStep> = Vec::new();
1494    let mut scratch: Vec<crate::ast::ScratchBuf> = Vec::new();
1495    let mut ref_scratch: Vec<(usize, usize)> = Vec::new();
1496    let mut max_inputs = 0usize;
1497    let mut max_outputs = 0usize;
1498
1499    for (node_idx, node) in nodes.iter().enumerate() {
1500        let input_slots = flatten_input_slots(
1501            wiring,
1502            nodes,
1503            node_idx,
1504            port_offsets,
1505            input_starts,
1506            input_widths,
1507        );
1508        let output_slots = flatten_output_slots(nodes, node_idx, port_offsets);
1509
1510        max_inputs = max_inputs.max(input_slots.len());
1511        max_outputs = max_outputs.max(output_slots.len());
1512
1513        let scratch_start = scratch.len();
1514        let wire_types: Vec<crate::ast::PortType> = wiring[node_idx]
1515            .iter()
1516            .map(|src| match src {
1517                WireSource::Input(c) => input_types
1518                    .get(*c)
1519                    .copied()
1520                    .unwrap_or(crate::ast::PortType::U64),
1521                WireSource::NodeOutput(j, p) => nodes[*j].meta().outs[*p].typ,
1522            })
1523            .collect();
1524        let op = if let Some(op) = node.compiled_u64() {
1525            ClosureOp::U64(op)
1526        } else if let Some(op) = crate::compile::assembly::identity_op(node.as_ref()) {
1527            ClosureOp::U64(op)
1528        } else if let Some(kit) = ref_copy_or_slot(node.as_ref(), &wire_types) {
1529            scratch.extend(kit.scratch.iter().map(|e| crate::ast::ScratchBuf::new(*e)));
1530            let starts = flatten_ref_output_starts(nodes, node_idx, port_offsets);
1531            ref_scratch.extend(crate::compile::assembly::scratch_pairs(
1532                &node.meta().name,
1533                &starts,
1534                &kit.scratch,
1535                scratch_start,
1536            ));
1537            ClosureOp::Slot(kit.op)
1538        } else {
1539            return Err(format!("node '{}' has no compiled form", node.meta().name));
1540        };
1541        steps.push(HybridStep::Closure(ClosureStep {
1542            op,
1543            input_slots,
1544            output_slots,
1545            scratch_range: (scratch_start, scratch.len()),
1546            accepts_none: node.accepts_none_inputs(),
1547            node: node_idx,
1548        }));
1549    }
1550    let node_step: Vec<usize> = (0..nodes.len()).collect();
1551
1552    let output_types = output_types_of(nodes, port_offsets, input_starts, input_types, &output_map);
1553    build_pushpull_from_steps(
1554        steps,
1555        scratch,
1556        ref_scratch,
1557        ref_slots,
1558        wiring,
1559        nodes,
1560        coord_count,
1561        total_slots,
1562        output_map,
1563        max_inputs,
1564        max_outputs,
1565        input_starts,
1566        input_widths,
1567        output_types,
1568        externs,
1569        constant,
1570        volatile,
1571        attribution,
1572        node_step,
1573    )
1574}
1575
1576/// Shared construction of `HybridKernelPushPull` from assembled steps.
1577///
1578/// Computes provenance bitmasks from the DAG wiring and builds the
1579/// step_dependents list for push-side invalidation and the slot_provenance
1580/// table for pull-side cone guard.
1581#[allow(clippy::too_many_arguments)]
1582fn build_pushpull_from_steps(
1583    steps: Vec<HybridStep>,
1584    scratch: Vec<crate::ast::ScratchBuf>,
1585    ref_scratch: Vec<(usize, usize)>,
1586    ref_slots: Vec<bool>,
1587    wiring: &[Vec<WireSource>],
1588    nodes: &[Box<dyn PolydatNode>],
1589    coord_count: usize,
1590    total_slots: usize,
1591    output_map: HashMap<String, usize>,
1592    max_inputs: usize,
1593    max_outputs: usize,
1594    _input_starts: &[usize],
1595    input_widths: &[usize],
1596    output_types: HashMap<String, crate::ast::PortType>,
1597    externs: crate::compile::externs::Externs,
1598    constant: Vec<bool>,
1599    volatile: Vec<bool>,
1600    attribution: std::sync::Arc<crate::compile::Attribution>,
1601    node_step: Vec<usize>,
1602) -> Result<HybridKernelPushPull, String> {
1603    let step_count = steps.len();
1604    debug_assert_eq!(node_step.len(), nodes.len());
1605    debug_assert!(node_step.iter().all(|&s| s < step_count));
1606    // Node lists from the runtime model become step lists: a segment
1607    // depends on what any member depends on.
1608    let to_steps = |list: &[usize]| -> Vec<usize> {
1609        let mut v: Vec<usize> = list.iter().map(|&n| node_step[n]).collect();
1610        v.sort_unstable();
1611        v.dedup();
1612        v
1613    };
1614    // One slot past the layout is the tracker (A7).
1615    let mut buffer = vec![0u64; total_slots + 1];
1616    let mut none = vec![false; total_slots];
1617    let any_none = externs.seed(&mut buffer, Some(&mut none));
1618
1619    // Compute per-node provenance and invert into per-input step dependents.
1620    // Since each step currently maps to one node, step index == node index.
1621    // Dependents come back per-INPUT; expand to per-SLOT so the kernels'
1622    // slot-indexed dirty tracking / changed-mask bits stay coherent under
1623    // multi-slot inputs (§8.4 layer 1). Identity for all-scalar inputs.
1624    let node_provenance = crate::kernel::PolydatProgram::compute_provenance(nodes, wiring);
1625    let input_dependents: Vec<Vec<usize>> =
1626        crate::kernel::PolydatProgram::compute_dependents(&node_provenance, input_widths.len())
1627            .iter()
1628            .map(|d| to_steps(d))
1629            .collect();
1630    let step_dependents: Vec<Vec<usize>> = input_widths
1631        .iter()
1632        .enumerate()
1633        .flat_map(|(i, w)| {
1634            std::iter::repeat_n(input_dependents.get(i).cloned().unwrap_or_default(), *w)
1635        })
1636        .collect();
1637
1638    let step_outs: Vec<&[usize]> = steps.iter().map(|s| s.output_slots()).collect();
1639    let slot_provenance =
1640        crate::compile::slot_provenance(coord_count, total_slots, &step_outs, &step_dependents);
1641
1642    // The runtime model's lifecycle classification, passed in per node
1643    // from the one rule the interpreter's fold applies, folded onto the
1644    // steps: a segment is constant only if every member is, volatile
1645    // or a side channel if any member is.
1646    debug_assert_eq!(constant.len(), nodes.len());
1647    debug_assert_eq!(volatile.len(), nodes.len());
1648    let mut step_constant = vec![true; step_count];
1649    let mut step_volatile = vec![false; step_count];
1650    let mut side = vec![false; step_count];
1651    for (n, node) in nodes.iter().enumerate() {
1652        let s = node_step[n];
1653        step_constant[s] &= constant[n];
1654        step_volatile[s] |= volatile[n];
1655        side[s] |= matches!(node.purity(), crate::ast::Purity::SideChannel { .. });
1656    }
1657    let volatile = step_volatile;
1658    let constants: Vec<usize> = (0..step_count).filter(|&i| step_constant[i]).collect();
1659    let step_inputs: Vec<&[usize]> = steps.iter().map(|s| s.input_slots()).collect();
1660    let step_outputs: Vec<&[usize]> = steps.iter().map(|s| s.output_slots()).collect();
1661    let plan = crate::compile::Invalidation::from_provenance(
1662        step_dependents.clone(),
1663        &step_inputs,
1664        &step_outputs,
1665        &output_map,
1666        total_slots,
1667    );
1668    let mut slot_step: Vec<Option<usize>> = vec![None; total_slots];
1669    for (i, outs) in step_outputs.iter().enumerate() {
1670        for &s in outs.iter() {
1671            slot_step[s] = Some(i);
1672        }
1673    }
1674    drop(step_inputs);
1675    drop(step_outputs);
1676
1677    let dirty: Vec<Vec<usize>> = plan.input_dependents.clone();
1678    let volatile_steps: Vec<usize> = (0..step_count).filter(|&i| volatile[i]).collect();
1679    let mut kernel = HybridKernelPushPull {
1680        core: HybridCore {
1681            buffer,
1682            coord_count,
1683            steps: std::sync::Arc::new(steps),
1684            output_map,
1685            gather_buf: vec![0u64; max_inputs.max(1)],
1686            scatter_buf: vec![0u64; max_outputs.max(1)],
1687            scratch,
1688            ref_slots,
1689            ref_scratch,
1690            output_types,
1691            externs,
1692            traversals: Vec::new().into(),
1693            resolved_outputs: Vec::new(),
1694            _nodes: std::sync::Arc::new(Vec::new()),
1695            drive: crate::compile::Drive {
1696                coords: Vec::new(),
1697                stale: true,
1698            },
1699            none,
1700            ran: vec![0; step_count],
1701            epoch: 0,
1702            all_ran: false,
1703            clean: vec![false; step_count],
1704            use_clean: true,
1705            plan: std::sync::Arc::new(plan),
1706            volatile: volatile.into(),
1707            side: side.into(),
1708            slot_step: slot_step.into(),
1709            sites: attribution,
1710            cur_step: 0,
1711            tracker: total_slots,
1712            all: (0..step_count).collect::<Vec<usize>>().into(),
1713            dirty: dirty.into(),
1714            any_none,
1715            volatile_steps: volatile_steps.into(),
1716        },
1717        slot_provenance,
1718        changed_mask: crate::kernel::ProvMask::all_below(coord_count), // all dirty on first eval
1719        force_run: false,
1720    };
1721    // The compile-constant fold of the runtime model, on this engine: a
1722    // step no input reaches runs at build, once, and is current from
1723    // then on, so what is knowable at build is known at build and fails
1724    // at build.
1725    kernel.core.begin_epoch();
1726    kernel.core.run_steps(&constants);
1727    kernel.core.drive.stale = true;
1728    Ok(kernel)
1729}
1730
1731/// The slot kit for a closure step: a copy of a `Ref2` value into the
1732/// step's own scratch (`identity`, a `__port_` passthrough; axiom S3),
1733/// else the node's own kit.
1734fn ref_copy_or_slot(
1735    node: &dyn PolydatNode,
1736    wire_types: &[crate::ast::PortType],
1737) -> Option<crate::ast::CompiledSlotKit> {
1738    let meta = node.meta();
1739    if (meta.name == "identity" || meta.name.starts_with("__port_"))
1740        && meta.outs.len() == 1
1741        && meta.outs[0].typ.slot_color() == crate::ast::SlotColor::Ref2
1742    {
1743        return crate::compile::assembly::ref_copy_kit(meta.outs[0].typ);
1744    }
1745    node.compiled_slot(wire_types)
1746}
1747
1748// ── The engine-independent surface (engine_parity.md, step 4) ──────
1749
1750#[cfg(feature = "jit")]
1751impl HybridKernelRaw {
1752    /// Nothing to mark: every run evaluates everything.
1753    fn mark_all_dirty(&mut self) {}
1754}
1755
1756impl HybridKernelPull {
1757    /// The next evaluation runs whatever the cone guard says.
1758    fn mark_all_dirty(&mut self) {
1759        self.changed_mask = crate::kernel::ProvMask::all_below(self.core.coord_count);
1760        self.force_run = true;
1761    }
1762}
1763
1764impl HybridKernelPushPull {
1765    /// Every step reruns at the next evaluation.
1766    fn mark_all_dirty(&mut self) {
1767        self.core.clean.fill(false);
1768        self.changed_mask = crate::kernel::ProvMask::all_below(self.core.coord_count);
1769        self.force_run = true;
1770    }
1771
1772    /// The same program with no provenance: every run evaluates
1773    /// everything.
1774    #[cfg(feature = "jit")]
1775    pub(crate) fn into_raw(self) -> HybridKernelRaw {
1776        let mut core = self.core;
1777        core.set_use_clean(false);
1778        HybridKernelRaw { core }
1779    }
1780
1781    /// The same program with the cone guard only.
1782    #[cfg(feature = "jit")]
1783    pub(crate) fn into_pull(self) -> HybridKernelPull {
1784        let mut core = self.core;
1785        core.set_use_clean(false);
1786        let changed_mask = crate::kernel::ProvMask::all_below(core.coord_count);
1787        HybridKernelPull {
1788            core,
1789            slot_provenance: self.slot_provenance,
1790            changed_mask,
1791            force_run: false,
1792        }
1793    }
1794}
1795
1796use crate::compile::select::{Engine, Provenance};
1797
1798#[cfg(feature = "jit")]
1799crate::compile::impl_kernel_trait!(HybridKernelRaw, Engine::Native(Provenance::Raw));
1800crate::compile::impl_kernel_trait!(HybridKernelPull, Engine::Native(Provenance::Pull));
1801crate::compile::impl_kernel_trait!(HybridKernelPushPull, Engine::Native(Provenance::PushPull));
1802
1803/// The pending coordinates through the `Kernel` trait, for the hybrid
1804/// kernels: every evaluation runs the whole program until the hybrid
1805/// kernel keeps the closure tier's cone and `None` bookkeeping.
1806macro_rules! hybrid_drive {
1807    ($ty:ident, $set_coords:ident) => {
1808        impl $ty {
1809            /// The named output through the `Kernel` trait: the pending
1810            /// coordinates are applied, a round begins if a write is pending,
1811            /// and only the output's cone runs.
1812            fn pull_value(&mut self, name: &str) -> crate::ast::Value {
1813                let coords = std::mem::take(&mut self.core.drive.coords);
1814                self.$set_coords(&coords);
1815                self.core.drive.coords = coords;
1816                self.pull_output(name)
1817            }
1818            /// [`Self::pull_value`] by output index.
1819            fn pull_value_at(&mut self, index: usize) -> crate::ast::Value {
1820                let coords = std::mem::take(&mut self.core.drive.coords);
1821                self.$set_coords(&coords);
1822                self.core.drive.coords = coords;
1823                self.core.pull_at(index)
1824            }
1825            fn eval_pending(&mut self) {
1826                let coords = std::mem::take(&mut self.core.drive.coords);
1827                self.eval(&coords);
1828                self.core.drive.coords = coords;
1829            }
1830        }
1831    };
1832}
1833#[cfg(feature = "jit")]
1834hybrid_drive!(HybridKernelRaw, set_coords);
1835hybrid_drive!(HybridKernelPull, set_inputs);
1836hybrid_drive!(HybridKernelPushPull, set_inputs);
1837
1838/// One step: SRD-74 Rule 1, then the segment or the closure. A step
1839/// that does not accept `None` emits `None` on every output when any
1840/// input is `None`, without running; native code never accepts it, and
1841/// a node downstream of an unset extern is a closure, so a `None`
1842/// reaches a segment only when a host cleared an extern after the
1843/// build. With `none_free` the mask is known clear and is not read.
1844#[inline(always)]
1845fn run_hybrid_step(
1846    step: &HybridStep,
1847    none_free: bool,
1848    buffer: &mut [u64],
1849    none: &mut [bool],
1850    gather: &mut [u64],
1851    scatter: &mut [u64],
1852    scratch: &mut [crate::ast::ScratchBuf],
1853) {
1854    if !none_free && step.input_slots().iter().any(|&s| none[s]) {
1855        #[cfg(feature = "jit")]
1856        if let HybridStep::Jit(_) = step {
1857            panic!(
1858                "a `None` reached native code in a hybrid kernel: an extern was cleared \
1859                 after the build (docs/design/engine_parity.md, A12)"
1860            );
1861        }
1862        if !step.accepts_none() {
1863            for &s in step.output_slots() {
1864                none[s] = true;
1865            }
1866            return;
1867        }
1868    }
1869    match step {
1870        #[cfg(feature = "jit")]
1871        HybridStep::Jit(seg) => {
1872            // Through the setjmp wrapper when the code calls a helper,
1873            // so its failure is the longjmp the kernel catches rather
1874            // than an abort; bare when it calls nothing.
1875            let code_fn = seg.code_fn;
1876            let buf_const = buffer.as_ptr();
1877            let buf_mut = buffer.as_mut_ptr();
1878            let sc = scratch.as_mut_ptr();
1879            if seg.fallible {
1880                crate::compile::jit::invoke_with_catch(move || unsafe {
1881                    (code_fn)(buf_const, buf_mut, sc);
1882                });
1883            } else {
1884                unsafe { (code_fn)(buf_const, buf_mut, sc) };
1885            }
1886        }
1887        HybridStep::Closure(cs) => {
1888            for (i, &slot) in cs.input_slots.iter().enumerate() {
1889                gather[i] = buffer[slot];
1890            }
1891            match &cs.op {
1892                ClosureOp::U64(op) => op(
1893                    &gather[..cs.input_slots.len()],
1894                    &mut scatter[..cs.output_slots.len()],
1895                ),
1896                ClosureOp::Slot(op) => op(
1897                    &gather[..cs.input_slots.len()],
1898                    &mut scatter[..cs.output_slots.len()],
1899                    &mut scratch[cs.scratch_range.0..cs.scratch_range.1],
1900                ),
1901            }
1902            for (i, &slot) in cs.output_slots.iter().enumerate() {
1903                buffer[slot] = scatter[i];
1904            }
1905        }
1906    }
1907    if !none_free {
1908        for &s in step.output_slots() {
1909            none[s] = false;
1910        }
1911    }
1912}