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;
55#[cfg(feature = "jit")]
56pub(crate) mod fusion_units;
57pub mod hybrid;
58#[cfg(feature = "jit")]
59pub mod jit;
60pub mod lattice;
61/// The boundary marshalling a compiled node kit reads its arguments
62/// and writes its outputs through (compiled_handles.md §4): a node
63/// crate's kits use it as the core's own do.
64pub mod marshal;
65pub mod roundtrip_lint;
66pub mod select;
67pub mod simd_plan;
68#[cfg(feature = "jit")]
69pub mod simd_tier1;
70
71/// Axiom S2 typed accessors, shared by the P2 and hybrid kernel
72/// types (both expose `self.core.ref_entry(slot)`). Each returns
73/// a borrow whose lifetime ties to `&self`, so the borrow checker
74/// statically prevents holding a slice across the next
75/// `eval(&mut self)` — stale Ref reads are compile errors.
76macro_rules! ref_readers {
77 () => {
78 /// Borrow a `vec_f32` output's current contents.
79 pub fn read_vec_f32(&self, slot: usize) -> &[f32] {
80 match self.core.ref_entry(slot) {
81 crate::ast::ScratchBuf::F32(v) => v,
82 other => panic!("slot {slot} is not f32-lane scratch: {other:?}"),
83 }
84 }
85 /// Borrow a `vec_f64` output's current contents.
86 pub fn read_vec_f64(&self, slot: usize) -> &[f64] {
87 match self.core.ref_entry(slot) {
88 crate::ast::ScratchBuf::F64(v) => v,
89 other => panic!("slot {slot} is not f64-lane scratch: {other:?}"),
90 }
91 }
92 /// Borrow a `vec_f16` output's current contents.
93 pub fn read_vec_f16(&self, slot: usize) -> &[half::f16] {
94 match self.core.ref_entry(slot) {
95 crate::ast::ScratchBuf::F16(v) => v,
96 other => panic!("slot {slot} is not f16-lane scratch: {other:?}"),
97 }
98 }
99 /// Borrow a `vec_i8` output's current contents.
100 pub fn read_vec_i8(&self, slot: usize) -> &[i8] {
101 match self.core.ref_entry(slot) {
102 crate::ast::ScratchBuf::I8(v) => v,
103 other => panic!("slot {slot} is not i8-lane scratch: {other:?}"),
104 }
105 }
106 /// Borrow a `vec_i16` output's current contents.
107 pub fn read_vec_i16(&self, slot: usize) -> &[i16] {
108 match self.core.ref_entry(slot) {
109 crate::ast::ScratchBuf::I16(v) => v,
110 other => panic!("slot {slot} is not i16-lane scratch: {other:?}"),
111 }
112 }
113 /// Borrow a `vec_i32` output's current contents.
114 pub fn read_vec_i32(&self, slot: usize) -> &[i32] {
115 match self.core.ref_entry(slot) {
116 crate::ast::ScratchBuf::I32(v) => v,
117 other => panic!("slot {slot} is not i32-lane scratch: {other:?}"),
118 }
119 }
120 /// Borrow a `vec_i64` output's current contents.
121 pub fn read_vec_i64(&self, slot: usize) -> &[i64] {
122 match self.core.ref_entry(slot) {
123 crate::ast::ScratchBuf::I64(v) => v,
124 other => panic!("slot {slot} is not i64-lane scratch: {other:?}"),
125 }
126 }
127 };
128}
129pub(crate) use ref_readers;
130
131/// The accessors every compiled kernel type carries, whichever tier it
132/// belongs to: reading an output by name or by slot, the externs and
133/// the cursors it declares, and applying the coordinates a `Kernel`
134/// caller left pending before a pull or an eval. Every one of them
135/// delegates to `self.core`, so none decides anything about evaluation
136/// — which is why seven types can share one copy.
137///
138/// `$set_coords` is the coordinate writer the type uses: the provenance
139/// modes that track a changed-input mask apply coordinates through
140/// `set_inputs`, the rest through `set_coords`. It is the only thing
141/// that varies, and the closure tier and the hybrid had a macro each to
142/// vary it.
143macro_rules! kernel_accessors {
144 ($set_coords:ident) => {
145 /// How many inputs are coordinates. (The core's `coord_count`
146 /// field counts the buffer slots all inputs occupy.)
147 pub fn coord_count(&self) -> usize {
148 self.core.externs.coordinate_count()
149 }
150
151 /// The slot of a named output.
152 pub fn resolve_output(&self, name: &str) -> Option<usize> {
153 self.core.output_map.get(name).copied()
154 }
155
156 /// Read an output by pre-resolved slot index. Panics on
157 /// Ref2-colored slots (axiom S2) — use `read_vec_*`.
158 #[inline]
159 pub fn get_slot(&self, slot: usize) -> u64 {
160 self.core.guard_ref_slot(slot);
161 self.core.buffer[slot]
162 }
163
164 /// Read a named output variate after `eval()`. Panics on
165 /// Ref2-colored outputs (axiom S2) — use `read_vec_*`.
166 #[inline]
167 pub fn get(&self, name: &str) -> u64 {
168 let slot = self.core.output_map[name];
169 self.core.guard_ref_slot(slot);
170 self.core.buffer[slot]
171 }
172
173 /// The named output as a typed `Value`, decoded by its port
174 /// type: a `Ref2` output is copied out through its pair
175 /// (compiled_handles.md §4), so the caller never holds a
176 /// pointer; a slot that holds `None` reads as `None`.
177 pub fn get_value(&self, name: &str) -> crate::ast::Value {
178 self.core.value_of(name)
179 }
180
181 /// A named output, its cone run if a write is pending.
182 pub fn pull_output(&mut self, name: &str) -> crate::ast::Value {
183 self.core.pull_named(name)
184 }
185
186 /// The named output through the `Kernel` trait: the pending
187 /// coordinates are applied, a round begins if a write is pending, and
188 /// only the output's cone runs.
189 fn pull_value(&mut self, name: &str) -> crate::ast::Value {
190 self.apply_pending_coords();
191 self.pull_output(name)
192 }
193
194 /// [`Self::pull_value`] by output index.
195 fn pull_value_at(&mut self, index: usize) -> crate::ast::Value {
196 self.apply_pending_coords();
197 self.core.pull_at(index)
198 }
199
200 /// The coordinates of a pending write, applied once: a pull
201 /// after the first in a round finds nothing written and skips
202 /// the comparison. The round itself begins in the core, which
203 /// clears the pending flag.
204 #[inline]
205 fn apply_pending_coords(&mut self) {
206 if self.core.drive.stale {
207 let coords = std::mem::take(&mut self.core.drive.coords);
208 self.$set_coords(&coords);
209 self.core.drive.coords = coords;
210 }
211 }
212
213 /// `eval` through the `Kernel` trait: the pending coordinates,
214 /// then every step.
215 fn eval_pending(&mut self) {
216 let coords = std::mem::take(&mut self.core.drive.coords);
217 self.eval(&coords);
218 self.core.drive.coords = coords;
219 }
220
221 /// The kernel's externs by name and declared type.
222 pub fn externs(&self) -> Vec<(&str, crate::ast::PortType)> {
223 self.core.externs.names()
224 }
225
226 /// The cursors the program declares, with the partitions the
227 /// compiler resolved where its `over` clause and extent were
228 /// constant, as `PolydatProgram::cursor_schemas` reports them.
229 pub fn cursor_schemas(&self) -> &[crate::iteration::source::SourceSchema] {
230 self.core.externs.cursor_schemas()
231 }
232
233 /// Narrow a cursor to one partition, as `narrow_cursor` does on
234 /// the interpreter: its `Ext` slot and six scalar projections
235 /// are set as externs.
236 pub fn set_cursor(
237 &mut self,
238 name: &str,
239 partition: &crate::iteration::cursor_partition::Partition,
240 ) -> Result<(), crate::kernel::WriteError> {
241 for (slot, value) in self.core.externs.cursor_writes(name, partition)? {
242 self.set_input(&slot, value)?;
243 }
244 Ok(())
245 }
246
247 crate::compile::ref_readers!();
248 };
249}
250pub(crate) use kernel_accessors;
251
252/// SRD-74's fusion rule: whether a node may join the run of fused
253/// code being formed, as far as `None` is concerned. The one predicate
254/// both fusers apply — the interpreter's cone planner and the hybrid's
255/// segment batcher — so that what one admits the other admits.
256///
257/// Fused code answers a `None` on a boundary input by making every one
258/// of its outputs `None`, because native code cannot carry one. That is
259/// SRD-74 Rule 1 and it is the right answer for a node that propagates
260/// a `None`. It is the wrong answer for a node that *consumes* one and
261/// keeps going — `to_json` writes `null`, a `printf` with an `Option`
262/// argument writes its own text — so such a node may join only when
263/// every one of its inputs comes from inside, where a `None` cannot
264/// arrive: either no boundary input is `None` and the run proceeds
265/// normally, or one is and the whole run answers `None` without any
266/// member running at all.
267///
268/// `eligible` is indexed by node and filled in topological order, so a
269/// node's producers are decided before it is.
270#[cfg(feature = "jit")]
271pub(crate) fn none_rule_admits(
272 accepts_none: bool,
273 wiring: &[crate::kernel::WireSource],
274 eligible: &[bool],
275) -> bool {
276 !accepts_none
277 || wiring
278 .iter()
279 .all(|src| matches!(src, crate::kernel::WireSource::NodeOutput(j, _) if eligible[*j]))
280}
281
282/// The highest tier a node can reach, given the types of the wires
283/// feeding it: the one answer to a question four places were asking
284/// separately.
285///
286/// The order is the one every builder walks. Native first, since a node
287/// with a lowering joins a segment; then the compiled forms, in the
288/// order `assembly::node_step_op` tries them — a copy step, the scalar
289/// `compiled_u64`, the node's own slot kit; then the interpreter.
290///
291/// The wire types are not optional. `compiled_slot` is offered per call
292/// site with the types the kernel fixed, so a caller without them can
293/// only ask the first two questions, and the three callers that
294/// reported a tier rather than selecting one did exactly that — they
295/// asked `compiled_u64().is_some()` and called a node with a slot kit
296/// `Phase1`, which is what the binary printed for `printf`.
297pub fn node_tier(
298 node: &dyn crate::ast::PolydatNode,
299 wire_types: &[crate::ast::PortType],
300) -> crate::ast::CompileLevel {
301 #[cfg(feature = "jit")]
302 if !matches!(
303 crate::compile::jit::classify_node_typed(node, wire_types),
304 crate::compile::jit::JitOp::Fallback
305 ) {
306 return crate::ast::CompileLevel::Phase3;
307 }
308 let meta = node.meta();
309 let is_copy =
310 (meta.name == "identity" || meta.name.starts_with("__port_")) && meta.outs.len() == 1;
311 let has_kit = node
312 .compiled_slot(
313 wire_types,
314 crate::compile::select::Engine::Closures(crate::compile::select::Provenance::Auto),
315 )
316 .is_some();
317 if is_copy || node.compiled_u64().is_some() || has_kit {
318 crate::ast::CompileLevel::Phase2
319 } else {
320 crate::ast::CompileLevel::Phase1
321 }
322}
323
324/// The provenance of every buffer slot: which input slots reach it,
325/// as an exact multi-word mask, for the pull-side cone guard of every
326/// compiled kernel. `input_dependents` is indexed by input slot (a
327/// multi-slot input repeats its list per slot) and lists the steps
328/// downstream of that slot; `step_output_slots` gives each step's
329/// output slots, which all take the step's mask. A coordinate slot's
330/// provenance is itself.
331pub(crate) fn slot_provenance(
332 coord_count: usize,
333 total_slots: usize,
334 step_output_slots: &[&[usize]],
335 input_dependents: &[Vec<usize>],
336) -> Vec<crate::kernel::ProvMask> {
337 use crate::kernel::ProvMask;
338 let step_count = step_output_slots.len();
339 let mut step_prov: Vec<ProvMask> = (0..step_count).map(|_| ProvMask::empty()).collect();
340 for (input_slot, deps) in input_dependents.iter().enumerate() {
341 for &step in deps {
342 if step < step_count {
343 step_prov[step].set(input_slot);
344 }
345 }
346 }
347 let mut slots: Vec<ProvMask> = (0..total_slots).map(|_| ProvMask::empty()).collect();
348 for (i, slot) in slots.iter_mut().enumerate().take(coord_count) {
349 slot.set(i);
350 }
351 for (step, outs) in step_output_slots.iter().enumerate() {
352 for &slot in outs.iter() {
353 if slot < slots.len() {
354 slots[slot] = step_prov[step].clone();
355 }
356 }
357 }
358 slots
359}
360
361/// The coordinates a host set last on a compiled kernel and whether
362/// they have been evaluated: what the [`Kernel`](crate::kernel::Kernel)
363/// trait's `set_inputs` and `pull` keep between calls.
364#[derive(Clone, Default)]
365pub(crate) struct Drive {
366 pub(crate) coords: Vec<u64>,
367 pub(crate) stale: bool,
368}
369
370/// The slot surface of a compiled kernel: the extended API, over and
371/// above the [`Kernel`](crate::kernel::Kernel) trait every engine
372/// answers.
373///
374/// Every compiled engine lays its program out over one flat `u64` slot
375/// buffer (engines.md §6). That layout is an implementation detail, and
376/// this trait is where it is admitted: a slot index instead of an
377/// output name, a raw `u64` instead of a `Value`, a borrow into the
378/// scratch a by-reference output writes. The interpreter does not
379/// implement it and cannot — its buffers are typed `Value`s and it has
380/// no slot to name — which is the point: the shape of this trait *is*
381/// the thing the compiled tiers share and the interpreter does not.
382///
383/// **This is not the surface for running a program.** Driving a kernel
384/// is `Kernel`, on every engine, and a host that never names an engine
385/// never sees this trait. Reach for it when the implementation detail
386/// is the subject: a differential test asserting on what was laid out,
387/// a benchmark measuring a tier without the `Value` construction and
388/// the name lookup a `pull` pays, a diagnostic reporting on a slot.
389///
390/// It is a subtrait rather than a wider `Kernel`, so it is opt-in at
391/// the import: a caller who does not write `use SlotKernel` does not
392/// have these methods on their kernel at all. And it is reachable
393/// without naming a kernel type, through
394/// [`PolydatAssembler::compile_slots`](crate::compile::assembly::PolydatAssembler::compile_slots),
395/// which hands back a `Box<dyn SlotKernel>` that upcasts to
396/// `Box<dyn Kernel>` wherever the ordinary surface will do.
397pub trait SlotKernel: crate::kernel::Kernel {
398 /// The buffer slot a named output writes, resolved once so a
399 /// caller reading the same output every cycle pays no lookup.
400 fn resolve_output(&self, name: &str) -> Option<usize>;
401
402 /// The raw `u64` in `slot`, as it stands: no evaluation, no
403 /// decoding. Panics on a `Ref2` slot (axiom S2), which has no
404 /// scalar to read — use the `read_vec_*` borrows.
405 fn get_slot(&self, slot: usize) -> u64;
406
407 /// [`Self::get_slot`] by output name.
408 fn get(&self, name: &str) -> u64;
409
410 /// A named output decoded by its port type, a `Ref2` output copied
411 /// out through its pair so the caller never holds a pointer. Reads
412 /// what is there; [`Kernel::pull`](crate::kernel::Kernel::pull)
413 /// evaluates first.
414 fn get_value(&self, name: &str) -> crate::ast::Value;
415
416 /// Set the coordinates, evaluate what `slot` needs, and return its
417 /// raw `u64`. The whole read in one call and one `u64`, which is
418 /// what a tier benchmark wants: `pull_at` gives the same value
419 /// through a `Value` it has to construct.
420 fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64;
421
422 /// Set the coordinates and run every step, the whole program in
423 /// one call. [`Kernel::eval`](crate::kernel::Kernel::eval) is the
424 /// same evaluation over coordinates already written with
425 /// `set_inputs`; this is the form that takes them, which is what a
426 /// loop over a coordinate range wants.
427 fn eval_at(&mut self, coords: &[u64]);
428
429 /// Borrow a `vec_f32` output's current contents. The borrow ties to
430 /// `&self`, so holding one across the next evaluation is a compile
431 /// error rather than a stale read (axiom S2).
432 fn read_vec_f32(&self, slot: usize) -> &[f32];
433 /// Borrow a `vec_f64` output's current contents.
434 fn read_vec_f64(&self, slot: usize) -> &[f64];
435 /// Borrow a `vec_f16` output's current contents.
436 fn read_vec_f16(&self, slot: usize) -> &[half::f16];
437 /// Borrow a `vec_i8` output's current contents.
438 fn read_vec_i8(&self, slot: usize) -> &[i8];
439 /// Borrow a `vec_i16` output's current contents.
440 fn read_vec_i16(&self, slot: usize) -> &[i16];
441 /// Borrow a `vec_i32` output's current contents.
442 fn read_vec_i32(&self, slot: usize) -> &[i32];
443 /// Borrow a `vec_i64` output's current contents.
444 fn read_vec_i64(&self, slot: usize) -> &[i64];
445}
446
447/// [`SlotKernel`] for a compiled kernel, forwarding to the inherent
448/// methods the type already has. The trait is the surface; the
449/// inherent copies are what it forwards to and what this crate calls.
450macro_rules! impl_slot_kernel {
451 ($ty:ident) => {
452 impl crate::compile::SlotKernel for $ty {
453 fn resolve_output(&self, name: &str) -> Option<usize> {
454 $ty::resolve_output(self, name)
455 }
456 fn get_slot(&self, slot: usize) -> u64 {
457 $ty::get_slot(self, slot)
458 }
459 fn get(&self, name: &str) -> u64 {
460 $ty::get(self, name)
461 }
462 fn get_value(&self, name: &str) -> crate::ast::Value {
463 $ty::get_value(self, name)
464 }
465 fn eval_for_slot(&mut self, coords: &[u64], slot: usize) -> u64 {
466 $ty::eval_for_slot(self, coords, slot)
467 }
468 fn eval_at(&mut self, coords: &[u64]) {
469 $ty::eval(self, coords)
470 }
471 fn read_vec_f32(&self, slot: usize) -> &[f32] {
472 $ty::read_vec_f32(self, slot)
473 }
474 fn read_vec_f64(&self, slot: usize) -> &[f64] {
475 $ty::read_vec_f64(self, slot)
476 }
477 fn read_vec_f16(&self, slot: usize) -> &[half::f16] {
478 $ty::read_vec_f16(self, slot)
479 }
480 fn read_vec_i8(&self, slot: usize) -> &[i8] {
481 $ty::read_vec_i8(self, slot)
482 }
483 fn read_vec_i16(&self, slot: usize) -> &[i16] {
484 $ty::read_vec_i16(self, slot)
485 }
486 fn read_vec_i32(&self, slot: usize) -> &[i32] {
487 $ty::read_vec_i32(self, slot)
488 }
489 fn read_vec_i64(&self, slot: usize) -> &[i64] {
490 $ty::read_vec_i64(self, slot)
491 }
492 }
493 };
494}
495pub(crate) use impl_slot_kernel;
496
497/// The [`Kernel`](crate::kernel::Kernel) impl every compiled kernel
498/// shares: the type's inherent `eval_pending`, `pull_value`,
499/// `pull_value_at`, `set_input`, `set_input_at`, `set_cursor`,
500/// `mark_all_dirty`, and a `core` with `drive`, `externs`,
501/// `coord_count`, `output_types`, `output_map`, `buffer`,
502/// `traversals`, and `plan`/`invalidate_all`/`attach_cell`/
503/// `slot_value`.
504macro_rules! impl_kernel_trait {
505 ($ty:ident) => {
506 impl crate::kernel::Kernel for $ty {
507 fn engine(&self) -> crate::compile::select::Engine {
508 self.core.engine
509 }
510 fn set_inputs(&mut self, coords: &[u64]) {
511 self.core.drive.coords.clear();
512 self.core.drive.coords.extend_from_slice(coords);
513 self.core.drive.stale = true;
514 }
515 fn set_input(
516 &mut self,
517 name: &str,
518 value: crate::ast::Value,
519 ) -> Result<(), crate::kernel::WriteError> {
520 if self.core.externs.is_const_name(name) {
521 return Err(crate::kernel::WriteError::ConstSlot {
522 slot: name.to_string(),
523 });
524 }
525 self.core.drive.stale = true;
526 $ty::set_input(self, name, value)
527 }
528 fn const_inits(&self) -> &[crate::kernel::ConstInit] {
529 self.core.externs.const_inits()
530 }
531 fn init_input_at(
532 &mut self,
533 index: usize,
534 value: crate::ast::Value,
535 ) -> Result<(), crate::kernel::WriteError> {
536 self.core.drive.stale = true;
537 $ty::set_input_at(self, index, value)
538 }
539 fn set_cursor(
540 &mut self,
541 name: &str,
542 partition: &crate::iteration::cursor_partition::Partition,
543 ) -> Result<(), crate::kernel::WriteError> {
544 self.core.drive.stale = true;
545 $ty::set_cursor(self, name, partition)
546 }
547 fn eval(&mut self) {
548 self.eval_pending();
549 self.core.drive.stale = false;
550 }
551 fn pull(&mut self, name: &str) -> crate::ast::Value {
552 self.pull_value(name)
553 }
554 fn input_names(&self) -> Vec<String> {
555 self.core.externs.input_names().to_vec()
556 }
557 /// In declaration order, as the interpreter lists them: the
558 /// assembler sets them on every compiled kernel.
559 fn output_names(&self) -> Vec<String> {
560 self.core.externs.output_names().to_vec()
561 }
562 fn output_type(&self, name: &str) -> Option<crate::ast::PortType> {
563 self.core.output_types.get(name).copied()
564 }
565 fn externs(&self) -> Vec<(String, crate::ast::PortType)> {
566 self.core
567 .externs
568 .names()
569 .into_iter()
570 .map(|(n, t)| (n.to_string(), t))
571 .collect()
572 }
573 fn cursor_schemas(&self) -> &[crate::iteration::source::SourceSchema] {
574 self.core.externs.cursor_schemas()
575 }
576 fn input_value(&self, name: &str) -> Option<crate::ast::Value> {
577 self.core.externs.value(name).or_else(|| {
578 let i = self
579 .core
580 .externs
581 .input_names()
582 .iter()
583 .position(|n| n == name)?;
584 if i < self.core.externs.coordinate_count() {
585 let pending = self.core.drive.coords.get(i).copied();
586 Some(crate::ast::Value::U64(
587 pending.unwrap_or(self.core.buffer[i]),
588 ))
589 } else {
590 None
591 }
592 })
593 }
594 fn traversals(&self) -> &[crate::dsl::traversal::Traversal] {
595 &self.core.traversals
596 }
597 fn plan(&self) -> crate::EnginePlan {
598 self.core.plan()
599 }
600 fn input_index(&self, name: &str) -> Option<usize> {
601 self.core
602 .externs
603 .input_names()
604 .iter()
605 .position(|n| n == name)
606 }
607 fn set_input_at(
608 &mut self,
609 index: usize,
610 value: crate::ast::Value,
611 ) -> Result<(), crate::kernel::WriteError> {
612 if self.core.externs.is_const_index(index) {
613 return Err(crate::kernel::WriteError::ConstSlot {
614 slot: self.core.externs.input_names()[index].clone(),
615 });
616 }
617 self.core.drive.stale = true;
618 $ty::set_input_at(self, index, value)
619 }
620 fn output_index(&self, name: &str) -> Option<usize> {
621 self.core
622 .externs
623 .output_names()
624 .iter()
625 .position(|n| n == name)
626 }
627 fn pull_at(&mut self, index: usize) -> crate::ast::Value {
628 self.pull_value_at(index)
629 }
630 fn traverse(&mut self, index: usize) -> Result<crate::kernel::TraversalStream, String> {
631 let traversal = self.core.traversals.get(index).cloned().ok_or_else(|| {
632 format!(
633 "no traversal at index {index}; the program declares {}",
634 self.core.traversals.len()
635 )
636 })?;
637 crate::kernel::activation::open_traversal(self, traversal)
638 }
639 fn invalidate_all(&mut self) {
640 self.mark_all_dirty();
641 self.core.invalidate_all();
642 }
643 fn shared_cells(&self) -> Vec<crate::kernel::SharedCellEntry> {
644 self.core.externs.shared_cells()
645 }
646 fn output_cell(&self, name: &str) -> Option<crate::kernel::SharedCell> {
647 self.core.output_cell_for(name)
648 }
649 fn output_modifier(&self, name: &str) -> crate::dsl::ast::BindingModifier {
650 self.core.externs.output_modifier(name)
651 }
652 fn cells_in_scope(&self) -> Vec<crate::kernel::SharedCellEntry> {
653 self.core.externs.cells_in_scope()
654 }
655 fn set_transit_cells(&mut self, cells: Vec<crate::kernel::SharedCellEntry>) {
656 self.core.externs.set_transit_cells(cells);
657 }
658 fn input_port_type(&self, name: &str) -> Option<crate::ast::PortType> {
659 self.core.externs.input_port_type(name)
660 }
661 fn bind_input_cell(&mut self, name: &str, cell: crate::kernel::SharedCell) -> bool {
662 let Some(slot) = self.core.externs.bind_cell(name, cell) else {
663 return false;
664 };
665 // The slot's value is the cell's from the next refresh,
666 // so everything downstream of it reruns.
667 self.core.dirty_input(slot);
668 true
669 }
670 fn attach_shared_cell(
671 &mut self,
672 name: &str,
673 cell: crate::kernel::SharedCell,
674 ) -> Result<(), String> {
675 self.core.attach_cell(name, cell)
676 }
677 fn into_program(
678 mut self: Box<Self>,
679 ) -> std::sync::Arc<dyn crate::kernel::KernelProgram> {
680 self.mark_all_dirty();
681 self.core.drive.stale = true;
682 std::sync::Arc::new(crate::kernel::SharedKernel(*self))
683 }
684 fn ledger(&self) -> &std::sync::Arc<crate::kernel::CompileLedger> {
685 self.core.externs.ledger()
686 }
687 fn coord_count(&self) -> usize {
688 self.core.externs.coordinate_count()
689 }
690 fn input_value_at(&self, index: usize) -> Option<crate::ast::Value> {
691 if index < self.core.externs.coordinate_count() {
692 let pending = self.core.drive.coords.get(index).copied();
693 return Some(crate::ast::Value::U64(
694 pending.unwrap_or(self.core.buffer[index]),
695 ));
696 }
697 self.core.externs.value_at(index)
698 }
699 fn input_default_at(&self, index: usize) -> Option<crate::ast::Value> {
700 if index < self.core.externs.coordinate_count() {
701 return Some(crate::ast::Value::U64(0));
702 }
703 self.core.externs.default_at(index)
704 }
705 fn input_is_cell_bound(&self, index: usize) -> bool {
706 self.core.externs.is_cell_bound_at(index)
707 }
708 fn reset_inputs(&mut self) {
709 let count = self.core.externs.input_names().len();
710 for index in self.core.externs.coordinate_count()..count {
711 if self.core.externs.is_cell_bound_at(index) {
712 continue;
713 }
714 let (Some(now), Some(default)) = (
715 self.core.externs.value_at(index),
716 self.core.externs.default_at(index),
717 ) else {
718 continue;
719 };
720 if now != default {
721 // The typed write, so what depends on the input
722 // is marked as any write marks it. A declared
723 // default satisfies its own slot.
724 let _ = crate::kernel::Kernel::set_input_at(self, index, default);
725 }
726 }
727 }
728 fn fork(&self) -> Box<dyn crate::kernel::Kernel> {
729 // A clone is a new state of the same program with this
730 // one's values: shared slots keep their cells, transit
731 // cells travel, and broadcast cells stay with the
732 // original, which is what descendants are bound to.
733 Box::new(self.clone())
734 }
735 fn publish_broadcasts(&mut self) {
736 if !self.core.externs.broadcasts() {
737 return;
738 }
739 let names: Vec<String> = self.core.externs.output_names().to_vec();
740 for name in names {
741 let Some(&slot) = self.core.output_map.get(&name) else {
742 continue;
743 };
744 if self.core.externs.published_output(slot).is_none() {
745 continue;
746 }
747 // The pull by name publishes through the cell. A
748 // failure is left for the pull that needs the value.
749 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
750 self.pull_value(&name);
751 }));
752 }
753 }
754 fn commit_write_throughs(&mut self) -> Result<(), String> {
755 let pairs = self.core.externs.write_throughs().to_vec();
756 let mut pending = Vec::with_capacity(pairs.len());
757 for (export, source) in &pairs {
758 let Some(slot_type) = self.core.externs.input_port_type(export) else {
759 continue;
760 };
761 let value = self.pull_value(source);
762 let value =
763 crate::kernel::check_write_through_type(export, source, slot_type, value)?;
764 pending.push((export.clone(), value));
765 }
766 for (export, value) in pending {
767 crate::kernel::Kernel::set_input(self, &export, value)
768 .map_err(|e| format!("write-through into `{export}`: {e}"))?;
769 }
770 Ok(())
771 }
772 fn program_id(&self) -> crate::kernel::ProgramId {
773 crate::kernel::ProgramId(self.core.program_identity())
774 }
775 fn input_type_origin(&self, name: &str) -> Option<crate::kernel::TypeOrigin> {
776 self.core.externs.input_type_origin(name)
777 }
778 }
779
780 impl crate::kernel::KernelInternals for $ty {
781 fn set_write_throughs(&mut self, pairs: Vec<(String, String)>) {
782 self.core.externs.set_write_throughs(pairs);
783 }
784 /// A compiled kernel keeps the traversals; each carries the
785 /// comprehension its producer resolved to at compile time.
786 fn set_traversals(
787 &mut self,
788 traversals: Vec<crate::dsl::traversal::Traversal>,
789 _producers: Vec<crate::dsl::traversal::Producer>,
790 ) {
791 self.core.traversals = traversals.into();
792 }
793 fn slot_value(&self, slot: usize, ty: crate::ast::PortType) -> crate::ast::Value {
794 self.core.slot_value(slot, ty)
795 }
796 /// Only for an output fixed for the kernel's life, a const or
797 /// a value folded at build: a computed output's slot holds its
798 /// last evaluated value, which is not the scope's.
799 fn folded_value(&self, name: &str) -> Option<crate::ast::Value> {
800 if !self.core.externs.is_fixed_output(name) {
801 return None;
802 }
803 let slot = *self.core.output_map.get(name)?;
804 let ty = *self.core.output_types.get(name)?;
805 Some(self.core.slot_value(slot, ty))
806 }
807 fn set_cursor_extent(&mut self, index: usize, extent: u64) {
808 self.core.externs.set_cursor_extent(index, extent);
809 }
810 fn reset_to_program(&mut self) {
811 self.core.externs.reset_to_program(&mut self.core.buffer);
812 self.mark_all_dirty();
813 }
814 }
815 };
816}
817pub(crate) use impl_kernel_trait;
818
819/// The bookkeeping every compiled engine keeps, whatever its steps
820/// are: the evaluation round and what ran in it, the clean flags and
821/// what a write dirties, the extern writes and the cell refresh, the
822/// reference pairs a step publishes, and reading an output back.
823///
824/// Both compiled cores carry the same fields for these and, until this
825/// macro, the same eighteen method bodies byte for byte. None of them
826/// touches the step list, which is the one thing the two tiers
827/// genuinely differ about: a step on the closure tier is always a
828/// closure, and on the native tier it is a closure or a run of native
829/// code. That difference lives in the run loops, which stay per tier.
830macro_rules! shared_core_methods {
831 () => {
832 /// Axiom S9: every reference pair in the buffer names the
833 /// scratch entry that owns it. A slot is skipped when nothing
834 /// has been published into it — its step has not run, or it
835 /// carries `None`.
836 ///
837 /// The two tiers wrote this assertion separately and their
838 /// skip predicates had drifted apart: one skipped a `None`
839 /// slot only when a step owned it, the other whenever the
840 /// slot was `None`. Nothing is published either way, so the
841 /// looser test is the right one and is now the only one.
842 ///
843 /// Axiom S2 typed accessor core: resolve a Ref pair's first
844 /// slot to its kernel-owned scratch entry. The returned
845 /// borrow ties to `&self`, so holding it across the next
846 /// `eval(&mut self)` is a compile error — stale reads are
847 /// statically impossible.
848 fn ref_entry(&self, slot: usize) -> &crate::ast::ScratchBuf {
849 match self.ref_scratch.iter().find(|(s, _)| *s == slot) {
850 Some(&(_, idx)) => &self.scratch[idx],
851 None if self.ref_slots.get(slot).copied().unwrap_or(false) => panic!(
852 "slot {slot} is a Ref pair owned by the CALLER (a kernel \
853 input) — read it on the caller side"
854 ),
855 None => panic!("slot {slot} is not a Ref2-colored slot"),
856 }
857 }
858
859 /// Run `body` with the failure path armed: a panic inside a
860 /// step is recorded quietly and re-raised enriched, as the
861 /// interpreter re-raises a node's (A7), and every reference
862 /// pair is checked afterwards in a debug build.
863 ///
864 /// `#[inline]` is load-bearing: this wraps every `eval`, and
865 /// without it the native rung of the ladder pays a call and
866 /// about seven percent.
867 #[inline]
868 fn run_guarded(&mut self, body: impl FnOnce(&mut Self)) {
869 let capture = crate::kernel::engines::EvalPanicCaptureGuard::arm();
870 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| body(self)));
871 drop(capture);
872 if let Err(payload) = outcome {
873 let sites = std::sync::Arc::clone(&self.sites);
874 let node = self.failing_node();
875 sites.reraise(payload, node, &self.buffer, Some(&self.none));
876 }
877 #[cfg(debug_assertions)]
878 self.validate_refs();
879 }
880
881 /// Gated to `debug_assertions` to match its call sites, which
882 /// compile out in release.
883 #[cfg(debug_assertions)]
884 fn validate_refs(&self) {
885 for &(slot, idx) in &self.ref_scratch {
886 let unpublished = self.none[slot]
887 || matches!(self.slot_step.get(slot), Some(Some(step)) if self.ran[*step] == 0);
888 if unpublished {
889 continue;
890 }
891 let (p, l) = self.scratch[idx].ptr_len();
892 assert!(
893 self.buffer[slot] == p && self.buffer[slot + 1] == l,
894 "S9 ref-validator: slot pair ({slot}, {}) = ({:#x}, {}) \
895 does not match scratch[{idx}] = ({p:#x}, {l}) — a slot \
896 op failed to republish or wrote the wrong slots",
897 slot + 1,
898 self.buffer[slot],
899 self.buffer[slot + 1],
900 );
901 }
902 }
903
904 fn attach_cell(
905 &mut self,
906 name: &str,
907 cell: crate::kernel::SharedCell,
908 ) -> Result<(), String> {
909 let slot = self.externs.attach_cell(name, cell)?;
910 self.dirty_input(slot);
911 self.drive.stale = true;
912 Ok(())
913 }
914
915 #[inline]
916 fn begin_epoch(&mut self) {
917 if self.externs.cells_dirty() {
918 self.externs.refresh_cells(&mut self.buffer);
919 }
920 self.dirty_refreshed();
921 self.epoch += 1;
922 self.all_ran = false;
923 for &i in self.volatile_steps.iter() {
924 self.clean[i] = false;
925 }
926 self.drive.stale = false;
927 }
928
929 /// A read that begins no round still re-evaluates every volatile
930 /// step its cone reaches, once, and the steps downstream of it
931 /// (runtime_model.md R1.v); steps upstream of a volatile step
932 /// keep their currency. A new round already leaves every
933 /// volatile step unrun. Most programs have no volatile step and
934 /// pay the emptiness check.
935 #[inline]
936 fn rearm_volatile(&mut self) {
937 if self.volatile_steps.is_empty() {
938 return;
939 }
940 for &i in self.volatile_steps.iter() {
941 self.ran[i] = 0;
942 }
943 self.all_ran = false;
944 }
945
946 #[inline]
947 fn dirty_input(&mut self, slot: usize) {
948 if let Some(deps) = self.dirty.get(slot) {
949 for &i in deps {
950 self.clean[i] = false;
951 }
952 }
953 }
954
955 /// Inlined into every evaluation, so only the check lives here:
956 /// a cell refresh changing a slot is rare, and its work is kept
957 /// out of line where it does not grow the hot path.
958 #[inline]
959 fn dirty_refreshed(&mut self) {
960 if self.externs.has_changed() {
961 self.dirty_refreshed_slots();
962 }
963 }
964
965 #[cold]
966 #[inline(never)]
967 fn dirty_refreshed_slots(&mut self) {
968 let changed = self.externs.take_changed();
969 for &slot in &changed {
970 // The cell's value is the slot's now: a slot that was
971 // unset (an extern with no default, bound to a parent's
972 // cell) holds a value, and one the cell cleared holds
973 // none, as a direct write would leave it.
974 if let Some(mask) = self.none.get_mut(slot) {
975 *mask = self.externs.slot_is_unset(slot);
976 }
977 if let Some(deps) = self.plan.input_dependents.get(slot) {
978 for &i in deps {
979 self.ran[i] = 0;
980 self.clean[i] = false;
981 }
982 self.all_ran = false;
983 }
984 }
985 self.externs.return_changed(changed);
986 let was = self.any_none;
987 self.any_none = self.externs.any_unset();
988 if was && !self.any_none {
989 self.none.fill(false);
990 }
991 }
992
993 #[inline]
994 fn eval_all(&mut self) {
995 let fresh = self.drive.stale;
996 if fresh {
997 self.begin_epoch();
998 } else {
999 self.refresh_cells();
1000 self.rearm_volatile();
1001 }
1002 if fresh && !self.use_clean && !self.any_none {
1003 self.run_guarded(|core| core.run_fresh());
1004 } else {
1005 let all = std::sync::Arc::clone(&self.all);
1006 self.run_steps(&all);
1007 }
1008 }
1009
1010 fn extern_written(&mut self, slot: usize, unset: bool) {
1011 self.none[slot] = unset;
1012 let was = self.any_none;
1013 self.any_none = self.externs.any_unset();
1014 if was && !self.any_none {
1015 self.none.fill(false);
1016 }
1017 self.dirty_input(slot);
1018 self.drive.stale = true;
1019 }
1020
1021 #[inline]
1022 fn guard_ref_slot(&self, slot: usize) {
1023 if self.ref_slots.get(slot).copied().unwrap_or(false) {
1024 panic!(
1025 "S2 pointer containment: slot {slot} is Ref2-colored; raw u64 readers \
1026 would leak an interior address. Use the typed borrow-checked accessor \
1027 (read_vec_*), the boundary decode, or copy out."
1028 );
1029 }
1030 }
1031
1032 fn invalidate_all(&mut self) {
1033 self.clean.fill(false);
1034 self.all_ran = false;
1035 self.drive.stale = true;
1036 }
1037
1038 /// A pull by index publishes as a pull by name does, so a child
1039 /// bound to this output reads what the parent computed. The flag
1040 /// is checked before the pull rather than after it: holding the
1041 /// value across the check cost the native rungs 6 percent.
1042 #[inline]
1043 fn pull_at(&mut self, index: usize) -> crate::ast::Value {
1044 if self.externs.broadcasts() {
1045 return self.pull_at_publishing(index);
1046 }
1047 self.pull_at_value(index)
1048 }
1049
1050 /// `pull_at` under a descendant: the pull, then the publish.
1051 #[cold]
1052 #[inline(never)]
1053 fn pull_at_publishing(&mut self, index: usize) -> crate::ast::Value {
1054 let value = self.pull_at_value(index);
1055 let slot = self.resolved_outputs[index]
1056 .as_ref()
1057 .expect("resolved by the pull")
1058 .0;
1059 self.publish_slot(slot, &value);
1060 value
1061 }
1062
1063 #[inline(always)]
1064 fn pull_at_value(&mut self, index: usize) -> crate::ast::Value {
1065 if self.resolved_outputs.len() <= index {
1066 self.resolved_outputs.resize(index + 1, None);
1067 }
1068 if self.resolved_outputs[index].is_none() {
1069 let name = self
1070 .externs
1071 .output_names()
1072 .get(index)
1073 .cloned()
1074 .unwrap_or_else(|| {
1075 panic!(
1076 "no output at index {index}; this kernel declares {}",
1077 self.externs.output_names().len()
1078 )
1079 });
1080 let slot = self.output_map[&name];
1081 let ty = self
1082 .output_types
1083 .get(&name)
1084 .copied()
1085 .unwrap_or(crate::ast::PortType::U64);
1086 let cone = self
1087 .plan
1088 .cones
1089 .get(&name)
1090 .map(|c| std::sync::Arc::<[usize]>::from(c.as_slice()));
1091 let can_fail = cone
1092 .as_ref()
1093 .is_some_and(|c| c.iter().any(|&i| self.step_can_fail(i)));
1094 self.resolved_outputs[index] = Some((slot, ty, cone, can_fail));
1095 }
1096 if self.drive.stale {
1097 self.begin_epoch();
1098 } else {
1099 self.refresh_cells();
1100 self.rearm_volatile();
1101 }
1102 let resolved = self.resolved_outputs[index]
1103 .as_ref()
1104 .expect("resolved above");
1105 let (slot, ty, can_fail) = (resolved.0, resolved.1, resolved.3);
1106 if let Some(order) = &resolved.2 {
1107 // Borrowed across the run rather than cloned: the order
1108 // lives behind an `Arc` this state holds, and running
1109 // steps never touches the resolved outputs.
1110 let order: *const [usize] = &**order;
1111 let order = unsafe { &*order };
1112 if can_fail {
1113 self.run_steps(order);
1114 } else {
1115 // No step of the cone can fail, so there is no
1116 // failure to capture and attribute.
1117 self.run_order(order);
1118 }
1119 }
1120 self.slot_value(slot, ty)
1121 }
1122
1123 /// Publish the value at `slot` through its broadcast cell, if a
1124 /// descendant asked for one.
1125 fn publish_slot(&self, slot: usize, value: &crate::ast::Value) {
1126 if let Some(cell) = self.externs.published_output(slot) {
1127 cell.publish(value.clone());
1128 }
1129 }
1130
1131 fn pull_named(&mut self, name: &str) -> crate::ast::Value {
1132 if self.drive.stale {
1133 self.begin_epoch();
1134 } else {
1135 self.refresh_cells();
1136 self.rearm_volatile();
1137 }
1138 let plan = std::sync::Arc::clone(&self.plan);
1139 if let Some(order) = plan.cones.get(name) {
1140 self.run_steps(order);
1141 }
1142 let value = self.value_of(name);
1143 self.broadcast(name, &value);
1144 value
1145 }
1146
1147 /// Publish a freshly computed output through its broadcast cell,
1148 /// if a descendant asked for one, so a child that bound its
1149 /// matching input slot to the same cell reads the new value
1150 /// (cross_fiber_invalidation.md §3.1, "broadcast outputs").
1151 ///
1152 /// A program nobody built a subscope under has no cells at all,
1153 /// and pays the emptiness check.
1154 #[inline]
1155 fn broadcast(&mut self, name: &str, value: &crate::ast::Value) {
1156 if !self.externs.broadcasts() {
1157 return;
1158 }
1159 if let Some(&slot) = self.output_map.get(name) {
1160 self.publish_slot(slot, value);
1161 }
1162 }
1163
1164 /// The broadcast cell for a named output, created on the first
1165 /// ask. The interpreter seeds one per output at construction;
1166 /// a compiled kernel makes them only when a descendant binds to
1167 /// one, so a program with no subscope under it allocates none.
1168 ///
1169 /// Keyed by the output's slot, which is what `output_map`
1170 /// answers and what the buffer is indexed by, so the vector is
1171 /// as long as the buffer rather than as long as the output list.
1172 fn output_cell_for(&self, name: &str) -> Option<crate::kernel::SharedCell> {
1173 let slot = *self.output_map.get(name)?;
1174 // An output no step has computed yet holds its type's zero
1175 // in the buffer; the cell starts at `None`, as the
1176 // interpreter's does, until the first pull publishes.
1177 let uncomputed = !self.all_ran
1178 && matches!(self.slot_step.get(slot), Some(Some(step)) if self.ran[*step] == 0);
1179 let initial = if uncomputed {
1180 crate::ast::Value::None
1181 } else {
1182 self.value_of(name)
1183 };
1184 Some(self.externs.output_cell(slot, initial))
1185 }
1186
1187 #[inline]
1188 fn refresh_cells(&mut self) {
1189 if self.externs.cells_dirty() {
1190 self.externs.refresh_cells(&mut self.buffer);
1191 self.dirty_refreshed();
1192 }
1193 }
1194
1195 fn republish_refs(&mut self) {
1196 for &(slot, idx) in &self.ref_scratch {
1197 let (p, l) = self.scratch[idx].ptr_len();
1198 self.buffer[slot] = p;
1199 self.buffer[slot + 1] = l;
1200 }
1201 self.externs.seed(&mut self.buffer, None);
1202 }
1203
1204 #[inline]
1205 fn run_steps(&mut self, order: &[usize]) {
1206 self.run_guarded(|core| core.run_order(order));
1207 }
1208
1209 /// `run_steps` for the build-time constant fold: the same steps
1210 /// under the same guard, but a failure comes back as the
1211 /// message [`Attribution::reraise`] would have raised. A step
1212 /// that fails here fails before any kernel exists, so it is an
1213 /// error the builder returns rather than a panic out of a
1214 /// constructor.
1215 fn fold_steps(&mut self, order: &[usize]) -> Result<(), crate::KernelError> {
1216 let capture = crate::kernel::engines::EvalPanicCaptureGuard::arm();
1217 let outcome =
1218 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.run_order(order)));
1219 drop(capture);
1220 if let Err(payload) = outcome {
1221 let sites = std::sync::Arc::clone(&self.sites);
1222 let node = self.failing_node();
1223 return Err(crate::KernelError::ConstantFold {
1224 reason: sites.describe(payload, node, &self.buffer, Some(&self.none)),
1225 });
1226 }
1227 Ok(())
1228 }
1229
1230 fn set_extern(
1231 &mut self,
1232 name: &str,
1233 value: crate::ast::Value,
1234 ) -> Result<usize, crate::kernel::WriteError> {
1235 let (slot, unset) = self.externs.set(name, value, &mut self.buffer)?;
1236 self.extern_written(slot, unset);
1237 Ok(slot)
1238 }
1239
1240 fn set_extern_at(
1241 &mut self,
1242 index: usize,
1243 value: crate::ast::Value,
1244 ) -> Result<usize, crate::kernel::WriteError> {
1245 let (slot, unset) = self.externs.set_at(index, value, &mut self.buffer)?;
1246 self.extern_written(slot, unset);
1247 Ok(slot)
1248 }
1249
1250 fn slot_value(&self, slot: usize, ty: crate::ast::PortType) -> crate::ast::Value {
1251 if self.none.get(slot).copied().unwrap_or(false) {
1252 return crate::ast::Value::None;
1253 }
1254 crate::compile::marshal::decode_output(&self.buffer, slot, ty)
1255 }
1256
1257 fn value_of(&self, name: &str) -> crate::ast::Value {
1258 let slot = self.output_map[name];
1259 let ty = self
1260 .output_types
1261 .get(name)
1262 .copied()
1263 .unwrap_or(crate::ast::PortType::U64);
1264 self.slot_value(slot, ty)
1265 }
1266 };
1267}
1268pub(crate) use shared_core_methods;
1269
1270/// The dirty-register plan of a compiled kernel: which steps each input
1271/// slot invalidates when it changes, and which steps each named output
1272/// needs. The evaluation loops consume only this; provenance derives it
1273/// today, and a host that knows its write and read patterns may supply
1274/// a narrower plan later without touching the loops
1275/// (docs/design/engines.md §3.1).
1276pub(crate) struct Invalidation {
1277 /// Per input slot (coordinates and externs alike), the steps that
1278 /// depend on it, transitively.
1279 pub(crate) input_dependents: Vec<Vec<usize>>,
1280 /// Per named output, the steps of its cone in evaluation order.
1281 pub(crate) cones: std::collections::HashMap<String, Vec<usize>>,
1282}
1283
1284impl Invalidation {
1285 /// The plan provenance gives: every step downstream of an input is
1286 /// invalidated by it, and every step upstream of an output is in
1287 /// its cone. `inputs` and `outputs` are each step's slots;
1288 /// `output_slots` names the outputs.
1289 pub(crate) fn from_provenance(
1290 input_dependents: Vec<Vec<usize>>,
1291 step_inputs: &[&[usize]],
1292 step_outputs: &[&[usize]],
1293 output_slots: &std::collections::HashMap<String, usize>,
1294 total_slots: usize,
1295 ) -> Self {
1296 let step_count = step_inputs.len();
1297 let mut slot_step: Vec<Option<usize>> = vec![None; total_slots];
1298 for (i, outs) in step_outputs.iter().enumerate() {
1299 for &s in outs.iter() {
1300 slot_step[s] = Some(i);
1301 }
1302 }
1303 let cones = output_slots
1304 .iter()
1305 .map(|(name, &slot)| {
1306 let mut wanted = vec![false; step_count];
1307 let mut stack: Vec<usize> = slot_step[slot].into_iter().collect();
1308 while let Some(i) = stack.pop() {
1309 if wanted[i] {
1310 continue;
1311 }
1312 wanted[i] = true;
1313 stack.extend(step_inputs[i].iter().filter_map(|&s| slot_step[s]));
1314 }
1315 (
1316 name.clone(),
1317 (0..step_count).filter(|&i| wanted[i]).collect(),
1318 )
1319 })
1320 .collect();
1321 Self {
1322 input_dependents,
1323 cones,
1324 }
1325 }
1326}
1327
1328/// Where each compiled step came from, for the failure path only
1329/// (engines.md §3.4). A step's panic is caught at the step
1330/// boundary and re-raised enriched exactly as the interpreter enriches
1331/// a node's: the node's name, the outputs it feeds, the program's
1332/// diagnostic context, and its input values decoded from the buffer
1333/// where the slot types allow. `sites` is indexed by program node:
1334/// the closure and pure-native kernels have one step per node, and
1335/// the hybrid kernel names the failing member of a segment through
1336/// its tracker slot.
1337#[derive(Default)]
1338pub(crate) struct Attribution {
1339 pub(crate) sites: Vec<NodeSite>,
1340 /// The program's diagnostic context (`PolydatProgram::context`).
1341 pub(crate) context: String,
1342}
1343
1344/// One node's identity for the failure path.
1345pub(crate) struct NodeSite {
1346 pub(crate) name: String,
1347 /// The declared outputs the node feeds, sorted.
1348 pub(crate) outputs: Vec<String>,
1349 /// `(first slot, port type)` of every input port, in port order.
1350 pub(crate) inputs: Vec<(usize, crate::ast::PortType)>,
1351}
1352
1353impl Attribution {
1354 /// The inputs of `step` as diagnostic text, from the buffer, each
1355 /// copied out and printed as the interpreter prints the same value:
1356 /// `None` where the mask says so, and the port type alone where the
1357 /// slot cannot be decoded, so the report itself never fails.
1358 fn inputs_of(&self, step: usize, buffer: &[u64], none: Option<&[bool]>) -> Vec<String> {
1359 let Some(site) = self.sites.get(step) else {
1360 return Vec::new();
1361 };
1362 let _quiet = crate::kernel::engines::EvalPanicCaptureGuard::arm();
1363 site.inputs
1364 .iter()
1365 .map(|&(slot, ty)| {
1366 if none.is_some_and(|m| m.get(slot).copied().unwrap_or(false)) {
1367 return "None".to_string();
1368 }
1369 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1370 crate::kernel::engines::format_value_for_diag(&marshal::decode_output(
1371 buffer, slot, ty,
1372 ))
1373 }))
1374 .unwrap_or_else(|_| format!("{ty:?}"))
1375 })
1376 .collect()
1377 }
1378
1379 /// Re-raise a step's panic enriched as the interpreter enriches a
1380 /// node's (`kernel::engines::enrich_panic`). `step` beyond the
1381 /// sites (native code that failed before naming a step) reports an
1382 /// unknown node, as the interpreter does for an index it lacks.
1383 pub(crate) fn reraise(
1384 &self,
1385 payload: Box<dyn std::any::Any + Send>,
1386 step: usize,
1387 buffer: &[u64],
1388 none: Option<&[bool]>,
1389 ) -> ! {
1390 crate::kernel::engines::reraise_enriched(self.describe(payload, step, buffer, none))
1391 }
1392
1393 /// The same message [`Self::reraise`] raises, returned instead. The
1394 /// build-time constant fold uses it: a step that fails there fails
1395 /// before any kernel exists, so it is an error the builder returns
1396 /// and not a panic out of a constructor.
1397 pub(crate) fn describe(
1398 &self,
1399 payload: Box<dyn std::any::Any + Send>,
1400 step: usize,
1401 buffer: &[u64],
1402 none: Option<&[bool]>,
1403 ) -> String {
1404 let site = self.sites.get(step);
1405 let name = site
1406 .map(|s| s.name.clone())
1407 .unwrap_or_else(|| format!("<unknown node #{step}>"));
1408 let outputs: Vec<&str> = site
1409 .map(|s| s.outputs.iter().map(String::as_str).collect())
1410 .unwrap_or_default();
1411 let inputs = self.inputs_of(step, buffer, none);
1412 crate::kernel::engines::enrich_panic(payload, &name, &outputs, &self.context, &inputs)
1413 }
1414}