Skip to main content

polydat_core/kernel/subcontext/
kernel.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! [`ScopeKernel<M>`] — typed wrapper around a kernel of any engine.
5//!
6//! `ScopeKernel<M>` is the typed surface (subcontext_construction.md
7//! §1); the interpreter's `PolydatKernel` stays public, and its
8//! construction primitives are sealed (`from_program` is crate-private,
9//! `materialize_wiring_from_outer` private), so a child is built only
10//! through the typed surface or the binder. A child is built on its
11//! parent's engine.
12//!
13//! The kernel exposes:
14//! - [`Self::subcontext_builder`] — yields a typed
15//!   [`super::SubcontextBuilder`] over this kernel's
16//!   [`super::ParentView`]. The single public entry point for child
17//!   construction on the typed path.
18//! - [`Self::spawn`] — the single chokepoint where every
19//!   cross-binding is resolved; takes a closed
20//!   [`super::ScopeModule`] artifact, applies the
21//!   cross-binding rules (subcontext_construction.md §4), returns a typed child kernel and
22//!   records the spawn under `name` in this kernel's registry.
23//! - [`Self::release_child`] — drop a registry entry to allow
24//!   re-spawn under the same name (for per-iteration
25//!   re-traversal).
26
27use std::collections::HashMap;
28use std::marker::PhantomData;
29use std::sync::{Arc, Mutex};
30
31use crate::ast::{PortType, Value};
32use crate::kernel::{Kernel, PolydatKernel, SharedCell};
33
34use super::builder::{ParentView, SubcontextBuilder};
35use super::error::{ContractViolation, SourceContext};
36use super::module::{ScopeModule, WriteThroughBinding};
37use super::name::ChildName;
38use super::pull::RegisteredPullConsumer;
39
40/// Phantom-marker brand for the workload-root scope kernel —
41/// the top of any spawn type chain. Tests / examples that need
42/// a "starting" identity use this.
43#[derive(Debug)]
44pub struct RootMarker;
45
46/// Phantom-marker brand for "child of `P`". `spawn` returns
47/// `ScopeKernel<Child<P>>`, distinct at the type level from a
48/// sibling's `Child<P>` *value* but type-compatible at the
49/// module-identity level (subcontext_construction.md §1).
50#[derive(Debug)]
51pub struct Child<P>(PhantomData<fn() -> P>);
52
53/// Internal record of a spawned child — used for the
54/// duplicate-spawn diagnostic.
55#[derive(Debug)]
56struct ChildEntry {
57    site: SourceContext,
58}
59
60/// Typed wrapper around a kernel of any engine.
61///
62/// Construction via this type goes through the subcontext protocol
63/// (`subcontext_builder` → `finalize` → `spawn`), which builds each
64/// child on its parent's engine; a root is wrapped with
65/// [`wrap_root_kernel`].
66pub struct ScopeKernel<M> {
67    name: ChildName,
68    inner: Arc<Mutex<Box<dyn Kernel>>>,
69    site: SourceContext,
70    children: Mutex<HashMap<ChildName, ChildEntry>>,
71    consumers: Mutex<Vec<RegisteredPullConsumer>>,
72    /// Rule 2 write-through bindings. Per-cycle eval of this
73    /// kernel must call [`Self::commit_write_throughs`] after
74    /// producing values to fan them through the parent's
75    /// `SharedCell`s.
76    write_throughs: Vec<WriteThroughBinding>,
77    _module: PhantomData<fn() -> M>,
78}
79
80impl<M> std::fmt::Debug for ScopeKernel<M> {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.debug_struct("ScopeKernel")
83            .field("name", &self.name)
84            .field("site", &self.site)
85            .finish()
86    }
87}
88
89/// One shared cell visible at a parent scope, reified for
90/// transitive cross-binding. Returned by
91/// [`ScopeKernel::shared_cells_in_scope`].
92///
93/// Carries the name a child must use to bind to the cell,
94/// the port type (so Rule 2 / `extern` synthesis at finalize
95/// can declare a typed input slot), and the cell handle (so
96/// spawn can attach it to the child's matching input).
97///
98/// "In scope" semantics: a cell visible at the parent is one
99/// the parent itself can read or write at this scope —
100/// covering both:
101///
102/// 1. Cells the parent declared via its own program
103///    (`shared X := <init>` produces a cell-bound input slot).
104/// 2. Cells inherited from the parent's own ancestors
105///    (attached during the parent's spawn). Without this
106///    case, a `shared` cell at the workload root would not
107///    propagate to grand-children whose immediate parent's
108///    body never references the name.
109///
110/// Both cases are answered by walking the parent's input
111/// slots and reading `PolydatState::shared_cell` for each (see
112/// `PolydatKernel::shared_cells_in_scope`).
113#[derive(Clone)]
114pub struct SharedCellInScope {
115    /// The binding's name.
116    pub name: String,
117    /// The cell's declared type.
118    pub port_type: PortType,
119    /// The cell.
120    pub cell: SharedCell,
121}
122
123impl std::fmt::Debug for SharedCellInScope {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        f.debug_struct("SharedCellInScope")
126            .field("name", &self.name)
127            .field("port_type", &self.port_type)
128            .finish()
129    }
130}
131
132impl<M> ScopeKernel<M> {
133    /// Enumerate every shared cell visible at this scope: the
134    /// kernel's [`Kernel::cells_in_scope`], its own and those it
135    /// carries for its descendants.
136    pub fn shared_cells_in_scope(&self) -> Vec<SharedCellInScope> {
137        let inner = self.lock_inner();
138        inner
139            .cells_in_scope()
140            .into_iter()
141            .map(|e| SharedCellInScope {
142                name: e.name,
143                port_type: e.port_type,
144                cell: e.cell,
145            })
146            .collect()
147    }
148
149    /// Internal constructor — only the spawn path and
150    /// [`wrap_root_kernel`] produce a `ScopeKernel` directly. Public
151    /// callers go through the protocol.
152    pub(crate) fn new_internal(
153        name: ChildName,
154        kernel: Box<dyn Kernel>,
155        site: SourceContext,
156        consumers: Vec<RegisteredPullConsumer>,
157    ) -> Self {
158        Self::new_with_write_throughs(name, kernel, site, consumers, Vec::new())
159    }
160
161    pub(crate) fn new_with_write_throughs(
162        name: ChildName,
163        kernel: Box<dyn Kernel>,
164        site: SourceContext,
165        consumers: Vec<RegisteredPullConsumer>,
166        write_throughs: Vec<WriteThroughBinding>,
167    ) -> Self {
168        Self {
169            name,
170            inner: Arc::new(Mutex::new(kernel)),
171            site,
172            children: Mutex::new(HashMap::new()),
173            consumers: Mutex::new(consumers),
174            write_throughs,
175            _module: PhantomData,
176        }
177    }
178
179    /// The structured name this kernel was spawned under (for
180    /// child kernels) or its self-label (for root kernels).
181    pub fn name(&self) -> &ChildName {
182        &self.name
183    }
184
185    /// Diagnostic site for this kernel's construction.
186    pub fn site(&self) -> &SourceContext {
187        &self.site
188    }
189
190    /// Borrow the underlying kernel. The lock is released when the
191    /// returned guard is dropped. Exposed for callers that drive the
192    /// kernel directly.
193    pub fn lock_inner(&self) -> std::sync::MutexGuard<'_, Box<dyn Kernel>> {
194        self.inner
195            .lock()
196            .expect("ScopeKernel inner kernel poisoned")
197    }
198
199    /// The pull consumers registered with this kernel. Used by
200    /// the activity-side fixture adapter at seal time.
201    pub fn consumers(&self) -> Vec<RegisteredPullConsumer> {
202        self.consumers
203            .lock()
204            .expect("ScopeKernel consumers poisoned")
205            .clone()
206    }
207
208    /// Whether `name` is recorded in this kernel's named-child
209    /// registry. Diagnostic; the subcontext tests assert against this.
210    pub fn has_child(&self, name: &ChildName) -> bool {
211        self.children
212            .lock()
213            .expect("ScopeKernel children registry poisoned")
214            .contains_key(name)
215    }
216
217    /// Drop the named child from this kernel's registry. The
218    /// child kernel itself is unaffected — only the registry
219    /// entry. After release, the same name may be spawned again
220    /// (typical for comprehension scopes that re-traverse per
221    /// iteration). See subcontext_construction.md §4.1.
222    pub fn release_child(&self, name: &ChildName) {
223        self.children
224            .lock()
225            .expect("ScopeKernel children registry poisoned")
226            .remove(name);
227    }
228
229    /// Begin construction of a child sub-context
230    /// (subcontext_construction.md §1): the builder takes the
231    /// parent's [`ParentView`] — its names, modifiers, ledger, and
232    /// in-scope cells, which is everything finalize reads of a
233    /// parent — accumulates
234    /// module matter, and produces a closed [`ScopeModule`] artifact
235    /// at finalize. It holds no reference to the parent kernel, so a
236    /// caller that only has a kernel needs no `ScopeKernel` to stand up
237    /// a child.
238    pub fn subcontext_builder(&self) -> SubcontextBuilder<M> {
239        SubcontextBuilder::new(ParentView::of_kernel(self.lock_inner().as_ref()))
240    }
241
242    /// Spawn a child kernel from a closed [`ScopeModule`]
243    /// artifact (subcontext_construction.md §4): this is the single
244    /// chokepoint where every cross-binding is resolved.
245    ///
246    /// The artifact arrives with Rule 1 (name closure) and Rule 2
247    /// (the shared write-through rewrite) already applied by
248    /// [`SubcontextBuilder::finalize`]. Spawn instantiates the
249    /// closed program on this parent's engine
250    /// ([`ScopeModule::instantiate_under`]), whose binder does the
251    /// live binding: attaches every parent-visible `SharedCell` to
252    /// a matching child slot and forwards the rest as transit
253    /// (Rule 2's cell attach, SC8), value-copies or cell-attaches
254    /// parent outputs into child externs (Rules 4 and 5),
255    /// initializes the child so its consts see post-bind inputs
256    /// (Rule 3), and freezes the scope coordinates. A refused copy or
257    /// a failing const is [`ContractViolation::Bind`]. Per-cycle
258    /// publication to the cells is [`Self::commit_write_throughs`].
259    pub fn spawn(
260        self: &Arc<Self>,
261        name: ChildName,
262        artifact: ScopeModule<Child<M>>,
263    ) -> Result<ScopeKernel<Child<M>>, ContractViolation> {
264        // ----- Named-child registry guard
265        // (subcontext_construction.md §4.1) -----
266        {
267            let mut children = self
268                .children
269                .lock()
270                .expect("ScopeKernel children registry poisoned");
271            if let Some(prior) = children.get(&name) {
272                return Err(ContractViolation::DuplicateChild {
273                    name: name.clone(),
274                    prior_site: Box::new(prior.site.clone()),
275                    this_site: artifact.context.clone(),
276                });
277            }
278            children.insert(
279                name.clone(),
280                ChildEntry {
281                    site: artifact.context.clone(),
282                },
283            );
284        }
285
286        // ----- Cross-binding resolution -----
287        // Single chokepoint: `materialize_wiring_from_outer` walks every
288        // cell visible at the parent (own slots + transit
289        // cells inherited from ancestors), attaches each to
290        // any matching child slot, and forwards the rest as
291        // transit on the child kernel. This is the transitive
292        // cascade — an ancestral `shared X` cell remains
293        // visible to deep descendants regardless of how many
294        // intermediate scopes' bodies skip the name.
295        //
296        // Honours Rule 1 (import resolution validated at
297        // finalize), Rule 2 (write-through rewrite produces
298        // the matching child input slot finalize-side),
299        // Rule 4 (coordinate routing via IterationExtern
300        // input-kind), Rule 5 (closure-binding economy —
301        // unreferenced names skip cell attachment but still
302        // ride the transit channel for grand-children).
303        let built = {
304            let parent_inner = self.lock_inner();
305            artifact.instantiate_under(parent_inner.as_ref(), parent_inner.engine(), &[])
306        };
307        let child_kernel = match built {
308            Ok(kernel) => kernel,
309            Err(e) => {
310                // A child that was never built leaves its name free.
311                self.release_child(&name);
312                return Err(e.into());
313            }
314        };
315
316        let child_site = artifact.context.clone();
317        let child_consumers = artifact.consumers.clone();
318        let child_write_throughs = artifact.write_throughs.clone();
319
320        Ok(ScopeKernel::new_with_write_throughs(
321            name,
322            child_kernel,
323            child_site,
324            child_consumers,
325            child_write_throughs,
326        ))
327    }
328
329    /// The Rule 2 write-through bindings carried by this kernel.
330    /// Empty for the vast majority of kernels; populated only
331    /// when the artifact's `finalize` rewrote a child export to
332    /// a parent `shared` cell write.
333    pub fn write_throughs(&self) -> &[WriteThroughBinding] {
334        &self.write_throughs
335    }
336
337    /// Per-cycle Rule 2 commit: pulls every write-through's
338    /// synthetic source output (`__write_<X>`) and stores its
339    /// value through the corresponding child input slot for
340    /// `<X>`. Because `materialize_wiring_from_outer` attached the parent's
341    /// `SharedCell` to that slot, the write propagates to the
342    /// cell, where it becomes visible to the parent and to any
343    /// sibling that shares the same cell.
344    ///
345    /// No-op for kernels with no write-throughs.
346    ///
347    /// TYPE-STABLE (scope_model.md §6.1): each pending
348    /// value passes the boundary of [`Kernel::commit_write_throughs`]:
349    /// matching types pass, catalog adapters heal (widening), and an
350    /// unhealable mismatch is an `Err` at the write site.
351    pub fn commit_write_throughs(&self) -> Result<(), String> {
352        if self.write_throughs.is_empty() {
353            return Ok(());
354        }
355        self.lock_inner().commit_write_throughs()
356    }
357}
358
359/// Construct a workload-root [`ScopeKernel<RootMarker>`] from a
360/// compiled kernel of any engine: the door into the typed scope path,
361/// where a host spawns children it keeps and releases by name rather
362/// than dropping a kernel on the floor. Every child spawned under it
363/// runs on its engine.
364pub fn wrap_root_kernel(
365    kernel: Box<dyn Kernel>,
366    label: impl Into<String>,
367) -> Arc<ScopeKernel<RootMarker>> {
368    let label = label.into();
369    let name = ChildName::from_segments([label.clone()]);
370    let site = SourceContext::new(label);
371    Arc::new(ScopeKernel::new_internal(name, kernel, site, Vec::new()))
372}
373
374/// Typed Polydat matter accepted by both kernel-construction
375/// paths — root and subscope. Opaque externally: the only way
376/// to obtain a `PolydatMatter` value is via [`PolydatMatter::builder`].
377///
378/// Internally carries one of three input forms — fresh source,
379/// pre-parsed statements (the "module parser" output), or a
380/// pre-compiled program. The builder validates that exactly
381/// one form is provided.
382pub struct PolydatMatter<'a> {
383    pub(crate) inner: PolydatMatterInner<'a>,
384}
385
386pub(crate) enum PolydatMatterInner<'a> {
387    Source(SourceMatter),
388    Statements(StatementsMatter),
389    Program(ProgramMatter<'a>),
390}
391
392pub(crate) struct SourceMatter {
393    pub(crate) label: String,
394    pub(crate) body: String,
395    pub(crate) result_bindings: Option<String>,
396    pub(crate) inherited_outputs: Vec<String>,
397    pub(crate) options: super::builder::CompileOptions,
398}
399
400pub(crate) struct StatementsMatter {
401    pub(crate) label: String,
402    pub(crate) statements: Vec<crate::dsl::ast::Statement>,
403    pub(crate) result_bindings: Option<String>,
404    pub(crate) inherited_outputs: Vec<String>,
405    pub(crate) options: super::builder::CompileOptions,
406}
407
408pub(crate) struct ProgramMatter<'a> {
409    pub(crate) program: Arc<dyn crate::kernel::KernelProgram>,
410    pub(crate) iter_bindings: &'a [(String, Value)],
411}
412
413impl<'a> PolydatMatter<'a> {
414    /// Begin building Polydat matter. The builder is the only
415    /// constructor of `PolydatMatter`; the variants and their
416    /// fields are not exposed.
417    #[inline]
418    pub fn builder() -> PolydatMatterBuilder<'a> {
419        PolydatMatterBuilder::new()
420    }
421}
422
423/// Builder for [`PolydatMatter`]. Configure exactly one input form
424/// (source, pre-parsed statements, or program), plus optional
425/// metadata, then call [`Self::build`].
426#[derive(Default)]
427pub struct PolydatMatterBuilder<'a> {
428    label: Option<String>,
429    body: Option<String>,
430    statements: Option<Vec<crate::dsl::ast::Statement>>,
431    program: Option<Arc<dyn crate::kernel::KernelProgram>>,
432    iter_bindings: &'a [(String, Value)],
433    result_bindings: Option<String>,
434    inherited_outputs: Vec<String>,
435    options: super::builder::CompileOptions,
436}
437
438impl<'a> PolydatMatterBuilder<'a> {
439    fn new() -> Self {
440        Self::default()
441    }
442
443    /// Diagnostic label for this matter. Surfaces in compile
444    /// errors and as the child's `SourceContext`.
445    pub fn label(mut self, label: impl Into<String>) -> Self {
446        self.label = Some(label.into());
447        self
448    }
449
450    /// Provide Polydat source as a string. Mutually exclusive with
451    /// [`Self::statements`] and [`Self::program`].
452    pub fn source(mut self, body: impl Into<String>) -> Self {
453        self.body = Some(body.into());
454        self
455    }
456
457    /// Provide Polydat source as pre-parsed AST statements. Mutually
458    /// exclusive with [`Self::source`] and [`Self::program`].
459    /// Use when the caller has already run the module parser
460    /// (e.g. when synthesising scope source from a structured
461    /// model and wanting to skip a string round-trip).
462    pub fn statements(mut self, stmts: Vec<crate::dsl::ast::Statement>) -> Self {
463        self.statements = Some(stmts);
464        self
465    }
466
467    /// Provide a pre-compiled program on any engine
468    /// ([`crate::Kernel::into_program`], or an interpreter
469    /// `Arc<PolydatProgram>`). Mutually exclusive with
470    /// [`Self::source`] and [`Self::statements`]. Used for per-
471    /// fiber state forks, comprehension iteration, and other
472    /// call sites that hold a compiled program directly.
473    pub fn program(mut self, program: Arc<dyn crate::kernel::KernelProgram>) -> Self {
474        self.program = Some(program);
475        self
476    }
477
478    /// Iter-var bindings applied before the parent binds the
479    /// child. Only meaningful for the program form.
480    pub fn iter_bindings(mut self, bindings: &'a [(String, Value)]) -> Self {
481        self.iter_bindings = bindings;
482        self
483    }
484
485    /// Result-binding source (subcontext_construction.md §3.2). Folded through
486    /// [`super::SubcontextBuilder::add_result_bindings`] at
487    /// finalize. Only meaningful for source / statements forms.
488    pub fn result_bindings(mut self, src: impl Into<String>) -> Self {
489        self.result_bindings = Some(src.into());
490        self
491    }
492
493    /// Names to pass through `mark_inherited_outputs` so the
494    /// scope tree can distinguish own exports from cascade-
495    /// inherited names. Source / statements forms only.
496    pub fn inherited_outputs(mut self, names: Vec<String>) -> Self {
497        self.inherited_outputs = names;
498        self
499    }
500
501    /// Compile-time knobs (lib paths, strict mode, required
502    /// outputs, cursor limit). Source / statements forms only.
503    pub fn options(mut self, options: super::builder::CompileOptions) -> Self {
504        self.options = options;
505        self
506    }
507
508    /// Validate and produce typed matter. Errors when zero or
509    /// more than one input form is configured.
510    pub fn build(self) -> Result<PolydatMatter<'a>, String> {
511        let forms = [
512            self.body.is_some(),
513            self.statements.is_some(),
514            self.program.is_some(),
515        ];
516        let count = forms.iter().filter(|x| **x).count();
517        if count == 0 {
518            return Err(
519                "PolydatMatter::builder: no input form set (use .source / .statements / .program)"
520                    .into(),
521            );
522        }
523        if count > 1 {
524            return Err(
525                "PolydatMatter::builder: multiple input forms set; choose exactly one".into(),
526            );
527        }
528        let label = self.label.unwrap_or_else(|| "(matter)".to_string());
529        let inner = if let Some(body) = self.body {
530            PolydatMatterInner::Source(SourceMatter {
531                label,
532                body,
533                result_bindings: self.result_bindings,
534                inherited_outputs: self.inherited_outputs,
535                options: self.options,
536            })
537        } else if let Some(stmts) = self.statements {
538            PolydatMatterInner::Statements(StatementsMatter {
539                label,
540                statements: stmts,
541                result_bindings: self.result_bindings,
542                inherited_outputs: self.inherited_outputs,
543                options: self.options,
544            })
545        } else {
546            // program
547            PolydatMatterInner::Program(ProgramMatter {
548                program: self.program.expect("program form set per count above"),
549                iter_bindings: self.iter_bindings,
550            })
551        };
552        Ok(PolydatMatter { inner })
553    }
554}
555
556impl PolydatMatter<'_> {
557    /// THE subscope-construction path. Per the kernel-construction
558    /// invariant, this is how a parent kernel of any engine produces a
559    /// child. `compile_polydat` produces root kernels; everything else
560    /// is a subscope and routes here.
561    ///
562    /// A child from source or statements is compiled once and bound on
563    /// `parent`'s engine through the binder every engine shares
564    /// ([`super::ScopeModule::instantiate_under`]). A child from a
565    /// compiled program is bound on that program's engine
566    /// ([`crate::kernel::bind_under`]). Cell propagation,
567    /// scope-coordinate plumbing, and Rule 2 write-throughs flow from
568    /// `parent` into the returned child.
569    pub fn build_under(self, parent: &dyn Kernel) -> Result<Box<dyn Kernel>, ContractViolation> {
570        use super::module::BodyFragment;
571        let (label, strict, inherited, options, body, result_bindings) = match self.inner {
572            PolydatMatterInner::Program(p) => {
573                return Ok(crate::kernel::bind_under(
574                    parent,
575                    p.program,
576                    p.iter_bindings,
577                )?);
578            }
579            PolydatMatterInner::Source(s) => (
580                s.label,
581                s.options.strict,
582                s.inherited_outputs,
583                s.options,
584                BodyFragment::PolydatSource(s.body),
585                s.result_bindings,
586            ),
587            PolydatMatterInner::Statements(s) => (
588                s.label,
589                s.options.strict,
590                s.inherited_outputs,
591                s.options,
592                BodyFragment::Statements(s.statements),
593                s.result_bindings,
594            ),
595        };
596        let mut builder: SubcontextBuilder<RootMarker> =
597            SubcontextBuilder::new(ParentView::of_kernel(parent));
598        builder
599            .context(SourceContext::new(label.clone()))
600            .mark_inherited_outputs(inherited)
601            .with_compile_options(options)
602            .body(body);
603        if let Some(src) = result_bindings {
604            builder.add_result_bindings(&src)?;
605        }
606        let module = builder.finalize()?;
607        let mut child = module.instantiate_under(parent, parent.engine(), &[])?;
608        enforce_l2f_strict(child.as_mut(), strict, &label)?;
609        Ok(child)
610    }
611}
612
613impl PolydatKernel {
614    /// [`PolydatMatter::build_under`] this kernel: the child runs on
615    /// the interpreter, this kernel's engine.
616    pub fn build_subscope(
617        &self,
618        matter: PolydatMatter<'_>,
619    ) -> Result<Box<dyn Kernel>, ContractViolation> {
620        matter.build_under(self)
621    }
622}
623
624/// L2.f strict-mode hardening — when strict is on, escalate a
625/// const's silent fall-through to a hard error
626/// (`ContractViolation::StrictNonePropagation`,
627/// subcontext_construction.md §7). Per
628/// composition_substrate.md L2.f's strict-mode hardening
629/// clause: an intermediate-layer `const X := <expr>` whose
630/// RHS evaluates to `Value::None` at initialization normally
631/// falls through to the outer scope's `X` via the
632/// conditional-shadow semantics (none_semantics.md). Strict
633/// mode rejects this silent fall-through, forcing the author
634/// to either ensure the const yields a defined value or
635/// remove the binding and declare `extern X` explicitly if
636/// fall-through to outer was intended.
637///
638/// A const's own value is its expression's: `__init_<name>` for a
639/// const captured at initialization, the output itself for one
640/// folded at build.
641fn enforce_l2f_strict(
642    child: &mut dyn Kernel,
643    strict: bool,
644    label: &str,
645) -> Result<(), ContractViolation> {
646    if !strict {
647        return Ok(());
648    }
649    let consts: Vec<String> = child
650        .output_names()
651        .into_iter()
652        .filter(|n| child.output_modifier(n).is_const() && !n.starts_with("__init_"))
653        .collect();
654    let bindings: Vec<String> = consts
655        .into_iter()
656        .filter(|name| {
657            let own = child
658                .const_inits()
659                .iter()
660                .find(|c| &c.name == name && !c.register)
661                .map_or_else(|| name.clone(), |c| c.source.clone());
662            matches!(child.pull(&own), Value::None)
663        })
664        .collect();
665    if bindings.is_empty() {
666        return Ok(());
667    }
668    Err(ContractViolation::StrictNonePropagation {
669        bindings,
670        site: SourceContext::new(label.to_string()),
671    })
672}
673
674// Per the kernel-construction invariant, two paths exist:
675//
676//   1. Root kernel built from source via `compile_polydat` (and family).
677//   2. Subscope kernel bound under a parent kernel of any engine via
678//      `PolydatMatter::build_under`, `ScopeKernel::spawn`,
679//      `ScopeModule::instantiate_under`, or `bind_under`, each
680//      parent-supervised through the one binder.