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.