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