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}