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;
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}
113
114/// Definition of a named input to the Polydat graph.
115///
116/// All inputs — coordinates, iteration externs, and external-write ports
117/// — are defined uniformly. Coordinates default to `Value::U64(0)`,
118/// captures default to `Value::None` (unset until a capture writes
119/// to them) or to their declared default. The `kind` field carries
120/// the lifecycle classification used by the init-binding contract
121/// (see SRD 11 §"Init Binding Contract").
122#[derive(Debug, Clone)]
123pub struct InputDef {
124    /// Input name (e.g., "cycle", "username").
125    pub name: String,
126    /// Default value. Coordinates default to U64(0), captures
127    /// to their declared default (or None if unset).
128    pub default: Value,
129    /// The declared port type for this input. Used by the assembler
130    /// for type checking when wiring nodes to this input.
131    pub port_type: crate::ast::PortType,
132    /// Lifecycle classification. The assembler's coordinate inputs
133    /// are `Coordinate`; the DSL compiler sets `IterationExtern` for
134    /// iteration externs and `ExternalWrite` for `extern` ports.
135    pub kind: InputKind,
136    /// Whether the author wrote this input's type or the compiler
137    /// inferred it (input_variance.md §3). An inferred input is *open*:
138    /// the host may write a value of another type, and whether the
139    /// compiler converts it is `CompileOptions::input_variance`'s call.
140    pub type_origin: TypeOrigin,
141    /// For a converted input: the type its consumers read, which is
142    /// what the input reports as its type. The slot itself is `Dyn`
143    /// and holds the value as written; `None` for every other input.
144    pub converts_to: Option<crate::ast::PortType>,
145}
146
147/// How an input's type was established (input_variance.md §3).
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum TypeOrigin {
150    /// The author wrote it: `input x: T`, `extern x: T`, `shared x`.
151    /// The input is invariant, and a write of another type is refused.
152    Declared,
153    /// The compiler inferred it: an `input` with no type, or an
154    /// auto-extern typed from the binding that reads it.
155    Inferred,
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use std::collections::HashMap;
162    use std::sync::Arc;
163
164    #[test]
165    fn capture_inputs_persist_across_set_inputs() {
166        // Program with 1 coordinate (cycle) + 2 capture inputs
167        let program = Arc::new(PolydatProgram::with_inputs(
168            vec![],
169            vec![],
170            vec![
171                InputDef {
172                    name: "cycle".into(),
173                    default: Value::U64(0),
174                    port_type: crate::ast::PortType::U64,
175                    kind: InputKind::Coordinate,
176                    type_origin: crate::kernel::TypeOrigin::Declared,
177                    converts_to: None,
178                },
179                InputDef {
180                    name: "balance".into(),
181                    default: Value::F64(0.0),
182                    port_type: crate::ast::PortType::F64,
183                    kind: InputKind::ExternalWrite,
184                    type_origin: crate::kernel::TypeOrigin::Declared,
185                    converts_to: None,
186                },
187                InputDef {
188                    name: "auth_token".into(),
189                    default: Value::Str("anonymous".into()),
190                    port_type: crate::ast::PortType::Str,
191                    kind: InputKind::ExternalWrite,
192                    type_origin: crate::kernel::TypeOrigin::Declared,
193                    converts_to: None,
194                },
195            ],
196            1, // coord_count
197            HashMap::new(),
198            Vec::new(),
199            "",
200            "(test)",
201            crate::kernel::CompileLedger::new(),
202        ));
203        let mut state = program.create_state();
204
205        // Default values for capture inputs
206        assert_eq!(state.get_input(1), Value::F64(0.0));
207        assert_eq!(state.get_input(2), Value::Str("anonymous".into()));
208
209        // Set capture inputs individually
210        state.set_input(1, Value::F64(1234.56));
211        state.set_input(2, Value::Str("token_abc".into()));
212        assert_eq!(state.get_input(1), Value::F64(1234.56));
213        assert_eq!(state.get_input(2), Value::Str("token_abc".into()));
214
215        // Capture inputs persist when coordinates change
216        state.set_inputs(&[42]);
217        assert_eq!(state.get_input(1), Value::F64(1234.56));
218        assert_eq!(state.get_input(2), Value::Str("token_abc".into()));
219    }
220
221    #[test]
222    fn reset_inputs_restores_capture_defaults() {
223        let program = Arc::new(PolydatProgram::with_inputs(
224            vec![],
225            vec![],
226            vec![
227                InputDef {
228                    name: "cycle".into(),
229                    default: Value::U64(0),
230                    port_type: crate::ast::PortType::U64,
231                    kind: InputKind::Coordinate,
232                    type_origin: crate::kernel::TypeOrigin::Declared,
233                    converts_to: None,
234                },
235                InputDef {
236                    name: "token".into(),
237                    default: Value::Str("anon".into()),
238                    port_type: crate::ast::PortType::Str,
239                    kind: InputKind::ExternalWrite,
240                    type_origin: crate::kernel::TypeOrigin::Declared,
241                    converts_to: None,
242                },
243            ],
244            1,
245            HashMap::new(),
246            Vec::new(),
247            "",
248            "(test)",
249            crate::kernel::CompileLedger::new(),
250        ));
251        let mut state = program.create_state();
252
253        state.set_input(1, Value::Str("alice".into()));
254        assert_eq!(state.get_input(1), Value::Str("alice".into()));
255
256        // Reset only capture inputs (from coord_count onward)
257        state.reset_inputs_from(1);
258        assert_eq!(state.get_input(1), Value::Str("anon".into()));
259    }
260
261    #[test]
262    fn invalidate_all_keeps_inputs_and_reset_restores_defaults() {
263        let program = Arc::new(PolydatProgram::with_inputs(
264            vec![],
265            vec![],
266            vec![
267                InputDef {
268                    name: "cycle".into(),
269                    default: Value::U64(0),
270                    port_type: crate::ast::PortType::U64,
271                    kind: InputKind::Coordinate,
272                    type_origin: crate::kernel::TypeOrigin::Declared,
273                    converts_to: None,
274                },
275                InputDef {
276                    name: "token".into(),
277                    default: Value::Str("anon".into()),
278                    port_type: crate::ast::PortType::Str,
279                    kind: InputKind::ExternalWrite,
280                    type_origin: crate::kernel::TypeOrigin::Declared,
281                    converts_to: None,
282                },
283            ],
284            1,
285            HashMap::new(),
286            Vec::new(),
287            "",
288            "(test)",
289            crate::kernel::CompileLedger::new(),
290        ));
291        let mut state = program.create_state();
292
293        state.set_inputs(&[42]);
294        state.set_input(1, Value::Str("alice".into()));
295
296        state.invalidate_all();
297        assert_eq!(state.get_input(0), Value::U64(42));
298        assert_eq!(state.get_input(1), Value::Str("alice".into()));
299        state.reset_inputs_from(0);
300        assert_eq!(state.get_input(0), Value::U64(0));
301        assert_eq!(state.get_input(1), Value::Str("anon".into()));
302    }
303
304    // ---------------------------------------------------------------
305    // WireCost tests: config wire warnings for various DAG shapes
306    // ---------------------------------------------------------------
307
308    /// A test node with one Config wire input and one Data wire input.
309    /// Simulates a node with an expensive LUT that's configured by
310    /// the first input and driven by the second.
311    struct ConfigWireTestNode {
312        meta: crate::ast::NodeMeta,
313    }
314
315    impl ConfigWireTestNode {
316        fn new() -> Self {
317            use crate::ast::{Port, Slot};
318            Self {
319                meta: crate::ast::NodeMeta {
320                    name: "config_test".into(),
321                    outs: vec![Port::u64("output")],
322                    ins: vec![
323                        Slot::Wire(Port::u64("config_param").config()),
324                        Slot::Wire(Port::u64("data_input")),
325                    ],
326                },
327            }
328        }
329    }
330
331    impl crate::ast::PolydatNode for ConfigWireTestNode {
332        fn meta(&self) -> &crate::ast::NodeMeta {
333            &self.meta
334        }
335        fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
336            let config = inputs[0].as_u64();
337            let data = inputs[1].as_u64();
338            outputs[0] = Value::U64(config.wrapping_add(data));
339        }
340    }
341
342    #[test]
343    fn wire_cost_no_warning_when_config_is_init_time() {
344        // DAG: constant(42) → config_test.config_param
345        //      cycle → hash → config_test.data_input
346        // Config wire fed by init-time constant → no warning
347        use crate::compile::assembly::{PolydatAssembler, WireRef};
348        use crate::dsl::events::CompileEventLog;
349        use crate::library::identity::ConstU64;
350        use crate::library::identity::Identity;
351
352        let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
353        asm.add_node("config_val", Box::new(ConstU64::new(42)), vec![]);
354        asm.add_node(
355            "hashed",
356            Box::new(Identity::new(crate::ast::PortType::U64)),
357            vec![WireRef::input("cycle")],
358        );
359        asm.add_node(
360            "test_node",
361            Box::new(ConfigWireTestNode::new()),
362            vec![WireRef::node("config_val"), WireRef::node("hashed")],
363        );
364        asm.add_output("result", WireRef::node("test_node"));
365
366        let mut log = CompileEventLog::new();
367        let k = asm.compile_with_log(Some(&mut log)).unwrap();
368        let _program = k.into_program();
369
370        // Check: no ConfigWireCycleWarning in events
371        let warnings: Vec<_> = log
372            .events()
373            .iter()
374            .filter(|e| {
375                matches!(
376                    e,
377                    crate::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
378                )
379            })
380            .collect();
381        assert!(
382            warnings.is_empty(),
383            "no warning expected when config wire is init-time: {warnings:?}"
384        );
385    }
386
387    #[test]
388    fn wire_cost_warning_when_config_is_cycle_time() {
389        // DAG: cycle → hash → config_test.config_param  (BAD: config from cycle)
390        //      cycle → config_test.data_input
391        // Config wire fed by cycle-time node → should warn
392        use crate::compile::assembly::{PolydatAssembler, WireRef};
393        use crate::dsl::events::CompileEventLog;
394        use crate::library::identity::Identity;
395
396        let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
397        asm.add_node(
398            "hashed",
399            Box::new(Identity::new(crate::ast::PortType::U64)),
400            vec![WireRef::input("cycle")],
401        );
402        asm.add_node(
403            "test_node",
404            Box::new(ConfigWireTestNode::new()),
405            vec![
406                WireRef::node("hashed"), // config_param ← cycle-time!
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!(
426            warnings.len(),
427            1,
428            "expected exactly one config wire warning: {warnings:?}"
429        );
430    }
431
432    /// The same graph under `strict`, on every engine a build can name.
433    ///
434    /// A config wire fed by a cycle-time source warns by default and is
435    /// refused under strict, and both of the assembler's strict checks
436    /// run through one function that every engine's build calls
437    /// (`refuse_strict` → `strict_violation`). The adapter half is
438    /// pinned across engines in `entry_points`; this pins the config
439    /// half, which needs a node with a config-typed wire port and so
440    /// lives beside the one that has it (F-H3).
441    #[test]
442    fn strict_refuses_a_cycle_fed_config_wire_on_every_engine() {
443        use crate::compile::assembly::{PolydatAssembler, WireRef};
444        use crate::compile::select::{Engine, Provenance};
445        use crate::library::identity::Identity;
446
447        let build = || {
448            let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
449            asm.add_node(
450                "hashed",
451                Box::new(Identity::new(crate::ast::PortType::U64)),
452                vec![WireRef::input("cycle")],
453            );
454            asm.add_node(
455                "test_node",
456                Box::new(ConfigWireTestNode::new()),
457                vec![WireRef::node("hashed"), WireRef::input("cycle")],
458            );
459            asm.add_output("result", WireRef::node("test_node"));
460            asm.set_strict(true);
461            asm
462        };
463
464        let mut engines = vec![
465            Engine::Interpreter(crate::JitMode::Auto),
466            Engine::Closures(Provenance::Auto),
467        ];
468        if cfg!(feature = "jit") {
469            engines.push(Engine::Native(Provenance::Auto));
470            engines.push(Engine::PureNative(Provenance::Auto));
471        }
472        for engine in engines {
473            let outcome = build().compile_with(engine);
474            let err = match outcome {
475                Err(e) => e.to_string(),
476                Ok(_) => panic!("{engine}: strict must refuse a cycle-fed config wire"),
477            };
478            assert!(
479                err.contains("config"),
480                "{engine}: the refusal should name the config wire: {err}"
481            );
482        }
483        // Without strict the same graph builds, warning rather than
484        // refusing: strict is what turns the advice into an error. On
485        // the interpreter, because this node is a test fixture with no
486        // compiled form and the compiled tiers refuse it for that
487        // unrelated reason — which is also why the refusals above are
488        // checked for the word `config` rather than merely being
489        // errors.
490        let mut asm = build();
491        asm.set_strict(false);
492        assert!(
493            asm.compile_with(Engine::Interpreter(crate::JitMode::Auto))
494                .is_ok()
495        );
496    }
497
498    #[test]
499    fn wire_cost_warning_when_config_is_coordinate_direct() {
500        // DAG: cycle → config_test.config_param  (BAD: coordinate direct to config)
501        //      cycle → config_test.data_input
502        use crate::compile::assembly::{PolydatAssembler, WireRef};
503        use crate::dsl::events::CompileEventLog;
504
505        let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
506        asm.add_node(
507            "test_node",
508            Box::new(ConfigWireTestNode::new()),
509            vec![
510                WireRef::input("cycle"), // config_param ← coordinate!
511                WireRef::input("cycle"), // data_input ← cycle
512            ],
513        );
514        asm.add_output("result", WireRef::node("test_node"));
515
516        let mut log = CompileEventLog::new();
517        let _k = asm.compile_with_log(Some(&mut log)).unwrap();
518
519        let warnings: Vec<_> = log
520            .events()
521            .iter()
522            .filter(|e| {
523                matches!(
524                    e,
525                    crate::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
526                )
527            })
528            .collect();
529        assert_eq!(warnings.len(), 1, "config wire from coordinate should warn");
530    }
531
532    #[test]
533    fn wire_cost_no_warning_data_wire_from_cycle() {
534        // DAG: constant(10) → config_test.config_param (init-time, ok)
535        //      cycle → config_test.data_input           (cycle-time, ok for Data wire)
536        // Only the data wire is cycle-time → no warning
537        use crate::compile::assembly::{PolydatAssembler, WireRef};
538        use crate::dsl::events::CompileEventLog;
539        use crate::library::identity::ConstU64;
540
541        let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
542        asm.add_node("config_val", Box::new(ConstU64::new(10)), vec![]);
543        asm.add_node(
544            "test_node",
545            Box::new(ConfigWireTestNode::new()),
546            vec![
547                WireRef::node("config_val"), // config_param ← constant
548                WireRef::input("cycle"),     // data_input ← cycle (Data wire, ok)
549            ],
550        );
551        asm.add_output("result", WireRef::node("test_node"));
552
553        let mut log = CompileEventLog::new();
554        let _k = asm.compile_with_log(Some(&mut log)).unwrap();
555
556        let warnings: Vec<_> = log
557            .events()
558            .iter()
559            .filter(|e| {
560                matches!(
561                    e,
562                    crate::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
563                )
564            })
565            .collect();
566        assert!(warnings.is_empty(), "data wire from cycle should not warn");
567    }
568
569    #[test]
570    fn wire_cost_diamond_config_from_init() {
571        // Diamond DAG using two ConfigWireTestNodes:
572        //   constant(5) → inner.config_param ─┐
573        //   constant(3) → inner.data_input    ─┤→ inner.output → outer.config_param
574        //   cycle → hash → outer.data_input
575        // inner is fully init-time → its output feeds outer's config wire → no warning
576        use crate::compile::assembly::{PolydatAssembler, WireRef};
577        use crate::dsl::events::CompileEventLog;
578        use crate::library::identity::ConstU64;
579        use crate::library::identity::Identity;
580
581        let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
582        asm.add_node("a", Box::new(ConstU64::new(5)), vec![]);
583        asm.add_node("b", Box::new(ConstU64::new(3)), vec![]);
584        asm.add_node(
585            "inner",
586            Box::new(ConfigWireTestNode::new()),
587            vec![WireRef::node("a"), WireRef::node("b")],
588        );
589        asm.add_node(
590            "hashed",
591            Box::new(Identity::new(crate::ast::PortType::U64)),
592            vec![WireRef::input("cycle")],
593        );
594        asm.add_node(
595            "outer",
596            Box::new(ConfigWireTestNode::new()),
597            vec![
598                WireRef::node("inner"),  // config_param ← init-time (5+3)
599                WireRef::node("hashed"), // data_input ← cycle-time
600            ],
601        );
602        asm.add_output("result", WireRef::node("outer"));
603
604        let mut log = CompileEventLog::new();
605        let _k = asm.compile_with_log(Some(&mut log)).unwrap();
606
607        // inner's config wire from constant is fine. outer's config wire
608        // from init-time inner output is also fine.
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!(
620            warnings.is_empty(),
621            "init-time derived config should not warn: {warnings:?}"
622        );
623    }
624
625    #[test]
626    fn wire_cost_diamond_config_from_mixed() {
627        // Mixed init/cycle feeding config:
628        //   constant(5) → mixer.config_param ─┐
629        //   cycle → mixer.data_input          ─┤→ mixer.output → outer.config_param
630        //   cycle → outer.data_input
631        // mixer depends on cycle → its output is cycle-time → outer's config wire warns
632        use crate::compile::assembly::{PolydatAssembler, WireRef};
633        use crate::dsl::events::CompileEventLog;
634        use crate::library::identity::ConstU64;
635
636        let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
637        asm.add_node("five", Box::new(ConstU64::new(5)), vec![]);
638        asm.add_node(
639            "mixer",
640            Box::new(ConfigWireTestNode::new()),
641            vec![
642                WireRef::node("five"),   // config_param ← init
643                WireRef::input("cycle"), // data_input ← cycle
644            ],
645        );
646        asm.add_node(
647            "outer",
648            Box::new(ConfigWireTestNode::new()),
649            vec![
650                WireRef::node("mixer"),  // config_param ← cycle-tainted!
651                WireRef::input("cycle"), // data_input
652            ],
653        );
654        asm.add_output("result", WireRef::node("outer"));
655
656        let mut log = CompileEventLog::new();
657        let _k = asm.compile_with_log(Some(&mut log)).unwrap();
658
659        let warnings: Vec<_> = log
660            .events()
661            .iter()
662            .filter(|e| {
663                matches!(
664                    e,
665                    crate::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
666                )
667            })
668            .collect();
669        // outer's config from cycle-tainted mixer should warn.
670        // mixer's config from constant should NOT warn.
671        assert_eq!(
672            warnings.len(),
673            1,
674            "exactly one warning for outer's config: {warnings:?}"
675        );
676    }
677}