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}