Skip to main content

polydat_core/kernel/
mod.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Polydat runtime kernel: compiled DAG with pull-through evaluation.
5//!
6//! ## Architecture
7//!
8//! ```text
9//! PolydatProgram (Arc, immutable, shared across all fibers)
10//! ┌──────────────────────────────────────────────────────────────┐
11//! │  nodes[]           — Box<dyn PolydatNode> in topological order│
12//! │  wiring[]          — per-node input source tables            │
13//! │  input_defs[]      — coordinates first, then externs          │
14//! │  output_map/list   — name → (node_idx, port_idx), in order   │
15//! │  input_dependents  — per input, the nodes downstream of it   │
16//! │  traversals[]      — the `for` bodies, one program each      │
17//! │  ledger            — the tree's CompileLedger                │
18//! └──────────────────────────────────────────────────────────────┘
19//!
20//! PolydatState (per-fiber, mutable, private — never shared)
21//! ┌──────────────────────────────────────────────────────────────┐
22//! │  EngineCore:                                                 │
23//! │    buffers[][]       — per-node output value slots:          │
24//! │      ┌───────────┐                                           │
25//! │      │ node 0    │ [Value, Value, ...]  (one per output port)│
26//! │      │ node 1    │ [Value]                                   │
27//! │      └───────────┘                                           │
28//! │    node_clean[]      — whether a node's buffers are current  │
29//! │    inputs[]          — current input values, coords + externs│
30//! │    input_defaults[]  — what reset restores                   │
31//! │    shared_cells[]    — cell-bound input slots                 │
32//! │    output_cells[]    — cell-bound `shared` outputs            │
33//! │    input_scratch[]   — temp buffer for node input gathering  │
34//! │    node_scratch[]    — per-node memo space                   │
35//! └──────────────────────────────────────────────────────────────┘
36//!
37//! Evaluation:
38//!   1. kernel.set_inputs(&[cycle])  → writes the coordinates and
39//!                                     marks their dependents unclean
40//!   2. kernel.pull("name")          → walks the output's cone, skips
41//!                                     clean nodes, returns the buffer
42//!
43//! Workload params:
44//!   Numeric and string workload params are injected into the Polydat
45//!   source as constant bindings before compilation. They resolve
46//!   as normal Polydat outputs — no separate globals mechanism needed.
47//! ```
48
49pub mod activation;
50mod api;
51mod api_impl;
52pub(crate) mod engines;
53pub mod intern;
54pub mod interp;
55mod manifest;
56mod opt;
57mod program;
58mod scope;
59mod state;
60pub mod subcontext;
61pub use activation::{Activation, CursorSlice, TraversalStream};
62
63pub(crate) use api::SharedKernel;
64pub(crate) use api::internals::KernelInternals;
65pub use api::{Construction, Dataflow, Kernel, KernelProgram, Metadata, WireKey, WriteError};
66pub use engines::*;
67pub use intern::{StaticInterner, static_pair};
68pub use manifest::{ManifestEntry, extract_manifest};
69pub use opt::KernelOptLevel;
70pub use program::*;
71pub use scope::{ScopeCoord, format_scope_coordinate_path};
72pub use state::*;
73
74use crate::ast::Value;
75
76/// Source of a value for a node input port.
77#[derive(Debug, Clone)]
78pub enum WireSource {
79    /// A named input, by index into the unified input array.
80    /// Includes both coordinate inputs and capture inputs.
81    Input(usize),
82    /// Output of another node: `(node_index, output_port_index)`.
83    NodeOutput(usize, usize),
84}
85
86/// Classification of a named input by its evaluation lifecycle.
87///
88/// See `crates/polydat/docs/design/evaluation_model.md` for the lifecycle classification.
89/// The init-binding contract uses this to decide whether a wire
90/// to an `Input(idx)` is effectively-const at scope-init time:
91/// `IterationExtern` slots count as effectively-const (rebound
92/// once per scope activation); `Coordinate` and `ExternalWrite`
93/// slots are dynamic and disqualify any init binding that reaches
94/// them.
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum InputKind {
97    /// Dimensional input declared by `input (cycle: u64, ...: u64)` —
98    /// dynamic, written at every coordinate advance.
99    Coordinate,
100    /// External slot populated by `materialize_wiring_from_outer` from an
101    /// enclosing `for_each` / `for_combinations` clause —
102    /// effectively-const for the duration of one scope activation.
103    IterationExtern,
104    /// External port declared by `extern name: type = default` —
105    /// written by capture extraction during op execution; dynamic
106    /// across cycle boundaries within a stanza.
107    ExternalWrite,
108}
109
110/// Definition of a named input to the Polydat graph.
111///
112/// All inputs — coordinates, iteration externs, and external-write ports
113/// — are defined uniformly. Coordinates default to `Value::U64(0)`,
114/// captures default to `Value::None` (unset until a capture writes
115/// to them) or to their declared default. The `kind` field carries
116/// the lifecycle classification used by the init-binding contract
117/// (see SRD 11 §"Init Binding Contract").
118#[derive(Debug, Clone)]
119pub struct InputDef {
120    /// Input name (e.g., "cycle", "username").
121    pub name: String,
122    /// Default value. Coordinates default to U64(0), captures
123    /// to their declared default (or None if unset).
124    pub default: Value,
125    /// The declared port type for this input. Used by the assembler
126    /// for type checking when wiring nodes to this input.
127    pub port_type: crate::ast::PortType,
128    /// Lifecycle classification. The assembler's coordinate inputs
129    /// are `Coordinate`; the DSL compiler sets `IterationExtern` for
130    /// iteration externs and `ExternalWrite` for `extern` ports.
131    pub kind: InputKind,
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use std::collections::HashMap;
138    use std::sync::Arc;
139
140    #[test]
141    fn capture_inputs_persist_across_set_inputs() {
142        // Program with 1 coordinate (cycle) + 2 capture inputs
143        let program = Arc::new(PolydatProgram::with_inputs(
144            vec![],
145            vec![],
146            vec![
147                InputDef {
148                    name: "cycle".into(),
149                    default: Value::U64(0),
150                    port_type: crate::ast::PortType::U64,
151                    kind: InputKind::Coordinate,
152                },
153                InputDef {
154                    name: "balance".into(),
155                    default: Value::F64(0.0),
156                    port_type: crate::ast::PortType::F64,
157                    kind: InputKind::ExternalWrite,
158                },
159                InputDef {
160                    name: "auth_token".into(),
161                    default: Value::Str("anonymous".into()),
162                    port_type: crate::ast::PortType::Str,
163                    kind: InputKind::ExternalWrite,
164                },
165            ],
166            1, // coord_count
167            HashMap::new(),
168            Vec::new(),
169            "",
170            "(test)",
171            crate::kernel::CompileLedger::new(),
172        ));
173        let mut state = program.create_state();
174
175        // Default values for capture inputs
176        assert_eq!(state.get_input(1), Value::F64(0.0));
177        assert_eq!(state.get_input(2), Value::Str("anonymous".into()));
178
179        // Set capture inputs individually
180        state.set_input(1, Value::F64(1234.56));
181        state.set_input(2, Value::Str("token_abc".into()));
182        assert_eq!(state.get_input(1), Value::F64(1234.56));
183        assert_eq!(state.get_input(2), Value::Str("token_abc".into()));
184
185        // Capture inputs persist when coordinates change
186        state.set_inputs(&[42]);
187        assert_eq!(state.get_input(1), Value::F64(1234.56));
188        assert_eq!(state.get_input(2), Value::Str("token_abc".into()));
189    }
190
191    #[test]
192    fn reset_inputs_restores_capture_defaults() {
193        let program = Arc::new(PolydatProgram::with_inputs(
194            vec![],
195            vec![],
196            vec![
197                InputDef {
198                    name: "cycle".into(),
199                    default: Value::U64(0),
200                    port_type: crate::ast::PortType::U64,
201                    kind: InputKind::Coordinate,
202                },
203                InputDef {
204                    name: "token".into(),
205                    default: Value::Str("anon".into()),
206                    port_type: crate::ast::PortType::Str,
207                    kind: InputKind::ExternalWrite,
208                },
209            ],
210            1,
211            HashMap::new(),
212            Vec::new(),
213            "",
214            "(test)",
215            crate::kernel::CompileLedger::new(),
216        ));
217        let mut state = program.create_state();
218
219        state.set_input(1, Value::Str("alice".into()));
220        assert_eq!(state.get_input(1), Value::Str("alice".into()));
221
222        // Reset only capture inputs (from coord_count onward)
223        state.reset_inputs_from(1);
224        assert_eq!(state.get_input(1), Value::Str("anon".into()));
225    }
226
227    #[test]
228    fn invalidate_all_keeps_inputs_and_reset_restores_defaults() {
229        let program = Arc::new(PolydatProgram::with_inputs(
230            vec![],
231            vec![],
232            vec![
233                InputDef {
234                    name: "cycle".into(),
235                    default: Value::U64(0),
236                    port_type: crate::ast::PortType::U64,
237                    kind: InputKind::Coordinate,
238                },
239                InputDef {
240                    name: "token".into(),
241                    default: Value::Str("anon".into()),
242                    port_type: crate::ast::PortType::Str,
243                    kind: InputKind::ExternalWrite,
244                },
245            ],
246            1,
247            HashMap::new(),
248            Vec::new(),
249            "",
250            "(test)",
251            crate::kernel::CompileLedger::new(),
252        ));
253        let mut state = program.create_state();
254
255        state.set_inputs(&[42]);
256        state.set_input(1, Value::Str("alice".into()));
257
258        state.invalidate_all();
259        assert_eq!(state.get_input(0), Value::U64(42));
260        assert_eq!(state.get_input(1), Value::Str("alice".into()));
261        state.reset_inputs_from(0);
262        assert_eq!(state.get_input(0), Value::U64(0));
263        assert_eq!(state.get_input(1), Value::Str("anon".into()));
264    }
265
266    // ---------------------------------------------------------------
267    // WireCost tests: config wire warnings for various DAG shapes
268    // ---------------------------------------------------------------
269
270    /// A test node with one Config wire input and one Data wire input.
271    /// Simulates a node with an expensive LUT that's configured by
272    /// the first input and driven by the second.
273    struct ConfigWireTestNode {
274        meta: crate::ast::NodeMeta,
275    }
276
277    impl ConfigWireTestNode {
278        fn new() -> Self {
279            use crate::ast::{Port, Slot};
280            Self {
281                meta: crate::ast::NodeMeta {
282                    name: "config_test".into(),
283                    outs: vec![Port::u64("output")],
284                    ins: vec![
285                        Slot::Wire(Port::u64("config_param").config()),
286                        Slot::Wire(Port::u64("data_input")),
287                    ],
288                },
289            }
290        }
291    }
292
293    impl crate::ast::PolydatNode for ConfigWireTestNode {
294        fn meta(&self) -> &crate::ast::NodeMeta {
295            &self.meta
296        }
297        fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
298            let config = inputs[0].as_u64();
299            let data = inputs[1].as_u64();
300            outputs[0] = Value::U64(config.wrapping_add(data));
301        }
302    }
303
304    #[test]
305    fn wire_cost_no_warning_when_config_is_init_time() {
306        // DAG: constant(42) → config_test.config_param
307        //      cycle → hash → config_test.data_input
308        // Config wire fed by init-time constant → no warning
309        use crate::compile::assembly::{PolydatAssembler, WireRef};
310        use crate::dsl::events::CompileEventLog;
311        use crate::library::identity::ConstU64;
312        use crate::library::identity::Identity;
313
314        let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
315        asm.add_node("config_val", Box::new(ConstU64::new(42)), vec![]);
316        asm.add_node(
317            "hashed",
318            Box::new(Identity::new(crate::ast::PortType::U64)),
319            vec![WireRef::input("cycle")],
320        );
321        asm.add_node(
322            "test_node",
323            Box::new(ConfigWireTestNode::new()),
324            vec![WireRef::node("config_val"), WireRef::node("hashed")],
325        );
326        asm.add_output("result", WireRef::node("test_node"));
327
328        let mut log = CompileEventLog::new();
329        let k = asm.compile_with_log(Some(&mut log)).unwrap();
330        let _program = k.into_program();
331
332        // Check: no ConfigWireCycleWarning in events
333        let warnings: Vec<_> = log
334            .events()
335            .iter()
336            .filter(|e| {
337                matches!(
338                    e,
339                    crate::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
340                )
341            })
342            .collect();
343        assert!(
344            warnings.is_empty(),
345            "no warning expected when config wire is init-time: {warnings:?}"
346        );
347    }
348
349    #[test]
350    fn wire_cost_warning_when_config_is_cycle_time() {
351        // DAG: cycle → hash → config_test.config_param  (BAD: config from cycle)
352        //      cycle → config_test.data_input
353        // Config wire fed by cycle-time node → should warn
354        use crate::compile::assembly::{PolydatAssembler, WireRef};
355        use crate::dsl::events::CompileEventLog;
356        use crate::library::identity::Identity;
357
358        let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
359        asm.add_node(
360            "hashed",
361            Box::new(Identity::new(crate::ast::PortType::U64)),
362            vec![WireRef::input("cycle")],
363        );
364        asm.add_node(
365            "test_node",
366            Box::new(ConfigWireTestNode::new()),
367            vec![
368                WireRef::node("hashed"), // config_param ← cycle-time!
369                WireRef::input("cycle"), // data_input ← cycle
370            ],
371        );
372        asm.add_output("result", WireRef::node("test_node"));
373
374        let mut log = CompileEventLog::new();
375        let _k = asm.compile_with_log(Some(&mut log)).unwrap();
376
377        let warnings: Vec<_> = log
378            .events()
379            .iter()
380            .filter(|e| {
381                matches!(
382                    e,
383                    crate::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
384                )
385            })
386            .collect();
387        assert_eq!(
388            warnings.len(),
389            1,
390            "expected exactly one config wire warning: {warnings:?}"
391        );
392    }
393
394    #[test]
395    fn wire_cost_warning_when_config_is_coordinate_direct() {
396        // DAG: cycle → config_test.config_param  (BAD: coordinate direct to config)
397        //      cycle → config_test.data_input
398        use crate::compile::assembly::{PolydatAssembler, WireRef};
399        use crate::dsl::events::CompileEventLog;
400
401        let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
402        asm.add_node(
403            "test_node",
404            Box::new(ConfigWireTestNode::new()),
405            vec![
406                WireRef::input("cycle"), // config_param ← coordinate!
407                WireRef::input("cycle"), // data_input ← cycle
408            ],
409        );
410        asm.add_output("result", WireRef::node("test_node"));
411
412        let mut log = CompileEventLog::new();
413        let _k = asm.compile_with_log(Some(&mut log)).unwrap();
414
415        let warnings: Vec<_> = log
416            .events()
417            .iter()
418            .filter(|e| {
419                matches!(
420                    e,
421                    crate::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
422                )
423            })
424            .collect();
425        assert_eq!(warnings.len(), 1, "config wire from coordinate should warn");
426    }
427
428    #[test]
429    fn wire_cost_no_warning_data_wire_from_cycle() {
430        // DAG: constant(10) → config_test.config_param (init-time, ok)
431        //      cycle → config_test.data_input           (cycle-time, ok for Data wire)
432        // Only the data wire is cycle-time → no warning
433        use crate::compile::assembly::{PolydatAssembler, WireRef};
434        use crate::dsl::events::CompileEventLog;
435        use crate::library::identity::ConstU64;
436
437        let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
438        asm.add_node("config_val", Box::new(ConstU64::new(10)), vec![]);
439        asm.add_node(
440            "test_node",
441            Box::new(ConfigWireTestNode::new()),
442            vec![
443                WireRef::node("config_val"), // config_param ← constant
444                WireRef::input("cycle"),     // data_input ← cycle (Data wire, ok)
445            ],
446        );
447        asm.add_output("result", WireRef::node("test_node"));
448
449        let mut log = CompileEventLog::new();
450        let _k = asm.compile_with_log(Some(&mut log)).unwrap();
451
452        let warnings: Vec<_> = log
453            .events()
454            .iter()
455            .filter(|e| {
456                matches!(
457                    e,
458                    crate::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
459                )
460            })
461            .collect();
462        assert!(warnings.is_empty(), "data wire from cycle should not warn");
463    }
464
465    #[test]
466    fn wire_cost_diamond_config_from_init() {
467        // Diamond DAG using two ConfigWireTestNodes:
468        //   constant(5) → inner.config_param ─┐
469        //   constant(3) → inner.data_input    ─┤→ inner.output → outer.config_param
470        //   cycle → hash → outer.data_input
471        // inner is fully init-time → its output feeds outer's config wire → no warning
472        use crate::compile::assembly::{PolydatAssembler, WireRef};
473        use crate::dsl::events::CompileEventLog;
474        use crate::library::identity::ConstU64;
475        use crate::library::identity::Identity;
476
477        let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
478        asm.add_node("a", Box::new(ConstU64::new(5)), vec![]);
479        asm.add_node("b", Box::new(ConstU64::new(3)), vec![]);
480        asm.add_node(
481            "inner",
482            Box::new(ConfigWireTestNode::new()),
483            vec![WireRef::node("a"), WireRef::node("b")],
484        );
485        asm.add_node(
486            "hashed",
487            Box::new(Identity::new(crate::ast::PortType::U64)),
488            vec![WireRef::input("cycle")],
489        );
490        asm.add_node(
491            "outer",
492            Box::new(ConfigWireTestNode::new()),
493            vec![
494                WireRef::node("inner"),  // config_param ← init-time (5+3)
495                WireRef::node("hashed"), // data_input ← cycle-time
496            ],
497        );
498        asm.add_output("result", WireRef::node("outer"));
499
500        let mut log = CompileEventLog::new();
501        let _k = asm.compile_with_log(Some(&mut log)).unwrap();
502
503        // inner's config wire from constant is fine. outer's config wire
504        // from init-time inner output is also fine.
505        let warnings: Vec<_> = log
506            .events()
507            .iter()
508            .filter(|e| {
509                matches!(
510                    e,
511                    crate::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
512                )
513            })
514            .collect();
515        assert!(
516            warnings.is_empty(),
517            "init-time derived config should not warn: {warnings:?}"
518        );
519    }
520
521    #[test]
522    fn wire_cost_diamond_config_from_mixed() {
523        // Mixed init/cycle feeding config:
524        //   constant(5) → mixer.config_param ─┐
525        //   cycle → mixer.data_input          ─┤→ mixer.output → outer.config_param
526        //   cycle → outer.data_input
527        // mixer depends on cycle → its output is cycle-time → outer's config wire warns
528        use crate::compile::assembly::{PolydatAssembler, WireRef};
529        use crate::dsl::events::CompileEventLog;
530        use crate::library::identity::ConstU64;
531
532        let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
533        asm.add_node("five", Box::new(ConstU64::new(5)), vec![]);
534        asm.add_node(
535            "mixer",
536            Box::new(ConfigWireTestNode::new()),
537            vec![
538                WireRef::node("five"),   // config_param ← init
539                WireRef::input("cycle"), // data_input ← cycle
540            ],
541        );
542        asm.add_node(
543            "outer",
544            Box::new(ConfigWireTestNode::new()),
545            vec![
546                WireRef::node("mixer"),  // config_param ← cycle-tainted!
547                WireRef::input("cycle"), // data_input
548            ],
549        );
550        asm.add_output("result", WireRef::node("outer"));
551
552        let mut log = CompileEventLog::new();
553        let _k = asm.compile_with_log(Some(&mut log)).unwrap();
554
555        let warnings: Vec<_> = log
556            .events()
557            .iter()
558            .filter(|e| {
559                matches!(
560                    e,
561                    crate::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
562                )
563            })
564            .collect();
565        // outer's config from cycle-tainted mixer should warn.
566        // mixer's config from constant should NOT warn.
567        assert_eq!(
568            warnings.len(),
569            1,
570            "exactly one warning for outer's config: {warnings:?}"
571        );
572    }
573}