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