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.