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