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}