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}