Skip to main content

polydat_core/compile/
mod.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Kernel compilation: assembled DAG → fast executable kernel.
5//!
6//! Everything in this module is on the path between
7//! [`assembly::PolydatAssembler`] and an executable kernel. The
8//! pipeline:
9//!
10//! ```text
11//! PolydatAssembler  ──(fusion pass)──▶  fused DAG
12//!                                       │
13//!                            (select::choose_kernel)
14//!                                       │
15//!                  ┌────────────────────┼────────────────────┐
16//!                  ▼                    ▼                    ▼
17//!         closures::Kernel       hybrid::Kernel       jit::Kernel
18//!         (Phase 2 u64 closures) (per-node optimal)   (Phase 3 native)
19//! ```
20//!
21//! - [`assembly`]: the public construction surface
22//!   ([`assembly::PolydatAssembler`] + [`assembly::WireRef`]).
23//! - [`fusion`]: graph-level subgraph fusion pass; runs during
24//!   assembly after wiring resolution.
25//! - [`select`]: variant-selection heuristic; chooses the
26//!   monomorphic kernel type at construction time.
27//! - [`closures`]: Phase 2 monomorphic u64-only kernels.
28//! - [`hybrid`]: per-node optimal kernel (JIT segments + closure
29//!   segments sharing a flat u64 buffer).
30//! - `jit`: Phase 3 Cranelift JIT compilation
31//!   (feature-gated on `jit`).
32
33pub mod assembly;
34pub mod closures;
35pub mod cone;
36#[cfg(all(test, feature = "jit"))]
37mod cone_tests;
38pub(crate) mod externs;
39pub mod fusion;
40pub mod hybrid;
41#[cfg(feature = "jit")]
42pub mod jit;
43pub mod lattice;
44/// The boundary marshalling a compiled node kit reads its arguments
45/// and writes its outputs through (compiled_handles.md §4): a node
46/// crate's kits use it as the core's own do.
47pub mod marshal;
48pub mod roundtrip_lint;
49pub mod select;
50pub mod simd_plan;
51#[cfg(feature = "jit")]
52pub mod simd_tier1;
53
54/// Axiom S2 typed accessors, shared by the P2 and hybrid kernel
55/// types (both expose `self.core.ref_entry(slot)`). Each returns
56/// a borrow whose lifetime ties to `&self`, so the borrow checker
57/// statically prevents holding a slice across the next
58/// `eval(&mut self)` — stale Ref reads are compile errors.
59macro_rules! ref_readers {
60    () => {
61        /// Borrow a `vec_f32` output's current contents.
62        pub fn read_vec_f32(&self, slot: usize) -> &[f32] {
63            match self.core.ref_entry(slot) {
64                crate::ast::ScratchBuf::F32(v) => v,
65                other => panic!("slot {slot} is not f32-lane scratch: {other:?}"),
66            }
67        }
68        /// Borrow a `vec_f64` output's current contents.
69        pub fn read_vec_f64(&self, slot: usize) -> &[f64] {
70            match self.core.ref_entry(slot) {
71                crate::ast::ScratchBuf::F64(v) => v,
72                other => panic!("slot {slot} is not f64-lane scratch: {other:?}"),
73            }
74        }
75        /// Borrow a `vec_f16` output's current contents.
76        pub fn read_vec_f16(&self, slot: usize) -> &[half::f16] {
77            match self.core.ref_entry(slot) {
78                crate::ast::ScratchBuf::F16(v) => v,
79                other => panic!("slot {slot} is not f16-lane scratch: {other:?}"),
80            }
81        }
82        /// Borrow a `vec_i8` output's current contents.
83        pub fn read_vec_i8(&self, slot: usize) -> &[i8] {
84            match self.core.ref_entry(slot) {
85                crate::ast::ScratchBuf::I8(v) => v,
86                other => panic!("slot {slot} is not i8-lane scratch: {other:?}"),
87            }
88        }
89        /// Borrow a `vec_i16` output's current contents.
90        pub fn read_vec_i16(&self, slot: usize) -> &[i16] {
91            match self.core.ref_entry(slot) {
92                crate::ast::ScratchBuf::I16(v) => v,
93                other => panic!("slot {slot} is not i16-lane scratch: {other:?}"),
94            }
95        }
96        /// Borrow a `vec_i32` output's current contents.
97        pub fn read_vec_i32(&self, slot: usize) -> &[i32] {
98            match self.core.ref_entry(slot) {
99                crate::ast::ScratchBuf::I32(v) => v,
100                other => panic!("slot {slot} is not i32-lane scratch: {other:?}"),
101            }
102        }
103        /// Borrow a `vec_i64` output's current contents.
104        pub fn read_vec_i64(&self, slot: usize) -> &[i64] {
105            match self.core.ref_entry(slot) {
106                crate::ast::ScratchBuf::I64(v) => v,
107                other => panic!("slot {slot} is not i64-lane scratch: {other:?}"),
108            }
109        }
110    };
111}
112pub(crate) use ref_readers;
113
114/// The provenance of every buffer slot: which input slots reach it,
115/// as an exact multi-word mask, for the pull-side cone guard of every
116/// compiled kernel. `input_dependents` is indexed by input slot (a
117/// multi-slot input repeats its list per slot) and lists the steps
118/// downstream of that slot; `step_output_slots` gives each step's
119/// output slots, which all take the step's mask. A coordinate slot's
120/// provenance is itself.
121pub(crate) fn slot_provenance(
122    coord_count: usize,
123    total_slots: usize,
124    step_output_slots: &[&[usize]],
125    input_dependents: &[Vec<usize>],
126) -> Vec<crate::kernel::ProvMask> {
127    use crate::kernel::ProvMask;
128    let step_count = step_output_slots.len();
129    let mut step_prov: Vec<ProvMask> = (0..step_count).map(|_| ProvMask::empty()).collect();
130    for (input_slot, deps) in input_dependents.iter().enumerate() {
131        for &step in deps {
132            if step < step_count {
133                step_prov[step].set(input_slot);
134            }
135        }
136    }
137    let mut slots: Vec<ProvMask> = (0..total_slots).map(|_| ProvMask::empty()).collect();
138    for (i, slot) in slots.iter_mut().enumerate().take(coord_count) {
139        slot.set(i);
140    }
141    for (step, outs) in step_output_slots.iter().enumerate() {
142        for &slot in outs.iter() {
143            if slot < slots.len() {
144                slots[slot] = step_prov[step].clone();
145            }
146        }
147    }
148    slots
149}
150
151/// The coordinates a host set last on a compiled kernel and whether
152/// they have been evaluated: what the [`Kernel`](crate::kernel::Kernel)
153/// trait's `set_inputs` and `pull` keep between calls.
154#[derive(Clone, Default)]
155pub(crate) struct Drive {
156    pub(crate) coords: Vec<u64>,
157    pub(crate) stale: bool,
158}
159
160/// The [`Kernel`](crate::kernel::Kernel) impl every compiled kernel
161/// shares: the type's inherent `eval`, `set_input`, `set_cursor`,
162/// `get_value`, `mark_all_dirty`, and a `core` with a
163/// `drive`, `externs`, `coord_count`, and `output_types`.
164macro_rules! impl_kernel_trait {
165    ($ty:ident, $engine:expr) => {
166        impl crate::kernel::Kernel for $ty {
167            fn engine(&self) -> crate::compile::select::Engine {
168                $engine
169            }
170            fn set_inputs(&mut self, coords: &[u64]) {
171                self.core.drive.coords.clear();
172                self.core.drive.coords.extend_from_slice(coords);
173                self.core.drive.stale = true;
174            }
175            fn set_input(&mut self, name: &str, value: crate::ast::Value) -> Result<(), String> {
176                self.core.drive.stale = true;
177                $ty::set_input(self, name, value)
178            }
179            fn set_cursor(
180                &mut self,
181                name: &str,
182                partition: &crate::iteration::cursor_partition::Partition,
183            ) -> Result<(), String> {
184                self.core.drive.stale = true;
185                $ty::set_cursor(self, name, partition)
186            }
187            fn eval(&mut self) {
188                self.eval_pending();
189                self.core.drive.stale = false;
190            }
191            fn pull(&mut self, name: &str) -> crate::ast::Value {
192                self.pull_value(name)
193            }
194            fn input_names(&self) -> Vec<String> {
195                self.core.externs.input_names().to_vec()
196            }
197            /// In declaration order, as the interpreter lists them: the
198            /// assembler sets them on every compiled kernel.
199            fn output_names(&self) -> Vec<String> {
200                self.core.externs.output_names().to_vec()
201            }
202            fn output_type(&self, name: &str) -> Option<crate::ast::PortType> {
203                self.core.output_types.get(name).copied()
204            }
205            fn externs(&self) -> Vec<(String, crate::ast::PortType)> {
206                self.core
207                    .externs
208                    .names()
209                    .into_iter()
210                    .map(|(n, t)| (n.to_string(), t))
211                    .collect()
212            }
213            fn cursor_schemas(&self) -> &[crate::iteration::source::SourceSchema] {
214                self.core.externs.cursor_schemas()
215            }
216            fn input_value(&self, name: &str) -> Option<crate::ast::Value> {
217                self.core.externs.value(name).or_else(|| {
218                    let i = self
219                        .core
220                        .externs
221                        .input_names()
222                        .iter()
223                        .position(|n| n == name)?;
224                    if i < self.core.coord_count {
225                        let pending = self.core.drive.coords.get(i).copied();
226                        Some(crate::ast::Value::U64(
227                            pending.unwrap_or(self.core.buffer[i]),
228                        ))
229                    } else {
230                        None
231                    }
232                })
233            }
234            fn traversals(&self) -> &[crate::dsl::traversal::Traversal] {
235                &self.core.traversals
236            }
237            fn plan(&self) -> crate::EnginePlan {
238                self.core.plan()
239            }
240            fn input_index(&self, name: &str) -> Option<usize> {
241                self.core
242                    .externs
243                    .input_names()
244                    .iter()
245                    .position(|n| n == name)
246            }
247            fn set_input_at(
248                &mut self,
249                index: usize,
250                value: crate::ast::Value,
251            ) -> Result<(), String> {
252                self.core.drive.stale = true;
253                $ty::set_input_at(self, index, value)
254            }
255            fn output_index(&self, name: &str) -> Option<usize> {
256                self.core
257                    .externs
258                    .output_names()
259                    .iter()
260                    .position(|n| n == name)
261            }
262            fn pull_at(&mut self, index: usize) -> crate::ast::Value {
263                self.pull_value_at(index)
264            }
265            fn traverse(&mut self, index: usize) -> Result<crate::kernel::TraversalStream, String> {
266                let traversal = self.core.traversals.get(index).cloned().ok_or_else(|| {
267                    format!(
268                        "no traversal at index {index}; the program declares {}",
269                        self.core.traversals.len()
270                    )
271                })?;
272                crate::kernel::activation::open_traversal(self, traversal)
273            }
274            fn invalidate_all(&mut self) {
275                self.mark_all_dirty();
276                self.core.invalidate_all();
277            }
278            fn shared_cells(&self) -> Vec<crate::kernel::SharedCellEntry> {
279                self.core.externs.shared_cells()
280            }
281            fn attach_shared_cell(
282                &mut self,
283                name: &str,
284                cell: crate::kernel::SharedCell,
285            ) -> Result<(), String> {
286                self.core.attach_cell(name, cell)
287            }
288            fn into_program(
289                mut self: Box<Self>,
290            ) -> std::sync::Arc<dyn crate::kernel::KernelProgram> {
291                self.mark_all_dirty();
292                self.core.drive.stale = true;
293                std::sync::Arc::new(crate::kernel::SharedKernel(*self))
294            }
295        }
296
297        impl crate::kernel::KernelInternals for $ty {
298            /// A compiled kernel keeps the traversals; each carries the
299            /// comprehension its producer resolved to at compile time.
300            fn set_traversals(
301                &mut self,
302                traversals: Vec<crate::dsl::traversal::Traversal>,
303                _producers: Vec<crate::dsl::traversal::Producer>,
304            ) {
305                self.core.traversals = traversals.into();
306            }
307            fn slot_value(&self, slot: usize, ty: crate::ast::PortType) -> crate::ast::Value {
308                self.core.slot_value(slot, ty)
309            }
310            fn folded_value(&self, name: &str) -> Option<crate::ast::Value> {
311                let slot = *self.core.output_map.get(name)?;
312                let ty = *self.core.output_types.get(name)?;
313                Some(self.core.slot_value(slot, ty))
314            }
315            fn set_cursor_extent(&mut self, index: usize, extent: u64) {
316                self.core.externs.set_cursor_extent(index, extent);
317            }
318            fn reset_to_program(&mut self) {
319                self.core.externs.reset_to_program(&mut self.core.buffer);
320                self.mark_all_dirty();
321            }
322        }
323    };
324}
325pub(crate) use impl_kernel_trait;
326
327/// The dirty-register plan of a compiled kernel: which steps each input
328/// slot invalidates when it changes, and which steps each named output
329/// needs. The evaluation loops consume only this; provenance derives it
330/// today, and a host that knows its write and read patterns may supply
331/// a narrower plan later without touching the loops
332/// (docs/design/engine_parity.md, step 5).
333pub(crate) struct Invalidation {
334    /// Per input slot (coordinates and externs alike), the steps that
335    /// depend on it, transitively.
336    pub(crate) input_dependents: Vec<Vec<usize>>,
337    /// Per named output, the steps of its cone in evaluation order.
338    pub(crate) cones: std::collections::HashMap<String, Vec<usize>>,
339}
340
341impl Invalidation {
342    /// The plan provenance gives: every step downstream of an input is
343    /// invalidated by it, and every step upstream of an output is in
344    /// its cone. `inputs` and `outputs` are each step's slots;
345    /// `output_slots` names the outputs.
346    pub(crate) fn from_provenance(
347        input_dependents: Vec<Vec<usize>>,
348        step_inputs: &[&[usize]],
349        step_outputs: &[&[usize]],
350        output_slots: &std::collections::HashMap<String, usize>,
351        total_slots: usize,
352    ) -> Self {
353        let step_count = step_inputs.len();
354        let mut slot_step: Vec<Option<usize>> = vec![None; total_slots];
355        for (i, outs) in step_outputs.iter().enumerate() {
356            for &s in outs.iter() {
357                slot_step[s] = Some(i);
358            }
359        }
360        let cones = output_slots
361            .iter()
362            .map(|(name, &slot)| {
363                let mut wanted = vec![false; step_count];
364                let mut stack: Vec<usize> = slot_step[slot].into_iter().collect();
365                while let Some(i) = stack.pop() {
366                    if wanted[i] {
367                        continue;
368                    }
369                    wanted[i] = true;
370                    stack.extend(step_inputs[i].iter().filter_map(|&s| slot_step[s]));
371                }
372                (
373                    name.clone(),
374                    (0..step_count).filter(|&i| wanted[i]).collect(),
375                )
376            })
377            .collect();
378        Self {
379            input_dependents,
380            cones,
381        }
382    }
383}
384
385/// Where each compiled step came from, for the failure path only
386/// (engine_parity.md, A7). A step's panic is caught at the step
387/// boundary and re-raised enriched exactly as the interpreter enriches
388/// a node's: the node's name, the outputs it feeds, the program's
389/// diagnostic context, and its input values decoded from the buffer
390/// where the slot types allow. Step index is node index on every
391/// compiled engine.
392#[derive(Default)]
393pub(crate) struct Attribution {
394    pub(crate) sites: Vec<NodeSite>,
395    /// The program's diagnostic context (`PolydatProgram::context`).
396    pub(crate) context: String,
397}
398
399/// One node's identity for the failure path.
400pub(crate) struct NodeSite {
401    pub(crate) name: String,
402    /// The declared outputs the node feeds, sorted.
403    pub(crate) outputs: Vec<String>,
404    /// `(first slot, port type)` of every input port, in port order.
405    pub(crate) inputs: Vec<(usize, crate::ast::PortType)>,
406}
407
408impl Attribution {
409    /// The inputs of `step` as diagnostic text, from the buffer, each
410    /// copied out and printed as the interpreter prints the same value:
411    /// `None` where the mask says so, and the port type alone where the
412    /// slot cannot be decoded, so the report itself never fails.
413    fn inputs_of(&self, step: usize, buffer: &[u64], none: Option<&[bool]>) -> Vec<String> {
414        let Some(site) = self.sites.get(step) else {
415            return Vec::new();
416        };
417        let _quiet = crate::kernel::engines::EvalPanicCaptureGuard::arm();
418        site.inputs
419            .iter()
420            .map(|&(slot, ty)| {
421                if none.is_some_and(|m| m.get(slot).copied().unwrap_or(false)) {
422                    return "None".to_string();
423                }
424                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
425                    crate::kernel::engines::format_value_for_diag(&marshal::decode_output(
426                        buffer, slot, ty,
427                    ))
428                }))
429                .unwrap_or_else(|_| format!("{ty:?}"))
430            })
431            .collect()
432    }
433
434    /// Re-raise a step's panic enriched as the interpreter enriches a
435    /// node's (`kernel::engines::enrich_panic`). `step` beyond the
436    /// sites (native code that failed before naming a step) reports an
437    /// unknown node, as the interpreter does for an index it lacks.
438    pub(crate) fn reraise(
439        &self,
440        payload: Box<dyn std::any::Any + Send>,
441        step: usize,
442        buffer: &[u64],
443        none: Option<&[bool]>,
444    ) -> ! {
445        let site = self.sites.get(step);
446        let name = site
447            .map(|s| s.name.clone())
448            .unwrap_or_else(|| format!("<unknown node #{step}>"));
449        let outputs: Vec<&str> = site
450            .map(|s| s.outputs.iter().map(String::as_str).collect())
451            .unwrap_or_default();
452        let inputs = self.inputs_of(step, buffer, none);
453        let enriched =
454            crate::kernel::engines::enrich_panic(payload, &name, &outputs, &self.context, &inputs);
455        crate::kernel::engines::reraise_enriched(enriched)
456    }
457}