Skip to main content

polydat_core/kernel/subcontext/
module.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! [`ScopeModule<M>`] — closed, immutable module-matter artifact.
5//!
6//! The artifact carries everything the parent needs to spawn — type
7//! contracts, the compiled program, registered pull consumers —
8//! but holds no live reference to the parent
9//! (subcontext_construction.md §3, SC2). The artifact can be moved,
10//! debug-printed, and cached for reuse.
11
12use std::collections::HashMap;
13use std::marker::PhantomData;
14use std::sync::{Arc, Mutex};
15
16use crate::dsl::ast::Statement;
17use crate::kernel::PolydatProgram;
18
19use super::error::SourceContext;
20use super::pull::RegisteredPullConsumer;
21use super::spec::{ExportSpec, ImportSpec};
22
23/// Body fragment — what the builder accepts via
24/// [`super::SubcontextBuilder::body`].
25///
26/// See subcontext_construction.md §2.1. `PolydatSource` is for user-facing
27/// `bindings:` / `result:` content (parsed at finalize);
28/// `Statements` is for synthesisers that already produce GK
29/// programmatically.
30#[derive(Debug, Clone)]
31pub enum BodyFragment {
32    /// User-facing Polydat source. Parsed via the existing
33    /// `lexer + parser` pipeline at finalize.
34    PolydatSource(String),
35    /// Pre-parsed statements — submitted directly without
36    /// round-tripping through Polydat source strings. Reuses
37    /// [`Statement`] from the existing AST, so synthesisers
38    /// don't carry a parallel enum.
39    Statements(Vec<Statement>),
40}
41
42/// Typed handle bundle (subcontext_construction.md §1).
43///
44/// The bundle is minimal by design: a handle for each declared
45/// import / export, identified by name. Slot resolution happens
46/// against the compiled program (`find_input` /
47/// `output_map_lookup`) rather than through cached indices here.
48///
49/// `M` is the module-identity phantom — [`super::Child<P>`] for
50/// modules built under parent `P`. Handles issued by one module
51/// can't be applied to a sibling at the type level.
52pub struct ScopeContract<M> {
53    imports: Vec<ImportHandle<M>>,
54    exports: Vec<ExportHandle<M>>,
55    _module: PhantomData<fn() -> M>,
56}
57
58impl<M> ScopeContract<M> {
59    pub(crate) fn from_specs(imports: &[ImportSpec], exports: &[ExportSpec]) -> Self {
60        Self {
61            imports: imports
62                .iter()
63                .map(|s| ImportHandle {
64                    name: s.name.clone(),
65                    _module: PhantomData,
66                })
67                .collect(),
68            exports: exports
69                .iter()
70                .map(|s| ExportHandle {
71                    name: s.name.clone(),
72                    _module: PhantomData,
73                })
74                .collect(),
75            _module: PhantomData,
76        }
77    }
78
79    /// The imports the module declares, in declaration order.
80    pub fn imports(&self) -> &[ImportHandle<M>] {
81        &self.imports
82    }
83
84    /// The exports the module declares, in declaration order.
85    pub fn exports(&self) -> &[ExportHandle<M>] {
86        &self.exports
87    }
88}
89
90impl<M> std::fmt::Debug for ScopeContract<M> {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.debug_struct("ScopeContract")
93            .field("imports", &self.imports)
94            .field("exports", &self.exports)
95            .finish()
96    }
97}
98
99/// A typed handle to a named import slot. Brand `M` ties it to
100/// the module that issued it.
101pub struct ImportHandle<M> {
102    name: String,
103    _module: PhantomData<fn() -> M>,
104}
105
106impl<M> ImportHandle<M> {
107    pub fn name(&self) -> &str {
108        &self.name
109    }
110}
111
112impl<M> std::fmt::Debug for ImportHandle<M> {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        f.debug_tuple("ImportHandle").field(&self.name).finish()
115    }
116}
117
118/// A typed handle to a named export slot. Brand `M` ties it to
119/// the module that issued it.
120pub struct ExportHandle<M> {
121    name: String,
122    _module: PhantomData<fn() -> M>,
123}
124
125impl<M> ExportHandle<M> {
126    pub fn name(&self) -> &str {
127        &self.name
128    }
129}
130
131impl<M> std::fmt::Debug for ExportHandle<M> {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        f.debug_tuple("ExportHandle").field(&self.name).finish()
134    }
135}
136
137/// A Rule 2 write-through binding produced by the builder when a
138/// child's `X := <expr>` collides with a parent `shared X`
139/// export. The child's compiled program produces a synthetic
140/// output named [`Self::source_output`] (typically
141/// `__write_<X>`); spawn's binder wires the parent's `SharedCell`
142/// into the child's `X` input slot. After
143/// per-cycle eval, [`super::ScopeKernel::commit_write_throughs`]
144/// pulls the synthetic output and stores its value through the
145/// child's input slot, which propagates to the cell.
146#[derive(Debug, Clone)]
147pub struct WriteThroughBinding {
148    /// The name as declared on the parent (and as the child sees
149    /// it via `extern`). The parent's shared cell is keyed on
150    /// this name.
151    pub export_name: String,
152    /// The synthetic output the rewrite emits in the child
153    /// program — its `pull` produces the value to write through.
154    pub source_output: String,
155}
156
157/// Closed, immutable module-matter artifact.
158///
159/// Produced by [`super::SubcontextBuilder::finalize`]; consumed
160/// by [`super::ScopeKernel::spawn`]. The artifact carries no
161/// live reference to its parent — it can be stored, inspected,
162/// or moved freely.
163pub struct ScopeModule<M> {
164    pub(crate) imports: Vec<ImportSpec>,
165    pub(crate) exports: Vec<ExportSpec>,
166    pub(crate) program: Arc<PolydatProgram>,
167    /// The body after the Rule 2 rewrite, and the settings it was
168    /// compiled under: what [`ScopeModule::program_on`] needs to build
169    /// the same body for another engine.
170    pub(crate) statements: Vec<Statement>,
171    pub(crate) options: crate::dsl::compile::CompileOptions,
172    /// The outputs the body re-exports for descendants without owning
173    /// them: marked on this module's program on every engine, so each
174    /// engine's program has the same canonical hash.
175    pub(crate) inherited_outputs: Vec<String>,
176    /// This module's program per engine, built on the first ask for
177    /// that engine and shared by every instance after it — the same
178    /// carrier a `for` body and a tile's projection body use
179    /// (`dsl::traversal::BodySource`).
180    ///
181    /// The interpreter's is the one `finalize` already built, so it is
182    /// in the table from the start. A module instantiated a thousand
183    /// times compiles once per engine and allocates a state per
184    /// instance.
185    pub(crate) programs: Mutex<HashMap<crate::Engine, Arc<dyn crate::kernel::KernelProgram>>>,
186    pub(crate) contract: ScopeContract<M>,
187    pub(crate) context: SourceContext,
188    pub(crate) consumers: Vec<RegisteredPullConsumer>,
189    /// Rule 2 write-through bindings — child exports rewritten
190    /// at finalize to feed parent `shared` cells. Empty for the
191    /// vast majority of modules; populated only when the parent
192    /// has a `shared` export with a name the child redefines.
193    pub(crate) write_throughs: Vec<WriteThroughBinding>,
194    /// Diagnostics emitted during finalize (warnings, etc.). Free-
195    /// form strings; downstream tooling can surface them.
196    pub(crate) diagnostics: Vec<String>,
197    pub(crate) _module: PhantomData<fn() -> M>,
198}
199
200impl<M> ScopeModule<M> {
201    /// This module's program on `engine`, compiled on the first ask
202    /// for that engine and shared by every instance after it.
203    ///
204    /// This is the kernel image. A module instantiated many times —
205    /// once per coordinate, per fiber, per scenario visit — compiles
206    /// once per engine here and pays only a state per instance, which
207    /// is what [`Self::instantiate_under`] does with it.
208    pub fn program_on(
209        &self,
210        engine: crate::Engine,
211    ) -> Result<Arc<dyn crate::kernel::KernelProgram>, crate::KernelError> {
212        let mut programs = self
213            .programs
214            .lock()
215            .unwrap_or_else(|poisoned| poisoned.into_inner());
216        if let Some(program) = programs.get(&engine) {
217            return Ok(program.clone());
218        }
219        let mut kernel = crate::dsl::compile::compile_template_with_engine(
220            &crate::dsl::ast::PolydatFile {
221                statements: self.statements.clone(),
222            },
223            &self.options,
224            engine,
225        )?;
226        if !self.inherited_outputs.is_empty() {
227            crate::kernel::KernelInternals::set_inherited_outputs(
228                kernel.as_mut(),
229                self.inherited_outputs.clone(),
230            );
231        }
232        let program = kernel.into_program();
233        programs.insert(engine, program.clone());
234        Ok(program)
235    }
236
237    /// One instance of this module under `parent`: a kernel on
238    /// `engine` over the program [`Self::program_on`] holds, with the
239    /// parent's cells attached, its values copied in, and
240    /// `iter_bindings` written before either.
241    ///
242    /// The image is shared; the instance is a state. Calling this a
243    /// second time with different coordinates or different input
244    /// values compiles nothing.
245    ///
246    /// `engine` is the caller's to name, and the parent's own is the
247    /// answer a caller usually wants: a child belongs to the kernel it
248    /// was bound under, the way a `for` body belongs to the kernel
249    /// that opened it.
250    pub fn instantiate_under(
251        &self,
252        parent: &dyn crate::kernel::Kernel,
253        engine: crate::Engine,
254        iter_bindings: &[(String, crate::ast::Value)],
255    ) -> Result<Box<dyn crate::kernel::Kernel>, crate::KernelError> {
256        let program = self.program_on(engine)?;
257        let mut child = crate::kernel::bind_under(parent, program, iter_bindings)?;
258        // The write-throughs the builder produced travel with every
259        // instance, on every engine, so `commit_write_throughs` knows
260        // them without the program having to.
261        if !self.write_throughs.is_empty() {
262            child.set_write_throughs(
263                self.write_throughs
264                    .iter()
265                    .map(|wt| (wt.export_name.clone(), wt.source_output.clone()))
266                    .collect(),
267            );
268        }
269        Ok(child)
270    }
271
272    /// The imports the module declares.
273    pub fn imports(&self) -> &[ImportSpec] {
274        &self.imports
275    }
276
277    /// The exports the module declares.
278    pub fn exports(&self) -> &[ExportSpec] {
279        &self.exports
280    }
281
282    /// The body's compiled program.
283    pub fn program(&self) -> &Arc<PolydatProgram> {
284        &self.program
285    }
286
287    /// The contract the module was built against.
288    pub fn contract(&self) -> &ScopeContract<M> {
289        &self.contract
290    }
291
292    /// Where the module comes from.
293    pub fn context(&self) -> &SourceContext {
294        &self.context
295    }
296
297    /// The pull consumers registered on the module's outputs.
298    pub fn consumers(&self) -> &[RegisteredPullConsumer] {
299        &self.consumers
300    }
301
302    /// Diagnostics the build recorded.
303    pub fn diagnostics(&self) -> &[String] {
304        &self.diagnostics
305    }
306
307    /// Rule 2 write-through bindings produced by the builder for
308    /// child exports that collide with a parent `shared` export.
309    pub fn write_throughs(&self) -> &[WriteThroughBinding] {
310        &self.write_throughs
311    }
312}
313
314impl<M> std::fmt::Debug for ScopeModule<M> {
315    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
316        f.debug_struct("ScopeModule")
317            .field("imports", &self.imports)
318            .field("exports", &self.exports)
319            .field("context", &self.context)
320            .field("consumer_count", &self.consumers.len())
321            .field("write_throughs", &self.write_throughs)
322            .field("diagnostics", &self.diagnostics)
323            .finish()
324    }
325}