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