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