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[] — broadcast cells of computed 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;
52mod bind;
53pub(crate) mod engines;
54pub mod intern;
55pub mod interp;
56mod manifest;
57mod opt;
58mod program;
59mod scope;
60mod state;
61pub mod subcontext;
62pub use activation::{Activation, CursorSlice, TraversalStream};
63pub use bind::{bind_under, propagate_inputs};
64
65pub(crate) use api::SharedKernel;
66pub(crate) use api::internals::KernelInternals;
67pub use api::{
68 Construction, Dataflow, Kernel, KernelProgram, Metadata, ProgramId, WireKey, WriteError,
69};
70pub use engines::*;
71pub use intern::{StaticInterner, static_pair};
72pub use manifest::{ManifestEntry, extract_manifest};
73pub use opt::KernelOptLevel;
74pub use program::*;
75pub use scope::{ScopeCoord, format_scope_coordinate_path};
76pub use state::*;
77
78use crate::ast::Value;
79
80/// Source of a value for a node input port.
81#[derive(Debug, Clone)]
82pub enum WireSource {
83 /// A named input, by index into the unified input array.
84 /// Includes both coordinate inputs and capture inputs.
85 Input(usize),
86 /// Output of another node: `(node_index, output_port_index)`.
87 NodeOutput(usize, usize),
88}
89
90/// Classification of a named input by its evaluation lifecycle.
91///
92/// See `crates/polydat/docs/design/evaluation_model.md` for the lifecycle classification.
93/// The init-binding contract uses this to decide whether a wire
94/// to an `Input(idx)` is effectively-const at scope-init time:
95/// `IterationExtern` slots count as effectively-const (rebound
96/// once per scope activation); `Coordinate` and `ExternalWrite`
97/// slots are dynamic and disqualify any init binding that reaches
98/// them.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum InputKind {
101 /// Dimensional input declared by `input (cycle: u64, ...: u64)` —
102 /// dynamic, written at every coordinate advance.
103 Coordinate,
104 /// External slot populated by `materialize_wiring_from_outer` from an
105 /// enclosing `for_each` / `for_combinations` clause —
106 /// effectively-const for the duration of one scope activation.
107 IterationExtern,
108 /// External port declared by `extern name: type = default` —
109 /// written by capture extraction during op execution; dynamic
110 /// across cycle boundaries within a stanza.
111 ExternalWrite,
112 /// The captured value of a `const` binding (`__const_<name>`),
113 /// written only when the kernel is initialized ([`ConstInit`]). A
114 /// host write to it is refused (`WriteError::ConstSlot`).
115 Const,
116}
117
118/// How one `const` binding is initialized.
119///
120/// When a kernel is initialized, `source` (the const's own expression,
121/// compiled as the output `__init_<name>`) is evaluated once and its
122/// value written into `slot` (`__const_<name>`), which everything that
123/// reads the const reads. A `None` value falls back to the `fallback`
124/// input, the value a binder copied from the enclosing scope, so a const
125/// that yields nothing leaves the outer binding visible. Nothing
126/// re-evaluates the const afterwards; `Kernel::init` does it again.
127///
128/// The assembler writes these records when it resolves a graph, for
129/// every const whose value is not known at build, and resolves each
130/// name to its index then, so initialization looks nothing up.
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct ConstInit {
133 /// The const's name, and the output that reads it.
134 pub name: String,
135 /// The input slot holding the captured value.
136 pub slot: String,
137 /// The output computing the const's expression.
138 pub source: String,
139 /// The input holding the enclosing scope's value, when the const's
140 /// expression references names.
141 pub fallback: Option<String>,
142 /// Whether this is the computed starting value of a `shared`
143 /// register rather than a const: `slot` is the register's own input,
144 /// and initialization writes it only while nothing has written the
145 /// register, so a scope attached to a register another scope
146 /// declared never seeds it again.
147 pub register: bool,
148 /// `slot`'s index among the kernel's inputs.
149 pub(crate) slot_index: usize,
150 /// `source`'s index among the kernel's outputs.
151 pub(crate) source_index: usize,
152 /// `fallback`'s index among the kernel's inputs.
153 pub(crate) fallback_index: Option<usize>,
154}
155
156/// The text of a caught panic: its `String` or `&str` payload.
157pub(crate) fn panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
158 if let Some(s) = payload.downcast_ref::<String>() {
159 s.clone()
160 } else if let Some(s) = payload.downcast_ref::<&str>() {
161 s.to_string()
162 } else {
163 "<non-string panic payload>".to_string()
164 }
165}
166
167/// Definition of a named input to the Polydat graph.
168///
169/// All inputs — coordinates, iteration externs, and external-write ports
170/// — are defined uniformly. Coordinates default to `Value::U64(0)`,
171/// captures default to `Value::None` (unset until a capture writes
172/// to them) or to their declared default. The `kind` field carries
173/// the lifecycle classification used by the const-binding contract
174/// (evaluation_model.md, "Const Binding Contract").
175#[derive(Debug, Clone)]
176pub struct InputDef {
177 /// Input name (e.g., "cycle", "username").
178 pub name: String,
179 /// Default value. Coordinates default to U64(0), captures
180 /// to their declared default (or None if unset).
181 pub default: Value,
182 /// The declared port type for this input. Used by the assembler
183 /// for type checking when wiring nodes to this input.
184 pub port_type: crate::ast::PortType,
185 /// Lifecycle classification. The assembler's coordinate inputs
186 /// are `Coordinate`; the DSL compiler sets `IterationExtern` for
187 /// iteration externs and `ExternalWrite` for `extern` ports.
188 pub kind: InputKind,
189 /// Whether the author wrote this input's type or the compiler
190 /// inferred it (input_variance.md §3). An inferred input is *open*:
191 /// the host may write a value of another type, and whether the
192 /// compiler converts it is `CompileOptions::input_variance`'s call.
193 pub type_origin: TypeOrigin,
194 /// For a converted input: the type its consumers read, which is
195 /// what the input reports as its type. The slot itself is `Dyn`
196 /// and holds the value as written; `None` for every other input.
197 pub converts_to: Option<crate::ast::PortType>,
198}
199
200/// How an input's type was established (input_variance.md §3).
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum TypeOrigin {
203 /// The author wrote it: `input x: T`, `extern x: T`, `shared x`.
204 /// The input is invariant, and a write of another type is refused.
205 Declared,
206 /// The compiler inferred it: an `input` with no type, or an
207 /// auto-extern typed from the binding that reads it.
208 Inferred,
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use std::collections::HashMap;
215 use std::sync::Arc;
216
217 #[test]
218 fn capture_inputs_persist_across_set_inputs() {
219 // Program with 1 coordinate (cycle) + 2 capture inputs
220 let program = Arc::new(PolydatProgram::with_inputs(
221 vec![],
222 vec![],
223 vec![
224 InputDef {
225 name: "cycle".into(),
226 default: Value::U64(0),
227 port_type: crate::ast::PortType::U64,
228 kind: InputKind::Coordinate,
229 type_origin: crate::kernel::TypeOrigin::Declared,
230 converts_to: None,
231 },
232 InputDef {
233 name: "balance".into(),
234 default: Value::F64(0.0),
235 port_type: crate::ast::PortType::F64,
236 kind: InputKind::ExternalWrite,
237 type_origin: crate::kernel::TypeOrigin::Declared,
238 converts_to: None,
239 },
240 InputDef {
241 name: "auth_token".into(),
242 default: Value::Str("anonymous".into()),
243 port_type: crate::ast::PortType::Str,
244 kind: InputKind::ExternalWrite,
245 type_origin: crate::kernel::TypeOrigin::Declared,
246 converts_to: None,
247 },
248 ],
249 1, // coord_count
250 HashMap::new(),
251 Vec::new(),
252 "",
253 "(test)",
254 crate::kernel::CompileLedger::new(),
255 ));
256 let mut state = program.create_state();
257
258 // Default values for capture inputs
259 assert_eq!(state.get_input(1), Value::F64(0.0));
260 assert_eq!(state.get_input(2), Value::Str("anonymous".into()));
261
262 // Set capture inputs individually
263 state.set_input(1, Value::F64(1234.56));
264 state.set_input(2, Value::Str("token_abc".into()));
265 assert_eq!(state.get_input(1), Value::F64(1234.56));
266 assert_eq!(state.get_input(2), Value::Str("token_abc".into()));
267
268 // Capture inputs persist when coordinates change
269 state.set_inputs(&[42]);
270 assert_eq!(state.get_input(1), Value::F64(1234.56));
271 assert_eq!(state.get_input(2), Value::Str("token_abc".into()));
272 }
273
274 #[test]
275 fn reset_inputs_restores_capture_defaults() {
276 let program = Arc::new(PolydatProgram::with_inputs(
277 vec![],
278 vec![],
279 vec![
280 InputDef {
281 name: "cycle".into(),
282 default: Value::U64(0),
283 port_type: crate::ast::PortType::U64,
284 kind: InputKind::Coordinate,
285 type_origin: crate::kernel::TypeOrigin::Declared,
286 converts_to: None,
287 },
288 InputDef {
289 name: "token".into(),
290 default: Value::Str("anon".into()),
291 port_type: crate::ast::PortType::Str,
292 kind: InputKind::ExternalWrite,
293 type_origin: crate::kernel::TypeOrigin::Declared,
294 converts_to: None,
295 },
296 ],
297 1,
298 HashMap::new(),
299 Vec::new(),
300 "",
301 "(test)",
302 crate::kernel::CompileLedger::new(),
303 ));
304 let mut state = program.create_state();
305
306 state.set_input(1, Value::Str("alice".into()));
307 assert_eq!(state.get_input(1), Value::Str("alice".into()));
308
309 // Reset only capture inputs (from coord_count onward)
310 state.reset_inputs_from(1);
311 assert_eq!(state.get_input(1), Value::Str("anon".into()));
312 }
313
314 #[test]
315 fn invalidate_all_keeps_inputs_and_reset_restores_defaults() {
316 let program = Arc::new(PolydatProgram::with_inputs(
317 vec![],
318 vec![],
319 vec![
320 InputDef {
321 name: "cycle".into(),
322 default: Value::U64(0),
323 port_type: crate::ast::PortType::U64,
324 kind: InputKind::Coordinate,
325 type_origin: crate::kernel::TypeOrigin::Declared,
326 converts_to: None,
327 },
328 InputDef {
329 name: "token".into(),
330 default: Value::Str("anon".into()),
331 port_type: crate::ast::PortType::Str,
332 kind: InputKind::ExternalWrite,
333 type_origin: crate::kernel::TypeOrigin::Declared,
334 converts_to: None,
335 },
336 ],
337 1,
338 HashMap::new(),
339 Vec::new(),
340 "",
341 "(test)",
342 crate::kernel::CompileLedger::new(),
343 ));
344 let mut state = program.create_state();
345
346 state.set_inputs(&[42]);
347 state.set_input(1, Value::Str("alice".into()));
348
349 state.invalidate_all();
350 assert_eq!(state.get_input(0), Value::U64(42));
351 assert_eq!(state.get_input(1), Value::Str("alice".into()));
352 state.reset_inputs_from(0);
353 assert_eq!(state.get_input(0), Value::U64(0));
354 assert_eq!(state.get_input(1), Value::Str("anon".into()));
355 }
356
357 // ---------------------------------------------------------------
358 // WireCost tests: config wire warnings for various DAG shapes
359 // ---------------------------------------------------------------
360
361 /// A test node with one Config wire input and one Data wire input.
362 /// Simulates a node with an expensive LUT that's configured by
363 /// the first input and driven by the second.
364 struct ConfigWireTestNode {
365 meta: crate::ast::NodeMeta,
366 }
367
368 impl ConfigWireTestNode {
369 fn new() -> Self {
370 use crate::ast::{Port, Slot};
371 Self {
372 meta: crate::ast::NodeMeta {
373 name: "config_test".into(),
374 outs: vec![Port::u64("output")],
375 ins: vec![
376 Slot::Wire(Port::u64("config_param").config()),
377 Slot::Wire(Port::u64("data_input")),
378 ],
379 },
380 }
381 }
382 }
383
384 impl crate::ast::PolydatNode for ConfigWireTestNode {
385 fn meta(&self) -> &crate::ast::NodeMeta {
386 &self.meta
387 }
388 fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
389 let config = inputs[0].as_u64();
390 let data = inputs[1].as_u64();
391 outputs[0] = Value::U64(config.wrapping_add(data));
392 }
393 }
394
395 #[test]
396 fn wire_cost_no_warning_when_config_is_init_time() {
397 // DAG: constant(42) → config_test.config_param
398 // cycle → hash → config_test.data_input
399 // Config wire fed by init-time constant → no warning
400 use crate::compile::assembly::{PolydatAssembler, WireRef};
401 use crate::dsl::events::CompileEventLog;
402 use crate::library::identity::ConstU64;
403 use crate::library::identity::Identity;
404
405 let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
406 asm.add_node("config_val", Box::new(ConstU64::new(42)), vec![]);
407 asm.add_node(
408 "hashed",
409 Box::new(Identity::new(crate::ast::PortType::U64)),
410 vec![WireRef::input("cycle")],
411 );
412 asm.add_node(
413 "test_node",
414 Box::new(ConfigWireTestNode::new()),
415 vec![WireRef::node("config_val"), WireRef::node("hashed")],
416 );
417 asm.add_output("result", WireRef::node("test_node"));
418
419 let mut log = CompileEventLog::new();
420 let k = asm.compile_with_log(Some(&mut log)).unwrap();
421 let _program = k.into_program();
422
423 // Check: no ConfigWireCycleWarning in events
424 let warnings: Vec<_> = log
425 .events()
426 .iter()
427 .filter(|e| {
428 matches!(
429 e,
430 crate::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
431 )
432 })
433 .collect();
434 assert!(
435 warnings.is_empty(),
436 "no warning expected when config wire is init-time: {warnings:?}"
437 );
438 }
439
440 #[test]
441 fn wire_cost_warning_when_config_is_cycle_time() {
442 // DAG: cycle → hash → config_test.config_param (BAD: config from cycle)
443 // cycle → config_test.data_input
444 // Config wire fed by cycle-time node → should warn
445 use crate::compile::assembly::{PolydatAssembler, WireRef};
446 use crate::dsl::events::CompileEventLog;
447 use crate::library::identity::Identity;
448
449 let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
450 asm.add_node(
451 "hashed",
452 Box::new(Identity::new(crate::ast::PortType::U64)),
453 vec![WireRef::input("cycle")],
454 );
455 asm.add_node(
456 "test_node",
457 Box::new(ConfigWireTestNode::new()),
458 vec![
459 WireRef::node("hashed"), // config_param ← cycle-time!
460 WireRef::input("cycle"), // data_input ← cycle
461 ],
462 );
463 asm.add_output("result", WireRef::node("test_node"));
464
465 let mut log = CompileEventLog::new();
466 let _k = asm.compile_with_log(Some(&mut log)).unwrap();
467
468 let warnings: Vec<_> = log
469 .events()
470 .iter()
471 .filter(|e| {
472 matches!(
473 e,
474 crate::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
475 )
476 })
477 .collect();
478 assert_eq!(
479 warnings.len(),
480 1,
481 "expected exactly one config wire warning: {warnings:?}"
482 );
483 }
484
485 /// The same graph under `strict`, on every engine a build can name.
486 ///
487 /// A config wire fed by a cycle-time source warns by default and is
488 /// refused under strict, and both of the assembler's strict checks
489 /// run through one function that every engine's build calls
490 /// (`refuse_strict` → `strict_violation`). The adapter half is
491 /// pinned across engines in `entry_points`; this pins the config
492 /// half, which needs a node with a config-typed wire port and so
493 /// lives beside the one that has it.
494 #[test]
495 fn strict_refuses_a_cycle_fed_config_wire_on_every_engine() {
496 use crate::compile::assembly::{PolydatAssembler, WireRef};
497 use crate::compile::select::{Engine, Provenance};
498 use crate::library::identity::Identity;
499
500 let build = || {
501 let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
502 asm.add_node(
503 "hashed",
504 Box::new(Identity::new(crate::ast::PortType::U64)),
505 vec![WireRef::input("cycle")],
506 );
507 asm.add_node(
508 "test_node",
509 Box::new(ConfigWireTestNode::new()),
510 vec![WireRef::node("hashed"), WireRef::input("cycle")],
511 );
512 asm.add_output("result", WireRef::node("test_node"));
513 asm.set_strict(true);
514 asm
515 };
516
517 let mut engines = vec![
518 Engine::Interpreter(crate::JitMode::Auto),
519 Engine::Closures(Provenance::Auto),
520 ];
521 if cfg!(feature = "jit") {
522 engines.push(Engine::Native(Provenance::Auto));
523 engines.push(Engine::PureNative(Provenance::Auto));
524 }
525 for engine in engines {
526 let outcome = build().compile_with(engine);
527 let err = match outcome {
528 Err(e) => e.to_string(),
529 Ok(_) => panic!("{engine}: strict must refuse a cycle-fed config wire"),
530 };
531 assert!(
532 err.contains("config"),
533 "{engine}: the refusal should name the config wire: {err}"
534 );
535 }
536 // Without strict the same graph builds, warning rather than
537 // refusing: strict is what turns the advice into an error. On
538 // the interpreter, because this node is a test fixture with no
539 // compiled form and the compiled tiers refuse it for that
540 // unrelated reason — which is also why the refusals above are
541 // checked for the word `config` rather than merely being
542 // errors.
543 let mut asm = build();
544 asm.set_strict(false);
545 assert!(
546 asm.compile_with(Engine::Interpreter(crate::JitMode::Auto))
547 .is_ok()
548 );
549 }
550
551 #[test]
552 fn wire_cost_warning_when_config_is_coordinate_direct() {
553 // DAG: cycle → config_test.config_param (BAD: coordinate direct to config)
554 // cycle → config_test.data_input
555 use crate::compile::assembly::{PolydatAssembler, WireRef};
556 use crate::dsl::events::CompileEventLog;
557
558 let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
559 asm.add_node(
560 "test_node",
561 Box::new(ConfigWireTestNode::new()),
562 vec![
563 WireRef::input("cycle"), // config_param ← coordinate!
564 WireRef::input("cycle"), // data_input ← cycle
565 ],
566 );
567 asm.add_output("result", WireRef::node("test_node"));
568
569 let mut log = CompileEventLog::new();
570 let _k = asm.compile_with_log(Some(&mut log)).unwrap();
571
572 let warnings: Vec<_> = log
573 .events()
574 .iter()
575 .filter(|e| {
576 matches!(
577 e,
578 crate::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
579 )
580 })
581 .collect();
582 assert_eq!(warnings.len(), 1, "config wire from coordinate should warn");
583 }
584
585 #[test]
586 fn wire_cost_no_warning_data_wire_from_cycle() {
587 // DAG: constant(10) → config_test.config_param (init-time, ok)
588 // cycle → config_test.data_input (cycle-time, ok for Data wire)
589 // Only the data wire is cycle-time → no warning
590 use crate::compile::assembly::{PolydatAssembler, WireRef};
591 use crate::dsl::events::CompileEventLog;
592 use crate::library::identity::ConstU64;
593
594 let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
595 asm.add_node("config_val", Box::new(ConstU64::new(10)), vec![]);
596 asm.add_node(
597 "test_node",
598 Box::new(ConfigWireTestNode::new()),
599 vec![
600 WireRef::node("config_val"), // config_param ← constant
601 WireRef::input("cycle"), // data_input ← cycle (Data wire, ok)
602 ],
603 );
604 asm.add_output("result", WireRef::node("test_node"));
605
606 let mut log = CompileEventLog::new();
607 let _k = asm.compile_with_log(Some(&mut log)).unwrap();
608
609 let warnings: Vec<_> = log
610 .events()
611 .iter()
612 .filter(|e| {
613 matches!(
614 e,
615 crate::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
616 )
617 })
618 .collect();
619 assert!(warnings.is_empty(), "data wire from cycle should not warn");
620 }
621
622 #[test]
623 fn wire_cost_diamond_config_from_init() {
624 // Diamond DAG using two ConfigWireTestNodes:
625 // constant(5) → inner.config_param ─┐
626 // constant(3) → inner.data_input ─┤→ inner.output → outer.config_param
627 // cycle → hash → outer.data_input
628 // inner is fully init-time → its output feeds outer's config wire → no warning
629 use crate::compile::assembly::{PolydatAssembler, WireRef};
630 use crate::dsl::events::CompileEventLog;
631 use crate::library::identity::ConstU64;
632 use crate::library::identity::Identity;
633
634 let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
635 asm.add_node("a", Box::new(ConstU64::new(5)), vec![]);
636 asm.add_node("b", Box::new(ConstU64::new(3)), vec![]);
637 asm.add_node(
638 "inner",
639 Box::new(ConfigWireTestNode::new()),
640 vec![WireRef::node("a"), WireRef::node("b")],
641 );
642 asm.add_node(
643 "hashed",
644 Box::new(Identity::new(crate::ast::PortType::U64)),
645 vec![WireRef::input("cycle")],
646 );
647 asm.add_node(
648 "outer",
649 Box::new(ConfigWireTestNode::new()),
650 vec![
651 WireRef::node("inner"), // config_param ← init-time (5+3)
652 WireRef::node("hashed"), // data_input ← cycle-time
653 ],
654 );
655 asm.add_output("result", WireRef::node("outer"));
656
657 let mut log = CompileEventLog::new();
658 let _k = asm.compile_with_log(Some(&mut log)).unwrap();
659
660 // inner's config wire from constant is fine. outer's config wire
661 // from init-time inner output is also fine.
662 let warnings: Vec<_> = log
663 .events()
664 .iter()
665 .filter(|e| {
666 matches!(
667 e,
668 crate::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
669 )
670 })
671 .collect();
672 assert!(
673 warnings.is_empty(),
674 "init-time derived config should not warn: {warnings:?}"
675 );
676 }
677
678 #[test]
679 fn wire_cost_diamond_config_from_mixed() {
680 // Mixed init/cycle feeding config:
681 // constant(5) → mixer.config_param ─┐
682 // cycle → mixer.data_input ─┤→ mixer.output → outer.config_param
683 // cycle → outer.data_input
684 // mixer depends on cycle → its output is cycle-time → outer's config wire warns
685 use crate::compile::assembly::{PolydatAssembler, WireRef};
686 use crate::dsl::events::CompileEventLog;
687 use crate::library::identity::ConstU64;
688
689 let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
690 asm.add_node("five", Box::new(ConstU64::new(5)), vec![]);
691 asm.add_node(
692 "mixer",
693 Box::new(ConfigWireTestNode::new()),
694 vec![
695 WireRef::node("five"), // config_param ← init
696 WireRef::input("cycle"), // data_input ← cycle
697 ],
698 );
699 asm.add_node(
700 "outer",
701 Box::new(ConfigWireTestNode::new()),
702 vec![
703 WireRef::node("mixer"), // config_param ← cycle-tainted!
704 WireRef::input("cycle"), // data_input
705 ],
706 );
707 asm.add_output("result", WireRef::node("outer"));
708
709 let mut log = CompileEventLog::new();
710 let _k = asm.compile_with_log(Some(&mut log)).unwrap();
711
712 let warnings: Vec<_> = log
713 .events()
714 .iter()
715 .filter(|e| {
716 matches!(
717 e,
718 crate::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
719 )
720 })
721 .collect();
722 // outer's config from cycle-tainted mixer should warn.
723 // mixer's config from constant should NOT warn.
724 assert_eq!(
725 warnings.len(),
726 1,
727 "exactly one warning for outer's config: {warnings:?}"
728 );
729 }
730}