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_template_with_engine(
217            &crate::dsl::ast::PolydatFile {
218                statements: self.statements.clone(),
219            },
220            &self.options,
221            engine,
222        )?;
223        let program = kernel.into_program();
224        programs.insert(engine, program.clone());
225        Ok(program)
226    }
227
228    /// One instance of this module under `parent`: a kernel on
229    /// `engine` over the program [`Self::program_on`] holds, with the
230    /// parent's cells attached, its values copied in, and
231    /// `iter_bindings` written before either.
232    ///
233    /// The image is shared; the instance is a state. Calling this a
234    /// second time with different coordinates or different input
235    /// values compiles nothing.
236    ///
237    /// `engine` is the caller's to name, and the parent's own is the
238    /// answer a caller usually wants: a child belongs to the kernel it
239    /// was bound under, the way a `for` body belongs to the kernel
240    /// that opened it.
241    pub fn instantiate_under(
242        &self,
243        parent: &dyn crate::kernel::Kernel,
244        engine: crate::Engine,
245        iter_bindings: &[(String, crate::ast::Value)],
246    ) -> Result<Box<dyn crate::kernel::Kernel>, crate::KernelError> {
247        let program = self.program_on(engine)?;
248        let mut child = crate::kernel::bind_under(parent, program, iter_bindings)?;
249        // The write-throughs the builder produced travel with every
250        // instance, on every engine, so `commit_write_throughs` knows
251        // them without the program having to.
252        if !self.write_throughs.is_empty() {
253            child.set_write_throughs(
254                self.write_throughs
255                    .iter()
256                    .map(|wt| (wt.export_name.clone(), wt.source_output.clone()))
257                    .collect(),
258            );
259        }
260        Ok(child)
261    }
262
263    /// The imports the module declares.
264    pub fn imports(&self) -> &[ImportSpec] {
265        &self.imports
266    }
267
268    /// The exports the module declares.
269    pub fn exports(&self) -> &[ExportSpec] {
270        &self.exports
271    }
272
273    /// The body's compiled program.
274    pub fn program(&self) -> &Arc<PolydatProgram> {
275        &self.program
276    }
277
278    /// The contract the module was built against.
279    pub fn contract(&self) -> &ScopeContract<M> {
280        &self.contract
281    }
282
283    /// Where the module comes from.
284    pub fn context(&self) -> &SourceContext {
285        &self.context
286    }
287
288    /// The pull consumers registered on the module's outputs.
289    pub fn consumers(&self) -> &[RegisteredPullConsumer] {
290        &self.consumers
291    }
292
293    /// Diagnostics the build recorded.
294    pub fn diagnostics(&self) -> &[String] {
295        &self.diagnostics
296    }
297
298    /// Rule 2 write-through bindings produced by the builder for
299    /// child exports that collide with a parent `shared` export.
300    pub fn write_throughs(&self) -> &[WriteThroughBinding] {
301        &self.write_throughs
302    }
303}
304
305impl<M> std::fmt::Debug for ScopeModule<M> {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        f.debug_struct("ScopeModule")
308            .field("imports", &self.imports)
309            .field("exports", &self.exports)
310            .field("context", &self.context)
311            .field("consumer_count", &self.consumers.len())
312            .field("write_throughs", &self.write_throughs)
313            .field("diagnostics", &self.diagnostics)
314            .finish()
315    }
316}