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