polydat_core/compile/jit/kernels.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! JIT kernel types: structs and impls for all four kernel variants.
5//!
6//! `JitCore` holds the shared buffer, slot map, and module handle.
7//! The two kernel structs (`JitKernelRaw` and `JitKernelPushPull`)
8//! wrap a `JitCore` and a
9//! compiled function pointer, providing `eval` and accessor methods.
10
11use std::collections::HashMap;
12
13use cranelift_jit::JITModule;
14
15use crate::ast::PolydatNode;
16use crate::kernel::ProvMask;
17
18/// Finalized native code, shared by every kernel created from one
19/// program. The module's memory is never written after finalization,
20/// so sharing it across threads is sound; the wrapper exists so a
21/// kernel clone is a new state over the same code. The slot kits the
22/// code calls by address live beside it, for as long as it does.
23#[derive(Clone)]
24pub struct JitCode(std::sync::Arc<FinalizedModule>);
25
26/// A JIT module after finalization, which nothing writes again, the
27/// kits its code calls, and whether the code calls anything at all.
28struct FinalizedModule {
29 #[allow(dead_code)]
30 module: JITModule,
31 #[allow(dead_code)]
32 kits: Vec<super::codegen::SlotKitRef>,
33 fallible: bool,
34}
35
36/// The scratch a native kernel's state owns: one entry per entry the
37/// steps' kits declare, and the `(first slot, entry)` pairs of the
38/// scratch-backed `Ref2` outputs among them (axiom S9(a)).
39#[derive(Clone, Default)]
40pub(crate) struct ScratchPlan {
41 pub(crate) elems: Vec<crate::ast::ScratchElem>,
42 pub(crate) refs: Vec<(usize, usize)>,
43}
44
45// SAFETY: the module is finalized before it is wrapped and never
46// touched again; only its code runs, from any thread.
47unsafe impl Send for FinalizedModule {}
48unsafe impl Sync for FinalizedModule {}
49
50impl JitCode {
51 pub(crate) fn new(
52 module: JITModule,
53 kits: Vec<super::codegen::SlotKitRef>,
54 fallible: bool,
55 ) -> Self {
56 JitCode(std::sync::Arc::new(FinalizedModule {
57 module,
58 kits,
59 fallible,
60 }))
61 }
62
63 /// Whether the code can fail: it calls a helper, and a helper can
64 /// raise a node's failure through the longjmp catch. Code with no
65 /// call is arithmetic over the buffer, which cannot fail, so the
66 /// site that runs it needs no catch around it (the jump buffer, the
67 /// panic capture, and the unwind guard are the fixed cost of an
68 /// evaluation on the pure tier).
69 pub(crate) fn fallible(&self) -> bool {
70 self.0.fallible
71 }
72}
73
74/// Shared fields for all JIT kernel variants. A clone is a new state
75/// of the same program: the code and the nodes are shared, everything
76/// else is the clone's own (engines.md §3.5), and every extern
77/// pair in its buffer points into its own storage (axiom S3), never
78/// into the state it was cloned from.
79pub(super) struct JitCore {
80 /// The engine this kernel runs, as it reports it: the tier and
81 /// the provenance mode it was built with. State rather than a
82 /// property of the type, so one kernel type can serve a tier
83 /// that runs native code and one that runs none.
84 pub(super) engine: crate::compile::select::Engine,
85 pub(super) buffer: Vec<u64>,
86 pub(super) coord_count: usize,
87 pub(super) output_map: HashMap<String, usize>,
88 /// Slots the raw readers refuse: `Ref2` pairs (axiom S2). Set by
89 /// the assembler once the layout is known; empty means no such
90 /// slot.
91 pub(super) guard_slots: Vec<bool>,
92 /// Port type of each named output, for `get_value`'s decode.
93 pub(super) output_types: HashMap<String, crate::ast::PortType>,
94 /// The extern inputs, written through at every set.
95 pub(super) externs: crate::compile::externs::Externs,
96 /// The traversals the program declares (SRD 113), opened through the
97 /// `Kernel` trait.
98 pub(super) traversals: std::sync::Arc<[crate::dsl::traversal::Traversal]>,
99 pub(super) _module: JitCode,
100 /// Whether the code calls a helper, and so runs under the catch.
101 pub(super) fallible: bool,
102 pub(super) _nodes: std::sync::Arc<Vec<Box<dyn PolydatNode>>>,
103 /// The coordinates set through the `Kernel` trait, pending
104 /// evaluation.
105 pub(super) drive: crate::compile::Drive,
106 /// Where each step came from, for the failure path (A7).
107 pub(super) sites: std::sync::Arc<crate::compile::Attribution>,
108 /// The slot past the layout where native code names the step it
109 /// is in before calling a helper; `u64::MAX` before any.
110 pub(super) tracker: usize,
111 /// The scratch entries the steps' kits write into, owned by this
112 /// state (axiom S3); native code receives the base pointer.
113 pub(super) scratch: Vec<crate::ast::ScratchBuf>,
114 /// Axiom S9(a): (first slot of a Ref pair → scratch index) for
115 /// every scratch-backed Ref output.
116 pub(super) ref_scratch: Vec<(usize, usize)>,
117 /// The steps that are never current (runtime_model.md, R1.v): a
118 /// nondeterministic node or one downstream of it. Every write
119 /// clears their clean flags, so the next pull whose cone holds one
120 /// runs it again.
121 pub(super) volatile_steps: Vec<usize>,
122 /// The fusion units, and which of them each output slot's cone
123 /// holds.
124 pub(super) cones: ConePlan,
125 /// A clean flag per unit: set when the unit runs, cleared by a write
126 /// to an input in its provenance (runtime_model.md R2).
127 pub(super) unit_clean: Vec<u8>,
128 /// The units of the never-current steps, cleared at every write.
129 pub(super) volatile_units: Vec<usize>,
130 /// The program's one function, taking a list of units to run.
131 pub(super) entry: super::codegen::NativeDispatchFn,
132 /// Each output by index, as the host names it: its slot and type,
133 /// filled on the first pull by index so a pull does not look an
134 /// output up by name.
135 pub(super) outputs_at: Vec<(usize, crate::ast::PortType)>,
136}
137
138/// What a pull on this tier runs: the fusion units of its output's
139/// cone, in order, as a compiled kernel walks an output's precomputed
140/// cone order (runtime_model.md R2). The units are the planner's
141/// (`compile::fusion_units`): connected, convex groups of steps, each
142/// one block of the program's function. A pull hands the function its
143/// cone's units, and the function runs the ones that are not current
144/// and nothing else, so its cost is its cone's and not the program's.
145/// Every output's cone is found at build, into a table by slot, so a
146/// pull indexes it rather than searching or hashing.
147#[derive(Clone, Default)]
148pub(super) struct ConePlan {
149 /// Each step's input slots.
150 inputs: std::sync::Arc<[Box<[usize]>]>,
151 /// The step that writes each slot, or `usize::MAX` for a slot no
152 /// step writes (an input or an extern).
153 producer: std::sync::Arc<[usize]>,
154 /// The unit each step belongs to.
155 unit_of: std::sync::Arc<[u32]>,
156 /// Each unit's steps.
157 members: std::sync::Arc<[Box<[usize]>]>,
158 /// Every unit, in order, for a full evaluation.
159 all: std::sync::Arc<[u32]>,
160 /// Each slot's cone, by the slot it ends in: the units it holds, in
161 /// order. Every output's is found at build; another slot's, one a
162 /// raw read by slot asks for, when first asked.
163 by_slot: Vec<Option<std::sync::Arc<[u32]>>>,
164}
165
166impl ConePlan {
167 pub(super) fn new(
168 steps: &[(super::codegen::JitOp, Vec<usize>, Vec<usize>)],
169 slots: usize,
170 units: &crate::compile::fusion_units::UnitPlan,
171 outputs: impl IntoIterator<Item = usize>,
172 ) -> Self {
173 let mut producer = vec![usize::MAX; slots + 1];
174 for (i, (_, _, outs)) in steps.iter().enumerate() {
175 for &s in outs {
176 if s < producer.len() {
177 producer[s] = i;
178 }
179 }
180 }
181 let mut plan = ConePlan {
182 inputs: steps
183 .iter()
184 .map(|(_, ins, _)| ins.clone().into_boxed_slice())
185 .collect(),
186 producer: producer.into(),
187 unit_of: units.unit_of.iter().map(|&u| u as u32).collect(),
188 members: units
189 .units
190 .iter()
191 .map(|m| m.clone().into_boxed_slice())
192 .collect(),
193 all: (0..units.units.len() as u32).collect(),
194 by_slot: vec![None; slots + 1],
195 };
196 for slot in outputs {
197 plan.of(slot);
198 }
199 plan
200 }
201
202 /// The number of units.
203 pub(super) fn unit_count(&self) -> usize {
204 self.all.len()
205 }
206
207 /// The unit a step belongs to.
208 pub(super) fn unit_of(&self, step: usize) -> usize {
209 self.unit_of[step] as usize
210 }
211
212 /// Every unit, in order.
213 pub(super) fn all(&self) -> &[u32] {
214 &self.all
215 }
216
217 /// The units of the cone `slot` depends on, in order, found once.
218 /// A unit runs whole, so the set is closed over every member's
219 /// producers, not only the producers of the steps the output reads:
220 /// a member outside the cone still runs, and its inputs must be
221 /// current when it does.
222 #[inline]
223 pub(super) fn of(&mut self, slot: usize) -> &[u32] {
224 if self.by_slot.get(slot).is_none_or(|c| c.is_none()) {
225 self.find(slot);
226 }
227 self.by_slot[slot].as_deref().unwrap_or(&[])
228 }
229
230 #[cold]
231 fn find(&mut self, slot: usize) {
232 if slot >= self.by_slot.len() {
233 self.by_slot.resize(slot + 1, None);
234 }
235 {
236 let mut unit_seen = vec![false; self.members.len()];
237 let producer_of = |s: usize| self.producer.get(s).copied().filter(|&p| p != usize::MAX);
238 let mut stack: Vec<usize> = producer_of(slot).into_iter().collect();
239 let mut units: Vec<u32> = Vec::new();
240 while let Some(step) = stack.pop() {
241 let unit = self.unit_of[step];
242 if unit_seen[unit as usize] {
243 continue;
244 }
245 unit_seen[unit as usize] = true;
246 units.push(unit);
247 for &m in self.members[unit as usize].iter() {
248 stack.extend(self.inputs[m].iter().filter_map(|&s| producer_of(s)));
249 }
250 }
251 // Unit numbers are in dependency order, so sorted is a valid
252 // order to run them in.
253 units.sort_unstable();
254 units.dedup();
255 self.by_slot[slot] = Some(units.into());
256 }
257 }
258}
259
260impl Clone for JitCore {
261 fn clone(&self) -> Self {
262 let mut core = JitCore {
263 engine: self.engine,
264 buffer: self.buffer.clone(),
265 coord_count: self.coord_count,
266 output_map: self.output_map.clone(),
267 guard_slots: self.guard_slots.clone(),
268 output_types: self.output_types.clone(),
269 externs: self.externs.clone(),
270 traversals: self.traversals.clone(),
271 _module: self._module.clone(),
272 fallible: self.fallible,
273 _nodes: self._nodes.clone(),
274 drive: self.drive.clone(),
275 sites: self.sites.clone(),
276 tracker: self.tracker,
277 scratch: self.scratch.clone(),
278 ref_scratch: self.ref_scratch.clone(),
279 volatile_steps: self.volatile_steps.clone(),
280 cones: self.cones.clone(),
281 unit_clean: self.unit_clean.clone(),
282 volatile_units: self.volatile_units.clone(),
283 entry: self.entry,
284 outputs_at: self.outputs_at.clone(),
285 };
286 // Every pair points into this state's own storage (axiom S3):
287 // a step's scratch entry, the value an extern stores.
288 for &(slot, idx) in &core.ref_scratch {
289 let (p, l) = core.scratch[idx].ptr_len();
290 core.buffer[slot] = p;
291 core.buffer[slot + 1] = l;
292 }
293 core.externs.seed(&mut core.buffer, None);
294 core
295 }
296}
297
298impl JitCore {
299 /// Nothing to mark here: a cell another holder published to is
300 /// handled where the pending write is applied, where the kernels
301 /// mark every step dirty, since the dependents lists do not name a
302 /// cell's readers.
303 fn dirty_input(&mut self, _slot: usize) {}
304
305 /// The pure tier broadcasts nothing. It is the differential oracle
306 /// behind the hybrid and Tier-1's carrier, not a surface a host
307 /// composes under (engines.md §1, §8), so no descendant binds to
308 /// one of its outputs and it makes no cell to bind to.
309 fn output_cell_for(&self, _name: &str) -> Option<crate::kernel::SharedCell> {
310 None
311 }
312
313 /// Axiom S2 typed accessor core (borrow ties to `&self`), as the
314 /// closure tier and the hybrid have it. The pure tier owns the
315 /// same scratch and the same `(slot → entry)` map, so the typed
316 /// borrows read the same way here; `guard_slots` is this core's
317 /// name for the per-slot `Ref2` mask.
318 fn ref_entry(&self, slot: usize) -> &crate::ast::ScratchBuf {
319 match self.ref_scratch.iter().find(|(s, _)| *s == slot) {
320 Some(&(_, idx)) => &self.scratch[idx],
321 None if self.guard_slots.get(slot).copied().unwrap_or(false) => panic!(
322 "slot {slot} is a Ref pair owned by the CALLER (a kernel \
323 input) — read it on the caller side"
324 ),
325 None => panic!("slot {slot} is not a Ref2-colored slot"),
326 }
327 }
328
329 /// The value at `slot` decoded as `ty`, a pair copied out.
330 pub(super) fn slot_value(&self, slot: usize, ty: crate::ast::PortType) -> crate::ast::Value {
331 crate::compile::marshal::decode_output(&self.buffer, slot, ty)
332 }
333
334 /// One native function is the program.
335 pub(super) fn plan(&self) -> crate::EnginePlan {
336 crate::EnginePlan {
337 native_segments: 1,
338 ..Default::default()
339 }
340 }
341
342 /// The next pull or evaluation applies the pending write first.
343 pub(super) fn invalidate_all(&mut self) {
344 self.drive.stale = true;
345 }
346
347 #[allow(clippy::too_many_arguments)]
348 pub(super) fn new(
349 total_slots: usize,
350 coord_count: usize,
351 output_map: HashMap<String, usize>,
352 code: JitCode,
353 nodes: Vec<Box<dyn PolydatNode>>,
354 scratch: ScratchPlan,
355 volatile_steps: Vec<usize>,
356 entry: super::codegen::NativeDispatchFn,
357 cones: ConePlan,
358 ) -> Self {
359 let mut volatile_units: Vec<usize> =
360 volatile_steps.iter().map(|&s| cones.unit_of(s)).collect();
361 volatile_units.sort_unstable();
362 volatile_units.dedup();
363 let unit_count = cones.unit_count();
364 let mut core = Self {
365 // The pure tier, not `Native`: this core belongs to a
366 // kernel that refused every node without a native lowering
367 // rather than running its closure, and `engine()` reports
368 // what ran. The raw builder overwrites the mode.
369 engine: crate::compile::select::Engine::PureNative(
370 crate::compile::select::Provenance::PushPull,
371 ),
372 buffer: vec![0u64; total_slots + 1],
373 coord_count,
374 output_map,
375 guard_slots: Vec::new(),
376 output_types: HashMap::new(),
377 externs: crate::compile::externs::Externs::default(),
378 traversals: Vec::new().into(),
379 fallible: code.fallible(),
380 _module: code,
381 _nodes: std::sync::Arc::new(nodes),
382 drive: crate::compile::Drive::default(),
383 sites: std::sync::Arc::default(),
384 tracker: total_slots,
385 scratch: scratch
386 .elems
387 .iter()
388 .map(|e| crate::ast::ScratchBuf::new(*e))
389 .collect(),
390 ref_scratch: scratch.refs,
391 volatile_steps,
392 cones,
393 unit_clean: vec![0u8; unit_count],
394 volatile_units,
395 entry,
396 outputs_at: Vec::new(),
397 };
398 // Every pair names its own entry from the start (axiom S3), as
399 // a clone's do. A pull runs only its cone, so a step outside
400 // every cone pulled so far has not run, and its pair must still
401 // name its entry, empty until the step writes it (S9(a)).
402 for &(slot, idx) in &core.ref_scratch {
403 let (p, l) = core.scratch[idx].ptr_len();
404 core.buffer[slot] = p;
405 core.buffer[slot + 1] = l;
406 }
407 core
408 }
409
410 /// Run the units of `slot`'s cone that are not current, or of the
411 /// whole program for `None`. The function is handed the cone's
412 /// precomputed order and the clean flags: it tests each unit's flag
413 /// itself, runs the stale ones, and marks each current as it ends.
414 #[inline]
415 pub(super) fn run_units(&mut self, slot: Option<usize>) {
416 let units: &[u32] = match slot {
417 Some(s) => self.cones.of(s),
418 None => self.cones.all(),
419 };
420 // The order lives behind an `Arc` the run does not touch, so
421 // its address holds across the call.
422 let list = units.as_ptr();
423 let len = units.len() as u64;
424 let entry = self.entry;
425 let buf_const = self.buffer.as_ptr();
426 let buf_mut = self.buffer.as_mut_ptr();
427 let sc = self.scratch.as_mut_ptr();
428 let clean = self.unit_clean.as_mut_ptr();
429 self.run(move || unsafe {
430 (entry)(buf_const, buf_mut, sc, list, len, clean);
431 });
432 }
433
434 /// Every unit is dirty: a new round, or a cell another holder
435 /// published to, whose readers no dependents list names.
436 pub(super) fn dirty_all_units(&mut self) {
437 self.unit_clean.fill(0);
438 }
439
440 /// The never-current units are dirty again: every write does this
441 /// (runtime_model.md R1.v).
442 pub(super) fn dirty_volatile_units(&mut self) {
443 for &u in &self.volatile_units {
444 self.unit_clean[u] = 0;
445 }
446 }
447
448 /// The output at `index` in the host's order: its slot and type.
449 fn output_at(&mut self, index: usize) -> (usize, crate::ast::PortType) {
450 if self.outputs_at.is_empty() {
451 self.outputs_at = self
452 .externs
453 .output_names()
454 .iter()
455 .map(|n| {
456 let slot = self.output_map[n];
457 let ty = self
458 .output_types
459 .get(n)
460 .copied()
461 .unwrap_or(crate::ast::PortType::U64);
462 (slot, ty)
463 })
464 .collect();
465 }
466 *self
467 .outputs_at
468 .get(index)
469 .unwrap_or_else(|| panic!("no output at index {index}"))
470 }
471
472 /// Whether a write must run the program regardless of the cone
473 /// guard: a never-current step exists (R1.v).
474 #[inline]
475 fn has_volatile(&self) -> bool {
476 !self.volatile_steps.is_empty()
477 }
478
479 /// Axiom S9(a): every scratch-backed pair in the buffer names its
480 /// own entry, checked after a run in debug builds.
481 #[cfg(debug_assertions)]
482 fn validate_refs(&self) {
483 for &(slot, idx) in &self.ref_scratch {
484 let (p, l) = self.scratch[idx].ptr_len();
485 assert!(
486 self.buffer[slot] == p && self.buffer[slot + 1] == l,
487 "S9 ref-validator: slot pair ({slot}, {}) = ({:#x}, {}) does not match \
488 scratch[{idx}] = ({p:#x}, {l})",
489 slot + 1,
490 self.buffer[slot],
491 self.buffer[slot + 1],
492 );
493 }
494 }
495
496 /// Install the extern inputs, written through into the buffer now.
497 pub(super) fn set_externs(&mut self, externs: crate::compile::externs::Externs) {
498 externs.seed(&mut self.buffer, None);
499 self.externs = externs;
500 }
501
502 /// Set an extern by name; returns its slot for dirty marking.
503 fn set_extern(
504 &mut self,
505 name: &str,
506 value: crate::ast::Value,
507 ) -> Result<usize, crate::kernel::WriteError> {
508 Ok(self.externs.set(name, value, &mut self.buffer)?.0)
509 }
510
511 /// [`Self::set_extern`] by input index.
512 fn set_extern_at(
513 &mut self,
514 index: usize,
515 value: crate::ast::Value,
516 ) -> Result<usize, crate::kernel::WriteError> {
517 Ok(self.externs.set_at(index, value, &mut self.buffer)?.0)
518 }
519
520 /// Bind a `shared` binding to `cell` (engine parity, step 9). The
521 /// next pull or evaluation reads it.
522 fn attach_cell(&mut self, name: &str, cell: crate::kernel::SharedCell) -> Result<(), String> {
523 self.externs.attach_cell(name, cell)?;
524 self.drive.stale = true;
525 Ok(())
526 }
527
528 /// Run one native evaluation: take what cells other holders
529 /// published, refuse an unset extern (native code cannot carry a
530 /// `None`; engines.md §3.3), run inside the longjmp catch.
531 #[inline]
532 pub(super) fn run(&mut self, native: impl FnOnce()) {
533 if self.externs.cells_dirty() {
534 self.externs.refresh_cells(&mut self.buffer);
535 }
536 if let Some((name, ty)) = self.externs.first_unset() {
537 panic!(
538 "extern '{name}' ({ty}) has no value on the pure native tier, which \
539 cannot carry a `None`: every step is native code and there is no \
540 closure to propagate one through. Either it was declared without a \
541 default and never set, or a host cleared it after the build. Set it \
542 with set_input before pulling, or run this program on `native`, which \
543 answers a cleared extern with `None` as the interpreter does \
544 (docs/design/engines.md §3.3)"
545 );
546 }
547 // Code that calls no helper cannot fail: it runs bare. Otherwise
548 // native code names the step it is in before each helper call;
549 // a failure before any names none. The capture guard is armed
550 // for the run, so the helper's panic is recorded quietly and
551 // re-raised enriched, as the interpreter re-raises a node's (A7).
552 if !self.fallible {
553 native();
554 } else {
555 self.buffer[self.tracker] = u64::MAX;
556 let capture = crate::kernel::engines::EvalPanicCaptureGuard::arm();
557 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
558 super::codegen::invoke_with_catch(native)
559 }));
560 drop(capture);
561 if let Err(payload) = outcome {
562 let step = self.buffer[self.tracker] as usize;
563 let sites = std::sync::Arc::clone(&self.sites);
564 sites.reraise(payload, step, &self.buffer, None);
565 }
566 }
567 #[cfg(debug_assertions)]
568 self.validate_refs();
569 }
570
571 /// The compile-constant fold of the runtime model on this tier: a
572 /// step no input reaches runs at build, once, and is current from
573 /// then on, so what is knowable at build is known at build and
574 /// fails at build.
575 ///
576 /// The other two compiled tiers keep a step list and run the
577 /// constant steps out of it. This tier has one compiled function
578 /// and no list, so the constant steps are compiled a second time
579 /// into an entry of their own, run once over this core's buffer and
580 /// scratch, and dropped with the code that held them. The steps
581 /// carry absolute slot indices, so the entry writes the same slots
582 /// the whole-program function would have.
583 ///
584 /// Externs are not consulted: a compile-constant step is one no
585 /// input reaches, extern inputs included, so a program whose
586 /// externs are still unset folds its constants anyway. That is the
587 /// difference from [`Self::run`], which refuses an unset extern
588 /// because a real evaluation reads them.
589 pub(super) fn fold_constants(
590 &mut self,
591 folded: &[(super::codegen::JitOp, Vec<usize>, Vec<usize>)],
592 origin: &[usize],
593 total_slots: usize,
594 ) -> Result<(), crate::KernelError> {
595 if folded.is_empty() {
596 return Ok(());
597 }
598 // Graph order is topological and a constant depends on
599 // constants alone, so the filtered order is a valid order.
600 let (code_fn, code) = super::codegen::compile_jit_entry(folded, Some(total_slots))
601 .map_err(|reason| crate::KernelError::ConstantFold { reason })?;
602 let buf_ptr_const = self.buffer.as_ptr();
603 let buf_ptr_mut = self.buffer.as_mut_ptr();
604 let sc = self.scratch.as_mut_ptr();
605 let native = move || unsafe {
606 (code_fn)(buf_ptr_const, buf_ptr_mut, sc);
607 };
608 if !code.fallible() {
609 native();
610 } else {
611 self.buffer[self.tracker] = u64::MAX;
612 let capture = crate::kernel::engines::EvalPanicCaptureGuard::arm();
613 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
614 super::codegen::invoke_with_catch(native)
615 }));
616 drop(capture);
617 if let Err(payload) = outcome {
618 // The entry counts its own steps, so the tracker holds
619 // an index into `folded`; the attribution is keyed by
620 // the program's step, which `origin` gives back.
621 let step = self.buffer[self.tracker] as usize;
622 let step = origin.get(step).copied().unwrap_or(step);
623 let sites = std::sync::Arc::clone(&self.sites);
624 return Err(crate::KernelError::ConstantFold {
625 reason: sites.describe(payload, step, &self.buffer, None),
626 });
627 }
628 }
629 // `code` owns the executable memory the call ran in, so it is
630 // kept alive to here and dropped after, not before.
631 drop(code);
632 Ok(())
633 }
634}
635
636macro_rules! jit_accessors {
637 () => {
638 crate::compile::ref_readers!();
639
640 /// Returns the number of coordinate inputs this kernel accepts.
641 pub fn coord_count(&self) -> usize {
642 self.core.coord_count
643 }
644
645 /// Returns the buffer slot index for the named output, if present.
646 pub fn resolve_output(&self, name: &str) -> Option<usize> {
647 self.core.output_map.get(name).copied()
648 }
649
650 /// Returns the raw u64 value stored in the named output slot.
651 #[inline]
652 pub fn get(&self, name: &str) -> u64 {
653 self.get_slot(self.core.output_map[name])
654 }
655
656 /// Returns the raw u64 value stored at the given buffer slot
657 /// index. Refuses a `Ref2` slot (axiom S2): read those
658 /// through [`Self::get_value`].
659 #[inline]
660 pub fn get_slot(&self, slot: usize) -> u64 {
661 if self.core.guard_slots.get(slot).copied().unwrap_or(false) {
662 panic!(
663 "slot {slot} is Ref2-colored; a raw u64 read would leak an interior \
664 address. Use get_value to decode it."
665 );
666 }
667 self.core.buffer[slot]
668 }
669
670 /// The named output as a typed `Value`, decoded by its port type:
671 /// a reference pair is copied out, so the caller never holds a
672 /// reference into the buffer.
673 pub fn get_value(&self, name: &str) -> crate::ast::Value {
674 let slot = self.core.output_map[name];
675 let ty = self
676 .core
677 .output_types
678 .get(name)
679 .copied()
680 .unwrap_or(crate::ast::PortType::U64);
681 crate::compile::marshal::decode_output(&self.core.buffer, slot, ty)
682 }
683
684 /// Record the slots raw readers must refuse and each output's
685 /// port type. Called by the assembler after construction.
686 pub(crate) fn set_slot_info(
687 &mut self,
688 guard_slots: Vec<bool>,
689 output_types: HashMap<String, crate::ast::PortType>,
690 ) {
691 self.core.guard_slots = guard_slots;
692 self.core.output_types = output_types;
693 }
694
695 /// Where each step came from, for the failure path (A7).
696 pub(crate) fn set_attribution(
697 &mut self,
698 sites: std::sync::Arc<crate::compile::Attribution>,
699 ) {
700 self.core.sites = sites;
701 }
702
703 /// Run this program's compile-constant steps once, at build; see
704 /// [`JitCore::fold_constants`]. Called by the assembler after
705 /// the attribution is in place, so a constant that fails names
706 /// its node.
707 pub(crate) fn fold_constants(
708 &mut self,
709 folded: &[(super::codegen::JitOp, Vec<usize>, Vec<usize>)],
710 origin: &[usize],
711 total_slots: usize,
712 ) -> Result<(), crate::KernelError> {
713 self.core.fold_constants(folded, origin, total_slots)
714 }
715
716 /// Set an extern by name, as `PolydatState::set_input` does on
717 /// the interpreter. The value must be of the declared port
718 /// type. The value is written through into the buffer at once,
719 /// whatever its color, and every step downstream of the extern
720 /// reruns at the next evaluation.
721 pub fn set_input(
722 &mut self,
723 name: &str,
724 value: crate::ast::Value,
725 ) -> Result<(), crate::kernel::WriteError> {
726 let slot = self.core.set_extern(name, value)?;
727 self.mark_input_changed(slot);
728 Ok(())
729 }
730
731 /// [`Self::set_input`] by input index.
732 pub fn set_input_at(
733 &mut self,
734 index: usize,
735 value: crate::ast::Value,
736 ) -> Result<(), crate::kernel::WriteError> {
737 let slot = self.core.set_extern_at(index, value)?;
738 self.mark_input_changed(slot);
739 Ok(())
740 }
741
742 /// The kernel's externs by name and declared type.
743 pub fn externs(&self) -> Vec<(&str, crate::ast::PortType)> {
744 self.core.externs.names()
745 }
746
747 /// Every step downstream of a coordinate reruns at the next
748 /// evaluation: the state a kernel created from a shared program
749 /// starts in.
750 fn mark_all_dirty(&mut self) {
751 for i in 0..self.core.coord_count {
752 self.mark_input_changed(i);
753 }
754 }
755
756 /// The named output through the `Kernel` trait: the pending
757 /// writes are applied and the output's cone runs, and nothing
758 /// else (engines.md §3.1).
759 fn pull_value(&mut self, name: &str) -> crate::ast::Value {
760 let slot = self.core.output_map[name];
761 let ty = self
762 .core
763 .output_types
764 .get(name)
765 .copied()
766 .unwrap_or(crate::ast::PortType::U64);
767 self.pull_slot(slot, ty)
768 }
769
770 /// [`Self::pull_value`] by output index, through the index's
771 /// slot and type rather than its name.
772 fn pull_value_at(&mut self, index: usize) -> crate::ast::Value {
773 let (slot, ty) = self.core.output_at(index);
774 self.pull_slot(slot, ty)
775 }
776
777 /// `eval` through the `Kernel` trait: the pending coordinates.
778 fn eval_pending(&mut self) {
779 let coords = std::mem::take(&mut self.core.drive.coords);
780 self.eval(&coords);
781 self.core.drive.coords = coords;
782 }
783
784 /// The cursors the program declares, with the partitions the
785 /// compiler resolved where its `over` clause and extent were
786 /// constant, as `PolydatProgram::cursor_schemas` reports them.
787 pub fn cursor_schemas(&self) -> &[crate::iteration::source::SourceSchema] {
788 self.core.externs.cursor_schemas()
789 }
790
791 /// Narrow a cursor to one partition, as `narrow_cursor` does on
792 /// the interpreter: its `Ext` slot and six scalar projections
793 /// are set as externs.
794 pub fn set_cursor(
795 &mut self,
796 name: &str,
797 partition: &crate::iteration::cursor_partition::Partition,
798 ) -> Result<(), crate::kernel::WriteError> {
799 for (slot, value) in self.core.externs.cursor_writes(name, partition)? {
800 self.set_input(&slot, value)?;
801 }
802 Ok(())
803 }
804 };
805}
806
807// ── JitKernelRaw ───────────────────────────────────────────
808
809/// Raw JIT kernel: no provenance. Every write begins a round in which
810/// every unit is dirty; `eval` runs them all, and a pull runs its
811/// output's cone, each unit at most once in the round.
812#[derive(Clone)]
813#[doc(hidden)]
814pub struct JitKernelRaw {
815 pub(super) core: JitCore,
816}
817
818impl JitKernelRaw {
819 /// A pull through the `Kernel` trait: a pending write begins a
820 /// round, then the output's cone runs.
821 fn pull_slot(&mut self, slot: usize, ty: crate::ast::PortType) -> crate::ast::Value {
822 if self.core.drive.stale || self.core.externs.cells_dirty() {
823 let coords = std::mem::take(&mut self.core.drive.coords);
824 self.write_coords(&coords);
825 self.core.drive.coords = coords;
826 self.core.drive.stale = false;
827 self.core.dirty_all_units();
828 }
829 self.core.run_units(Some(slot));
830 self.core.slot_value(slot, ty)
831 }
832
833 /// The coordinates, written into the buffer.
834 #[inline]
835 fn write_coords(&mut self, coords: &[u64]) {
836 // Written one by one, as the other kernels write them: a slice
837 // copy of a runtime length is a call to memcpy, which costs
838 // more than the three stores it replaces.
839 for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
840 if self.core.buffer[i] != c {
841 self.core.buffer[i] = c;
842 }
843 }
844 }
845 /// Evaluate the kernel with the given coordinate values.
846 ///
847 /// Predicate violations (`is_positive`, `in_range`,
848 /// `is_one_of`) from JIT-lowered code surface as normal
849 /// Rust panics carrying the violation message. The
850 /// longjmp wrapper in `super::codegen::invoke_with_catch`
851 /// handles the transition back to Rust land when the code
852 /// calls a helper; code that calls none cannot fail and
853 /// runs bare.
854 #[inline]
855 pub fn eval(&mut self, coords: &[u64]) {
856 self.write_coords(coords);
857 self.core.dirty_all_units();
858 self.core.run_units(None);
859 }
860
861 /// Evaluate and return the value at the given buffer slot index.
862 #[inline]
863 pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
864 self.eval(coords);
865 self.core.buffer[slot]
866 }
867
868 /// A write begins a round: every unit is dirty again.
869 fn mark_input_changed(&mut self, _slot: usize) {
870 self.core.dirty_all_units();
871 }
872
873 jit_accessors!();
874}
875
876// ── JitKernelPushPull ──────────────────────────────────────
877
878/// Full optimization: push-side dirty tracking + pull-side cone guard.
879#[derive(Clone)]
880#[doc(hidden)]
881pub struct JitKernelPushPull {
882 pub(super) core: JitCore,
883 /// Per input slot, the units that read it, directly or not.
884 pub(super) input_dependents: Vec<Vec<usize>>,
885 pub(super) slot_provenance: Vec<ProvMask>,
886 pub(super) changed_mask: ProvMask,
887 /// Set by `set_input`: an extern changed, so the next evaluation
888 /// runs whatever the cone guard says.
889 pub(super) force_run: bool,
890}
891
892impl JitKernelPushPull {
893 #[inline]
894 fn set_inputs(&mut self, coords: &[u64]) {
895 self.changed_mask.clear();
896 for (i, &c) in coords.iter().enumerate().take(self.core.coord_count) {
897 if self.core.buffer[i] != c {
898 self.core.buffer[i] = c;
899 self.changed_mask.set(i);
900 self.dirty_dependents(i);
901 }
902 }
903 // A write makes every never-current unit run again (R1.v),
904 // whatever the cone guard would say of the pulled output.
905 if self.core.has_volatile() {
906 self.core.dirty_volatile_units();
907 self.force_run = true;
908 }
909 }
910
911 /// The units downstream of an input slot are dirty.
912 #[inline]
913 fn dirty_dependents(&mut self, slot: usize) {
914 if let Some(units) = self.input_dependents.get(slot) {
915 for &u in units {
916 self.core.unit_clean[u] = 0;
917 }
918 }
919 }
920
921 /// Every unit downstream of the slot reruns, and the next
922 /// evaluation runs whatever the cone guard says.
923 fn mark_input_changed(&mut self, slot: usize) {
924 self.dirty_dependents(slot);
925 self.core.dirty_volatile_units();
926 self.force_run = true;
927 }
928
929 /// A pull through the `Kernel` trait: the pending coordinates dirty
930 /// their dependents, then the output's cone runs its dirty units.
931 fn pull_slot(&mut self, slot: usize, ty: crate::ast::PortType) -> crate::ast::Value {
932 if self.core.drive.stale {
933 let coords = std::mem::take(&mut self.core.drive.coords);
934 self.set_inputs(&coords);
935 self.core.drive.coords = coords;
936 self.core.drive.stale = false;
937 }
938 // A cell another holder published to is a changed input whose
939 // readers the dependents lists do not name: every unit reruns.
940 if self.core.externs.cells_dirty() {
941 self.core.dirty_all_units();
942 }
943 self.core.run_units(Some(slot));
944 self.core.slot_value(slot, ty)
945 }
946
947 /// Evaluate the kernel with the given coordinate values.
948 #[inline]
949 pub fn eval(&mut self, coords: &[u64]) {
950 self.set_inputs(coords);
951 self.force_run = false;
952 self.core.run_units(None);
953 }
954
955 /// Evaluate and return the value at the given buffer slot index,
956 /// applying both push and pull optimizations.
957 #[inline]
958 pub fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
959 self.set_inputs(coords);
960 if !self.force_run
961 && slot < self.slot_provenance.len()
962 && !self.slot_provenance[slot].intersects(&self.changed_mask)
963 {
964 return self.core.buffer[slot];
965 }
966 self.force_run = false;
967 self.core.run_units(Some(slot));
968 self.core.buffer[slot]
969 }
970
971 jit_accessors!();
972}
973
974// ── The engine-independent surface (engines.md §3.5) ──────
975
976crate::compile::impl_kernel_trait!(JitKernelRaw);
977crate::compile::impl_kernel_trait!(JitKernelPushPull);
978crate::compile::impl_slot_kernel!(JitKernelRaw);
979crate::compile::impl_slot_kernel!(JitKernelPushPull);