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.