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 for
8//! Phase 1 (legacy call sites still construct it directly), and
9//! becomes `pub(crate)` in Phase 4 once the migration lands.
10//!
11//! The kernel exposes:
12//! - [`Self::subcontext_builder`] — yields a typed
13//!   [`super::SubcontextBuilder`] from an `Arc<Self>`. The single
14//!   public entry point for child construction.
15//! - [`Self::spawn`] — the single chokepoint where every
16//!   cross-binding is resolved; takes a closed
17//!   [`super::ScopeModule`] artifact, applies SRD-67's
18//!   cross-binding rules, returns a typed child kernel and
19//!   records the spawn under `name` in this kernel's registry.
20//! - [`Self::release_child`] — drop a registry entry to allow
21//!   re-spawn under the same name (for per-iteration
22//!   re-traversal).
23
24use std::collections::HashMap;
25use std::marker::PhantomData;
26use std::sync::{Arc, Mutex};
27
28use crate::ast::{PortType, Value};
29use crate::kernel::{PolydatKernel, SharedCell};
30
31use super::builder::SubcontextBuilder;
32use super::error::{ContractViolation, SourceContext};
33use super::module::{ScopeModule, WriteThroughBinding};
34use super::name::ChildName;
35use super::pull::RegisteredPullConsumer;
36
37/// Phantom-marker brand for the workload-root scope kernel —
38/// the top of any spawn type chain. Tests / examples that need
39/// a "starting" identity use this.
40#[derive(Debug)]
41pub struct RootMarker;
42
43/// Phantom-marker brand for "child of `P`". `spawn` returns
44/// `ScopeKernel<Child<P>>`, distinct at the type level from a
45/// sibling's `Child<P>` *value* but type-compatible at the
46/// module-identity level (per SRD-67 §"Decision 6").
47#[derive(Debug)]
48pub struct Child<P>(PhantomData<fn() -> P>);
49
50/// Internal record of a spawned child — used for the
51/// duplicate-spawn diagnostic.
52#[derive(Debug)]
53struct ChildEntry {
54    site: SourceContext,
55}
56
57/// Typed wrapper around an `Arc<PolydatKernel>`.
58///
59/// Construction via this type goes through the SRD-67 protocol
60/// (`subcontext_builder` → `finalize` → `spawn`); direct
61/// construction from a `PolydatKernel` is `pub(crate)` for the
62/// Phase 1 internal bridge.
63pub struct ScopeKernel<M> {
64    name: ChildName,
65    inner: Arc<Mutex<PolydatKernel>>,
66    site: SourceContext,
67    children: Mutex<HashMap<ChildName, ChildEntry>>,
68    consumers: Mutex<Vec<RegisteredPullConsumer>>,
69    /// Rule 2 write-through bindings. Per-cycle eval of this
70    /// kernel must call [`Self::commit_write_throughs`] after
71    /// producing values to fan them through the parent's
72    /// `SharedCell`s.
73    write_throughs: Vec<WriteThroughBinding>,
74    _module: PhantomData<fn() -> M>,
75}
76
77impl<M> std::fmt::Debug for ScopeKernel<M> {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        f.debug_struct("ScopeKernel")
80            .field("name", &self.name)
81            .field("site", &self.site)
82            .finish()
83    }
84}
85
86/// One shared cell visible at a parent scope, reified for
87/// transitive cross-binding. Returned by
88/// [`ScopeKernel::shared_cells_in_scope`].
89///
90/// Carries the name a child must use to bind to the cell,
91/// the port type (so Rule 2 / `extern` synthesis at finalize
92/// can declare a typed input slot), and the cell handle (so
93/// spawn can attach it to the child's matching input).
94///
95/// "In scope" semantics: a cell visible at the parent is one
96/// the parent itself can read or write at this scope —
97/// covering both:
98///
99/// 1. Cells the parent declared via its own program
100///    (`shared X := <init>` produces a cell-bound input slot).
101/// 2. Cells inherited from the parent's own ancestors
102///    (attached during the parent's spawn). Without this
103///    case, a `shared` cell at the workload root would not
104///    propagate to grand-children whose immediate parent's
105///    body never references the name.
106///
107/// Both cases are answered by walking the parent's input
108/// slots and reading `crate::kernel::engines::Engines::shared_cell`.
109#[derive(Clone)]
110pub struct SharedCellInScope {
111    /// The binding's name.
112    pub name: String,
113    /// The cell's declared type.
114    pub port_type: PortType,
115    /// The cell.
116    pub cell: SharedCell,
117}
118
119impl std::fmt::Debug for SharedCellInScope {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        f.debug_struct("SharedCellInScope")
122            .field("name", &self.name)
123            .field("port_type", &self.port_type)
124            .finish()
125    }
126}
127
128impl<M> ScopeKernel<M> {
129    /// Enumerate every shared cell visible at this scope.
130    /// Delegates to [`PolydatKernel::shared_cells_in_scope`] —
131    /// the carrier lives at the kernel layer so it survives
132    /// any wrap/unwrap dance the activity layer does. The
133    /// `SharedCellInScope` re-export is kept for callers in
134    /// the SRD-67 builder; it's a thin alias over the kernel
135    /// layer's `SharedCellEntry`.
136    pub fn shared_cells_in_scope(&self) -> Vec<SharedCellInScope> {
137        let inner = self.lock_inner();
138        inner
139            .shared_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 callers within the crate (the
150    /// builder / spawn path; tests via `Self::wrap_for_test`)
151    /// produce a `ScopeKernel` directly. Public callers go
152    /// through the protocol.
153    pub(crate) fn new_internal(
154        name: ChildName,
155        kernel: PolydatKernel,
156        site: SourceContext,
157        consumers: Vec<RegisteredPullConsumer>,
158    ) -> Self {
159        Self::new_with_write_throughs(name, kernel, site, consumers, Vec::new())
160    }
161
162    pub(crate) fn new_with_write_throughs(
163        name: ChildName,
164        kernel: PolydatKernel,
165        site: SourceContext,
166        consumers: Vec<RegisteredPullConsumer>,
167        write_throughs: Vec<WriteThroughBinding>,
168    ) -> Self {
169        Self {
170            name,
171            inner: Arc::new(Mutex::new(kernel)),
172            site,
173            children: Mutex::new(HashMap::new()),
174            consumers: Mutex::new(consumers),
175            write_throughs,
176            _module: PhantomData,
177        }
178    }
179
180    /// The structured name this kernel was spawned under (for
181    /// child kernels) or its self-label (for root kernels).
182    pub fn name(&self) -> &ChildName {
183        &self.name
184    }
185
186    /// Diagnostic site for this kernel's construction.
187    pub fn site(&self) -> &SourceContext {
188        &self.site
189    }
190
191    /// Borrow the underlying `PolydatKernel` for read-only
192    /// operations. The lock is released when the returned guard
193    /// is dropped. Phase 1 exposes this so legacy call sites
194    /// (and tests) can still drive the kernel via the existing
195    /// API; Phase 4 narrows or removes it once the migration
196    /// completes.
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/// In Phase 4 once the migration completes, the workload-root
384/// path will produce a `ScopeKernel<RootMarker>` directly from
385/// the workload-load entry point.
386pub(crate) fn wrap_root_kernel(
387    kernel: PolydatKernel,
388    label: impl Into<String>,
389) -> Arc<ScopeKernel<RootMarker>> {
390    let label = label.into();
391    let name = ChildName::from_segments([label.clone()]);
392    let site = SourceContext::new(label);
393    Arc::new(ScopeKernel::new_internal(name, kernel, site, Vec::new()))
394}
395
396/// Typed Polydat matter accepted by both kernel-construction
397/// paths — root and subscope. Opaque externally: the only way
398/// to obtain a `PolydatMatter` value is via [`PolydatMatter::builder`].
399///
400/// Internally carries one of three input forms — fresh source,
401/// pre-parsed statements (the "module parser" output), or a
402/// pre-compiled program. The builder validates that exactly
403/// one form is provided.
404pub struct PolydatMatter<'a> {
405    pub(crate) inner: PolydatMatterInner<'a>,
406}
407
408pub(crate) enum PolydatMatterInner<'a> {
409    Source(SourceMatter),
410    Statements(StatementsMatter),
411    Program(ProgramMatter<'a>),
412}
413
414pub(crate) struct SourceMatter {
415    pub(crate) label: String,
416    pub(crate) body: String,
417    pub(crate) result_bindings: Option<String>,
418    pub(crate) inherited_outputs: Vec<String>,
419    pub(crate) options: super::builder::CompileOptions,
420}
421
422pub(crate) struct StatementsMatter {
423    pub(crate) label: String,
424    pub(crate) statements: Vec<crate::dsl::ast::Statement>,
425    pub(crate) result_bindings: Option<String>,
426    pub(crate) inherited_outputs: Vec<String>,
427    pub(crate) options: super::builder::CompileOptions,
428}
429
430pub(crate) struct ProgramMatter<'a> {
431    pub(crate) program: Arc<crate::kernel::PolydatProgram>,
432    pub(crate) iter_bindings: &'a [(String, Value)],
433}
434
435impl<'a> PolydatMatter<'a> {
436    /// Begin building Polydat matter. The builder is the only
437    /// constructor of `PolydatMatter`; the variants and their
438    /// fields are not exposed.
439    #[inline]
440    pub fn builder() -> PolydatMatterBuilder<'a> {
441        PolydatMatterBuilder::new()
442    }
443}
444
445/// Builder for [`PolydatMatter`]. Configure exactly one input form
446/// (source, pre-parsed statements, or program), plus optional
447/// metadata, then call [`Self::build`].
448#[derive(Default)]
449pub struct PolydatMatterBuilder<'a> {
450    label: Option<String>,
451    body: Option<String>,
452    statements: Option<Vec<crate::dsl::ast::Statement>>,
453    program: Option<Arc<crate::kernel::PolydatProgram>>,
454    iter_bindings: &'a [(String, Value)],
455    result_bindings: Option<String>,
456    inherited_outputs: Vec<String>,
457    options: super::builder::CompileOptions,
458}
459
460impl<'a> PolydatMatterBuilder<'a> {
461    fn new() -> Self {
462        Self::default()
463    }
464
465    /// Diagnostic label for this matter. Surfaces in compile
466    /// errors and the `__transient` parent name during the
467    /// SubcontextBuilder dance.
468    pub fn label(mut self, label: impl Into<String>) -> Self {
469        self.label = Some(label.into());
470        self
471    }
472
473    /// Provide Polydat source as a string. Mutually exclusive with
474    /// [`Self::statements`] and [`Self::program`].
475    pub fn source(mut self, body: impl Into<String>) -> Self {
476        self.body = Some(body.into());
477        self
478    }
479
480    /// Provide Polydat source as pre-parsed AST statements. Mutually
481    /// exclusive with [`Self::source`] and [`Self::program`].
482    /// Use when the caller has already run the module parser
483    /// (e.g. when synthesising scope source from a structured
484    /// model and wanting to skip a string round-trip).
485    pub fn statements(mut self, stmts: Vec<crate::dsl::ast::Statement>) -> Self {
486        self.statements = Some(stmts);
487        self
488    }
489
490    /// Provide a pre-compiled program. Mutually exclusive with
491    /// [`Self::source`] and [`Self::statements`]. Used for per-
492    /// fiber state forks, comprehension iteration, and other
493    /// call sites that hold a compiled program directly.
494    pub fn program(mut self, program: Arc<crate::kernel::PolydatProgram>) -> Self {
495        self.program = Some(program);
496        self
497    }
498
499    /// Iter-var bindings applied before the parent binds the
500    /// child. Only meaningful for the program form.
501    pub fn iter_bindings(mut self, bindings: &'a [(String, Value)]) -> Self {
502        self.iter_bindings = bindings;
503        self
504    }
505
506    /// SRD-66 result-binding source. Folded through
507    /// [`super::SubcontextBuilder::add_result_bindings`] at
508    /// finalize. Only meaningful for source / statements forms.
509    pub fn result_bindings(mut self, src: impl Into<String>) -> Self {
510        self.result_bindings = Some(src.into());
511        self
512    }
513
514    /// Names to pass through `mark_inherited_outputs` so the
515    /// scope tree can distinguish own exports from cascade-
516    /// inherited names. Source / statements forms only.
517    pub fn inherited_outputs(mut self, names: Vec<String>) -> Self {
518        self.inherited_outputs = names;
519        self
520    }
521
522    /// Compile-time knobs (lib paths, strict mode, required
523    /// outputs, cursor limit). Source / statements forms only.
524    pub fn options(mut self, options: super::builder::CompileOptions) -> Self {
525        self.options = options;
526        self
527    }
528
529    /// Validate and produce typed matter. Errors when zero or
530    /// more than one input form is configured.
531    pub fn build(self) -> Result<PolydatMatter<'a>, String> {
532        let forms = [
533            self.body.is_some(),
534            self.statements.is_some(),
535            self.program.is_some(),
536        ];
537        let count = forms.iter().filter(|x| **x).count();
538        if count == 0 {
539            return Err(
540                "PolydatMatter::builder: no input form set (use .source / .statements / .program)"
541                    .into(),
542            );
543        }
544        if count > 1 {
545            return Err(
546                "PolydatMatter::builder: multiple input forms set; choose exactly one".into(),
547            );
548        }
549        let label = self.label.unwrap_or_else(|| "(matter)".to_string());
550        let inner = if let Some(body) = self.body {
551            PolydatMatterInner::Source(SourceMatter {
552                label,
553                body,
554                result_bindings: self.result_bindings,
555                inherited_outputs: self.inherited_outputs,
556                options: self.options,
557            })
558        } else if let Some(stmts) = self.statements {
559            PolydatMatterInner::Statements(StatementsMatter {
560                label,
561                statements: stmts,
562                result_bindings: self.result_bindings,
563                inherited_outputs: self.inherited_outputs,
564                options: self.options,
565            })
566        } else {
567            // program
568            PolydatMatterInner::Program(ProgramMatter {
569                program: self.program.expect("program form set per count above"),
570                iter_bindings: self.iter_bindings,
571            })
572        };
573        Ok(PolydatMatter { inner })
574    }
575}
576
577impl PolydatKernel {
578    /// THE subscope-construction path. Per the kernel-construction
579    /// invariant, this is the ONE method through which a parent
580    /// kernel produces a child. `compile_polydat` produces root
581    /// kernels; everything else is a subscope and routes here.
582    ///
583    /// Cell propagation, scope-coordinate plumbing, and Rule 2
584    /// write-throughs flow from `self` (the parent) into the
585    /// returned child. Returns the child kernel plus any
586    /// write-through bindings finalize produced (empty for the
587    /// program-matter form, populated for the source-matter
588    /// form when a result-LHS collides with a parent `shared`
589    /// cell).
590    pub fn build_subscope(
591        &self,
592        matter: PolydatMatter<'_>,
593    ) -> Result<PolydatKernel, ContractViolation> {
594        use super::module::BodyFragment;
595        match matter.inner {
596            PolydatMatterInner::Program(p) => {
597                Ok(self.materialize_subscope(p.program, p.iter_bindings))
598            }
599            PolydatMatterInner::Source(s) => {
600                let strict = s.options.strict;
601                let label = s.label.clone();
602                let transient = self.transient_typed_parent(&s.label);
603                let mut builder = transient.clone().subcontext_builder();
604                builder
605                    .context(SourceContext::new(s.label.clone()))
606                    .mark_inherited_outputs(s.inherited_outputs)
607                    .with_compile_options(s.options)
608                    .body(BodyFragment::PolydatSource(s.body));
609                if let Some(src) = s.result_bindings {
610                    builder.add_result_bindings(&src)?;
611                }
612                let module = builder.finalize()?;
613                let child = self.materialize_subscope(module.program.clone(), &[]);
614                drop(transient);
615                enforce_l2f_strict(&child, strict, &label)?;
616                Ok(child)
617            }
618            PolydatMatterInner::Statements(s) => {
619                let strict = s.options.strict;
620                let label = s.label.clone();
621                let transient = self.transient_typed_parent(&s.label);
622                let mut builder = transient.clone().subcontext_builder();
623                builder
624                    .context(SourceContext::new(s.label.clone()))
625                    .mark_inherited_outputs(s.inherited_outputs)
626                    .with_compile_options(s.options)
627                    .body(BodyFragment::Statements(s.statements));
628                if let Some(src) = s.result_bindings {
629                    builder.add_result_bindings(&src)?;
630                }
631                let module = builder.finalize()?;
632                let child = self.materialize_subscope(module.program.clone(), &[]);
633                drop(transient);
634                enforce_l2f_strict(&child, strict, &label)?;
635                Ok(child)
636            }
637        }
638    }
639
640    /// Snapshot a typed `ScopeKernel<RootMarker>` over `self`'s
641    /// live cell view, used as the transient parent the
642    /// SubcontextBuilder validates against.
643    fn transient_typed_parent(&self, label: &str) -> Arc<ScopeKernel<RootMarker>> {
644        wrap_root_kernel(self.snapshot_with_cells(), format!("{label}__transient"))
645    }
646}
647
648/// L2.f strict-mode hardening — when strict is on, escalate
649/// silent Plan B fall-through to a hard error. Per
650/// composition_substrate.md L2.f's strict-mode hardening
651/// clause: an intermediate-layer `const X := <expr>` whose
652/// RHS evaluates to `Value::None` at scope-init normally
653/// falls through to the outer scope's `X` via the
654/// conditional-shadow semantics (none_semantics.md). Strict
655/// mode rejects this silent fall-through, forcing the author
656/// to either ensure the const yields a defined value or
657/// remove the binding and declare `extern X` explicitly if
658/// fall-through to outer was intended.
659fn enforce_l2f_strict(
660    child: &PolydatKernel,
661    strict: bool,
662    label: &str,
663) -> Result<(), ContractViolation> {
664    if !strict {
665        return Ok(());
666    }
667    let bindings = child.find_l2f_violations();
668    if bindings.is_empty() {
669        return Ok(());
670    }
671    Err(ContractViolation::StrictNonePropagation {
672        bindings,
673        site: SourceContext::new(label.to_string()),
674    })
675}
676
677// `bind_program_under_parent` and the `build_kernel_under_parent_*`
678// family of free-function bridges are removed. Per the kernel-
679// construction invariant, only two paths exist:
680//
681//   1. Root kernel built from source via `compile_polydat` (and family).
682//   2. Subscope kernel materialized by a parent kernel via
683//      [`PolydatKernel::materialize_subscope`] or
684//      [`PolydatKernel::build_subscope_from_source`] — all methods on
685//      `PolydatKernel` itself, parent-supervised, typed.
686//
687// External callers go through these PolydatKernel-controlled paths
688// directly; no free-function bridges remain.
689
690// `instance_program` is removed. The two sanctioned construction
691// paths are:
692//
693//   1. Root kernel built from source via `compile_polydat` family.
694//   2. Subscope kernel materialized by an existing parent
695//      kernel via `PolydatKernel::materialize_subscope` or
696//      `PolydatKernel::build_subscope_from_source`.
697//
698// Tests that need a kernel from pre-compiled program matter use
699// `PolydatAssembler::compile()` (which returns a root kernel) directly.